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.
This commit is contained in:
jatmn
2026-08-10 16:20:52 -07:00
parent 37df782f52
commit fdcf668524
6 changed files with 125 additions and 23 deletions
+9
View File
@@ -601,6 +601,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({
+5 -1
View File
@@ -819,10 +819,14 @@ export function hasClinePassEnvOnlyProviderIntent(
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) &&
hasNoExplicitNonOpenAICompatibleProvider(processEnv)
hasNoExplicitNonOpenAIProvider(processEnv)
)
}
+39
View File
@@ -289,6 +289,7 @@ test('buildApismartProfileEnv prefers APISMART_MODEL over OPENAI_MODEL', () => {
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', () => {
@@ -300,6 +301,44 @@ test('buildApismartProfileEnv refuses to copy the dedicated credential to a cust
assert.equal(env, 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',
+40 -21
View File
@@ -679,6 +679,7 @@ export function buildApismartProfileEnv(options: {
defaultModel,
OPENAI_API_KEY: key,
APISMART_API_KEY: key,
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'apismart',
}
}
@@ -1396,12 +1397,17 @@ function hasConcreteProviderSelection(
return true
}
// Env-only provider setups — no CLAUDE_CODE_USE_* flag needed
// Env-only provider setups — no CLAUDE_CODE_USE_* flag needed.
// ApiSmart deliberately follows AIMLAPI here: a bare APISMART_API_KEY is NOT
// a "concrete selection" that skips the persisted-profile launch path.
// Env-only ApiSmart routing is applied later via resolveEnvOnlyProviderRouteId
// + applyApismartEnvOnlyDefaults. Treating the dedicated key as concrete would
// short-circuit buildStartupEnvFromProfile and let an ambient mirrored
// OPENAI_API_KEY reach a keyless ApiSmart proxy profile on relaunch.
return (
sanitizeApiKey(processEnv.FIREWORKS_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.NEARAI_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined ||
sanitizeApiKey(processEnv.APISMART_API_KEY) !== undefined
sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined
)
}
@@ -2052,12 +2058,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
@@ -2066,7 +2072,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() &&
!isApismartBaseUrl(env.OPENAI_BASE_URL)
const isNoncanonicalDedicatedOpenAILaunch =
isNoncanonicalAimlapiLaunch || isNoncanonicalApismartLaunch
if (isNoncanonicalDedicatedOpenAILaunch) {
delete env.OPENAI_API_KEY
delete env.OPENAI_API_KEYS
const persistedCredential = resolveOpenAICredentialEnvSelection(persistedEnv)
@@ -2126,16 +2138,23 @@ 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 &&
!isApismartBaseUrl(dedicatedBaseUrl)
const withholdAmbientDedicatedKey =
withholdAmbientAimlapiKey || withholdAmbientApismartKey
const dedicatedValue = withholdAmbientDedicatedKey
? sanitizeApiKey(persistedEnv[dedicatedKey])
: (dedicatedKey === 'AIMLAPI_API_KEY' && openAICredential?.kind === 'usable'
? sanitizeApiKey(openAICredential.value)
@@ -2157,9 +2176,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) {
+19 -1
View File
@@ -827,6 +827,7 @@ describe('applyProviderProfileToProcessEnv', () => {
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')
})
@@ -863,6 +864,7 @@ describe('applyProviderProfileToProcessEnv', () => {
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.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])(
@@ -3214,7 +3216,7 @@ describe('setActiveProviderProfile', () => {
}
})
test('retargeted ApiSmart profiles persist without their dedicated credential', async () => {
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)
@@ -3241,9 +3243,25 @@ describe('setActiveProviderProfile', () => {
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 })
+13
View File
@@ -1080,6 +1080,13 @@ 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'
}
if (route.gatewayId === 'nvidia-nim') {
openAIProfileEnv.NVIDIA_NIM = '1'
}
@@ -1442,6 +1449,12 @@ function buildOpenAICompatibleStartupEnv(
if (isAimlapiProfile) {
env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'aimlapi'
}
// 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')) {