diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index ab2db76d..64728ebc 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -53,7 +53,7 @@ export interface ProviderPresetDefinition { icon?: string; } -export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1'; +export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api'; export const ORCAROUTER_BASE_URL = 'https://api.orcarouter.ai/v1'; /** diff --git a/tests/unit/api/profile-writer-anthropic.test.ts b/tests/unit/api/profile-writer-anthropic.test.ts index 5a497d98..f25c016b 100644 --- a/tests/unit/api/profile-writer-anthropic.test.ts +++ b/tests/unit/api/profile-writer-anthropic.test.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { createApiProfile } from '../../../src/api/services/profile-writer'; +import { getPresetById } from '../../../src/api/services/provider-presets'; describe('profile-writer Anthropic direct', () => { let tempHome = ''; @@ -131,10 +132,14 @@ describe('profile-writer Anthropic direct', () => { expect(config.profile_targets?.['hf-target-claude']).toBeUndefined(); }); - it('preserves OpenRouter ANTHROPIC_API_KEY blank behavior', () => { + it('preserves OpenRouter ANTHROPIC_API_KEY blank behavior and resolves preset baseUrl without duplicating /v1 (#1728)', () => { + const preset = getPresetById('openrouter'); + expect(preset).toBeDefined(); + expect(preset?.baseUrl).toBe('https://openrouter.ai/api'); + const result = createApiProfile( 'openrouter-test', - 'https://openrouter.ai/api/v1', + preset!.baseUrl, 'sk-or-testkey', { default: 'anthropic/claude-opus-4.5', opus: 'anthropic/claude-opus-4.5', sonnet: 'anthropic/claude-opus-4.5', haiku: 'anthropic/claude-opus-4.5' } ); @@ -144,8 +149,9 @@ describe('profile-writer Anthropic direct', () => { const settingsPath = path.join(tempHome, '.ccs', 'openrouter-test.settings.json'); const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - // OpenRouter: proxy mode with ANTHROPIC_API_KEY explicitly blank - expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://openrouter.ai/api/v1'); + // OpenRouter: proxy mode with ANTHROPIC_API_KEY explicitly blank and baseUrl without /v1 + expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://openrouter.ai/api'); + expect(`${settings.env.ANTHROPIC_BASE_URL}/v1/messages`).toBe('https://openrouter.ai/api/v1/messages'); expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBe('sk-or-testkey'); expect(settings.env.ANTHROPIC_API_KEY).toBe(''); }); diff --git a/tests/unit/api/provider-presets.test.ts b/tests/unit/api/provider-presets.test.ts index 69386dbe..69bb4501 100644 --- a/tests/unit/api/provider-presets.test.ts +++ b/tests/unit/api/provider-presets.test.ts @@ -113,9 +113,12 @@ describe('provider-presets', () => { expect(isValidPresetId('te')).toBe(true); }); - it('uses OpenRouter v1 as the OpenAI-compatible API root', () => { + it('uses OpenRouter /api (without /v1) as the Anthropic-compatible API root to avoid double /v1 404 in Claude Code (#1728)', () => { const preset = getPresetById('openrouter'); - expect(preset?.baseUrl).toBe('https://openrouter.ai/api/v1'); + expect(preset?.baseUrl).toBe('https://openrouter.ai/api'); + const finalEndpoint = `${preset?.baseUrl}/v1/messages`; + expect(finalEndpoint).toBe('https://openrouter.ai/api/v1/messages'); + expect(finalEndpoint).not.toContain('/v1/v1/'); }); it('keeps Anthropic direct last in the recommended preset order and reuses the Claude logo', () => {