fix(ai): do not treat XAI_API_KEY as SuperGrok availability
Paid-key-only setups were marked signed in for xai-oauth, so the shared grok-4.5 default picker preferred SuperGrok over xai/grok-4.5.
This commit is contained in:
@@ -148,6 +148,9 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where Ollama requests without a user-role message would fail to generate output or silently fail with a misleading error.
|
||||
### Fixed
|
||||
|
||||
- Stopped treating `XAI_API_KEY` as SuperGrok (`xai-oauth`) sign-in for availability, so paid-key-only setups default to `xai/grok-4.5` instead of the zero-cost SuperGrok catalog path.
|
||||
|
||||
## [17.2.5] - 2026-08-03
|
||||
|
||||
|
||||
@@ -2687,7 +2687,7 @@ export class AuthStorage {
|
||||
if (this.#runtimeOverrides.has(provider)) return true;
|
||||
if (this.#configOverrides.has(provider)) return true;
|
||||
if (this.#getCredentialsForProvider(provider).length > 0) return true;
|
||||
if (getEnvApiKey(provider)) return true;
|
||||
if (this.#hasDedicatedEnvAuth(provider)) return true;
|
||||
if (this.#fallbackResolver?.(provider)) return true;
|
||||
return false;
|
||||
}
|
||||
@@ -2711,6 +2711,22 @@ export class AuthStorage {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Env auth that belongs to this provider, not a cross-provider alias.
|
||||
*
|
||||
* `getEnvApiKey("xai-oauth")` also accepts `XAI_API_KEY` so an explicit
|
||||
* `xai-oauth/…` stream can still borrow the paid key. Availability and
|
||||
* origin must not: otherwise an API-key-only setup marks SuperGrok as
|
||||
* signed in and `pickDefaultAvailableModel` prefers `xai-oauth/grok-4.5`
|
||||
* over paid `xai/grok-4.5`.
|
||||
*/
|
||||
#hasDedicatedEnvAuth(provider: string): boolean {
|
||||
if (provider === "xai-oauth") {
|
||||
return Boolean($env.XAI_OAUTH_TOKEN?.trim());
|
||||
}
|
||||
return Boolean(getEnvApiKey(provider));
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify where a provider's auth comes from, following the same precedence
|
||||
* as {@link AuthStorage.getApiKey}: runtime override → config override →
|
||||
@@ -2727,7 +2743,7 @@ export class AuthStorage {
|
||||
if (stored.some(credential => credential.type === "api_key" && credential.source === "login")) {
|
||||
return { kind: "api_key" };
|
||||
}
|
||||
if (getEnvApiKey(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
|
||||
if (this.#hasDedicatedEnvAuth(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
|
||||
if (stored.some(credential => credential.type === "api_key")) return { kind: "api_key" };
|
||||
if (this.#fallbackResolver?.(provider)) return { kind: "fallback" };
|
||||
return undefined;
|
||||
|
||||
@@ -29,6 +29,49 @@ describe("xAI API login wiring", () => {
|
||||
expect(getEnvApiKey("xai")).toBe("xai-env-key");
|
||||
});
|
||||
|
||||
test("XAI_API_KEY alone does not mark SuperGrok as available", async () => {
|
||||
const originalOauthToken = Bun.env.XAI_OAUTH_TOKEN;
|
||||
Bun.env.XAI_API_KEY = "xai-env-key";
|
||||
delete Bun.env.XAI_OAUTH_TOKEN;
|
||||
const store = new SqliteAuthCredentialStore(new Database(":memory:"));
|
||||
const storage = new AuthStorage(store);
|
||||
await storage.reload();
|
||||
try {
|
||||
expect(storage.hasAuth("xai")).toBe(true);
|
||||
expect(storage.hasAuth("xai-oauth")).toBe(false);
|
||||
expect(storage.getCredentialOrigin("xai")).toEqual({ kind: "env", envVar: "XAI_API_KEY" });
|
||||
expect(storage.getCredentialOrigin("xai-oauth")).toBeUndefined();
|
||||
} finally {
|
||||
if (originalOauthToken === undefined) {
|
||||
delete Bun.env.XAI_OAUTH_TOKEN;
|
||||
} else {
|
||||
Bun.env.XAI_OAUTH_TOKEN = originalOauthToken;
|
||||
}
|
||||
store.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("XAI_OAUTH_TOKEN marks SuperGrok available without a paid API key", async () => {
|
||||
const originalOauthToken = Bun.env.XAI_OAUTH_TOKEN;
|
||||
delete Bun.env.XAI_API_KEY;
|
||||
Bun.env.XAI_OAUTH_TOKEN = "xai-oauth-env";
|
||||
const store = new SqliteAuthCredentialStore(new Database(":memory:"));
|
||||
const storage = new AuthStorage(store);
|
||||
await storage.reload();
|
||||
try {
|
||||
expect(storage.hasAuth("xai")).toBe(false);
|
||||
expect(storage.hasAuth("xai-oauth")).toBe(true);
|
||||
expect(storage.getCredentialOrigin("xai-oauth")).toEqual({ kind: "env" });
|
||||
} finally {
|
||||
if (originalOauthToken === undefined) {
|
||||
delete Bun.env.XAI_OAUTH_TOKEN;
|
||||
} else {
|
||||
Bun.env.XAI_OAUTH_TOKEN = originalOauthToken;
|
||||
}
|
||||
store.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("AuthStorage.login('xai') validates against /models and stores the pasted key", async () => {
|
||||
const fetchCalls: Array<{ url: string; init: RequestInit | undefined }> = [];
|
||||
const fetchMock: FetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
||||
|
||||
@@ -371,6 +371,7 @@
|
||||
- Changed the default model for SuperGrok OAuth (`xai-oauth`) from `grok-4.3` to `grok-4.5`.
|
||||
- Included `reasoning.encrypted_content` in Responses `include` for paid xAI and SuperGrok OAuth models.
|
||||
- Replayed encrypted xAI reasoning on follow-up Responses turns for `xai` and `xai-oauth`.
|
||||
- Kept automatic model selection on paid `xai/grok-4.5` when only `XAI_API_KEY` is set, instead of preferring SuperGrok `xai-oauth/grok-4.5`.
|
||||
|
||||
## [17.2.5] - 2026-08-03
|
||||
|
||||
|
||||
@@ -437,6 +437,36 @@ describe("pickDefaultAvailableModel", () => {
|
||||
expect(result?.provider).toBe("zhipu-coding-plan");
|
||||
expect(result?.id).toBe("glm-5.1");
|
||||
});
|
||||
|
||||
test("prefers SuperGrok over paid xAI when both defaults are present", () => {
|
||||
const paid = buildModel({
|
||||
id: "grok-4.5",
|
||||
name: "Grok 4.5",
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
|
||||
contextWindow: 500000,
|
||||
maxTokens: 500000,
|
||||
});
|
||||
const oauth = buildModel({
|
||||
id: "grok-4.5",
|
||||
name: "Grok 4.5",
|
||||
api: "openai-responses",
|
||||
provider: "xai-oauth",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 500000,
|
||||
maxTokens: 500000,
|
||||
});
|
||||
|
||||
expect(pickDefaultAvailableModel([paid, oauth])?.provider).toBe("xai-oauth");
|
||||
expect(pickDefaultAvailableModel([paid])?.provider).toBe("xai");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseModelPattern", () => {
|
||||
|
||||
Reference in New Issue
Block a user