fix(xai): stop restoring unidentifiable persisted proxy keys

Retargeted xAI launch treated any persisted OPENAI_API_KEY as
proxy-owned when XAI_API_KEY was already gone. Keep live shell
keys, stamp --provider xai route identity, and read durable OAuth
credentials for runtime-limit cache lookups.
This commit is contained in:
jatmn
2026-08-16 15:44:27 -07:00
parent 046c7ecacf
commit 17d76df576
8 changed files with 104 additions and 8 deletions
+3 -3
View File
@@ -126,7 +126,7 @@ describe('resolveModelRuntimeLimits', () => {
it('uses the stable xAI OAuth cache identity for discovered runtime limits', async () => {
await withTempConfigDir(async () => {
const xaiCredentials = await import('../utils/xaiCredentials.js')
const readSpy = spyOn(xaiCredentials, 'getCachedXaiCredentials').mockReturnValue({
const readSpy = spyOn(xaiCredentials, 'readXaiCredentials').mockReturnValue({
accessToken: 'rotating-access-token',
refreshToken: 'stable-account-identity',
tokenEndpoint: 'https://auth.x.ai/oauth/token',
@@ -297,13 +297,13 @@ describe('resolveModelRuntimeLimits', () => {
const originalXaiKey = process.env.XAI_API_KEY
const originalOpenAIKey = process.env.OPENAI_API_KEY
const xaiCredentials = await import('../utils/xaiCredentials.js')
const readSpy = spyOn(xaiCredentials, 'getCachedXaiCredentials').mockImplementation(
const readSpy = spyOn(xaiCredentials, 'readXaiCredentials').mockImplementation(
() =>
({
accessToken: 'oauth-token',
refreshToken: 'refresh-token',
tokenEndpoint: 'https://auth.x.ai/oauth/token',
}) as ReturnType<typeof xaiCredentials.getCachedXaiCredentials>,
}) as ReturnType<typeof xaiCredentials.readXaiCredentials>,
)
try {
delete process.env.XAI_API_KEY
+5 -3
View File
@@ -28,8 +28,8 @@ import {
import { parseCustomHeadersEnv } from '../utils/providerCustomHeaders.js'
import { firstUsableCredential } from '../services/api/credentialPool.js'
import {
getCachedXaiCredentials,
getXaiDiscoveryCacheIdentity,
readXaiCredentials,
} from '../utils/xaiCredentials.js'
import { ZAI_GLM_OPENAI_SHIM } from './transport/zaiGlmShim.js'
import { resolveAimlapiAttributionHeaders } from './aimlapi/config.js'
@@ -467,13 +467,15 @@ function findCachedCatalogEntryForApiName(
}),
)
// Mirror picker/discovery OAuth injection so hybrid cache partitions match
// for xAI sessions that have a stored token but no env API key.
// for xAI sessions that have a stored token but no env API key. Use the
// synchronous storage reader (not the 30s memory-only getter) so a cold
// request-planning path still finds the OAuth-partitioned discovery entry.
if (
!apiKey &&
routeId === 'xai' &&
isCanonicalXaiInferenceBaseUrl(baseUrl)
) {
const credentials = getCachedXaiCredentials()
const credentials = readXaiCredentials()
apiKey = firstUsableCredential(credentials?.accessToken)
const cacheIdentity = getXaiDiscoveryCacheIdentity(credentials) ?? apiKey
const cacheKey = getDiscoveryCacheKey(routeId, {
+26
View File
@@ -53,6 +53,7 @@ const originalEnv = {
GEMINI_AUTH_MODE: process.env.GEMINI_AUTH_MODE,
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
OPENAI_API_KEYS: process.env.OPENAI_API_KEYS,
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
OPENAI_API_BASE: process.env.OPENAI_API_BASE,
OPENAI_API_FORMAT: process.env.OPENAI_API_FORMAT,
@@ -109,6 +110,7 @@ function clearEnvForMiniMaxOnlyTest(): void {
delete process.env.GEMINI_AUTH_MODE
delete process.env.GOOGLE_API_KEY
delete process.env.OPENAI_API_KEY
delete process.env.OPENAI_API_KEYS
delete process.env.OPENAI_BASE_URL
delete process.env.OPENAI_API_BASE
delete process.env.OPENAI_MODEL
@@ -152,6 +154,7 @@ beforeEach(async () => {
delete process.env.CLAUDE_CODE_USE_MISTRAL
delete process.env.GOOGLE_API_KEY
delete process.env.OPENAI_API_KEY
delete process.env.OPENAI_API_KEYS
delete process.env.OPENAI_BASE_URL
delete process.env.OPENAI_API_BASE
delete process.env.OPENAI_API_FORMAT
@@ -199,6 +202,7 @@ afterEach(() => {
restoreEnv('GEMINI_AUTH_MODE', originalEnv.GEMINI_AUTH_MODE)
restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY)
restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY)
restoreEnv('OPENAI_API_KEYS', originalEnv.OPENAI_API_KEYS)
restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL)
restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE)
restoreEnv('OPENAI_API_FORMAT', originalEnv.OPENAI_API_FORMAT)
@@ -253,6 +257,7 @@ test('first-party Anthropic requests execute the configured fetch wrapper withou
delete process.env.CLAUDE_CODE_USE_GITHUB
delete process.env.CLAUDE_CODE_USE_MISTRAL
delete process.env.OPENAI_API_KEY
delete process.env.OPENAI_API_KEYS
delete process.env.OPENAI_BASE_URL
delete process.env.OPENAI_API_BASE
delete process.env.OPENAI_MODEL
@@ -1018,6 +1023,27 @@ test('env-only xAI fallback does not copy XAI_API_KEY onto an insecure xAI URL',
expect(process.env.XAI_API_KEY).toBe('xai-test-key')
})
test('env-only xAI fallback filters mirrored keys out of OPENAI_API_KEYS on an insecure xAI 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.XAI_API_KEY = 'xai-test-key'
process.env.OPENAI_API_KEY = 'xai-test-key'
process.env.OPENAI_API_KEYS = 'xai-test-key,pooled-other-key'
process.env.OPENAI_BASE_URL = 'http://api.x.ai/v1'
await getAnthropicClient({
maxRetries: 0,
model: 'grok-4',
})
expect(process.env.OPENAI_API_KEY).toBeUndefined()
expect(process.env.OPENAI_API_KEYS).toBe('pooled-other-key')
expect(process.env.XAI_API_KEY).toBe('xai-test-key')
})
test('env-only xAI fallback copies XAI_API_KEY on the canonical HTTPS URL', async () => {
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.GEMINI_API_KEY
+10 -1
View File
@@ -57,7 +57,7 @@ import {
shouldUseFirstPartyAnthropicAuthForProvider,
type ProviderOverride,
} from './authRouting.js'
import { hasUsableOpenAICredential } from './credentialPool.js'
import { hasUsableOpenAICredential, parseCredentialList } from './credentialPool.js'
import { AnthropicVertex } from './vertexClient.js'
import { importOptionalRuntimeModule } from '../../utils/optionalRuntimeModule.js'
@@ -261,6 +261,15 @@ function applyXaiEnvOnlyDefaults(): void {
process.env.OPENAI_API_KEY = process.env.XAI_API_KEY
} else {
delete process.env.OPENAI_API_KEY
const xaiSecrets = new Set(parseCredentialList(process.env.XAI_API_KEY))
const pooled = parseCredentialList(process.env.OPENAI_API_KEYS).filter(
value => !xaiSecrets.has(value),
)
if (pooled.length > 0) {
process.env.OPENAI_API_KEYS = pooled.join(',')
} else {
delete process.env.OPENAI_API_KEYS
}
}
delete process.env.OPENAI_API_FORMAT
delete process.env.OPENAI_AZURE_STYLE
+11
View File
@@ -26,6 +26,7 @@ const ENV_KEYS = [
'OPENAI_API_BASE',
'OPENAI_API_KEY',
'OPENAI_API_KEYS',
'CLAUDE_CODE_PROVIDER_ROUTE_ID',
'OPENAI_MODEL',
'OPENAI_API_FORMAT',
'OPENAI_AZURE_STYLE',
@@ -78,6 +79,7 @@ const RESET_KEYS = [
'OPENAI_API_BASE',
'OPENAI_API_KEY',
'OPENAI_API_KEYS',
'CLAUDE_CODE_PROVIDER_ROUTE_ID',
'OPENAI_MODEL',
'OPENAI_API_FORMAT',
'OPENAI_AZURE_STYLE',
@@ -1154,10 +1156,19 @@ describe('applyProviderFlag - xai', () => {
applyProviderFlag('xai', [])
expect(process.env.OPENAI_BASE_URL).toBe('https://proxy.example/v1')
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('xai')
expect(process.env.OPENAI_API_KEY).toBeUndefined()
expect(process.env.XAI_API_KEY).toBe('xai-secret-key')
})
test('stamps xAI route identity on a retargeted proxy URL', () => {
process.env.OPENAI_BASE_URL = 'https://proxy.example/v1'
applyProviderFlag('xai', [])
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('xai')
})
test('strips a mirrored XAI_API_KEY from OPENAI_API_KEY on a retargeted proxy URL', () => {
process.env.OPENAI_BASE_URL = 'https://proxy.example/v1'
process.env.XAI_API_KEY = 'xai-secret-key'
+4
View File
@@ -573,6 +573,10 @@ export function applyProviderFlag(
process.env.OPENAI_BASE_URL ??= 'https://api.x.ai/v1'
process.env.OPENAI_MODEL ??= defaultModel ?? 'grok-4.6'
if (model) process.env.OPENAI_MODEL = model
// Same stamp as profile apply: a proxy URL cannot host-match api.x.ai,
// so retained route identity is what keeps catalog/runtime on xAI
// without authorizing dedicated credentials for that host.
process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'xai'
// Keep a user proxy URL (`??=` above), but never turn the dedicated
// xAI secret into a generic credential for that host. A mirrored copy
// that already equals XAI_API_KEY is stripped; a distinct OPENAI_API_KEY
+37
View File
@@ -1698,6 +1698,24 @@ test('openai launch drops legacy persisted xAI keys from a retargeted proxy prof
assert.equal(env.XAI_API_KEY, undefined)
})
test('openai launch drops a legacy persisted OPENAI_API_KEY from a retargeted xAI proxy without XAI_API_KEY', async () => {
const env = await buildLaunchEnv({
profile: 'openai',
persisted: profile('openai', {
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'xai',
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
OPENAI_MODEL: 'grok-4.6',
OPENAI_API_KEY: 'legacy-xai-key',
}),
goal: 'coding',
processEnv: {},
})
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'xai')
assert.equal(env.OPENAI_API_KEY, undefined)
assert.equal(env.XAI_API_KEY, undefined)
})
test('legacy xai launch withholds ambient custom headers from a retargeted proxy URL', async () => {
const env = await buildLaunchEnv({
profile: 'xai',
@@ -1739,6 +1757,25 @@ test('legacy xai launch withholds dedicated credentials from a retargeted proxy
assert.equal(env.OPENAI_API_KEY, undefined)
})
test('xai launch drops a legacy persisted OPENAI_API_KEY from a retargeted proxy without XAI_API_KEY', async () => {
const env = await buildLaunchEnv({
profile: 'xai',
persisted: profile('xai', {
OPENAI_BASE_URL: 'https://proxy.example.com/v1',
OPENAI_MODEL: 'grok-4.6',
XAI_CREDENTIAL_SOURCE: 'oauth',
OPENAI_API_KEY: 'legacy-xai-key',
}),
goal: 'balanced',
processEnv: {},
})
assert.equal(env.OPENAI_BASE_URL, 'https://proxy.example.com/v1')
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'xai')
assert.equal(env.OPENAI_API_KEY, undefined)
assert.equal(env.XAI_API_KEY, undefined)
})
test('startup env withholds ambient xAI keys from a persisted xai OAuth proxy profile', async () => {
const env = await buildStartupEnvFromProfile({
persisted: profile('xai', {
+8 -1
View File
@@ -1533,8 +1533,15 @@ export function assignDistinctXaiProxyGenericCredential(
env[selection.envVar] = filtered.join(',')
return true
}
// Live shell keys can be a distinct proxy credential even when XAI_API_KEY
// is already gone. Persisted OPENAI_API_KEY is not: older xAI proxy files
// stored the dedicated secret there, and with an empty xaiSecrets set every
// persisted value looks "distinct". Only restore persisted generics when we
// can subtract known xAI secrets.
if (!restoreGeneric(resolveOpenAICredentialEnvSelection(processEnv))) {
restoreGeneric(resolveOpenAICredentialEnvSelection(persistedEnv))
if (xaiSecrets.size > 0) {
restoreGeneric(resolveOpenAICredentialEnvSelection(persistedEnv))
}
}
}