fix(apismart): centralize route capability contracts

This commit is contained in:
jatmn
2026-08-10 14:56:08 -07:00
parent 3194fe36e3
commit f8cd975e68
33 changed files with 1236 additions and 150 deletions
+1
View File
@@ -268,6 +268,7 @@ Advanced and source-build guides:
| OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers |
| Z.AI GLM Coding Plan | `/provider` or OpenAI-compatible env vars | Uses `OPENAI_API_KEY` at `https://api.z.ai/api/coding/paas/v4` and defaults to `glm-5.2` |
| AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog |
| 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 |
| 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` |
+3
View File
@@ -127,6 +127,7 @@ const PRESET_ORDER = [
'Anthropic',
'Alibaba Coding Plan (China)',
'Alibaba Coding Plan',
'ApiSmart',
'Atlas Cloud',
'Azure OpenAI',
'Bankr',
@@ -692,6 +693,8 @@ test('ProviderManager shows API mode picker for custom OpenAI-compatible provide
await waitForFrameOutput(mounted.getOutput, frame =>
frame.includes('Base URL'),
)
mounted.stdin.write('\x7f'.repeat(64))
mounted.stdin.write('https://proxy.example/v1')
mounted.stdin.write('\r')
await waitForFrameOutput(mounted.getOutput, frame =>
frame.includes('Default model'),
+7 -17
View File
@@ -43,8 +43,8 @@ import {
routeShowsAuthHeader,
routeShowsAuthHeaderValue,
routeShowsCustomHeaders,
resolveProfileCapabilityRouteId,
resolveProfileRoute,
resolveRouteIdFromBaseUrl,
} from '../integrations/index.js'
import {
provisionAimlapiKey,
@@ -337,18 +337,6 @@ function getGithubCredentialSourceFromEnv(
return 'none'
}
function resolveProviderEditorRouteId(
provider: ProviderProfile['provider'],
baseUrl?: string,
): string {
const route = resolveProfileRoute(provider).routeId
if (route !== 'openai') {
return route
}
return resolveRouteIdFromBaseUrl(baseUrl) ?? route
}
function routeSupportsResponsesModel(routeId: string, model: string): boolean {
return openAIShimSupportsApiFormatForModel(
getRouteDescriptor(routeId)?.transportConfig.openaiShim,
@@ -885,7 +873,7 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
const formSteps = React.useMemo(
() => {
const routeId = resolveProviderEditorRouteId(draftProvider, draft.baseUrl)
const routeId = resolveProfileCapabilityRouteId(draftProvider, draft.baseUrl)
const showsAuthHeader = routeShowsAuthHeader(routeId)
const showsAuthHeaderValue = routeShowsAuthHeaderValue(routeId)
const showsCustomHeaders = routeShowsCustomHeaders(routeId)
@@ -1659,7 +1647,7 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
setErrorMessage('Base URL must be a real Anthropic-compatible endpoint.')
return
}
const routeId = resolveProviderEditorRouteId(provider, nextDraft.baseUrl)
const routeId = resolveProfileCapabilityRouteId(provider, nextDraft.baseUrl)
const supportsApiFormat = routeSupportsApiFormatSelection(routeId)
const showsAuthHeader = routeShowsAuthHeader(routeId)
const showsAuthHeaderValue = routeShowsAuthHeaderValue(routeId)
@@ -1768,7 +1756,7 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
nextDraft: ProviderDraft,
provider: ProviderProfile['provider'],
): ProviderDraft {
const routeId = resolveProviderEditorRouteId(provider, nextDraft.baseUrl)
const routeId = resolveProfileCapabilityRouteId(provider, nextDraft.baseUrl)
const preferredResponsesMode = nextDraft.apiFormat === 'responses_compat' ? 'responses_compat' : 'responses'
const apiFormat =
routeSupportsApiFormatSelection(routeId) &&
@@ -2198,7 +2186,9 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
Provider type:{' '}
{getRouteProviderTypeLabel(resolveProfileRoute(draftProvider).routeId)}
</Text>
{routeSupportsCustomHeaders(resolveProfileRoute(draftProvider).routeId) ? (
{routeSupportsCustomHeaders(
resolveProfileCapabilityRouteId(draftProvider, draft.baseUrl),
) ? (
<Text dimColor>
Advanced: this provider supports custom request headers when you
need them.
+33
View File
@@ -45,6 +45,9 @@ const ENV_KEYS = [
'OPENAI_BASE_URL',
'OPENAI_API_KEY',
'OPENAI_MODEL',
'APISMART_API_KEY',
'APISMART_MODEL',
'CLAUDE_CODE_PROVIDER_ROUTE_ID',
'GEMINI_MODEL',
'MISTRAL_MODEL',
'ANTHROPIC_MODEL',
@@ -52,6 +55,14 @@ const ENV_KEYS = [
'NVIDIA_NIM',
'MINIMAX_API_KEY',
'XAI_API_KEY',
'MIMO_API_KEY',
'VENICE_API_KEY',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'LONGCAT_API_KEY',
'CLINE_API_KEY',
'CLINE_API_MODEL',
'AIMLAPI_API_KEY',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
@@ -358,6 +369,28 @@ describe('detectProvider — rawModel fallback when URL is generic', () => {
// --- Explicit env flags win over URL heuristics ---
describe('detectProvider — explicit dedicated-provider env flags', () => {
test('credential-only ApiSmart setup uses route defaults before client initialization', () => {
process.env.APISMART_API_KEY = 'apismart-test-key'
process.env.APISMART_MODEL = 'KIMI_K3'
expect(detectProvider()).toEqual({
name: 'ApiSmart',
model: 'KIMI_K3',
baseUrl: 'https://gw.apismart.ai/v1',
isLocal: false,
})
})
test('labels the resolved ApiSmart route when another dedicated key is ambient', () => {
process.env.APISMART_API_KEY = 'apismart-test-key'
process.env.MINIMAX_API_KEY = 'ambient-minimax-key'
expect(detectProvider()).toMatchObject({
name: 'ApiSmart',
baseUrl: 'https://gw.apismart.ai/v1',
})
})
test('NVIDIA_NIM=1 overrides aggregator URL', () => {
setupOpenAIMode('https://openrouter.ai/api/v1', 'some-nim-model')
process.env.NVIDIA_NIM = '1'
+12 -3
View File
@@ -9,6 +9,7 @@ import { isLocalProviderUrl, resolveProviderRequest } from '../services/api/prov
import {
getRouteLabel,
isMiniMaxBaseUrl,
resolveEnvOnlyProviderRouteId,
resolveRouteIdFromBaseUrl,
} from '../integrations/routeMetadata.js'
import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js'
@@ -80,7 +81,11 @@ const LOGO_CLAUDE = [
export function detectProvider(modelOverride?: string): { name: string; model: string; baseUrl: string; isLocal: boolean } {
const useGemini = process.env.CLAUDE_CODE_USE_GEMINI === '1' || process.env.CLAUDE_CODE_USE_GEMINI === 'true'
const useGithub = process.env.CLAUDE_CODE_USE_GITHUB === '1' || process.env.CLAUDE_CODE_USE_GITHUB === 'true'
const useOpenAI = process.env.CLAUDE_CODE_USE_OPENAI === '1' || process.env.CLAUDE_CODE_USE_OPENAI === 'true'
const envOnlyProviderRouteId = resolveEnvOnlyProviderRouteId(process.env)
const useOpenAI =
process.env.CLAUDE_CODE_USE_OPENAI === '1' ||
process.env.CLAUDE_CODE_USE_OPENAI === 'true' ||
envOnlyProviderRouteId === 'apismart'
const useMistral = process.env.CLAUDE_CODE_USE_MISTRAL === '1' || process.env.CLAUDE_CODE_USE_MISTRAL === 'true'
if (useGemini) {
@@ -103,17 +108,21 @@ export function detectProvider(modelOverride?: string): { name: string; model: s
}
if (useOpenAI) {
const rawModel = modelOverride || process.env.OPENAI_MODEL || 'gpt-4o'
const resolvedRequest = resolveProviderRequest({
model: rawModel,
model: modelOverride,
baseUrl: process.env.OPENAI_BASE_URL,
fallbackModel:
envOnlyProviderRouteId === 'apismart' ? undefined : 'gpt-4o',
processEnv: process.env,
})
const rawModel = resolvedRequest.requestedModel
const baseUrl = resolvedRequest.baseUrl
const isLocal = isLocalProviderUrl(baseUrl)
const routeId = resolveRouteIdFromBaseUrl(baseUrl)
let name = 'OpenAI'
// Explicit dedicated-provider env flags win.
if (process.env.NVIDIA_NIM) name = 'NVIDIA NIM'
else if (routeId === 'apismart') name = getRouteLabel(routeId) ?? name
else if (process.env.MINIMAX_API_KEY) name = 'MiniMax'
else if (
resolvedRequest.transport === 'codex_responses' ||
+27
View File
@@ -110,6 +110,33 @@ afterEach(() => {
})
describe('discoverModelsForRoute', () => {
test('drops caller headers when the route disables custom headers', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
let capturedHeaders: Headers | undefined
setMockFetch(mock((_input: string | URL | Request, init?: RequestInit) => {
capturedHeaders = new Headers(init?.headers)
return Promise.resolve(
new Response(
JSON.stringify({ data: [{ id: 'KIMI_K3' }] }),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
),
)
}) as unknown as typeof globalThis.fetch)
const result = await discoverModelsForRoute('apismart', {
forceRefresh: true,
apiKey: 'apismart-test-key',
headers: { 'X-Proxy-Secret': 'stale-secret' },
})
expect(result?.source).toBe('network')
expect(capturedHeaders?.get('x-proxy-secret')).toBeNull()
expect(capturedHeaders?.get('authorization')).toBe(
'Bearer apismart-test-key',
)
})
test('uses built-in openai-compatible discovery and caches results for dynamic routes', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
+2 -1
View File
@@ -181,7 +181,8 @@ export function getRouteDiscoveryHeaders(
): Record<string, string> | undefined {
const transportConfig = getRouteDescriptor(routeId)?.transportConfig
const acceptsCallerHeaders =
getRouteCatalog(routeId)?.discovery?.requiresAuth !== false
getRouteCatalog(routeId)?.discovery?.requiresAuth !== false &&
transportConfig?.openaiShim?.supportsAuthHeaders !== false
// Descriptor headers are attribution, not transport plumbing: an `aimlapi`
// profile keeps its route id while pointing at a user-controlled proxy, so the
// `/models` request must be filtered on the same canonical predicate the
+1
View File
@@ -142,6 +142,7 @@ export {
isLongcatBaseUrl,
normalizeXiaomiMimoBaseUrl,
resolveActiveRouteIdFromEnv,
resolveProfileCapabilityRouteId,
resolveRouteIdFromBaseUrl,
routeSupportsApiFormatSelection,
routeSupportsAuthHeaders,
+94
View File
@@ -6,14 +6,98 @@ import {
getRouteDefaultBaseUrl,
getRouteDefaultModel,
getRouteProviderTypeLabel,
getUsableRouteConfigEnvValue,
hasUsableRouteCredentialEnvValue,
isApismartBaseUrl,
isCloudflareBaseUrl,
isLongcatBaseUrl,
profileTargetsRoute,
resolveActiveRouteIdFromEnv,
resolveProfileCapabilityRouteId,
resolveRouteCredentialValue,
resolveRouteIdFromBaseUrl,
} from './routeMetadata.js'
test('profile capability routing keeps provider identity and endpoint boundaries together', () => {
expect(resolveProfileCapabilityRouteId('apismart')).toBe('apismart')
expect(resolveProfileCapabilityRouteId('openai')).toBe('openai')
expect(
resolveProfileCapabilityRouteId(
'custom',
'http://localhost:11434/v1',
),
).toBe('ollama')
expect(
resolveProfileCapabilityRouteId(
'custom',
'https://gw.apismart.ai/v1',
),
).toBe('apismart')
expect(
resolveProfileCapabilityRouteId(
'custom',
'https://api.aimlapi.com/v1',
),
).toBe('aimlapi')
expect(
resolveProfileCapabilityRouteId(
'gemini',
'https://gw.apismart.ai/v1',
),
).toBe('gemini')
expect(
resolveProfileCapabilityRouteId(
'openai',
'https://gw.apismart.ai/v1',
),
).toBe('apismart')
expect(
resolveProfileCapabilityRouteId(
'apismart',
'https://proxy.example/v1',
),
).toBe('custom')
})
test('profile endpoint routing separates credential ownership from editor capabilities', () => {
expect(profileTargetsRoute('apismart', undefined, 'apismart')).toBe(true)
expect(profileTargetsRoute('openai', undefined, 'apismart')).toBe(false)
expect(
profileTargetsRoute('custom', 'https://gw.apismart.ai/v1', 'apismart'),
).toBe(true)
expect(
profileTargetsRoute('apismart', 'https://proxy.example/v1', 'apismart'),
).toBe(false)
})
test('route credential usability applies OpenAI placeholder rules to dedicated keys', () => {
expect(hasUsableRouteCredentialEnvValue('APISMART_API_KEY', 'SUA_CHAVE')).toBe(
false,
)
expect(
hasUsableRouteCredentialEnvValue('APISMART_API_KEY', 'apismart-key'),
).toBe(true)
})
test('route config normalization rejects nullish environment sentinels', () => {
expect(getUsableRouteConfigEnvValue(' https://proxy.example/v1 ')).toBe(
'https://proxy.example/v1',
)
for (const value of [undefined, '', ' undefined ', 'NULL']) {
expect(getUsableRouteConfigEnvValue(value)).toBeUndefined()
}
})
test('ApiSmart env-only intent respects an explicit OpenAI-compatible opt-out', () => {
expect(
resolveActiveRouteIdFromEnv({
CLAUDE_CODE_USE_OPENAI: '0',
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: 'KIMI_K3',
}),
).toBe('anthropic')
})
test('isCloudflareBaseUrl matches Workers AI host but not the shared AI Gateway', () => {
// Workers AI lives on api.cloudflare.com.
expect(
@@ -476,6 +560,16 @@ test('resolveActiveRouteIdFromEnv treats ApiSmart credential-only env as ApiSmar
).toBe('apismart')
})
test('resolveActiveRouteIdFromEnv treats the generic OpenAI flag as compatible with ApiSmart intent', () => {
expect(
resolveActiveRouteIdFromEnv({
CLAUDE_CODE_USE_OPENAI: '1',
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: 'KIMI_K3',
}),
).toBe('apismart')
})
test('resolveActiveRouteIdFromEnv ignores placeholder ApiSmart credentials', () => {
expect(
resolveActiveRouteIdFromEnv({
+132 -11
View File
@@ -205,7 +205,7 @@ function readFirstNonEmptyEnvValue(
): string | undefined {
for (const envVar of envVars) {
const value = processEnv[envVar]
if (hasUsableEnvCredentialValue(envVar, value)) {
if (hasUsableRouteCredentialEnvValue(envVar, value)) {
return value!.trim()
}
}
@@ -213,7 +213,7 @@ function readFirstNonEmptyEnvValue(
return undefined
}
function hasUsableEnvCredentialValue(
export function hasUsableRouteCredentialEnvValue(
envVar: string,
value: string | undefined,
): boolean {
@@ -221,11 +221,20 @@ function hasUsableEnvCredentialValue(
return false
}
if (envVar === 'APISMART_API_KEY') {
const normalized = value.trim().toLowerCase()
return (
normalized !== '' &&
normalized !== 'sua_chave' &&
normalized !== 'null' &&
normalized !== 'undefined'
)
}
if (
envVar === 'OPENAI_API_KEYS' ||
envVar === 'OPENAI_API_KEY' ||
envVar === 'AIMLAPI_API_KEY' ||
envVar === 'APISMART_API_KEY'
envVar === 'AIMLAPI_API_KEY'
) {
return hasUsableOpenAICredential(value)
}
@@ -234,8 +243,8 @@ function hasUsableEnvCredentialValue(
function hasAnyUsableOpenAICredential(processEnv: NodeJS.ProcessEnv): boolean {
return (
hasUsableEnvCredentialValue('OPENAI_API_KEYS', processEnv.OPENAI_API_KEYS) ||
hasUsableEnvCredentialValue('OPENAI_API_KEY', processEnv.OPENAI_API_KEY)
hasUsableRouteCredentialEnvValue('OPENAI_API_KEYS', processEnv.OPENAI_API_KEYS) ||
hasUsableRouteCredentialEnvValue('OPENAI_API_KEY', processEnv.OPENAI_API_KEY)
)
}
@@ -244,6 +253,32 @@ function hasNonEmptyEnvValue(value: string | undefined): boolean {
return Boolean(trimmed && trimmed !== 'undefined' && trimmed !== 'null')
}
export function getUsableRouteConfigEnvValue(
value: string | undefined,
): string | undefined {
const trimmed = value?.trim()
if (!trimmed) return undefined
const normalized = trimmed.toLowerCase()
return normalized === 'undefined' || normalized === 'null'
? undefined
: trimmed
}
export function getUsableRouteModelEnvValue(
value: string | undefined,
): string | undefined {
return getUsableRouteConfigEnvValue(value)
}
export function hasExplicitOpenAICompatibleOptOut(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
processEnv.CLAUDE_CODE_USE_OPENAI !== undefined &&
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)
)
}
export function isMiniMaxBaseUrl(value: string | undefined): boolean {
const trimmed = value?.trim()
if (!trimmed) {
@@ -661,7 +696,13 @@ function hasNoExplicitNonOpenAIProvider(
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_MISTRAL) &&
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_BEDROCK) &&
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_VERTEX) &&
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_FOUNDRY)
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_FOUNDRY) &&
!(
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI) &&
hasNonEmptyEnvValue(processEnv.ANTHROPIC_BASE_URL) &&
(hasNonEmptyEnvValue(processEnv.ANTHROPIC_AUTH_TOKEN) ||
hasNonEmptyEnvValue(processEnv.ANTHROPIC_API_KEY))
)
)
}
@@ -816,13 +857,35 @@ export function hasClinePassEnvOnlyProviderIntent(
)
}
function hasApismartProviderIntent(
processEnv: NodeJS.ProcessEnv,
hasCredential: boolean,
): boolean {
const explicitRouteId = processEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID?.trim()
return (
hasCredential &&
(!explicitRouteId || explicitRouteId === 'apismart') &&
!hasExplicitOpenAICompatibleOptOut(processEnv) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isApismartBaseUrl) &&
hasNoExplicitNonOpenAIProvider(processEnv)
)
}
export function hasConfiguredApismartProviderIntent(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return hasApismartProviderIntent(
processEnv,
Boolean(processEnv.APISMART_API_KEY?.trim()),
)
}
export function hasApismartEnvOnlyProviderIntent(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
hasUsableOpenAICredential(processEnv.APISMART_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isApismartBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
return hasApismartProviderIntent(
processEnv,
hasUsableOpenAICredential(processEnv.APISMART_API_KEY),
)
}
@@ -1115,6 +1178,64 @@ function profileRouteHonorsBaseUrlBoundary(
return true
}
/**
* Reports whether a profile's endpoint belongs to a specific route. A concrete
* endpoint owns credential routing regardless of the editor label; without an
* endpoint, only the declared provider may supply that route's default.
*/
export function profileTargetsRoute(
provider: string,
baseUrl: string | undefined,
targetRouteId: string,
): boolean {
const trimmedBaseUrl = baseUrl?.trim()
if (trimmedBaseUrl) {
return resolveRouteIdFromBaseUrl(trimmedBaseUrl) === targetRouteId
}
return resolveProfileRoute(provider).routeId === targetRouteId
}
/**
* Resolves the route whose capabilities apply to a saved or edited profile.
* A concrete endpoint refines OpenAI-compatible profiles, while native
* provider families retain their declared transport contract. An
* endpoint-scoped provider retargeted elsewhere becomes a generic route.
*/
export function resolveProfileCapabilityRouteId(
provider: string,
baseUrl?: string,
): string {
const providerRouteId = resolveProfileRoute(provider).routeId
if (providerRouteId === 'custom-anthropic') {
return providerRouteId
}
const routeIdFromBaseUrl = resolveRouteIdFromBaseUrl(baseUrl)
if (providerRouteId === 'custom') {
return routeIdFromBaseUrl ?? providerRouteId
}
const providerTransportKind = getTransportKindForRoute(providerRouteId)
if (
providerTransportKind !== 'openai-compatible' &&
providerTransportKind !== 'local'
) {
return providerRouteId
}
if (routeIdFromBaseUrl) {
return routeIdFromBaseUrl
}
if (
baseUrl?.trim() &&
!profileRouteHonorsBaseUrlBoundary(providerRouteId, baseUrl)
) {
return 'custom'
}
return providerRouteId
}
export function resolveActiveRouteIdFromEnv(
processEnv: NodeJS.ProcessEnv = process.env,
options?: {
+30
View File
@@ -10,6 +10,8 @@ import {
resolveOutOfProcessTeammateProviderFromCliArgs,
shouldEnforceModelAllowlist,
} from './agentRouting.js'
import { resolveRouteCredentialValue } from '../../integrations/routeMetadata.js'
import { resolveProviderRequest } from './providerConfig.js'
import { getAgentModel } from '../../utils/model/agent.js'
import * as agentModelModule from '../../utils/model/agent.js'
import type { SettingsJson } from '../../utils/settings/types.js'
@@ -731,6 +733,34 @@ describe('applyAgentProviderOverrideToEnv', () => {
expect(env.GEMINI_API_KEY).toBe('gemini-key')
expect(env.ANTHROPIC_API_KEY).toBe('anthropic-key')
})
test('replaces parent ApiSmart model and credential for an ApiSmart agent override', () => {
const env: Record<string, string | undefined> = {
CLAUDE_CODE_USE_OPENAI: '1',
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'apismart',
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
OPENAI_MODEL: 'KIMI_K3',
OPENAI_API_KEY: 'parent-secret',
APISMART_API_KEY: 'parent-secret',
APISMART_MODEL: 'KIMI_K3',
}
applyAgentProviderOverrideToEnv(
{
model: 'GLM_5.2',
baseURL: 'https://gw.apismart.ai/v1',
apiKey: 'child-secret',
},
env,
)
expect(resolveProviderRequest({ processEnv: env }).requestedModel).toBe('GLM_5.2')
expect(
resolveRouteCredentialValue({ routeId: 'apismart', processEnv: env }),
).toBe('child-secret')
expect(env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
expect(env.APISMART_MODEL).toBeUndefined()
})
})
describe('shouldEnforceModelAllowlist', () => {
+19
View File
@@ -2,6 +2,11 @@ import type { SettingsJson } from '../../utils/settings/types.js'
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
import { getAgentModel } from '../../utils/model/agent.js'
import { isModelAlias } from '../../utils/model/aliases.js'
import {
getRouteCredentialEnvVars,
getRouteDescriptor,
resolveRouteIdFromBaseUrl,
} from '../../integrations/routeMetadata.js'
/**
* Provider override resolved from agent routing config.
@@ -44,6 +49,7 @@ const PROVIDER_ENV_VARS_TO_CLEAR_FOR_OVERRIDE = [
'CLAUDE_CODE_USE_MISTRAL',
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED',
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID',
'CLAUDE_CODE_PROVIDER_ROUTE_ID',
'NVIDIA_NIM',
'ANTHROPIC_MODEL',
'ANTHROPIC_BASE_URL',
@@ -57,6 +63,8 @@ const PROVIDER_ENV_VARS_TO_CLEAR_FOR_OVERRIDE = [
'OPENAI_AUTH_HEADER',
'OPENAI_AUTH_SCHEME',
'OPENAI_AUTH_HEADER_VALUE',
'APISMART_API_KEY',
'APISMART_MODEL',
] as const
/**
@@ -362,4 +370,15 @@ export function applyAgentProviderOverrideToEnv(
env.OPENAI_MODEL = providerOverride.model
env.OPENAI_BASE_URL = providerOverride.baseURL
env.OPENAI_API_KEY = providerOverride.apiKey
const routeId = resolveRouteIdFromBaseUrl(providerOverride.baseURL)
if (routeId) {
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = routeId
const descriptor = getRouteDescriptor(routeId)
if (descriptor?.setup.dedicatedCredentialsOnly) {
for (const credentialEnvVar of getRouteCredentialEnvVars(routeId)) {
env[credentialEnvVar] = providerOverride.apiKey
}
}
}
}
+122 -1
View File
@@ -6,6 +6,7 @@ import {
registerGateway,
} from '../../integrations/index.js'
import { publicBuildVersion } from '../../utils/version.js'
import { resolveEnvOnlyProviderRouteId } from '../../integrations/routeMetadata.js'
// bun:test keeps mock.module() registrations process-global across test files.
// Load and re-register the real module before importing the client so a prior
@@ -317,7 +318,7 @@ test('first-party Anthropic requests execute the configured fetch wrapper withou
expect(capturedHeaders).toBeDefined()
})
test('routes a custom Anthropic endpoint with ANTHROPIC_AUTH_TOKEN without requiring an API key', async () => {
test('routes a custom Anthropic endpoint with ANTHROPIC_AUTH_TOKEN despite an ambient ApiSmart key', async () => {
let capturedUrl: string | undefined
let capturedHeaders: Headers | undefined
@@ -331,10 +332,13 @@ test('routes a custom Anthropic endpoint with ANTHROPIC_AUTH_TOKEN without requi
process.env.ANTHROPIC_API_KEY = 'must-not-forward'
process.env.ANTHROPIC_AUTH_TOKEN = 'custom-anthropic-token'
process.env.ANTHROPIC_BASE_URL = 'https://anthropic.example/api/v1'
process.env.APISMART_API_KEY = 'ambient-apismart-key'
process.env.USER_TYPE = 'ant'
process.env.USE_STAGING_OAUTH = '1'
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Tenant: tenant-a\nauthorization: stale-value'
expect(resolveEnvOnlyProviderRouteId(process.env)).toBeNull()
const fetchOverride = (async (input, init) => {
capturedUrl =
typeof input === 'string'
@@ -1536,6 +1540,123 @@ test('strips Anthropic-specific custom headers before sending OpenAI-compatible
expect(capturedHeaders?.get('authorization')).toBe('Bearer openai-test-key')
})
test('does not forward ambient custom headers to routes that disable custom headers', async () => {
let capturedHeaders: Headers | undefined
let capturedUrl: string | undefined
clearEnvForMiniMaxOnlyTest()
process.env.APISMART_API_KEY = 'apismart-test-key'
process.env.APISMART_MODEL = 'KIMI_K3'
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Proxy-Secret: stale-secret'
globalThis.fetch = (async (input, init) => {
capturedUrl =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url
capturedHeaders = new Headers(init?.headers)
return new Response(
JSON.stringify({
id: 'chatcmpl-apismart',
model: 'KIMI_K3',
choices: [
{
message: { role: 'assistant', content: 'ok' },
finish_reason: 'stop',
},
],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as unknown as FetchType
const client = (await getAnthropicClient({
maxRetries: 0,
model: 'KIMI_K3',
})) as unknown as ShimClient
await client.beta.messages.create({
model: 'KIMI_K3',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 64,
stream: false,
})
expect(capturedUrl).toBe('https://gw.apismart.ai/v1/chat/completions')
expect(capturedHeaders?.get('x-proxy-secret')).toBeNull()
expect(capturedHeaders?.get('authorization')).toBe('Bearer apismart-test-key')
})
test.each([
['OPENAI_BASE_URL', 'null'],
['OPENAI_BASE_URL', ' UNDEFINED '],
['OPENAI_API_BASE', 'NULL'],
['OPENAI_API_BASE', ' undefined '],
] as const)(
'ApiSmart env-only defaults ignore nullish %s value %s',
async (envVar, sentinel) => {
clearEnvForMiniMaxOnlyTest()
process.env.APISMART_API_KEY = 'apismart-test-key'
process.env.APISMART_MODEL = 'KIMI_K3'
process.env[envVar] = sentinel
await getAnthropicClient({ maxRetries: 0, model: 'KIMI_K3' })
expect(process.env.OPENAI_BASE_URL).toBe('https://gw.apismart.ai/v1')
},
)
test('enforces route header capabilities for an ApiSmart provider override', async () => {
let capturedHeaders: Headers | undefined
clearEnvForMiniMaxOnlyTest()
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Proxy-Secret: stale-secret'
process.env.OPENAI_AUTH_HEADER = 'X-Parent-Secret'
process.env.OPENAI_AUTH_SCHEME = 'raw'
process.env.OPENAI_AUTH_HEADER_VALUE = 'stale-parent-token'
globalThis.fetch = (async (_input, init) => {
capturedHeaders = new Headers(init?.headers)
return new Response(
JSON.stringify({
id: 'chatcmpl-apismart-override',
model: 'KIMI_K3',
choices: [
{
message: { role: 'assistant', content: 'ok' },
finish_reason: 'stop',
},
],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as unknown as FetchType
const client = (await getAnthropicClient({
maxRetries: 0,
providerOverride: {
model: 'KIMI_K3',
baseURL: 'https://gw.apismart.ai/v1',
apiKey: 'child-apismart-key',
},
})) as unknown as ShimClient
await client.beta.messages.create({
model: 'unused',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 64,
stream: false,
})
expect(capturedHeaders?.get('x-proxy-secret')).toBeNull()
expect(capturedHeaders?.get('x-parent-secret')).toBeNull()
expect(capturedHeaders?.get('authorization')).toBe(
'Bearer child-apismart-key',
)
})
test('strips Anthropic-specific custom headers on providerOverride shim requests too', async () => {
let capturedHeaders: Headers | undefined
+19 -6
View File
@@ -45,9 +45,13 @@ import {
getNearaiBaseUrlOverride,
getRouteDefaultBaseUrl,
getRouteDefaultModel,
getUsableRouteConfigEnvValue,
getUsableRouteModelEnvValue,
getXaiBaseUrlOverride,
getXiaomiMimoBaseUrlOverride,
resolveEnvOnlyProviderRouteId,
resolveRouteIdFromBaseUrl,
routeSupportsCustomHeaders,
} from '../../integrations/routeMetadata.js'
import { resolveOpenAIShimRuntimeContext } from '../../integrations/runtimeMetadata.js'
import {
@@ -376,12 +380,12 @@ function applyAimlapiEnvOnlyDefaults(): void {
function applyApismartEnvOnlyDefaults(): void {
const baseUrlOverride =
process.env.OPENAI_BASE_URL?.trim() ||
process.env.OPENAI_API_BASE?.trim() ||
getUsableRouteConfigEnvValue(process.env.OPENAI_BASE_URL) ||
getUsableRouteConfigEnvValue(process.env.OPENAI_API_BASE) ||
undefined
const modelOverride =
process.env.APISMART_MODEL?.trim() ||
process.env.OPENAI_MODEL?.trim() ||
getUsableRouteModelEnvValue(process.env.APISMART_MODEL) ||
getUsableRouteModelEnvValue(process.env.OPENAI_MODEL) ||
undefined
process.env.CLAUDE_CODE_USE_OPENAI = '1'
@@ -489,7 +493,17 @@ export async function getAnthropicClient({
const containerId = process.env.CLAUDE_CODE_CONTAINER_ID
const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID
const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP
const customHeaders = getCustomHeaders()
const envOnlyProviderRouteId = resolveEnvOnlyProviderRouteId(process.env)
const outboundRouteId =
resolveRouteIdFromBaseUrl(
providerOverride?.baseURL ??
process.env.OPENAI_BASE_URL ??
process.env.OPENAI_API_BASE,
) ?? envOnlyProviderRouteId
const customHeaders =
outboundRouteId && !routeSupportsCustomHeaders(outboundRouteId)
? {}
: getCustomHeaders()
const defaultHeaders: { [key: string]: string } = {
'x-app': 'cli',
'User-Agent': getUserAgent(),
@@ -516,7 +530,6 @@ export async function getAnthropicClient({
defaultHeaders['x-anthropic-additional-protection'] = 'true'
}
const envOnlyProviderRouteId = resolveEnvOnlyProviderRouteId(process.env)
const useMiniMaxEnvOnlyProvider = shouldUseMiniMaxEnvOnlyProvider(
model,
envOnlyProviderRouteId,
+1 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from 'bun:test'
import { CredentialPool, firstUsableCredential, hasInvalidCredentialPlaceholder, parseCredentialList } from './credentialPool.js'
import { CredentialPool, firstUsableCredential, hasInvalidCredentialPlaceholder, hasUsableOpenAICredential, parseCredentialList } from './credentialPool.js'
test('parseCredentialList trims comma-separated keys', () => {
expect(parseCredentialList(' key-a, key-b ,,key-c ')).toEqual([
+6
View File
@@ -522,6 +522,12 @@ class OpenAIShimMessages {
runtimeShimContext.routeId === null ||
getRouteDescriptor(runtimeShimContext.routeId)?.setup
.dedicatedCredentialsOnly !== true,
routeSupportsAuthHeaders:
runtimeShimContext.descriptor?.transportConfig.openaiShim
?.supportsAuthHeaders !== false,
routeSupportsCustomHeaders:
runtimeShimContext.descriptor?.transportConfig.openaiShim
?.supportsAuthHeaders !== false,
getCredentialPool: value => this.getCredentialPool(value),
filterAnthropicHeaders, isGeminiMode, resolveRouteCredentialValue, isXaiBaseUrl, isLongcatBaseUrl, parseCredentialList, resolveXaiAccessToken, hasInvalidCredentialPlaceholder, buildOpenAICompatibilityErrorMessage, isAzureStyleBaseUrl, resolveGeminiCredential, COPILOT_HEADERS, getSessionId, getLocalProviderRetryBaseUrls, buildOllamaChatUrl, logForDebugging, redactUrlForDiagnostics, redactSecretValueForDisplay, headersWithRequestUrl, classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, markOpenAIRequestNonReplayable, fetchRequest: (url, init) => fetchWithHeadersDeadline(url, init, { callerSignal: options?.signal, timeoutMs: apiTimeoutMs }), isResponseHeadersTimeout: error => error instanceof ResponseHeadersTimeoutError, requestBodyContainsImages, formatRetryAfterHint, redactUrlsInMessage, sleepMs, shouldAttemptLocalToollessRetry, refreshCopilotTokenOn401, isCopilotTokenExpiredError, convertOllamaStreamingResponse, convertOllamaNonStreamingResponse, logApiCallStart, logApiCallEnd, stableStringifyJson, APIError, GITHUB_429_MAX_RETRIES, GITHUB_429_BASE_DELAY_SEC, GITHUB_429_MAX_DELAY_SEC, request, params, options, requestProcessEnv, fastPath, shimConfig, runtimeShimContext, body, effectiveTransport, useNativeOllamaChat, buildResponsesBody, serializeBody, isLocal, isGithub, isGithubCopilot, isGithubModels, omitTools,
})
@@ -585,7 +585,7 @@ test('OPENAI_API_KEYS rejects placeholder values before sending requests', async
max_tokens: 32,
stream: false,
}),
).rejects.toThrow(/SUA_CHAVE|Authentication failed/)
).rejects.toThrow(/invalid credential placeholder|Authentication failed/)
expect(authorizations).toEqual([])
})
+27 -5
View File
@@ -47,6 +47,8 @@ type RequestExecutorContext = {
defaultHeaders: Record<string, string>
providerOverride?: { apiKey?: string }
routeAcceptsGenericOpenAICredentials: boolean
routeSupportsAuthHeaders: boolean
routeSupportsCustomHeaders: boolean
getCredentialPool: (rawCredentials: string) => CredentialPool | null
filterAnthropicHeaders: (
headers?: Record<string, string>,
@@ -181,6 +183,8 @@ export async function executeOpenAIRequest(
defaultHeaders,
providerOverride,
routeAcceptsGenericOpenAICredentials,
routeSupportsAuthHeaders,
routeSupportsCustomHeaders,
getCredentialPool,
filterAnthropicHeaders,
isGeminiMode,
@@ -240,11 +244,25 @@ export async function executeOpenAIRequest(
isGithubCopilot,
isGithubModels,
} = context
const managedClientHeaders = Object.fromEntries(
Object.entries(defaultHeaders).filter(([name]) =>
[
'user-agent',
'x-app',
'x-client-app',
'x-claude-code-session-id',
'x-claude-remote-container-id',
'x-claude-remote-session-id',
].includes(name.toLowerCase()),
),
)
const baseHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...filterAnthropicHeaders(shimConfig.headers),
...defaultHeaders,
...filterAnthropicHeaders(options?.headers),
...(routeSupportsCustomHeaders ? defaultHeaders : managedClientHeaders),
...(routeSupportsCustomHeaders
? filterAnthropicHeaders(options?.headers)
: {}),
}
const isGemini = isGeminiMode()
@@ -331,7 +349,9 @@ export async function executeOpenAIRequest(
?.defaultAuthHeader
const configuredAuthHeaderValue = catalogAuthHeader
? undefined
: requestProcessEnv.OPENAI_AUTH_HEADER_VALUE?.trim()
: routeSupportsAuthHeaders
? requestProcessEnv.OPENAI_AUTH_HEADER_VALUE?.trim()
: undefined
if (configuredAuthHeaderValue && /[\r\n]/.test(configuredAuthHeaderValue)) {
throw new Error(
'OPENAI_AUTH_HEADER_VALUE must not contain CR/LF characters',
@@ -339,7 +359,9 @@ export async function executeOpenAIRequest(
}
const customAuthHeader = catalogAuthHeader
? undefined
: requestProcessEnv.OPENAI_AUTH_HEADER?.trim()
: routeSupportsAuthHeaders
? requestProcessEnv.OPENAI_AUTH_HEADER?.trim()
: undefined
const hasCustomAuthHeader = Boolean(
customAuthHeader && /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(customAuthHeader),
)
@@ -354,7 +376,7 @@ export async function executeOpenAIRequest(
401,
undefined,
buildOpenAICompatibilityErrorMessage(
'OpenAI API error 401: invalid credential pool placeholder SUA_CHAVE detected',
'OpenAI API error 401: invalid credential placeholder detected',
{
category: 'auth_invalid',
requestUrl: request.baseUrl,
+72
View File
@@ -340,6 +340,19 @@ test('resolveProviderRequest uses APISMART_MODEL when APISMART_API_KEY is presen
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
})
test('resolveProviderRequest keeps ApiSmart intent with the generic OpenAI compatibility flag', () => {
const request = resolveProviderRequest({
processEnv: {
CLAUDE_CODE_USE_OPENAI: '1',
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: 'KIMI_K3',
},
})
expect(request.requestedModel).toBe('KIMI_K3')
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
})
test('resolveProviderRequest falls back to OPENAI_MODEL for ApiSmart when APISMART_MODEL is unset', () => {
const request = resolveProviderRequest({
processEnv: {
@@ -365,6 +378,52 @@ test('resolveProviderRequest treats blank APISMART_MODEL as unset for ApiSmart',
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
})
test.each(['undefined', 'null', ' NULL '])(
'resolveProviderRequest treats APISMART_MODEL=%s as unset',
placeholder => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: placeholder,
},
})
expect(request.requestedModel).toBe('DEEPSEEK_V4_FLASH')
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
},
)
test.each(['undefined', 'null', ' NULL '])(
'resolveProviderRequest treats ApiSmart OPENAI_MODEL fallback %s as unset',
placeholder => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
OPENAI_MODEL: placeholder,
},
})
expect(request.requestedModel).toBe('DEEPSEEK_V4_FLASH')
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
},
)
test.each(['null', ' NULL ', 'undefined'])(
'resolveProviderRequest treats OPENAI_BASE_URL=%s as unset for ApiSmart',
placeholder => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: 'KIMI_K3',
OPENAI_BASE_URL: placeholder,
},
})
expect(request.requestedModel).toBe('KIMI_K3')
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
},
)
test('resolveProviderRequest uses the ApiSmart route default when no model env is set', () => {
const request = resolveProviderRequest({
processEnv: {
@@ -431,6 +490,19 @@ test('resolveProviderRequest ignores ApiSmart model when explicit OPENAI_BASE_UR
expect(request.baseUrl).toBe('https://api.openai.com/v1')
})
test('resolveProviderRequest ignores an ambient ApiSmart key for an explicit native provider', () => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: 'KIMI_K3',
CLAUDE_CODE_USE_BEDROCK: '1',
},
})
expect(request.requestedModel).toBe('codexplan')
expect(request.baseUrl).toBe('https://chatgpt.com/backend-api/codex')
})
test('resolveProviderRequest resolves the GPT-5.6 family Codex aliases', () => {
const sol = resolveProviderRequest({ model: 'gpt-5.6-sol', processEnv: {} })
expect(sol.resolvedModel).toBe('gpt-5.6-sol')
+10 -8
View File
@@ -26,10 +26,11 @@ import { getCatalogEntriesForRoute } from '../../integrations/registry.js'
import {
getRouteDefaultBaseUrl,
getRouteDefaultModel,
getUsableRouteModelEnvValue,
hasApismartEnvOnlyProviderIntent,
isApismartBaseUrl,
isClinePassBaseUrl,
} from '../../integrations/routeMetadata.js'
import { hasUsableOpenAICredential } from './credentialPool.js'
import {
openAIShimSupportsApiFormatForModel,
resolveOpenAIShimRuntimeContext,
@@ -235,13 +236,13 @@ function isPrivateIpv6Address(hostname: string): boolean {
}
// Reads an env-var-style string intended as a URL or path, rejecting both
// empty strings and the literal string "undefined" that Windows shells can
// empty strings and literal nullish strings that shells and dotenv templates
// write when a variable is unset-then-referenced without quotes (issue #336).
function asEnvUrl(value: string | undefined): string | undefined {
if (!value) return undefined
const trimmed = value.trim()
if (!trimmed) return undefined
if (trimmed === 'undefined') {
if (trimmed.toLowerCase() === 'undefined' || trimmed.toLowerCase() === 'null') {
return undefined
}
return trimmed
@@ -256,11 +257,12 @@ function asNamedEnvUrl(
const trimmed = value.trim()
if (!trimmed) return undefined
if (trimmed === 'undefined') {
const normalized = trimmed.toLowerCase()
if (normalized === 'undefined' || normalized === 'null') {
if (!warnedUndefinedEnvNames.has(envName)) {
warnedUndefinedEnvNames.add(envName)
logForDebugging(
`[provider-config] Environment variable ${envName} is the literal string "undefined"; ignoring it.`,
`[provider-config] Environment variable ${envName} is the literal string "${trimmed}"; ignoring it.`,
{ level: 'warn' },
)
}
@@ -933,7 +935,7 @@ export function resolveProviderRequest(options?: {
const isMistralMode = isEnvTruthy(processEnv.CLAUDE_CODE_USE_MISTRAL)
const isGeminiMode = isEnvTruthy(processEnv.CLAUDE_CODE_USE_GEMINI)
const isClinePassMode = Boolean(processEnv.CLINE_API_KEY?.trim())
const isApismartMode = hasUsableOpenAICredential(processEnv.APISMART_API_KEY)
const isApismartMode = hasApismartEnvOnlyProviderIntent(processEnv)
const explicitBaseUrl = asEnvUrl(options?.baseUrl)
const normalizedMistralEnvBaseUrl = asNamedEnvUrl(
@@ -1012,8 +1014,8 @@ export function resolveProviderRequest(options?: {
? processEnv.CLINE_API_MODEL?.trim() ||
processEnv.OPENAI_MODEL?.trim()
: effectiveApismartMode
? processEnv.APISMART_MODEL?.trim() ||
processEnv.OPENAI_MODEL?.trim()
? getUsableRouteModelEnvValue(processEnv.APISMART_MODEL) ||
getUsableRouteModelEnvValue(processEnv.OPENAI_MODEL)
: processEnv.OPENAI_MODEL?.trim()) ||
options?.fallbackModel?.trim() ||
(isGeminiMode ? DEFAULT_GEMINI_MODEL : undefined) ||
@@ -75,6 +75,8 @@ const SAVED_ENV = {
ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL,
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
MIMO_API_KEY: process.env.MIMO_API_KEY,
APISMART_API_KEY: process.env.APISMART_API_KEY,
APISMART_MODEL: process.env.APISMART_MODEL,
OPENAI_MODEL: process.env.OPENAI_MODEL,
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
CODEX_API_KEY: process.env.CODEX_API_KEY,
@@ -131,6 +133,8 @@ beforeEach(async () => {
delete process.env.CLAUDE_CODE_USE_FOUNDRY
delete process.env.NVIDIA_NIM
delete process.env.MINIMAX_API_KEY
delete process.env.APISMART_API_KEY
delete process.env.APISMART_MODEL
delete process.env.ANTHROPIC_MODEL
delete process.env.MIMO_API_KEY
delete process.env.OPENAI_MODEL
@@ -340,6 +344,41 @@ test('getDefaultMainLoopModelSetting defaults MiniMax to M3', async () => {
expect(getDefaultMainLoopModel()).toBe('MiniMax-M3')
})
test('getDefaultMainLoopModelSetting uses the credential-only ApiSmart model', async () => {
process.env.APISMART_API_KEY = 'apismart-key'
process.env.APISMART_MODEL = 'KIMI_K3'
const {
getDefaultMainLoopModel,
getDefaultMainLoopModelSetting,
} = await importFreshModelModule()
expect(getDefaultMainLoopModelSetting()).toBe('KIMI_K3')
expect(getDefaultMainLoopModel()).toBe('KIMI_K3')
})
test('ApiSmart model selection is consistent across main and helper tiers', async () => {
process.env.APISMART_API_KEY = 'apismart-key'
process.env.APISMART_MODEL = 'KIMI_K3'
const {
getDefaultHaikuModel,
getDefaultOpusModel,
getDefaultSonnetModel,
getSmallFastModel,
} = await importFreshModelModule()
expect(getDefaultOpusModel()).toBe('KIMI_K3')
expect(getDefaultSonnetModel()).toBe('KIMI_K3')
expect(getDefaultHaikuModel()).toBe('KIMI_K3')
expect(getSmallFastModel()).toBe('KIMI_K3')
})
test('getDefaultMainLoopModelSetting uses the ApiSmart descriptor default', async () => {
process.env.APISMART_API_KEY = 'apismart-key'
const { getDefaultMainLoopModelSetting } = await importFreshModelModule()
expect(getDefaultMainLoopModelSetting()).toBe('DEEPSEEK_V4_FLASH')
})
test('getDefaultMainLoopModelSetting uses the NVIDIA NIM route model', async () => {
process.env.NVIDIA_NIM = '1'
process.env.CLAUDE_CODE_USE_OPENAI = '1'
+27 -1
View File
@@ -35,7 +35,11 @@ import { type ModelAlias, isModelAlias } from './aliases.js'
import { capitalize } from '../stringUtils.js'
import { DEFAULT_GEMINI_MODEL } from '../providerProfile.js'
import { getAntModelOverrideConfig, resolveAntModel } from './antModels.js'
import { getRouteDefaultModel } from '../../integrations/routeMetadata.js'
import {
getRouteDefaultModel,
getUsableRouteModelEnvValue,
resolveEnvOnlyProviderRouteId,
} from '../../integrations/routeMetadata.js'
export type ModelShortName = string
export type ModelName = string
@@ -45,6 +49,18 @@ function getMiniMaxModelEnv(): string | undefined {
return process.env.ANTHROPIC_MODEL || process.env.OPENAI_MODEL
}
function getApiSmartModelEnv(): string | undefined {
if (resolveEnvOnlyProviderRouteId(process.env) !== 'apismart') {
return undefined
}
return (
getUsableRouteModelEnvValue(process.env.APISMART_MODEL) ||
getUsableRouteModelEnvValue(process.env.OPENAI_MODEL) ||
getRouteDefaultModel('apismart') ||
'DEEPSEEK_V4_FLASH'
)
}
function normalizeModelSetting(value: unknown): ModelName | ModelAlias | undefined {
if (typeof value !== 'string') return undefined
const trimmed = value.trim()
@@ -56,6 +72,8 @@ export function getSmallFastModel(): ModelName {
if (isCustomAnthropicProvider()) {
return process.env.ANTHROPIC_MODEL || getDefaultHaikuModel()
}
const apiSmartModel = getApiSmartModelEnv()
if (apiSmartModel) return apiSmartModel
// For Gemini provider, use a fast model
if (getAPIProvider() === 'gemini') {
return process.env.GEMINI_MODEL || 'gemini-2.0-flash-lite'
@@ -195,6 +213,8 @@ export function getDefaultOpusModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
}
const apiSmartModel = getApiSmartModelEnv()
if (apiSmartModel) return apiSmartModel
// Gemini provider
if (getAPIProvider() === 'gemini') {
return process.env.GEMINI_MODEL || 'gemini-2.5-pro'
@@ -245,6 +265,8 @@ export function getDefaultSonnetModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
}
const apiSmartModel = getApiSmartModelEnv()
if (apiSmartModel) return apiSmartModel
// Gemini provider
if (getAPIProvider() === 'gemini') {
return process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL
@@ -293,6 +315,8 @@ export function getDefaultHaikuModel(): ModelName {
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
}
const apiSmartModel = getApiSmartModelEnv()
if (apiSmartModel) return apiSmartModel
// Mistral provider
if (getAPIProvider() === 'mistral') {
return process.env.MISTRAL_MODEL || 'ministral-3b-latest'
@@ -379,6 +403,8 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
if (isCustomAnthropicProvider()) {
return process.env.ANTHROPIC_MODEL || getDefaultSonnetModel()
}
const apiSmartModel = getApiSmartModelEnv()
if (apiSmartModel) return apiSmartModel
// GitHub Copilot provider: check settings.model first, then env, then default
if (getAPIProvider() === 'github') {
const settings = getSettings_DEPRECATED() || {}
+98
View File
@@ -13,6 +13,7 @@ import {
applyModelFlagFromArgs,
VALID_PROVIDERS,
} from './providerFlag.js'
import { resolveEnvOnlyProviderRouteId } from '../integrations/routeMetadata.js'
const ENV_KEYS = [
'CLAUDE_CODE_USE_OPENAI',
@@ -22,6 +23,7 @@ const ENV_KEYS = [
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'CLAUDE_CODE_PROVIDER_ROUTE_ID',
'OPENAI_BASE_URL',
'OPENAI_API_BASE',
'OPENAI_API_KEY',
@@ -73,6 +75,7 @@ const RESET_KEYS = [
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'CLAUDE_CODE_PROVIDER_ROUTE_ID',
'OPENAI_BASE_URL',
'OPENAI_API_BASE',
'OPENAI_API_KEY',
@@ -191,6 +194,16 @@ describe('applyProviderFlag - anthropic', () => {
expect(result.error).toBeUndefined()
expect(process.env.ANTHROPIC_API_KEY).toBe('first-party-key')
})
test('explicit Anthropic selection suppresses an ambient ApiSmart key', () => {
process.env.ANTHROPIC_API_KEY = 'first-party-key'
process.env.APISMART_API_KEY = 'ambient-apismart-key'
applyProviderFlag('anthropic', [])
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('anthropic')
expect(resolveEnvOnlyProviderRouteId(process.env)).toBeNull()
})
})
describe('applyProviderFlag - custom Anthropic-compatible', () => {
@@ -1019,6 +1032,41 @@ describe('applyProviderFlag - apismart', () => {
expect(process.env.OPENAI_MODEL).toBe('KIMI_K3')
})
test('ApiSmart-specific model replaces a stale generic OpenAI model', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.APISMART_MODEL = 'KIMI_K3'
process.env.OPENAI_MODEL = 'gpt-4o'
applyProviderFlag('apismart', [])
expect(process.env.OPENAI_MODEL).toBe('KIMI_K3')
})
test.each(['undefined', 'null', ' NULL '])(
'ignores stale generic model sentinel %s for ApiSmart',
placeholder => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.OPENAI_MODEL = placeholder
applyProviderFlag('apismart', [])
expect(process.env.OPENAI_MODEL).toBe('DEEPSEEK_V4_FLASH')
},
)
test.each(['undefined', 'null', ' NULL '])(
'ignores stale generic base URL sentinel %s for ApiSmart',
placeholder => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.OPENAI_BASE_URL = placeholder
applyProviderFlag('apismart', [])
expect(process.env.OPENAI_BASE_URL).toBe('https://gw.apismart.ai/v1')
expect(process.env.OPENAI_API_KEY).toBe('apismart-secret-key')
},
)
test('makes an explicit --model override APISMART_MODEL', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.APISMART_MODEL = 'KIMI_K3'
@@ -1029,6 +1077,16 @@ describe('applyProviderFlag - apismart', () => {
expect(process.env.APISMART_MODEL).toBe('GLM_5.2')
})
test('standalone --model overrides credential-only ApiSmart model selection', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.APISMART_MODEL = 'KIMI_K3'
applyModelFlagFromArgs(['--model', 'GLM_5.2'])
expect(process.env.OPENAI_MODEL).toBe('GLM_5.2')
expect(process.env.APISMART_MODEL).toBe('GLM_5.2')
})
test('dedicated key overrides a lingering OPENAI_API_KEY from another provider', () => {
process.env.OPENAI_API_KEY = 'existing-openai-key'
process.env.APISMART_API_KEY = 'apismart-secret-key'
@@ -1046,14 +1104,54 @@ describe('applyProviderFlag - apismart', () => {
expect(process.env.OPENAI_API_KEY).toBeUndefined()
})
test.each(['SUA_CHAVE', 'null', 'undefined', ' NULL '])(
'does not mirror placeholder ApiSmart credential %s',
placeholder => {
process.env.APISMART_API_KEY = placeholder
applyProviderFlag('apismart', [])
expect(process.env.OPENAI_API_KEY).toBeUndefined()
},
)
test('clears a copied ApiSmart key from OPENAI_API_KEY when switching to another provider', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.OPENAI_API_KEY = 'apismart-secret-key'
process.env.OPENAI_BASE_URL = 'https://gw.apismart.ai/v1'
applyProviderFlag('openai', [])
expect(process.env.OPENAI_API_KEY).toBeUndefined()
expect(process.env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1')
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('openai')
expect(resolveEnvOnlyProviderRouteId(process.env)).toBeNull()
})
test.each([
['openai', 'https://api.openai.com/v1'],
['github', 'https://api.githubcopilot.com'],
['ollama', 'http://localhost:11434/v1'],
['nvidia-nim', 'https://integrate.api.nvidia.com/v1'],
['bankr', 'https://llm.bankr.bot/v1'],
['xai', 'https://api.x.ai/v1'],
['xiaomi-mimo', 'https://api.xiaomimimo.com/v1'],
['venice', 'https://api.venice.ai/api/v1'],
])(
'explicit %s selection replaces a stale ApiSmart endpoint',
(provider, expectedBaseUrl) => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.OPENAI_API_KEY = 'apismart-secret-key'
process.env.OPENAI_BASE_URL = 'https://gw.apismart.ai/v1'
process.env.OPENAI_MODEL = 'KIMI_K3'
applyProviderFlag(provider, [])
expect(process.env.OPENAI_BASE_URL).toBe(expectedBaseUrl)
expect(process.env.OPENAI_API_KEY).not.toBe('apismart-secret-key')
expect(process.env.OPENAI_MODEL).not.toBe('KIMI_K3')
},
)
})
describe('applyProviderFlag - xai', () => {
+75 -9
View File
@@ -21,6 +21,7 @@ import {
getAllVendors,
getGateway,
getVendor,
getTransportKindForRoute,
isCloudflareBaseUrl,
isLongcatBaseUrl,
routeSupportsApiFormatSelection,
@@ -29,7 +30,13 @@ import {
resolveRouteIdFromBaseUrl,
} from '../integrations/index.js'
import { PRESET_VENDOR_MAP } from '../integrations/compatibility.js'
import { isApismartBaseUrl } from '../integrations/routeMetadata.js'
import {
hasApismartEnvOnlyProviderIntent,
getUsableRouteConfigEnvValue,
getUsableRouteModelEnvValue,
hasUsableRouteCredentialEnvValue,
isApismartBaseUrl,
} from '../integrations/routeMetadata.js'
import { isFirstPartyAnthropicBaseUrlForEnv } from './anthropicBaseUrl.js'
const PREFERRED_PROVIDER_ORDER = [
@@ -170,8 +177,7 @@ function getRouteDefaults(provider: string): {
}
function normalizeBaseUrlEnv(value: string | undefined): string | undefined {
const trimmed = value?.trim()
return trimmed && trimmed !== 'undefined' ? trimmed : undefined
return getUsableRouteConfigEnvValue(value)
}
function getConfiguredOpenAIBaseUrl(): string | undefined {
@@ -193,7 +199,6 @@ function shouldReplaceStaleKnownBaseUrl(provider: string): boolean {
const targetRouteId = resolveProfileRoute(provider).routeId
return (
targetRouteId !== 'openai' &&
targetRouteId !== 'custom' &&
targetRouteId !== 'unknown-fallback' &&
currentRouteId !== targetRouteId
@@ -231,6 +236,25 @@ function applyOpenAIBaseUrlDefault(provider: string, baseUrl?: string): void {
}
}
function replaceStaleKnownBaseUrlForExplicitProvider(
provider: string,
baseUrl: string | undefined,
): boolean {
const routeId = resolveProfileRoute(provider).routeId
const transportKind = getTransportKindForRoute(routeId)
if (
(transportKind !== 'openai-compatible' && transportKind !== 'local') ||
!baseUrl ||
isPlaceholderBaseUrl(baseUrl) ||
!shouldReplaceStaleKnownBaseUrl(provider)
) {
return false
}
process.env.OPENAI_BASE_URL = baseUrl.trim()
return true
}
function clearUnsupportedOpenAIShimSettings(routeId: string): void {
if (!routeSupportsApiFormatSelection(routeId)) {
delete process.env.OPENAI_API_FORMAT
@@ -273,6 +297,7 @@ export function applyModelFlagFromArgs(args: string[]): void {
const useGithub =
process.env.CLAUDE_CODE_USE_GITHUB === '1' ||
process.env.CLAUDE_CODE_USE_GITHUB === 'true'
const useApismartEnvOnly = hasApismartEnvOnlyProviderIntent(process.env)
if (useGemini) {
process.env.GEMINI_MODEL = model
@@ -280,6 +305,12 @@ export function applyModelFlagFromArgs(args: string[]): void {
process.env.MISTRAL_MODEL = model
} else if (useOpenAI || useGithub) {
process.env.OPENAI_MODEL = model
if (useApismartEnvOnly) {
process.env.APISMART_MODEL = model
}
} else if (useApismartEnvOnly) {
process.env.OPENAI_MODEL = model
process.env.APISMART_MODEL = model
} else {
process.env.ANTHROPIC_MODEL = model
}
@@ -305,6 +336,12 @@ export function applyProviderFlag(
}
}
// Record explicit CLI provider intent separately from compatibility flags.
// In particular, built-in Anthropic has no CLAUDE_CODE_USE_* flag, while an
// OpenAI-compatible flag alone does not identify a concrete gateway.
process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID =
resolveProfileRoute(provider).routeId
const opengatewayApiKey = process.env.OPENGATEWAY_API_KEY?.trim()
const copiedOpenAIKeyProvider =
process.env.OPENAI_API_KEY !== undefined &&
@@ -366,6 +403,19 @@ export function applyProviderFlag(
const model = parseModelFlag(args)
const { defaultBaseUrl, defaultModel } = getRouteDefaults(provider)
// A CLI provider selection is a route transition, not just a flag change.
// Replace endpoints owned by the previously selected known route for every
// OpenAI-shim provider, while preserving unknown user-managed proxy URLs.
const replacedKnownProviderEndpoint =
replaceStaleKnownBaseUrlForExplicitProvider(provider, defaultBaseUrl)
if (replacedKnownProviderEndpoint && !model) {
if (defaultModel) {
process.env.OPENAI_MODEL = defaultModel
} else {
delete process.env.OPENAI_MODEL
}
}
// Azure-style routing changes both request paths and authentication. It is
// only meaningful for an explicit OpenAI/Azure configuration, so never let
// it follow a provider switch to another OpenAI-compatible endpoint.
@@ -428,6 +478,10 @@ export function applyProviderFlag(
case 'openai':
process.env.CLAUDE_CODE_USE_OPENAI = '1'
applyOpenAIBaseUrlDefault(
provider,
defaultBaseUrl ?? 'https://api.openai.com/v1',
)
if (model) process.env.OPENAI_MODEL = model
break
@@ -607,10 +661,19 @@ export function applyProviderFlag(
provider,
defaultBaseUrl ?? 'https://gw.apismart.ai/v1',
)
process.env.OPENAI_MODEL ??=
process.env.APISMART_MODEL?.trim() ||
defaultModel ||
'DEEPSEEK_V4_FLASH'
{
const apismartModel = getUsableRouteModelEnvValue(
process.env.APISMART_MODEL,
)
if (apismartModel) {
process.env.OPENAI_MODEL = apismartModel
} else {
process.env.OPENAI_MODEL =
getUsableRouteModelEnvValue(process.env.OPENAI_MODEL) ??
defaultModel ??
'DEEPSEEK_V4_FLASH'
}
}
if (model) {
process.env.OPENAI_MODEL = model
process.env.APISMART_MODEL = model
@@ -620,7 +683,10 @@ export function applyProviderFlag(
// and clear any stale generic key so another provider's credential is
// never forwarded to ApiSmart.
if (
process.env.APISMART_API_KEY &&
hasUsableRouteCredentialEnvValue(
'APISMART_API_KEY',
process.env.APISMART_API_KEY,
) &&
isApismartBaseUrl(getConfiguredOpenAIBaseUrl())
) {
process.env.OPENAI_API_KEY = process.env.APISMART_API_KEY
+73
View File
@@ -35,6 +35,7 @@ import {
PROFILE_FILE_NAME,
redactSecretValueForDisplay,
saveProfileFile,
sanitizeOpenAICredentialPool,
sanitizeProviderConfigValue,
hasInvalidOpenAICredentialPool,
selectAutoProfile,
@@ -278,6 +279,47 @@ test('openai launch preserves persisted ApiSmart dedicated credentials across re
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'apismart')
})
test('openai launch withholds a persisted mirrored ApiSmart key after live endpoint retargeting', async () => {
const env = await buildLaunchEnv({
profile: 'openai',
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
OPENAI_MODEL: 'DEEPSEEK_V4_FLASH',
OPENAI_API_KEY: 'apismart-secret-key',
APISMART_API_KEY: 'apismart-secret-key',
}),
goal: 'coding',
processEnv: {
OPENAI_BASE_URL: 'https://proxy.example/v1',
},
})
assert.equal(env.OPENAI_BASE_URL, 'https://proxy.example/v1')
assert.equal(env.OPENAI_API_KEY, undefined)
assert.equal(env.APISMART_API_KEY, undefined)
})
test('openai launch keeps a live generic key when retargeting a persisted ApiSmart profile', async () => {
const env = await buildLaunchEnv({
profile: 'openai',
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
OPENAI_MODEL: 'DEEPSEEK_V4_FLASH',
OPENAI_API_KEY: 'apismart-secret-key',
APISMART_API_KEY: 'apismart-secret-key',
}),
goal: 'coding',
processEnv: {
OPENAI_BASE_URL: 'https://proxy.example/v1',
OPENAI_API_KEY: 'proxy-key',
},
})
assert.equal(env.OPENAI_BASE_URL, 'https://proxy.example/v1')
assert.equal(env.OPENAI_API_KEY, 'proxy-key')
assert.equal(env.APISMART_API_KEY, undefined)
})
test('buildApismartProfileEnv prefers APISMART_MODEL over OPENAI_MODEL', () => {
const env = buildApismartProfileEnv({
apiKey: 'apismart-secret-key',
@@ -300,6 +342,12 @@ test('buildApismartProfileEnv refuses to copy the dedicated credential to a cust
assert.equal(env, null)
})
for (const placeholder of ['SUA_CHAVE', 'null', 'undefined', ' NULL ']) {
test(`buildApismartProfileEnv rejects placeholder credential ${placeholder}`, () => {
assert.equal(buildApismartProfileEnv({ apiKey: placeholder }), null)
})
}
test('openai launch carries APISMART_API_KEY only when the route resolves to apismart', async () => {
const offRoute = await buildLaunchEnv({
profile: 'openai',
@@ -966,6 +1014,30 @@ test('applyStartupEnvFromProfile applies valid startup env (issue #1651)', async
assert.equal(processEnv.OPENGATEWAY_API_KEY, 'test-key')
})
test('invalid ambient ApiSmart placeholder does not suppress a saved profile', async () => {
const processEnv: NodeJS.ProcessEnv = {
APISMART_API_KEY: ' SUA_CHAVE ',
}
const warnings: string[] = []
const error = await applyStartupEnvFromProfile({
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_MODEL: 'gpt-4o',
OPENAI_API_KEY: 'saved-openai-key',
}),
processEnv,
onValidationError: message => warnings.push(message),
})
assert.equal(error, null)
assert.deepEqual(warnings, [])
assert.equal(processEnv.OPENAI_BASE_URL, 'https://api.openai.com/v1')
assert.equal(processEnv.OPENAI_MODEL, 'gpt-4o')
assert.equal(processEnv.OPENAI_API_KEY, 'saved-openai-key')
assert.equal(processEnv.APISMART_API_KEY, undefined)
})
test('buildStartupEnvFromProfile preserves explicit OpenAI-compatible env without a saved profile', async () => {
const env = await buildStartupEnvFromProfile({
persisted: null,
@@ -2728,6 +2800,7 @@ test('openai launch preserves invalid live pooled credentials for launch validat
assert.equal(env.OPENAI_API_KEY, undefined)
assert.equal(hasInvalidOpenAICredentialPool(env.OPENAI_API_KEYS), true)
})
test('openai launch lets a live singular key override a saved pool', async () => {
const env = await buildLaunchEnv({
profile: 'openai',
+35 -15
View File
@@ -25,6 +25,9 @@ import { getErrnoCode } from './errors.js'
import {
getRouteDefaultBaseUrl,
getRouteDefaultModel,
getRouteCredentialEnvVars,
hasExplicitOpenAICompatibleOptOut,
hasUsableRouteCredentialEnvValue,
isApismartBaseUrl,
isLongcatBaseUrl,
normalizeXiaomiMimoBaseUrl,
@@ -644,7 +647,7 @@ export function buildApismartProfileEnv(options: {
}): ProfileEnv | null {
const processEnv = options.processEnv ?? process.env
const key = sanitizeApiKey(options.apiKey ?? processEnv.APISMART_API_KEY)
if (!key) {
if (!key || !hasUsableRouteCredentialEnvValue('APISMART_API_KEY', key)) {
return null
}
@@ -1316,15 +1319,6 @@ function hasExplicitNonOpenAIProviderSelection(
)
}
function hasExplicitOpenAICompatibleOptOut(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
return (
processEnv.CLAUDE_CODE_USE_OPENAI !== undefined &&
!isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)
)
}
function hasConcreteProviderSelection(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
@@ -1401,7 +1395,10 @@ function hasConcreteProviderSelection(
sanitizeApiKey(processEnv.FIREWORKS_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.NEARAI_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.APISMART_API_KEY) !== undefined
hasUsableRouteCredentialEnvValue(
'APISMART_API_KEY',
processEnv.APISMART_API_KEY,
)
)
}
@@ -2015,10 +2012,9 @@ export async function buildLaunchEnv(options: {
} else {
delete env.OPENAI_AUTH_HEADER_VALUE
}
const openAICredential = resolveOpenAICredentialEnvOverride(
processEnv,
persistedEnv,
)
const liveOpenAICredential = resolveOpenAICredentialEnvSelection(processEnv)
const openAICredential =
liveOpenAICredential || resolveOpenAICredentialEnvSelection(persistedEnv)
if (openAICredential) {
env[openAICredential.envVar] = openAICredential.value
}
@@ -2045,6 +2041,30 @@ export async function buildLaunchEnv(options: {
const effectiveOpenAIRouteId =
resolvedOpenAIRouteId ||
(shouldUsePersistedOpenAIRouteId ? persistedOpenAIRouteId : undefined)
const persistedCredentialRouteId =
resolveRouteIdFromBaseUrl(persistedOpenAIBaseUrl) || persistedOpenAIRouteId
const persistedOpenAICredential =
resolveOpenAICredentialEnvSelection(persistedEnv)
const persistedGenericCredentialMirrorsDedicatedRoute =
!liveOpenAICredential &&
persistedOpenAICredential?.envVar === 'OPENAI_API_KEY' &&
!!persistedCredentialRouteId &&
getRouteCredentialEnvVars(persistedCredentialRouteId)
.filter(
envVar =>
envVar !== 'OPENAI_API_KEY' && envVar !== 'OPENAI_API_KEYS',
)
.some(
envVar =>
sanitizeApiKey(persistedEnv[envVar]) ===
persistedOpenAICredential.value,
)
if (
persistedGenericCredentialMirrorsDedicatedRoute &&
effectiveOpenAIRouteId !== persistedCredentialRouteId
) {
delete env.OPENAI_API_KEY
}
if (resolvedOpenAIRouteId && resolvedOpenAIRouteId !== 'openai') {
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = resolvedOpenAIRouteId
} else if (shouldUsePersistedOpenAIRouteId && persistedOpenAIRouteId) {
+141 -1
View File
@@ -830,6 +830,24 @@ describe('applyProviderProfileToProcessEnv', () => {
expect(getFreshAPIProvider()).toBe('openai')
})
test.each(['SUA_CHAVE', 'null', 'undefined', ' NULL '])(
'rejects an ApiSmart profile with invalid credential placeholder %p',
async apiKey => {
const { addProviderProfile } = await importFreshProviderProfileModules()
const saved = addProviderProfile({
provider: 'apismart',
name: 'ApiSmart invalid',
baseUrl: 'https://gw.apismart.ai/v1',
model: 'KIMI_K3',
apiKey,
})
expect(saved).toBeNull()
expect(mockConfigState.providerProfiles).toEqual([])
},
)
test('apismart profile clears a stale route-specific model before applying its saved model', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
@@ -857,12 +875,46 @@ describe('applyProviderProfileToProcessEnv', () => {
await importFreshProviderProfileModules()
applyProviderProfileToProcessEnv(
buildApismartProfile({ baseUrl: 'https://proxy.example/v1' }),
buildApismartProfile({
baseUrl: 'https://proxy.example/v1',
authHeader: 'X-Proxy-Key',
authScheme: 'raw',
authHeaderValue: 'proxy-secret',
customHeaders: { 'X-Team': 'devtools' },
}),
)
expect(process.env.OPENAI_BASE_URL).toBe('https://proxy.example/v1')
expect(process.env.OPENAI_API_KEY).toBeUndefined()
expect(process.env.APISMART_API_KEY).toBeUndefined()
expect(process.env.OPENAI_AUTH_HEADER).toBe('X-Proxy-Key')
expect(process.env.OPENAI_AUTH_SCHEME).toBe('raw')
expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBe('proxy-secret')
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBe('X-Team: devtools')
})
test('custom profile targeting ApiSmart mirrors its key into the endpoint credential', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
applyProviderProfileToProcessEnv(
buildProfile({
provider: 'custom',
baseUrl: 'https://gw.apismart.ai/v1',
model: 'KIMI_K3',
apiKey: 'apismart-custom-profile-key',
apiFormat: 'responses',
authHeader: 'X-Api-Key',
authScheme: 'raw',
authHeaderValue: 'unsupported-custom-auth',
}),
)
expect(process.env.OPENAI_API_KEY).toBe('apismart-custom-profile-key')
expect(process.env.APISMART_API_KEY).toBe('apismart-custom-profile-key')
expect(process.env.OPENAI_API_FORMAT).toBeUndefined()
expect(process.env.OPENAI_AUTH_HEADER).toBeUndefined()
expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBeUndefined()
})
test('cloudflare profile applies OpenAI-compatible env with CLOUDFLARE_API_TOKEN mirror', async () => {
@@ -3189,6 +3241,46 @@ describe('setActiveProviderProfile', () => {
}
})
test('removes stale startup persistence when an active profile has no usable startup credential', 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 { createProfileFile, saveProfileFile } = await import(
`./providerProfile.js?ts=${Date.now()}-${Math.random()}`
)
saveProfileFile(
createProfileFile('openai', {
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_MODEL: 'gpt-4o',
OPENAI_API_KEY: 'stale-openai-key',
}),
{ configDir },
)
const apismartProfile = buildApismartProfile({
id: 'apismart_keyless',
apiKey: undefined,
})
saveMockGlobalConfig(current => ({
...current,
providerProfiles: [apismartProfile],
}))
expect(
setActiveProviderProfile('apismart_keyless', { configDir })?.id,
).toBe('apismart_keyless')
expect(existsSync(join(configDir, '.openclaude-profile.json'))).toBe(false)
} finally {
process.chdir(originalCwd)
rmSync(tempDir, { recursive: true, force: true })
rmSync(configDir, { recursive: true, force: true })
}
})
test('retargeted ApiSmart profiles persist without their dedicated credential', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
@@ -3201,6 +3293,10 @@ describe('setActiveProviderProfile', () => {
const apismartProfile = buildApismartProfile({
id: 'apismart_proxy',
baseUrl: 'https://proxy.example/v1',
authHeader: 'X-Proxy-Key',
authScheme: 'raw',
authHeaderValue: 'proxy-secret',
customHeaders: { 'X-Team': 'devtools' },
})
saveMockGlobalConfig(current => ({
@@ -3218,6 +3314,10 @@ describe('setActiveProviderProfile', () => {
expect(persisted.env).toEqual({
OPENAI_BASE_URL: 'https://proxy.example/v1',
OPENAI_MODEL: 'DEEPSEEK_V4_FLASH',
OPENAI_AUTH_HEADER: 'X-Proxy-Key',
OPENAI_AUTH_SCHEME: 'raw',
OPENAI_AUTH_HEADER_VALUE: 'proxy-secret',
ANTHROPIC_CUSTOM_HEADERS: 'X-Team: devtools',
})
} finally {
process.chdir(originalCwd)
@@ -3226,6 +3326,46 @@ describe('setActiveProviderProfile', () => {
}
})
test('persists credentials by concrete ApiSmart endpoint before provider label', 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 profile = buildAtlasCloudProfile({
id: 'atlas_label_apismart_endpoint',
baseUrl: 'https://gw.apismart.ai/v1',
model: 'KIMI_K3',
apiKey: 'endpoint-owned-key',
})
saveMockGlobalConfig(current => ({
...current,
providerProfiles: [profile],
}))
expect(
setActiveProviderProfile(profile.id, { configDir })?.id,
).toBe(profile.id)
const persisted = JSON.parse(
readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'),
)
expect(persisted.env).toMatchObject({
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
OPENAI_MODEL: 'KIMI_K3',
OPENAI_API_KEY: 'endpoint-owned-key',
APISMART_API_KEY: 'endpoint-owned-key',
})
expect(persisted.env.ATLAS_CLOUD_API_KEY).toBeUndefined()
} finally {
process.chdir(originalCwd)
rmSync(tempDir, { recursive: true, force: true })
rmSync(configDir, { recursive: true, force: true })
}
})
test('persists Xiaomi MiMo profiles using a legacy-compatible openai startup profile', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
+41 -50
View File
@@ -45,6 +45,7 @@ import {
routeSupportsApiFormatSelection,
routeSupportsAuthHeaders,
routeSupportsCustomHeaders,
resolveProfileCapabilityRouteId,
resolveProfileRoute,
resolveRouteIdFromBaseUrl,
type ResolvedProfileRoute,
@@ -54,10 +55,11 @@ import {
getRouteDefaultBaseUrl,
isCloudflareBaseUrl,
isClinePassBaseUrl,
isApismartBaseUrl,
isFireworksBaseUrl,
isLongcatBaseUrl,
isNearaiBaseUrl,
hasUsableRouteCredentialEnvValue,
profileTargetsRoute,
isXaiBaseUrl,
isXiaomiMimoBaseUrl,
resolveEnvOnlyProviderRouteId,
@@ -153,8 +155,7 @@ function isClinePassProfile(profile: ProviderProfile): boolean {
}
function isApismartProfile(profile: ProviderProfile): boolean {
const baseUrl = profile.baseUrl?.trim()
return !baseUrl || isApismartBaseUrl(baseUrl)
return profileTargetsRoute(profile.provider, profile.baseUrl, 'apismart')
}
function deriveGithubEnterpriseUrl(baseUrl: string | undefined): string | undefined {
@@ -221,36 +222,6 @@ function normalizeBaseUrl(value: string): string {
return trimValue(value).replace(/\/+$/, '')
}
function resolveProfileCapabilityRouteId(
provider: string,
baseUrl?: string,
): string {
const providerRouteId = resolveProfileRoute(provider).routeId
if (providerRouteId === 'custom-anthropic') {
return providerRouteId
}
const routeIdFromBaseUrl = resolveRouteIdFromBaseUrl(baseUrl)
if (routeIdFromBaseUrl) {
return routeIdFromBaseUrl
}
// Cloudflare and LongCat profiles retargeted away from their dedicated
// endpoints run generically at runtime. Mirror that boundary here so
// capability-driven surfaces are not stripped based on stale route ids.
if (
(providerRouteId === 'cloudflare' || providerRouteId === 'longcat') &&
baseUrl &&
!(providerRouteId === 'cloudflare'
? isCloudflareBaseUrl(baseUrl)
: isLongcatBaseUrl(baseUrl))
) {
return 'custom'
}
return providerRouteId
}
function normalizeProfileModelLookupKey(model: string | undefined): string {
// Strip a trailing [1m] tag along with any ?query suffix: the tag is a
// client-side context opt-in, not part of the model's identity, so a saved
@@ -331,6 +302,7 @@ function sanitizeProfile(profile: ProviderProfile): ProviderProfile | null {
const provider = trimValue(profile.provider)
const baseUrl = normalizeBaseUrl(profile.baseUrl)
const model = trimValue(profile.model)
const apiKey = trimOrUndefined(profile.apiKey)
const apiFormat = parseOpenAICompatibleApiFormat(profile.apiFormat)
const azureStyle = profile.azureStyle === true
const authHeader = sanitizeAuthHeader(profile.authHeader)
@@ -346,6 +318,13 @@ function sanitizeProfile(profile: ProviderProfile): ProviderProfile | null {
if (!id || !name || !baseUrl || !model || !provider) {
return null
}
if (
apiKey &&
profileTargetsRoute(provider, baseUrl, 'apismart') &&
!hasUsableRouteCredentialEnvValue('APISMART_API_KEY', apiKey)
) {
return null
}
const maxContextLength =
typeof profile.maxContextLength === 'number' &&
@@ -361,7 +340,7 @@ function sanitizeProfile(profile: ProviderProfile): ProviderProfile | null {
provider,
baseUrl,
model,
apiKey: trimOrUndefined(profile.apiKey),
apiKey,
}
if (supportsApiFormat && apiFormat) {
sanitized.apiFormat = apiFormat
@@ -996,8 +975,14 @@ export function applyProviderProfileToProcessEnv(
const withholdRetargetedApismartCredential =
route.routeId === 'apismart' && !isApismartProfile(profile)
if (profile.apiKey && !withholdRetargetedApismartCredential) {
openAIProfileEnv.OPENAI_API_KEY = profile.apiKey
const profileApiKey =
isApismartProfile(profile) &&
profile.apiKey &&
!hasUsableRouteCredentialEnvValue('APISMART_API_KEY', profile.apiKey)
? undefined
: profile.apiKey
if (profileApiKey && !withholdRetargetedApismartCredential) {
openAIProfileEnv.OPENAI_API_KEY = profileApiKey
if (route.vendorId === 'minimax' || normalizedProfileBaseUrl.toLowerCase().includes('minimax')) {
openAIProfileEnv.MINIMAX_API_KEY = profile.apiKey
}
@@ -1031,7 +1016,7 @@ export function applyProviderProfileToProcessEnv(
openAIProfileEnv.ATLAS_CLOUD_API_KEY = profile.apiKey
}
if (isApismartProfile(profile)) {
openAIProfileEnv.APISMART_API_KEY = profile.apiKey
openAIProfileEnv.APISMART_API_KEY = profileApiKey
}
if (isClinePassProfile(profile)) {
openAIProfileEnv.CLINE_API_KEY = profile.apiKey
@@ -1590,6 +1575,23 @@ function buildStartupProfileFromActiveProfile(
})),
}
case 'openai': {
// A concrete endpoint owns persistence just as it owns live credential
// routing. Handle dedicated ApiSmart endpoints before provider-label
// special cases (Atlas, NVIDIA, MiniMax, etc.), otherwise a retargeted
// profile works live but persists the wrong provider's credential.
if (isApismartProfile(activeProfile)) {
const env =
buildApismartProfileEnv({
model: getPrimaryModel(activeProfile.model),
baseUrl: activeProfile.baseUrl,
apiKey: activeProfile.apiKey,
processEnv: process.env,
}) ?? null
return env
? { profile: 'openai', env: applySupportedProfileCustomHeaders(activeProfile, env) }
: null
}
if (route.gatewayId === 'nvidia-nim') {
const env =
buildNvidiaNimProfileEnv({
@@ -1655,19 +1657,6 @@ function buildStartupProfileFromActiveProfile(
: null
}
if (route.routeId === 'apismart' && isApismartProfile(activeProfile)) {
const env =
buildApismartProfileEnv({
model: getPrimaryModel(activeProfile.model),
baseUrl: activeProfile.baseUrl,
apiKey: activeProfile.apiKey,
processEnv: process.env,
}) ?? null
return env
? { profile: 'openai', env: applySupportedProfileCustomHeaders(activeProfile, env) }
: null
}
if (route.vendorId === 'nearai') {
const env = buildOpenAICompatibleStartupEnv(activeProfile)
return env ? { profile: 'openai', env } : null
@@ -1754,6 +1743,8 @@ export function setActiveProviderProfile(
if (startupProfile) {
const file = createProfileFile(startupProfile.profile, startupProfile.env)
saveProfileFile(file, options)
} else {
deleteProfileFile(options)
}
return activeProfile
@@ -23,6 +23,9 @@ describe('clearStartupProviderOverrides', () => {
MINIMAX_API_KEY: 'sk-minimax',
VENICE_API_KEY: 'sk-venice',
LONGCAT_API_KEY: 'sk-longcat',
APISMART_API_KEY: 'sk-apismart',
APISMART_MODEL: 'KIMI_K3',
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'apismart',
ANTHROPIC_AUTH_TOKEN: 'stale-proxy-token',
KEEP_ME: '1',
},
@@ -47,6 +50,9 @@ describe('clearStartupProviderOverrides', () => {
MINIMAX_API_KEY: undefined,
VENICE_API_KEY: undefined,
LONGCAT_API_KEY: undefined,
APISMART_API_KEY: undefined,
APISMART_MODEL: undefined,
CLAUDE_CODE_PROVIDER_ROUTE_ID: undefined,
ANTHROPIC_AUTH_TOKEN: undefined,
}),
}),
+3
View File
@@ -9,6 +9,7 @@ export const STARTUP_PROVIDER_OVERRIDE_ENV_KEYS = [
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'CLAUDE_CODE_PROVIDER_ROUTE_ID',
'OPENAI_BASE_URL',
'OPENAI_API_BASE',
'OPENAI_MODEL',
@@ -42,6 +43,8 @@ export const STARTUP_PROVIDER_OVERRIDE_ENV_KEYS = [
'NVIDIA_NIM',
'VENICE_API_KEY',
'LONGCAT_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
] as const
type GlobalConfigWithEnv = {
+37
View File
@@ -195,6 +195,43 @@ test('non-canonical ApiSmart host falls back to generic OpenAI validation', asyn
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
})
test.each(['SUA_CHAVE', 'null', 'undefined', ' NULL '])(
'ApiSmart validation rejects placeholder dedicated credential %s',
async placeholder => {
await expect(
getProviderValidationError({
APISMART_API_KEY: placeholder,
APISMART_MODEL: 'KIMI_K3',
}),
).resolves.toBe('ApiSmart auth is required. Set APISMART_API_KEY.')
},
)
test.each(['SUA_CHAVE', 'null', 'undefined', ' NULL '])(
'ApiSmart compatibility mode rejects placeholder dedicated credential %s',
async placeholder => {
await expect(
getProviderValidationError({
CLAUDE_CODE_USE_OPENAI: '1',
APISMART_API_KEY: placeholder,
APISMART_MODEL: 'KIMI_K3',
}),
).resolves.toBe('ApiSmart auth is required. Set APISMART_API_KEY.')
},
)
test('ApiSmart placeholder does not override a runnable explicit OpenAI setup', async () => {
await expect(
getProviderValidationError({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_API_KEY: 'sk-valid-openai-key',
OPENAI_MODEL: 'gpt-4o',
APISMART_API_KEY: 'SUA_CHAVE',
APISMART_MODEL: 'KIMI_K3',
}),
).resolves.toBeNull()
})
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'
+40 -20
View File
@@ -15,6 +15,8 @@ import {
getRouteCredentialValue,
getRouteDescriptor,
getRouteDefaultModel,
hasConfiguredApismartProviderIntent,
hasUsableRouteCredentialEnvValue,
isApismartBaseUrl,
isCloudflareBaseUrl,
isLongcatBaseUrl,
@@ -30,7 +32,6 @@ import {
resolveProviderRequest,
shouldUseCodexTransport,
} from '../services/api/providerConfig.js'
import { hasUsableOpenAICredential } from '../services/api/credentialPool.js'
import { getGlobalClaudeFile } from './env.js'
import { isBareMode } from './envUtils.js'
import {
@@ -126,26 +127,10 @@ function hasNonEmptyEnvValue(
return typeof env[envVar] === 'string' && env[envVar]!.trim() !== ''
}
function hasUsableCredentialEnvValue(
env: NodeJS.ProcessEnv,
envVar: string,
): boolean {
const value = env[envVar]
if (typeof value !== 'string') {
return false
}
if (envVar === 'OPENAI_API_KEYS' || envVar === 'OPENAI_API_KEY') {
return hasUsableOpenAICredential(value)
}
return value.trim() !== ''
}
function hasOpenAICredential(env: NodeJS.ProcessEnv): boolean {
return (
hasUsableCredentialEnvValue(env, 'OPENAI_API_KEYS') ||
hasUsableCredentialEnvValue(env, 'OPENAI_API_KEY')
hasUsableRouteCredentialEnvValue('OPENAI_API_KEYS', env.OPENAI_API_KEYS) ||
hasUsableRouteCredentialEnvValue('OPENAI_API_KEY', env.OPENAI_API_KEY)
)
}
@@ -256,6 +241,37 @@ function getRuntimeValidationTarget(
return enabledTarget
}
const activeRouteId = resolveActiveRouteIdFromEnv(env)
if (
activeRouteId === 'openai' &&
!hasOpenAICredential(env) &&
hasConfiguredApismartProviderIntent(env)
) {
return validationTargets.find(
target => target.descriptor.id === 'apismart',
)
}
if (activeRouteId) {
const activeRouteTarget = validationTargets.find(
target => target.descriptor.id === activeRouteId,
)
const activeRouting = activeRouteTarget
? getValidationRouting(activeRouteTarget)
: undefined
if (
activeRouteTarget &&
!(useOpenAI && activeRouting?.skipWhenUseOpenAI)
) {
return activeRouteTarget
}
}
if (hasConfiguredApismartProviderIntent(env)) {
return validationTargets.find(
target => target.descriptor.id === 'apismart',
)
}
if (!useOpenAI) {
return undefined
}
@@ -264,6 +280,7 @@ function getRuntimeValidationTarget(
model: env.OPENAI_MODEL,
baseUrl: env.OPENAI_BASE_URL,
fallbackModel: getRouteDefaultModel('openai'),
processEnv: env,
})
const baseUrlMatchedTarget = validationTargets.find(target => {
@@ -360,7 +377,9 @@ function getCredentialEnvValidationError(
}
if (
credentialEnvVars.some(envVar => hasUsableCredentialEnvValue(env, envVar))
credentialEnvVars.some(envVar =>
hasUsableRouteCredentialEnvValue(envVar, env[envVar]),
)
) {
return null
}
@@ -535,6 +554,7 @@ export async function getProviderValidationError(
model: env.OPENAI_MODEL,
baseUrl: env.OPENAI_BASE_URL,
fallbackModel: getRouteDefaultModel('openai'),
processEnv: env,
})
const genericRouteValidation = getGenericRouteCredentialValidationError(
env,
+2
View File
@@ -78,6 +78,8 @@ export const envVars: EnvVar[] = [
{ name: 'GEMINI_API_KEY', description: 'Gemini API key (the preset reads this, not GOOGLE_API_KEY).' },
{ name: 'XAI_API_KEY', description: "xAI Grok key (or sign in with 'openclaude auth xai login')." },
{ name: 'AIMLAPI_API_KEY', description: 'AI/ML API key.' },
{ name: 'APISMART_API_KEY', description: 'ApiSmart gateway key; selects the ApiSmart route when no conflicting endpoint is configured.' },
{ name: 'APISMART_MODEL', description: 'Optional ApiSmart model override; defaults to DEEPSEEK_V4_FLASH.' },
{ name: 'CLOUDFLARE_API_TOKEN', description: 'Cloudflare Workers AI token.' },
{ name: 'NVIDIA_API_KEY', description: 'NVIDIA NIM key.' },
{ name: 'NEARAI_API_KEY', description: 'NEAR AI unified gateway key.' },