mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 20:58:14 -04:00
feat(gateway): add API Route hybrid gateway (#2225)
* feat(gateway): add API Route hybrid gateway Add API Route as a descriptor-first OpenAI-compatible aggregating gateway with hybrid catalog discovery, dedicated API_ROUTE_API_KEY credentials, and route boundary protections. * fix(gateway): address review feedback for API Route gateway * test(api-route): verify auth header withheld on noncanonical URL * fix(api-route): honor generic saved profile credentials * test(api-route): cover generic profile credential fallback * fix(api-route): protect route vars from settings overrides * fix(api-route): allow dedicated vars in provider env files * test(api-route): isolate provider flag environment * fix(api-route): preserve env-only intent across startup profiles * test(api-route): cover provider env file loading * test(api-route): cover host-managed settings boundary * test(api-route): cover saved profile ownership and switch cleanup * test(api-route): preserve env-only startup selection * test(api-route): verify saved profile credential boundary * test(api-route): accept canonical saved profile credentials * test(api-route): cover saved profile relaunch * sync: apply upstream xAI OAuth state validation * sync: apply upstream xAI OAuth state validation * sync: apply upstream xAI OAuth state validation * test(api-route): preserve env-file precedence and atomicity * test(api-route): cover saved-only profile contract * fix(gateway): register api-route for proxy credential withholding and sanitize model flag - Add api-route to isNoncanonicalDedicatedOpenAILaunch to strip ambient credentials - Register persistedApiRouteProxy in applyStartupEnvFromProfile - Delete OPENAI_API_KEYS on noncanonical base URLs in applyApiRouteEnvOnlyDefaults - Sanitize API_ROUTE_MODEL with usableProviderModelEnvValue in applyProviderFlag - Add regression tests covering credential withholding and model placeholder handling * fix(gateway): clear OPENAI_API_KEYS in canonical API Route selection - Delete OPENAI_API_KEYS when dedicated API_ROUTE_API_KEY is mirrored in applyApiRouteEnvOnlyDefaults - Delete OPENAI_API_KEYS in applyProviderFlag for canonical api-route selection - Add request-level regression test verifying API_ROUTE_API_KEY precedence over OPENAI_API_KEYS - Add providerFlag test confirming credential pool cleanup * fix(api-route): complete saved profile credential parity
This commit is contained in:
@@ -216,6 +216,11 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
|
|||||||
# OPENAI_BASE_URL=https://api.aimlapi.com/v1
|
# OPENAI_BASE_URL=https://api.aimlapi.com/v1
|
||||||
# OPENAI_MODEL=gpt-4o
|
# 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
|
# For LLMTR, prefer its dedicated key. Raw env setup must also set
|
||||||
# OPENAI_BASE_URL and OPENAI_MODEL. OPENAI_API_KEY remains supported:
|
# OPENAI_BASE_URL and OPENAI_MODEL. OPENAI_API_KEY remains supported:
|
||||||
# LLMTR_API_KEY=your-llmtr-key-here
|
# LLMTR_API_KEY=your-llmtr-key-here
|
||||||
|
|||||||
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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` |
|
| 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` |
|
| 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` |
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ const EXPECTED_PRESETS = [
|
|||||||
'opencode',
|
'opencode',
|
||||||
'opencode-go',
|
'opencode-go',
|
||||||
'clinepass',
|
'clinepass',
|
||||||
|
'api-route',
|
||||||
] as const satisfies readonly ProviderPreset[]
|
] as const satisfies readonly ProviderPreset[]
|
||||||
|
|
||||||
describe('compatibility mappings', () => {
|
describe('compatibility mappings', () => {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
})
|
||||||
@@ -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'],
|
||||||
|
},
|
||||||
|
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 },
|
||||||
|
})
|
||||||
@@ -20,6 +20,7 @@ import vendorXai from '../vendors/xai.js'
|
|||||||
import vendorXiaomiMimo from '../vendors/xiaomi-mimo.js'
|
import vendorXiaomiMimo from '../vendors/xiaomi-mimo.js'
|
||||||
import vendorZai from '../vendors/zai.js'
|
import vendorZai from '../vendors/zai.js'
|
||||||
import gatewayAimlapi from '../gateways/aimlapi.js'
|
import gatewayAimlapi from '../gateways/aimlapi.js'
|
||||||
|
import gatewayApiRoute from '../gateways/api-route.js'
|
||||||
import gatewayApismart from '../gateways/apismart.js'
|
import gatewayApismart from '../gateways/apismart.js'
|
||||||
import gatewayAtlasCloud from '../gateways/atlas-cloud.js'
|
import gatewayAtlasCloud from '../gateways/atlas-cloud.js'
|
||||||
import gatewayAtomicChat from '../gateways/atomic-chat.js'
|
import gatewayAtomicChat from '../gateways/atomic-chat.js'
|
||||||
@@ -93,7 +94,7 @@ import modelXai from '../models/xai.js'
|
|||||||
import modelXiaomiMimo from '../models/xiaomi-mimo.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 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 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 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[])[]
|
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[])[]
|
||||||
|
|||||||
@@ -90,6 +90,21 @@ export const PROVIDER_PRESET_MANIFEST = [
|
|||||||
"DASHSCOPE_API_KEY"
|
"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",
|
"preset": "apismart",
|
||||||
"routeKind": "gateway",
|
"routeKind": "gateway",
|
||||||
@@ -591,6 +606,7 @@ export const ORDERED_PROVIDER_PRESETS = [
|
|||||||
"anthropic",
|
"anthropic",
|
||||||
"dashscope-cn",
|
"dashscope-cn",
|
||||||
"dashscope-intl",
|
"dashscope-intl",
|
||||||
|
"api-route",
|
||||||
"apismart",
|
"apismart",
|
||||||
"atlas-cloud",
|
"atlas-cloud",
|
||||||
"azure-openai",
|
"azure-openai",
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ function hasUsableEnvValue(envVar: string, value: string | undefined): boolean {
|
|||||||
envVar === 'LLMTR_API_KEY' ||
|
envVar === 'LLMTR_API_KEY' ||
|
||||||
envVar === 'CMD_API_KEY' ||
|
envVar === 'CMD_API_KEY' ||
|
||||||
envVar === 'COMMANDCODE_API_KEY' ||
|
envVar === 'COMMANDCODE_API_KEY' ||
|
||||||
envVar === 'COMMAND_CODE_API_KEY'
|
envVar === 'COMMAND_CODE_API_KEY' ||
|
||||||
|
envVar === 'API_ROUTE_API_KEY'
|
||||||
) {
|
) {
|
||||||
return hasUsableOpenAICredential(value)
|
return hasUsableOpenAICredential(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1223,6 +1223,15 @@ test('resolveActiveRouteIdFromEnv honors an explicit OpenAI opt-out over Concent
|
|||||||
expect(resolveActiveRouteIdFromEnv({ CLAUDE_CODE_USE_OPENAI: '0', CONCENTRATE_API_KEY: 'concentrate-key' })).not.toBe('concentrate')
|
expect(resolveActiveRouteIdFromEnv({ CLAUDE_CODE_USE_OPENAI: '0', CONCENTRATE_API_KEY: 'concentrate-key' })).not.toBe('concentrate')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('resolveActiveRouteIdFromEnv honors an explicit OpenAI opt-out over API Route', () => {
|
||||||
|
expect(
|
||||||
|
resolveActiveRouteIdFromEnv({
|
||||||
|
CLAUDE_CODE_USE_OPENAI: '0',
|
||||||
|
API_ROUTE_API_KEY: 'api-route-key',
|
||||||
|
}),
|
||||||
|
).not.toBe('api-route')
|
||||||
|
})
|
||||||
|
|
||||||
test('resolveActiveRouteIdFromEnv refines generic OpenAI profile by Concentrate base URL', () => {
|
test('resolveActiveRouteIdFromEnv refines generic OpenAI profile by Concentrate base URL', () => {
|
||||||
expect(
|
expect(
|
||||||
resolveActiveRouteIdFromEnv({
|
resolveActiveRouteIdFromEnv({
|
||||||
|
|||||||
@@ -246,7 +246,8 @@ function hasUsableEnvCredentialValue(
|
|||||||
envVar === 'LLMTR_API_KEY' ||
|
envVar === 'LLMTR_API_KEY' ||
|
||||||
envVar === 'CMD_API_KEY' ||
|
envVar === 'CMD_API_KEY' ||
|
||||||
envVar === 'COMMANDCODE_API_KEY' ||
|
envVar === 'COMMANDCODE_API_KEY' ||
|
||||||
envVar === 'COMMAND_CODE_API_KEY'
|
envVar === 'COMMAND_CODE_API_KEY' ||
|
||||||
|
envVar === 'API_ROUTE_API_KEY'
|
||||||
) {
|
) {
|
||||||
return hasUsableOpenAICredential(value)
|
return hasUsableOpenAICredential(value)
|
||||||
}
|
}
|
||||||
@@ -539,6 +540,54 @@ export function isCanonicalCommandcodeInferenceBaseUrl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const API_ROUTE_CANONICAL_INFERENCE_BASE_URL =
|
||||||
|
'https://global.api-route.com/v1'
|
||||||
|
|
||||||
|
export function isCanonicalApiRouteInferenceBaseUrl(
|
||||||
|
value: string | undefined,
|
||||||
|
): boolean {
|
||||||
|
const trimmed = value?.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canonical = new URL(API_ROUTE_CANONICAL_INFERENCE_BASE_URL)
|
||||||
|
const candidate = new URL(trimmed)
|
||||||
|
const normalizePath = (pathname: string): string =>
|
||||||
|
pathname.replace(/\/+$/, '') || '/'
|
||||||
|
return (
|
||||||
|
candidate.protocol === 'https:' &&
|
||||||
|
!candidate.port &&
|
||||||
|
!candidate.search &&
|
||||||
|
!candidate.hash &&
|
||||||
|
candidate.hostname.toLowerCase() === canonical.hostname.toLowerCase() &&
|
||||||
|
normalizePath(candidate.pathname) === normalizePath(canonical.pathname)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isApiRouteBaseUrl(value: string | undefined): boolean {
|
||||||
|
const trimmed = value?.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(trimmed)
|
||||||
|
return (
|
||||||
|
parsed.protocol === 'https:' &&
|
||||||
|
!parsed.port &&
|
||||||
|
(parsed.hostname.toLowerCase() === 'global.api-route.com' ||
|
||||||
|
parsed.hostname.toLowerCase() === 'api-route.com')
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getConcentrateBaseUrlOverride(
|
export function getConcentrateBaseUrlOverride(
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
@@ -1039,6 +1088,18 @@ export function hasConcentrateEnvOnlyProviderIntent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasApiRouteEnvOnlyProviderIntent(
|
||||||
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
hasUsableOpenAICredential(processEnv.API_ROUTE_API_KEY) &&
|
||||||
|
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isApiRouteBaseUrl) &&
|
||||||
|
!(processEnv.CLAUDE_CODE_USE_OPENAI !== undefined &&
|
||||||
|
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) &&
|
||||||
|
hasNoExplicitNonOpenAIProvider(processEnv)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveEnvOnlyProviderRouteId(
|
export function resolveEnvOnlyProviderRouteId(
|
||||||
processEnv: NodeJS.ProcessEnv = process.env,
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
):
|
):
|
||||||
@@ -1053,6 +1114,7 @@ export function resolveEnvOnlyProviderRouteId(
|
|||||||
| 'clinepass'
|
| 'clinepass'
|
||||||
| 'apismart'
|
| 'apismart'
|
||||||
| 'concentrate'
|
| 'concentrate'
|
||||||
|
| 'api-route'
|
||||||
| null {
|
| null {
|
||||||
if (
|
if (
|
||||||
hasMiniMaxRouteIntent(processEnv) &&
|
hasMiniMaxRouteIntent(processEnv) &&
|
||||||
@@ -1105,6 +1167,10 @@ export function resolveEnvOnlyProviderRouteId(
|
|||||||
return 'concentrate'
|
return 'concentrate'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasApiRouteEnvOnlyProviderIntent(processEnv)) {
|
||||||
|
return 'api-route'
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1203,6 +1269,13 @@ export function resolveRouteCredentialValue(
|
|||||||
) {
|
) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
routeId === 'api-route' &&
|
||||||
|
options?.baseUrl !== undefined &&
|
||||||
|
!isCanonicalApiRouteInferenceBaseUrl(options.baseUrl)
|
||||||
|
) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
return getRouteCredentialValue(routeId, processEnv)
|
return getRouteCredentialValue(routeId, processEnv)
|
||||||
}
|
}
|
||||||
@@ -1327,6 +1400,12 @@ export function resolveRouteIdFromBaseUrl(
|
|||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
route.id === 'api-route' &&
|
||||||
|
!isCanonicalApiRouteInferenceBaseUrl(baseUrl)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return route.id
|
return route.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1347,7 +1426,9 @@ export function resolveRouteIdFromBaseUrl(
|
|||||||
(route.id === 'llmtr' &&
|
(route.id === 'llmtr' &&
|
||||||
!isCanonicalLlmtrInferenceBaseUrl(baseUrl)) ||
|
!isCanonicalLlmtrInferenceBaseUrl(baseUrl)) ||
|
||||||
(route.id === 'commandcode' &&
|
(route.id === 'commandcode' &&
|
||||||
!isCanonicalCommandcodeInferenceBaseUrl(baseUrl))
|
!isCanonicalCommandcodeInferenceBaseUrl(baseUrl)) ||
|
||||||
|
(route.id === 'api-route' &&
|
||||||
|
!isApiRouteBaseUrl(baseUrl))
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1393,6 +1474,9 @@ function profileRouteHonorsBaseUrlBoundary(
|
|||||||
if (routeId === 'commandcode') {
|
if (routeId === 'commandcode') {
|
||||||
return isCanonicalCommandcodeInferenceBaseUrl(baseUrl)
|
return isCanonicalCommandcodeInferenceBaseUrl(baseUrl)
|
||||||
}
|
}
|
||||||
|
if (routeId === 'api-route') {
|
||||||
|
return isCanonicalApiRouteInferenceBaseUrl(baseUrl)
|
||||||
|
}
|
||||||
if (routeId === 'zai') {
|
if (routeId === 'zai') {
|
||||||
return !baseUrl || isCanonicalZaiCodingPlanBaseUrl(baseUrl)
|
return !baseUrl || isCanonicalZaiCodingPlanBaseUrl(baseUrl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ const originalEnv = {
|
|||||||
LONGCAT_API_KEY: process.env.LONGCAT_API_KEY,
|
LONGCAT_API_KEY: process.env.LONGCAT_API_KEY,
|
||||||
AIMLAPI_API_KEY: process.env.AIMLAPI_API_KEY,
|
AIMLAPI_API_KEY: process.env.AIMLAPI_API_KEY,
|
||||||
APISMART_API_KEY: process.env.APISMART_API_KEY,
|
APISMART_API_KEY: process.env.APISMART_API_KEY,
|
||||||
|
API_ROUTE_API_KEY: process.env.API_ROUTE_API_KEY,
|
||||||
|
API_ROUTE_MODEL: process.env.API_ROUTE_MODEL,
|
||||||
CONCENTRATE_API_KEY: process.env.CONCENTRATE_API_KEY,
|
CONCENTRATE_API_KEY: process.env.CONCENTRATE_API_KEY,
|
||||||
CONCENTRATE_BASE_URL: process.env.CONCENTRATE_BASE_URL,
|
CONCENTRATE_BASE_URL: process.env.CONCENTRATE_BASE_URL,
|
||||||
CONCENTRATE_MODEL: process.env.CONCENTRATE_MODEL,
|
CONCENTRATE_MODEL: process.env.CONCENTRATE_MODEL,
|
||||||
@@ -209,6 +211,8 @@ beforeEach(async () => {
|
|||||||
delete process.env.LONGCAT_API_KEY
|
delete process.env.LONGCAT_API_KEY
|
||||||
delete process.env.AIMLAPI_API_KEY
|
delete process.env.AIMLAPI_API_KEY
|
||||||
delete process.env.APISMART_API_KEY
|
delete process.env.APISMART_API_KEY
|
||||||
|
delete process.env.API_ROUTE_API_KEY
|
||||||
|
delete process.env.API_ROUTE_MODEL
|
||||||
delete process.env.CONCENTRATE_API_KEY
|
delete process.env.CONCENTRATE_API_KEY
|
||||||
delete process.env.CONCENTRATE_BASE_URL
|
delete process.env.CONCENTRATE_BASE_URL
|
||||||
delete process.env.CONCENTRATE_MODEL
|
delete process.env.CONCENTRATE_MODEL
|
||||||
@@ -274,6 +278,8 @@ afterEach(() => {
|
|||||||
restoreEnv('LONGCAT_API_KEY', originalEnv.LONGCAT_API_KEY)
|
restoreEnv('LONGCAT_API_KEY', originalEnv.LONGCAT_API_KEY)
|
||||||
restoreEnv('AIMLAPI_API_KEY', originalEnv.AIMLAPI_API_KEY)
|
restoreEnv('AIMLAPI_API_KEY', originalEnv.AIMLAPI_API_KEY)
|
||||||
restoreEnv('APISMART_API_KEY', originalEnv.APISMART_API_KEY)
|
restoreEnv('APISMART_API_KEY', originalEnv.APISMART_API_KEY)
|
||||||
|
restoreEnv('API_ROUTE_API_KEY', originalEnv.API_ROUTE_API_KEY)
|
||||||
|
restoreEnv('API_ROUTE_MODEL', originalEnv.API_ROUTE_MODEL)
|
||||||
restoreEnv('CONCENTRATE_API_KEY', originalEnv.CONCENTRATE_API_KEY)
|
restoreEnv('CONCENTRATE_API_KEY', originalEnv.CONCENTRATE_API_KEY)
|
||||||
restoreEnv('CONCENTRATE_BASE_URL', originalEnv.CONCENTRATE_BASE_URL)
|
restoreEnv('CONCENTRATE_BASE_URL', originalEnv.CONCENTRATE_BASE_URL)
|
||||||
restoreEnv('CONCENTRATE_MODEL', originalEnv.CONCENTRATE_MODEL)
|
restoreEnv('CONCENTRATE_MODEL', originalEnv.CONCENTRATE_MODEL)
|
||||||
@@ -930,6 +936,206 @@ test('env-only ApiSmart setup withholds its key from a noncanonical same-host UR
|
|||||||
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('routes env-only API Route requests through the OpenAI-compatible shim', async () => {
|
||||||
|
let capturedUrl: string | undefined
|
||||||
|
let capturedHeaders: Headers | undefined
|
||||||
|
let capturedBody: Record<string, unknown> | undefined
|
||||||
|
|
||||||
|
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||||
|
delete process.env.GEMINI_API_KEY
|
||||||
|
delete process.env.GEMINI_MODEL
|
||||||
|
delete process.env.GEMINI_BASE_URL
|
||||||
|
delete process.env.GEMINI_AUTH_MODE
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-test-key'
|
||||||
|
process.env.API_ROUTE_MODEL = 'claude-sonnet-4-6'
|
||||||
|
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Proxy-Auth: ambient-proxy-secret'
|
||||||
|
|
||||||
|
globalThis.fetch = (async (input, init) => {
|
||||||
|
capturedUrl =
|
||||||
|
typeof input === 'string'
|
||||||
|
? input
|
||||||
|
: input instanceof URL
|
||||||
|
? input.toString()
|
||||||
|
: input.url
|
||||||
|
capturedHeaders = new Headers(init?.headers)
|
||||||
|
capturedBody = JSON.parse(String(init?.body)) as Record<string, unknown>
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
id: 'chatcmpl-api-route',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'api-route ok',
|
||||||
|
},
|
||||||
|
finish_reason: 'stop',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 8,
|
||||||
|
completion_tokens: 3,
|
||||||
|
total_tokens: 11,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}) as typeof fetch
|
||||||
|
|
||||||
|
const client = await getAnthropicClient({
|
||||||
|
maxRetries: 0,
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
})
|
||||||
|
const response = await client.messages.create({
|
||||||
|
max_tokens: 64,
|
||||||
|
messages: [{ role: 'user', content: 'hello' }],
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.content[0]).toEqual({
|
||||||
|
type: 'text',
|
||||||
|
text: 'api-route ok',
|
||||||
|
})
|
||||||
|
expect(capturedUrl).toBe(
|
||||||
|
'https://global.api-route.com/v1/chat/completions',
|
||||||
|
)
|
||||||
|
expect(capturedHeaders?.get('authorization')).toBe(
|
||||||
|
'Bearer api-route-test-key',
|
||||||
|
)
|
||||||
|
expect(capturedHeaders?.get('x-proxy-auth')).toBeNull()
|
||||||
|
expect(capturedBody?.model).toBe('claude-sonnet-4-6')
|
||||||
|
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1')
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe('https://global.api-route.com/v1')
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('api-route-test-key')
|
||||||
|
expect(process.env.OPENAI_MODEL).toBe('claude-sonnet-4-6')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('env-only API Route setup prefers API_ROUTE_API_KEY over stale OPENAI_API_KEYS pool', async () => {
|
||||||
|
let capturedHeaders: Headers | undefined
|
||||||
|
|
||||||
|
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||||
|
delete process.env.GEMINI_API_KEY
|
||||||
|
delete process.env.GEMINI_MODEL
|
||||||
|
delete process.env.GEMINI_BASE_URL
|
||||||
|
delete process.env.GEMINI_AUTH_MODE
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-correct-key'
|
||||||
|
process.env.OPENAI_API_KEYS = 'stale-key-1,stale-key-2'
|
||||||
|
process.env.OPENAI_BASE_URL = 'https://global.api-route.com/v1'
|
||||||
|
|
||||||
|
globalThis.fetch = (async (_input, init) => {
|
||||||
|
capturedHeaders = new Headers(init?.headers)
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
id: 'chatcmpl-api-route-precedence',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'api-route ok',
|
||||||
|
},
|
||||||
|
finish_reason: 'stop',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 8,
|
||||||
|
completion_tokens: 3,
|
||||||
|
total_tokens: 11,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}) as typeof fetch
|
||||||
|
|
||||||
|
const client = await getAnthropicClient({
|
||||||
|
maxRetries: 0,
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
})
|
||||||
|
await client.messages.create({
|
||||||
|
max_tokens: 64,
|
||||||
|
messages: [{ role: 'user', content: 'hello' }],
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(capturedHeaders?.get('authorization')).toBe(
|
||||||
|
'Bearer api-route-correct-key',
|
||||||
|
)
|
||||||
|
expect(capturedHeaders?.get('authorization')).not.toContain('stale-key')
|
||||||
|
expect(process.env.OPENAI_API_KEYS).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('api-route-correct-key')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('env-only API Route setup withholds its key from a noncanonical URL', async () => {
|
||||||
|
let capturedHeaders: Headers | undefined
|
||||||
|
|
||||||
|
delete process.env.CLAUDE_CODE_USE_GEMINI
|
||||||
|
delete process.env.GEMINI_API_KEY
|
||||||
|
delete process.env.GEMINI_MODEL
|
||||||
|
delete process.env.GEMINI_BASE_URL
|
||||||
|
delete process.env.GEMINI_AUTH_MODE
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-test-key'
|
||||||
|
process.env.OPENAI_BASE_URL = 'https://global.api-route.com/v1/models'
|
||||||
|
process.env.OPENAI_API_KEYS = 'ambient-key-pool-1,ambient-key-pool-2'
|
||||||
|
|
||||||
|
globalThis.fetch = (async (_input, init) => {
|
||||||
|
capturedHeaders = new Headers(init?.headers)
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
id: 'chatcmpl-api-route-noncanonical',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'api-route noncanonical ok',
|
||||||
|
},
|
||||||
|
finish_reason: 'stop',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 8,
|
||||||
|
completion_tokens: 3,
|
||||||
|
total_tokens: 11,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}) as typeof fetch
|
||||||
|
|
||||||
|
const client = await getAnthropicClient({
|
||||||
|
maxRetries: 0,
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
})
|
||||||
|
await client.messages.create({
|
||||||
|
max_tokens: 64,
|
||||||
|
messages: [{ role: 'user', content: 'hello' }],
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(capturedHeaders?.get('authorization')).toBeNull()
|
||||||
|
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1')
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe(
|
||||||
|
'https://global.api-route.com/v1/models',
|
||||||
|
)
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_API_KEYS).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
test('routes env-only Concentrate requests through the OpenAI-compatible shim', async () => {
|
test('routes env-only Concentrate requests through the OpenAI-compatible shim', async () => {
|
||||||
let capturedUrl: string | undefined
|
let capturedUrl: string | undefined
|
||||||
let capturedHeaders: Headers | undefined
|
let capturedHeaders: Headers | undefined
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
getLongcatBaseUrlOverride,
|
getLongcatBaseUrlOverride,
|
||||||
getMiniMaxBaseUrlOverride,
|
getMiniMaxBaseUrlOverride,
|
||||||
getNearaiBaseUrlOverride,
|
getNearaiBaseUrlOverride,
|
||||||
|
isCanonicalApiRouteInferenceBaseUrl,
|
||||||
isCanonicalApismartInferenceBaseUrl,
|
isCanonicalApismartInferenceBaseUrl,
|
||||||
isCanonicalConcentrateInferenceBaseUrl,
|
isCanonicalConcentrateInferenceBaseUrl,
|
||||||
getRouteDefaultBaseUrl,
|
getRouteDefaultBaseUrl,
|
||||||
@@ -450,6 +451,39 @@ function applyConcentrateEnvOnlyDefaults(): void {
|
|||||||
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyApiRouteEnvOnlyDefaults(): void {
|
||||||
|
const baseUrlOverride =
|
||||||
|
usableProviderConfigEnvValue(process.env.OPENAI_BASE_URL) ||
|
||||||
|
usableProviderConfigEnvValue(process.env.OPENAI_API_BASE) ||
|
||||||
|
undefined
|
||||||
|
const modelOverride =
|
||||||
|
usableProviderConfigEnvValue(process.env.API_ROUTE_MODEL) ||
|
||||||
|
usableProviderConfigEnvValue(process.env.OPENAI_MODEL) ||
|
||||||
|
undefined
|
||||||
|
const apiKey = process.env.API_ROUTE_API_KEY
|
||||||
|
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
process.env.OPENAI_BASE_URL =
|
||||||
|
baseUrlOverride ?? getRouteDefaultBaseUrl('api-route')
|
||||||
|
process.env.OPENAI_MODEL = modelOverride ?? getRouteDefaultModel('api-route')
|
||||||
|
if (
|
||||||
|
hasUsableOpenAICredential(apiKey) &&
|
||||||
|
isCanonicalApiRouteInferenceBaseUrl(process.env.OPENAI_BASE_URL)
|
||||||
|
) {
|
||||||
|
process.env.OPENAI_API_KEY = apiKey
|
||||||
|
delete process.env.OPENAI_API_KEYS
|
||||||
|
} else {
|
||||||
|
delete process.env.OPENAI_API_KEY
|
||||||
|
delete process.env.OPENAI_API_KEYS
|
||||||
|
}
|
||||||
|
delete process.env.OPENAI_API_FORMAT
|
||||||
|
delete process.env.OPENAI_AZURE_STYLE
|
||||||
|
delete process.env.OPENAI_AUTH_HEADER
|
||||||
|
delete process.env.OPENAI_AUTH_SCHEME
|
||||||
|
delete process.env.OPENAI_AUTH_HEADER_VALUE
|
||||||
|
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||||
|
}
|
||||||
|
|
||||||
function usableProviderConfigEnvValue(
|
function usableProviderConfigEnvValue(
|
||||||
value: string | undefined,
|
value: string | undefined,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
@@ -574,6 +608,8 @@ export async function getAnthropicClient({
|
|||||||
envOnlyProviderRouteId === 'apismart' && !useMiniMaxEnvOnlyProvider
|
envOnlyProviderRouteId === 'apismart' && !useMiniMaxEnvOnlyProvider
|
||||||
const useConcentrateEnvOnlyProvider =
|
const useConcentrateEnvOnlyProvider =
|
||||||
envOnlyProviderRouteId === 'concentrate' && !useMiniMaxEnvOnlyProvider
|
envOnlyProviderRouteId === 'concentrate' && !useMiniMaxEnvOnlyProvider
|
||||||
|
const useApiRouteEnvOnlyProvider =
|
||||||
|
envOnlyProviderRouteId === 'api-route' && !useMiniMaxEnvOnlyProvider
|
||||||
if (useMiniMaxEnvOnlyProvider) applyMiniMaxEnvOnlyDefaults(model)
|
if (useMiniMaxEnvOnlyProvider) applyMiniMaxEnvOnlyDefaults(model)
|
||||||
if (useXiaomiMimoEnvOnlyProvider) applyXiaomiMimoEnvOnlyDefaults()
|
if (useXiaomiMimoEnvOnlyProvider) applyXiaomiMimoEnvOnlyDefaults()
|
||||||
if (useXaiEnvOnlyProvider) applyXaiEnvOnlyDefaults()
|
if (useXaiEnvOnlyProvider) applyXaiEnvOnlyDefaults()
|
||||||
@@ -583,6 +619,7 @@ export async function getAnthropicClient({
|
|||||||
if (useAimlapiEnvOnlyProvider) applyAimlapiEnvOnlyDefaults()
|
if (useAimlapiEnvOnlyProvider) applyAimlapiEnvOnlyDefaults()
|
||||||
if (useApismartEnvOnlyProvider) applyApismartEnvOnlyDefaults()
|
if (useApismartEnvOnlyProvider) applyApismartEnvOnlyDefaults()
|
||||||
if (useConcentrateEnvOnlyProvider) applyConcentrateEnvOnlyDefaults()
|
if (useConcentrateEnvOnlyProvider) applyConcentrateEnvOnlyDefaults()
|
||||||
|
if (useApiRouteEnvOnlyProvider) applyApiRouteEnvOnlyDefaults()
|
||||||
|
|
||||||
const containerId = process.env.CLAUDE_CODE_CONTAINER_ID
|
const containerId = process.env.CLAUDE_CODE_CONTAINER_ID
|
||||||
const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID
|
const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const originalEnv = {
|
|||||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
OPENAI_API_KEYS: process.env.OPENAI_API_KEYS,
|
OPENAI_API_KEYS: process.env.OPENAI_API_KEYS,
|
||||||
LLMTR_API_KEY: process.env.LLMTR_API_KEY,
|
LLMTR_API_KEY: process.env.LLMTR_API_KEY,
|
||||||
|
API_ROUTE_API_KEY: process.env.API_ROUTE_API_KEY,
|
||||||
CMD_API_KEY: process.env.CMD_API_KEY,
|
CMD_API_KEY: process.env.CMD_API_KEY,
|
||||||
COMMANDCODE_API_KEY: process.env.COMMANDCODE_API_KEY,
|
COMMANDCODE_API_KEY: process.env.COMMANDCODE_API_KEY,
|
||||||
COMMAND_CODE_API_KEY: process.env.COMMAND_CODE_API_KEY,
|
COMMAND_CODE_API_KEY: process.env.COMMAND_CODE_API_KEY,
|
||||||
@@ -455,6 +456,7 @@ beforeEach(async () => {
|
|||||||
process.env.OPENAI_API_KEY = 'test-key'
|
process.env.OPENAI_API_KEY = 'test-key'
|
||||||
delete process.env.OPENAI_API_KEYS
|
delete process.env.OPENAI_API_KEYS
|
||||||
delete process.env.LLMTR_API_KEY
|
delete process.env.LLMTR_API_KEY
|
||||||
|
delete process.env.API_ROUTE_API_KEY
|
||||||
delete process.env.CMD_API_KEY
|
delete process.env.CMD_API_KEY
|
||||||
delete process.env.COMMANDCODE_API_KEY
|
delete process.env.COMMANDCODE_API_KEY
|
||||||
delete process.env.COMMAND_CODE_API_KEY
|
delete process.env.COMMAND_CODE_API_KEY
|
||||||
@@ -508,6 +510,7 @@ afterEach(() => {
|
|||||||
restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY)
|
restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY)
|
||||||
restoreEnv('OPENAI_API_KEYS', originalEnv.OPENAI_API_KEYS)
|
restoreEnv('OPENAI_API_KEYS', originalEnv.OPENAI_API_KEYS)
|
||||||
restoreEnv('LLMTR_API_KEY', originalEnv.LLMTR_API_KEY)
|
restoreEnv('LLMTR_API_KEY', originalEnv.LLMTR_API_KEY)
|
||||||
|
restoreEnv('API_ROUTE_API_KEY', originalEnv.API_ROUTE_API_KEY)
|
||||||
restoreEnv('CMD_API_KEY', originalEnv.CMD_API_KEY)
|
restoreEnv('CMD_API_KEY', originalEnv.CMD_API_KEY)
|
||||||
restoreEnv('COMMANDCODE_API_KEY', originalEnv.COMMANDCODE_API_KEY)
|
restoreEnv('COMMANDCODE_API_KEY', originalEnv.COMMANDCODE_API_KEY)
|
||||||
restoreEnv('COMMAND_CODE_API_KEY', originalEnv.COMMAND_CODE_API_KEY)
|
restoreEnv('COMMAND_CODE_API_KEY', originalEnv.COMMAND_CODE_API_KEY)
|
||||||
@@ -588,6 +591,22 @@ test('Concentrate selection prefers its dedicated key over a generic OPENAI_API_
|
|||||||
expect(captured.authorization).toBe('Bearer concentrate-key')
|
expect(captured.authorization).toBe('Bearer concentrate-key')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('API Route selection prefers its dedicated key over a generic OPENAI_API_KEYS pool', async () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-key'
|
||||||
|
process.env.OPENAI_API_KEYS = 'generic-openai-key-a,generic-openai-key-b'
|
||||||
|
delete process.env.OPENAI_API_KEY
|
||||||
|
delete process.env.OPENAI_BASE_URL
|
||||||
|
delete process.env.OPENAI_MODEL
|
||||||
|
|
||||||
|
const result = applyProviderFlag('api-route', [])
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
|
||||||
|
const captured = await captureChatCompletionRequest('claude-sonnet-4-6')
|
||||||
|
|
||||||
|
expect(captured.url).toBe('https://global.api-route.com/v1/chat/completions')
|
||||||
|
expect(captured.authorization).toBe('Bearer api-route-key')
|
||||||
|
})
|
||||||
|
|
||||||
test('selected LLMTR route sends LLMTR_API_KEY through the generic route credential resolver', async () => {
|
test('selected LLMTR route sends LLMTR_API_KEY through the generic route credential resolver', async () => {
|
||||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||||
delete process.env.OPENAI_API_KEYS
|
delete process.env.OPENAI_API_KEYS
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ export async function executeOpenAIRequest(
|
|||||||
requestProcessEnv.ATLAS_CLOUD_API_KEY,
|
requestProcessEnv.ATLAS_CLOUD_API_KEY,
|
||||||
requestProcessEnv.APISMART_API_KEY,
|
requestProcessEnv.APISMART_API_KEY,
|
||||||
requestProcessEnv.CONCENTRATE_API_KEY,
|
requestProcessEnv.CONCENTRATE_API_KEY,
|
||||||
|
requestProcessEnv.API_ROUTE_API_KEY,
|
||||||
requestProcessEnv.NEARAI_API_KEY,
|
requestProcessEnv.NEARAI_API_KEY,
|
||||||
requestProcessEnv.FIREWORKS_API_KEY,
|
requestProcessEnv.FIREWORKS_API_KEY,
|
||||||
requestProcessEnv.LONGCAT_API_KEY,
|
requestProcessEnv.LONGCAT_API_KEY,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const TEST_ENV_KEYS = [
|
|||||||
'COMMAND_CODE_API_KEY',
|
'COMMAND_CODE_API_KEY',
|
||||||
'APISMART_API_KEY',
|
'APISMART_API_KEY',
|
||||||
'APISMART_MODEL',
|
'APISMART_MODEL',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'OPENAI_API_KEYS',
|
'OPENAI_API_KEYS',
|
||||||
'OPENAI_API_KEY',
|
'OPENAI_API_KEY',
|
||||||
'OPENAI_AZURE_STYLE',
|
'OPENAI_AZURE_STYLE',
|
||||||
@@ -298,6 +300,49 @@ describe('loadEnvFile', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('loads documented API Route env-only provider setup values', () => {
|
||||||
|
const filePath = writeTempEnvFile([
|
||||||
|
'API_ROUTE_API_KEY=api-route-key',
|
||||||
|
'API_ROUTE_MODEL=claude-sonnet-4-6',
|
||||||
|
].join('\n'))
|
||||||
|
|
||||||
|
const loaded = loadEnvFile(filePath)
|
||||||
|
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBe('api-route-key')
|
||||||
|
expect(process.env.API_ROUTE_MODEL).toBe('claude-sonnet-4-6')
|
||||||
|
expect(loaded).toEqual({
|
||||||
|
API_ROUTE_API_KEY: 'api-route-key',
|
||||||
|
API_ROUTE_MODEL: 'claude-sonnet-4-6',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves existing API Route env values over provider env-file values', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'shell-api-route-key'
|
||||||
|
process.env.API_ROUTE_MODEL = 'shell-api-route-model'
|
||||||
|
const filePath = writeTempEnvFile([
|
||||||
|
'API_ROUTE_API_KEY=file-api-route-key',
|
||||||
|
'API_ROUTE_MODEL=file-api-route-model',
|
||||||
|
].join('\n'))
|
||||||
|
|
||||||
|
const loaded = loadEnvFile(filePath)
|
||||||
|
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBe('shell-api-route-key')
|
||||||
|
expect(process.env.API_ROUTE_MODEL).toBe('shell-api-route-model')
|
||||||
|
expect(loaded).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects API Route env files atomically when any variable is unsupported', () => {
|
||||||
|
const filePath = writeTempEnvFile([
|
||||||
|
'API_ROUTE_API_KEY=api-route-key',
|
||||||
|
'UNSAFE_API_ROUTE_SETTING=blocked',
|
||||||
|
].join('\n'))
|
||||||
|
|
||||||
|
expect(() => loadEnvFile(filePath)).toThrow(
|
||||||
|
'Unsupported variable UNSAFE_API_ROUTE_SETTING in --provider-env-file',
|
||||||
|
)
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('loads the dedicated LLMTR credential without selecting a route', () => {
|
it('loads the dedicated LLMTR credential without selecting a route', () => {
|
||||||
const filePath = writeTempEnvFile('LLMTR_API_KEY=llmtr-key')
|
const filePath = writeTempEnvFile('LLMTR_API_KEY=llmtr-key')
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const ALLOWED_ENV_FILE_KEYS = new Set([
|
|||||||
'ATLAS_CLOUD_API_KEY',
|
'ATLAS_CLOUD_API_KEY',
|
||||||
'APISMART_API_KEY',
|
'APISMART_API_KEY',
|
||||||
'APISMART_MODEL',
|
'APISMART_MODEL',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'AWS_BEARER_TOKEN_BEDROCK',
|
'AWS_BEARER_TOKEN_BEDROCK',
|
||||||
'AWS_DEFAULT_REGION',
|
'AWS_DEFAULT_REGION',
|
||||||
'AWS_PROFILE',
|
'AWS_PROFILE',
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import {
|
|||||||
loadEnvFile,
|
loadEnvFile,
|
||||||
rememberLoadedEnvFileValues,
|
rememberLoadedEnvFileValues,
|
||||||
} from './envFile.js'
|
} from './envFile.js'
|
||||||
import { applyConfigEnvironmentVariables } from './managedEnv.js'
|
import {
|
||||||
|
applyConfigEnvironmentVariables,
|
||||||
|
applySafeConfigEnvironmentVariables,
|
||||||
|
} from './managedEnv.js'
|
||||||
|
|
||||||
const ENV_KEYS = [
|
const ENV_KEYS = [
|
||||||
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
|
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
|
||||||
@@ -28,6 +31,8 @@ const ENV_KEYS = [
|
|||||||
'CMD_API_KEY',
|
'CMD_API_KEY',
|
||||||
'COMMANDCODE_API_KEY',
|
'COMMANDCODE_API_KEY',
|
||||||
'COMMAND_CODE_API_KEY',
|
'COMMAND_CODE_API_KEY',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'OPENAI_API_KEY',
|
'OPENAI_API_KEY',
|
||||||
'OPENAI_AZURE_STYLE',
|
'OPENAI_AZURE_STYLE',
|
||||||
'OPENAI_BASE_URL',
|
'OPENAI_BASE_URL',
|
||||||
@@ -95,6 +100,36 @@ function writeTempEnvFile(content: string): string {
|
|||||||
return filePath
|
return filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('host-managed API Route settings boundary', () => {
|
||||||
|
for (const [name, applySettings] of [
|
||||||
|
['pre-trust', applySafeConfigEnvironmentVariables],
|
||||||
|
['post-trust', applyConfigEnvironmentVariables],
|
||||||
|
] as const) {
|
||||||
|
it(`preserves host routing against API Route settings during ${name} merge`, () => {
|
||||||
|
process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = '1'
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1'
|
||||||
|
process.env.OPENAI_MODEL = 'host-model'
|
||||||
|
process.env.OPENAI_API_KEY = 'host-key'
|
||||||
|
saveGlobalConfig(current => ({
|
||||||
|
...current,
|
||||||
|
env: {
|
||||||
|
API_ROUTE_API_KEY: 'settings-api-route-key',
|
||||||
|
API_ROUTE_MODEL: 'settings-api-route-model',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
applySettings()
|
||||||
|
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBeUndefined()
|
||||||
|
expect(process.env.API_ROUTE_MODEL).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1')
|
||||||
|
expect(process.env.OPENAI_MODEL).toBe('host-model')
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('host-key')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
describe('applyConfigEnvironmentVariables', () => {
|
describe('applyConfigEnvironmentVariables', () => {
|
||||||
it('preserves the complete host-managed Command Code route against settings env', () => {
|
it('preserves the complete host-managed Command Code route against settings env', () => {
|
||||||
process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = '1'
|
process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = '1'
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
|||||||
'CMD_API_KEY',
|
'CMD_API_KEY',
|
||||||
'COMMANDCODE_API_KEY',
|
'COMMANDCODE_API_KEY',
|
||||||
'COMMAND_CODE_API_KEY',
|
'COMMAND_CODE_API_KEY',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
'OPENAI_API_KEY',
|
'OPENAI_API_KEY',
|
||||||
'OPENAI_API_KEYS',
|
'OPENAI_API_KEYS',
|
||||||
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
||||||
@@ -65,6 +66,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
|||||||
'ANTHROPIC_SMALL_FAST_MODEL',
|
'ANTHROPIC_SMALL_FAST_MODEL',
|
||||||
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
||||||
'CLAUDE_CODE_SUBAGENT_MODEL',
|
'CLAUDE_CODE_SUBAGENT_MODEL',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'OPENAI_MODEL',
|
'OPENAI_MODEL',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ const SAVED_ENV = {
|
|||||||
CONCENTRATE_API_KEY: process.env.CONCENTRATE_API_KEY,
|
CONCENTRATE_API_KEY: process.env.CONCENTRATE_API_KEY,
|
||||||
CONCENTRATE_BASE_URL: process.env.CONCENTRATE_BASE_URL,
|
CONCENTRATE_BASE_URL: process.env.CONCENTRATE_BASE_URL,
|
||||||
CONCENTRATE_MODEL: process.env.CONCENTRATE_MODEL,
|
CONCENTRATE_MODEL: process.env.CONCENTRATE_MODEL,
|
||||||
|
API_ROUTE_API_KEY: process.env.API_ROUTE_API_KEY,
|
||||||
|
API_ROUTE_MODEL: process.env.API_ROUTE_MODEL,
|
||||||
OPENAI_MODEL: process.env.OPENAI_MODEL,
|
OPENAI_MODEL: process.env.OPENAI_MODEL,
|
||||||
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
|
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
|
||||||
CODEX_API_KEY: process.env.CODEX_API_KEY,
|
CODEX_API_KEY: process.env.CODEX_API_KEY,
|
||||||
@@ -142,6 +144,8 @@ beforeEach(async () => {
|
|||||||
delete process.env.CONCENTRATE_API_KEY
|
delete process.env.CONCENTRATE_API_KEY
|
||||||
delete process.env.CONCENTRATE_BASE_URL
|
delete process.env.CONCENTRATE_BASE_URL
|
||||||
delete process.env.CONCENTRATE_MODEL
|
delete process.env.CONCENTRATE_MODEL
|
||||||
|
delete process.env.API_ROUTE_API_KEY
|
||||||
|
delete process.env.API_ROUTE_MODEL
|
||||||
delete process.env.OPENAI_MODEL
|
delete process.env.OPENAI_MODEL
|
||||||
delete process.env.OPENAI_BASE_URL
|
delete process.env.OPENAI_BASE_URL
|
||||||
delete process.env.CODEX_API_KEY
|
delete process.env.CODEX_API_KEY
|
||||||
@@ -434,6 +438,39 @@ test.each(['null', 'undefined', ' '])(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test('API Route selects its dedicated model before client normalization', async () => {
|
||||||
|
saveGlobalConfig(current => ({ ...current, model: 'stale-other-provider-model' }))
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-test'
|
||||||
|
process.env.API_ROUTE_MODEL = 'custom-api-route-model'
|
||||||
|
|
||||||
|
const {
|
||||||
|
getDefaultMainLoopModelSetting,
|
||||||
|
getMainLoopModel,
|
||||||
|
getUserSpecifiedModelSetting,
|
||||||
|
} = await importFreshModelModule()
|
||||||
|
expect(getUserSpecifiedModelSetting()).toBe('custom-api-route-model')
|
||||||
|
expect(getDefaultMainLoopModelSetting()).toBe('custom-api-route-model')
|
||||||
|
expect(getMainLoopModel()).toBe('custom-api-route-model')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('API Route honors its legacy OpenAI model fallback before client normalization', async () => {
|
||||||
|
saveGlobalConfig(current => ({ ...current, model: 'stale-other-provider-model' }))
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-test'
|
||||||
|
process.env.OPENAI_MODEL = 'legacy-api-route-model'
|
||||||
|
|
||||||
|
const { getMainLoopModel, getUserSpecifiedModelSetting } =
|
||||||
|
await importFreshModelModule()
|
||||||
|
expect(getUserSpecifiedModelSetting()).toBe('legacy-api-route-model')
|
||||||
|
expect(getMainLoopModel()).toBe('legacy-api-route-model')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('API Route uses its descriptor default before client normalization', async () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-test'
|
||||||
|
|
||||||
|
const { getDefaultMainLoopModelSetting } = await importFreshModelModule()
|
||||||
|
expect(getDefaultMainLoopModelSetting()).toBe('claude-sonnet-4-6')
|
||||||
|
})
|
||||||
|
|
||||||
test('getDefaultMainLoopModelSetting uses the NVIDIA NIM route model', async () => {
|
test('getDefaultMainLoopModelSetting uses the NVIDIA NIM route model', async () => {
|
||||||
process.env.NVIDIA_NIM = '1'
|
process.env.NVIDIA_NIM = '1'
|
||||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
|||||||
@@ -69,6 +69,17 @@ function getAllowedConcentrateConfigModel(): string | undefined {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllowedApiRouteConfigModel(): string | undefined {
|
||||||
|
for (const value of [
|
||||||
|
process.env.API_ROUTE_MODEL,
|
||||||
|
process.env.OPENAI_MODEL,
|
||||||
|
]) {
|
||||||
|
const model = getUsableProviderConfigModel(value)
|
||||||
|
if (model && isModelAllowed(model)) return model
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function getSmallFastModel(): ModelName {
|
export function getSmallFastModel(): ModelName {
|
||||||
if (process.env.ANTHROPIC_SMALL_FAST_MODEL) return process.env.ANTHROPIC_SMALL_FAST_MODEL
|
if (process.env.ANTHROPIC_SMALL_FAST_MODEL) return process.env.ANTHROPIC_SMALL_FAST_MODEL
|
||||||
if (isCustomAnthropicProvider()) {
|
if (isCustomAnthropicProvider()) {
|
||||||
@@ -173,7 +184,9 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined {
|
|||||||
// consume its dedicated setting here rather than falling through to a
|
// consume its dedicated setting here rather than falling through to a
|
||||||
// saved model from an unrelated provider.
|
// saved model from an unrelated provider.
|
||||||
? getAllowedConcentrateConfigModel()
|
? getAllowedConcentrateConfigModel()
|
||||||
: (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) ||
|
: activeRouteId === 'api-route'
|
||||||
|
? getAllowedApiRouteConfigModel()
|
||||||
|
: (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) ||
|
||||||
(provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) ||
|
(provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) ||
|
||||||
(provider === 'minimax' ? getMiniMaxModelEnv() : undefined) ||
|
(provider === 'minimax' ? getMiniMaxModelEnv() : undefined) ||
|
||||||
(isOpenAIShimProvider ? process.env.OPENAI_MODEL : undefined) ||
|
(isOpenAIShimProvider ? process.env.OPENAI_MODEL : undefined) ||
|
||||||
@@ -404,6 +417,13 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
|||||||
'deepseek-v4-flash'
|
'deepseek-v4-flash'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (resolveActiveRouteIdFromEnv(process.env) === 'api-route') {
|
||||||
|
return (
|
||||||
|
getAllowedApiRouteConfigModel() ||
|
||||||
|
getRouteDefaultModel('api-route') ||
|
||||||
|
'claude-sonnet-4-6'
|
||||||
|
)
|
||||||
|
}
|
||||||
// Custom Anthropic-compatible endpoints intentionally retain the legacy
|
// Custom Anthropic-compatible endpoints intentionally retain the legacy
|
||||||
// firstParty provider category, so prefer their explicitly configured model
|
// firstParty provider category, so prefer their explicitly configured model
|
||||||
// before the subscription and PAYG defaults below.
|
// before the subscription and PAYG defaults below.
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ const ENV_KEYS = [
|
|||||||
'ATLAS_CLOUD_API_KEY',
|
'ATLAS_CLOUD_API_KEY',
|
||||||
'APISMART_API_KEY',
|
'APISMART_API_KEY',
|
||||||
'APISMART_MODEL',
|
'APISMART_MODEL',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'CONCENTRATE_API_KEY',
|
'CONCENTRATE_API_KEY',
|
||||||
'CONCENTRATE_BASE_URL',
|
'CONCENTRATE_BASE_URL',
|
||||||
'CONCENTRATE_MODEL',
|
'CONCENTRATE_MODEL',
|
||||||
@@ -101,6 +103,8 @@ const RESET_KEYS = [
|
|||||||
'ATLAS_CLOUD_API_KEY',
|
'ATLAS_CLOUD_API_KEY',
|
||||||
'APISMART_API_KEY',
|
'APISMART_API_KEY',
|
||||||
'APISMART_MODEL',
|
'APISMART_MODEL',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'CONCENTRATE_API_KEY',
|
'CONCENTRATE_API_KEY',
|
||||||
'CONCENTRATE_BASE_URL',
|
'CONCENTRATE_BASE_URL',
|
||||||
'CONCENTRATE_MODEL',
|
'CONCENTRATE_MODEL',
|
||||||
@@ -1848,3 +1852,150 @@ describe('applyProviderFlag - concentrate', () => {
|
|||||||
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined()
|
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('applyProviderFlag - api-route', () => {
|
||||||
|
test('sets API Route OpenAI-compatible defaults and mirrors API_ROUTE_API_KEY', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
|
||||||
|
const result = applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1')
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe('https://global.api-route.com/v1')
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('api-route-secret-key')
|
||||||
|
expect(process.env.OPENAI_MODEL).toBe('claude-sonnet-4-6')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('uses API_ROUTE_MODEL from env when --model is not provided', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
process.env.API_ROUTE_MODEL = 'custom-api-route-model'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_MODEL).toBe('custom-api-route-model')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('explicit --model overrides API_ROUTE_MODEL', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
process.env.API_ROUTE_MODEL = 'custom-api-route-model'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', ['--model', 'flag-model'])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_MODEL).toBe('flag-model')
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each(['', ' ', 'null', 'undefined', ' NULL '])(
|
||||||
|
'does not contaminate OPENAI_MODEL with unusable API_ROUTE_MODEL %j',
|
||||||
|
placeholder => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
process.env.API_ROUTE_MODEL = placeholder
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_MODEL).toBe('claude-sonnet-4-6')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
test('does not forward the dedicated key to a preserved custom base URL', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
process.env.OPENAI_BASE_URL = 'https://custom-proxy.example.com/v1'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe(
|
||||||
|
'https://custom-proxy.example.com/v1',
|
||||||
|
)
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('dedicated key overrides a lingering OPENAI_API_KEY from another provider', () => {
|
||||||
|
process.env.OPENAI_API_KEY = 'existing-openai-key'
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('api-route-secret-key')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('clears OPENAI_API_KEYS when dedicated API Route key is set on canonical endpoint', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
process.env.OPENAI_API_KEYS = 'stale-key-1,stale-key-2'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('api-route-secret-key')
|
||||||
|
expect(process.env.OPENAI_API_KEYS).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('clears a stale OPENAI_API_KEY when no API Route key is set', () => {
|
||||||
|
delete process.env.API_ROUTE_API_KEY
|
||||||
|
process.env.OPENAI_API_KEY = 'existing-openai-key'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])(
|
||||||
|
'does not mirror placeholder API Route credential %s',
|
||||||
|
placeholder => {
|
||||||
|
process.env.API_ROUTE_API_KEY = placeholder
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
test('clears unsupported OpenAI shim settings from a previous route', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
process.env.OPENAI_API_FORMAT = 'responses'
|
||||||
|
process.env.OPENAI_AUTH_HEADER = 'x-api-key'
|
||||||
|
process.env.OPENAI_AUTH_SCHEME = 'raw'
|
||||||
|
process.env.OPENAI_AUTH_HEADER_VALUE = 'stale-value'
|
||||||
|
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Proxy-Auth: proxy-secret'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_API_FORMAT).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_AUTH_HEADER).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_AUTH_SCHEME).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBeUndefined()
|
||||||
|
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('clears a copied API Route key from OPENAI_API_KEY when switching to another provider', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
process.env.OPENAI_API_KEY = 'api-route-secret-key'
|
||||||
|
|
||||||
|
applyProviderFlag('openai', [])
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('switching from API Route to Ollama replaces the route identity and credentials', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
applyProviderFlag('ollama', [])
|
||||||
|
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBeUndefined()
|
||||||
|
expect(process.env.API_ROUTE_MODEL).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe('http://localhost:11434/v1')
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('ollama')
|
||||||
|
expect(resolveActiveRouteIdFromEnv(process.env)).toBe('ollama')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('switching from API Route to OpenAI replaces the known gateway endpoint', () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-secret-key'
|
||||||
|
|
||||||
|
applyProviderFlag('api-route', [])
|
||||||
|
applyProviderFlag('openai', [])
|
||||||
|
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1')
|
||||||
|
expect(resolveActiveRouteIdFromEnv(process.env)).toBe('openai')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import { PRESET_VENDOR_MAP } from '../integrations/compatibility.js'
|
import { PRESET_VENDOR_MAP } from '../integrations/compatibility.js'
|
||||||
import { getCommandcodeChatCompletionsModelError } from '../integrations/gateways/commandcode.js'
|
import { getCommandcodeChatCompletionsModelError } from '../integrations/gateways/commandcode.js'
|
||||||
import {
|
import {
|
||||||
|
isCanonicalApiRouteInferenceBaseUrl,
|
||||||
isCanonicalApismartInferenceBaseUrl,
|
isCanonicalApismartInferenceBaseUrl,
|
||||||
isCanonicalConcentrateInferenceBaseUrl,
|
isCanonicalConcentrateInferenceBaseUrl,
|
||||||
isCanonicalLlmtrInferenceBaseUrl,
|
isCanonicalLlmtrInferenceBaseUrl,
|
||||||
@@ -401,6 +402,9 @@ export function applyProviderFlag(
|
|||||||
: process.env.OPENAI_API_KEY !== undefined &&
|
: process.env.OPENAI_API_KEY !== undefined &&
|
||||||
process.env.OPENAI_API_KEY === process.env.APISMART_API_KEY
|
process.env.OPENAI_API_KEY === process.env.APISMART_API_KEY
|
||||||
? 'apismart'
|
? 'apismart'
|
||||||
|
: process.env.OPENAI_API_KEY !== undefined &&
|
||||||
|
process.env.OPENAI_API_KEY === process.env.API_ROUTE_API_KEY
|
||||||
|
? 'api-route'
|
||||||
: process.env.OPENAI_API_KEY !== undefined &&
|
: process.env.OPENAI_API_KEY !== undefined &&
|
||||||
process.env.OPENAI_API_KEY === process.env.CONCENTRATE_API_KEY
|
process.env.OPENAI_API_KEY === process.env.CONCENTRATE_API_KEY
|
||||||
? 'concentrate'
|
? 'concentrate'
|
||||||
@@ -470,6 +474,8 @@ export function applyProviderFlag(
|
|||||||
// OpenAI-compatible env-only route override it later in startup.
|
// OpenAI-compatible env-only route override it later in startup.
|
||||||
delete process.env.APISMART_API_KEY
|
delete process.env.APISMART_API_KEY
|
||||||
delete process.env.APISMART_MODEL
|
delete process.env.APISMART_MODEL
|
||||||
|
delete process.env.API_ROUTE_API_KEY
|
||||||
|
delete process.env.API_ROUTE_MODEL
|
||||||
delete process.env.ANTHROPIC_AUTH_TOKEN
|
delete process.env.ANTHROPIC_AUTH_TOKEN
|
||||||
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||||
break
|
break
|
||||||
@@ -517,6 +523,8 @@ export function applyProviderFlag(
|
|||||||
// known gateway endpoint, but preserve a user-supplied custom endpoint.
|
// known gateway endpoint, but preserve a user-supplied custom endpoint.
|
||||||
delete process.env.APISMART_API_KEY
|
delete process.env.APISMART_API_KEY
|
||||||
delete process.env.APISMART_MODEL
|
delete process.env.APISMART_MODEL
|
||||||
|
delete process.env.API_ROUTE_API_KEY
|
||||||
|
delete process.env.API_ROUTE_MODEL
|
||||||
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
|
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
|
||||||
if (effectiveModel) process.env.OPENAI_MODEL = effectiveModel
|
if (effectiveModel) process.env.OPENAI_MODEL = effectiveModel
|
||||||
break
|
break
|
||||||
@@ -888,7 +896,9 @@ export function applyProviderFlag(
|
|||||||
(provider === 'llmtr' &&
|
(provider === 'llmtr' &&
|
||||||
isCanonicalLlmtrInferenceBaseUrl(getConfiguredOpenAIBaseUrl())) ||
|
isCanonicalLlmtrInferenceBaseUrl(getConfiguredOpenAIBaseUrl())) ||
|
||||||
(provider === 'commandcode' &&
|
(provider === 'commandcode' &&
|
||||||
isCanonicalCommandcodeInferenceBaseUrl(getConfiguredOpenAIBaseUrl()))
|
isCanonicalCommandcodeInferenceBaseUrl(getConfiguredOpenAIBaseUrl())) ||
|
||||||
|
(provider === 'api-route' &&
|
||||||
|
isCanonicalApiRouteInferenceBaseUrl(getConfiguredOpenAIBaseUrl()))
|
||||||
) {
|
) {
|
||||||
clearUnsupportedOpenAIShimSettings(provider)
|
clearUnsupportedOpenAIShimSettings(provider)
|
||||||
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||||
@@ -918,6 +928,35 @@ export function applyProviderFlag(
|
|||||||
delete process.env.OPENAI_API_KEY
|
delete process.env.OPENAI_API_KEY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
provider === 'api-route' &&
|
||||||
|
isCanonicalApiRouteInferenceBaseUrl(getConfiguredOpenAIBaseUrl())
|
||||||
|
) {
|
||||||
|
const dedicatedApiRouteKey = hasUsableOpenAICredential(
|
||||||
|
process.env.API_ROUTE_API_KEY,
|
||||||
|
)
|
||||||
|
? process.env.API_ROUTE_API_KEY
|
||||||
|
: undefined
|
||||||
|
if (dedicatedApiRouteKey) {
|
||||||
|
process.env.OPENAI_API_KEY = dedicatedApiRouteKey
|
||||||
|
delete process.env.OPENAI_API_KEYS
|
||||||
|
} else {
|
||||||
|
delete process.env.OPENAI_API_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (provider === 'api-route') {
|
||||||
|
const apiRouteModel = usableProviderModelEnvValue(
|
||||||
|
process.env.API_ROUTE_MODEL,
|
||||||
|
)
|
||||||
|
if (apiRouteModel) {
|
||||||
|
process.env.OPENAI_MODEL = apiRouteModel
|
||||||
|
} else {
|
||||||
|
process.env.OPENAI_MODEL ??=
|
||||||
|
usableProviderModelEnvValue(process.env.OPENAI_MODEL) ||
|
||||||
|
defaultModel ||
|
||||||
|
'claude-sonnet-4-6'
|
||||||
|
}
|
||||||
|
}
|
||||||
if (defaultModel) {
|
if (defaultModel) {
|
||||||
process.env.OPENAI_MODEL ??= defaultModel
|
process.env.OPENAI_MODEL ??= defaultModel
|
||||||
}
|
}
|
||||||
@@ -925,9 +964,9 @@ export function applyProviderFlag(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// A provider flag selects a complete route for this process. Concentrate's
|
// A provider flag selects a complete route for this process. Dedicated
|
||||||
// dedicated variables are another source of route identity, so leaving them
|
// variables are another source of route identity, so leaving them
|
||||||
// behind can re-select Concentrate after a later OpenAI-compatible provider
|
// behind can re-select a provider after a later OpenAI-compatible provider
|
||||||
// has applied its defaults. Keep their lifecycle at the selection boundary,
|
// has applied its defaults. Keep their lifecycle at the selection boundary,
|
||||||
// rather than relying on individual switch branches to remember cleanup.
|
// rather than relying on individual switch branches to remember cleanup.
|
||||||
// This runs only after the selected branch succeeds, so an invalid
|
// This runs only after the selected branch succeeds, so an invalid
|
||||||
@@ -935,6 +974,10 @@ export function applyProviderFlag(
|
|||||||
if (provider !== 'concentrate') {
|
if (provider !== 'concentrate') {
|
||||||
clearConcentrateProviderState()
|
clearConcentrateProviderState()
|
||||||
}
|
}
|
||||||
|
if (provider !== 'api-route') {
|
||||||
|
delete process.env.API_ROUTE_API_KEY
|
||||||
|
delete process.env.API_ROUTE_MODEL
|
||||||
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3405,6 +3405,37 @@ test('openai launch removes legacy generic credentials from a noncanonical Conce
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('API Route saved profile relaunch uses persisted generic key/model over ambient dedicated state', async () => {
|
||||||
|
const env = await buildLaunchEnv({
|
||||||
|
profile: 'openai',
|
||||||
|
persisted: profile('openai', {
|
||||||
|
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'api-route',
|
||||||
|
OPENAI_BASE_URL: 'https://global.api-route.com/v1',
|
||||||
|
OPENAI_MODEL: 'saved-api-route-model',
|
||||||
|
OPENAI_API_KEY: 'saved-api-route-key',
|
||||||
|
}),
|
||||||
|
goal: 'balanced',
|
||||||
|
processEnv: {
|
||||||
|
API_ROUTE_API_KEY: 'ambient-api-route-key',
|
||||||
|
API_ROUTE_MODEL: 'ambient-api-route-model',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(env.API_ROUTE_API_KEY, undefined)
|
||||||
|
assert.equal(env.API_ROUTE_MODEL, undefined)
|
||||||
|
assert.equal(env.OPENAI_BASE_URL, 'https://global.api-route.com/v1')
|
||||||
|
assert.equal(env.OPENAI_MODEL, 'saved-api-route-model')
|
||||||
|
assert.equal(env.OPENAI_API_KEY, 'saved-api-route-key')
|
||||||
|
assert.equal(
|
||||||
|
resolveRouteCredentialValue({
|
||||||
|
routeId: 'api-route',
|
||||||
|
baseUrl: env.OPENAI_BASE_URL,
|
||||||
|
processEnv: env,
|
||||||
|
}),
|
||||||
|
'saved-api-route-key',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('buildStartupEnvFromProfile preserves Concentrate env-only setup over a saved profile', async () => {
|
test('buildStartupEnvFromProfile preserves Concentrate env-only setup over a saved profile', async () => {
|
||||||
const env = await buildStartupEnvFromProfile({
|
const env = await buildStartupEnvFromProfile({
|
||||||
persisted: profile('openai', {
|
persisted: profile('openai', {
|
||||||
@@ -3427,6 +3458,50 @@ test('buildStartupEnvFromProfile preserves Concentrate env-only setup over a sav
|
|||||||
assert.equal(resolveActiveRouteIdFromEnv(env), 'concentrate')
|
assert.equal(resolveActiveRouteIdFromEnv(env), 'concentrate')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('applyStartupEnvFromProfile preserves API Route env-only setup over a saved profile', async () => {
|
||||||
|
const processEnv: NodeJS.ProcessEnv = {
|
||||||
|
API_ROUTE_API_KEY: 'api-route-env-only-key',
|
||||||
|
API_ROUTE_MODEL: 'api-route-env-only-model',
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = await applyStartupEnvFromProfile({
|
||||||
|
persisted: profile('openai', {
|
||||||
|
OPENAI_BASE_URL: 'https://api.openai.com/v1',
|
||||||
|
OPENAI_MODEL: 'gpt-4o',
|
||||||
|
OPENAI_API_KEY: 'sk-persisted',
|
||||||
|
}),
|
||||||
|
processEnv,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(error, null)
|
||||||
|
assert.equal(processEnv.API_ROUTE_API_KEY, 'api-route-env-only-key')
|
||||||
|
assert.equal(processEnv.API_ROUTE_MODEL, 'api-route-env-only-model')
|
||||||
|
assert.equal(processEnv.OPENAI_BASE_URL, undefined)
|
||||||
|
assert.equal(processEnv.OPENAI_API_KEY, undefined)
|
||||||
|
assert.equal(resolveActiveRouteIdFromEnv(processEnv), 'api-route')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('applyStartupEnvFromProfile preserves API Route key-only descriptor-default intent', async () => {
|
||||||
|
const processEnv: NodeJS.ProcessEnv = {
|
||||||
|
API_ROUTE_API_KEY: 'api-route-env-only-key',
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = await applyStartupEnvFromProfile({
|
||||||
|
persisted: profile('openai', {
|
||||||
|
OPENAI_BASE_URL: 'https://api.openai.com/v1',
|
||||||
|
OPENAI_MODEL: 'gpt-4o',
|
||||||
|
OPENAI_API_KEY: 'sk-persisted',
|
||||||
|
}),
|
||||||
|
processEnv,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(error, null)
|
||||||
|
assert.equal(processEnv.API_ROUTE_API_KEY, 'api-route-env-only-key')
|
||||||
|
assert.equal(processEnv.OPENAI_MODEL, undefined)
|
||||||
|
assert.equal(resolveActiveRouteIdFromEnv(processEnv), 'api-route')
|
||||||
|
assert.equal(await getProviderValidationError(processEnv), null)
|
||||||
|
})
|
||||||
|
|
||||||
test('buildConcentrateProfileEnv prefers CONCENTRATE_MODEL over OPENAI_MODEL', () => {
|
test('buildConcentrateProfileEnv prefers CONCENTRATE_MODEL over OPENAI_MODEL', () => {
|
||||||
const env = buildConcentrateProfileEnv({
|
const env = buildConcentrateProfileEnv({
|
||||||
apiKey: 'concentrate-secret-key',
|
apiKey: 'concentrate-secret-key',
|
||||||
@@ -3476,3 +3551,64 @@ test('buildOpenAIProfileEnv does not stamp a generic key as Concentrate credenti
|
|||||||
assert.equal(env?.OPENAI_API_KEY, 'generic-proxy-key')
|
assert.equal(env?.OPENAI_API_KEY, 'generic-proxy-key')
|
||||||
assert.equal(env?.CONCENTRATE_API_KEY, undefined)
|
assert.equal(env?.CONCENTRATE_API_KEY, undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('openai launch withholds ambient API Route credentials from a keyless proxy profile on restart', async () => {
|
||||||
|
for (const ambient of [
|
||||||
|
{ OPENAI_API_KEY: 'ambient-api-route-key' },
|
||||||
|
{ OPENAI_API_KEYS: 'ambient-key-a,ambient-key-b' },
|
||||||
|
]) {
|
||||||
|
const env = await buildLaunchEnv({
|
||||||
|
profile: 'openai',
|
||||||
|
persisted: profile('openai', {
|
||||||
|
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'api-route',
|
||||||
|
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
|
||||||
|
OPENAI_MODEL: 'claude-sonnet-4-6',
|
||||||
|
}),
|
||||||
|
goal: 'coding',
|
||||||
|
processEnv: {
|
||||||
|
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
|
||||||
|
...ambient,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'api-route')
|
||||||
|
assert.equal(env.OPENAI_API_KEY, undefined)
|
||||||
|
assert.equal(env.OPENAI_API_KEYS, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canonical = await buildLaunchEnv({
|
||||||
|
profile: 'openai',
|
||||||
|
persisted: profile('openai', {
|
||||||
|
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'api-route',
|
||||||
|
OPENAI_BASE_URL: 'https://global.api-route.com/v1',
|
||||||
|
OPENAI_MODEL: 'claude-sonnet-4-6',
|
||||||
|
}),
|
||||||
|
goal: 'coding',
|
||||||
|
processEnv: {
|
||||||
|
OPENAI_BASE_URL: 'https://global.api-route.com/v1',
|
||||||
|
OPENAI_API_KEY: 'ambient-api-route-key',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.equal(canonical.OPENAI_API_KEY, 'ambient-api-route-key')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('buildStartupEnvFromProfile applies saved API Route proxy profile even when ambient credentials are present', async () => {
|
||||||
|
const processEnv: NodeJS.ProcessEnv = {
|
||||||
|
OPENAI_API_KEY: 'ambient-key',
|
||||||
|
OPENAI_API_KEYS: 'ambient-key-1,ambient-key-2',
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = await buildStartupEnvFromProfile({
|
||||||
|
persisted: profile('openai', {
|
||||||
|
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'api-route',
|
||||||
|
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
|
||||||
|
OPENAI_MODEL: 'claude-sonnet-4-6',
|
||||||
|
}),
|
||||||
|
processEnv,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(env.OPENAI_BASE_URL, 'https://proxy.example.com/v1')
|
||||||
|
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'api-route')
|
||||||
|
assert.equal(env.OPENAI_API_KEY, undefined)
|
||||||
|
assert.equal(env.OPENAI_API_KEYS, undefined)
|
||||||
|
})
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { getErrnoCode } from './errors.js'
|
|||||||
import {
|
import {
|
||||||
getRouteDefaultBaseUrl,
|
getRouteDefaultBaseUrl,
|
||||||
getRouteDefaultModel,
|
getRouteDefaultModel,
|
||||||
|
isCanonicalApiRouteInferenceBaseUrl,
|
||||||
isCanonicalApismartInferenceBaseUrl,
|
isCanonicalApismartInferenceBaseUrl,
|
||||||
isCanonicalConcentrateInferenceBaseUrl,
|
isCanonicalConcentrateInferenceBaseUrl,
|
||||||
isCanonicalLlmtrInferenceBaseUrl,
|
isCanonicalLlmtrInferenceBaseUrl,
|
||||||
@@ -118,6 +119,8 @@ const PROFILE_ENV_KEYS = [
|
|||||||
'ATLAS_CLOUD_API_KEY',
|
'ATLAS_CLOUD_API_KEY',
|
||||||
'APISMART_API_KEY',
|
'APISMART_API_KEY',
|
||||||
'APISMART_MODEL',
|
'APISMART_MODEL',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'NEARAI_API_KEY',
|
'NEARAI_API_KEY',
|
||||||
'FIREWORKS_API_KEY',
|
'FIREWORKS_API_KEY',
|
||||||
'LONGCAT_API_KEY',
|
'LONGCAT_API_KEY',
|
||||||
@@ -219,6 +222,7 @@ export type ProfileEnv = {
|
|||||||
MIMO_API_KEY?: string
|
MIMO_API_KEY?: string
|
||||||
ATLAS_CLOUD_API_KEY?: string
|
ATLAS_CLOUD_API_KEY?: string
|
||||||
APISMART_API_KEY?: string
|
APISMART_API_KEY?: string
|
||||||
|
API_ROUTE_API_KEY?: string
|
||||||
CLINE_API_KEY?: string
|
CLINE_API_KEY?: string
|
||||||
NEARAI_API_KEY?: string
|
NEARAI_API_KEY?: string
|
||||||
FIREWORKS_API_KEY?: string
|
FIREWORKS_API_KEY?: string
|
||||||
@@ -1491,6 +1495,7 @@ function hasConcreteProviderSelection(
|
|||||||
// before the env-only resolver has a chance to see it.
|
// before the env-only resolver has a chance to see it.
|
||||||
return (
|
return (
|
||||||
sanitizeApiKey(processEnv.APISMART_API_KEY) !== undefined ||
|
sanitizeApiKey(processEnv.APISMART_API_KEY) !== undefined ||
|
||||||
|
sanitizeApiKey(processEnv.API_ROUTE_API_KEY) !== undefined ||
|
||||||
sanitizeApiKey(processEnv.FIREWORKS_API_KEY) !== undefined ||
|
sanitizeApiKey(processEnv.FIREWORKS_API_KEY) !== undefined ||
|
||||||
sanitizeApiKey(processEnv.NEARAI_API_KEY) !== undefined ||
|
sanitizeApiKey(processEnv.NEARAI_API_KEY) !== undefined ||
|
||||||
sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined ||
|
sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined ||
|
||||||
@@ -2181,12 +2186,17 @@ export async function buildLaunchEnv(options: {
|
|||||||
effectiveOpenAIRouteId === 'commandcode' &&
|
effectiveOpenAIRouteId === 'commandcode' &&
|
||||||
!!env.OPENAI_BASE_URL?.trim() &&
|
!!env.OPENAI_BASE_URL?.trim() &&
|
||||||
!isCanonicalCommandcodeInferenceBaseUrl(env.OPENAI_BASE_URL)
|
!isCanonicalCommandcodeInferenceBaseUrl(env.OPENAI_BASE_URL)
|
||||||
|
const isNoncanonicalApiRouteLaunch =
|
||||||
|
effectiveOpenAIRouteId === 'api-route' &&
|
||||||
|
!!env.OPENAI_BASE_URL?.trim() &&
|
||||||
|
!isCanonicalApiRouteInferenceBaseUrl(env.OPENAI_BASE_URL)
|
||||||
const isNoncanonicalDedicatedOpenAILaunch =
|
const isNoncanonicalDedicatedOpenAILaunch =
|
||||||
isNoncanonicalAimlapiLaunch ||
|
isNoncanonicalAimlapiLaunch ||
|
||||||
isNoncanonicalApismartLaunch ||
|
isNoncanonicalApismartLaunch ||
|
||||||
isNoncanonicalConcentrateLaunch ||
|
isNoncanonicalConcentrateLaunch ||
|
||||||
isNoncanonicalLlmtrLaunch ||
|
isNoncanonicalLlmtrLaunch ||
|
||||||
isNoncanonicalCommandcodeLaunch
|
isNoncanonicalCommandcodeLaunch ||
|
||||||
|
isNoncanonicalApiRouteLaunch
|
||||||
if (isNoncanonicalDedicatedOpenAILaunch) {
|
if (isNoncanonicalDedicatedOpenAILaunch) {
|
||||||
delete env.OPENAI_API_KEY
|
delete env.OPENAI_API_KEY
|
||||||
delete env.OPENAI_API_KEYS
|
delete env.OPENAI_API_KEYS
|
||||||
@@ -2510,12 +2520,18 @@ export async function buildStartupEnvFromProfile(options?: {
|
|||||||
persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'commandcode' &&
|
persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'commandcode' &&
|
||||||
!!persisted.env.OPENAI_BASE_URL?.trim() &&
|
!!persisted.env.OPENAI_BASE_URL?.trim() &&
|
||||||
!isCanonicalCommandcodeInferenceBaseUrl(persisted.env.OPENAI_BASE_URL)
|
!isCanonicalCommandcodeInferenceBaseUrl(persisted.env.OPENAI_BASE_URL)
|
||||||
|
const persistedApiRouteProxy =
|
||||||
|
persisted?.profile === 'openai' &&
|
||||||
|
persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'api-route' &&
|
||||||
|
!!persisted.env.OPENAI_BASE_URL?.trim() &&
|
||||||
|
!isCanonicalApiRouteInferenceBaseUrl(persisted.env.OPENAI_BASE_URL)
|
||||||
if (
|
if (
|
||||||
hasConcreteProviderSelection(processEnv) &&
|
hasConcreteProviderSelection(processEnv) &&
|
||||||
!persistedApismartProxy &&
|
!persistedApismartProxy &&
|
||||||
!persistedConcentrateProxy &&
|
!persistedConcentrateProxy &&
|
||||||
!persistedLlmtrProxy &&
|
!persistedLlmtrProxy &&
|
||||||
!persistedCommandcodeProxy
|
!persistedCommandcodeProxy &&
|
||||||
|
!persistedApiRouteProxy
|
||||||
) {
|
) {
|
||||||
return processEnv
|
return processEnv
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ async function importFreshProvidersModule() {
|
|||||||
|
|
||||||
const originalEnv = { ...process.env }
|
const originalEnv = { ...process.env }
|
||||||
const originalCwd = process.cwd()
|
const originalCwd = process.cwd()
|
||||||
|
const originalFetch = globalThis.fetch
|
||||||
|
|
||||||
const RESTORED_KEYS = [
|
const RESTORED_KEYS = [
|
||||||
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED',
|
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED',
|
||||||
@@ -73,6 +74,8 @@ const RESTORED_KEYS = [
|
|||||||
'ATLAS_CLOUD_API_KEY',
|
'ATLAS_CLOUD_API_KEY',
|
||||||
'APISMART_API_KEY',
|
'APISMART_API_KEY',
|
||||||
'APISMART_MODEL',
|
'APISMART_MODEL',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'LLMTR_API_KEY',
|
'LLMTR_API_KEY',
|
||||||
'CMD_API_KEY',
|
'CMD_API_KEY',
|
||||||
'COMMANDCODE_API_KEY',
|
'COMMANDCODE_API_KEY',
|
||||||
@@ -136,6 +139,7 @@ afterEach(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mock.restore()
|
mock.restore()
|
||||||
|
globalThis.fetch = originalFetch
|
||||||
mockConfigState = createMockConfigState()
|
mockConfigState = createMockConfigState()
|
||||||
process.chdir(originalCwd)
|
process.chdir(originalCwd)
|
||||||
if (testConfigDir) {
|
if (testConfigDir) {
|
||||||
@@ -297,6 +301,17 @@ function buildConcentrateProfile(overrides: Partial<ProviderProfile> = {}): Prov
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildApiRouteProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
|
||||||
|
return buildProfile({
|
||||||
|
provider: 'api-route',
|
||||||
|
name: 'API Route',
|
||||||
|
baseUrl: 'https://global.api-route.com/v1',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
apiKey: 'api-route-test-key',
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function buildLlmtrProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
|
function buildLlmtrProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
|
||||||
return buildProfile({
|
return buildProfile({
|
||||||
provider: 'llmtr',
|
provider: 'llmtr',
|
||||||
@@ -394,6 +409,147 @@ describe('applyProviderProfileToProcessEnv', () => {
|
|||||||
).toBe('selected-new')
|
).toBe('selected-new')
|
||||||
}, 20_000)
|
}, 20_000)
|
||||||
|
|
||||||
|
test('API Route saved profile mirrors its dedicated credential and route identity', async () => {
|
||||||
|
const { applyProviderProfileToProcessEnv } =
|
||||||
|
await importFreshProviderProfileModules()
|
||||||
|
|
||||||
|
applyProviderProfileToProcessEnv(
|
||||||
|
buildProfile({
|
||||||
|
provider: 'api-route',
|
||||||
|
name: 'API Route',
|
||||||
|
baseUrl: 'https://global.api-route.com/v1',
|
||||||
|
model: 'saved-api-route-model',
|
||||||
|
apiKey: 'saved-api-route-key',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBe('saved-api-route-key')
|
||||||
|
expect(process.env.API_ROUTE_MODEL).toBeUndefined()
|
||||||
|
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('api-route')
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe('https://global.api-route.com/v1')
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('saved-api-route-key')
|
||||||
|
expect(process.env.OPENAI_MODEL).toBe('saved-api-route-model')
|
||||||
|
expect(
|
||||||
|
resolveRouteCredentialValue({
|
||||||
|
routeId: 'api-route',
|
||||||
|
baseUrl: process.env.OPENAI_BASE_URL,
|
||||||
|
processEnv: process.env,
|
||||||
|
}),
|
||||||
|
).toBe('saved-api-route-key')
|
||||||
|
}, 20_000)
|
||||||
|
|
||||||
|
test('API Route saved profile clears competing dedicated env and uses its saved key/model', async () => {
|
||||||
|
const { applyProviderProfileToProcessEnv } =
|
||||||
|
await importFreshProviderProfileModules()
|
||||||
|
process.env.API_ROUTE_API_KEY = 'ambient-api-route-key'
|
||||||
|
process.env.API_ROUTE_MODEL = 'ambient-api-route-model'
|
||||||
|
|
||||||
|
applyProviderProfileToProcessEnv(
|
||||||
|
buildProfile({
|
||||||
|
provider: 'api-route',
|
||||||
|
name: 'API Route',
|
||||||
|
baseUrl: 'https://global.api-route.com/v1',
|
||||||
|
model: 'saved-api-route-model',
|
||||||
|
apiKey: 'saved-api-route-key',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBe('saved-api-route-key')
|
||||||
|
expect(process.env.API_ROUTE_MODEL).toBeUndefined()
|
||||||
|
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('api-route')
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('saved-api-route-key')
|
||||||
|
expect(process.env.OPENAI_MODEL).toBe('saved-api-route-model')
|
||||||
|
expect(
|
||||||
|
resolveRouteCredentialValue({
|
||||||
|
routeId: 'api-route',
|
||||||
|
baseUrl: process.env.OPENAI_BASE_URL,
|
||||||
|
processEnv: process.env,
|
||||||
|
}),
|
||||||
|
).toBe('saved-api-route-key')
|
||||||
|
}, 20_000)
|
||||||
|
|
||||||
|
test('switching away from API Route clears dedicated route state', async () => {
|
||||||
|
const { applyProviderProfileToProcessEnv } =
|
||||||
|
await importFreshProviderProfileModules()
|
||||||
|
process.env.API_ROUTE_API_KEY = 'ambient-api-route-key'
|
||||||
|
process.env.API_ROUTE_MODEL = 'ambient-api-route-model'
|
||||||
|
|
||||||
|
applyProviderProfileToProcessEnv(
|
||||||
|
buildProfile({
|
||||||
|
provider: 'anthropic',
|
||||||
|
name: 'Anthropic',
|
||||||
|
baseUrl: 'https://api.anthropic.com',
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
apiKey: 'anthropic-key',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBeUndefined()
|
||||||
|
expect(process.env.API_ROUTE_MODEL).toBeUndefined()
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||||
|
expect(process.env.ANTHROPIC_API_KEY).toBe('anthropic-key')
|
||||||
|
}, 20_000)
|
||||||
|
|
||||||
|
test('API Route profile without a base URL uses the canonical endpoint and ambient key', async () => {
|
||||||
|
const { applyProviderProfileToProcessEnv } =
|
||||||
|
await importFreshProviderProfileModules()
|
||||||
|
process.env.API_ROUTE_API_KEY = 'ambient-api-route-key'
|
||||||
|
|
||||||
|
applyProviderProfileToProcessEnv(
|
||||||
|
buildApiRouteProfile({ baseUrl: undefined, apiKey: undefined }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_BASE_URL).toBe('https://global.api-route.com/v1')
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBe('ambient-api-route-key')
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBe('ambient-api-route-key')
|
||||||
|
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('api-route')
|
||||||
|
}, 20_000)
|
||||||
|
|
||||||
|
test('retargeted API Route profile withholds its dedicated credential and skips discovery', async () => {
|
||||||
|
const fetchMock = mock(() => Promise.resolve(new Response('{}')))
|
||||||
|
globalThis.fetch = fetchMock as unknown as typeof fetch
|
||||||
|
const { setActiveProviderProfile } = await importFreshProviderProfileModules()
|
||||||
|
const profile = buildApiRouteProfile({
|
||||||
|
id: 'api_route_proxy',
|
||||||
|
baseUrl: 'https://proxy.example/v1',
|
||||||
|
})
|
||||||
|
saveMockGlobalConfig(current => ({
|
||||||
|
...current,
|
||||||
|
providerProfiles: [profile],
|
||||||
|
}))
|
||||||
|
|
||||||
|
setActiveProviderProfile(profile.id, { configDir: testConfigDir ?? undefined })
|
||||||
|
await Promise.resolve()
|
||||||
|
|
||||||
|
expect(process.env.OPENAI_API_KEY).toBeUndefined()
|
||||||
|
expect(process.env.API_ROUTE_API_KEY).toBeUndefined()
|
||||||
|
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('api-route')
|
||||||
|
expect(fetchMock).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
const persisted = JSON.parse(
|
||||||
|
readFileSync(join(testConfigDir!, '.openclaude-profile.json'), 'utf8'),
|
||||||
|
)
|
||||||
|
expect(persisted.env).toEqual({
|
||||||
|
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'api-route',
|
||||||
|
OPENAI_BASE_URL: 'https://proxy.example/v1',
|
||||||
|
OPENAI_MODEL: 'claude-sonnet-4-6',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { buildStartupEnvFromProfile } = await import(
|
||||||
|
`./providerProfile.js?ts=${Date.now()}-${Math.random()}`
|
||||||
|
)
|
||||||
|
const startupEnv = await buildStartupEnvFromProfile({
|
||||||
|
persisted,
|
||||||
|
processEnv: {
|
||||||
|
API_ROUTE_API_KEY: 'ambient-api-route-key',
|
||||||
|
OPENAI_API_KEY: 'ambient-api-route-key',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(startupEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('api-route')
|
||||||
|
expect(startupEnv.OPENAI_API_KEY).toBeUndefined()
|
||||||
|
expect(startupEnv.API_ROUTE_API_KEY).toBeUndefined()
|
||||||
|
}, 20_000)
|
||||||
|
|
||||||
test('keyless canonical LLMTR profile adopts its ambient dedicated key', async () => {
|
test('keyless canonical LLMTR profile adopts its ambient dedicated key', async () => {
|
||||||
const { applyProviderProfileToProcessEnv } =
|
const { applyProviderProfileToProcessEnv } =
|
||||||
await importFreshProviderProfileModules()
|
await importFreshProviderProfileModules()
|
||||||
@@ -4315,6 +4471,60 @@ describe('setActiveProviderProfile', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('keyed canonical API Route profiles persist and restart with route identity', async () => {
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
|
||||||
|
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||||
|
process.chdir(tempDir)
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { setActiveProviderProfile } =
|
||||||
|
await importFreshProviderProfileModules()
|
||||||
|
const apiRouteProfile = buildApiRouteProfile({
|
||||||
|
id: 'api_route_profile',
|
||||||
|
})
|
||||||
|
saveMockGlobalConfig(current => ({
|
||||||
|
...current,
|
||||||
|
providerProfiles: [apiRouteProfile],
|
||||||
|
}))
|
||||||
|
|
||||||
|
setActiveProviderProfile(apiRouteProfile.id, { configDir })
|
||||||
|
const persisted = JSON.parse(
|
||||||
|
readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(persisted.env).toMatchObject({
|
||||||
|
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'api-route',
|
||||||
|
OPENAI_BASE_URL: 'https://global.api-route.com/v1',
|
||||||
|
OPENAI_API_KEY: 'api-route-test-key',
|
||||||
|
API_ROUTE_API_KEY: 'api-route-test-key',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { buildStartupEnvFromProfile } = await import(
|
||||||
|
`./providerProfile.js?ts=${Date.now()}-${Math.random()}`
|
||||||
|
)
|
||||||
|
const startupEnv = await buildStartupEnvFromProfile({
|
||||||
|
persisted,
|
||||||
|
processEnv: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(startupEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('api-route')
|
||||||
|
expect(startupEnv.OPENAI_BASE_URL).toBe('https://global.api-route.com/v1')
|
||||||
|
expect(startupEnv.OPENAI_API_KEY).toBe('api-route-test-key')
|
||||||
|
expect(
|
||||||
|
resolveRouteCredentialValue({
|
||||||
|
routeId: 'api-route',
|
||||||
|
baseUrl: startupEnv.OPENAI_BASE_URL,
|
||||||
|
processEnv: startupEnv,
|
||||||
|
}),
|
||||||
|
).toBe('api-route-test-key')
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
rmSync(tempDir, { recursive: true, force: true })
|
||||||
|
rmSync(configDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('keyed canonical LLMTR profiles persist their saved dedicated credential across restart', async () => {
|
test('keyed canonical LLMTR profiles persist their saved dedicated credential across restart', async () => {
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
|
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
|
||||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
getRouteDefaultBaseUrl,
|
getRouteDefaultBaseUrl,
|
||||||
isCloudflareBaseUrl,
|
isCloudflareBaseUrl,
|
||||||
isClinePassBaseUrl,
|
isClinePassBaseUrl,
|
||||||
|
isCanonicalApiRouteInferenceBaseUrl,
|
||||||
isCanonicalApismartInferenceBaseUrl,
|
isCanonicalApismartInferenceBaseUrl,
|
||||||
isCanonicalConcentrateInferenceBaseUrl,
|
isCanonicalConcentrateInferenceBaseUrl,
|
||||||
isCanonicalLlmtrInferenceBaseUrl,
|
isCanonicalLlmtrInferenceBaseUrl,
|
||||||
@@ -184,6 +185,15 @@ function isConcentrateProfile(profile: ProviderProfile): boolean {
|
|||||||
return !baseUrl || isCanonicalConcentrateInferenceBaseUrl(baseUrl)
|
return !baseUrl || isCanonicalConcentrateInferenceBaseUrl(baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isApiRouteProfile(profile: ProviderProfile): boolean {
|
||||||
|
const { route } = resolveProfileCompatibility(profile.provider)
|
||||||
|
if (route.routeId !== 'api-route') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const baseUrl = profile.baseUrl?.trim()
|
||||||
|
return !baseUrl || isCanonicalApiRouteInferenceBaseUrl(baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
function isLlmtrProfile(profile: ProviderProfile): boolean {
|
function isLlmtrProfile(profile: ProviderProfile): boolean {
|
||||||
const { route } = resolveProfileCompatibility(profile.provider)
|
const { route } = resolveProfileCompatibility(profile.provider)
|
||||||
if (route.routeId !== 'llmtr') {
|
if (route.routeId !== 'llmtr') {
|
||||||
@@ -297,6 +307,7 @@ export function resolveProfileCapabilityRouteId(
|
|||||||
(providerRouteId === 'cloudflare' ||
|
(providerRouteId === 'cloudflare' ||
|
||||||
providerRouteId === 'longcat' ||
|
providerRouteId === 'longcat' ||
|
||||||
providerRouteId === 'concentrate' ||
|
providerRouteId === 'concentrate' ||
|
||||||
|
providerRouteId === 'api-route' ||
|
||||||
providerRouteId === 'llmtr' ||
|
providerRouteId === 'llmtr' ||
|
||||||
providerRouteId === 'commandcode') &&
|
providerRouteId === 'commandcode') &&
|
||||||
baseUrl &&
|
baseUrl &&
|
||||||
@@ -306,6 +317,8 @@ export function resolveProfileCapabilityRouteId(
|
|||||||
? isLongcatBaseUrl(baseUrl)
|
? isLongcatBaseUrl(baseUrl)
|
||||||
: providerRouteId === 'concentrate'
|
: providerRouteId === 'concentrate'
|
||||||
? isCanonicalConcentrateInferenceBaseUrl(baseUrl)
|
? isCanonicalConcentrateInferenceBaseUrl(baseUrl)
|
||||||
|
: providerRouteId === 'api-route'
|
||||||
|
? isCanonicalApiRouteInferenceBaseUrl(baseUrl)
|
||||||
: providerRouteId === 'commandcode'
|
: providerRouteId === 'commandcode'
|
||||||
? isCanonicalCommandcodeInferenceBaseUrl(baseUrl)
|
? isCanonicalCommandcodeInferenceBaseUrl(baseUrl)
|
||||||
: isCanonicalLlmtrInferenceBaseUrl(baseUrl))
|
: isCanonicalLlmtrInferenceBaseUrl(baseUrl))
|
||||||
@@ -941,6 +954,10 @@ function isProcessEnvAlignedWithProfile(
|
|||||||
? !includeApiKey ||
|
? !includeApiKey ||
|
||||||
sameOptionalEnvValue(processEnv.APISMART_API_KEY, profile.apiKey)
|
sameOptionalEnvValue(processEnv.APISMART_API_KEY, profile.apiKey)
|
||||||
: true) &&
|
: true) &&
|
||||||
|
(isApiRouteProfile(profile)
|
||||||
|
? !includeApiKey ||
|
||||||
|
sameOptionalEnvValue(processEnv.API_ROUTE_API_KEY, profile.apiKey)
|
||||||
|
: true) &&
|
||||||
(isCommandcodeProfile(profile)
|
(isCommandcodeProfile(profile)
|
||||||
? !includeApiKey ||
|
? !includeApiKey ||
|
||||||
sameOptionalEnvValue(
|
sameOptionalEnvValue(
|
||||||
@@ -1110,9 +1127,11 @@ export function applyProviderProfileToProcessEnv(
|
|||||||
? getRouteDefaultBaseUrl('apismart') ?? profile.baseUrl
|
? getRouteDefaultBaseUrl('apismart') ?? profile.baseUrl
|
||||||
: route.routeId === 'concentrate' && !profile.baseUrl?.trim()
|
: route.routeId === 'concentrate' && !profile.baseUrl?.trim()
|
||||||
? getRouteDefaultBaseUrl('concentrate') ?? profile.baseUrl
|
? getRouteDefaultBaseUrl('concentrate') ?? profile.baseUrl
|
||||||
: route.routeId === 'commandcode' && !profile.baseUrl?.trim()
|
: route.routeId === 'api-route' && !profile.baseUrl?.trim()
|
||||||
? getRouteDefaultBaseUrl('commandcode') ?? profile.baseUrl
|
? getRouteDefaultBaseUrl('api-route') ?? profile.baseUrl
|
||||||
: profile.baseUrl
|
: route.routeId === 'commandcode' && !profile.baseUrl?.trim()
|
||||||
|
? getRouteDefaultBaseUrl('commandcode') ?? profile.baseUrl
|
||||||
|
: profile.baseUrl
|
||||||
const openAIProfileEnv: ProfileEnv = {
|
const openAIProfileEnv: ProfileEnv = {
|
||||||
OPENAI_BASE_URL: normalizedProfileBaseUrl,
|
OPENAI_BASE_URL: normalizedProfileBaseUrl,
|
||||||
OPENAI_MODEL: primaryModel,
|
OPENAI_MODEL: primaryModel,
|
||||||
@@ -1143,6 +1162,8 @@ export function applyProviderProfileToProcessEnv(
|
|||||||
route.routeId === 'apismart' && !isApismartProfile(profile)
|
route.routeId === 'apismart' && !isApismartProfile(profile)
|
||||||
const withholdRetargetedConcentrateCredential =
|
const withholdRetargetedConcentrateCredential =
|
||||||
route.routeId === 'concentrate' && !isConcentrateProfile(profile)
|
route.routeId === 'concentrate' && !isConcentrateProfile(profile)
|
||||||
|
const withholdRetargetedApiRouteCredential =
|
||||||
|
route.routeId === 'api-route' && !isApiRouteProfile(profile)
|
||||||
const withholdRetargetedLlmtrCredential =
|
const withholdRetargetedLlmtrCredential =
|
||||||
route.routeId === 'llmtr' && !isLlmtrProfile(profile)
|
route.routeId === 'llmtr' && !isLlmtrProfile(profile)
|
||||||
const withholdRetargetedCommandcodeCredential =
|
const withholdRetargetedCommandcodeCredential =
|
||||||
@@ -1151,6 +1172,7 @@ export function applyProviderProfileToProcessEnv(
|
|||||||
profile.apiKey &&
|
profile.apiKey &&
|
||||||
!withholdRetargetedApismartCredential &&
|
!withholdRetargetedApismartCredential &&
|
||||||
!withholdRetargetedConcentrateCredential &&
|
!withholdRetargetedConcentrateCredential &&
|
||||||
|
!withholdRetargetedApiRouteCredential &&
|
||||||
!withholdRetargetedLlmtrCredential &&
|
!withholdRetargetedLlmtrCredential &&
|
||||||
!withholdRetargetedCommandcodeCredential
|
!withholdRetargetedCommandcodeCredential
|
||||||
) {
|
) {
|
||||||
@@ -1193,6 +1215,9 @@ export function applyProviderProfileToProcessEnv(
|
|||||||
if (isConcentrateProfile(profile)) {
|
if (isConcentrateProfile(profile)) {
|
||||||
openAIProfileEnv.CONCENTRATE_API_KEY = profile.apiKey
|
openAIProfileEnv.CONCENTRATE_API_KEY = profile.apiKey
|
||||||
}
|
}
|
||||||
|
if (isApiRouteProfile(profile)) {
|
||||||
|
openAIProfileEnv.API_ROUTE_API_KEY = profile.apiKey
|
||||||
|
}
|
||||||
if (isCommandcodeProfile(profile)) {
|
if (isCommandcodeProfile(profile)) {
|
||||||
openAIProfileEnv.CMD_API_KEY = profile.apiKey
|
openAIProfileEnv.CMD_API_KEY = profile.apiKey
|
||||||
}
|
}
|
||||||
@@ -1284,6 +1309,17 @@ export function applyProviderProfileToProcessEnv(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (route.routeId === 'api-route') {
|
||||||
|
openAIProfileEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'api-route'
|
||||||
|
if (isApiRouteProfile(profile) && !profile.apiKey) {
|
||||||
|
const ambientApiRouteKey = sanitizeApiKey(process.env.API_ROUTE_API_KEY)
|
||||||
|
if (ambientApiRouteKey) {
|
||||||
|
openAIProfileEnv.OPENAI_API_KEY =
|
||||||
|
openAIProfileEnv.OPENAI_API_KEY ?? ambientApiRouteKey
|
||||||
|
openAIProfileEnv.API_ROUTE_API_KEY = ambientApiRouteKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// A keyless canonical LLMTR profile may use the dedicated credential from
|
// A keyless canonical LLMTR profile may use the dedicated credential from
|
||||||
// the ambient environment. Profile application clears all managed provider
|
// the ambient environment. Profile application clears all managed provider
|
||||||
// variables before installing this object, so adopt it here while the
|
// variables before installing this object, so adopt it here while the
|
||||||
@@ -1600,12 +1636,18 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const activeProfileRouteId = resolveProfileRoute(activeProfile.provider).routeId
|
const activeProfileRouteId = resolveProfileRoute(activeProfile.provider).routeId
|
||||||
|
const normalizedBaseUrl =
|
||||||
|
activeProfileRouteId === 'api-route' && !activeProfile.baseUrl?.trim()
|
||||||
|
? getRouteDefaultBaseUrl('api-route') ?? activeProfile.baseUrl
|
||||||
|
: activeProfile.baseUrl
|
||||||
const withholdRetargetedApismartCredential =
|
const withholdRetargetedApismartCredential =
|
||||||
activeProfileRouteId === 'apismart' &&
|
activeProfileRouteId === 'apismart' &&
|
||||||
!isApismartProfile(activeProfile)
|
!isApismartProfile(activeProfile)
|
||||||
const withholdRetargetedConcentrateCredential =
|
const withholdRetargetedConcentrateCredential =
|
||||||
activeProfileRouteId === 'concentrate' &&
|
activeProfileRouteId === 'concentrate' &&
|
||||||
!isConcentrateProfile(activeProfile)
|
!isConcentrateProfile(activeProfile)
|
||||||
|
const withholdRetargetedApiRouteCredential =
|
||||||
|
activeProfileRouteId === 'api-route' && !isApiRouteProfile(activeProfile)
|
||||||
const withholdRetargetedLlmtrCredential =
|
const withholdRetargetedLlmtrCredential =
|
||||||
activeProfileRouteId === 'llmtr' && !isLlmtrProfile(activeProfile)
|
activeProfileRouteId === 'llmtr' && !isLlmtrProfile(activeProfile)
|
||||||
const withholdRetargetedCommandcodeCredential =
|
const withholdRetargetedCommandcodeCredential =
|
||||||
@@ -1615,6 +1657,7 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
activeProfile.provider === 'aimlapi' ||
|
activeProfile.provider === 'aimlapi' ||
|
||||||
resolveRouteIdFromBaseUrl(activeProfile.baseUrl) === 'aimlapi'
|
resolveRouteIdFromBaseUrl(activeProfile.baseUrl) === 'aimlapi'
|
||||||
const isConcentrateProfileFlag = isConcentrateProfile(activeProfile)
|
const isConcentrateProfileFlag = isConcentrateProfile(activeProfile)
|
||||||
|
const isApiRouteProfileFlag = isApiRouteProfile(activeProfile)
|
||||||
const isLlmtrProfileFlag = isLlmtrProfile(activeProfile)
|
const isLlmtrProfileFlag = isLlmtrProfile(activeProfile)
|
||||||
const isCommandcodeProfileFlag = isCommandcodeProfile(activeProfile)
|
const isCommandcodeProfileFlag = isCommandcodeProfile(activeProfile)
|
||||||
|
|
||||||
@@ -1622,13 +1665,14 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
activeProfile.apiKey &&
|
activeProfile.apiKey &&
|
||||||
!withholdRetargetedApismartCredential &&
|
!withholdRetargetedApismartCredential &&
|
||||||
!withholdRetargetedConcentrateCredential &&
|
!withholdRetargetedConcentrateCredential &&
|
||||||
|
!withholdRetargetedApiRouteCredential &&
|
||||||
!withholdRetargetedLlmtrCredential &&
|
!withholdRetargetedLlmtrCredential &&
|
||||||
!withholdRetargetedCommandcodeCredential
|
!withholdRetargetedCommandcodeCredential
|
||||||
) {
|
) {
|
||||||
const strictEnv = buildOpenAIProfileEnv({
|
const strictEnv = buildOpenAIProfileEnv({
|
||||||
goal: 'balanced',
|
goal: 'balanced',
|
||||||
model: activeProfile.model,
|
model: activeProfile.model,
|
||||||
baseUrl: activeProfile.baseUrl,
|
baseUrl: normalizedBaseUrl,
|
||||||
apiKey: activeProfile.apiKey,
|
apiKey: activeProfile.apiKey,
|
||||||
apiFormat: activeProfile.apiFormat,
|
apiFormat: activeProfile.apiFormat,
|
||||||
azureStyle: activeProfile.azureStyle ? '1' : undefined,
|
azureStyle: activeProfile.azureStyle ? '1' : undefined,
|
||||||
@@ -1665,6 +1709,10 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
if (isConcentrateProfileFlag) {
|
if (isConcentrateProfileFlag) {
|
||||||
strictEnv.CONCENTRATE_API_KEY = activeProfile.apiKey
|
strictEnv.CONCENTRATE_API_KEY = activeProfile.apiKey
|
||||||
}
|
}
|
||||||
|
if (isApiRouteProfileFlag) {
|
||||||
|
strictEnv.API_ROUTE_API_KEY = activeProfile.apiKey
|
||||||
|
strictEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'api-route'
|
||||||
|
}
|
||||||
if (isLlmtrProfileFlag) {
|
if (isLlmtrProfileFlag) {
|
||||||
strictEnv.LLMTR_API_KEY = activeProfile.apiKey
|
strictEnv.LLMTR_API_KEY = activeProfile.apiKey
|
||||||
}
|
}
|
||||||
@@ -1698,7 +1746,7 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const env: ProfileEnv = {
|
const env: ProfileEnv = {
|
||||||
OPENAI_BASE_URL: activeProfile.baseUrl,
|
OPENAI_BASE_URL: normalizedBaseUrl,
|
||||||
OPENAI_MODEL: getPrimaryModel(activeProfile.model),
|
OPENAI_MODEL: getPrimaryModel(activeProfile.model),
|
||||||
...(activeProfile.apiFormat ? { OPENAI_API_FORMAT: activeProfile.apiFormat } : {}),
|
...(activeProfile.apiFormat ? { OPENAI_API_FORMAT: activeProfile.apiFormat } : {}),
|
||||||
...(activeProfile.azureStyle ? { OPENAI_AZURE_STYLE: '1' } : {}),
|
...(activeProfile.azureStyle ? { OPENAI_AZURE_STYLE: '1' } : {}),
|
||||||
@@ -1732,6 +1780,9 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
if (activeProfileRouteId === 'concentrate') {
|
if (activeProfileRouteId === 'concentrate') {
|
||||||
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'concentrate'
|
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'concentrate'
|
||||||
}
|
}
|
||||||
|
if (activeProfileRouteId === 'api-route') {
|
||||||
|
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'api-route'
|
||||||
|
}
|
||||||
if (activeProfileRouteId === 'llmtr') {
|
if (activeProfileRouteId === 'llmtr') {
|
||||||
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'llmtr'
|
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'llmtr'
|
||||||
}
|
}
|
||||||
@@ -1742,6 +1793,7 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
activeProfile.apiKey &&
|
activeProfile.apiKey &&
|
||||||
!withholdRetargetedApismartCredential &&
|
!withholdRetargetedApismartCredential &&
|
||||||
!withholdRetargetedConcentrateCredential &&
|
!withholdRetargetedConcentrateCredential &&
|
||||||
|
!withholdRetargetedApiRouteCredential &&
|
||||||
!withholdRetargetedLlmtrCredential &&
|
!withholdRetargetedLlmtrCredential &&
|
||||||
!withholdRetargetedCommandcodeCredential
|
!withholdRetargetedCommandcodeCredential
|
||||||
) {
|
) {
|
||||||
@@ -1773,6 +1825,9 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
if (isConcentrateProfileFlag) {
|
if (isConcentrateProfileFlag) {
|
||||||
env.CONCENTRATE_API_KEY = activeProfile.apiKey
|
env.CONCENTRATE_API_KEY = activeProfile.apiKey
|
||||||
}
|
}
|
||||||
|
if (isApiRouteProfileFlag) {
|
||||||
|
env.API_ROUTE_API_KEY = activeProfile.apiKey
|
||||||
|
}
|
||||||
if (isLlmtrProfileFlag) {
|
if (isLlmtrProfileFlag) {
|
||||||
env.LLMTR_API_KEY = activeProfile.apiKey
|
env.LLMTR_API_KEY = activeProfile.apiKey
|
||||||
}
|
}
|
||||||
@@ -1801,6 +1856,12 @@ function buildOpenAICompatibleStartupEnv(
|
|||||||
if (isCloudflareBaseUrl(activeProfile.baseUrl)) {
|
if (isCloudflareBaseUrl(activeProfile.baseUrl)) {
|
||||||
env.CLOUDFLARE_API_TOKEN = activeProfile.apiKey
|
env.CLOUDFLARE_API_TOKEN = activeProfile.apiKey
|
||||||
}
|
}
|
||||||
|
} else if (isApiRouteProfileFlag) {
|
||||||
|
const ambientApiRouteKey = sanitizeApiKey(process.env.API_ROUTE_API_KEY)
|
||||||
|
if (ambientApiRouteKey) {
|
||||||
|
env.OPENAI_API_KEY = ambientApiRouteKey
|
||||||
|
env.API_ROUTE_API_KEY = ambientApiRouteKey
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete env.OPENAI_API_KEY
|
delete env.OPENAI_API_KEY
|
||||||
}
|
}
|
||||||
@@ -2033,6 +2094,9 @@ function triggerStartupDiscoveryRefreshForProfile(
|
|||||||
if (route.routeId === 'apismart' && !isApismartProfile(profile)) {
|
if (route.routeId === 'apismart' && !isApismartProfile(profile)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (route.routeId === 'api-route' && !isApiRouteProfile(profile)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (route.routeId === 'llmtr' && !isLlmtrProfile(profile)) {
|
if (route.routeId === 'llmtr' && !isLlmtrProfile(profile)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ const ENV_KEYS = [
|
|||||||
'COMMANDCODE_API_KEY',
|
'COMMANDCODE_API_KEY',
|
||||||
'COMMAND_CODE_API_KEY',
|
'COMMAND_CODE_API_KEY',
|
||||||
'APISMART_API_KEY',
|
'APISMART_API_KEY',
|
||||||
|
'API_ROUTE_API_KEY',
|
||||||
|
'API_ROUTE_MODEL',
|
||||||
'CONCENTRATE_API_KEY',
|
'CONCENTRATE_API_KEY',
|
||||||
'CONCENTRATE_BASE_URL',
|
'CONCENTRATE_BASE_URL',
|
||||||
'CONCENTRATE_MODEL',
|
'CONCENTRATE_MODEL',
|
||||||
@@ -479,6 +481,46 @@ test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test('API Route saved generic profile credential validates on the canonical endpoint', async () => {
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
process.env.OPENAI_BASE_URL = 'https://global.api-route.com/v1'
|
||||||
|
process.env.OPENAI_MODEL = 'saved-model'
|
||||||
|
process.env.OPENAI_API_KEY = 'saved-profile-key'
|
||||||
|
delete process.env.API_ROUTE_API_KEY
|
||||||
|
|
||||||
|
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('API Route key-only setup validates before client defaults are applied', async () => {
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-key'
|
||||||
|
|
||||||
|
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('noncanonical same-host base rejects a selected API_ROUTE_API_KEY route', async () => {
|
||||||
|
process.env.OPENAI_BASE_URL = 'https://global.api-route.com/staging/v1'
|
||||||
|
process.env.API_ROUTE_API_KEY = 'api-route-key'
|
||||||
|
|
||||||
|
await expect(getProviderValidationError(process.env)).resolves.toBe(
|
||||||
|
'API Route credentials require the canonical https://global.api-route.com/v1 endpoint.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])(
|
||||||
|
'API Route validation rejects placeholder API_ROUTE_API_KEY %s',
|
||||||
|
async placeholder => {
|
||||||
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
process.env.OPENAI_BASE_URL = 'https://global.api-route.com/v1'
|
||||||
|
process.env.API_ROUTE_API_KEY = placeholder
|
||||||
|
delete process.env.OPENAI_API_KEY
|
||||||
|
delete process.env.OPENAI_API_KEYS
|
||||||
|
|
||||||
|
await expect(getProviderValidationError(process.env)).resolves.toBe(
|
||||||
|
'API Route auth is required. Set API_ROUTE_API_KEY.',
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
test('codex auth error redacts descriptor-declared provider secret values used as model text', async () => {
|
test('codex auth error redacts descriptor-declared provider secret values used as model text', async () => {
|
||||||
const providerSecret = 'ogw-provider-secret'
|
const providerSecret = 'ogw-provider-secret'
|
||||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
getRouteCredentialValue,
|
getRouteCredentialValue,
|
||||||
getRouteDescriptor,
|
getRouteDescriptor,
|
||||||
getRouteDefaultModel,
|
getRouteDefaultModel,
|
||||||
|
isCanonicalApiRouteInferenceBaseUrl,
|
||||||
isCanonicalApismartInferenceBaseUrl,
|
isCanonicalApismartInferenceBaseUrl,
|
||||||
isCanonicalConcentrateInferenceBaseUrl,
|
isCanonicalConcentrateInferenceBaseUrl,
|
||||||
isCanonicalCommandcodeInferenceBaseUrl,
|
isCanonicalCommandcodeInferenceBaseUrl,
|
||||||
@@ -144,6 +145,7 @@ function hasUsableCredentialEnvValue(
|
|||||||
envVar === 'OPENAI_API_KEY' ||
|
envVar === 'OPENAI_API_KEY' ||
|
||||||
envVar === 'AIMLAPI_API_KEY' ||
|
envVar === 'AIMLAPI_API_KEY' ||
|
||||||
envVar === 'APISMART_API_KEY' ||
|
envVar === 'APISMART_API_KEY' ||
|
||||||
|
envVar === 'API_ROUTE_API_KEY' ||
|
||||||
envVar === 'CONCENTRATE_API_KEY' ||
|
envVar === 'CONCENTRATE_API_KEY' ||
|
||||||
envVar === 'LLMTR_API_KEY' ||
|
envVar === 'LLMTR_API_KEY' ||
|
||||||
envVar === 'CMD_API_KEY' ||
|
envVar === 'CMD_API_KEY' ||
|
||||||
@@ -270,12 +272,13 @@ function getRuntimeValidationTarget(
|
|||||||
return enabledTarget
|
return enabledTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
// The documented CONCENTRATE_API_KEY-only setup is routed before the client
|
// The documented CONCENTRATE_API_KEY-only and API_ROUTE_API_KEY-only setup
|
||||||
// applies its default base URL. Select its descriptor directly so startup
|
// is routed before the client applies its default base URL. Select its descriptor
|
||||||
// validates the dedicated credential, including a noncanonical dedicated
|
// directly so startup validates the dedicated credential, including a noncanonical
|
||||||
// base URL, instead of returning early for an unset OpenAI mode.
|
// dedicated base URL, instead of returning early for an unset OpenAI mode.
|
||||||
if (resolveActiveRouteIdFromEnv(env) === 'concentrate') {
|
const activeRouteId = resolveActiveRouteIdFromEnv(env)
|
||||||
return validationTargets.find(target => target.descriptor.id === 'concentrate')
|
if (activeRouteId === 'concentrate' || activeRouteId === 'api-route') {
|
||||||
|
return validationTargets.find(target => target.descriptor.id === activeRouteId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!useOpenAI) {
|
if (!useOpenAI) {
|
||||||
@@ -306,6 +309,8 @@ function getRuntimeValidationTarget(
|
|||||||
!isCanonicalApismartInferenceBaseUrl(request.baseUrl)) ||
|
!isCanonicalApismartInferenceBaseUrl(request.baseUrl)) ||
|
||||||
(target.descriptor.id === 'concentrate' &&
|
(target.descriptor.id === 'concentrate' &&
|
||||||
!isCanonicalConcentrateInferenceBaseUrl(request.baseUrl)) ||
|
!isCanonicalConcentrateInferenceBaseUrl(request.baseUrl)) ||
|
||||||
|
(target.descriptor.id === 'api-route' &&
|
||||||
|
!isCanonicalApiRouteInferenceBaseUrl(request.baseUrl)) ||
|
||||||
(target.descriptor.id === 'commandcode' &&
|
(target.descriptor.id === 'commandcode' &&
|
||||||
!isCanonicalCommandcodeInferenceBaseUrl(request.baseUrl)))
|
!isCanonicalCommandcodeInferenceBaseUrl(request.baseUrl)))
|
||||||
) {
|
) {
|
||||||
@@ -435,6 +440,17 @@ async function getDescriptorValidationError(
|
|||||||
return 'Concentrate credentials require the canonical https://api.concentrate.ai/v1 endpoint.'
|
return 'Concentrate credentials require the canonical https://api.concentrate.ai/v1 endpoint.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const apiRouteBaseUrl =
|
||||||
|
env.OPENAI_BASE_URL?.trim() ||
|
||||||
|
env.OPENAI_API_BASE?.trim()
|
||||||
|
if (
|
||||||
|
target.descriptor.id === 'api-route' &&
|
||||||
|
apiRouteBaseUrl &&
|
||||||
|
!isCanonicalApiRouteInferenceBaseUrl(apiRouteBaseUrl)
|
||||||
|
) {
|
||||||
|
return 'API Route credentials require the canonical https://global.api-route.com/v1 endpoint.'
|
||||||
|
}
|
||||||
|
|
||||||
switch (validation.kind) {
|
switch (validation.kind) {
|
||||||
case 'credential-env':
|
case 'credential-env':
|
||||||
return getCredentialEnvValidationError(validation, env, options.request)
|
return getCredentialEnvValidationError(validation, env, options.request)
|
||||||
|
|||||||
@@ -204,6 +204,14 @@ export const providers: Provider[] = [
|
|||||||
envVars: ['APISMART_API_KEY', 'APISMART_MODEL'],
|
envVars: ['APISMART_API_KEY', 'APISMART_MODEL'],
|
||||||
notes: 'Unified OpenAI-compatible gateway at https://gw.apismart.ai/v1; defaults to DEEPSEEK_V4_FLASH with hybrid /v1/models discovery.',
|
notes: 'Unified OpenAI-compatible gateway at https://gw.apismart.ai/v1; defaults to DEEPSEEK_V4_FLASH with hybrid /v1/models discovery.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'api-route',
|
||||||
|
name: 'API Route',
|
||||||
|
group: 'gateways',
|
||||||
|
setup: '/provider or env vars',
|
||||||
|
envVars: ['API_ROUTE_API_KEY', 'API_ROUTE_MODEL'],
|
||||||
|
notes: 'OpenAI-compatible multi-model gateway at https://global.api-route.com/v1; defaults to claude-sonnet-4-6 with hybrid /v1/models discovery.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'cloudflare',
|
id: 'cloudflare',
|
||||||
name: 'Cloudflare Workers AI',
|
name: 'Cloudflare Workers AI',
|
||||||
|
|||||||
Reference in New Issue
Block a user