feat(integrations): add ApiSmart OpenAI-compatible gateway provider (#2109)

* feat(integrations): add ApiSmart OpenAI-compatible gateway provider

Add a hybrid-catalog ApiSmart gateway with dedicated APISMART_API_KEY/APISMART_MODEL
env wiring, route detection, profile persistence, and regression tests so the
provider works via --provider, env-only setup, and saved profiles.

* fix(apismart): protect dedicated credentials

* fix(apismart): enforce route credential boundaries

* fix(apismart): honor explicit competing route models

* fix(apismart): validate credentials and default profiles

* test(apismart): isolate env-only client tests

* fix(apismart): align routing and validation contracts

* fix(apismart): centralize route capability contracts

* Revert "fix(apismart): centralize route capability contracts"

This reverts commit f8cd975e68.

* fix(apismart): reject template credential placeholders at the shared root

Expand the shared credential usability helpers so dotenv sentinels like
null/undefined cannot win ApiSmart env-only precedence or get mirrored into
OPENAI_API_KEY, and restore focused ApiSmart docs after dropping the
shared routing centralization.

* fix(apismart): reject null base-URL and placeholder profile keys

Treat dotenv `null`/`undefined` OPENAI_BASE_URL sentinels as unset so
--provider apismart can apply defaults, and align profile/validation
credential checks with the shared placeholder contract.

* fix(apismart): match AIMLAPI env-only intent and proxy credential withholding

Use the OpenAI-compatible env-only gate so lingering CLAUDE_CODE_USE_OPENAI still keeps ApiSmart identity, retain route id on retargeted profiles, and withhold ambient credentials on non-canonical relaunches.

* fix(apismart): restore AIMLAPI credential and canonical URL parity

Backfill APISMART_API_KEY on relaunch and keyless canonical profiles, and gate credential forwarding on an exact /v1 inference URL so dedicatedCredentialsOnly auth and ambient keys stay aligned with AIMLAPI.

* fix: simplify apismart gateway integration

* fix: reject credential placeholders consistently

* test: remove obsolete apismart exception coverage

* test: remove stale apismart model fixture

* fix(apismart): restore dedicated provider contract

* fix(apismart): enforce credential boundaries

* fix(apismart): clear inherited auth headers

* test(apismart): strengthen credential boundaries
This commit is contained in:
JATMN
2026-08-11 19:08:38 +08:00
committed by GitHub
parent 7743cf280e
commit ff91642364
33 changed files with 1944 additions and 60 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` |
+2
View File
@@ -82,6 +82,8 @@ const ENV_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'CLINE_API_KEY',
+1
View File
@@ -19,6 +19,7 @@ import type { ProviderPreset } from '../utils/providerProfiles.js'
const EXPECTED_PRESETS = [
'anthropic',
'atlas-cloud',
'apismart',
'aimlapi',
'openai',
'ollama',
+27
View File
@@ -18,6 +18,7 @@ const originalEnv = {
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
OPENAI_API_KEYS: process.env.OPENAI_API_KEYS,
OPENAI_MODEL: process.env.OPENAI_MODEL,
APISMART_API_KEY: process.env.APISMART_API_KEY,
ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS,
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
@@ -59,6 +60,7 @@ function clearProviderEnv(): void {
delete process.env.OPENAI_API_KEY
delete process.env.OPENAI_API_KEYS
delete process.env.OPENAI_MODEL
delete process.env.APISMART_API_KEY
delete process.env.ANTHROPIC_CUSTOM_HEADERS
delete process.env.CLAUDE_CODE_USE_OPENAI
delete process.env.CLAUDE_CODE_USE_GEMINI
@@ -93,6 +95,7 @@ afterEach(() => {
restoreEnvValue('OPENAI_API_KEY')
restoreEnvValue('OPENAI_API_KEYS')
restoreEnvValue('OPENAI_MODEL')
restoreEnvValue('APISMART_API_KEY')
restoreEnvValue('ANTHROPIC_CUSTOM_HEADERS')
restoreEnvValue('CLAUDE_CODE_USE_OPENAI')
restoreEnvValue('CLAUDE_CODE_USE_GEMINI')
@@ -110,6 +113,30 @@ afterEach(() => {
})
describe('discoverModelsForRoute', () => {
test('does not send an ApiSmart key to an overridden discovery URL', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
process.env.APISMART_API_KEY = 'apismart-secret'
let didFetch = false
let authorization: string | null | undefined
setMockFetch(mock((_input: string | URL | Request, init?: RequestInit) => {
didFetch = true
authorization = new Headers(init?.headers).get('authorization')
return Promise.resolve(
new Response(JSON.stringify({ data: [] }), {
headers: { 'Content-Type': 'application/json' },
}),
)
}) as unknown as typeof globalThis.fetch)
await discoverModelsForRoute('apismart', {
baseUrl: 'https://proxy.example/v1',
forceRefresh: true,
})
expect(didFetch).toBe(true)
expect(authorization).not.toBe('Bearer apismart-secret')
})
test('uses built-in openai-compatible discovery and caches results for dynamic routes', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
+14 -1
View File
@@ -15,6 +15,7 @@ import type {
import { resolveRouteIdFromBaseUrl } from './index.js'
import {
getRouteDescriptor,
isCanonicalApismartInferenceBaseUrl,
resolveActiveRouteIdFromEnv,
resolveRouteCredentialValue,
} from './routeMetadata.js'
@@ -152,12 +153,23 @@ function getRouteBaseUrl(
function getRouteDiscoveryApiKey(
routeId: string,
options?: { apiKey?: string },
options?: { baseUrl?: string; apiKey?: string },
): string | undefined {
if (getRouteCatalog(routeId)?.discovery?.requiresAuth === false) {
return undefined
}
const baseUrl = getRouteBaseUrl(routeId, options)
// ApiSmart's dedicated token must never be used for an overridden discovery
// URL. Apply the same exact inference-endpoint boundary used by requests and
// profiles before considering either a caller-provided or ambient key.
if (
routeId === 'apismart' &&
!isCanonicalApismartInferenceBaseUrl(baseUrl)
) {
return undefined
}
if (hasInvalidCredentialPlaceholder(options?.apiKey)) {
return undefined
}
@@ -170,6 +182,7 @@ function getRouteDiscoveryApiKey(
return firstUsableCredential(
resolveRouteCredentialValue({
routeId,
baseUrl,
processEnv: process.env,
}),
)
@@ -0,0 +1,44 @@
import { describe, expect, test } from 'bun:test'
import apismart from './apismart.js'
const mapModel = apismart.catalog?.discovery?.mapModel
describe('apismart gateway', () => {
test('uses hybrid discovery with dedicated credentials', () => {
expect(apismart.id).toBe('apismart')
expect(apismart.catalog?.source).toBe('hybrid')
expect(apismart.catalog?.discovery?.kind).toBe('openai-compatible')
expect(apismart.catalog?.discovery?.requiresAuth).toBe(true)
expect(apismart.setup.dedicatedCredentialsOnly).toBe(true)
expect(apismart.setup.credentialEnvVars).toEqual(['APISMART_API_KEY'])
expect(apismart.defaultBaseUrl).toBe('https://gw.apismart.ai/v1')
expect(apismart.defaultModel).toBe('DEEPSEEK_V4_FLASH')
expect(mapModel).toBeDefined()
})
test('curated catalog keeps ApiSmart case-sensitive model ids', () => {
const models = apismart.catalog?.models ?? []
expect(models.some(model => model.apiName === 'DEEPSEEK_V4_FLASH')).toBe(
true,
)
expect(models.some(model => model.apiName === 'GLM_5.2')).toBe(true)
expect(models.some(model => model.apiName === 'QWEN_3_7_MAX')).toBe(true)
})
test('mapModel keeps chat models and drops image/video ids', () => {
if (!mapModel) throw new Error('mapModel missing')
expect(mapModel({ id: 'DEEPSEEK_V4_FLASH', owned_by: 'DeepSeek' })).toEqual(
{
id: 'DEEPSEEK_V4_FLASH',
apiName: 'DEEPSEEK_V4_FLASH',
label: 'DEEPSEEK_V4_FLASH (DeepSeek)',
},
)
expect(mapModel({ id: 'seedream-5.0' })).toBeNull()
expect(mapModel({ id: 'seedance-2.0' })).toBeNull()
expect(mapModel({ id: 'happyhorse-1.0' })).toBeNull()
expect(mapModel({})).toBeNull()
expect(mapModel(null)).toBeNull()
})
})
+227
View File
@@ -0,0 +1,227 @@
import { defineGateway } from '../define.js'
const NON_CHAT_MODEL_PATTERN =
/(seedream|seedance|happyhorse|image|video|vision-preview)/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 mapApismartModel(raw: unknown) {
if (!isRecord(raw)) {
return null
}
const id = getTrimmedString(raw, 'id')
if (!id || NON_CHAT_MODEL_PATTERN.test(id)) {
return null
}
const ownedBy =
getTrimmedString(raw, 'owned_by') || getTrimmedString(raw, 'ownedBy')
const label = ownedBy ? `${id} (${ownedBy})` : id
return {
id,
apiName: id,
label,
}
}
// Curated from https://www.apismart.ai/models (PAGE_MODEL_LIST LLM entries).
// Exact Model IDs are case-sensitive per ApiSmart docs.
const curatedModels = [
{
id: 'DEEPSEEK_V4_FLASH',
apiName: 'DEEPSEEK_V4_FLASH',
aliases: ['deepseek-v4-flash'],
modelDescriptorId: 'deepseek-v4-flash',
label: 'DeepSeek V4 Flash',
contextWindow: 1_048_576,
maxOutputTokens: 8_192,
},
{
id: 'DEEPSEEK_V4_PRO',
apiName: 'DEEPSEEK_V4_PRO',
aliases: ['deepseek-v4-pro'],
modelDescriptorId: 'deepseek-v4-pro',
label: 'DeepSeek V4 Pro',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'DEEPSEEK_V3_2',
apiName: 'DEEPSEEK_V3_2',
aliases: ['deepseek-v3.2'],
modelDescriptorId: 'deepseek-ai/deepseek-v3.2',
label: 'DeepSeek V3.2',
contextWindow: 131_072,
maxOutputTokens: 8_192,
},
{
id: 'KIMI_K2_6',
apiName: 'KIMI_K2_6',
aliases: ['kimi-k2.6'],
modelDescriptorId: 'kimi-k2.6',
label: 'Kimi K2.6',
contextWindow: 262_144,
maxOutputTokens: 16_384,
},
{
id: 'KIMI_K2_5',
apiName: 'KIMI_K2_5',
aliases: ['kimi-k2.5'],
modelDescriptorId: 'kimi-k2.5',
label: 'Kimi K2.5',
contextWindow: 262_144,
maxOutputTokens: 16_384,
},
{
id: 'KIMI_K3',
apiName: 'KIMI_K3',
aliases: ['kimi-k3', 'k3'],
modelDescriptorId: 'k3',
label: 'Kimi K3',
contextWindow: 1_075_200,
maxOutputTokens: 8_192,
},
{
id: 'GLM_5.2',
apiName: 'GLM_5.2',
aliases: ['glm-5.2'],
modelDescriptorId: 'glm-5.2',
label: 'GLM 5.2',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'GLM_5_1',
apiName: 'GLM_5_1',
aliases: ['glm-5.1'],
modelDescriptorId: 'glm-5.1',
label: 'GLM 5.1',
contextWindow: 204_800,
maxOutputTokens: 16_384,
},
{
id: 'GLM_5',
apiName: 'GLM_5',
aliases: ['glm-5'],
modelDescriptorId: 'glm-5',
label: 'GLM 5',
contextWindow: 209_920,
maxOutputTokens: 16_384,
},
{
id: 'QWEN_3_7_MAX',
apiName: 'QWEN_3_7_MAX',
aliases: ['qwen3.7-max'],
modelDescriptorId: 'qwen3.7-max',
label: 'Qwen 3.7 Max',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'QWEN_3_6_PLUS',
apiName: 'QWEN_3_6_PLUS',
aliases: ['qwen3.6-plus'],
modelDescriptorId: 'qwen3.6-plus',
label: 'Qwen 3.6 Plus',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'MINIMAX_M3',
apiName: 'MINIMAX_M3',
aliases: ['minimax-m3'],
modelDescriptorId: 'minimax-m3',
label: 'MiniMax M3',
contextWindow: 1_048_576,
maxOutputTokens: 16_384,
},
{
id: 'MINIMAX_M2_5',
apiName: 'MINIMAX_M2_5',
aliases: ['minimax-m2.5'],
modelDescriptorId: 'minimax-m2.5',
label: 'MiniMax M2.5',
contextWindow: 200_704,
maxOutputTokens: 16_384,
},
{
id: 'MINIMAX_M2_5_HIGHSPEED',
apiName: 'MINIMAX_M2_5_HIGHSPEED',
aliases: ['minimax-m2.5-highspeed'],
modelDescriptorId: 'minimax-m2.5-highspeed',
label: 'MiniMax M2.5 HighSpeed',
contextWindow: 204_800,
maxOutputTokens: 16_384,
},
]
export default defineGateway({
id: 'apismart',
label: 'ApiSmart',
category: 'aggregating',
defaultBaseUrl: 'https://gw.apismart.ai/v1',
defaultModel: 'DEEPSEEK_V4_FLASH',
supportsModelRouting: true,
setup: {
requiresAuth: true,
authMode: 'api-key',
credentialEnvVars: ['APISMART_API_KEY'],
dedicatedCredentialsOnly: true,
},
startup: {
probeReadiness: 'openai-compatible-models',
},
transportConfig: {
kind: 'openai-compatible',
openaiShim: {
supportsApiFormatSelection: false,
supportsAuthHeaders: false,
// ApiSmart chat-completions examples use max_tokens.
maxTokensField: 'max_tokens',
},
},
preset: {
id: 'apismart',
description: 'ApiSmart unified OpenAI-compatible gateway',
vendorId: 'openai',
apiKeyEnvVars: ['APISMART_API_KEY'],
modelEnvVars: ['APISMART_MODEL', 'OPENAI_MODEL'],
},
validation: {
kind: 'credential-env',
routing: {
matchDefaultBaseUrl: true,
matchBaseUrlHosts: ['gw.apismart.ai'],
},
credentialEnvVars: ['APISMART_API_KEY'],
missingCredentialMessage:
'ApiSmart auth is required. Set APISMART_API_KEY.',
},
catalog: {
// /v1/models exists and requires a Bearer key; curated LLM ids stay visible
// before discovery and when refresh fails.
source: 'hybrid',
discovery: {
kind: 'openai-compatible',
requiresAuth: true,
mapModel: mapApismartModel,
},
discoveryCacheTtl: '1d',
discoveryRefreshMode: 'background-if-stale',
allowManualRefresh: true,
models: [...curatedModels],
},
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 gatewayApismart from '../gateways/apismart.js'
import gatewayAtlasCloud from '../gateways/atlas-cloud.js'
import gatewayAtomicChat from '../gateways/atomic-chat.js'
import gatewayAzureOpenai from '../gateways/azure-openai.js'
@@ -89,7 +90,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, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, 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": "apismart",
"routeKind": "gateway",
"routeId": "apismart",
"vendorId": "openai",
"gatewayId": "apismart",
"description": "ApiSmart unified OpenAI-compatible gateway",
"apiKeyEnvVars": [
"APISMART_API_KEY"
],
"modelEnvVars": [
"APISMART_MODEL",
"OPENAI_MODEL"
]
},
{
"preset": "atlas-cloud",
"routeKind": "gateway",
@@ -527,6 +542,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"anthropic",
"dashscope-cn",
"dashscope-intl",
"apismart",
"atlas-cloud",
"azure-openai",
"bankr",
+200
View File
@@ -6,6 +6,8 @@ import {
getRouteDefaultBaseUrl,
getRouteDefaultModel,
getRouteProviderTypeLabel,
isApismartBaseUrl,
isCanonicalApismartInferenceBaseUrl,
isCloudflareBaseUrl,
isLongcatBaseUrl,
resolveActiveRouteIdFromEnv,
@@ -221,6 +223,18 @@ test('getRouteCredentialEnvVars omits the openai fallback for dedicatedCredentia
ATLAS_CLOUD_API_KEY: 'atlas-key',
}),
).toBe('atlas-key')
expect(getRouteCredentialEnvVars('apismart')).toEqual(['APISMART_API_KEY'])
expect(
getRouteCredentialValue('apismart', {
OPENAI_API_KEY: 'sk-openai-generic',
}),
).toBeUndefined()
expect(
getRouteCredentialValue('apismart', {
OPENAI_API_KEY: 'sk-openai-generic',
APISMART_API_KEY: 'apismart-key',
}),
).toBe('apismart-key')
})
test('getRouteCredentialValue reads the first configured route credential', () => {
@@ -291,6 +305,32 @@ test('route credential discovery ignores mixed placeholder OpenAI pools before s
).toBe('sk-openai-single')
})
test('ApiSmart dedicated credential is limited to the canonical inference base URL', () => {
const processEnv = { APISMART_API_KEY: 'apismart-secret' }
expect(
resolveRouteCredentialValue({
routeId: 'apismart',
baseUrl: 'https://gw.apismart.ai/v1',
processEnv,
}),
).toBe('apismart-secret')
expect(
resolveRouteCredentialValue({
routeId: 'apismart',
baseUrl: 'https://gw.apismart.ai/v1/models',
processEnv,
}),
).toBeUndefined()
expect(
resolveRouteCredentialValue({
routeId: 'apismart',
baseUrl: 'https://gw.apismart.ai/v2',
processEnv,
}),
).toBeUndefined()
})
test('Venice route metadata uses official OpenAI-compatible defaults', () => {
expect(getRouteDefaultBaseUrl('venice')).toBe('https://api.venice.ai/api/v1')
expect(getRouteDefaultModel('venice')).toBe('venice-uncensored')
@@ -305,6 +345,53 @@ test('AI/ML API route metadata uses official OpenAI-compatible defaults', () =>
expect(resolveRouteIdFromBaseUrl('https://api.aimlapi.com/v1/chat/completions')).toBe('aimlapi')
})
test('ApiSmart route metadata uses official OpenAI-compatible defaults', () => {
expect(getRouteDefaultBaseUrl('apismart')).toBe('https://gw.apismart.ai/v1')
expect(getRouteDefaultModel('apismart')).toBe('DEEPSEEK_V4_FLASH')
expect(resolveRouteIdFromBaseUrl('https://gw.apismart.ai/v1')).toBe('apismart')
expect(resolveRouteIdFromBaseUrl('https://gw.apismart.ai/v1/chat/completions')).toBe(
'apismart',
)
})
test('isApismartBaseUrl requires the documented HTTPS endpoint', () => {
expect(isApismartBaseUrl('https://gw.apismart.ai/v1')).toBe(true)
expect(isApismartBaseUrl('http://gw.apismart.ai/v1')).toBe(false)
expect(isApismartBaseUrl('https://gw.apismart.ai:8443/v1')).toBe(false)
expect(resolveRouteIdFromBaseUrl('http://gw.apismart.ai/v1')).toBe(null)
expect(resolveRouteIdFromBaseUrl('https://gw.apismart.ai:8443/v1')).toBe(null)
})
test('isCanonicalApismartInferenceBaseUrl requires the exact /v1 inference path', () => {
expect(isCanonicalApismartInferenceBaseUrl('https://gw.apismart.ai/v1')).toBe(
true,
)
expect(isCanonicalApismartInferenceBaseUrl('https://gw.apismart.ai/v1/')).toBe(
true,
)
expect(isCanonicalApismartInferenceBaseUrl('https://gw.apismart.ai/v1?x=1')).toBe(
false,
)
expect(isCanonicalApismartInferenceBaseUrl('https://gw.apismart.ai/v1#fragment')).toBe(
false,
)
expect(isCanonicalApismartInferenceBaseUrl('https://gw.apismart.ai')).toBe(
false,
)
expect(
isCanonicalApismartInferenceBaseUrl('https://gw.apismart.ai/v1/models'),
).toBe(false)
expect(
isCanonicalApismartInferenceBaseUrl('https://gw.apismart.ai/staging/v1'),
).toBe(false)
expect(isCanonicalApismartInferenceBaseUrl('https://gw.apismart.ai/v2')).toBe(
false,
)
// Host-scoped route match still accepts path suffixes for identity.
expect(isApismartBaseUrl('https://gw.apismart.ai/v1/models')).toBe(true)
expect(isApismartBaseUrl('https://gw.apismart.ai')).toBe(true)
})
test('AI/ML API route credential discovery ignores placeholder dedicated key', () => {
expect(
resolveRouteCredentialValue({
@@ -438,6 +525,110 @@ test('resolveActiveRouteIdFromEnv treats AI/ML API credential-only env as AI/ML
).toBe('aimlapi')
})
test('resolveActiveRouteIdFromEnv treats ApiSmart credential-only env as ApiSmart', () => {
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'apismart-key',
}),
).toBe('apismart')
})
test('resolveActiveRouteIdFromEnv ignores placeholder ApiSmart credentials', () => {
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'SUA_CHAVE',
}),
).not.toBe('apismart')
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'null',
}),
).not.toBe('apismart')
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'undefined',
}),
).not.toBe('apismart')
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'sua_chave',
AIMLAPI_API_KEY: 'aimlapi-key',
}),
).toBe('aimlapi')
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'null',
AIMLAPI_API_KEY: 'aimlapi-key',
}),
).toBe('aimlapi')
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'SUA_CHAVE',
AIMLAPI_API_KEY: 'aimlapi-key',
}),
).toBe('aimlapi')
expect(
resolveRouteCredentialValue({
routeId: 'apismart',
processEnv: { APISMART_API_KEY: 'SUA_CHAVE' },
}),
).toBeUndefined()
expect(
resolveRouteCredentialValue({
routeId: 'apismart',
processEnv: { APISMART_API_KEY: 'null' },
}),
).toBeUndefined()
})
test('resolveActiveRouteIdFromEnv prefers ApiSmart over ClinePass when both dedicated keys are set', () => {
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'apismart-key',
CLINE_API_KEY: 'cline-key',
}),
).toBe('apismart')
})
test('resolveActiveRouteIdFromEnv prefers ApiSmart over AI/ML API when both dedicated keys are set', () => {
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'apismart-key',
AIMLAPI_API_KEY: 'aimlapi-key',
}),
).toBe('apismart')
})
test('resolveActiveRouteIdFromEnv refines generic OpenAI profile by ApiSmart base URL', () => {
expect(
resolveActiveRouteIdFromEnv({
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_API_KEY: 'sk-openai-generic',
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
}),
).toBe('apismart')
})
test('resolveActiveRouteIdFromEnv does not retain ApiSmart identity for a retargeted profile', () => {
const baseUrl = 'https://proxy.example/v1'
expect(
resolveActiveRouteIdFromEnv(
{ CLAUDE_CODE_USE_OPENAI: '1', OPENAI_BASE_URL: baseUrl },
{ activeProfileProvider: 'apismart', activeProfileBaseUrl: baseUrl },
),
).toBe('custom')
})
test('resolveActiveRouteIdFromEnv honors an explicit competing route over an ambient ApiSmart key', () => {
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'apismart-key',
AIMLAPI_API_KEY: 'aimlapi-key',
OPENAI_BASE_URL: 'https://api.aimlapi.com/v1',
}),
).toBe('aimlapi')
})
test('resolveActiveRouteIdFromEnv prefers dedicated AI/ML API key over ambient OpenAI keys', () => {
expect(
resolveActiveRouteIdFromEnv({
@@ -467,6 +658,15 @@ test('resolveActiveRouteIdFromEnv keeps explicit OpenAI mode compatible with AI/
).toBe('aimlapi')
})
test('resolveActiveRouteIdFromEnv keeps explicit OpenAI mode compatible with ApiSmart key-only setup', () => {
expect(
resolveActiveRouteIdFromEnv({
APISMART_API_KEY: 'apismart-key',
CLAUDE_CODE_USE_OPENAI: '1',
}),
).toBe('apismart')
})
test('resolveActiveRouteIdFromEnv does not infer AI/ML API with a conflicting OpenAI base URL', () => {
expect(
resolveActiveRouteIdFromEnv({
+130 -2
View File
@@ -224,7 +224,8 @@ function hasUsableEnvCredentialValue(
if (
envVar === 'OPENAI_API_KEYS' ||
envVar === 'OPENAI_API_KEY' ||
envVar === 'AIMLAPI_API_KEY'
envVar === 'AIMLAPI_API_KEY' ||
envVar === 'APISMART_API_KEY'
) {
return hasUsableOpenAICredential(value)
}
@@ -402,6 +403,65 @@ export function isClinePassBaseUrl(value: string | undefined): boolean {
}
}
/**
* Host-scoped ApiSmart route match. Used for env-only conflict detection and
* base-URL route identity (including `/v1/chat/completions` path suffixes that
* still target the ApiSmart host). Credential forwarding and ambient-key
* withholding use {@link isCanonicalApismartInferenceBaseUrl} instead — same
* split AIMLAPI uses between host match and canonical inference URL.
*/
export function isApismartBaseUrl(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() === 'gw.apismart.ai'
)
} catch {
return false
}
}
/**
* Exact documented ApiSmart inference endpoint (`https://gw.apismart.ai/v1`).
* Path suffixes (`/v1/models`), alternate versions (`/v2`), and host-only URLs
* are not canonical — forwarding dedicated credentials there would send the
* key to the wrong request/discovery path.
*/
const APISMART_CANONICAL_INFERENCE_BASE_URL = 'https://gw.apismart.ai/v1'
export function isCanonicalApismartInferenceBaseUrl(
value: string | undefined,
): boolean {
const trimmed = value?.trim()
if (!trimmed) {
return false
}
try {
const canonical = new URL(APISMART_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
}
}
/**
* Checks whether the given URL value targets the Cloudflare Workers AI
* OpenAI-compatible API, i.e. `api.cloudflare.com` **and** the Workers AI path
@@ -591,6 +651,30 @@ function hasConflictingOpenAIBaseUrlForRoute(
)
}
function hasExplicitOpenAIBaseUrlForRoute(
processEnv: NodeJS.ProcessEnv,
isRouteBaseUrl: (value: string | undefined) => boolean,
): boolean {
if (hasNonEmptyEnvValue(processEnv.OPENAI_BASE_URL)) {
return isRouteBaseUrl(processEnv.OPENAI_BASE_URL)
}
return (
hasNonEmptyEnvValue(processEnv.OPENAI_API_BASE) &&
isRouteBaseUrl(processEnv.OPENAI_API_BASE)
)
}
function hasCompetingApismartCredential(
processEnv: NodeJS.ProcessEnv,
isRouteBaseUrl: (value: string | undefined) => boolean,
): boolean {
return (
hasUsableOpenAICredential(processEnv.APISMART_API_KEY) &&
!hasExplicitOpenAIBaseUrlForRoute(processEnv, isRouteBaseUrl)
)
}
function isAimlapiBaseUrl(baseUrl?: string): boolean {
return normalizeHost(baseUrl) === 'api.aimlapi.com'
}
@@ -627,6 +711,7 @@ export function hasAimlapiEnvOnlyProviderIntent(
): boolean {
return (
hasUsableOpenAICredential(processEnv.AIMLAPI_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isAimlapiBaseUrl) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isAimlapiBaseUrl) &&
hasNoExplicitNonOpenAIProvider(processEnv)
)
@@ -638,6 +723,7 @@ export function hasXaiEnvOnlyProviderIntent(
return (
hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isXaiBaseUrl) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isXaiBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -659,6 +745,7 @@ export function hasMiniMaxEnvOnlyProviderIntent(
(!hasAnyUsableOpenAICredential(processEnv) &&
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isMiniMaxBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)))
)
}
@@ -672,6 +759,7 @@ export function hasVeniceEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.XAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isVeniceBaseUrl) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isVeniceBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -687,6 +775,7 @@ export function hasXiaomiMimoEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.MINIMAX_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.VENICE_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isXiaomiMimoBaseUrl) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isXiaomiMimoBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -704,6 +793,7 @@ export function hasNearaiEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.LONGCAT_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isNearaiBaseUrl) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isNearaiBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -726,6 +816,7 @@ export function hasFireworksEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.LONGCAT_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isFireworksBaseUrl) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isFireworksBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -744,6 +835,7 @@ export function hasLongcatEnvOnlyProviderIntent(
!hasNonEmptyEnvValue(processEnv.NEARAI_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.FIREWORKS_API_KEY) &&
!hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isLongcatBaseUrl) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isLongcatBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
@@ -759,11 +851,26 @@ export function hasClinePassEnvOnlyProviderIntent(
): boolean {
return (
hasNonEmptyEnvValue(processEnv.CLINE_API_KEY) &&
!hasCompetingApismartCredential(processEnv, isClinePassBaseUrl) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isClinePassBaseUrl) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
)
}
export function hasApismartEnvOnlyProviderIntent(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
// Match AIMLAPI: ApiSmart is an OpenAI-compatible dedicated-key route, so a
// lingering CLAUDE_CODE_USE_OPENAI=1 from a prior OpenAI session must not
// suppress env-only ApiSmart identity. Only true non-OpenAI providers
// (Gemini/GitHub/Bedrock/...) block this intent.
return (
hasUsableOpenAICredential(processEnv.APISMART_API_KEY) &&
!hasConflictingOpenAIBaseUrlForRoute(processEnv, isApismartBaseUrl) &&
hasNoExplicitNonOpenAIProvider(processEnv)
)
}
export function resolveEnvOnlyProviderRouteId(
processEnv: NodeJS.ProcessEnv = process.env,
):
@@ -776,6 +883,7 @@ export function resolveEnvOnlyProviderRouteId(
| 'fireworks'
| 'longcat'
| 'clinepass'
| 'apismart'
| null {
if (
hasMiniMaxRouteIntent(processEnv) &&
@@ -820,6 +928,10 @@ export function resolveEnvOnlyProviderRouteId(
return 'clinepass'
}
if (hasApismartEnvOnlyProviderIntent(processEnv)) {
return 'apismart'
}
return null
}
@@ -884,6 +996,18 @@ export function resolveRouteCredentialValue(
return undefined
}
// ApiSmart's host is intentionally sufficient for route identity, but its
// dedicated credential is valid only for the documented inference base.
// Keep those concerns separate so discovery, versioned, or custom paths on
// the same host cannot receive the bearer token.
if (
routeId === 'apismart' &&
options?.baseUrl !== undefined &&
!isCanonicalApismartInferenceBaseUrl(options.baseUrl)
) {
return undefined
}
return getRouteCredentialValue(routeId, processEnv)
}
@@ -1004,7 +1128,8 @@ export function resolveRouteIdFromBaseUrl(
// unrelated Cloudflare API URL would inherit Workers-AI routing.
if (
(route.id === 'cloudflare' && !isCloudflareBaseUrl(baseUrl)) ||
(route.id === 'longcat' && !isLongcatBaseUrl(baseUrl))
(route.id === 'longcat' && !isLongcatBaseUrl(baseUrl)) ||
(route.id === 'apismart' && !isApismartBaseUrl(baseUrl))
) {
continue
}
@@ -1041,6 +1166,9 @@ function profileRouteHonorsBaseUrlBoundary(
if (routeId === 'longcat') {
return isLongcatBaseUrl(baseUrl)
}
if (routeId === 'apismart') {
return isApismartBaseUrl(baseUrl)
}
return true
}
+20
View File
@@ -68,6 +68,7 @@ const originalEnv = {
FIREWORKS_API_KEY: process.env.FIREWORKS_API_KEY,
LONGCAT_API_KEY: process.env.LONGCAT_API_KEY,
AIMLAPI_API_KEY: process.env.AIMLAPI_API_KEY,
APISMART_API_KEY: process.env.APISMART_API_KEY,
NVIDIA_NIM: process.env.NVIDIA_NIM,
NVIDIA_API_KEY: process.env.NVIDIA_API_KEY,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
@@ -121,6 +122,7 @@ function clearEnvForMiniMaxOnlyTest(): void {
delete process.env.FIREWORKS_API_KEY
delete process.env.LONGCAT_API_KEY
delete process.env.AIMLAPI_API_KEY
delete process.env.APISMART_API_KEY
delete process.env.NVIDIA_NIM
delete process.env.NVIDIA_API_KEY
process.env.ANTHROPIC_API_KEY = 'must-not-forward'
@@ -162,6 +164,7 @@ beforeEach(async () => {
delete process.env.FIREWORKS_API_KEY
delete process.env.LONGCAT_API_KEY
delete process.env.AIMLAPI_API_KEY
delete process.env.APISMART_API_KEY
delete process.env.OPENAI_AUTH_HEADER
delete process.env.OPENAI_AUTH_SCHEME
delete process.env.OPENAI_AUTH_HEADER_VALUE
@@ -211,6 +214,7 @@ afterEach(() => {
restoreEnv('FIREWORKS_API_KEY', originalEnv.FIREWORKS_API_KEY)
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('NVIDIA_NIM', originalEnv.NVIDIA_NIM)
restoreEnv('NVIDIA_API_KEY', originalEnv.NVIDIA_API_KEY)
restoreEnv('ANTHROPIC_API_KEY', originalEnv.ANTHROPIC_API_KEY)
@@ -751,6 +755,22 @@ test('env-only MiniMax fallback ignores non-MiniMax base overrides', async () =>
expect(process.env.OPENAI_MODEL).toBe('MiniMax-M2.7')
})
test('env-only ApiSmart setup withholds its key from a noncanonical same-host URL', async () => {
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.APISMART_API_KEY = 'apismart-test-key'
process.env.OPENAI_BASE_URL = 'https://gw.apismart.ai/v1/models'
await getAnthropicClient({ maxRetries: 0, model: 'DEEPSEEK_V4_FLASH' })
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1')
expect(process.env.OPENAI_BASE_URL).toBe('https://gw.apismart.ai/v1/models')
expect(process.env.OPENAI_API_KEY).toBeUndefined()
})
test('routes env-only AI/ML API requests through the OpenAI-compatible shim despite an ambient OpenAI key', async () => {
let capturedUrl: string | undefined
let capturedHeaders: Headers | undefined
+54
View File
@@ -43,6 +43,7 @@ import {
getLongcatBaseUrlOverride,
getMiniMaxBaseUrlOverride,
getNearaiBaseUrlOverride,
isCanonicalApismartInferenceBaseUrl,
getRouteDefaultBaseUrl,
getRouteDefaultModel,
getXaiBaseUrlOverride,
@@ -55,6 +56,7 @@ import {
shouldUseFirstPartyAnthropicAuthForProvider,
type ProviderOverride,
} from './authRouting.js'
import { hasUsableOpenAICredential } from './credentialPool.js'
import { AnthropicVertex } from './vertexClient.js'
import { importOptionalRuntimeModule } from '../../utils/optionalRuntimeModule.js'
@@ -374,6 +376,52 @@ function applyAimlapiEnvOnlyDefaults(): void {
delete process.env.OPENAI_AUTH_HEADER_VALUE
}
function applyApismartEnvOnlyDefaults(): void {
const baseUrlOverride =
usableProviderConfigEnvValue(process.env.OPENAI_BASE_URL) ||
usableProviderConfigEnvValue(process.env.OPENAI_API_BASE) ||
undefined
const modelOverride =
usableProviderConfigEnvValue(process.env.APISMART_MODEL) ||
usableProviderConfigEnvValue(process.env.OPENAI_MODEL) ||
undefined
const apiKey = process.env.APISMART_API_KEY
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL =
baseUrlOverride ?? getRouteDefaultBaseUrl('apismart')
process.env.OPENAI_MODEL = modelOverride ?? getRouteDefaultModel('apismart')
// Mirror only a usable dedicated key. Template placeholders must not become
// OPENAI_API_KEY side effects for later routes in this process. A same-host
// URL alone is not enough: the dedicated key belongs only to ApiSmart's
// documented /v1 inference endpoint.
if (
hasUsableOpenAICredential(apiKey) &&
isCanonicalApismartInferenceBaseUrl(process.env.OPENAI_BASE_URL)
) {
process.env.OPENAI_API_KEY = apiKey
} else {
delete process.env.OPENAI_API_KEY
}
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 {
const trimmed = value?.trim()
if (!trimmed) return undefined
const normalized = trimmed.toLowerCase()
return normalized === 'undefined' || normalized === 'null'
? undefined
: trimmed
}
export async function getAnthropicClient({
apiKey,
maxRetries,
@@ -511,6 +559,8 @@ export async function getAnthropicClient({
envOnlyProviderRouteId === 'longcat' && !useMiniMaxEnvOnlyProvider
const useAimlapiEnvOnlyProvider =
envOnlyProviderRouteId === 'aimlapi' && !useMiniMaxEnvOnlyProvider
const useApismartEnvOnlyProvider =
envOnlyProviderRouteId === 'apismart' && !useMiniMaxEnvOnlyProvider
if (useMiniMaxEnvOnlyProvider) {
applyMiniMaxEnvOnlyDefaults(model)
}
@@ -532,6 +582,9 @@ export async function getAnthropicClient({
if (useAimlapiEnvOnlyProvider) {
applyAimlapiEnvOnlyDefaults()
}
if (useApismartEnvOnlyProvider) {
applyApismartEnvOnlyDefaults()
}
const apiProvider = getAPIProvider()
const isFirstPartyBaseUrl = isFirstPartyAnthropicBaseUrl()
@@ -636,6 +689,7 @@ export async function getAnthropicClient({
useNearaiEnvOnlyProvider ||
useFireworksEnvOnlyProvider ||
useAimlapiEnvOnlyProvider ||
useApismartEnvOnlyProvider ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
+7 -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([
@@ -13,6 +13,12 @@ test('firstUsableCredential rejects pools containing placeholder credentials', (
expect(firstUsableCredential('key-a,key-b')).toBe('key-a')
expect(firstUsableCredential('key-a,SUA_CHAVE')).toBeUndefined()
expect(hasInvalidCredentialPlaceholder('key-a,SUA_CHAVE')).toBe(true)
expect(hasInvalidCredentialPlaceholder('null')).toBe(true)
expect(hasInvalidCredentialPlaceholder('undefined')).toBe(true)
expect(hasInvalidCredentialPlaceholder(' NULL ')).toBe(true)
expect(hasUsableOpenAICredential('null')).toBe(false)
expect(hasUsableOpenAICredential('sua_chave')).toBe(false)
expect(hasUsableOpenAICredential('apismart-key')).toBe(true)
})
test('CredentialPool rotates through healthy credentials', () => {
const pool = new CredentialPool(['key-a', 'key-b'])
+21 -2
View File
@@ -130,15 +130,34 @@ export function parseCredentialList(value: string | undefined): string[] {
.filter(Boolean)
}
// Template/dotenv sentinels that must never authenticate or win route
// precedence. Keep this list case-insensitive and shared so dedicated-key
// providers (ApiSmart, AIMLAPI, OpenAI pools) agree with profile sanitizers.
const INVALID_CREDENTIAL_PLACEHOLDERS = new Set([
'sua_chave',
'null',
'undefined',
])
export function isCredentialPlaceholder(value: string | undefined): boolean {
const trimmed = value?.trim()
if (!trimmed) {
return false
}
return INVALID_CREDENTIAL_PLACEHOLDERS.has(trimmed.toLowerCase())
}
export function hasInvalidCredentialPlaceholder(value: string | undefined): boolean {
return parseCredentialList(value).some(credential => credential === 'SUA_CHAVE')
return parseCredentialList(value).some(credential =>
isCredentialPlaceholder(credential),
)
}
export function hasUsableOpenAICredential(value: string | undefined): boolean {
const credentials = parseCredentialList(value)
return (
credentials.length > 0 &&
credentials.every(credential => credential !== 'SUA_CHAVE')
credentials.every(credential => !isCredentialPlaceholder(credential))
)
}
@@ -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([])
})
@@ -287,6 +287,7 @@ export async function executeOpenAIRequest(
requestProcessEnv.VENICE_API_KEY,
requestProcessEnv.MINIMAX_API_KEY,
requestProcessEnv.ATLAS_CLOUD_API_KEY,
requestProcessEnv.APISMART_API_KEY,
requestProcessEnv.NEARAI_API_KEY,
requestProcessEnv.FIREWORKS_API_KEY,
requestProcessEnv.LONGCAT_API_KEY,
@@ -353,7 +354,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,
+127
View File
@@ -314,6 +314,133 @@ test('resolveProviderRequest uses ClinePass model when no explicit base URL is s
expect(request.baseUrl).toBe('https://api.cline.bot/api/v1')
})
test('resolveProviderRequest honors an explicit ClinePass endpoint over an ambient ApiSmart key', () => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
CLINE_API_KEY: 'cp-key',
CLINE_API_MODEL: 'cline-pass/qwen3.7-max',
OPENAI_BASE_URL: 'https://api.cline.bot/api/v1',
},
})
expect(request.requestedModel).toBe('cline-pass/qwen3.7-max')
expect(request.baseUrl).toBe('https://api.cline.bot/api/v1')
})
test('resolveProviderRequest uses APISMART_MODEL when APISMART_API_KEY is present', () => {
const request = resolveProviderRequest({
processEnv: {
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: {
APISMART_API_KEY: 'apismart-key',
OPENAI_MODEL: 'GLM_5.2',
},
})
expect(request.requestedModel).toBe('GLM_5.2')
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
})
test('resolveProviderRequest treats blank APISMART_MODEL as unset for ApiSmart', () => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: ' ',
OPENAI_MODEL: 'QWEN_3_7_MAX',
},
})
expect(request.requestedModel).toBe('QWEN_3_7_MAX')
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
})
test.each(['null', 'undefined', ' NULL '])(
'resolveProviderRequest treats placeholder APISMART_MODEL %s as unset',
APISMART_MODEL => {
const request = resolveProviderRequest({
processEnv: { APISMART_API_KEY: 'apismart-key', APISMART_MODEL },
})
expect(request.requestedModel).toBe('DEEPSEEK_V4_FLASH')
},
)
test('resolveProviderRequest uses the ApiSmart route default when no model env is set', () => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
},
})
expect(request.requestedModel).toBe('DEEPSEEK_V4_FLASH')
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
})
test('resolveProviderRequest ignores APISMART_MODEL without APISMART_API_KEY', () => {
const request = resolveProviderRequest({
processEnv: {
APISMART_MODEL: 'KIMI_K3',
OPENAI_API_KEY: 'openai-key',
OPENAI_MODEL: 'gpt-4o',
},
})
expect(request.requestedModel).toBe('gpt-4o')
expect(request.baseUrl).toBe('https://api.openai.com/v1')
})
test('resolveProviderRequest ignores placeholder ApiSmart credentials', () => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'SUA_CHAVE',
APISMART_MODEL: 'KIMI_K3',
OPENAI_API_KEY: 'openai-key',
OPENAI_MODEL: 'gpt-4o',
},
})
expect(request.requestedModel).toBe('gpt-4o')
expect(request.baseUrl).toBe('https://api.openai.com/v1')
})
test('resolveProviderRequest prefers ApiSmart over ClinePass when both dedicated keys are set', () => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: 'KIMI_K3',
CLINE_API_KEY: 'cline-key',
CLINE_API_MODEL: 'cline-pass/qwen3.7-max',
},
})
expect(request.requestedModel).toBe('KIMI_K3')
expect(request.baseUrl).toBe('https://gw.apismart.ai/v1')
})
test('resolveProviderRequest ignores ApiSmart model when explicit OPENAI_BASE_URL points elsewhere', () => {
const request = resolveProviderRequest({
processEnv: {
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: 'KIMI_K3',
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_MODEL: 'gpt-4o',
},
})
expect(request.requestedModel).toBe('gpt-4o')
expect(request.baseUrl).toBe('https://api.openai.com/v1')
})
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')
+51 -6
View File
@@ -24,9 +24,12 @@ import {
} from './clinepassUsage/types.js'
import { getCatalogEntriesForRoute } from '../../integrations/registry.js'
import {
getRouteDefaultBaseUrl,
getRouteDefaultModel,
isApismartBaseUrl,
isClinePassBaseUrl,
} from '../../integrations/routeMetadata.js'
import { hasUsableOpenAICredential } from './credentialPool.js'
import {
openAIShimSupportsApiFormatForModel,
resolveOpenAIShimRuntimeContext,
@@ -238,7 +241,10 @@ function asEnvUrl(value: string | undefined): string | undefined {
if (!value) return undefined
const trimmed = value.trim()
if (!trimmed) return undefined
if (trimmed === 'undefined') {
const normalized = trimmed.toLowerCase()
// Windows/dotenv templates often materialize unset vars as the literal
// strings "undefined" or "null". Neither is a usable endpoint.
if (normalized === 'undefined' || normalized === 'null') {
return undefined
}
return trimmed
@@ -253,11 +259,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' },
)
}
@@ -267,6 +274,15 @@ function asNamedEnvUrl(
return trimmed
}
function asUsableModelEnvValue(value: string | undefined): string | undefined {
const trimmed = value?.trim()
if (!trimmed) return undefined
const normalized = trimmed.toLowerCase()
return normalized === 'undefined' || normalized === 'null'
? undefined
: trimmed
}
function readNestedString(
value: unknown,
paths: string[][],
@@ -930,6 +946,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 explicitBaseUrl = asEnvUrl(options?.baseUrl)
const normalizedMistralEnvBaseUrl = asNamedEnvUrl(
@@ -971,12 +988,33 @@ export function resolveProviderRequest(options?: {
explicitBaseUrl ?? primaryEnvBaseUrl ?? fallbackEnvBaseUrl
const hasConcreteNonClinePassBaseUrl =
Boolean(concreteBaseUrlBeforeDefault) && !isClinePassBaseUrl(concreteBaseUrlBeforeDefault)
const hasConcreteClinePassBaseUrl =
Boolean(concreteBaseUrlBeforeDefault) && isClinePassBaseUrl(concreteBaseUrlBeforeDefault)
const effectiveClinePassMode =
isClinePassMode && !isGithubMode && !hasConcreteNonClinePassBaseUrl
isClinePassMode &&
// With no endpoint identity, ApiSmart wins the ambiguous dedicated-key
// case. An explicit ClinePass endpoint is authoritative, however: an
// ambient ApiSmart key must not suppress CLINE_API_MODEL for that route.
(!isApismartMode || hasConcreteClinePassBaseUrl) &&
!isGithubMode &&
!hasConcreteNonClinePassBaseUrl
const clinePassDefaultModel = effectiveClinePassMode
? getRouteDefaultModel('clinepass')
: undefined
// ApiSmart model selection is only valid when no concrete non-ApiSmart
// base URL is explicitly provided via options or env. This prevents stale
// APISMART_API_KEY/APISMART_MODEL from overriding an explicit OPENAI_BASE_URL
// pointing at a different provider.
const hasConcreteNonApismartBaseUrl =
Boolean(concreteBaseUrlBeforeDefault) &&
!isApismartBaseUrl(concreteBaseUrlBeforeDefault)
const effectiveApismartMode =
isApismartMode && !isGithubMode && !hasConcreteNonApismartBaseUrl
const apismartDefaultModel = effectiveApismartMode
? getRouteDefaultModel('apismart')
: undefined
const requestedModel =
options?.model?.trim() ||
(isMistralMode
@@ -986,10 +1024,14 @@ export function resolveProviderRequest(options?: {
: effectiveClinePassMode
? processEnv.CLINE_API_MODEL?.trim() ||
processEnv.OPENAI_MODEL?.trim()
: processEnv.OPENAI_MODEL?.trim()) ||
: effectiveApismartMode
? asUsableModelEnvValue(processEnv.APISMART_MODEL) ||
asUsableModelEnvValue(processEnv.OPENAI_MODEL)
: processEnv.OPENAI_MODEL?.trim()) ||
options?.fallbackModel?.trim() ||
(isGeminiMode ? DEFAULT_GEMINI_MODEL : undefined) ||
clinePassDefaultModel ||
apismartDefaultModel ||
(isGithubMode ? 'github:copilot' : 'codexplan')
const descriptor = parseModelDescriptor(requestedModel)
@@ -997,7 +1039,10 @@ export function resolveProviderRequest(options?: {
explicitBaseUrl ??
primaryEnvBaseUrl ??
fallbackEnvBaseUrl ??
(effectiveClinePassMode ? DEFAULT_CLINEPASS_API_BASE_URL : undefined)
(effectiveClinePassMode ? DEFAULT_CLINEPASS_API_BASE_URL : undefined) ??
(effectiveApismartMode
? getRouteDefaultBaseUrl('apismart') ?? undefined
: undefined)
const githubEnterpriseEnvUrl = asGithubEnterpriseEnvUrl(
processEnv.GITHUB_ENTERPRISE_URL,
+18
View File
@@ -18,6 +18,8 @@ const TEST_ENV_KEYS = [
'CLAUDE_CODE_USE_OPENAI',
'CODEX_AUTH_JSON_PATH',
'CODEX_HOME',
'APISMART_API_KEY',
'APISMART_MODEL',
'OPENAI_API_KEYS',
'OPENAI_API_KEY',
'OPENAI_AZURE_STYLE',
@@ -274,6 +276,22 @@ describe('loadEnvFile', () => {
})
})
it('loads documented ApiSmart env-only provider setup values', () => {
const filePath = writeTempEnvFile([
'APISMART_API_KEY=apismart-key',
'APISMART_MODEL=KIMI_K3',
].join('\n'))
const loaded = loadEnvFile(filePath)
expect(process.env.APISMART_API_KEY).toBe('apismart-key')
expect(process.env.APISMART_MODEL).toBe('KIMI_K3')
expect(loaded).toEqual({
APISMART_API_KEY: 'apismart-key',
APISMART_MODEL: 'KIMI_K3',
})
})
it('loads documented Azure OpenAI API version values', () => {
const filePath = writeTempEnvFile(
'AZURE_OPENAI_API_VERSION=2024-12-01-preview',
+2
View File
@@ -15,6 +15,8 @@ const ALLOWED_ENV_FILE_KEYS = new Set([
'ANTHROPIC_VERTEX_BASE_URL',
'ANTHROPIC_VERTEX_PROJECT_ID',
'ATLAS_CLOUD_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'AWS_BEARER_TOKEN_BEDROCK',
'AWS_DEFAULT_REGION',
'AWS_PROFILE',
+141
View File
@@ -40,6 +40,8 @@ const ENV_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'LONGCAT_API_KEY',
'OPENGATEWAY_API_KEY',
'OPENGATEWAY_BASE_URL',
@@ -89,6 +91,8 @@ const RESET_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'LONGCAT_API_KEY',
'OPENGATEWAY_API_KEY',
'OPENGATEWAY_BASE_URL',
@@ -187,6 +191,16 @@ describe('applyProviderFlag - anthropic', () => {
expect(result.error).toBeUndefined()
expect(process.env.ANTHROPIC_API_KEY).toBe('first-party-key')
})
test('does not leave ApiSmart env-only selection active', () => {
process.env.APISMART_API_KEY = 'apismart-key'
process.env.APISMART_MODEL = 'KIMI_K3'
applyProviderFlag('anthropic', [])
expect(process.env.APISMART_API_KEY).toBeUndefined()
expect(process.env.APISMART_MODEL).toBeUndefined()
})
})
describe('applyProviderFlag - custom Anthropic-compatible', () => {
@@ -285,6 +299,16 @@ describe('applyProviderFlag - openai', () => {
applyProviderFlag('openai', ['--model', 'gpt-4o'])
expect(process.env.OPENAI_MODEL).toBe('gpt-4o')
})
test('does not leave ApiSmart env-only selection active', () => {
process.env.APISMART_API_KEY = 'apismart-key'
process.env.APISMART_MODEL = 'KIMI_K3'
applyProviderFlag('openai', [])
expect(process.env.APISMART_API_KEY).toBeUndefined()
expect(process.env.APISMART_MODEL).toBeUndefined()
})
})
describe('applyProviderFlag - cloudflare', () => {
@@ -968,6 +992,123 @@ describe('applyProviderFlag - atlas-cloud', () => {
})
})
describe('applyProviderFlag - apismart', () => {
test('sets ApiSmart OpenAI-compatible defaults and mirrors APISMART_API_KEY', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
const result = applyProviderFlag('apismart', [])
expect(result.error).toBeUndefined()
expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1')
expect(process.env.OPENAI_BASE_URL).toBe('https://gw.apismart.ai/v1')
expect(process.env.OPENAI_MODEL).toBe('DEEPSEEK_V4_FLASH')
expect(process.env.OPENAI_API_KEY).toBe('apismart-secret-key')
})
test('does not forward the dedicated key to a preserved custom base URL', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.OPENAI_BASE_URL = 'https://llm-proxy.internal.example/v1'
applyProviderFlag('apismart', [])
expect(process.env.OPENAI_BASE_URL).toBe('https://llm-proxy.internal.example/v1')
expect(process.env.OPENAI_API_KEY).toBeUndefined()
})
test.each(['null', 'undefined', ' NULL ', ' Undefined '])(
'treats placeholder OPENAI_BASE_URL %s as unset and applies ApiSmart defaults',
sentinel => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.OPENAI_BASE_URL = sentinel
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('clears unsupported OpenAI shim settings from a previous route', () => {
process.env.APISMART_API_KEY = 'apismart-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'
applyProviderFlag('apismart', [])
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()
})
test('clears inherited Anthropic custom headers', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Proxy-Auth: proxy-secret'
applyProviderFlag('apismart', [])
expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined()
})
test('uses APISMART_MODEL from env when --model is not provided', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.APISMART_MODEL = 'KIMI_K3'
applyProviderFlag('apismart', [])
expect(process.env.OPENAI_MODEL).toBe('KIMI_K3')
})
test('makes an explicit --model override APISMART_MODEL', () => {
process.env.APISMART_API_KEY = 'apismart-secret-key'
process.env.APISMART_MODEL = 'KIMI_K3'
applyProviderFlag('apismart', ['--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'
applyProviderFlag('apismart', [])
expect(process.env.OPENAI_API_KEY).toBe('apismart-secret-key')
})
test('clears a stale OPENAI_API_KEY when no ApiSmart key is set', () => {
process.env.OPENAI_API_KEY = 'existing-openai-key'
applyProviderFlag('apismart', [])
expect(process.env.OPENAI_API_KEY).toBeUndefined()
})
test.each(['SUA_CHAVE', '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'
applyProviderFlag('openai', [])
expect(process.env.OPENAI_API_KEY).toBeUndefined()
})
})
describe('applyProviderFlag - xai', () => {
test('sets CLAUDE_CODE_USE_OPENAI=1 with xAI defaults when unset', () => {
delete process.env.OPENAI_BASE_URL
+91 -1
View File
@@ -23,10 +23,14 @@ import {
getVendor,
isCloudflareBaseUrl,
isLongcatBaseUrl,
routeSupportsApiFormatSelection,
routeSupportsAuthHeaders,
resolveProfileRoute,
resolveRouteIdFromBaseUrl,
} from '../integrations/index.js'
import { PRESET_VENDOR_MAP } from '../integrations/compatibility.js'
import { isCanonicalApismartInferenceBaseUrl } from '../integrations/routeMetadata.js'
import { hasUsableOpenAICredential } from '../services/api/credentialPool.js'
import { isFirstPartyAnthropicBaseUrlForEnv } from './anthropicBaseUrl.js'
const PREFERRED_PROVIDER_ORDER = [
@@ -168,7 +172,16 @@ function getRouteDefaults(provider: string): {
function normalizeBaseUrlEnv(value: string | undefined): string | undefined {
const trimmed = value?.trim()
return trimmed && trimmed !== 'undefined' ? trimmed : undefined
if (!trimmed) {
return undefined
}
const normalized = trimmed.toLowerCase()
// dotenv / shell sentinels are not usable endpoints. Treating them as
// configured would preserve an invalid OPENAI_BASE_URL and block dedicated
// provider defaults (for example ApiSmart key mirroring).
return normalized === 'undefined' || normalized === 'null'
? undefined
: trimmed
}
function getConfiguredOpenAIBaseUrl(): string | undefined {
@@ -228,6 +241,28 @@ function applyOpenAIBaseUrlDefault(provider: string, baseUrl?: string): void {
}
}
function clearUnsupportedOpenAIShimSettings(routeId: string): void {
if (!routeSupportsApiFormatSelection(routeId)) {
delete process.env.OPENAI_API_FORMAT
}
if (!routeSupportsAuthHeaders(routeId)) {
delete process.env.OPENAI_AUTH_HEADER
delete process.env.OPENAI_AUTH_SCHEME
delete process.env.OPENAI_AUTH_HEADER_VALUE
}
}
function usableProviderModelEnvValue(
value: string | undefined,
): string | undefined {
const trimmed = value?.trim()
if (!trimmed) return undefined
const normalized = trimmed.toLowerCase()
return normalized === 'undefined' || normalized === 'null'
? undefined
: trimmed
}
/**
* Apply --model (without --provider) to process.env for the current process only.
*
@@ -316,6 +351,9 @@ export function applyProviderFlag(
process.env.OPENAI_API_KEY === process.env.ATLAS_CLOUD_API_KEY
? 'atlas-cloud'
: 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.NEARAI_API_KEY
? 'nearai'
: process.env.OPENAI_API_KEY !== undefined &&
@@ -369,6 +407,11 @@ export function applyProviderFlag(
if (hadCustomAnthropicEndpoint) {
delete process.env.ANTHROPIC_API_KEY
}
// `--provider anthropic` is an explicit selection even though the
// default provider has no positive mode flag. Do not let a dedicated
// OpenAI-compatible env-only route override it later in startup.
delete process.env.APISMART_API_KEY
delete process.env.APISMART_MODEL
delete process.env.ANTHROPIC_AUTH_TOKEN
delete process.env.ANTHROPIC_CUSTOM_HEADERS
break
@@ -411,6 +454,10 @@ export function applyProviderFlag(
case 'openai':
process.env.CLAUDE_CODE_USE_OPENAI = '1'
// An explicit generic OpenAI selection must not be reclassified as a
// dedicated env-only gateway during client startup.
delete process.env.APISMART_API_KEY
delete process.env.APISMART_MODEL
if (model) process.env.OPENAI_MODEL = model
break
@@ -580,6 +627,49 @@ export function applyProviderFlag(
}
break
case 'apismart':
process.env.CLAUDE_CODE_USE_OPENAI = '1'
// Keep provider-flag selection on the same descriptor-declared wire
// contract as env-only setup and saved profiles. ApiSmart does not
// support alternate API formats or custom auth headers.
clearUnsupportedOpenAIShimSettings('apismart')
delete process.env.ANTHROPIC_CUSTOM_HEADERS
applyOpenAIBaseUrlDefault(
provider,
defaultBaseUrl ?? 'https://gw.apismart.ai/v1',
)
{
const apismartModel = usableProviderModelEnvValue(
process.env.APISMART_MODEL,
)
if (apismartModel) {
process.env.OPENAI_MODEL = apismartModel
} else {
process.env.OPENAI_MODEL ??=
usableProviderModelEnvValue(process.env.OPENAI_MODEL) ||
defaultModel ||
'DEEPSEEK_V4_FLASH'
}
}
if (model) {
process.env.OPENAI_MODEL = model
process.env.APISMART_MODEL = model
}
// DedicatedCredentialsOnly: only APISMART_API_KEY authenticates this
// route. Mirror it into OPENAI_API_KEY for the shared shim transport,
// and clear any stale generic key so another provider's credential is
// never forwarded to ApiSmart. Only the documented `/v1` inference URL
// is eligible for mirroring (AIMLAPI canonical-host parity).
if (
hasUsableOpenAICredential(process.env.APISMART_API_KEY) &&
isCanonicalApismartInferenceBaseUrl(getConfiguredOpenAIBaseUrl())
) {
process.env.OPENAI_API_KEY = process.env.APISMART_API_KEY
} else {
delete process.env.OPENAI_API_KEY
}
break
case 'fireworks':
process.env.CLAUDE_CODE_USE_OPENAI = '1'
applyOpenAIBaseUrlDefault(provider, defaultBaseUrl)
+218
View File
@@ -16,6 +16,7 @@ import {
applyStartupEnvFromProfile,
buildStartupEnvFromProfile,
buildAtomicChatProfileEnv,
buildApismartProfileEnv,
buildCompatibilityProcessEnv,
buildCodexProfileEnv,
buildGeminiProfileEnv,
@@ -257,6 +258,204 @@ test('openai launch preserves persisted dedicated vendor credentials across rest
assert.equal(env.ATLAS_CLOUD_API_KEY, 'atlas-secret-key')
})
test('openai launch preserves persisted ApiSmart dedicated credentials across restart', 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: {},
})
assert.equal(env.OPENAI_BASE_URL, 'https://gw.apismart.ai/v1')
assert.equal(env.OPENAI_MODEL, 'DEEPSEEK_V4_FLASH')
assert.equal(env.OPENAI_API_KEY, 'apismart-secret-key')
assert.equal(env.APISMART_API_KEY, 'apismart-secret-key')
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'apismart')
})
test('openai launch prefers dedicated ApiSmart credentials over a legacy generic mirror', 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: 'legacy-generic-key',
APISMART_API_KEY: 'dedicated-key',
}),
goal: 'balanced',
processEnv: {},
})
assert.equal(env.APISMART_API_KEY, 'dedicated-key')
})
test('openai launch backfills APISMART_API_KEY from a legacy OpenAI-shaped ApiSmart profile', async () => {
// Pre-dedicated-key persisted envs only stored OPENAI_API_KEY. ApiSmart is
// dedicatedCredentialsOnly, so relaunch must recover APISMART_API_KEY from
// that mirrored value or the shim authenticates with nothing.
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',
}),
goal: 'coding',
processEnv: {},
})
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'apismart')
assert.equal(env.OPENAI_API_KEY, 'apismart-secret-key')
assert.equal(env.APISMART_API_KEY, 'apismart-secret-key')
assert.equal(
resolveRouteCredentialValue({
routeId: 'apismart',
processEnv: env,
baseUrl: env.OPENAI_BASE_URL,
}),
'apismart-secret-key',
)
})
test('openai launch never promotes an ambient generic key to an ApiSmart credential', async () => {
const env = await buildLaunchEnv({
profile: 'openai',
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
OPENAI_MODEL: 'DEEPSEEK_V4_FLASH',
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'apismart',
}),
goal: 'coding',
processEnv: {
OPENAI_API_KEY: 'generic-openai-key',
},
})
assert.equal(env.APISMART_API_KEY, undefined)
assert.equal(
resolveRouteCredentialValue({
routeId: 'apismart',
processEnv: env,
baseUrl: env.OPENAI_BASE_URL,
}),
undefined,
)
})
test('buildApismartProfileEnv prefers APISMART_MODEL over OPENAI_MODEL', () => {
const env = buildApismartProfileEnv({
apiKey: 'apismart-secret-key',
processEnv: {
APISMART_MODEL: 'KIMI_K3',
OPENAI_MODEL: 'GLM_5.2',
},
})
assert.ok(env)
assert.equal(env?.OPENAI_MODEL, 'KIMI_K3')
assert.equal(env?.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'apismart')
})
test('buildApismartProfileEnv refuses to copy the dedicated credential to a custom endpoint', () => {
const env = buildApismartProfileEnv({
apiKey: 'apismart-secret-key',
baseUrl: 'https://proxy.example/v1',
})
assert.equal(env, null)
})
test('buildApismartProfileEnv refuses non-canonical ApiSmart paths', () => {
assert.equal(
buildApismartProfileEnv({
apiKey: 'apismart-secret-key',
baseUrl: 'https://gw.apismart.ai/staging/v1',
}),
null,
)
assert.equal(
buildApismartProfileEnv({
apiKey: 'apismart-secret-key',
baseUrl: 'https://gw.apismart.ai',
}),
null,
)
})
test('openai launch withholds ambient ApiSmart credentials from a keyless proxy profile on restart', async () => {
const env = await buildLaunchEnv({
profile: 'openai',
persisted: profile('openai', {
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'apismart',
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
OPENAI_MODEL: 'DEEPSEEK_V4_FLASH',
}),
goal: 'coding',
processEnv: {
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
OPENAI_API_KEY: 'ambient-apismart-key',
APISMART_API_KEY: 'ambient-apismart-key',
},
})
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'apismart')
assert.equal(env.OPENAI_API_KEY, undefined)
assert.equal(env.APISMART_API_KEY, undefined)
const canonical = await buildLaunchEnv({
profile: 'openai',
persisted: profile('openai', {
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'apismart',
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
OPENAI_MODEL: 'DEEPSEEK_V4_FLASH',
}),
goal: 'coding',
processEnv: {
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
OPENAI_API_KEY: 'ambient-apismart-key',
APISMART_API_KEY: 'ambient-apismart-key',
},
})
assert.equal(canonical.OPENAI_API_KEY, 'ambient-apismart-key')
assert.equal(canonical.APISMART_API_KEY, 'ambient-apismart-key')
})
test('openai launch carries APISMART_API_KEY only when the route resolves to apismart', async () => {
const offRoute = await buildLaunchEnv({
profile: 'openai',
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_API_KEY: 'sk-openai',
APISMART_API_KEY: 'apismart-persisted',
}),
goal: 'coding',
processEnv: {
APISMART_API_KEY: 'apismart-ambient',
},
})
assert.equal(offRoute.APISMART_API_KEY, undefined)
const onRoute = await buildLaunchEnv({
profile: 'openai',
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://gw.apismart.ai/v1',
OPENAI_MODEL: 'DEEPSEEK_V4_FLASH',
OPENAI_API_KEY: 'apismart-key',
APISMART_API_KEY: 'apismart-key',
}),
goal: 'coding',
processEnv: {},
})
assert.equal(onRoute.APISMART_API_KEY, 'apismart-key')
})
test('openai launch prefers a live dedicated vendor key over the persisted one', async () => {
const env = await buildLaunchEnv({
profile: 'openai',
@@ -939,6 +1138,25 @@ test('buildStartupEnvFromProfile preserves concrete env-only NIM setup over stal
assert.equal(resolveActiveRouteIdFromEnv(env), 'nvidia-nim')
})
test('buildStartupEnvFromProfile preserves ApiSmart env-only setup over a saved profile', async () => {
const processEnv: NodeJS.ProcessEnv = {
APISMART_API_KEY: 'apismart-live',
}
const env = await buildStartupEnvFromProfile({
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_MODEL: 'gpt-4o',
OPENAI_API_KEY: 'stale-openai-key',
}),
processEnv,
})
assert.equal(env.APISMART_API_KEY, 'apismart-live')
assert.equal(resolveActiveRouteIdFromEnv(env), 'apismart')
assert.equal(env.OPENAI_BASE_URL, undefined)
})
test('buildStartupEnvFromProfile does not activate non-NIM env-only OpenAI-compatible setup', async () => {
const env = await buildStartupEnvFromProfile({
persisted: null,
+131 -24
View File
@@ -25,6 +25,7 @@ import { getErrnoCode } from './errors.js'
import {
getRouteDefaultBaseUrl,
getRouteDefaultModel,
isCanonicalApismartInferenceBaseUrl,
isLongcatBaseUrl,
normalizeXiaomiMimoBaseUrl,
resolveRouteCredentialValue,
@@ -111,6 +112,8 @@ const PROFILE_ENV_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'LONGCAT_API_KEY',
@@ -196,6 +199,7 @@ export type ProfileEnv = {
VENICE_API_KEY?: string
MIMO_API_KEY?: string
ATLAS_CLOUD_API_KEY?: string
APISMART_API_KEY?: string
CLINE_API_KEY?: string
NEARAI_API_KEY?: string
FIREWORKS_API_KEY?: string
@@ -632,6 +636,59 @@ export function buildAtlasCloudProfileEnv(options: {
}
}
export function buildApismartProfileEnv(options: {
model?: string | null
baseUrl?: string | null
apiKey?: string | null
processEnv?: NodeJS.ProcessEnv
}): ProfileEnv | null {
const processEnv = options.processEnv ?? process.env
const key = sanitizeApiKey(options.apiKey ?? processEnv.APISMART_API_KEY)
if (!key) {
return null
}
const defaultBaseUrl = getRouteDefaultBaseUrl('apismart')
const defaultModel = getRouteDefaultModel('apismart')
if (!defaultBaseUrl || !defaultModel) {
throw new Error('ApiSmart route defaults are missing from integration metadata.')
}
const secretSource: SecretValueSource = {
OPENAI_API_KEY: key,
APISMART_API_KEY: key,
}
const configuredBaseUrl =
sanitizeProviderConfigValue(options.baseUrl, secretSource) ||
sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL, secretSource)
// Only the documented `/v1` inference URL may carry the dedicated key.
// Host-only or path-suffixed ApiSmart URLs fall through to the generic
// OpenAI path (same canonical gate AIMLAPI uses for ambient forwarding).
if (
configuredBaseUrl &&
!isCanonicalApismartInferenceBaseUrl(configuredBaseUrl)
) {
return null
}
return {
OPENAI_BASE_URL: configuredBaseUrl || defaultBaseUrl,
OPENAI_MODEL:
normalizeProfileModel(
sanitizeProviderConfigValue(options.model, secretSource),
) ||
normalizeProfileModel(
sanitizeProviderConfigValue(processEnv.APISMART_MODEL, secretSource),
) ||
normalizeProfileModel(
sanitizeProviderConfigValue(processEnv.OPENAI_MODEL, secretSource),
) ||
defaultModel,
OPENAI_API_KEY: key,
APISMART_API_KEY: key,
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'apismart',
}
}
export function buildGeminiProfileEnv(options: {
model?: string | null
baseUrl?: string | null
@@ -1346,8 +1403,12 @@ function hasConcreteProviderSelection(
return true
}
// Env-only provider setups — no CLAUDE_CODE_USE_* flag needed
// Env-only provider setups — no CLAUDE_CODE_USE_* flag needed. These must
// survive startup-profile selection so client routing can apply their
// descriptor defaults; otherwise a saved/default profile clears the key
// before the env-only resolver has a chance to see it.
return (
sanitizeApiKey(processEnv.APISMART_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.FIREWORKS_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.NEARAI_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined
@@ -2001,12 +2062,12 @@ export async function buildLaunchEnv(options: {
} else {
delete env.CLAUDE_CODE_PROVIDER_ROUTE_ID
}
// A keyless retained aimlapi profile on a non-canonical (proxy) base URL must
// not receive the ambient canonical credential via the generic OPENAI_API_KEY
// /OPENAI_API_KEYS alias either (the generic selection above prefers the live
// shell value). Re-source the generic credential from the profile's OWN
// persisted env and drop a purely ambient one.
// Scoped to a launch that actually carries the aimlapi identity. A profile
// A keyless retained aimlapi/apismart profile on a non-canonical (proxy) base
// URL must not receive the ambient canonical credential via the generic
// OPENAI_API_KEY / OPENAI_API_KEYS alias either (the generic selection above
// prefers the live shell value). Re-source the generic credential from the
// profile's OWN persisted env and drop a purely ambient one.
// Scoped to a launch that actually carries the route identity. A profile
// retargeted to an endpoint it was not saved for keeps no identity, so it is
// handled by the route-agnostic precedence above rather than here: forcing the
// profile's own credential in would both hand a key to an endpoint it was not
@@ -2015,7 +2076,13 @@ export async function buildLaunchEnv(options: {
effectiveOpenAIRouteId === 'aimlapi' &&
!!env.OPENAI_BASE_URL?.trim() &&
!isCanonicalAimlapiInferenceBaseUrl(env.OPENAI_BASE_URL)
if (isNoncanonicalAimlapiLaunch) {
const isNoncanonicalApismartLaunch =
effectiveOpenAIRouteId === 'apismart' &&
!!env.OPENAI_BASE_URL?.trim() &&
!isCanonicalApismartInferenceBaseUrl(env.OPENAI_BASE_URL)
const isNoncanonicalDedicatedOpenAILaunch =
isNoncanonicalAimlapiLaunch || isNoncanonicalApismartLaunch
if (isNoncanonicalDedicatedOpenAILaunch) {
delete env.OPENAI_API_KEY
delete env.OPENAI_API_KEYS
const persistedCredential = resolveOpenAICredentialEnvSelection(persistedEnv)
@@ -2047,6 +2114,7 @@ export async function buildLaunchEnv(options: {
}
for (const dedicatedKey of [
'ATLAS_CLOUD_API_KEY',
'APISMART_API_KEY',
'NEARAI_API_KEY',
'FIREWORKS_API_KEY',
'LONGCAT_API_KEY',
@@ -2062,6 +2130,9 @@ export async function buildLaunchEnv(options: {
if (dedicatedKey === 'AIMLAPI_API_KEY' && effectiveOpenAIRouteId !== 'aimlapi') {
continue
}
if (dedicatedKey === 'APISMART_API_KEY' && effectiveOpenAIRouteId !== 'apismart') {
continue
}
if (dedicatedKey === 'NVIDIA_API_KEY' && effectiveOpenAIRouteId !== 'nvidia-nim') {
continue
}
@@ -2071,22 +2142,49 @@ export async function buildLaunchEnv(options: {
) {
continue
}
// On a non-canonical (proxy) aimlapi base URL, never source AIMLAPI_API_KEY
// from ambient/session credentials — that would leak the canonical AIMLAPI
// key to a user-controlled proxy on restart. The profile's OWN persisted key
// is still applied, since the user configured that key for that proxy.
const aimlapiBaseUrl = env.OPENAI_BASE_URL?.trim()
// On a non-canonical (proxy) aimlapi/apismart base URL, never source the
// dedicated key from ambient/session credentials — that would leak the
// canonical provider key to a user-controlled proxy on restart. The
// profile's OWN persisted key is still applied, since the user configured
// that key for that proxy.
const dedicatedBaseUrl = env.OPENAI_BASE_URL?.trim()
const withholdAmbientAimlapiKey =
dedicatedKey === 'AIMLAPI_API_KEY' &&
!!aimlapiBaseUrl &&
!isCanonicalAimlapiInferenceBaseUrl(aimlapiBaseUrl)
const dedicatedValue = withholdAmbientAimlapiKey
!!dedicatedBaseUrl &&
!isCanonicalAimlapiInferenceBaseUrl(dedicatedBaseUrl)
const withholdAmbientApismartKey =
dedicatedKey === 'APISMART_API_KEY' &&
!!dedicatedBaseUrl &&
!isCanonicalApismartInferenceBaseUrl(dedicatedBaseUrl)
const withholdAmbientDedicatedKey =
withholdAmbientAimlapiKey || withholdAmbientApismartKey
// AIMLAPI accepts generic OpenAI credentials, but ApiSmart is
// dedicatedCredentialsOnly. Never promote a shell OPENAI_API_KEY into the
// dedicated credential on relaunch.
const backfillDedicatedFromOpenAI =
dedicatedKey === 'AIMLAPI_API_KEY' &&
openAICredential?.kind === 'usable'
? sanitizeApiKey(openAICredential.value)
: undefined
// Older ApiSmart profiles predate APISMART_API_KEY and persisted their
// *profile-owned* credential only as OPENAI_API_KEY. Migrate that stored
// value for the canonical ApiSmart endpoint, but never use the live shell
// credential: the latter may belong to an unrelated OpenAI provider.
const persistedOpenAICredential = resolveOpenAICredentialEnvSelection(persistedEnv)
const backfillLegacyApismartProfileKey =
dedicatedKey === 'APISMART_API_KEY' &&
effectiveOpenAIRouteId === 'apismart' &&
!!dedicatedBaseUrl &&
isCanonicalApismartInferenceBaseUrl(dedicatedBaseUrl) &&
persistedOpenAICredential?.kind === 'usable'
? sanitizeApiKey(persistedOpenAICredential.value)
: undefined
const dedicatedValue = withholdAmbientDedicatedKey
? sanitizeApiKey(persistedEnv[dedicatedKey])
: (dedicatedKey === 'AIMLAPI_API_KEY' && openAICredential?.kind === 'usable'
? sanitizeApiKey(openAICredential.value)
: undefined) ||
: backfillDedicatedFromOpenAI ||
sanitizeApiKey(processEnv[dedicatedKey]) ||
sanitizeApiKey(persistedEnv[dedicatedKey])
sanitizeApiKey(persistedEnv[dedicatedKey]) ||
backfillLegacyApismartProfileKey
if (dedicatedValue) {
env[dedicatedKey] = dedicatedValue
}
@@ -2102,9 +2200,9 @@ export async function buildLaunchEnv(options: {
// client, and its own filter only drops `authorization`, `x-api-key` and
// `api-key` — a custom-named header such as `X-Proxy-Auth: <secret>` survives
// and is sent on every request. So an ambient value must be withheld from a
// non-canonical aimlapi launch exactly like the API key and the custom-auth
// trio; only headers the profile itself persisted are restored.
const customHeaders = isNoncanonicalAimlapiLaunch
// non-canonical aimlapi/apismart launch exactly like the API key and the
// custom-auth trio; only headers the profile itself persisted are restored.
const customHeaders = isNoncanonicalDedicatedOpenAILaunch
? persistedCustomHeaders
: shellCustomHeaders || persistedCustomHeaders
if (customHeaders) {
@@ -2178,7 +2276,16 @@ export async function buildStartupEnvFromProfile(options?: {
// If startup already has a concrete provider selection, keep trusting it.
// This prevents legacy profiles or the fresh-install default from becoming
// a silent third precedence layer over explicit env/flags.
if (hasConcreteProviderSelection(processEnv)) {
// A retained ApiSmart proxy profile carries route identity specifically to
// withhold ambient dedicated credentials from its noncanonical endpoint.
// Do not let an env-only key skip that guard; the persisted profile must be
// applied first so buildLaunchEnv can preserve the proxy boundary.
const persistedApismartProxy =
persisted?.profile === 'openai' &&
persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'apismart' &&
!!persisted.env.OPENAI_BASE_URL?.trim() &&
!isCanonicalApismartInferenceBaseUrl(persisted.env.OPENAI_BASE_URL)
if (hasConcreteProviderSelection(processEnv) && !persistedApismartProxy) {
return processEnv
}
+231
View File
@@ -70,6 +70,8 @@ const RESTORED_KEYS = [
'VENICE_API_KEY',
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'CLINE_API_KEY',
'HICAP_API_KEY',
'CLOUDFLARE_API_TOKEN',
@@ -265,6 +267,17 @@ function buildAtlasCloudProfile(overrides: Partial<ProviderProfile> = {}): Provi
})
}
function buildApismartProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
return buildProfile({
provider: 'apismart',
name: 'ApiSmart',
baseUrl: 'https://gw.apismart.ai/v1',
model: 'DEEPSEEK_V4_FLASH',
apiKey: 'apismart-test-key',
...overrides,
})
}
function buildClinePassProfile(overrides: Partial<ProviderProfile> = {}): ProviderProfile {
return buildProfile({
provider: 'clinepass',
@@ -799,6 +812,171 @@ describe('applyProviderProfileToProcessEnv', () => {
expect(getFreshAPIProvider()).toBe('openai')
})
test('apismart profile applies OpenAI-compatible env with APISMART_API_KEY mirror', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.CLAUDE_CODE_USE_GEMINI = '1'
applyProviderProfileToProcessEnv(buildApismartProfile())
const { getAPIProvider: getFreshAPIProvider } =
await importFreshProvidersModule()
expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined()
expect(String(process.env.CLAUDE_CODE_USE_OPENAI)).toBe('1')
expect(process.env.OPENAI_BASE_URL).toBe('https://gw.apismart.ai/v1')
expect(process.env.OPENAI_MODEL).toBe('DEEPSEEK_V4_FLASH')
expect(process.env.OPENAI_API_KEY).toBe('apismart-test-key')
expect(process.env.APISMART_API_KEY).toBe('apismart-test-key')
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
expect(getFreshAPIProvider()).toBe('openai')
})
test('apismart profile clears a stale route-specific model before applying its saved model', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.APISMART_MODEL = 'KIMI_K3'
applyProviderProfileToProcessEnv(buildApismartProfile())
expect(process.env.APISMART_MODEL).toBeUndefined()
expect(process.env.OPENAI_MODEL).toBe('DEEPSEEK_V4_FLASH')
})
test('apismart profile without a base URL retains its dedicated credential for the default route', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
applyProviderProfileToProcessEnv(buildApismartProfile({ baseUrl: undefined }))
expect(process.env.OPENAI_BASE_URL).toBe('https://gw.apismart.ai/v1')
expect(process.env.OPENAI_API_KEY).toBe('apismart-test-key')
expect(process.env.APISMART_API_KEY).toBe('apismart-test-key')
})
test('retargeted ApiSmart profile withholds its dedicated credential', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
applyProviderProfileToProcessEnv(
buildApismartProfile({ baseUrl: 'https://proxy.example/v1' }),
)
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.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
})
test('keyless ApiSmart profile resolves APISMART_API_KEY without persisting it', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.APISMART_API_KEY = 'ambient-apismart-key'
applyProviderProfileToProcessEnv(
buildApismartProfile({
apiKey: undefined,
baseUrl: 'https://gw.apismart.ai/v1',
}),
)
expect(process.env.OPENAI_API_KEY).toBe('ambient-apismart-key')
expect(process.env.APISMART_API_KEY).toBe('ambient-apismart-key')
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
}, 20_000)
test('keyless ApiSmart profile without a base URL resolves the ambient key as canonical', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.APISMART_API_KEY = 'ambient-apismart-key'
applyProviderProfileToProcessEnv(
buildApismartProfile({
apiKey: undefined,
baseUrl: undefined,
}),
)
expect(process.env.OPENAI_BASE_URL).toBe('https://gw.apismart.ai/v1')
expect(process.env.OPENAI_API_KEY).toBe('ambient-apismart-key')
expect(process.env.APISMART_API_KEY).toBe('ambient-apismart-key')
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
}, 20_000)
test('keyless canonical ApiSmart profile never promotes a generic OpenAI key', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.OPENAI_API_KEY = 'generic-openai-key'
applyProviderProfileToProcessEnv(
buildApismartProfile({
apiKey: undefined,
baseUrl: 'https://gw.apismart.ai/v1',
}),
)
expect(process.env.OPENAI_API_KEY).toBeUndefined()
expect(process.env.APISMART_API_KEY).toBeUndefined()
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
}, 20_000)
test('keyless custom ApiSmart profile preserves route identity without forwarding the ambient key', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.APISMART_API_KEY = 'ambient-apismart-key'
process.env.OPENAI_API_KEY = 'ambient-apismart-key'
applyProviderProfileToProcessEnv(
buildApismartProfile({
apiKey: undefined,
baseUrl: 'https://proxy.example.com/v1',
}),
)
expect(process.env.OPENAI_BASE_URL).toBe('https://proxy.example.com/v1')
expect(process.env.OPENAI_API_KEY).toBeUndefined()
expect(process.env.APISMART_API_KEY).toBeUndefined()
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
}, 20_000)
test('non-canonical ApiSmart host path withholds the dedicated credential', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
applyProviderProfileToProcessEnv(
buildApismartProfile({ baseUrl: 'https://gw.apismart.ai/staging/v1' }),
)
expect(process.env.OPENAI_BASE_URL).toBe('https://gw.apismart.ai/staging/v1')
expect(process.env.OPENAI_API_KEY).toBeUndefined()
expect(process.env.APISMART_API_KEY).toBeUndefined()
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
})
test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])(
'addProviderProfile drops placeholder ApiSmart credential %s',
async placeholder => {
const { addProviderProfile, getProviderProfiles } =
await importFreshProviderProfileModules()
saveMockGlobalConfig(current => ({
...current,
providerProfiles: [],
activeProviderProfileId: undefined,
}))
const saved = addProviderProfile({
provider: 'apismart',
name: 'ApiSmart',
baseUrl: 'https://gw.apismart.ai/v1',
model: 'DEEPSEEK_V4_FLASH',
apiKey: placeholder,
})
expect(saved?.apiKey).toBeUndefined()
expect(getProviderProfiles()[0]?.apiKey).toBeUndefined()
},
)
test('cloudflare profile applies OpenAI-compatible env with CLOUDFLARE_API_TOKEN mirror', async () => {
// Account-scoped URL: a real user has substituted `<ACCOUNT_ID>` for their
// Cloudflare account id. The env-build path should mirror the api key into
@@ -3123,6 +3301,59 @@ describe('setActiveProviderProfile', () => {
}
})
test('retargeted ApiSmart profiles keep route identity but persist without their dedicated 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 apismartProfile = buildApismartProfile({
id: 'apismart_proxy',
baseUrl: 'https://proxy.example/v1',
})
saveMockGlobalConfig(current => ({
...current,
providerProfiles: [apismartProfile],
}))
const result = setActiveProviderProfile('apismart_proxy', { configDir })
const persisted = JSON.parse(
readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'),
)
expect(result?.id).toBe('apismart_proxy')
expect(persisted.profile).toBe('openai')
expect(persisted.env).toEqual({
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'apismart',
OPENAI_BASE_URL: 'https://proxy.example/v1',
OPENAI_MODEL: 'DEEPSEEK_V4_FLASH',
})
const { buildStartupEnvFromProfile } = await import(
`./providerProfile.js?ts=${Date.now()}-${Math.random()}`
)
const startupEnv = await buildStartupEnvFromProfile({
persisted,
processEnv: {
APISMART_API_KEY: 'ambient-apismart-key',
OPENAI_API_KEY: 'ambient-apismart-key',
},
})
expect(startupEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart')
expect(startupEnv.APISMART_API_KEY).toBeUndefined()
expect(startupEnv.OPENAI_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-'))
+82 -10
View File
@@ -28,6 +28,7 @@ import {
buildXaiOAuthProfileEnv,
buildXiaomiMimoProfileEnv,
buildAtlasCloudProfileEnv,
buildApismartProfileEnv,
buildVertexProfileEnv,
clearManagedProfileEnv,
deleteProfileFile,
@@ -50,8 +51,10 @@ import {
type ProviderPreset,
} from '../integrations/index.js'
import {
getRouteDefaultBaseUrl,
isCloudflareBaseUrl,
isClinePassBaseUrl,
isCanonicalApismartInferenceBaseUrl,
isFireworksBaseUrl,
isLongcatBaseUrl,
isNearaiBaseUrl,
@@ -64,6 +67,7 @@ import {
sanitizeProfileCustomHeaders,
serializeProfileCustomHeaders,
} from './providerCustomHeaders.js'
import { sanitizeApiKey } from './providerSecrets.js'
import { getSettings_DEPRECATED } from './settings/settings.js'
export type { ProviderPreset } from '../integrations/index.js'
@@ -149,6 +153,14 @@ function isClinePassProfile(profile: ProviderProfile): boolean {
return route.routeId === 'clinepass' || isClinePassBaseUrl(profile.baseUrl)
}
function isApismartProfile(profile: ProviderProfile): boolean {
const baseUrl = profile.baseUrl?.trim()
// Missing base URL resolves to the ApiSmart default, which is canonical.
// Only the documented `/v1` inference URL may carry the dedicated key —
// host-only or path-suffixed ApiSmart URLs are treated as retargeted.
return !baseUrl || isCanonicalApismartInferenceBaseUrl(baseUrl)
}
function deriveGithubEnterpriseUrl(baseUrl: string | undefined): string | undefined {
if (!baseUrl?.trim()) return undefined
try {
@@ -353,7 +365,8 @@ function sanitizeProfile(profile: ProviderProfile): ProviderProfile | null {
provider,
baseUrl,
model,
apiKey: trimOrUndefined(profile.apiKey),
// Drop template/dotenv sentinels so placeholders never persist as keys.
apiKey: sanitizeApiKey(trimOrUndefined(profile.apiKey)),
}
if (supportsApiFormat && apiFormat) {
sanitized.apiFormat = apiFormat
@@ -797,6 +810,10 @@ function isProcessEnvAlignedWithProfile(
? !includeApiKey ||
sameOptionalEnvValue(processEnv.ATLAS_CLOUD_API_KEY, profile.apiKey)
: true) &&
(isApismartProfile(profile)
? !includeApiKey ||
sameOptionalEnvValue(processEnv.APISMART_API_KEY, profile.apiKey)
: true) &&
(isClinePassProfile(profile)
? !includeApiKey ||
sameOptionalEnvValue(processEnv.CLINE_API_KEY, profile.apiKey)
@@ -953,6 +970,8 @@ export function applyProviderProfileToProcessEnv(
const normalizedProfileBaseUrl =
route.routeId === 'xiaomi-mimo' || route.routeId === 'xiaomi-mimo-token'
? normalizeXiaomiMimoBaseUrl(profile.baseUrl) ?? profile.baseUrl
: route.routeId === 'apismart' && !profile.baseUrl?.trim()
? getRouteDefaultBaseUrl('apismart') ?? profile.baseUrl
: profile.baseUrl
const openAIProfileEnv: ProfileEnv = {
OPENAI_BASE_URL: normalizedProfileBaseUrl,
@@ -980,19 +999,21 @@ export function applyProviderProfileToProcessEnv(
}
}
if (profile.apiKey) {
const withholdRetargetedApismartCredential =
route.routeId === 'apismart' && !isApismartProfile(profile)
if (profile.apiKey && !withholdRetargetedApismartCredential) {
openAIProfileEnv.OPENAI_API_KEY = profile.apiKey
if (route.vendorId === 'minimax' || profile.baseUrl.toLowerCase().includes('minimax')) {
if (route.vendorId === 'minimax' || normalizedProfileBaseUrl.toLowerCase().includes('minimax')) {
openAIProfileEnv.MINIMAX_API_KEY = profile.apiKey
}
if (
route.gatewayId === 'nvidia-nim' ||
profile.baseUrl.toLowerCase().includes('nvidia') ||
profile.baseUrl.toLowerCase().includes('integrate.api.nvidia')
normalizedProfileBaseUrl.toLowerCase().includes('nvidia') ||
normalizedProfileBaseUrl.toLowerCase().includes('integrate.api.nvidia')
) {
openAIProfileEnv.NVIDIA_API_KEY = profile.apiKey
}
if (route.routeId === 'bankr' || profile.baseUrl.toLowerCase().includes('bankr')) {
if (route.routeId === 'bankr' || normalizedProfileBaseUrl.toLowerCase().includes('bankr')) {
openAIProfileEnv.BNKR_API_KEY = profile.apiKey
}
if (route.routeId === 'xai' || isXaiBaseUrl(profile.baseUrl)) {
@@ -1001,7 +1022,7 @@ export function applyProviderProfileToProcessEnv(
if (isAimlapiProfile) {
openAIProfileEnv.AIMLAPI_API_KEY = profile.apiKey
}
if (route.routeId === 'venice' || profile.baseUrl.toLowerCase().includes('api.venice.ai')) {
if (route.routeId === 'venice' || normalizedProfileBaseUrl.toLowerCase().includes('api.venice.ai')) {
openAIProfileEnv.VENICE_API_KEY = profile.apiKey
}
if (
@@ -1011,9 +1032,12 @@ export function applyProviderProfileToProcessEnv(
) {
openAIProfileEnv.MIMO_API_KEY = profile.apiKey
}
if (route.routeId === 'atlas-cloud' || profile.baseUrl.toLowerCase().includes('atlascloud')) {
if (route.routeId === 'atlas-cloud' || normalizedProfileBaseUrl.toLowerCase().includes('atlascloud')) {
openAIProfileEnv.ATLAS_CLOUD_API_KEY = profile.apiKey
}
if (isApismartProfile(profile)) {
openAIProfileEnv.APISMART_API_KEY = profile.apiKey
}
if (isClinePassProfile(profile)) {
openAIProfileEnv.CLINE_API_KEY = profile.apiKey
}
@@ -1059,6 +1083,23 @@ export function applyProviderProfileToProcessEnv(
openAIProfileEnv.AIMLAPI_API_KEY ?? ambientAimlapiKey
}
}
// Keep ApiSmart route identity even when the profile is retargeted to a
// proxy. Dedicated credentials stay withheld above; the route id is what
// lets buildLaunchEnv refuse ambient APISMART_API_KEY / mirrored
// OPENAI_API_KEY on relaunch (AIMLAPI parity).
if (route.routeId === 'apismart') {
openAIProfileEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'apismart'
// Keyless canonical ApiSmart profiles resolve ambient dedicated
// credentials the same way AIMLAPI does. Proxy / non-canonical hosts
// must not receive the ambient key.
if (isApismartProfile(profile)) {
const ambientApismartKey = sanitizeApiKey(process.env.APISMART_API_KEY)
openAIProfileEnv.OPENAI_API_KEY =
openAIProfileEnv.OPENAI_API_KEY ?? ambientApismartKey
openAIProfileEnv.APISMART_API_KEY =
openAIProfileEnv.APISMART_API_KEY ?? ambientApismartKey
}
}
if (route.gatewayId === 'nvidia-nim') {
openAIProfileEnv.NVIDIA_NIM = '1'
}
@@ -1340,11 +1381,14 @@ function buildOpenAICompatibleStartupEnv(
if (isCodexBaseUrl(activeProfile.baseUrl)) {
return null
}
const withholdRetargetedApismartCredential =
resolveProfileRoute(activeProfile.provider).routeId === 'apismart' &&
!isApismartProfile(activeProfile)
const isAimlapiProfile =
activeProfile.provider === 'aimlapi' ||
resolveRouteIdFromBaseUrl(activeProfile.baseUrl) === 'aimlapi'
if (activeProfile.apiKey) {
if (activeProfile.apiKey && !withholdRetargetedApismartCredential) {
const strictEnv = buildOpenAIProfileEnv({
goal: 'balanced',
model: activeProfile.model,
@@ -1369,6 +1413,9 @@ function buildOpenAICompatibleStartupEnv(
if (activeProfile.baseUrl?.toLowerCase().includes('atlascloud')) {
strictEnv.ATLAS_CLOUD_API_KEY = activeProfile.apiKey
}
if (isApismartProfile(activeProfile)) {
strictEnv.APISMART_API_KEY = activeProfile.apiKey
}
if (isClinePassProfile(activeProfile)) {
strictEnv.CLINE_API_KEY = activeProfile.apiKey
}
@@ -1415,7 +1462,13 @@ function buildOpenAICompatibleStartupEnv(
if (isAimlapiProfile) {
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'aimlapi'
}
if (activeProfile.apiKey) {
// Preserve ApiSmart identity on retargeted/proxy startup envs so relaunch
// withholding can refuse ambient dedicated credentials. Canonical profiles
// already stamp this via buildApismartProfileEnv.
if (resolveProfileRoute(activeProfile.provider).routeId === 'apismart') {
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'apismart'
}
if (activeProfile.apiKey && !withholdRetargetedApismartCredential) {
env.OPENAI_API_KEY = activeProfile.apiKey
if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) {
env.BNKR_API_KEY = activeProfile.apiKey
@@ -1438,6 +1491,9 @@ function buildOpenAICompatibleStartupEnv(
if (activeProfile.baseUrl?.toLowerCase().includes('atlascloud')) {
env.ATLAS_CLOUD_API_KEY = activeProfile.apiKey
}
if (isApismartProfile(activeProfile)) {
env.APISMART_API_KEY = activeProfile.apiKey
}
if (isClinePassProfile(activeProfile)) {
env.CLINE_API_KEY = activeProfile.apiKey
}
@@ -1627,6 +1683,19 @@ 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
@@ -1663,6 +1732,9 @@ function triggerStartupDiscoveryRefreshForProfile(
if (route.routeId === 'unknown-fallback') {
return
}
if (route.routeId === 'apismart' && !isApismartProfile(profile)) {
return
}
void refreshStartupDiscoveryForRoute(route.routeId, {
baseUrl: profile.baseUrl,
+5 -1
View File
@@ -162,10 +162,14 @@ describe('getKnownProviderSecretEnvKeys', () => {
})
describe('sanitizeApiKey', () => {
test('drops empty and the Portuguese placeholder', () => {
test('drops empty and credential placeholders', () => {
expect(sanitizeApiKey(undefined)).toBeUndefined()
expect(sanitizeApiKey('')).toBeUndefined()
expect(sanitizeApiKey('SUA_CHAVE')).toBeUndefined()
expect(sanitizeApiKey('sua_chave')).toBeUndefined()
expect(sanitizeApiKey('null')).toBeUndefined()
expect(sanitizeApiKey('undefined')).toBeUndefined()
expect(sanitizeApiKey(' NULL ')).toBeUndefined()
})
test('returns real keys unchanged', () => {
+13 -1
View File
@@ -87,7 +87,19 @@ export type SecretValueSource = Partial<Record<string, string | undefined>>
export function sanitizeApiKey(
key: string | null | undefined,
): string | undefined {
if (!key || key === 'SUA_CHAVE') return undefined
if (!key) return undefined
const trimmed = key.trim()
if (!trimmed) return undefined
const normalized = trimmed.toLowerCase()
// Keep profile/env sanitization aligned with credentialPool placeholders so
// template values like SUA_CHAVE / null / undefined never persist as keys.
if (
normalized === 'sua_chave' ||
normalized === 'null' ||
normalized === 'undefined'
) {
return undefined
}
return key
}
+42
View File
@@ -32,6 +32,7 @@ const ENV_KEYS = [
'MISTRAL_API_KEY',
'MINIMAX_API_KEY',
'LONGCAT_API_KEY',
'APISMART_API_KEY',
'NVIDIA_API_KEY',
'NVIDIA_NIM',
'BNKR_API_KEY',
@@ -185,6 +186,47 @@ test('non-OpenAI LongCat path falls back to generic OpenAI validation', async ()
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
})
test('non-canonical ApiSmart host falls back to generic OpenAI validation', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://gw.apismart.ai:8443/v1'
process.env.OPENAI_MODEL = 'custom-model'
process.env.OPENAI_API_KEY = 'generic-key'
await expect(getProviderValidationError(process.env)).resolves.toBeNull()
})
test('noncanonical ApiSmart paths do not validate a dedicated credential', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://gw.apismart.ai/v2'
process.env.OPENAI_MODEL = 'DEEPSEEK_V4_FLASH'
process.env.APISMART_API_KEY = 'apismart-key'
delete process.env.OPENAI_API_KEY
delete process.env.OPENAI_API_KEYS
const message = await getProviderValidationError(process.env)
expect(message).not.toBeNull()
expect(message).not.toContain('ApiSmart auth is required')
expect(message).toContain(
'OPENAI_API_KEYS or OPENAI_API_KEY is required when CLAUDE_CODE_USE_OPENAI=1',
)
})
test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])(
'ApiSmart validation rejects placeholder APISMART_API_KEY %s',
async placeholder => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://gw.apismart.ai/v1'
process.env.OPENAI_MODEL = 'DEEPSEEK_V4_FLASH'
process.env.APISMART_API_KEY = placeholder
delete process.env.OPENAI_API_KEY
delete process.env.OPENAI_API_KEYS
await expect(getProviderValidationError(process.env)).resolves.toBe(
'ApiSmart auth is required. Set APISMART_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'
+13 -8
View File
@@ -15,6 +15,7 @@ import {
getRouteCredentialValue,
getRouteDescriptor,
getRouteDefaultModel,
isCanonicalApismartInferenceBaseUrl,
isCloudflareBaseUrl,
isLongcatBaseUrl,
matchHostnameAgainstRouteHosts,
@@ -134,7 +135,12 @@ function hasUsableCredentialEnvValue(
return false
}
if (envVar === 'OPENAI_API_KEYS' || envVar === 'OPENAI_API_KEY') {
if (
envVar === 'OPENAI_API_KEYS' ||
envVar === 'OPENAI_API_KEY' ||
envVar === 'AIMLAPI_API_KEY' ||
envVar === 'APISMART_API_KEY'
) {
return hasUsableOpenAICredential(value)
}
@@ -271,17 +277,16 @@ function getRuntimeValidationTarget(
return false
}
// The Cloudflare Workers AI route is path-scoped, not just host-scoped:
// `api.cloudflare.com` also serves the REST management API. A host-only
// match on a non-Workers path (e.g. `.../client/v4/user/tokens/verify`)
// would pick the Cloudflare validation target and demand its Workers-AI
// auth instead of falling back to generic OpenAI validation. Mirror the
// runtime route resolver's boundary here.
// Some routes have stricter endpoint boundaries than a host match. Keep
// validation aligned with the runtime resolver so a custom endpoint on a
// shared host is not forced through a dedicated-credential contract.
if (
((target.descriptor.id === 'cloudflare' &&
!isCloudflareBaseUrl(request.baseUrl)) ||
(target.descriptor.id === 'longcat' &&
!isLongcatBaseUrl(request.baseUrl)))
!isLongcatBaseUrl(request.baseUrl)) ||
(target.descriptor.id === 'apismart' &&
!isCanonicalApismartInferenceBaseUrl(request.baseUrl)))
) {
return false
}
+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.' },
+8
View File
@@ -188,6 +188,14 @@ export const providers: Provider[] = [
envVars: ['ATLAS_CLOUD_API_KEY'],
notes: 'OpenAI-compatible hosted open models with reasoning support at https://api.atlascloud.ai/v1.',
},
{
id: 'apismart',
name: 'ApiSmart',
group: 'gateways',
setup: '/provider or env vars',
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: 'cloudflare',
name: 'Cloudflare Workers AI',