mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 12:50:54 -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_MODEL=gpt-4o
|
||||
|
||||
# For API Route, the dedicated key enables the OpenAI-compatible route:
|
||||
# API_ROUTE_API_KEY=your-api-route-key-here
|
||||
# OPENAI_BASE_URL=https://global.api-route.com/v1
|
||||
# OPENAI_MODEL=claude-sonnet-4-6
|
||||
|
||||
# For LLMTR, prefer its dedicated key. Raw env setup must also set
|
||||
# OPENAI_BASE_URL and OPENAI_MODEL. OPENAI_API_KEY remains supported:
|
||||
# LLMTR_API_KEY=your-llmtr-key-here
|
||||
|
||||
@@ -313,6 +313,7 @@ Advanced and source-build guides:
|
||||
| LLMTR | `/provider` or OpenAI-compatible env vars | Multi-model gateway at `https://llmtr.com/v1`; `/provider` and `--provider llmtr` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://llmtr.com/v1` and `OPENAI_MODEL`; accepts `LLMTR_API_KEY` or `OPENAI_API_KEY` after the route is selected and discovers tool-capable Chat Completions models from the public catalog |
|
||||
| Command Code | `/provider` or OpenAI-compatible env vars | Hybrid OpenAI-compatible gateway at `https://api.commandcode.ai/provider/v1`; `/provider` and `--provider commandcode` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://api.commandcode.ai/provider/v1` and `OPENAI_MODEL`; requires `CMD_API_KEY`, `COMMANDCODE_API_KEY`, or the official `COMMAND_CODE_API_KEY` after the route is selected and discovers Chat Completions models from the public catalog. Claude models are unsupported by this Chat Completions integration |
|
||||
| ApiSmart | `/provider` or `APISMART_API_KEY` | Uses `https://gw.apismart.ai/v1`, defaults to `DEEPSEEK_V4_FLASH`, and supports optional `APISMART_MODEL` plus authenticated model discovery |
|
||||
| API Route | `/provider` or `API_ROUTE_API_KEY` | Uses `https://global.api-route.com/v1`, defaults to `claude-sonnet-4-6`, and supports optional `API_ROUTE_MODEL` plus authenticated model discovery |
|
||||
| Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models |
|
||||
| Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` |
|
||||
| LongCat | `/provider` or env vars | Meituan LongCat OpenAI-compatible API at `https://api.longcat.chat/openai/v1`; uses `LONGCAT_API_KEY` and defaults to `LongCat-2.0` |
|
||||
|
||||
@@ -58,6 +58,7 @@ const EXPECTED_PRESETS = [
|
||||
'opencode',
|
||||
'opencode-go',
|
||||
'clinepass',
|
||||
'api-route',
|
||||
] as const satisfies readonly ProviderPreset[]
|
||||
|
||||
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 vendorZai from '../vendors/zai.js'
|
||||
import gatewayAimlapi from '../gateways/aimlapi.js'
|
||||
import gatewayApiRoute from '../gateways/api-route.js'
|
||||
import gatewayApismart from '../gateways/apismart.js'
|
||||
import gatewayAtlasCloud from '../gateways/atlas-cloud.js'
|
||||
import gatewayAtomicChat from '../gateways/atomic-chat.js'
|
||||
@@ -93,7 +94,7 @@ import modelXai from '../models/xai.js'
|
||||
import modelXiaomiMimo from '../models/xiaomi-mimo.js'
|
||||
|
||||
export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, vendorFireworks, vendorGemini, vendorLongcat, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
|
||||
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
|
||||
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApiRoute, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
|
||||
export const ANTHROPIC_PROXY_DESCRIPTORS = [anthropicproxyCustom] as const satisfies readonly AnthropicProxyDescriptor[]
|
||||
export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLing, brandLlama, brandLongcat, brandMacaron, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandTencent, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[]
|
||||
export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLing, modelLlama, modelLongcat, modelMacaron, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[]
|
||||
|
||||
@@ -90,6 +90,21 @@ export const PROVIDER_PRESET_MANIFEST = [
|
||||
"DASHSCOPE_API_KEY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"preset": "api-route",
|
||||
"routeKind": "gateway",
|
||||
"routeId": "api-route",
|
||||
"vendorId": "openai",
|
||||
"gatewayId": "api-route",
|
||||
"description": "API Route OpenAI-compatible multi-model gateway",
|
||||
"apiKeyEnvVars": [
|
||||
"API_ROUTE_API_KEY"
|
||||
],
|
||||
"modelEnvVars": [
|
||||
"API_ROUTE_MODEL",
|
||||
"OPENAI_MODEL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"preset": "apismart",
|
||||
"routeKind": "gateway",
|
||||
@@ -591,6 +606,7 @@ export const ORDERED_PROVIDER_PRESETS = [
|
||||
"anthropic",
|
||||
"dashscope-cn",
|
||||
"dashscope-intl",
|
||||
"api-route",
|
||||
"apismart",
|
||||
"atlas-cloud",
|
||||
"azure-openai",
|
||||
|
||||
@@ -47,7 +47,8 @@ function hasUsableEnvValue(envVar: string, value: string | undefined): boolean {
|
||||
envVar === 'LLMTR_API_KEY' ||
|
||||
envVar === 'CMD_API_KEY' ||
|
||||
envVar === 'COMMANDCODE_API_KEY' ||
|
||||
envVar === 'COMMAND_CODE_API_KEY'
|
||||
envVar === 'COMMAND_CODE_API_KEY' ||
|
||||
envVar === 'API_ROUTE_API_KEY'
|
||||
) {
|
||||
return hasUsableOpenAICredential(value)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
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', () => {
|
||||
expect(
|
||||
resolveActiveRouteIdFromEnv({
|
||||
|
||||
@@ -246,7 +246,8 @@ function hasUsableEnvCredentialValue(
|
||||
envVar === 'LLMTR_API_KEY' ||
|
||||
envVar === 'CMD_API_KEY' ||
|
||||
envVar === 'COMMANDCODE_API_KEY' ||
|
||||
envVar === 'COMMAND_CODE_API_KEY'
|
||||
envVar === 'COMMAND_CODE_API_KEY' ||
|
||||
envVar === 'API_ROUTE_API_KEY'
|
||||
) {
|
||||
return hasUsableOpenAICredential(value)
|
||||
}
|
||||
@@ -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(
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
): 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(
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
):
|
||||
@@ -1053,6 +1114,7 @@ export function resolveEnvOnlyProviderRouteId(
|
||||
| 'clinepass'
|
||||
| 'apismart'
|
||||
| 'concentrate'
|
||||
| 'api-route'
|
||||
| null {
|
||||
if (
|
||||
hasMiniMaxRouteIntent(processEnv) &&
|
||||
@@ -1105,6 +1167,10 @@ export function resolveEnvOnlyProviderRouteId(
|
||||
return 'concentrate'
|
||||
}
|
||||
|
||||
if (hasApiRouteEnvOnlyProviderIntent(processEnv)) {
|
||||
return 'api-route'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1203,6 +1269,13 @@ export function resolveRouteCredentialValue(
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
if (
|
||||
routeId === 'api-route' &&
|
||||
options?.baseUrl !== undefined &&
|
||||
!isCanonicalApiRouteInferenceBaseUrl(options.baseUrl)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return getRouteCredentialValue(routeId, processEnv)
|
||||
}
|
||||
@@ -1327,6 +1400,12 @@ export function resolveRouteIdFromBaseUrl(
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
route.id === 'api-route' &&
|
||||
!isCanonicalApiRouteInferenceBaseUrl(baseUrl)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
return route.id
|
||||
}
|
||||
}
|
||||
@@ -1347,7 +1426,9 @@ export function resolveRouteIdFromBaseUrl(
|
||||
(route.id === 'llmtr' &&
|
||||
!isCanonicalLlmtrInferenceBaseUrl(baseUrl)) ||
|
||||
(route.id === 'commandcode' &&
|
||||
!isCanonicalCommandcodeInferenceBaseUrl(baseUrl))
|
||||
!isCanonicalCommandcodeInferenceBaseUrl(baseUrl)) ||
|
||||
(route.id === 'api-route' &&
|
||||
!isApiRouteBaseUrl(baseUrl))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -1393,6 +1474,9 @@ function profileRouteHonorsBaseUrlBoundary(
|
||||
if (routeId === 'commandcode') {
|
||||
return isCanonicalCommandcodeInferenceBaseUrl(baseUrl)
|
||||
}
|
||||
if (routeId === 'api-route') {
|
||||
return isCanonicalApiRouteInferenceBaseUrl(baseUrl)
|
||||
}
|
||||
if (routeId === 'zai') {
|
||||
return !baseUrl || isCanonicalZaiCodingPlanBaseUrl(baseUrl)
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ const originalEnv = {
|
||||
LONGCAT_API_KEY: process.env.LONGCAT_API_KEY,
|
||||
AIMLAPI_API_KEY: process.env.AIMLAPI_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_BASE_URL: process.env.CONCENTRATE_BASE_URL,
|
||||
CONCENTRATE_MODEL: process.env.CONCENTRATE_MODEL,
|
||||
@@ -209,6 +211,8 @@ beforeEach(async () => {
|
||||
delete process.env.LONGCAT_API_KEY
|
||||
delete process.env.AIMLAPI_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_BASE_URL
|
||||
delete process.env.CONCENTRATE_MODEL
|
||||
@@ -274,6 +278,8 @@ afterEach(() => {
|
||||
restoreEnv('LONGCAT_API_KEY', originalEnv.LONGCAT_API_KEY)
|
||||
restoreEnv('AIMLAPI_API_KEY', originalEnv.AIMLAPI_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_BASE_URL', originalEnv.CONCENTRATE_BASE_URL)
|
||||
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()
|
||||
})
|
||||
|
||||
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 () => {
|
||||
let capturedUrl: string | undefined
|
||||
let capturedHeaders: Headers | undefined
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
getLongcatBaseUrlOverride,
|
||||
getMiniMaxBaseUrlOverride,
|
||||
getNearaiBaseUrlOverride,
|
||||
isCanonicalApiRouteInferenceBaseUrl,
|
||||
isCanonicalApismartInferenceBaseUrl,
|
||||
isCanonicalConcentrateInferenceBaseUrl,
|
||||
getRouteDefaultBaseUrl,
|
||||
@@ -450,6 +451,39 @@ function applyConcentrateEnvOnlyDefaults(): void {
|
||||
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(
|
||||
value: string | undefined,
|
||||
): string | undefined {
|
||||
@@ -574,6 +608,8 @@ export async function getAnthropicClient({
|
||||
envOnlyProviderRouteId === 'apismart' && !useMiniMaxEnvOnlyProvider
|
||||
const useConcentrateEnvOnlyProvider =
|
||||
envOnlyProviderRouteId === 'concentrate' && !useMiniMaxEnvOnlyProvider
|
||||
const useApiRouteEnvOnlyProvider =
|
||||
envOnlyProviderRouteId === 'api-route' && !useMiniMaxEnvOnlyProvider
|
||||
if (useMiniMaxEnvOnlyProvider) applyMiniMaxEnvOnlyDefaults(model)
|
||||
if (useXiaomiMimoEnvOnlyProvider) applyXiaomiMimoEnvOnlyDefaults()
|
||||
if (useXaiEnvOnlyProvider) applyXaiEnvOnlyDefaults()
|
||||
@@ -583,6 +619,7 @@ export async function getAnthropicClient({
|
||||
if (useAimlapiEnvOnlyProvider) applyAimlapiEnvOnlyDefaults()
|
||||
if (useApismartEnvOnlyProvider) applyApismartEnvOnlyDefaults()
|
||||
if (useConcentrateEnvOnlyProvider) applyConcentrateEnvOnlyDefaults()
|
||||
if (useApiRouteEnvOnlyProvider) applyApiRouteEnvOnlyDefaults()
|
||||
|
||||
const containerId = process.env.CLAUDE_CODE_CONTAINER_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_KEYS: process.env.OPENAI_API_KEYS,
|
||||
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,
|
||||
COMMANDCODE_API_KEY: process.env.COMMANDCODE_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'
|
||||
delete process.env.OPENAI_API_KEYS
|
||||
delete process.env.LLMTR_API_KEY
|
||||
delete process.env.API_ROUTE_API_KEY
|
||||
delete process.env.CMD_API_KEY
|
||||
delete process.env.COMMANDCODE_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_KEYS', originalEnv.OPENAI_API_KEYS)
|
||||
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('COMMANDCODE_API_KEY', originalEnv.COMMANDCODE_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')
|
||||
})
|
||||
|
||||
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 () => {
|
||||
process.env.LLMTR_API_KEY = 'llmtr-key'
|
||||
delete process.env.OPENAI_API_KEYS
|
||||
|
||||
@@ -327,6 +327,7 @@ export async function executeOpenAIRequest(
|
||||
requestProcessEnv.ATLAS_CLOUD_API_KEY,
|
||||
requestProcessEnv.APISMART_API_KEY,
|
||||
requestProcessEnv.CONCENTRATE_API_KEY,
|
||||
requestProcessEnv.API_ROUTE_API_KEY,
|
||||
requestProcessEnv.NEARAI_API_KEY,
|
||||
requestProcessEnv.FIREWORKS_API_KEY,
|
||||
requestProcessEnv.LONGCAT_API_KEY,
|
||||
|
||||
@@ -24,6 +24,8 @@ const TEST_ENV_KEYS = [
|
||||
'COMMAND_CODE_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'APISMART_MODEL',
|
||||
'API_ROUTE_API_KEY',
|
||||
'API_ROUTE_MODEL',
|
||||
'OPENAI_API_KEYS',
|
||||
'OPENAI_API_KEY',
|
||||
'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', () => {
|
||||
const filePath = writeTempEnvFile('LLMTR_API_KEY=llmtr-key')
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ const ALLOWED_ENV_FILE_KEYS = new Set([
|
||||
'ATLAS_CLOUD_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'APISMART_MODEL',
|
||||
'API_ROUTE_API_KEY',
|
||||
'API_ROUTE_MODEL',
|
||||
'AWS_BEARER_TOKEN_BEDROCK',
|
||||
'AWS_DEFAULT_REGION',
|
||||
'AWS_PROFILE',
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
loadEnvFile,
|
||||
rememberLoadedEnvFileValues,
|
||||
} from './envFile.js'
|
||||
import { applyConfigEnvironmentVariables } from './managedEnv.js'
|
||||
import {
|
||||
applyConfigEnvironmentVariables,
|
||||
applySafeConfigEnvironmentVariables,
|
||||
} from './managedEnv.js'
|
||||
|
||||
const ENV_KEYS = [
|
||||
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
|
||||
@@ -28,6 +31,8 @@ const ENV_KEYS = [
|
||||
'CMD_API_KEY',
|
||||
'COMMANDCODE_API_KEY',
|
||||
'COMMAND_CODE_API_KEY',
|
||||
'API_ROUTE_API_KEY',
|
||||
'API_ROUTE_MODEL',
|
||||
'OPENAI_API_KEY',
|
||||
'OPENAI_AZURE_STYLE',
|
||||
'OPENAI_BASE_URL',
|
||||
@@ -95,6 +100,36 @@ function writeTempEnvFile(content: string): string {
|
||||
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', () => {
|
||||
it('preserves the complete host-managed Command Code route against settings env', () => {
|
||||
process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = '1'
|
||||
|
||||
@@ -43,6 +43,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
'CMD_API_KEY',
|
||||
'COMMANDCODE_API_KEY',
|
||||
'COMMAND_CODE_API_KEY',
|
||||
'API_ROUTE_API_KEY',
|
||||
'OPENAI_API_KEY',
|
||||
'OPENAI_API_KEYS',
|
||||
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
||||
@@ -65,6 +66,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
'ANTHROPIC_SMALL_FAST_MODEL',
|
||||
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
||||
'CLAUDE_CODE_SUBAGENT_MODEL',
|
||||
'API_ROUTE_MODEL',
|
||||
'OPENAI_MODEL',
|
||||
])
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ const SAVED_ENV = {
|
||||
CONCENTRATE_API_KEY: process.env.CONCENTRATE_API_KEY,
|
||||
CONCENTRATE_BASE_URL: process.env.CONCENTRATE_BASE_URL,
|
||||
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_BASE_URL: process.env.OPENAI_BASE_URL,
|
||||
CODEX_API_KEY: process.env.CODEX_API_KEY,
|
||||
@@ -142,6 +144,8 @@ beforeEach(async () => {
|
||||
delete process.env.CONCENTRATE_API_KEY
|
||||
delete process.env.CONCENTRATE_BASE_URL
|
||||
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_BASE_URL
|
||||
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 () => {
|
||||
process.env.NVIDIA_NIM = '1'
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
|
||||
@@ -69,6 +69,17 @@ function getAllowedConcentrateConfigModel(): string | 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 {
|
||||
if (process.env.ANTHROPIC_SMALL_FAST_MODEL) return process.env.ANTHROPIC_SMALL_FAST_MODEL
|
||||
if (isCustomAnthropicProvider()) {
|
||||
@@ -173,7 +184,9 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined {
|
||||
// consume its dedicated setting here rather than falling through to a
|
||||
// saved model from an unrelated provider.
|
||||
? 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 === 'minimax' ? getMiniMaxModelEnv() : undefined) ||
|
||||
(isOpenAIShimProvider ? process.env.OPENAI_MODEL : undefined) ||
|
||||
@@ -404,6 +417,13 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
|
||||
'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
|
||||
// firstParty provider category, so prefer their explicitly configured model
|
||||
// before the subscription and PAYG defaults below.
|
||||
|
||||
@@ -47,6 +47,8 @@ const ENV_KEYS = [
|
||||
'ATLAS_CLOUD_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'APISMART_MODEL',
|
||||
'API_ROUTE_API_KEY',
|
||||
'API_ROUTE_MODEL',
|
||||
'CONCENTRATE_API_KEY',
|
||||
'CONCENTRATE_BASE_URL',
|
||||
'CONCENTRATE_MODEL',
|
||||
@@ -101,6 +103,8 @@ const RESET_KEYS = [
|
||||
'ATLAS_CLOUD_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'APISMART_MODEL',
|
||||
'API_ROUTE_API_KEY',
|
||||
'API_ROUTE_MODEL',
|
||||
'CONCENTRATE_API_KEY',
|
||||
'CONCENTRATE_BASE_URL',
|
||||
'CONCENTRATE_MODEL',
|
||||
@@ -1848,3 +1852,150 @@ describe('applyProviderFlag - concentrate', () => {
|
||||
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 { getCommandcodeChatCompletionsModelError } from '../integrations/gateways/commandcode.js'
|
||||
import {
|
||||
isCanonicalApiRouteInferenceBaseUrl,
|
||||
isCanonicalApismartInferenceBaseUrl,
|
||||
isCanonicalConcentrateInferenceBaseUrl,
|
||||
isCanonicalLlmtrInferenceBaseUrl,
|
||||
@@ -401,6 +402,9 @@ export function applyProviderFlag(
|
||||
: process.env.OPENAI_API_KEY !== undefined &&
|
||||
process.env.OPENAI_API_KEY === process.env.APISMART_API_KEY
|
||||
? '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 === process.env.CONCENTRATE_API_KEY
|
||||
? 'concentrate'
|
||||
@@ -470,6 +474,8 @@ export function applyProviderFlag(
|
||||
// OpenAI-compatible env-only route override it later in startup.
|
||||
delete process.env.APISMART_API_KEY
|
||||
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_CUSTOM_HEADERS
|
||||
break
|
||||
@@ -517,6 +523,8 @@ export function applyProviderFlag(
|
||||
// known gateway endpoint, but preserve a user-supplied custom endpoint.
|
||||
delete process.env.APISMART_API_KEY
|
||||
delete process.env.APISMART_MODEL
|
||||
delete process.env.API_ROUTE_API_KEY
|
||||
delete process.env.API_ROUTE_MODEL
|
||||
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
|
||||
if (effectiveModel) process.env.OPENAI_MODEL = effectiveModel
|
||||
break
|
||||
@@ -888,7 +896,9 @@ export function applyProviderFlag(
|
||||
(provider === 'llmtr' &&
|
||||
isCanonicalLlmtrInferenceBaseUrl(getConfiguredOpenAIBaseUrl())) ||
|
||||
(provider === 'commandcode' &&
|
||||
isCanonicalCommandcodeInferenceBaseUrl(getConfiguredOpenAIBaseUrl()))
|
||||
isCanonicalCommandcodeInferenceBaseUrl(getConfiguredOpenAIBaseUrl())) ||
|
||||
(provider === 'api-route' &&
|
||||
isCanonicalApiRouteInferenceBaseUrl(getConfiguredOpenAIBaseUrl()))
|
||||
) {
|
||||
clearUnsupportedOpenAIShimSettings(provider)
|
||||
delete process.env.ANTHROPIC_CUSTOM_HEADERS
|
||||
@@ -918,6 +928,35 @@ export function applyProviderFlag(
|
||||
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) {
|
||||
process.env.OPENAI_MODEL ??= defaultModel
|
||||
}
|
||||
@@ -925,9 +964,9 @@ export function applyProviderFlag(
|
||||
break
|
||||
}
|
||||
|
||||
// A provider flag selects a complete route for this process. Concentrate's
|
||||
// dedicated variables are another source of route identity, so leaving them
|
||||
// behind can re-select Concentrate after a later OpenAI-compatible provider
|
||||
// A provider flag selects a complete route for this process. Dedicated
|
||||
// variables are another source of route identity, so leaving them
|
||||
// behind can re-select a provider after a later OpenAI-compatible provider
|
||||
// has applied its defaults. Keep their lifecycle at the selection boundary,
|
||||
// rather than relying on individual switch branches to remember cleanup.
|
||||
// This runs only after the selected branch succeeds, so an invalid
|
||||
@@ -935,6 +974,10 @@ export function applyProviderFlag(
|
||||
if (provider !== 'concentrate') {
|
||||
clearConcentrateProviderState()
|
||||
}
|
||||
if (provider !== 'api-route') {
|
||||
delete process.env.API_ROUTE_API_KEY
|
||||
delete process.env.API_ROUTE_MODEL
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const env = await buildStartupEnvFromProfile({
|
||||
persisted: profile('openai', {
|
||||
@@ -3427,6 +3458,50 @@ test('buildStartupEnvFromProfile preserves Concentrate env-only setup over a sav
|
||||
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', () => {
|
||||
const env = buildConcentrateProfileEnv({
|
||||
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?.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 {
|
||||
getRouteDefaultBaseUrl,
|
||||
getRouteDefaultModel,
|
||||
isCanonicalApiRouteInferenceBaseUrl,
|
||||
isCanonicalApismartInferenceBaseUrl,
|
||||
isCanonicalConcentrateInferenceBaseUrl,
|
||||
isCanonicalLlmtrInferenceBaseUrl,
|
||||
@@ -118,6 +119,8 @@ const PROFILE_ENV_KEYS = [
|
||||
'ATLAS_CLOUD_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'APISMART_MODEL',
|
||||
'API_ROUTE_API_KEY',
|
||||
'API_ROUTE_MODEL',
|
||||
'NEARAI_API_KEY',
|
||||
'FIREWORKS_API_KEY',
|
||||
'LONGCAT_API_KEY',
|
||||
@@ -219,6 +222,7 @@ export type ProfileEnv = {
|
||||
MIMO_API_KEY?: string
|
||||
ATLAS_CLOUD_API_KEY?: string
|
||||
APISMART_API_KEY?: string
|
||||
API_ROUTE_API_KEY?: string
|
||||
CLINE_API_KEY?: string
|
||||
NEARAI_API_KEY?: string
|
||||
FIREWORKS_API_KEY?: string
|
||||
@@ -1491,6 +1495,7 @@ function hasConcreteProviderSelection(
|
||||
// before the env-only resolver has a chance to see it.
|
||||
return (
|
||||
sanitizeApiKey(processEnv.APISMART_API_KEY) !== undefined ||
|
||||
sanitizeApiKey(processEnv.API_ROUTE_API_KEY) !== undefined ||
|
||||
sanitizeApiKey(processEnv.FIREWORKS_API_KEY) !== undefined ||
|
||||
sanitizeApiKey(processEnv.NEARAI_API_KEY) !== undefined ||
|
||||
sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined ||
|
||||
@@ -2181,12 +2186,17 @@ export async function buildLaunchEnv(options: {
|
||||
effectiveOpenAIRouteId === 'commandcode' &&
|
||||
!!env.OPENAI_BASE_URL?.trim() &&
|
||||
!isCanonicalCommandcodeInferenceBaseUrl(env.OPENAI_BASE_URL)
|
||||
const isNoncanonicalApiRouteLaunch =
|
||||
effectiveOpenAIRouteId === 'api-route' &&
|
||||
!!env.OPENAI_BASE_URL?.trim() &&
|
||||
!isCanonicalApiRouteInferenceBaseUrl(env.OPENAI_BASE_URL)
|
||||
const isNoncanonicalDedicatedOpenAILaunch =
|
||||
isNoncanonicalAimlapiLaunch ||
|
||||
isNoncanonicalApismartLaunch ||
|
||||
isNoncanonicalConcentrateLaunch ||
|
||||
isNoncanonicalLlmtrLaunch ||
|
||||
isNoncanonicalCommandcodeLaunch
|
||||
isNoncanonicalCommandcodeLaunch ||
|
||||
isNoncanonicalApiRouteLaunch
|
||||
if (isNoncanonicalDedicatedOpenAILaunch) {
|
||||
delete env.OPENAI_API_KEY
|
||||
delete env.OPENAI_API_KEYS
|
||||
@@ -2510,12 +2520,18 @@ export async function buildStartupEnvFromProfile(options?: {
|
||||
persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'commandcode' &&
|
||||
!!persisted.env.OPENAI_BASE_URL?.trim() &&
|
||||
!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 (
|
||||
hasConcreteProviderSelection(processEnv) &&
|
||||
!persistedApismartProxy &&
|
||||
!persistedConcentrateProxy &&
|
||||
!persistedLlmtrProxy &&
|
||||
!persistedCommandcodeProxy
|
||||
!persistedCommandcodeProxy &&
|
||||
!persistedApiRouteProxy
|
||||
) {
|
||||
return processEnv
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ async function importFreshProvidersModule() {
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
const originalCwd = process.cwd()
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
const RESTORED_KEYS = [
|
||||
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED',
|
||||
@@ -73,6 +74,8 @@ const RESTORED_KEYS = [
|
||||
'ATLAS_CLOUD_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'APISMART_MODEL',
|
||||
'API_ROUTE_API_KEY',
|
||||
'API_ROUTE_MODEL',
|
||||
'LLMTR_API_KEY',
|
||||
'CMD_API_KEY',
|
||||
'COMMANDCODE_API_KEY',
|
||||
@@ -136,6 +139,7 @@ afterEach(() => {
|
||||
}
|
||||
|
||||
mock.restore()
|
||||
globalThis.fetch = originalFetch
|
||||
mockConfigState = createMockConfigState()
|
||||
process.chdir(originalCwd)
|
||||
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 {
|
||||
return buildProfile({
|
||||
provider: 'llmtr',
|
||||
@@ -394,6 +409,147 @@ describe('applyProviderProfileToProcessEnv', () => {
|
||||
).toBe('selected-new')
|
||||
}, 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 () => {
|
||||
const { applyProviderProfileToProcessEnv } =
|
||||
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 () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
getRouteDefaultBaseUrl,
|
||||
isCloudflareBaseUrl,
|
||||
isClinePassBaseUrl,
|
||||
isCanonicalApiRouteInferenceBaseUrl,
|
||||
isCanonicalApismartInferenceBaseUrl,
|
||||
isCanonicalConcentrateInferenceBaseUrl,
|
||||
isCanonicalLlmtrInferenceBaseUrl,
|
||||
@@ -184,6 +185,15 @@ function isConcentrateProfile(profile: ProviderProfile): boolean {
|
||||
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 {
|
||||
const { route } = resolveProfileCompatibility(profile.provider)
|
||||
if (route.routeId !== 'llmtr') {
|
||||
@@ -297,6 +307,7 @@ export function resolveProfileCapabilityRouteId(
|
||||
(providerRouteId === 'cloudflare' ||
|
||||
providerRouteId === 'longcat' ||
|
||||
providerRouteId === 'concentrate' ||
|
||||
providerRouteId === 'api-route' ||
|
||||
providerRouteId === 'llmtr' ||
|
||||
providerRouteId === 'commandcode') &&
|
||||
baseUrl &&
|
||||
@@ -306,6 +317,8 @@ export function resolveProfileCapabilityRouteId(
|
||||
? isLongcatBaseUrl(baseUrl)
|
||||
: providerRouteId === 'concentrate'
|
||||
? isCanonicalConcentrateInferenceBaseUrl(baseUrl)
|
||||
: providerRouteId === 'api-route'
|
||||
? isCanonicalApiRouteInferenceBaseUrl(baseUrl)
|
||||
: providerRouteId === 'commandcode'
|
||||
? isCanonicalCommandcodeInferenceBaseUrl(baseUrl)
|
||||
: isCanonicalLlmtrInferenceBaseUrl(baseUrl))
|
||||
@@ -941,6 +954,10 @@ function isProcessEnvAlignedWithProfile(
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.APISMART_API_KEY, profile.apiKey)
|
||||
: true) &&
|
||||
(isApiRouteProfile(profile)
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.API_ROUTE_API_KEY, profile.apiKey)
|
||||
: true) &&
|
||||
(isCommandcodeProfile(profile)
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(
|
||||
@@ -1110,9 +1127,11 @@ export function applyProviderProfileToProcessEnv(
|
||||
? getRouteDefaultBaseUrl('apismart') ?? profile.baseUrl
|
||||
: route.routeId === 'concentrate' && !profile.baseUrl?.trim()
|
||||
? getRouteDefaultBaseUrl('concentrate') ?? profile.baseUrl
|
||||
: route.routeId === 'commandcode' && !profile.baseUrl?.trim()
|
||||
? getRouteDefaultBaseUrl('commandcode') ?? profile.baseUrl
|
||||
: profile.baseUrl
|
||||
: route.routeId === 'api-route' && !profile.baseUrl?.trim()
|
||||
? getRouteDefaultBaseUrl('api-route') ?? profile.baseUrl
|
||||
: route.routeId === 'commandcode' && !profile.baseUrl?.trim()
|
||||
? getRouteDefaultBaseUrl('commandcode') ?? profile.baseUrl
|
||||
: profile.baseUrl
|
||||
const openAIProfileEnv: ProfileEnv = {
|
||||
OPENAI_BASE_URL: normalizedProfileBaseUrl,
|
||||
OPENAI_MODEL: primaryModel,
|
||||
@@ -1143,6 +1162,8 @@ export function applyProviderProfileToProcessEnv(
|
||||
route.routeId === 'apismart' && !isApismartProfile(profile)
|
||||
const withholdRetargetedConcentrateCredential =
|
||||
route.routeId === 'concentrate' && !isConcentrateProfile(profile)
|
||||
const withholdRetargetedApiRouteCredential =
|
||||
route.routeId === 'api-route' && !isApiRouteProfile(profile)
|
||||
const withholdRetargetedLlmtrCredential =
|
||||
route.routeId === 'llmtr' && !isLlmtrProfile(profile)
|
||||
const withholdRetargetedCommandcodeCredential =
|
||||
@@ -1151,6 +1172,7 @@ export function applyProviderProfileToProcessEnv(
|
||||
profile.apiKey &&
|
||||
!withholdRetargetedApismartCredential &&
|
||||
!withholdRetargetedConcentrateCredential &&
|
||||
!withholdRetargetedApiRouteCredential &&
|
||||
!withholdRetargetedLlmtrCredential &&
|
||||
!withholdRetargetedCommandcodeCredential
|
||||
) {
|
||||
@@ -1193,6 +1215,9 @@ export function applyProviderProfileToProcessEnv(
|
||||
if (isConcentrateProfile(profile)) {
|
||||
openAIProfileEnv.CONCENTRATE_API_KEY = profile.apiKey
|
||||
}
|
||||
if (isApiRouteProfile(profile)) {
|
||||
openAIProfileEnv.API_ROUTE_API_KEY = profile.apiKey
|
||||
}
|
||||
if (isCommandcodeProfile(profile)) {
|
||||
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
|
||||
// the ambient environment. Profile application clears all managed provider
|
||||
// variables before installing this object, so adopt it here while the
|
||||
@@ -1600,12 +1636,18 @@ function buildOpenAICompatibleStartupEnv(
|
||||
return null
|
||||
}
|
||||
const activeProfileRouteId = resolveProfileRoute(activeProfile.provider).routeId
|
||||
const normalizedBaseUrl =
|
||||
activeProfileRouteId === 'api-route' && !activeProfile.baseUrl?.trim()
|
||||
? getRouteDefaultBaseUrl('api-route') ?? activeProfile.baseUrl
|
||||
: activeProfile.baseUrl
|
||||
const withholdRetargetedApismartCredential =
|
||||
activeProfileRouteId === 'apismart' &&
|
||||
!isApismartProfile(activeProfile)
|
||||
const withholdRetargetedConcentrateCredential =
|
||||
activeProfileRouteId === 'concentrate' &&
|
||||
!isConcentrateProfile(activeProfile)
|
||||
const withholdRetargetedApiRouteCredential =
|
||||
activeProfileRouteId === 'api-route' && !isApiRouteProfile(activeProfile)
|
||||
const withholdRetargetedLlmtrCredential =
|
||||
activeProfileRouteId === 'llmtr' && !isLlmtrProfile(activeProfile)
|
||||
const withholdRetargetedCommandcodeCredential =
|
||||
@@ -1615,6 +1657,7 @@ function buildOpenAICompatibleStartupEnv(
|
||||
activeProfile.provider === 'aimlapi' ||
|
||||
resolveRouteIdFromBaseUrl(activeProfile.baseUrl) === 'aimlapi'
|
||||
const isConcentrateProfileFlag = isConcentrateProfile(activeProfile)
|
||||
const isApiRouteProfileFlag = isApiRouteProfile(activeProfile)
|
||||
const isLlmtrProfileFlag = isLlmtrProfile(activeProfile)
|
||||
const isCommandcodeProfileFlag = isCommandcodeProfile(activeProfile)
|
||||
|
||||
@@ -1622,13 +1665,14 @@ function buildOpenAICompatibleStartupEnv(
|
||||
activeProfile.apiKey &&
|
||||
!withholdRetargetedApismartCredential &&
|
||||
!withholdRetargetedConcentrateCredential &&
|
||||
!withholdRetargetedApiRouteCredential &&
|
||||
!withholdRetargetedLlmtrCredential &&
|
||||
!withholdRetargetedCommandcodeCredential
|
||||
) {
|
||||
const strictEnv = buildOpenAIProfileEnv({
|
||||
goal: 'balanced',
|
||||
model: activeProfile.model,
|
||||
baseUrl: activeProfile.baseUrl,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
apiKey: activeProfile.apiKey,
|
||||
apiFormat: activeProfile.apiFormat,
|
||||
azureStyle: activeProfile.azureStyle ? '1' : undefined,
|
||||
@@ -1665,6 +1709,10 @@ function buildOpenAICompatibleStartupEnv(
|
||||
if (isConcentrateProfileFlag) {
|
||||
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) {
|
||||
strictEnv.LLMTR_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
@@ -1698,7 +1746,7 @@ function buildOpenAICompatibleStartupEnv(
|
||||
}
|
||||
|
||||
const env: ProfileEnv = {
|
||||
OPENAI_BASE_URL: activeProfile.baseUrl,
|
||||
OPENAI_BASE_URL: normalizedBaseUrl,
|
||||
OPENAI_MODEL: getPrimaryModel(activeProfile.model),
|
||||
...(activeProfile.apiFormat ? { OPENAI_API_FORMAT: activeProfile.apiFormat } : {}),
|
||||
...(activeProfile.azureStyle ? { OPENAI_AZURE_STYLE: '1' } : {}),
|
||||
@@ -1732,6 +1780,9 @@ function buildOpenAICompatibleStartupEnv(
|
||||
if (activeProfileRouteId === 'concentrate') {
|
||||
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'concentrate'
|
||||
}
|
||||
if (activeProfileRouteId === 'api-route') {
|
||||
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'api-route'
|
||||
}
|
||||
if (activeProfileRouteId === 'llmtr') {
|
||||
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'llmtr'
|
||||
}
|
||||
@@ -1742,6 +1793,7 @@ function buildOpenAICompatibleStartupEnv(
|
||||
activeProfile.apiKey &&
|
||||
!withholdRetargetedApismartCredential &&
|
||||
!withholdRetargetedConcentrateCredential &&
|
||||
!withholdRetargetedApiRouteCredential &&
|
||||
!withholdRetargetedLlmtrCredential &&
|
||||
!withholdRetargetedCommandcodeCredential
|
||||
) {
|
||||
@@ -1773,6 +1825,9 @@ function buildOpenAICompatibleStartupEnv(
|
||||
if (isConcentrateProfileFlag) {
|
||||
env.CONCENTRATE_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (isApiRouteProfileFlag) {
|
||||
env.API_ROUTE_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (isLlmtrProfileFlag) {
|
||||
env.LLMTR_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
@@ -1801,6 +1856,12 @@ function buildOpenAICompatibleStartupEnv(
|
||||
if (isCloudflareBaseUrl(activeProfile.baseUrl)) {
|
||||
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 {
|
||||
delete env.OPENAI_API_KEY
|
||||
}
|
||||
@@ -2033,6 +2094,9 @@ function triggerStartupDiscoveryRefreshForProfile(
|
||||
if (route.routeId === 'apismart' && !isApismartProfile(profile)) {
|
||||
return
|
||||
}
|
||||
if (route.routeId === 'api-route' && !isApiRouteProfile(profile)) {
|
||||
return
|
||||
}
|
||||
if (route.routeId === 'llmtr' && !isLlmtrProfile(profile)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ const ENV_KEYS = [
|
||||
'COMMANDCODE_API_KEY',
|
||||
'COMMAND_CODE_API_KEY',
|
||||
'APISMART_API_KEY',
|
||||
'API_ROUTE_API_KEY',
|
||||
'API_ROUTE_MODEL',
|
||||
'CONCENTRATE_API_KEY',
|
||||
'CONCENTRATE_BASE_URL',
|
||||
'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 () => {
|
||||
const providerSecret = 'ogw-provider-secret'
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getRouteCredentialValue,
|
||||
getRouteDescriptor,
|
||||
getRouteDefaultModel,
|
||||
isCanonicalApiRouteInferenceBaseUrl,
|
||||
isCanonicalApismartInferenceBaseUrl,
|
||||
isCanonicalConcentrateInferenceBaseUrl,
|
||||
isCanonicalCommandcodeInferenceBaseUrl,
|
||||
@@ -144,6 +145,7 @@ function hasUsableCredentialEnvValue(
|
||||
envVar === 'OPENAI_API_KEY' ||
|
||||
envVar === 'AIMLAPI_API_KEY' ||
|
||||
envVar === 'APISMART_API_KEY' ||
|
||||
envVar === 'API_ROUTE_API_KEY' ||
|
||||
envVar === 'CONCENTRATE_API_KEY' ||
|
||||
envVar === 'LLMTR_API_KEY' ||
|
||||
envVar === 'CMD_API_KEY' ||
|
||||
@@ -270,12 +272,13 @@ function getRuntimeValidationTarget(
|
||||
return enabledTarget
|
||||
}
|
||||
|
||||
// The documented CONCENTRATE_API_KEY-only setup is routed before the client
|
||||
// applies its default base URL. Select its descriptor directly so startup
|
||||
// validates the dedicated credential, including a noncanonical dedicated
|
||||
// base URL, instead of returning early for an unset OpenAI mode.
|
||||
if (resolveActiveRouteIdFromEnv(env) === 'concentrate') {
|
||||
return validationTargets.find(target => target.descriptor.id === 'concentrate')
|
||||
// The documented CONCENTRATE_API_KEY-only and API_ROUTE_API_KEY-only setup
|
||||
// is routed before the client applies its default base URL. Select its descriptor
|
||||
// directly so startup validates the dedicated credential, including a noncanonical
|
||||
// dedicated base URL, instead of returning early for an unset OpenAI mode.
|
||||
const activeRouteId = resolveActiveRouteIdFromEnv(env)
|
||||
if (activeRouteId === 'concentrate' || activeRouteId === 'api-route') {
|
||||
return validationTargets.find(target => target.descriptor.id === activeRouteId)
|
||||
}
|
||||
|
||||
if (!useOpenAI) {
|
||||
@@ -306,6 +309,8 @@ function getRuntimeValidationTarget(
|
||||
!isCanonicalApismartInferenceBaseUrl(request.baseUrl)) ||
|
||||
(target.descriptor.id === 'concentrate' &&
|
||||
!isCanonicalConcentrateInferenceBaseUrl(request.baseUrl)) ||
|
||||
(target.descriptor.id === 'api-route' &&
|
||||
!isCanonicalApiRouteInferenceBaseUrl(request.baseUrl)) ||
|
||||
(target.descriptor.id === 'commandcode' &&
|
||||
!isCanonicalCommandcodeInferenceBaseUrl(request.baseUrl)))
|
||||
) {
|
||||
@@ -435,6 +440,17 @@ async function getDescriptorValidationError(
|
||||
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) {
|
||||
case 'credential-env':
|
||||
return getCredentialEnvValidationError(validation, env, options.request)
|
||||
|
||||
@@ -204,6 +204,14 @@ export const providers: Provider[] = [
|
||||
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.',
|
||||
},
|
||||
{
|
||||
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',
|
||||
name: 'Cloudflare Workers AI',
|
||||
|
||||
Reference in New Issue
Block a user