Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9b05cb0
feat(gateway): add API Route hybrid gateway
DennyHo0917 Sep 12, 2026
5530cd5
fix(gateway): address review feedback for API Route gateway
DennyHo0917 Sep 17, 2026
31293de
test(api-route): verify auth header withheld on noncanonical URL
DennyHo0917 Sep 20, 2026
b75042c
fix(api-route): honor generic saved profile credentials
DennyHo0917 Sep 20, 2026
26bde3d
test(api-route): cover generic profile credential fallback
DennyHo0917 Sep 20, 2026
290a851
fix(api-route): protect route vars from settings overrides
DennyHo0917 Sep 20, 2026
20cdb48
fix(api-route): allow dedicated vars in provider env files
DennyHo0917 Sep 20, 2026
dd10e70
test(api-route): isolate provider flag environment
DennyHo0917 Sep 20, 2026
cbe3ec6
fix(api-route): preserve env-only intent across startup profiles
DennyHo0917 Sep 20, 2026
fbcf8e8
test(api-route): cover provider env file loading
DennyHo0917 Sep 20, 2026
e54e23f
test(api-route): cover host-managed settings boundary
DennyHo0917 Sep 20, 2026
b892bf7
test(api-route): cover saved profile ownership and switch cleanup
DennyHo0917 Sep 20, 2026
77869f9
test(api-route): preserve env-only startup selection
DennyHo0917 Sep 20, 2026
0391c56
test(api-route): verify saved profile credential boundary
DennyHo0917 Sep 20, 2026
6edae47
test(api-route): accept canonical saved profile credentials
DennyHo0917 Sep 20, 2026
1bb49e5
test(api-route): cover saved profile relaunch
DennyHo0917 Sep 20, 2026
9f2a566
sync: apply upstream xAI OAuth state validation
DennyHo0917 Sep 20, 2026
a80a525
sync: apply upstream xAI OAuth state validation
DennyHo0917 Sep 20, 2026
7556586
sync: apply upstream xAI OAuth state validation
DennyHo0917 Sep 20, 2026
093adb6
Merge upstream main at d16318a
DennyHo0917 Sep 20, 2026
90e0d5d
test(api-route): preserve env-file precedence and atomicity
DennyHo0917 Sep 20, 2026
226808d
test(api-route): cover saved-only profile contract
DennyHo0917 Sep 20, 2026
409ccf7
fix(gateway): register api-route for proxy credential withholding and…
DennyHo0917 Sep 21, 2026
e610e8e
fix(gateway): clear OPENAI_API_KEYS in canonical API Route selection
DennyHo0917 Sep 21, 2026
4e00875
fix(api-route): complete saved profile credential parity
DennyHo0917 Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# OPENAI_BASE_URL=https://api.aimlapi.com/v1
# OPENAI_MODEL=gpt-4o

# For API Route, the dedicated key enables the OpenAI-compatible route:
# API_ROUTE_API_KEY=your-api-route-key-here
# OPENAI_BASE_URL=https://global.api-route.com/v1
# OPENAI_MODEL=claude-sonnet-4-6

# For LLMTR, prefer its dedicated key. Raw env setup must also set
# OPENAI_BASE_URL and OPENAI_MODEL. OPENAI_API_KEY remains supported:
# LLMTR_API_KEY=your-llmtr-key-here
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ Advanced and source-build guides:
| LLMTR | `/provider` or OpenAI-compatible env vars | Multi-model gateway at `https://llmtr.com/v1`; `/provider` and `--provider llmtr` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://llmtr.com/v1` and `OPENAI_MODEL`; accepts `LLMTR_API_KEY` or `OPENAI_API_KEY` after the route is selected and discovers tool-capable Chat Completions models from the public catalog |
| Command Code | `/provider` or OpenAI-compatible env vars | Hybrid OpenAI-compatible gateway at `https://api.commandcode.ai/provider/v1`; `/provider` and `--provider commandcode` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://api.commandcode.ai/provider/v1` and `OPENAI_MODEL`; requires `CMD_API_KEY`, `COMMANDCODE_API_KEY`, or the official `COMMAND_CODE_API_KEY` after the route is selected and discovers Chat Completions models from the public catalog. Claude models are unsupported by this Chat Completions integration |
| ApiSmart | `/provider` or `APISMART_API_KEY` | Uses `https://gw.apismart.ai/v1`, defaults to `DEEPSEEK_V4_FLASH`, and supports optional `APISMART_MODEL` plus authenticated model discovery |
| API Route | `/provider` or `API_ROUTE_API_KEY` | Uses `https://global.api-route.com/v1`, defaults to `claude-sonnet-4-6`, and supports optional `API_ROUTE_MODEL` plus authenticated model discovery |
| Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models |
| Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` |
| LongCat | `/provider` or env vars | Meituan LongCat OpenAI-compatible API at `https://api.longcat.chat/openai/v1`; uses `LONGCAT_API_KEY` and defaults to `LongCat-2.0` |
Expand Down
1 change: 1 addition & 0 deletions src/integrations/compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const EXPECTED_PRESETS = [
'opencode',
'opencode-go',
'clinepass',
'api-route',
] as const satisfies readonly ProviderPreset[]

