From 6ef2a4f3f1002eff20a892ed19915317eb78eb2e Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Wed, 27 May 2026 15:25:09 +0000 Subject: [PATCH] fix(ai,coding-agent): gate xai-oauth on dedicated credential source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cross-provider env fallback (stream.ts: "xai-oauth" → XAI_OAUTH_TOKEN || XAI_API_KEY) lets an XAI_API_KEY-only setup silently satisfy the xai-oauth credential branch in resolveXAIHttpCredentials. Once the helper enters that branch it resolves baseURL under xai-oauth instead of xai, bypassing providers.xai.baseUrl overrides for image/TTS traffic. Add AuthStorage.hasNonEnvCredential — hasAuth minus the env-fallback leg — and gate the xai-oauth branch on (dedicated credential source || $env.XAI_OAUTH_TOKEN). The XAI_API_KEY borrow now falls through to the xai branch, preserving back-compat while restoring provider-level baseUrl precedence for users with a dedicated xai-oauth source. Op: correct Restores: ref:feat/xai-grok-oauth@015437534 --- packages/ai/src/auth-storage.ts | 19 ++++++++++++ packages/coding-agent/src/lib/xai-http.ts | 35 +++++++++++++++++------ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index 14523369e..e80306258 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -1280,6 +1280,25 @@ export class AuthStorage { return false; } + /** + * True iff a dedicated, non-env credential source is configured for this + * provider — i.e. anything in the cascade EXCEPT `getEnvApiKey(provider)`. + * + * Mirrors `hasAuth` minus the env-fallback leg. Useful for callers that + * need to distinguish "the user explicitly configured this provider" + * from "an env var happens to alias this provider via the cross-provider + * fallback map" (see e.g. `xai-oauth → XAI_OAUTH_TOKEN || XAI_API_KEY` in + * `stream.ts`). Without that distinction, an `XAI_API_KEY`-only setup + * silently satisfies xai-oauth and routes around `providers.xai.baseUrl`. + */ + hasNonEnvCredential(provider: string): boolean { + if (this.#runtimeOverrides.has(provider)) return true; + if (this.#configOverrides.has(provider)) return true; + if (this.#getCredentialsForProvider(provider).length > 0) return true; + if (this.#fallbackResolver?.(provider)) return true; + return false; + } + /** * Check if OAuth credentials are configured for a provider. */ diff --git a/packages/coding-agent/src/lib/xai-http.ts b/packages/coding-agent/src/lib/xai-http.ts index 97cac4c96..8786be8cc 100644 --- a/packages/coding-agent/src/lib/xai-http.ts +++ b/packages/coding-agent/src/lib/xai-http.ts @@ -18,10 +18,25 @@ export function ohMyPiXAIUserAgent(): string { /** * Resolve xAI credentials for HTTP tool calls. * - * Priority: - * 1. xai-oauth (SuperGrok subscription token via AuthStorage; refresh - * cascade runs inside ModelRegistry.getApiKeyForProvider). - * 2. XAI_API_KEY environment variable (legacy/headless). + * Credential priority: + * 1. xai-oauth — only when a *dedicated* xai-oauth source exists. Composed + * of two checks against the registry layer: + * a. `authStorage.hasNonEnvCredential("xai-oauth")` covers stored + * credentials (OAuth or api_key), runtime overrides (CLI + * `--api-key` for xai-oauth), config overrides (models.yml + * `providers.xai-oauth.apiKey`), and fallback resolvers. + * b. `$env.XAI_OAUTH_TOKEN` covers the xai-oauth-specific env var. + * `XAI_API_KEY` is intentionally NOT a signal here, even though the + * env-fallback map (`stream.ts: "xai-oauth"`) lets xai-oauth borrow it + * as a back-compat convenience: the borrow lets API-key-only setups + * satisfy the xai-oauth branch and then resolve baseUrl under + * xai-oauth instead of xai, silently bypassing `providers.xai.baseUrl` + * overrides for image/TTS traffic. The gate routes the borrow case to + * step 2 while preserving every dedicated xai-oauth path. + * 2. xai (plain API key). Delegates to ModelRegistry.getApiKeyForProvider + * which runs AuthStorage.getApiKey's full cascade: runtime override → + * models.yml config override → stored api_key credential → OAuth + * resolution → XAI_API_KEY env var → custom fallback resolver. * * Returns null when neither credential is available. Caller is responsible * for surfacing an actionable error message in that case. @@ -32,12 +47,16 @@ export function ohMyPiXAIUserAgent(): string { export async function resolveXAIHttpCredentials(modelRegistry: ModelRegistry): Promise { const baseURL = ($env.XAI_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, ""); - const oauthKey = await modelRegistry.getApiKeyForProvider("xai-oauth"); - if (oauthKey) { - return { provider: "xai-oauth", apiKey: oauthKey, baseURL }; + const hasDedicatedXaiOAuth = + modelRegistry.authStorage.hasNonEnvCredential("xai-oauth") || Boolean($env.XAI_OAUTH_TOKEN); + if (hasDedicatedXaiOAuth) { + const oauthKey = await modelRegistry.getApiKeyForProvider("xai-oauth"); + if (oauthKey) { + return { provider: "xai-oauth", apiKey: oauthKey, baseURL }; + } } - const apiKey = $env.XAI_API_KEY; + const apiKey = await modelRegistry.getApiKeyForProvider("xai"); if (apiKey) { return { provider: "xai", apiKey, baseURL }; }