mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -1533,10 +1533,17 @@ 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))) {
|
||||
if (xaiSecrets.size > 0) {
|
||||
restoreGeneric(resolveOpenAICredentialEnvSelection(persistedEnv))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildLaunchEnv(options: {
|
||||
profile: ProviderProfile
|
||||
|
||||
Reference in New Issue
Block a user