mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
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:
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
Reference in New Issue
Block a user