describe('compatibility mappings', () => {
Expand Down
174 changes: 174 additions & 0 deletions src/integrations/gateways/api-route.test.ts
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',
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
).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')
})
125 changes: 125 additions & 0 deletions src/integrations/gateways/api-route.ts
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'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dedicated model setting ignored

When API Route is activated through API_ROUTE_API_KEY without a saved provider profile, this preset advertises API_ROUTE_MODEL, but the runtime env-only path has no API Route defaults handler and model selection reads only OPENAI_MODEL. As a result, API_ROUTE_MODEL and the declared claude-sonnet-4-6 default are ignored, so requests may use an unrelated OPENAI_MODEL value or fall back to gpt-4o.

},
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 },
})
3 changes: 2 additions & 1 deletion src/integrations/generated/integrationArtifacts.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import vendorXai from '../vendors/xai.js'
import vendorXiaomiMimo from '../vendors/xiaomi-mimo.js'
import vendorZai from '../vendors/zai.js'
import gatewayAimlapi from '../gateways/aimlapi.js'
import gatewayApiRoute from '../gateways/api-route.js'
import gatewayApismart from '../gateways/apismart.js'
import gatewayAtlasCloud from '../gateways/atlas-cloud.js'
import gatewayAtomicChat from '../gateways/atomic-chat.js'
Expand Down Expand Up @@ -93,7 +94,7 @@ import modelXai from '../models/xai.js'
import modelXiaomiMimo from '../models/xiaomi-mimo.js'

export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, vendorFireworks, vendorGemini, vendorLongcat, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApiRoute, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
export const ANTHROPIC_PROXY_DESCRIPTORS = [anthropicproxyCustom] as const satisfies readonly AnthropicProxyDescriptor[]
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLing, brandLlama, brandLongcat, brandMacaron, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandTencent, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLing, modelLlama, modelLongcat, modelMacaron, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
Expand Down
16 changes: 16 additions & 0 deletions src/integrations/generated/integrationManifest.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ export const PROVIDER_PRESET_MANIFEST = [
"DASHSCOPE_API_KEY"
]
},
{
"preset": "api-route",
"routeKind": "gateway",
"routeId": "api-route",
"vendorId": "openai",
"gatewayId": "api-route",
"description": "API Route OpenAI-compatible multi-model gateway",
"apiKeyEnvVars": [
"API_ROUTE_API_KEY"
],
"modelEnvVars": [
"API_ROUTE_MODEL",
"OPENAI_MODEL"
]
},
{
"preset": "apismart",
"routeKind": "gateway",
Expand Down Expand Up @@ -591,6 +606,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"anthropic",
"dashscope-cn",
"dashscope-intl",
"api-route",
"apismart",
"atlas-cloud",
"azure-openai",
Expand Down
3 changes: 2 additions & 1 deletion src/integrations/providerUiMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ function hasUsableEnvValue(envVar: string, value: string | undefined): boolean {
envVar === 'LLMTR_API_KEY' ||
envVar === 'CMD_API_KEY' ||
envVar === 'COMMANDCODE_API_KEY' ||
envVar === 'COMMAND_CODE_API_KEY'
envVar === 'COMMAND_CODE_API_KEY' ||
envVar === 'API_ROUTE_API_KEY'
) {
return hasUsableOpenAICredential(value)
}
Expand Down
Loading
Loading