fix(ai,coding-agent): gate xai-oauth on dedicated credential source

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
This commit is contained in:
metaphorics
2026-05-27 15:25:09 +00:00
parent 5be5053696
commit 6ef2a4f3f1
2 changed files with 46 additions and 8 deletions
+19
View File
@@ -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.
*/
+27 -8
View File
@@ -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<XAICredentials | null> {
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 };
}