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 }; }