mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 08:17:22 +00:00
fix: route OpenRouter profiles through v1 API
This commit is contained in:
@@ -9,6 +9,11 @@ function ensureSupportedProtocol(parsed: URL): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOpenRouterHost(hostname: string): boolean {
|
||||||
|
const normalized = hostname.toLowerCase();
|
||||||
|
return normalized === 'openrouter.ai' || normalized.endsWith('.openrouter.ai');
|
||||||
|
}
|
||||||
|
|
||||||
function buildResolvedUrl(baseUrl: string, suffix: string): string {
|
function buildResolvedUrl(baseUrl: string, suffix: string): string {
|
||||||
const parsed = new URL(baseUrl);
|
const parsed = new URL(baseUrl);
|
||||||
ensureSupportedProtocol(parsed);
|
ensureSupportedProtocol(parsed);
|
||||||
@@ -18,6 +23,11 @@ function buildResolvedUrl(baseUrl: string, suffix: string): string {
|
|||||||
return parsed.toString();
|
return parsed.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isOpenRouterHost(parsed.hostname) && pathname.endsWith('/api')) {
|
||||||
|
parsed.pathname = `${pathname}/v1${suffix}`;
|
||||||
|
return parsed.toString();
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname.endsWith('/v1') || pathname.endsWith('/api')) {
|
if (pathname.endsWith('/v1') || pathname.endsWith('/api')) {
|
||||||
parsed.pathname = `${pathname}${suffix.startsWith('/') ? suffix : `/${suffix}`}`;
|
parsed.pathname = `${pathname}${suffix.startsWith('/') ? suffix : `/${suffix}`}`;
|
||||||
return parsed.toString();
|
return parsed.toString();
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export interface ProviderPresetDefinition {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy aliases mapped to canonical preset IDs.
|
* Legacy aliases mapped to canonical preset IDs.
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ describe('profile-writer Anthropic direct', () => {
|
|||||||
it('preserves OpenRouter ANTHROPIC_API_KEY blank behavior', () => {
|
it('preserves OpenRouter ANTHROPIC_API_KEY blank behavior', () => {
|
||||||
const result = createApiProfile(
|
const result = createApiProfile(
|
||||||
'openrouter-test',
|
'openrouter-test',
|
||||||
'https://openrouter.ai/api',
|
'https://openrouter.ai/api/v1',
|
||||||
'sk-or-testkey',
|
'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' }
|
{ 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'));
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||||
|
|
||||||
// OpenRouter: proxy mode with ANTHROPIC_API_KEY explicitly blank
|
// OpenRouter: proxy mode with ANTHROPIC_API_KEY explicitly blank
|
||||||
expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://openrouter.ai/api');
|
expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://openrouter.ai/api/v1');
|
||||||
expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBe('sk-or-testkey');
|
expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBe('sk-or-testkey');
|
||||||
expect(settings.env.ANTHROPIC_API_KEY).toBe('');
|
expect(settings.env.ANTHROPIC_API_KEY).toBe('');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,6 +92,11 @@ describe('provider-presets', () => {
|
|||||||
expect(isValidPresetId('hf')).toBe(true);
|
expect(isValidPresetId('hf')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses OpenRouter v1 as the OpenAI-compatible API root', () => {
|
||||||
|
const preset = getPresetById('openrouter');
|
||||||
|
expect(preset?.baseUrl).toBe('https://openrouter.ai/api/v1');
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps Anthropic direct last in the recommended preset order and reuses the Claude logo', () => {
|
it('keeps Anthropic direct last in the recommended preset order and reuses the Claude logo', () => {
|
||||||
const recommendedPresetIds = PROVIDER_PRESETS.filter(
|
const recommendedPresetIds = PROVIDER_PRESETS.filter(
|
||||||
(preset) => preset.category === 'recommended'
|
(preset) => preset.category === 'recommended'
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import {
|
||||||
|
resolveOpenAIChatCompletionsUrl,
|
||||||
|
resolveOpenAIModelsUrl,
|
||||||
|
} from '../../../src/proxy/upstream-url';
|
||||||
|
|
||||||
|
describe('OpenAI-compatible upstream URL resolution', () => {
|
||||||
|
it('routes current OpenRouter API roots through /api/v1', () => {
|
||||||
|
expect(resolveOpenAIChatCompletionsUrl('https://openrouter.ai/api/v1')).toBe(
|
||||||
|
'https://openrouter.ai/api/v1/chat/completions'
|
||||||
|
);
|
||||||
|
expect(resolveOpenAIModelsUrl('https://openrouter.ai/api/v1')).toBe(
|
||||||
|
'https://openrouter.ai/api/v1/models'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('repairs legacy OpenRouter /api roots before appending OpenAI endpoints', () => {
|
||||||
|
expect(resolveOpenAIChatCompletionsUrl('https://openrouter.ai/api')).toBe(
|
||||||
|
'https://openrouter.ai/api/v1/chat/completions'
|
||||||
|
);
|
||||||
|
expect(resolveOpenAIModelsUrl('https://openrouter.ai/api')).toBe(
|
||||||
|
'https://openrouter.ai/api/v1/models'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not rewrite non-OpenRouter /api roots', () => {
|
||||||
|
expect(resolveOpenAIChatCompletionsUrl('https://example.test/api')).toBe(
|
||||||
|
'https://example.test/api/chat/completions'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user