fix: route OpenRouter profiles through v1 API

This commit is contained in:
Tam Nhu Tran
2026-05-05 11:44:58 -04:00
parent fcfd2d279e
commit dc8bbd85e7
5 changed files with 49 additions and 3 deletions
+10
View File
@@ -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 {
const parsed = new URL(baseUrl);
ensureSupportedProtocol(parsed);
@@ -18,6 +23,11 @@ function buildResolvedUrl(baseUrl: string, suffix: string): string {
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')) {
parsed.pathname = `${pathname}${suffix.startsWith('/') ? suffix : `/${suffix}`}`;
return parsed.toString();
+1 -1
View File
@@ -49,7 +49,7 @@ export interface ProviderPresetDefinition {
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.
@@ -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',
'https://openrouter.ai/api/v1',
'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');
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_API_KEY).toBe('');
});
+5
View File
@@ -92,6 +92,11 @@ describe('provider-presets', () => {
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', () => {
const recommendedPresetIds = PROVIDER_PRESETS.filter(
(preset) => preset.category === 'recommended'
+31
View File
@@ -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'
);
});
});