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..0416c321 100644 --- a/tests/unit/api/profile-writer-anthropic.test.ts +++ b/tests/unit/api/profile-writer-anthropic.test.ts @@ -134,7 +134,7 @@ describe('profile-writer Anthropic direct', () => { it('preserves OpenRouter ANTHROPIC_API_KEY blank behavior', () => { const result = createApiProfile( 'openrouter-test', - 'https://openrouter.ai/api/v1', + 'https://openrouter.ai/api', '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' } ); @@ -145,7 +145,7 @@ describe('profile-writer Anthropic direct', () => { 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'); + expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://openrouter.ai/api'); 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..d90ff2eb 100644 --- a/tests/unit/api/provider-presets.test.ts +++ b/tests/unit/api/provider-presets.test.ts @@ -1,6 +1,8 @@ -import { existsSync } from 'fs'; -import { resolve } from 'path'; +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join, resolve } from 'path'; import { describe, expect, it } from 'bun:test'; +import { createApiProfile } from '../../../src/api/services/profile-writer'; import { PROVIDER_PRESETS, getPresetById, @@ -113,9 +115,44 @@ 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('generates an OpenRouter profile whose ANTHROPIC_BASE_URL does not duplicate /v1 (#1728)', () => { + const preset = getPresetById('openrouter'); + expect(preset).toBeDefined(); + const tempHome = mkdtempSync(join(tmpdir(), 'ccs-openrouter-tdd-')); + const prevCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tempHome; + try { + const result = createApiProfile( + 'openrouter-tdd', + preset!.baseUrl, + 'sk-or-test-key', + { + default: preset!.defaultModel, + opus: preset!.defaultModel, + sonnet: preset!.defaultModel, + haiku: preset!.defaultModel, + } + ); + expect(result.success).toBe(true); + const settings = JSON.parse( + readFileSync(join(tempHome, '.ccs', 'openrouter-tdd.settings.json'), 'utf8') + ); + 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' + ); + } finally { + process.env.CCS_HOME = prevCcsHome; + rmSync(tempHome, { recursive: true, force: true }); + } }); it('keeps Anthropic direct last in the recommended preset order and reuses the Claude logo', () => {