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.
This commit is contained in:
jatmn
2026-08-10 17:31:53 -07:00
parent fdcf668524
commit bf11b2a8cf
7 changed files with 218 additions and 12 deletions
+25
View File
@@ -7,6 +7,7 @@ import {
getRouteDefaultModel,
getRouteProviderTypeLabel,
isApismartBaseUrl,
isCanonicalApismartInferenceBaseUrl,
isCloudflareBaseUrl,
isLongcatBaseUrl,
resolveActiveRouteIdFromEnv,
@@ -335,6 +336,30 @@ test('isApismartBaseUrl requires the documented HTTPS endpoint', () => {
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')).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({
+39
View File
@@ -403,6 +403,13 @@ 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) {
@@ -421,6 +428,38 @@ export function isApismartBaseUrl(value: string | undefined): boolean {
}
}
/**
* 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.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
+4 -3
View File
@@ -29,7 +29,7 @@ import {
resolveRouteIdFromBaseUrl,
} from '../integrations/index.js'
import { PRESET_VENDOR_MAP } from '../integrations/compatibility.js'
import { isApismartBaseUrl } from '../integrations/routeMetadata.js'
import { isCanonicalApismartInferenceBaseUrl } from '../integrations/routeMetadata.js'
import { hasUsableOpenAICredential } from '../services/api/credentialPool.js'
import { isFirstPartyAnthropicBaseUrlForEnv } from './anthropicBaseUrl.js'
@@ -648,10 +648,11 @@ export function applyProviderFlag(
// 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.
// 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) &&
isApismartBaseUrl(getConfiguredOpenAIBaseUrl())
isCanonicalApismartInferenceBaseUrl(getConfiguredOpenAIBaseUrl())
) {
process.env.OPENAI_API_KEY = process.env.APISMART_API_KEY
} else {
+45
View File
@@ -278,6 +278,34 @@ test('openai launch preserves persisted ApiSmart dedicated credentials across re
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'apismart')
})
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('buildApismartProfileEnv prefers APISMART_MODEL over OPENAI_MODEL', () => {
const env = buildApismartProfileEnv({
apiKey: 'apismart-secret-key',
@@ -301,6 +329,23 @@ test('buildApismartProfileEnv refuses to copy the dedicated credential to a cust
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',
+20 -7
View File
@@ -25,7 +25,7 @@ import { getErrnoCode } from './errors.js'
import {
getRouteDefaultBaseUrl,
getRouteDefaultModel,
isApismartBaseUrl,
isCanonicalApismartInferenceBaseUrl,
isLongcatBaseUrl,
normalizeXiaomiMimoBaseUrl,
resolveRouteCredentialValue,
@@ -660,7 +660,13 @@ export function buildApismartProfileEnv(options: {
const configuredBaseUrl =
sanitizeProviderConfigValue(options.baseUrl, secretSource) ||
sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL, secretSource)
if (configuredBaseUrl && !isApismartBaseUrl(configuredBaseUrl)) {
// 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
}
@@ -2075,7 +2081,7 @@ export async function buildLaunchEnv(options: {
const isNoncanonicalApismartLaunch =
effectiveOpenAIRouteId === 'apismart' &&
!!env.OPENAI_BASE_URL?.trim() &&
!isApismartBaseUrl(env.OPENAI_BASE_URL)
!isCanonicalApismartInferenceBaseUrl(env.OPENAI_BASE_URL)
const isNoncanonicalDedicatedOpenAILaunch =
isNoncanonicalAimlapiLaunch || isNoncanonicalApismartLaunch
if (isNoncanonicalDedicatedOpenAILaunch) {
@@ -2151,14 +2157,21 @@ export async function buildLaunchEnv(options: {
const withholdAmbientApismartKey =
dedicatedKey === 'APISMART_API_KEY' &&
!!dedicatedBaseUrl &&
!isApismartBaseUrl(dedicatedBaseUrl)
!isCanonicalApismartInferenceBaseUrl(dedicatedBaseUrl)
const withholdAmbientDedicatedKey =
withholdAmbientAimlapiKey || withholdAmbientApismartKey
// ApiSmart is dedicatedCredentialsOnly: relaunch must recover
// APISMART_API_KEY from a usable mirrored OPENAI_API_KEY the same way
// AIMLAPI recovers AIMLAPI_API_KEY, or the shim authenticates with nothing.
const backfillDedicatedFromOpenAI =
(dedicatedKey === 'AIMLAPI_API_KEY' ||
dedicatedKey === 'APISMART_API_KEY') &&
openAICredential?.kind === 'usable'
? sanitizeApiKey(openAICredential.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])
if (dedicatedValue) {
+68
View File
@@ -867,6 +867,74 @@ describe('applyProviderProfileToProcessEnv', () => {
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 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 => {
+17 -2
View File
@@ -54,7 +54,7 @@ import {
getRouteDefaultBaseUrl,
isCloudflareBaseUrl,
isClinePassBaseUrl,
isApismartBaseUrl,
isCanonicalApismartInferenceBaseUrl,
isFireworksBaseUrl,
isLongcatBaseUrl,
isNearaiBaseUrl,
@@ -155,7 +155,10 @@ function isClinePassProfile(profile: ProviderProfile): boolean {
function isApismartProfile(profile: ProviderProfile): boolean {
const baseUrl = profile.baseUrl?.trim()
return !baseUrl || isApismartBaseUrl(baseUrl)
// 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 {
@@ -1086,6 +1089,18 @@ export function applyProviderProfileToProcessEnv(
// 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 =
trimOrUndefined(process.env.APISMART_API_KEY) ??
trimOrUndefined(process.env.OPENAI_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'