-
Notifications
You must be signed in to change notification settings - Fork 9.1k
feat(gateway): add API Route hybrid gateway #2225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9b05cb0
5530cd5
31293de
b75042c
26bde3d
290a851
20cdb48
dd10e70
cbe3ec6
fbcf8e8
e54e23f
b892bf7
77869f9
0391c56
6edae47
1bb49e5
9f2a566
a80a525
7556586
093adb6
90e0d5d
226808d
409ccf7
e610e8e
4e00875
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| import { expect, test } from 'bun:test' | ||
|
|
||
| import { routeForPreset } from '../compatibility.js' | ||
| import { getProviderPresetUiMetadata } from '../providerUiMetadata.js' | ||
| import { | ||
| resolveActiveRouteIdFromEnv, | ||
| resolveRouteCredentialValue, | ||
| resolveRouteIdFromBaseUrl, | ||
| } from '../routeMetadata.js' | ||
| import gateway, { mapApiRouteModel } from './api-route.js' | ||
|
|
||
| test('API Route uses a dedicated hybrid OpenAI-compatible gateway contract', () => { | ||
| expect(gateway.defaultBaseUrl).toBe('https://global.api-route.com/v1') | ||
| expect(gateway.defaultModel).toBe('claude-sonnet-4-6') | ||
| expect(gateway.setup.credentialEnvVars).toEqual(['API_ROUTE_API_KEY']) | ||
| expect(gateway.setup.dedicatedCredentialsOnly).toBe(false) | ||
| expect(gateway.preset?.apiKeyEnvVars).toEqual(['API_ROUTE_API_KEY']) | ||
| expect(gateway.validation).toMatchObject({ | ||
| kind: 'credential-env', | ||
| routing: { matchDefaultBaseUrl: true }, | ||
| credentialEnvVars: ['API_ROUTE_API_KEY', 'OPENAI_API_KEYS', 'OPENAI_API_KEY'], | ||
| }) | ||
| expect(gateway.transportConfig).toEqual({ | ||
| kind: 'openai-compatible', | ||
| openaiShim: { | ||
| supportsApiFormatSelection: false, | ||
| supportsAuthHeaders: false, | ||
| maxTokensField: 'max_tokens', | ||
| }, | ||
| }) | ||
| expect(gateway.catalog?.source).toBe('hybrid') | ||
| expect(gateway.catalog?.discovery?.requiresAuth).toBe(true) | ||
| expect(gateway.catalog?.models?.map(model => model.apiName)).toEqual([ | ||
| 'claude-sonnet-4-6', | ||
| ]) | ||
| }) | ||
|
|
||
| test('API Route preset uses the existing generic profile path', () => { | ||
| expect(routeForPreset('api-route')).toEqual({ | ||
| vendorId: 'openai', | ||
| gatewayId: 'api-route', | ||
| routeId: 'api-route', | ||
| }) | ||
| expect( | ||
| getProviderPresetUiMetadata('api-route', { | ||
| API_ROUTE_API_KEY: 'test-api-route-key', | ||
| }), | ||
| ).toMatchObject({ | ||
| apiKey: 'test-api-route-key', | ||
| baseUrl: 'https://global.api-route.com/v1', | ||
| model: 'claude-sonnet-4-6', | ||
| provider: 'api-route', | ||
| routeId: 'api-route', | ||
| }) | ||
| expect( | ||
| getProviderPresetUiMetadata('api-route', { | ||
| API_ROUTE_API_KEY: 'test-api-route-key', | ||
| API_ROUTE_MODEL: 'custom-api-route-model', | ||
| OPENAI_MODEL: 'custom-openai-model', | ||
| }), | ||
| ).toMatchObject({ | ||
| model: 'custom-api-route-model', | ||
| }) | ||
| expect( | ||
| getProviderPresetUiMetadata('api-route', { | ||
| API_ROUTE_API_KEY: 'test-api-route-key', | ||
| OPENAI_MODEL: 'custom-openai-model', | ||
| }), | ||
| ).toMatchObject({ | ||
| model: 'custom-openai-model', | ||
| }) | ||
| }) | ||
|
|
||
| test('API Route profile credentials use the generic fallback only on the canonical inference URL', () => { | ||
| expect( | ||
| resolveRouteCredentialValue({ | ||
| routeId: 'api-route', | ||
| baseUrl: 'https://global.api-route.com/v1', | ||
| processEnv: { OPENAI_API_KEY: 'saved-profile-key' }, | ||
| }), | ||
| ).toBe('saved-profile-key') | ||
|
|
||
| expect( | ||
| resolveRouteCredentialValue({ | ||
| routeId: 'api-route', | ||
| baseUrl: 'https://proxy.example.com/v1', | ||
| processEnv: { OPENAI_API_KEY: 'saved-profile-key' }, | ||
| }), | ||
| ).toBeUndefined() | ||
|
|
||
| const env = { API_ROUTE_API_KEY: 'secret-key' } | ||
|
|
||
| expect( | ||
| resolveRouteCredentialValue({ | ||
| routeId: 'api-route', | ||
| baseUrl: 'https://global.api-route.com/v1', | ||
| processEnv: env, | ||
| }), | ||
| ).toBe('secret-key') | ||
|
|
||
| expect( | ||
| resolveRouteCredentialValue({ | ||
| routeId: 'api-route', | ||
| baseUrl: 'http://global.api-route.com/v1', | ||
| processEnv: env, | ||
| }), | ||
| ).toBeUndefined() | ||
|
|
||
| expect( | ||
| resolveRouteCredentialValue({ | ||
| routeId: 'api-route', | ||
| baseUrl: 'https://global.api-route.com/v1?tenant=proxy', | ||
| processEnv: env, | ||
| }), | ||
| ).toBeUndefined() | ||
|
|
||
| expect( | ||
| resolveRouteCredentialValue({ | ||
| routeId: 'api-route', | ||
| baseUrl: 'https://proxy.example.com/v1', | ||
| processEnv: env, | ||
| }), | ||
| ).toBeUndefined() | ||
| }) | ||
|
|
||
| test('API Route discovery maps chat models and drops non-chat IDs', () => { | ||
| expect( | ||
| mapApiRouteModel({ | ||
| id: 'claude-sonnet-4-6', | ||
| name: 'Claude Sonnet 4.6', | ||
| context_length: 200000, | ||
| }), | ||
| ).toEqual({ | ||
| id: 'claude-sonnet-4-6', | ||
| apiName: 'claude-sonnet-4-6', | ||
| label: 'Claude Sonnet 4.6', | ||
| contextWindow: 200000, | ||
| }) | ||
|
|
||
| expect( | ||
| mapApiRouteModel({ | ||
| id: 'gpt-4o-mini', | ||
| owned_by: 'openai', | ||
| }), | ||
| ).toEqual({ | ||
| id: 'gpt-4o-mini', | ||
| apiName: 'gpt-4o-mini', | ||
| label: 'gpt-4o-mini (openai)', | ||
| }) | ||
|
|
||
| expect(mapApiRouteModel({ id: 'text-embedding-3-small' })).toBeNull() | ||
| expect(mapApiRouteModel({ id: 'whisper-1' })).toBeNull() | ||
| expect(mapApiRouteModel({ id: 'dall-e-3' })).toBeNull() | ||
| expect(mapApiRouteModel({ id: 'gpt-image-1' })).toBeNull() | ||
| expect(mapApiRouteModel({ id: 'sora-turbo' })).toBeNull() | ||
| expect(mapApiRouteModel({ id: 'veo-2' })).toBeNull() | ||
| expect(mapApiRouteModel(null)).toBeNull() | ||
| expect(mapApiRouteModel({})).toBeNull() | ||
| }) | ||
|
|
||
| test('API Route route resolution detects endpoints and environment intent', () => { | ||
| expect(resolveRouteIdFromBaseUrl('https://global.api-route.com/v1')).toBe( | ||
| 'api-route', | ||
| ) | ||
| expect( | ||
| resolveRouteIdFromBaseUrl('https://global.api-route.com/v1/chat/completions'), | ||
| ).toBe('api-route') | ||
|
|
||
| expect( | ||
| resolveActiveRouteIdFromEnv({ | ||
| API_ROUTE_API_KEY: 'test-key', | ||
| }), | ||
| ).toBe('api-route') | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import { defineCatalog, defineGateway } from '../define.js' | ||
|
|
||
| const NON_CHAT_MODEL_PATTERN = | ||
| /(embedding|embed|dall-e|whisper|tts|rerank|moderation|omni-moderation|audio|^gpt-image-|^sora-|^veo-)/i | ||
|
|
||
|
|
||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||
| return typeof value === 'object' && value !== null | ||
| } | ||
|
|
||
| function getTrimmedString( | ||
| record: Record<string, unknown>, | ||
| key: string, | ||
| ): string | undefined { | ||
| const value = record[key] | ||
| return typeof value === 'string' ? value.trim() : undefined | ||
| } | ||
|
|
||
| function getPositiveInteger(value: unknown): number | undefined { | ||
| if ( | ||
| typeof value === 'number' && | ||
| Number.isFinite(value) && | ||
| Number.isInteger(value) && | ||
| value > 0 | ||
| ) { | ||
| return value | ||
| } | ||
| return undefined | ||
| } | ||
|
|
||
| export function mapApiRouteModel(raw: unknown) { | ||
| if (!isRecord(raw)) { | ||
| return null | ||
| } | ||
|
|
||
| const id = getTrimmedString(raw, 'id') | ||
| if (!id || NON_CHAT_MODEL_PATTERN.test(id)) { | ||
| return null | ||
| } | ||
|
|
||
| const name = getTrimmedString(raw, 'name') | ||
| const ownedBy = | ||
| getTrimmedString(raw, 'owned_by') || getTrimmedString(raw, 'ownedBy') | ||
| const label = name || (ownedBy ? `${id} (${ownedBy})` : id) | ||
| const contextWindow = | ||
| getPositiveInteger(raw.context_length) ?? | ||
| getPositiveInteger(raw.context_window) | ||
|
|
||
| return { | ||
| id, | ||
| apiName: id, | ||
| label, | ||
| ...(contextWindow !== undefined ? { contextWindow } : {}), | ||
| } | ||
| } | ||
|
|
||
| const curatedModels = [ | ||
| { | ||
| id: 'claude-sonnet-4-6', | ||
| apiName: 'claude-sonnet-4-6', | ||
| aliases: ['sonnet-4.6', 'claude-sonnet'], | ||
| modelDescriptorId: 'claude-sonnet-4-6', | ||
| label: 'Claude Sonnet 4.6', | ||
| contextWindow: 200_000, | ||
| maxOutputTokens: 8_192, | ||
| }, | ||
| ] | ||
|
|
||
| const catalog = defineCatalog({ | ||
| source: 'hybrid', | ||
| discovery: { | ||
| kind: 'openai-compatible', | ||
| requiresAuth: true, | ||
| mapModel: mapApiRouteModel, | ||
| }, | ||
| discoveryCacheTtl: '1d', | ||
| discoveryRefreshMode: 'background-if-stale', | ||
| allowManualRefresh: true, | ||
| models: [...curatedModels], | ||
| }) | ||
|
|
||
| export default defineGateway({ | ||
| id: 'api-route', | ||
| label: 'API Route', | ||
| category: 'aggregating', | ||
| defaultBaseUrl: 'https://global.api-route.com/v1', | ||
| defaultModel: 'claude-sonnet-4-6', | ||
| supportsModelRouting: true, | ||
| setup: { | ||
| requiresAuth: true, | ||
| authMode: 'api-key', | ||
| credentialEnvVars: ['API_ROUTE_API_KEY'], | ||
| dedicatedCredentialsOnly: false, | ||
| }, | ||
| startup: { | ||
| probeReadiness: 'openai-compatible-models', | ||
| }, | ||
| transportConfig: { | ||
| kind: 'openai-compatible', | ||
| openaiShim: { | ||
| supportsApiFormatSelection: false, | ||
| supportsAuthHeaders: false, | ||
| maxTokensField: 'max_tokens', | ||
| }, | ||
| }, | ||
| preset: { | ||
| id: 'api-route', | ||
| description: 'API Route OpenAI-compatible multi-model gateway', | ||
| vendorId: 'openai', | ||
| apiKeyEnvVars: ['API_ROUTE_API_KEY'], | ||
| modelEnvVars: ['API_ROUTE_MODEL', 'OPENAI_MODEL'], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When API Route is activated through |
||
| }, | ||
| validation: { | ||
| kind: 'credential-env', | ||
| routing: { | ||
| matchDefaultBaseUrl: true, | ||
| matchBaseUrlHosts: ['global.api-route.com'], | ||
| }, | ||
| credentialEnvVars: ['API_ROUTE_API_KEY', 'OPENAI_API_KEYS', 'OPENAI_API_KEY'], | ||
| missingCredentialMessage: | ||
| 'API Route auth is required. Set API_ROUTE_API_KEY.', | ||
| }, | ||
| catalog, | ||
| usage: { supported: false }, | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.