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