diff --git a/docs/provider-endpoint-constraints.md b/docs/provider-endpoint-constraints.md
index 0193702a2..44dbabe05 100644
--- a/docs/provider-endpoint-constraints.md
+++ b/docs/provider-endpoint-constraints.md
@@ -235,13 +235,16 @@ Reasoning fields are not interchangeable.
- Compat needs both policies: disable reasoning for any tool choice, and disable
reasoning only for forced tool choice.
-### xAI Grok through Responses/SuperGrok
+### xAI Grok through Responses (`xai` and `xai-oauth`)
-Keep these independent:
+Both the paid API-key provider (`xai` / `XAI_API_KEY`) and SuperGrok OAuth
+(`xai-oauth`) chat over `https://api.x.ai/v1/responses`. Keep these independent:
-- omit `reasoning.effort`
-- include or drop encrypted reasoning replay
-- filter reasoning-history wrappers
+- omit `reasoning.effort` unless the model is on the Grok effort-capable allowlist
+- omit `reasoning.summary` (the host rejects it; do not fall back to `"auto"`)
+- omit presence/frequency penalties (`/v1/responses` rejects them for every Grok model)
+- include `reasoning.encrypted_content` on the request
+- replay encrypted reasoning items on later turns
Some models reject only one of those fields; do not collapse them into one
"Grok mode" branch.
diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md
index 08c36e86c..feedcbda3 100644
--- a/packages/ai/CHANGELOG.md
+++ b/packages/ai/CHANGELOG.md
@@ -2,6 +2,12 @@
## [Unreleased]
+### 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. Explicit `xai-oauth/…` selectors still accept the paid key via the existing env fallback.
+- Omitted unsupported `reasoning.summary` on paid xAI Responses requests (`xai/grok-4.5`), matching SuperGrok, so a thinking level no longer serializes `summary: "auto"`.
+- Omitted presence/frequency penalties on all first-party xAI Responses models, including non-reasoning ids such as `xai/grok-2`.
+
## [17.3.4] - 2026-08-14
### Fixed
diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts
index 625f6b6d4..4748f28b1 100644
--- a/packages/ai/src/auth-storage.ts
+++ b/packages/ai/src/auth-storage.ts
@@ -2680,18 +2680,32 @@ export class AuthStorage {
}
/**
- * Check if any form of auth is configured for a provider.
- * Unlike getApiKey(), this doesn't refresh OAuth tokens.
+ * Dedicated auth for default-model availability (picker / `getAvailable`).
+ * Unlike {@link getApiKey}, this does not refresh OAuth tokens, and unlike
+ * {@link hasResolvableAuth} it ignores cross-provider env aliases so
+ * `XAI_API_KEY` does not auto-select SuperGrok (`xai-oauth`).
*/
hasAuth(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 (getEnvApiKey(provider)) return true;
+ if (this.#hasDedicatedEnvAuth(provider)) return true;
if (this.#fallbackResolver?.(provider)) return true;
return false;
}
+ /**
+ * Whether a request could resolve a key for this provider, including
+ * cross-provider env aliases (`xai-oauth` borrowing `XAI_API_KEY`).
+ * Use this for explicit model preflight (`xai-oauth/grok-4.5`); use
+ * {@link hasAuth} for auto-availability so the default picker stays on
+ * paid `xai` when only `XAI_API_KEY` is set.
+ */
+ hasResolvableAuth(provider: string): boolean {
+ if (this.hasAuth(provider)) return true;
+ return Boolean(getEnvApiKey(provider));
+ }
+
/**
* True iff a dedicated, non-env credential source is configured for this
* provider — i.e. anything in the cascade EXCEPT `getEnvApiKey(provider)`.
@@ -2711,6 +2725,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 +2757,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;
diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts
index d60c1004c..2b262b130 100644
--- a/packages/ai/src/providers/openai-completions.ts
+++ b/packages/ai/src/providers/openai-completions.ts
@@ -1575,17 +1575,19 @@ function buildParams(
if (options?.minP !== undefined) {
params.min_p = options.minP;
}
- if (options?.presencePenalty !== undefined) {
- params.presence_penalty = options.presencePenalty;
- }
- if (options?.repetitionPenalty !== undefined) {
- params.repetition_penalty = options.repetitionPenalty;
- }
- if (options?.frequencyPenalty !== undefined) {
- params.frequency_penalty = options.frequencyPenalty;
+ if (initialCompat.supportsPenaltyAndStopParams) {
+ if (options?.presencePenalty !== undefined) {
+ params.presence_penalty = options.presencePenalty;
+ }
+ if (options?.repetitionPenalty !== undefined) {
+ params.repetition_penalty = options.repetitionPenalty;
+ }
+ if (options?.frequencyPenalty !== undefined) {
+ params.frequency_penalty = options.frequencyPenalty;
+ }
}
}
- if (options?.stopSequences?.length) {
+ if (options?.stopSequences?.length && initialCompat.supportsPenaltyAndStopParams) {
const seqs = options.stopSequences;
params.stop = seqs.length === 1 ? seqs[0] : seqs.slice(0, 4);
}
diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts
index 21b587e7e..61055b2ab 100644
--- a/packages/ai/src/providers/openai-responses.ts
+++ b/packages/ai/src/providers/openai-responses.ts
@@ -1285,12 +1285,11 @@ export function buildParams(
filterReasoningHistory: options?.filterReasoningHistory,
omitReasoningEffort: options?.omitReasoningEffort,
});
- const reasoningSummary =
- model.provider === "xai-oauth"
- ? options?.reasoning === undefined
- ? undefined
- : null
- : options?.reasoningSummary;
+ const reasoningSummary = model.compat.supportsReasoningSummary
+ ? options?.reasoningSummary
+ : options?.reasoning === undefined
+ ? undefined
+ : null;
applyResponsesCompatPolicy(params, reasoningPolicy, {
reasoningSummary,
forceReasoningOff: options?.forceReasoningOff,
diff --git a/packages/ai/src/providers/openai-shared.ts b/packages/ai/src/providers/openai-shared.ts
index 9d4f87861..f7b16f00e 100644
--- a/packages/ai/src/providers/openai-shared.ts
+++ b/packages/ai/src/providers/openai-shared.ts
@@ -3308,7 +3308,7 @@ export function applyCommonResponsesSamplingParams
& {
- compat: Pick;
+ compat: Pick;
},
): void {
if (options?.maxTokens && !model.omitMaxOutputTokens) {
@@ -3325,8 +3325,10 @@ export function applyCommonResponsesSamplingParams("xai", "grok-code-fast-1");
-if (!model) throw new Error("Expected bundled xAI Grok model");
-if (model.api !== "openai-completions") throw new Error(`Expected Chat Completions model, received ${model.api}`);
const context: Context = { messages: [{ role: "user", content: "hello", timestamp: 0 }] };
const openAI56ResponsesModel = getBundledModel<"openai-responses">("openai", "gpt-5.6");
@@ -44,7 +41,7 @@ function chatCompletionsSse(): Response {
id: "chatcmpl-affinity",
object: "chat.completion.chunk",
created: 0,
- model: model.id,
+ model: openAI56CompletionsModel.id,
choices: [{ index: 0, delta, finish_reason: finishReason }],
});
@@ -56,7 +53,7 @@ function chatCompletionsSse(): Response {
async function captureRequest(
options: OpenAICompletionsOptions,
- requestModel: Model<"openai-completions"> = model,
+ requestModel: Model<"openai-completions"> = openAI56CompletionsModel,
requestContext: Context = context,
): Promise<{ headers: Headers; body: Record }> {
let requestHeaders: Headers | undefined;
@@ -83,7 +80,7 @@ async function captureRequest(
async function captureSimpleRequest(
options: SimpleStreamOptions,
- requestModel: Model<"openai-completions"> = model,
+ requestModel: Model<"openai-completions"> = openAI56CompletionsModel,
requestContext: Context = context,
): Promise<{ headers: Headers; body: Record }> {
let requestHeaders: Headers | undefined;
@@ -104,51 +101,6 @@ async function captureSimpleRequest(
return { headers: requestHeaders, body };
}
-describe("openai-completions xAI cache affinity", () => {
- const cases: Array<{
- name: string;
- options: OpenAICompletionsOptions;
- expectedHeader: string | null;
- }> = [
- {
- name: "uses sessionId when no prompt cache key is provided",
- options: { sessionId: "session-fallback" },
- expectedHeader: "session-fallback",
- },
- {
- name: "keeps the prompt cache key stable across a distinct side-channel session",
- options: { promptCacheKey: "stable-cache-key", sessionId: "side-channel-session" },
- expectedHeader: "stable-cache-key",
- },
- {
- name: "omits automatic affinity when caching is disabled",
- options: {
- promptCacheKey: "disabled-cache-key",
- sessionId: "disabled-session",
- cacheRetention: "none",
- },
- expectedHeader: null,
- },
- {
- name: "preserves a caller-provided mixed-case affinity header",
- options: {
- promptCacheKey: "automatic-cache-key",
- sessionId: "automatic-session",
- headers: { "X-Grok-Conv-Id": "caller-affinity" },
- },
- expectedHeader: "caller-affinity",
- },
- ];
-
- for (const { name, options, expectedHeader } of cases) {
- it(name, async () => {
- const { headers } = await captureRequest(options);
-
- expect(headers.get("x-grok-conv-id")).toBe(expectedHeader);
- });
- }
-});
-
describe("OpenAI Chat Completions explicit prompt cache policy", () => {
const historicalContext: Context = {
messages: [
diff --git a/packages/ai/test/openai-completions-compat.test.ts b/packages/ai/test/openai-completions-compat.test.ts
index d8bebd5d1..fec2436e4 100644
--- a/packages/ai/test/openai-completions-compat.test.ts
+++ b/packages/ai/test/openai-completions-compat.test.ts
@@ -210,6 +210,7 @@ describe("openai-completions compatibility", () => {
toolStrictMode: "none",
supportsReasoningParams: true,
supportsSamplingParams: true,
+ supportsPenaltyAndStopParams: true,
alwaysSendMaxTokens: false,
isOpenRouterHost: false,
isVercelGatewayHost: false,
diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts
index 1219f8e30..8abf761b5 100644
--- a/packages/ai/test/openai-completions-tool-result-images.test.ts
+++ b/packages/ai/test/openai-completions-tool-result-images.test.ts
@@ -51,6 +51,7 @@ const compat: ResolvedOpenAICompat = {
toolStrictMode: "none",
supportsReasoningParams: true,
supportsSamplingParams: true,
+ supportsPenaltyAndStopParams: true,
alwaysSendMaxTokens: false,
isOpenRouterHost: false,
isVercelGatewayHost: false,
diff --git a/packages/ai/test/openai-responses-cache-affinity.test.ts b/packages/ai/test/openai-responses-cache-affinity.test.ts
index fd10d44cf..4eb32abca 100644
--- a/packages/ai/test/openai-responses-cache-affinity.test.ts
+++ b/packages/ai/test/openai-responses-cache-affinity.test.ts
@@ -57,6 +57,20 @@ const xaiOAuthResponsesModel: Model<"openai-responses"> = {
reasoning: true,
}),
};
+const xaiApiKeyResponsesModel: Model<"openai-responses"> = {
+ ...model,
+ id: "grok-code-fast-1",
+ name: "Grok Code Fast 1",
+ provider: "xai",
+ baseUrl: "https://api.x.ai/v1",
+ compat: buildOpenAIResponsesCompat({
+ id: "grok-code-fast-1",
+ name: "Grok Code Fast 1",
+ provider: "xai",
+ baseUrl: "https://api.x.ai/v1",
+ reasoning: true,
+ }),
+};
const openAI56ResponsesModel: Model<"openai-responses"> = {
...model,
@@ -675,6 +689,16 @@ describe("openai-responses cache affinity", () => {
}
});
+ it("sets x-grok-conv-id cache affinity for paid xai Responses requests", async () => {
+ const captured = await captureDispatchedOpenAIResponseHeaders(
+ { sessionId: "session-fallback" },
+ xaiApiKeyResponsesModel,
+ );
+
+ expect(getHeader(captured.headers, "x-grok-conv-id")).toBe("session-fallback");
+ expect(captured.body?.prompt_cache_key).toBe("session-fallback");
+ });
+
it("sets OpenRouter Responses session_id from sessionId in the body", async () => {
const captured = await captureOpenAIResponseHeaders(
{ sessionId: "workflow-123", promptCacheKey: "cache-key-123" },
diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts
index 0add12576..8017e13bd 100644
--- a/packages/ai/test/stream.test.ts
+++ b/packages/ai/test/stream.test.ts
@@ -1000,7 +1000,7 @@ describe("Generate E2E Tests", () => {
);
});
- describe.skipIf(!e2eApiKey("XAI_API_KEY"))("xAI Provider (grok-code-fast-1 via OpenAI Completions)", () => {
+ describe.skipIf(!e2eApiKey("XAI_API_KEY"))("xAI Provider (grok-code-fast-1 via OpenAI Responses)", () => {
const llm = getBundledModel("xai", "grok-code-fast-1");
it(
diff --git a/packages/ai/test/xai-login.test.ts b/packages/ai/test/xai-login.test.ts
index 1cdd2ca1d..3571b531a 100644
--- a/packages/ai/test/xai-login.test.ts
+++ b/packages/ai/test/xai-login.test.ts
@@ -29,6 +29,52 @@ 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.hasResolvableAuth("xai")).toBe(true);
+ expect(storage.hasResolvableAuth("xai-oauth")).toBe(true);
+ expect(getEnvApiKey("xai-oauth")).toBe("xai-env-key");
+ 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) => {
diff --git a/packages/ai/test/xai-oauth-effort-strip.test.ts b/packages/ai/test/xai-oauth-effort-strip.test.ts
index c84012057..6b7f99d89 100644
--- a/packages/ai/test/xai-oauth-effort-strip.test.ts
+++ b/packages/ai/test/xai-oauth-effort-strip.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { buildParams } from "@oh-my-pi/pi-ai/providers/openai-responses";
-import type { Context } from "@oh-my-pi/pi-ai/types";
+import type { AssistantMessage, Context, Model } from "@oh-my-pi/pi-ai/types";
import { Effort } from "@oh-my-pi/pi-catalog/effort";
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
@@ -38,6 +38,23 @@ describe("effort-dial-less reasoner encoding (regression)", () => {
expect(grokR.thinking).toBeUndefined();
});
+ test("paid xai/grok-code-fast-1 reasons but carries no thinking config", () => {
+ const grokCodeFast = getBundledModel("xai", "grok-code-fast-1");
+ if (!grokCodeFast) throw new Error("xai/grok-code-fast-1 must be in bundled models.json");
+ expect(grokCodeFast.api).toBe("openai-responses");
+ expect(grokCodeFast.reasoning).toBe(true);
+ expect(grokCodeFast.thinking).toBeUndefined();
+ expect(getSupportedEfforts(grokCodeFast)).toEqual([]);
+ });
+
+ test("paid xai/grok-4.3 keeps its effort dial", () => {
+ const grok43 = getBundledModel("xai", "grok-4.3");
+ if (!grok43) throw new Error("xai/grok-4.3 must be in bundled models.json");
+ expect(grok43.api).toBe("openai-responses");
+ expect(grok43.thinking).toBeDefined();
+ expect(getSupportedEfforts(grok43).length).toBeGreaterThan(0);
+ });
+
test("the no-dial encoding stays scoped to openai-responses*", () => {
const claude = getBundledModel("anthropic", "claude-sonnet-4-6");
if (!claude) throw new Error("anthropic/claude-sonnet-4-6 must be in bundled models.json");
@@ -57,6 +74,7 @@ describe("xAI OAuth Responses reasoning payload (regression)", () => {
const { params } = buildParams(grok45, singleUserContext, undefined, undefined);
expect(params.reasoning).toBeUndefined();
+ expect(params.include).toContain("reasoning.encrypted_content");
});
test("xai-oauth/grok-4.5 omits unsupported reasoning summary", () => {
@@ -66,5 +84,142 @@ describe("xAI OAuth Responses reasoning payload (regression)", () => {
const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.High }, undefined);
expect(params.reasoning).toEqual({ effort: "high" });
+ expect(params.include).toContain("reasoning.encrypted_content");
+ });
+
+ test("paid xai/grok-4.5 omits unsupported reasoning summary", () => {
+ const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
+ if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
+
+ const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.High }, undefined);
+
+ expect(params.reasoning).toEqual({ effort: "high" });
+ expect(params.include).toContain("reasoning.encrypted_content");
+ });
+
+ test("paid xai/grok-4.5 requests encrypted reasoning content", () => {
+ const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
+ if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
+
+ const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.High }, undefined);
+
+ expect(params.include).toContain("reasoning.encrypted_content");
+ });
+
+ test("paid xai/grok-4.5 omits presence_penalty on reasoning models", () => {
+ const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
+ if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
+
+ const { params } = buildParams(
+ grok45,
+ singleUserContext,
+ { reasoning: Effort.High, presencePenalty: 0.4, temperature: 0.2 },
+ undefined,
+ );
+
+ expect(params).not.toHaveProperty("presence_penalty");
+ expect(params.temperature).toBe(0.2);
+ });
+
+ test("paid xai/grok-2 omits presence_penalty on non-reasoning Responses models", () => {
+ const grok2 = getBundledModel<"openai-responses">("xai", "grok-2");
+ if (!grok2) throw new Error("xai/grok-2 must be in bundled models.json");
+
+ const { params } = buildParams(grok2, singleUserContext, { presencePenalty: 0.4, temperature: 0.2 }, undefined);
+
+ expect(params).not.toHaveProperty("presence_penalty");
+ expect(params.temperature).toBe(0.2);
+ });
+
+ test("paid xai/grok-4.5 clamps minimal reasoning effort to low", () => {
+ const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
+ if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
+
+ const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.Minimal }, undefined);
+
+ expect(params.reasoning).toEqual({ effort: "low" });
+ });
+
+ test("xai-oauth/grok-4.5 clamps minimal reasoning effort to low", () => {
+ const grok45 = getBundledModel<"openai-responses">("xai-oauth", "grok-4.5");
+ if (!grok45) throw new Error("xai-oauth/grok-4.5 must be in bundled models.json");
+
+ const { params } = buildParams(grok45, singleUserContext, { reasoning: Effort.Minimal }, undefined);
+
+ expect(params.reasoning).toEqual({ effort: "low" });
+ });
+
+ test("xai-oauth/grok-4.5 replays encrypted reasoning on the next turn", () => {
+ const grok45 = getBundledModel<"openai-responses">("xai-oauth", "grok-4.5");
+ if (!grok45) throw new Error("xai-oauth/grok-4.5 must be in bundled models.json");
+
+ const { params } = buildParams(grok45, followUpContextWithEncryptedReasoning(grok45), undefined, undefined);
+
+ expect(params.include).toContain("reasoning.encrypted_content");
+ expect(findEncryptedReasoning(params.input)).toEqual({
+ type: "reasoning",
+ id: "rs_xai_next_turn",
+ encrypted_content: "enc_next_turn",
+ });
+ });
+
+ test("paid xai/grok-4.5 replays encrypted reasoning on the next turn", () => {
+ const grok45 = getBundledModel<"openai-responses">("xai", "grok-4.5");
+ if (!grok45) throw new Error("xai/grok-4.5 must be in bundled models.json");
+
+ const { params } = buildParams(grok45, followUpContextWithEncryptedReasoning(grok45), undefined, undefined);
+
+ expect(findEncryptedReasoning(params.input)).toEqual({
+ type: "reasoning",
+ id: "rs_xai_next_turn",
+ encrypted_content: "enc_next_turn",
+ });
});
});
+
+function followUpContextWithEncryptedReasoning(model: Model<"openai-responses">): Context {
+ const assistant: AssistantMessage = {
+ role: "assistant",
+ content: [
+ {
+ type: "thinking",
+ thinking: "internal plan",
+ thinkingSignature: JSON.stringify({
+ type: "reasoning",
+ id: "rs_xai_next_turn",
+ encrypted_content: "enc_next_turn",
+ }),
+ },
+ { type: "text", text: "done" },
+ ],
+ api: "openai-responses",
+ provider: model.provider,
+ model: model.id,
+ usage: {
+ input: 0,
+ output: 0,
+ cacheRead: 0,
+ cacheWrite: 0,
+ totalTokens: 0,
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
+ },
+ stopReason: "stop",
+ timestamp: 1,
+ };
+ return {
+ messages: [
+ { role: "user", content: "first", timestamp: 0 },
+ assistant,
+ { role: "user", content: "continue", timestamp: 2 },
+ ],
+ };
+}
+
+function findEncryptedReasoning(input: unknown): Record | undefined {
+ if (!Array.isArray(input)) return undefined;
+ return input.find(item => {
+ if (!item || typeof item !== "object") return false;
+ const candidate = item as { type?: unknown; encrypted_content?: unknown };
+ return candidate.type === "reasoning" && typeof candidate.encrypted_content === "string";
+ }) as Record | undefined;
+}
diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md
index ebef9e027..94cac5bfd 100644
--- a/packages/catalog/CHANGELOG.md
+++ b/packages/catalog/CHANGELOG.md
@@ -5,6 +5,26 @@
### Added
- Added support for GLM-5.3 on the z.AI provider. GLM-5.3 introduces a uniform wire-exact `low`/`high`/`max` reasoning-effort ladder on every host (replacing GLM-5.2's host-specific dialects), makes thinking mandatory (`thinking.type` must always be `enabled`; disabling is no longer supported), and defaults to `max` effort. The model is pinned to 1M context and set as the z.AI provider default.
+### Changed
+
+- Switched the paid xAI provider (`xai` / `XAI_API_KEY`) from Chat Completions to the OpenAI Responses API (`POST https://api.x.ai/v1/responses`), matching SuperGrok `xai-oauth`. Prompt-cache affinity (`x-grok-conv-id`), reasoning-effort allowlisting, and encrypted-reasoning replay rules are now shared across both first-party xAI hosts.
+- Changed the paid xAI (`XAI_API_KEY`) default model from `grok-4-fast-non-reasoning` to `grok-4.5`.
+- Changed the SuperGrok (`xai-oauth`) default model from `grok-4.3` to `grok-4.5`.
+- Requested `reasoning.encrypted_content` on first-party xAI Responses calls (`xai` and `xai-oauth`) via the `include` parameter.
+- Replayed xAI encrypted reasoning items on later Responses turns instead of stripping `type: "reasoning"` history.
+
+### Fixed
+
+- Invalidated stale paid-xAI model-cache rows written under Chat Completions so the Responses migration takes effect immediately instead of waiting for TTL expiry.
+- Clamped paid xAI Responses `minimal` reasoning effort to `low` (same wire map as SuperGrok) so `xai/grok-4.5` does not 400.
+- Suppressed presence/frequency penalties and stop sequences on xAI reasoning models so a configured `presencePenalty` does not 400 after the `grok-4.5` default change.
+- Stopped emitting stale `thinking.efforts` dials on paid xAI Responses catalog rows that reject `reasoning.effort` (`grok-code-fast-1`, `grok-build-0.1`, `grok-4.20-0309-reasoning`, and other off-allowlist reasoners).
+- Marked first-party xAI Responses hosts (`xai` and `xai-oauth`) as not supporting `reasoning.summary`, so paid `xai/grok-4.5` effort requests omit the unsupported field instead of sending `summary: "auto"`.
+- Removed unsupported `xhigh` (and `max`) thinking tiers from first-party Grok 4.5 / 4.3 / 3-mini Responses rows; leftover `xhigh`/`max` requests clamp to `high`. `grok-4.6*` and `grok-4.20-multi-agent*` advertise unmapped `xhigh`.
+- Stopped baking `reasoningEffortMap` on first-party xAI catalog rows that omit `reasoning.effort` (`omitReasoningEffort: true`).
+- Suppressed presence/frequency penalties on every first-party xAI Responses model, including non-reasoning ids such as `grok-2`; xAI's `/v1/responses` marks those fields unsupported.
+- Routed `grok-4.6` (added on main) through first-party xAI Responses and advertised its documented `xhigh` effort tier (4.5 stays 4-tier).
+
## [17.3.4] - 2026-08-14
### Added
diff --git a/packages/catalog/scripts/generated-policies.ts b/packages/catalog/scripts/generated-policies.ts
index abf8f82cc..58bbb0902 100644
--- a/packages/catalog/scripts/generated-policies.ts
+++ b/packages/catalog/scripts/generated-policies.ts
@@ -21,6 +21,7 @@ import { resolveModelThinking } from "../src/model-thinking";
import { isOllamaCloudOutputCapped, OLLAMA_CLOUD_MAX_OUTPUT_TOKENS } from "../src/provider-models/ollama";
import {
ALIBABA_TOKEN_PLAN_STATIC_MODELS,
+ applyXaiResponsesThinkingPolicy,
OPENAI_GPT_56_LONG_CONTEXT_COSTS,
resolveWaferServerlessThinkingFormat,
} from "../src/provider-models/openai-compat";
@@ -353,6 +354,10 @@ export function applyOllamaCloudOutputCap(models: ModelSpec[]): void {
}
function applyGeneratedModelPolicy(model: ModelSpec): void {
+ if ((model.provider === "xai" || model.provider === "xai-oauth") && model.api === "openai-responses") {
+ const updated = applyXaiResponsesThinkingPolicy(model as ModelSpec<"openai-responses">);
+ model.compat = updated.compat;
+ }
const copilotLimits = model.provider === "github-copilot" ? COPILOT_GENERATED_LIMITS[model.id] : undefined;
if (copilotLimits) {
model.contextWindow = copilotLimits.contextWindow;
diff --git a/packages/catalog/src/compat/openai.ts b/packages/catalog/src/compat/openai.ts
index ad7ba06b0..2eeec29f1 100644
--- a/packages/catalog/src/compat/openai.ts
+++ b/packages/catalog/src/compat/openai.ts
@@ -16,6 +16,7 @@ import {
isDeepseekModelIdOrName,
isGlm52ReasoningEffortModelId,
isGrokReasoningEffortCapable,
+ isGrokXHighEffortCapable,
isKimiK3ModelId,
isKimiK26ModelId,
isKimiModelId,
@@ -177,6 +178,22 @@ const MIMO_REASONING_EFFORT_MAP: NonNullable
xhigh: "high",
};
+/** Shared `minimal → low` clamp. xhigh-capable Grok keeps `xhigh` unmapped. */
+const XAI_RESPONSES_MINIMAL_EFFORT_MAP: NonNullable = {
+ minimal: "low",
+};
+/** Grok 4.5 / 4.3 / 3-mini: leftover `xhigh`/`max` clamp to `high`. */
+const XAI_RESPONSES_CLAMPED_EFFORT_MAP: NonNullable = {
+ minimal: "low",
+ xhigh: "high",
+ max: "high",
+};
+
+/** Wire effort remap for first-party xAI Responses. */
+export function xaiResponsesReasoningEffortMap(modelId: string): NonNullable {
+ return isGrokXHighEffortCapable(modelId) ? XAI_RESPONSES_MINIMAL_EFFORT_MAP : XAI_RESPONSES_CLAMPED_EFFORT_MAP;
+}
+
function mergeModelReasoningEffortMap(
compat: ResolvedOpenAISharedCompat,
modelId: string,
@@ -471,6 +488,8 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
// temperature/top_p/… with a 400 on every serving host (#5606).
supportsSamplingParams: !isOpenAISamplingRestrictedModelId(spec.id),
+ // xAI reasoning models 400 on presence/frequency penalties and stop.
+ supportsPenaltyAndStopParams: !(isGrok && Boolean(spec.reasoning)),
reasoningEffortMap: {},
supportsUsageInStreaming: !isCerebras,
// Kimi (including via OpenRouter and Fireworks router-form IDs such as
@@ -684,34 +703,46 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
const isLocalServingBackend =
(!PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) && LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider)) ||
hasLocalLoopbackBaseUrl(baseUrl);
+ const isXaiHost = modelMatchesHost({ provider: spec.provider, baseUrl }, "xai");
const compat: ResolvedOpenAIResponsesCompat = {
supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
supportsStrictMode: isAzure || detectStrictModeSupport(spec.provider, baseUrl),
- supportsReasoningEffort: spec.provider !== "xai-oauth" || isGrokReasoningEffortCapable(id),
+ // Paid `xai` and SuperGrok `xai-oauth` share api.x.ai `/v1/responses`.
+ // Only the Grok effort-capable allowlist accepts `reasoning.effort`;
+ // other reasoners (grok-build, grok-code-fast-1, …) 400 if it is sent.
+ supportsReasoningEffort: !isXaiHost || isGrokReasoningEffortCapable(id),
supportsLongPromptCacheRetention: isOpenAIUrl,
supportsPromptCacheBreakpoints,
promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined,
// Azure OpenAI and GitHub Copilot Responses paths require tool results
// to strictly match prior tool calls when building Responses inputs.
strictResponsesPairing: isAzure || spec.provider === "github-copilot",
- // GitHub Copilot and xAI OAuth reject `detail: "original"` (400 / 422).
- // Every other host preserves native-resolution frames (snapcompact relies
- // on `original`). Detect Copilot by provider id or base-URL host so a
- // model pointed at the Copilot host under a different provider id still
- // clamps; xai-oauth is provider-id only (same host family as paid `xai`).
+ // GitHub Copilot and first-party xAI `/v1/responses` reject
+ // `detail: "original"` (400 / 422). Every other host preserves
+ // native-resolution frames (snapcompact relies on `original`). Detect
+ // Copilot by provider id or base-URL host so a model pointed at the
+ // Copilot host under a different provider id still clamps.
supportsImageDetailOriginal:
- spec.provider !== "xai-oauth" && !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
- reasoningEffortMap: {},
+ !isXaiHost && !modelMatchesHost({ provider: spec.provider, baseUrl }, "githubCopilot"),
+ // api.x.ai rejects `reasoning.summary` (SuperGrok and paid key alike).
+ supportsReasoningSummary: !isXaiHost,
+ reasoningEffortMap: isXaiHost ? { ...xaiResponsesReasoningEffortMap(id) } : {},
supportsReasoningParams: true,
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
// temperature/top_p/… with a 400 on every serving host (#5606).
supportsSamplingParams: !isOpenAISamplingRestrictedModelId(id),
+ // xAI `/v1/responses` rejects presence/frequency penalties for every
+ // model, not only reasoners (https://docs.x.ai/developers/rest-api-reference/inference/chat).
+ supportsPenaltyAndStopParams: !isXaiHost,
thinkingFormat,
reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
omitReasoningEffort: false,
- includeEncryptedReasoning: spec.provider !== "xai-oauth",
- filterReasoningHistory: spec.provider === "xai-oauth" || (isOpenRouter && isAnthropicModel),
+ // Ask xAI `/v1/responses` for `reasoning.encrypted_content` and replay
+ // those items on later turns. OpenRouter Anthropic still filters
+ // reasoning wrappers independently.
+ includeEncryptedReasoning: true,
+ filterReasoningHistory: isOpenRouter && isAnthropicModel,
disableReasoningOnForcedToolChoice: isKimiModel,
disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,
supportsToolChoice: true,
@@ -752,13 +783,24 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),
emptyLengthFinishIsContextError: spec.provider === "ollama",
usesOpenAIToolCallIdLimit: spec.provider === "openai",
- promptCacheSessionHeader: spec.provider === "xai-oauth" ? "x-grok-conv-id" : undefined,
+ promptCacheSessionHeader: isXaiHost ? "x-grok-conv-id" : undefined,
streamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs,
streamIdleTimeoutMs: isLocalServingBackend
? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
: spec.compat?.streamIdleTimeoutMs,
};
applyCompatOverrides(compat, spec.compat);
+ if (isXaiHost) {
+ const canonical = xaiResponsesReasoningEffortMap(id);
+ compat.reasoningEffortMap = { ...compat.reasoningEffortMap, ...canonical };
+ // xhigh-capable Grok advertises unmapped `xhigh`; drop a stale clamp
+ // from previous snapshots so 4.6 / 16-agent mode is not rewritten to `high`.
+ for (const key of ["xhigh", "max"] as const) {
+ if (!(key in canonical)) {
+ delete compat.reasoningEffortMap[key];
+ }
+ }
+ }
if (spec.compat?.reasoningDisableMode === undefined) {
compat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);
}
@@ -776,6 +818,7 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
strictResponsesPairing: compat.strictResponsesPairing,
supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
+ supportsReasoningSummary: compat.supportsReasoningSummary,
isVercelGatewayHost: compat.isVercelGatewayHost,
} satisfies ResponsesOnlyCompat;
}
diff --git a/packages/catalog/src/hosts.ts b/packages/catalog/src/hosts.ts
index 176f77125..5c7f3e2fb 100644
--- a/packages/catalog/src/hosts.ts
+++ b/packages/catalog/src/hosts.ts
@@ -47,7 +47,7 @@ export const KNOWN_HOSTS = {
},
umans: { providers: ["umans"], urlMarkers: ["api.code.umans.ai"] },
xiaomi: { providers: ["xiaomi"], providerPrefixes: ["xiaomi-token-plan-"], urlMarkers: ["xiaomimimo.com"] },
- xai: { providers: ["xai"], urlMarkers: ["api.x.ai"] },
+ xai: { providers: ["xai", "xai-oauth"], urlMarkers: ["api.x.ai"] },
mistral: { providers: ["mistral"], urlMarkers: ["mistral.ai"] },
together: { providers: ["together"], urlMarkers: ["api.together.xyz"] },
baseten: { providers: ["baseten"], urlMarkers: ["baseten.co"] },
diff --git a/packages/catalog/src/identity/family.ts b/packages/catalog/src/identity/family.ts
index e57ca16f7..9395c3e13 100644
--- a/packages/catalog/src/identity/family.ts
+++ b/packages/catalog/src/identity/family.ts
@@ -110,7 +110,13 @@ export const isGrokModelId = memo((modelId: string): boolean => {
return /(?:^|[./_-])grok(?:[-.]|$)/i.test(modelId);
});
-const GROK_EFFORT_CAPABLE_PREFIXES = ["grok-3-mini", "grok-4.20-multi-agent", "grok-4.3", "grok-4.5"] as const;
+const GROK_EFFORT_CAPABLE_PREFIXES = [
+ "grok-3-mini",
+ "grok-4.20-multi-agent",
+ "grok-4.3",
+ "grok-4.5",
+ "grok-4.6",
+] as const;
/**
* Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
@@ -123,6 +129,27 @@ export const isGrokReasoningEffortCapable = memo((modelId: string): boolean => {
return GROK_EFFORT_CAPABLE_PREFIXES.some(prefix => bare.startsWith(prefix));
});
+/**
+ * `grok-4.20-multi-agent*` uses `reasoning.effort` to pick agent count
+ * (`xhigh` is the 16-agent mode). Other first-party Grok effort SKUs stay on
+ * `low|medium|high` unless {@link isGrokXHighEffortCapable} (currently
+ * `grok-4.6*` plus multi-agent).
+ * https://docs.x.ai/developers/model-capabilities/text/reasoning
+ */
+export const isGrokMultiAgentModelId = memo((modelId: string): boolean => {
+ return bareModelId(modelId).trim().toLowerCase().startsWith("grok-4.20-multi-agent");
+});
+
+/**
+ * First-party Grok SKUs whose Responses wire accepts `reasoning.effort: "xhigh"`.
+ * `grok-4.6*` documents xhigh as a reasoning depth; multi-agent uses it as
+ * 16-agent mode. `grok-4.5` / `grok-4.3` / `grok-3-mini` do not.
+ */
+export const isGrokXHighEffortCapable = memo((modelId: string): boolean => {
+ if (isGrokMultiAgentModelId(modelId)) return true;
+ return bareModelId(modelId).trim().toLowerCase().startsWith("grok-4.6");
+});
+
/**
* MiniMax M2-generation family (M2, M2.1, M2.5, M2.7, including `-highspeed`/
* `-lightning`/`-her`/`-turbo` variants, dotless aliases like `minimax-m21`,
diff --git a/packages/catalog/src/model-thinking.ts b/packages/catalog/src/model-thinking.ts
index 79ee1e372..6d0c40428 100644
--- a/packages/catalog/src/model-thinking.ts
+++ b/packages/catalog/src/model-thinking.ts
@@ -27,6 +27,7 @@ import {
isDeepseekV4FlashModelId,
isGlm52ReasoningEffortModelId,
isGlm53ReasoningEffortModelId,
+ isGrokXHighEffortCapable,
isKimiK3ModelId,
isMimoModelIdOrName,
isMinimaxM2FamilyModelId,
@@ -404,6 +405,11 @@ function getModelDefinedEfforts(
// Baseten's gpt-oss router mirrors its GLM route: high/max only.
return HIGH_MAX_REASONING_EFFORTS;
}
+ // First-party Grok: `grok-4.6*` and `grok-4.20-multi-agent*` advertise
+ // `xhigh`. Other effort-capable SKUs stay on `minimal/low/medium/high`.
+ if (modelMatchesHost({ provider: spec.provider, baseUrl: spec.baseUrl ?? "" }, "xai")) {
+ return isGrokXHighEffortCapable(spec.id) ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS;
+ }
return isOpenAICompatReasoningApi(spec.api) &&
(isMinimaxM2FamilyModelId(spec.id) ||
isOpenAIGptOssModelId(spec.id) ||
diff --git a/packages/catalog/src/models.json b/packages/catalog/src/models.json
index 6c62a3435..d7eae1742 100644
--- a/packages/catalog/src/models.json
+++ b/packages/catalog/src/models.json
@@ -103716,7 +103716,7 @@
"grok-2": {
"id": "grok-2",
"name": "Grok 2",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103730,12 +103730,16 @@
"cacheWrite": 0
},
"contextWindow": 131072,
- "maxTokens": 8192
+ "maxTokens": 8192,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-2-1212": {
"id": "grok-2-1212",
"name": "Grok 2 (1212)",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103749,12 +103753,16 @@
"cacheWrite": 0
},
"contextWindow": 131072,
- "maxTokens": 8192
+ "maxTokens": 8192,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-2-latest": {
"id": "grok-2-latest",
"name": "Grok 2 Latest",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103768,12 +103776,16 @@
"cacheWrite": 0
},
"contextWindow": 131072,
- "maxTokens": 8192
+ "maxTokens": 8192,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-2-vision": {
"id": "grok-2-vision",
"name": "Grok 2 Vision",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103788,12 +103800,16 @@
"cacheWrite": 0
},
"contextWindow": 8192,
- "maxTokens": 4096
+ "maxTokens": 4096,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-2-vision-1212": {
"id": "grok-2-vision-1212",
"name": "Grok 2 Vision (1212)",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103808,12 +103824,16 @@
"cacheWrite": 0
},
"contextWindow": 8192,
- "maxTokens": 4096
+ "maxTokens": 4096,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-2-vision-latest": {
"id": "grok-2-vision-latest",
"name": "Grok 2 Vision Latest",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103828,12 +103848,16 @@
"cacheWrite": 0
},
"contextWindow": 8192,
- "maxTokens": 4096
+ "maxTokens": 4096,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-3": {
"id": "grok-3",
"name": "Grok 3",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103847,12 +103871,16 @@
"cacheWrite": 0
},
"contextWindow": 131072,
- "maxTokens": 8192
+ "maxTokens": 8192,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-3-fast": {
"id": "grok-3-fast",
"name": "Grok 3 Fast",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103866,12 +103894,16 @@
"cacheWrite": 0
},
"contextWindow": 131072,
- "maxTokens": 8192
+ "maxTokens": 8192,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-3-fast-latest": {
"id": "grok-3-fast-latest",
"name": "Grok 3 Fast Latest",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103885,12 +103917,16 @@
"cacheWrite": 0
},
"contextWindow": 131072,
- "maxTokens": 8192
+ "maxTokens": 8192,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-3-latest": {
"id": "grok-3-latest",
"name": "Grok 3 Latest",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -103904,12 +103940,16 @@
"cacheWrite": 0
},
"contextWindow": 131072,
- "maxTokens": 8192
+ "maxTokens": 8192,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-3-mini": {
"id": "grok-3-mini",
"name": "Grok 3 Mini",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -103931,13 +103971,25 @@
"low",
"medium",
"high"
- ]
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low",
+ "xhigh": "high",
+ "max": "high"
+ },
+ "supportsReasoningEffort": true,
+ "omitReasoningEffort": false
}
},
"grok-3-mini-fast": {
"id": "grok-3-mini-fast",
"name": "Grok 3 Mini Fast",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -103959,13 +104011,25 @@
"low",
"medium",
"high"
- ]
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low",
+ "xhigh": "high",
+ "max": "high"
+ },
+ "supportsReasoningEffort": true,
+ "omitReasoningEffort": false
}
},
"grok-3-mini-fast-latest": {
"id": "grok-3-mini-fast-latest",
"name": "Grok 3 Mini Fast Latest",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -103987,13 +104051,25 @@
"low",
"medium",
"high"
- ]
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low",
+ "xhigh": "high",
+ "max": "high"
+ },
+ "supportsReasoningEffort": true,
+ "omitReasoningEffort": false
}
},
"grok-3-mini-latest": {
"id": "grok-3-mini-latest",
"name": "Grok 3 Mini Latest",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104015,13 +104091,25 @@
"low",
"medium",
"high"
- ]
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low",
+ "xhigh": "high",
+ "max": "high"
+ },
+ "supportsReasoningEffort": true,
+ "omitReasoningEffort": false
}
},
"grok-4": {
"id": "grok-4",
"name": "Grok 4",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104036,20 +104124,15 @@
},
"contextWindow": 256000,
"maxTokens": 64000,
- "thinking": {
- "mode": "effort",
- "efforts": [
- "minimal",
- "low",
- "medium",
- "high"
- ]
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
}
},
"grok-4-1-fast": {
"id": "grok-4-1-fast",
"name": "Grok 4.1 Fast",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104065,20 +104148,15 @@
},
"contextWindow": 2000000,
"maxTokens": 30000,
- "thinking": {
- "mode": "effort",
- "efforts": [
- "minimal",
- "low",
- "medium",
- "high"
- ]
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
}
},
"grok-4-1-fast-non-reasoning": {
"id": "grok-4-1-fast-non-reasoning",
"name": "Grok 4.1 Fast (Non-Reasoning)",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -104093,12 +104171,16 @@
"cacheWrite": 0
},
"contextWindow": 2000000,
- "maxTokens": 30000
+ "maxTokens": 30000,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-4-fast": {
"id": "grok-4-fast",
"name": "Grok 4 Fast",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104114,20 +104196,15 @@
},
"contextWindow": 2000000,
"maxTokens": 30000,
- "thinking": {
- "mode": "effort",
- "efforts": [
- "minimal",
- "low",
- "medium",
- "high"
- ]
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
}
},
"grok-4-fast-non-reasoning": {
"id": "grok-4-fast-non-reasoning",
"name": "Grok 4 Fast (Non-Reasoning)",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -104142,12 +104219,16 @@
"cacheWrite": 0
},
"contextWindow": 2000000,
- "maxTokens": 30000
+ "maxTokens": 30000,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-4.20-0309-non-reasoning": {
"id": "grok-4.20-0309-non-reasoning",
"name": "Grok 4.20 (Non-Reasoning)",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -104162,12 +104243,16 @@
"cacheWrite": 0
},
"contextWindow": 1000000,
- "maxTokens": 30000
+ "maxTokens": 30000,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-4.20-0309-reasoning": {
"id": "grok-4.20-0309-reasoning",
"name": "Grok 4.20 (Reasoning)",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104183,21 +104268,15 @@
},
"contextWindow": 1000000,
"maxTokens": 30000,
- "thinking": {
- "mode": "effort",
- "efforts": [
- "minimal",
- "low",
- "medium",
- "high"
- ],
- "requiresEffort": true
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
}
},
"grok-4.20-beta-latest-non-reasoning": {
"id": "grok-4.20-beta-latest-non-reasoning",
"name": "Grok 4.20 Beta (Non-Reasoning)",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -104212,12 +104291,16 @@
"cacheWrite": 0
},
"contextWindow": 2000000,
- "maxTokens": 30000
+ "maxTokens": 30000,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-4.20-beta-latest-reasoning": {
"id": "grok-4.20-beta-latest-reasoning",
"name": "Grok 4.20 Beta (Reasoning)",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104233,21 +104316,15 @@
},
"contextWindow": 2000000,
"maxTokens": 30000,
- "thinking": {
- "mode": "effort",
- "efforts": [
- "minimal",
- "low",
- "medium",
- "high"
- ],
- "requiresEffort": true
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
}
},
"grok-4.20-multi-agent-beta-latest": {
"id": "grok-4.20-multi-agent-beta-latest",
"name": "Grok 4.20 Multi-Agent Beta",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104269,14 +104346,25 @@
"minimal",
"low",
"medium",
- "high"
- ]
+ "high",
+ "xhigh"
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low"
+ },
+ "supportsReasoningEffort": true,
+ "omitReasoningEffort": false
}
},
"grok-4.3": {
"id": "grok-4.3",
"name": "Grok 4.3",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104299,13 +104387,25 @@
"low",
"medium",
"high"
- ]
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low",
+ "xhigh": "high",
+ "max": "high"
+ },
+ "supportsReasoningEffort": true,
+ "omitReasoningEffort": false
}
},
"grok-4.5": {
"id": "grok-4.5",
"name": "Grok 4.5",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104328,13 +104428,25 @@
"low",
"medium",
"high"
- ]
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low",
+ "xhigh": "high",
+ "max": "high"
+ },
+ "supportsReasoningEffort": true,
+ "omitReasoningEffort": false
}
},
"grok-4.6": {
"id": "grok-4.6",
"name": "Grok 4.6",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104356,14 +104468,25 @@
"minimal",
"low",
"medium",
- "high"
- ]
+ "high",
+ "xhigh"
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low"
+ },
+ "supportsReasoningEffort": true,
+ "omitReasoningEffort": false
}
},
"grok-beta": {
"id": "grok-beta",
"name": "Grok Beta",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -104377,12 +104500,16 @@
"cacheWrite": 0
},
"contextWindow": 131072,
- "maxTokens": 4096
+ "maxTokens": 4096,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
},
"grok-build-0.1": {
"id": "grok-build-0.1",
"name": "Grok Build 0.1",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104398,20 +104525,15 @@
},
"contextWindow": 256000,
"maxTokens": 256000,
- "thinking": {
- "mode": "effort",
- "efforts": [
- "minimal",
- "low",
- "medium",
- "high"
- ]
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
}
},
"grok-code-fast-1": {
"id": "grok-code-fast-1",
"name": "Grok Code Fast 1",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
@@ -104426,20 +104548,15 @@
},
"contextWindow": 256000,
"maxTokens": 10000,
- "thinking": {
- "mode": "effort",
- "efforts": [
- "minimal",
- "low",
- "medium",
- "high"
- ]
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
}
},
"grok-vision-beta": {
"id": "grok-vision-beta",
"name": "Grok Vision Beta",
- "api": "openai-completions",
+ "api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": false,
@@ -104454,7 +104571,11 @@
"cacheWrite": 0
},
"contextWindow": 8192,
- "maxTokens": 4096
+ "maxTokens": 4096,
+ "compat": {
+ "supportsReasoningEffort": false,
+ "omitReasoningEffort": true
+ }
}
},
"xai-oauth": {
@@ -104480,11 +104601,8 @@
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
- "reasoningEffortMap": {
- "minimal": "low"
- },
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
"supportsImageDetailOriginal": false,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
@@ -104512,11 +104630,8 @@
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
- "reasoningEffortMap": {
- "minimal": "low"
- },
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
"supportsImageDetailOriginal": false,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
@@ -104559,8 +104674,8 @@
"reasoningEffortMap": {
"minimal": "low"
},
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
"supportsImageDetailOriginal": false,
"omitReasoningEffort": false,
"supportsReasoningEffort": true
@@ -104591,8 +104706,7 @@
"minimal",
"low",
"medium",
- "high",
- "xhigh"
+ "high"
],
"effortMap": {
"minimal": "low"
@@ -104602,10 +104716,12 @@
"supportsComputerUseConfig": false,
"compat": {
"reasoningEffortMap": {
- "minimal": "low"
+ "minimal": "low",
+ "xhigh": "high",
+ "max": "high"
},
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
"supportsImageDetailOriginal": false,
"omitReasoningEffort": false,
"supportsReasoningEffort": true
@@ -104630,6 +104746,52 @@
},
"contextWindow": 500000,
"maxTokens": 500000,
+ "thinking": {
+ "mode": "effort",
+ "efforts": [
+ "minimal",
+ "low",
+ "medium",
+ "high"
+ ],
+ "effortMap": {
+ "minimal": "low"
+ }
+ },
+ "supportsComputerUse": false,
+ "supportsComputerUseConfig": false,
+ "compat": {
+ "reasoningEffortMap": {
+ "minimal": "low",
+ "xhigh": "high",
+ "max": "high"
+ },
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
+ "supportsImageDetailOriginal": false,
+ "omitReasoningEffort": false,
+ "supportsReasoningEffort": true
+ }
+ },
+ "grok-4.6": {
+ "id": "grok-4.6",
+ "name": "Grok 4.6",
+ "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,
"thinking": {
"mode": "effort",
"efforts": [
@@ -104649,41 +104811,13 @@
"reasoningEffortMap": {
"minimal": "low"
},
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
"supportsImageDetailOriginal": false,
"omitReasoningEffort": false,
"supportsReasoningEffort": true
}
},
- "grok-4.6": {
- "id": "grok-4.6",
- "name": "Grok 4.6",
- "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,
- "supportsComputerUse": false,
- "supportsComputerUseConfig": false,
- "compat": {
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
- "supportsImageDetailOriginal": false,
- "omitReasoningEffort": true
- }
- },
"grok-build": {
"id": "grok-build",
"name": "Grok Build",
@@ -104706,11 +104840,8 @@
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
- "reasoningEffortMap": {
- "minimal": "low"
- },
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
"supportsImageDetailOriginal": false,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
@@ -104738,11 +104869,8 @@
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
- "reasoningEffortMap": {
- "minimal": "low"
- },
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
"supportsImageDetailOriginal": false,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
@@ -104769,11 +104897,8 @@
"supportsComputerUse": false,
"supportsComputerUseConfig": false,
"compat": {
- "reasoningEffortMap": {
- "minimal": "low"
- },
- "includeEncryptedReasoning": false,
- "filterReasoningHistory": true,
+ "includeEncryptedReasoning": true,
+ "filterReasoningHistory": false,
"supportsImageDetailOriginal": false,
"omitReasoningEffort": true,
"supportsReasoningEffort": false
@@ -112025,4 +112150,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/catalog/src/provider-models/descriptors.ts b/packages/catalog/src/provider-models/descriptors.ts
index 97f7ef54d..dc1d2afc3 100644
--- a/packages/catalog/src/provider-models/descriptors.ts
+++ b/packages/catalog/src/provider-models/descriptors.ts
@@ -478,13 +478,13 @@ export const CATALOG_PROVIDERS = [
},
{
id: "xai",
- defaultModel: "grok-4-fast-non-reasoning",
+ defaultModel: "grok-4.5",
envVars: ["XAI_API_KEY"],
createModelManagerOptions: (config: ModelManagerConfig) => xaiModelManagerOptions(config),
},
{
id: "xai-oauth",
- defaultModel: "grok-4.3",
+ defaultModel: "grok-4.5",
envVars: ["XAI_OAUTH_TOKEN", "XAI_API_KEY"],
createModelManagerOptions: (config: ModelManagerConfig) => xaiOAuthModelManagerOptions(config),
catalogDiscovery: {
diff --git a/packages/catalog/src/provider-models/openai-compat.ts b/packages/catalog/src/provider-models/openai-compat.ts
index bf985c9a5..347811244 100644
--- a/packages/catalog/src/provider-models/openai-compat.ts
+++ b/packages/catalog/src/provider-models/openai-compat.ts
@@ -1,5 +1,6 @@
import { USER_AGENT } from "@oh-my-pi/pi-utils";
import * as logger from "@oh-my-pi/pi-utils/logger";
+import { xaiResponsesReasoningEffortMap } from "../compat/openai";
import {
DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
fetchOpenAICompatibleModels,
@@ -1257,8 +1258,23 @@ export interface XaiModelManagerConfig {
fetch?: FetchImpl;
}
-export function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-completions"> {
- return createSimpleOpenAICompletionsOptions("xai", "https://api.x.ai/v1", config);
+export function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-responses"> {
+ return {
+ ...createOpenAICompatibleModelManagerOptions({
+ api: "openai-responses",
+ providerId: "xai",
+ defaultBaseUrl: "https://api.x.ai/v1",
+ config,
+ requireApiKey: true,
+ mapModel: mapWithBundledReference,
+ }),
+ // Completions → Responses migration: a fresh authoritative cache written
+ // by the old resolver stores `api: "openai-completions"` for these ids.
+ // Without a drop list, `online-if-uncached` skips the network and
+ // `mergeDynamicModel` lets the cached api win over the new static
+ // Responses entries until TTL expiry.
+ dropCachedModelIdsOnStaticMismatch: getBundledModels("xai").map(model => model.id),
+ };
}
export interface XaiOAuthModelManagerConfig {
@@ -1316,6 +1332,7 @@ export const XAI_OAUTH_CURATED_MODELS: readonly XAICuratedModel[] = [
},
{ id: "grok-4.3", contextWindow: 1_000_000, name: "Grok 4.3", input: ["text", "image"] },
{ id: "grok-4.5", contextWindow: 500_000, name: "Grok 4.5", input: ["text", "image"] },
+ { id: "grok-4.6", contextWindow: 500_000, name: "Grok 4.6", input: ["text", "image"] },
// grok-4.20-multi-agent-0309 is text-only per the bundled catalog; omit `input` for the default.
{ id: "grok-4.20-multi-agent-0309", contextWindow: 2_000_000, name: "Grok 4.20 (Multi-Agent)" },
{
@@ -1352,21 +1369,47 @@ const XAI_NON_CHAT_PREFIXES = ["grok-imagine-", "grok-stt-", "grok-voice-"] as c
function withXaiOAuthCompatDefaults(model: ModelSpec<"openai-responses">): ModelSpec<"openai-responses"> {
const compat = {
...(model.compat ?? {}),
- includeEncryptedReasoning: model.compat?.includeEncryptedReasoning ?? false,
- filterReasoningHistory: model.compat?.filterReasoningHistory ?? true,
+ includeEncryptedReasoning: model.compat?.includeEncryptedReasoning ?? true,
+ filterReasoningHistory: model.compat?.filterReasoningHistory ?? false,
supportsImageDetailOriginal: model.compat?.supportsImageDetailOriginal ?? false,
omitReasoningEffort: model.compat?.omitReasoningEffort ?? !isGrokReasoningEffortCapable(model.id),
};
return { ...model, compat };
}
-// Hermes-agent parity: only the `minimal -> low` clamp is applied (see
-// hermes-agent/agent/transports/codex.py:92 `_effort_clamp = {"minimal":
-// "low"}`). Hermes sends `xhigh` to xAI verbatim and we match that contract
-// — let xAI decide if the level is valid for the specific Grok model.
-// `resolveModelThinking` folds this into `model.thinking.effortMap`, downstream
-// of the omitReasoningEffort gate in pi-ai's stream.ts.
-const XAI_REASONING_EFFORT_MAP = { minimal: "low" } as const;
+// Hermes-agent parity for `minimal -> low` (see hermes-agent/agent/transports/
+// codex.py:92). Multi-agent Grok keeps `xhigh` unmapped (agent-count mode);
+// other first-party SKUs clamp leftover `xhigh`/`max` to `high`.
+// `resolveModelThinking` folds this into `model.thinking.effortMap`.
+
+/**
+ * Bake first-party xAI Responses effort-dial metadata onto a catalog spec.
+ *
+ * models.dev marks many Grok SKUs as reasoners and the thinking rebake would
+ * otherwise emit a default `minimal/low/medium/high` dial. api.x.ai only
+ * accepts `reasoning.effort` for {@link isGrokReasoningEffortCapable} ids —
+ * off-allowlist reasoners (`grok-code-fast-1`, `grok-build-0.1`,
+ * `grok-4.20-0309-reasoning`, …) 400 if the param is sent. SuperGrok
+ * (`xai-oauth`) already curates this via {@link mergeCuratedIntoModel}; paid
+ * `xai` rows come from stencil.so and need the same wire facts in the exported
+ * `models.json` so direct catalog readers do not present an unsupported dial.
+ *
+ * Explicit `compat.supportsReasoningEffort` / `omitReasoningEffort` win.
+ */
+export function applyXaiResponsesThinkingPolicy(model: ModelSpec<"openai-responses">): ModelSpec<"openai-responses"> {
+ const effortCapable = model.compat?.supportsReasoningEffort ?? isGrokReasoningEffortCapable(model.id);
+ const compat = {
+ ...(model.compat ?? {}),
+ supportsReasoningEffort: effortCapable,
+ omitReasoningEffort: model.compat?.omitReasoningEffort ?? !effortCapable,
+ };
+ if (effortCapable) {
+ compat.reasoningEffortMap = { ...xaiResponsesReasoningEffortMap(model.id) };
+ } else {
+ delete compat.reasoningEffortMap;
+ }
+ return { ...model, compat };
+}
// xai-oauth's /v1/models exposes no per-request output limit on the OAuth
// (Grok Build / SuperGrok) surface, so the curated catalog owns `maxTokens`
@@ -1381,9 +1424,9 @@ const XAI_REASONING_EFFORT_MAP = { minimal: "low" } as const;
// reasoning metadata and fetchOpenAICompatibleModels defaults reasoning to
// false). Caller supplies a `base` Model (either a freshly synthesised seed
// or a dynamic-fetched entry); the helper layers curated fields on top.
-// The `minimal -> low` effort clamp (XAI_REASONING_EFFORT_MAP) is always
-// merged in so dynamic-fetched models — which arrive without curated
-// compat keys — still get the clamp applyResponsesReasoningParams expects.
+// The effort remap from {@link xaiResponsesReasoningEffortMap} is merged
+// only onto effort-capable rows. Off-allowlist reasoners omit the wire
+// param, so a map on those specs is dead weight.
// The effort-dial pair (`supportsReasoningEffort`/`omitReasoningEffort`) is
// authoritative: a stale flag on `base` (previous snapshot or dynamic fetch)
// must not outlive an allowlist change in identity/family.ts.
@@ -1394,13 +1437,17 @@ function mergeCuratedIntoModel(
const effortCapable = curated.supportsReasoningEffort ?? isGrokReasoningEffortCapable(curated.id);
const compat = {
...(base.compat ?? {}),
- reasoningEffortMap: { ...XAI_REASONING_EFFORT_MAP, ...(base.compat?.reasoningEffortMap ?? {}) },
- includeEncryptedReasoning: base.compat?.includeEncryptedReasoning ?? false,
- filterReasoningHistory: base.compat?.filterReasoningHistory ?? true,
+ includeEncryptedReasoning: base.compat?.includeEncryptedReasoning ?? true,
+ filterReasoningHistory: false,
supportsImageDetailOriginal: base.compat?.supportsImageDetailOriginal ?? false,
omitReasoningEffort: !effortCapable,
supportsReasoningEffort: effortCapable,
};
+ if (effortCapable) {
+ compat.reasoningEffortMap = { ...xaiResponsesReasoningEffortMap(curated.id) };
+ } else {
+ delete compat.reasoningEffortMap;
+ }
return {
...base,
contextWindow: curated.contextWindow,
@@ -1502,7 +1549,7 @@ export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<"openai-res
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: curated.contextWindow,
maxTokens: curated.contextWindow,
- compat: { reasoningEffortMap: XAI_REASONING_EFFORT_MAP },
+ compat: { reasoningEffortMap: xaiResponsesReasoningEffortMap(curated.id) },
};
return mergeCuratedIntoModel(base, curated);
});
@@ -5713,6 +5760,15 @@ function openAiCompletionsDescriptor(
return simpleModelsDevDescriptor(modelsDevKey, providerId, "openai-completions", baseUrl, options);
}
+function openAiResponsesDescriptor(
+ modelsDevKey: string,
+ providerId: string,
+ baseUrl: string,
+ options: Omit = {},
+): ModelsDevProviderDescriptor {
+ return simpleModelsDevDescriptor(modelsDevKey, providerId, "openai-responses", baseUrl, options);
+}
+
function anthropicMessagesDescriptor(
modelsDevKey: string,
providerId: string,
@@ -5837,7 +5893,9 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor
defaultContextWindow: 131072,
}),
// --- xAI ---
- openAiCompletionsDescriptor("xai", "xai", "https://api.x.ai/v1"),
+ openAiResponsesDescriptor("xai", "xai", "https://api.x.ai/v1", {
+ transformModel: model => applyXaiResponsesThinkingPolicy(model as ModelSpec<"openai-responses">),
+ }),
// --- DeepSeek ---
openAiCompletionsDescriptor("deepseek", "deepseek", "https://api.deepseek.com", {
// Only ship the v4 family as built-ins; older deepseek-chat / deepseek-reasoner
diff --git a/packages/catalog/src/types.ts b/packages/catalog/src/types.ts
index 729ab4d89..5760208f9 100644
--- a/packages/catalog/src/types.ts
+++ b/packages/catalog/src/types.ts
@@ -363,6 +363,13 @@ export interface OpenAICompat {
* model id. Default: true. Issue #5606.
*/
supportsSamplingParams?: boolean;
+ /**
+ * Whether presence/frequency penalties and stop sequences may be sent.
+ * First-party xAI `/v1/responses` rejects penalty fields for every model.
+ * xAI reasoning models also reject them (and `stop`) on chat completions.
+ * When unset, auto-detected. Default: true.
+ */
+ supportsPenaltyAndStopParams?: boolean;
/** Always send a max-token field when the caller did not provide one. Default: auto-detected (Kimi-family models derive TPM limits from max_tokens). */
alwaysSendMaxTokens?: boolean;
/** Whether Responses-API tool-call/result history must be strictly paired. Default: auto-detected (Azure OpenAI, GitHub Copilot). */
@@ -578,6 +585,7 @@ export interface ResolvedOpenAISharedCompat {
reasoningEffortMap: Partial>;
supportsReasoningParams: boolean;
supportsSamplingParams: boolean;
+ supportsPenaltyAndStopParams: boolean;
thinkingFormat: OpenAIReasoningFormat;
/** Kimi Code transport selected by live per-model protocol metadata. */
kimiApiFormat?: OpenAICompat["kimiApiFormat"];
@@ -643,6 +651,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
| "reasoningEffortMap"
| "supportsReasoningParams"
| "supportsSamplingParams"
+ | "supportsPenaltyAndStopParams"
| "thinkingFormat"
| "kimiApiFormat"
| "reasoningDisableMode"
@@ -711,6 +720,12 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
strictResponsesPairing: boolean;
supportsImageDetailOriginal: boolean;
supportsObfuscationOptOut: boolean;
+ /**
+ * Whether `reasoning.summary` may be sent. First-party xAI `/v1/responses`
+ * rejects the field; handlers pass `null` so the wire omits it instead of
+ * filling `"auto"`.
+ */
+ supportsReasoningSummary: boolean;
streamIdleTimeoutMs?: number;
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
/** The model sits behind Vercel AI Gateway's Responses endpoint. */
diff --git a/packages/catalog/test/build.test.ts b/packages/catalog/test/build.test.ts
index cf9a672d2..db22929aa 100644
--- a/packages/catalog/test/build.test.ts
+++ b/packages/catalog/test/build.test.ts
@@ -223,12 +223,15 @@ describe("buildModel", () => {
});
});
-describe("xAI-OAuth Responses reasoning-effort suppression", () => {
- const grokResponsesSpec = (id: string): ModelSpec<"openai-responses"> => ({
+describe("xAI Responses reasoning-effort suppression", () => {
+ const grokResponsesSpec = (
+ id: string,
+ provider: "xai" | "xai-oauth" = "xai-oauth",
+ ): ModelSpec<"openai-responses"> => ({
id,
name: id,
api: "openai-responses",
- provider: "xai-oauth",
+ provider,
baseUrl: "https://api.x.ai/v1",
reasoning: true,
input: ["text"],
@@ -248,6 +251,47 @@ describe("xAI-OAuth Responses reasoning-effort suppression", () => {
expect(buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.3")).supportsReasoningEffort).toBe(true);
});
+ it("applies the same Responses dialect to paid xai and xai-oauth", () => {
+ const paid = buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.3", "xai"));
+ const oauth = buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.3", "xai-oauth"));
+ expect(paid.promptCacheSessionHeader).toBe("x-grok-conv-id");
+ expect(oauth.promptCacheSessionHeader).toBe("x-grok-conv-id");
+ expect(paid.includeEncryptedReasoning).toBe(true);
+ expect(oauth.includeEncryptedReasoning).toBe(true);
+ expect(paid.filterReasoningHistory).toBe(false);
+ expect(oauth.filterReasoningHistory).toBe(false);
+ expect(paid.supportsImageDetailOriginal).toBe(false);
+ expect(oauth.supportsImageDetailOriginal).toBe(false);
+ expect(paid.supportsReasoningEffort).toBe(true);
+ expect(oauth.supportsReasoningEffort).toBe(true);
+ expect(paid.reasoningEffortMap).toEqual({ minimal: "low", xhigh: "high", max: "high" });
+ expect(oauth.reasoningEffortMap).toEqual({ minimal: "low", xhigh: "high", max: "high" });
+ expect(
+ buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.20-multi-agent-0309", "xai")).reasoningEffortMap,
+ ).toEqual({ minimal: "low" });
+ expect(paid.supportsPenaltyAndStopParams).toBe(false);
+ expect(oauth.supportsPenaltyAndStopParams).toBe(false);
+ expect(paid.supportsReasoningSummary).toBe(false);
+ expect(oauth.supportsReasoningSummary).toBe(false);
+ });
+
+ it("suppresses penalty params on every first-party xAI Responses model", () => {
+ const reasoning = buildOpenAIResponsesCompat(grokResponsesSpec("grok-4.5", "xai"));
+ const nonReasoning = buildOpenAIResponsesCompat({
+ ...grokResponsesSpec("grok-2", "xai"),
+ reasoning: false,
+ });
+ expect(reasoning.supportsPenaltyAndStopParams).toBe(false);
+ expect(nonReasoning.supportsPenaltyAndStopParams).toBe(false);
+ });
+
+ it("omits effort for paid xai models off the Grok allowlist", () => {
+ const compat = buildOpenAIResponsesCompat(grokResponsesSpec("grok-code-fast-1", "xai"));
+ expect(compat.supportsReasoningEffort).toBe(false);
+ expect(compat.omitReasoningEffort).toBe(true);
+ expect(buildModel(grokResponsesSpec("grok-code-fast-1", "xai")).thinking).toBeUndefined();
+ });
+
it("lets an explicit compat.supportsReasoningEffort override the allowlist default", () => {
const compat = buildOpenAIResponsesCompat({
...grokResponsesSpec("grok-build"),
@@ -256,7 +300,7 @@ describe("xAI-OAuth Responses reasoning-effort suppression", () => {
expect(compat.supportsReasoningEffort).toBe(true);
});
- it("does not suppress effort for a non-xai-oauth provider with a grok-like id", () => {
+ it("does not suppress effort for a non-xAI provider with a grok-like id", () => {
const compat = buildOpenAIResponsesCompat({
...grokResponsesSpec("grok-build"),
provider: "openai",
diff --git a/packages/catalog/test/generated-policies.test.ts b/packages/catalog/test/generated-policies.test.ts
index 041973480..84b69e6e5 100644
--- a/packages/catalog/test/generated-policies.test.ts
+++ b/packages/catalog/test/generated-policies.test.ts
@@ -501,6 +501,43 @@ describe("generated model policies", () => {
expect(models[2]?.applyPatchToolType).toBeUndefined();
expect(models[3]?.applyPatchToolType).toBeUndefined();
});
+
+ it("strips paid xAI Responses effort dials for off-allowlist reasoners", () => {
+ const models: ModelSpec<"openai-responses">[] = [
+ createSpec({
+ id: "grok-code-fast-1",
+ api: "openai-responses",
+ provider: "xai",
+ thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
+ }),
+ createSpec({
+ id: "grok-4.5",
+ api: "openai-responses",
+ provider: "xai",
+ thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
+ }),
+ createSpec({
+ id: "grok-code-fast-1",
+ api: "openai-responses",
+ provider: "openrouter",
+ thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
+ }),
+ ];
+
+ applyGeneratedModelPolicies(models);
+
+ expect(models[0]?.thinking).toBeUndefined();
+ expect(models[0]?.compat).toMatchObject({
+ supportsReasoningEffort: false,
+ omitReasoningEffort: true,
+ });
+ expect(models[0]?.compat).not.toHaveProperty("reasoningEffortMap");
+ expect(models[1]?.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
+ expect(models[1]?.compat?.supportsReasoningEffort).toBe(true);
+ // Non-xAI hosts are outside this policy — no baked no-dial compat.
+ expect(models[2]?.thinking).toBeDefined();
+ expect(models[2]?.compat?.supportsReasoningEffort).toBeUndefined();
+ });
});
describe("applyOllamaCloudOutputCap", () => {
diff --git a/packages/catalog/test/identity-family.test.ts b/packages/catalog/test/identity-family.test.ts
index ffba1b095..52b57dedd 100644
--- a/packages/catalog/test/identity-family.test.ts
+++ b/packages/catalog/test/identity-family.test.ts
@@ -5,7 +5,9 @@ import {
isGeminiModelId,
isGlmVisionModelId,
isGrokModelId,
+ isGrokMultiAgentModelId,
isGrokReasoningEffortCapable,
+ isGrokXHighEffortCapable,
isKimiK26ModelId,
isKimiModelId,
isMinimaxM2FamilyModelId,
@@ -339,6 +341,7 @@ describe("isGrokReasoningEffortCapable", () => {
expect(isGrokReasoningEffortCapable("grok-4.20-multi-agent")).toBe(true);
expect(isGrokReasoningEffortCapable("xai-oauth/grok-4.3")).toBe(true);
expect(isGrokReasoningEffortCapable("xai-oauth/grok-4.5")).toBe(true);
+ expect(isGrokReasoningEffortCapable("xai-oauth/grok-4.6")).toBe(true);
expect(isGrokReasoningEffortCapable("openrouter/xai/grok-3-mini")).toBe(true);
});
@@ -349,3 +352,35 @@ describe("isGrokReasoningEffortCapable", () => {
expect(isGrokReasoningEffortCapable("")).toBe(false);
});
});
+
+describe("isGrokMultiAgentModelId", () => {
+ test("matches grok-4.20-multi-agent SKUs across namespaces", () => {
+ expect(isGrokMultiAgentModelId("grok-4.20-multi-agent")).toBe(true);
+ expect(isGrokMultiAgentModelId("grok-4.20-multi-agent-0309")).toBe(true);
+ expect(isGrokMultiAgentModelId("xai/grok-4.20-multi-agent-beta-latest")).toBe(true);
+ });
+
+ test("rejects other Grok ids", () => {
+ expect(isGrokMultiAgentModelId("grok-4.5")).toBe(false);
+ expect(isGrokMultiAgentModelId("grok-4.6")).toBe(false);
+ expect(isGrokMultiAgentModelId("grok-4.20-0309-reasoning")).toBe(false);
+ expect(isGrokMultiAgentModelId("")).toBe(false);
+ });
+});
+
+describe("isGrokXHighEffortCapable", () => {
+ test("matches grok-4.6 and multi-agent SKUs across namespaces", () => {
+ expect(isGrokXHighEffortCapable("grok-4.6")).toBe(true);
+ expect(isGrokXHighEffortCapable("xai/grok-4.6")).toBe(true);
+ expect(isGrokXHighEffortCapable("xai-oauth/grok-4.6")).toBe(true);
+ expect(isGrokXHighEffortCapable("grok-4.20-multi-agent-0309")).toBe(true);
+ });
+
+ test("rejects Grok SKUs that clamp leftover xhigh to high", () => {
+ expect(isGrokXHighEffortCapable("grok-4.5")).toBe(false);
+ expect(isGrokXHighEffortCapable("grok-4.3")).toBe(false);
+ expect(isGrokXHighEffortCapable("grok-3-mini")).toBe(false);
+ expect(isGrokXHighEffortCapable("grok-build")).toBe(false);
+ expect(isGrokXHighEffortCapable("")).toBe(false);
+ });
+});
diff --git a/packages/catalog/test/model-thinking.test.ts b/packages/catalog/test/model-thinking.test.ts
index e05ec9865..e55e767c0 100644
--- a/packages/catalog/test/model-thinking.test.ts
+++ b/packages/catalog/test/model-thinking.test.ts
@@ -884,6 +884,65 @@ describe("model thinking runtime helpers", () => {
expect(() => requireSupportedEffort(opus46, Effort.XHigh)).toThrow(/not supported/);
});
+ it("does not expose xhigh on first-party Grok 4.5 Responses models", () => {
+ const paid = createModel({
+ id: "grok-4.5",
+ api: "openai-responses",
+ provider: "xai",
+ baseUrl: "https://api.x.ai/v1",
+ });
+ const oauth = createModel({
+ id: "grok-4.5",
+ api: "openai-responses",
+ provider: "xai-oauth",
+ baseUrl: "https://api.x.ai/v1",
+ });
+
+ expect(paid.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
+ expect(oauth.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
+ expect(() => requireSupportedEffort(paid, Effort.XHigh)).toThrow(/not supported/);
+ });
+
+ it("exposes xhigh on first-party Grok 4.6 Responses models", () => {
+ const paid = createModel({
+ id: "grok-4.6",
+ api: "openai-responses",
+ provider: "xai",
+ baseUrl: "https://api.x.ai/v1",
+ });
+ const oauth = createModel({
+ id: "grok-4.6",
+ api: "openai-responses",
+ provider: "xai-oauth",
+ baseUrl: "https://api.x.ai/v1",
+ });
+
+ expect(paid.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
+ expect(oauth.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
+ expect(requireSupportedEffort(paid, Effort.XHigh)).toBe(Effort.XHigh);
+ expect(paid.compat.reasoningEffortMap?.xhigh).toBeUndefined();
+ });
+
+ it("exposes xhigh on first-party Grok multi-agent Responses models", () => {
+ const paid = createModel({
+ id: "grok-4.20-multi-agent-beta-latest",
+ api: "openai-responses",
+ provider: "xai",
+ baseUrl: "https://api.x.ai/v1",
+ });
+ const oauth = createModel({
+ id: "grok-4.20-multi-agent-0309",
+ api: "openai-responses",
+ provider: "xai-oauth",
+ baseUrl: "https://api.x.ai/v1",
+ });
+
+ expect(paid.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
+ expect(oauth.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
+ expect(requireSupportedEffort(paid, Effort.XHigh)).toBe(Effort.XHigh);
+ expect(paid.compat.reasoningEffortMap?.xhigh).toBeUndefined();
+ });
+
it("rejects effort requests against un-built reasoning specs", () => {
const spec = {
id: "broken-reasoner",
diff --git a/packages/catalog/test/xai-api-key-responses.test.ts b/packages/catalog/test/xai-api-key-responses.test.ts
new file mode 100644
index 000000000..c11849cec
--- /dev/null
+++ b/packages/catalog/test/xai-api-key-responses.test.ts
@@ -0,0 +1,103 @@
+import { describe, expect, it } from "bun:test";
+import * as fs from "node:fs/promises";
+import * as os from "node:os";
+import * as path from "node:path";
+import { resolveProviderModels } from "@oh-my-pi/pi-catalog/model-manager";
+import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
+import { CATALOG_PROVIDERS } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
+import { xaiModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
+import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
+
+const XAI_RESPONSES_SPEC: ModelSpec<"openai-responses"> = {
+ 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: 500_000,
+ maxTokens: 500_000,
+};
+const XAI_COMPLETIONS_SPEC: ModelSpec<"openai-completions"> = {
+ id: "grok-4.5",
+ name: "Grok 4.5",
+ api: "openai-completions",
+ provider: "xai",
+ baseUrl: "https://api.x.ai/v1",
+ reasoning: true,
+ input: ["text", "image"],
+ cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
+ contextWindow: 500_000,
+ maxTokens: 500_000,
+};
+
+describe("paid xai (XAI_API_KEY) Responses contract", () => {
+ it("registers xai on the catalog Responses discovery path", () => {
+ const entry = CATALOG_PROVIDERS.find(provider => provider.id === "xai");
+ expect(entry, "xai catalog descriptor").toBeDefined();
+ expect(entry!.defaultModel).toBe("grok-4.5");
+ expect(entry!.envVars).toContain("XAI_API_KEY");
+ const options = xaiModelManagerOptions({ apiKey: "test-key" });
+ expect(options.providerId).toBe("xai");
+ expect(options.fetchDynamicModels, "live /v1/models overlay").toBeTypeOf("function");
+ expect(options.dropCachedModelIdsOnStaticMismatch).toEqual(getBundledModels("xai").map(model => model.id));
+ expect(options.dropCachedModelIdsOnStaticMismatch).toContain("grok-4.5");
+ });
+
+ it("bundles every paid xai chat model on openai-responses", () => {
+ const models = getBundledModels("xai");
+ expect(models.length).toBeGreaterThan(0);
+ for (const model of models) {
+ expect(model.api, `${model.provider}/${model.id}`).toBe("openai-responses");
+ expect(model.baseUrl).toBe("https://api.x.ai/v1");
+ }
+ });
+
+ it("drops stale Chat Completions cache rows so Responses takes effect immediately", async () => {
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-catalog-xai-completions-cache-"));
+ const dbPath = path.join(tempDir, "models.db");
+ try {
+ await resolveProviderModels(
+ {
+ providerId: "xai",
+ staticModels: [XAI_COMPLETIONS_SPEC],
+ fetchDynamicModels: async () => [XAI_COMPLETIONS_SPEC],
+ cacheDbPath: dbPath,
+ },
+ "online",
+ );
+
+ let fetches = 0;
+ const migrated = await resolveProviderModels(
+ {
+ ...xaiModelManagerOptions(),
+ staticModels: [XAI_RESPONSES_SPEC],
+ cacheDbPath: dbPath,
+ fetchDynamicModels: async () => {
+ fetches += 1;
+ return [XAI_RESPONSES_SPEC];
+ },
+ },
+ "online-if-uncached",
+ );
+
+ expect(fetches).toBe(1);
+ expect(migrated.models.find(model => model.id === "grok-4.5")?.api).toBe("openai-responses");
+
+ const offline = await resolveProviderModels(
+ {
+ ...xaiModelManagerOptions(),
+ staticModels: [XAI_RESPONSES_SPEC],
+ cacheDbPath: dbPath,
+ fetchDynamicModels: async () => null,
+ },
+ "offline",
+ );
+ expect(offline.models.find(model => model.id === "grok-4.5")?.api).toBe("openai-responses");
+ } finally {
+ await fs.rm(tempDir, { recursive: true, force: true });
+ }
+ });
+});
diff --git a/packages/catalog/test/xai-oauth-bundle.test.ts b/packages/catalog/test/xai-oauth-bundle.test.ts
new file mode 100644
index 000000000..534cc488c
--- /dev/null
+++ b/packages/catalog/test/xai-oauth-bundle.test.ts
@@ -0,0 +1,86 @@
+import { describe, expect, it } from "bun:test";
+import MODELS_JSON from "@oh-my-pi/pi-catalog/models.json" with { type: "json" };
+import { CATALOG_PROVIDERS, DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
+import { buildXaiOAuthStaticSeed } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
+import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
+
+// Pins the invariant: bundled `models.json` carries every entry the runtime
+// curated catalog (XAI_OAUTH_CURATED_MODELS, surfaced via
+// buildXaiOAuthStaticSeed) emits. Without this, editing the curated list
+// without regenerating `models.json` silently regresses the boot-time
+// default-model resolver — the registry sees the runtime seed only after
+// `refresh()`, but interactive boot resolves the persisted default
+// synchronously from `#loadModels()`, which reads only `models.json`.
+//
+// Failure here means: run `bun run gen:models` and commit the diff.
+describe("xai-oauth bundled catalog (regression)", () => {
+ const bundled =
+ (MODELS_JSON as unknown as Record>>)["xai-oauth"] ?? {};
+ const seed = buildXaiOAuthStaticSeed();
+
+ it("defaults SuperGrok selection to grok-4.5", () => {
+ const entry = CATALOG_PROVIDERS.find(provider => provider.id === "xai-oauth");
+ expect(entry?.defaultModel).toBe("grok-4.5");
+ expect(DEFAULT_MODEL_PER_PROVIDER["xai-oauth"]).toBe("grok-4.5");
+ expect(bundled["grok-4.5"], "xai-oauth/grok-4.5 must be bundled for the default").toBeDefined();
+ });
+
+ it("bundles every curated id", () => {
+ const seededIds = seed.map(model => model.id).sort();
+ const bundledIds = Object.keys(bundled).sort();
+ expect(bundledIds).toEqual(seededIds);
+ });
+
+ for (const seededModel of seed) {
+ it(`matches contract for ${seededModel.id}`, () => {
+ const bundledEntry = bundled[seededModel.id];
+ expect(bundledEntry, `xai-oauth/${seededModel.id} missing from models.json`).toBeDefined();
+ expect(bundledEntry.id).toBe(seededModel.id);
+ expect(bundledEntry.name).toBe(seededModel.name);
+ expect(bundledEntry.provider).toBe("xai-oauth");
+ expect(bundledEntry.api).toBe("openai-responses");
+ expect(bundledEntry.contextWindow).toBe(seededModel.contextWindow);
+ expect(bundledEntry.reasoning).toBe(seededModel.reasoning);
+ // Input modality must survive both the curated seed and the bundle.
+ // Without this the static fallback used on offline boot strips
+ // vision capability silently (Codex PR #1127 review).
+ expect(bundledEntry.input).toEqual(seededModel.input);
+ expect(bundledEntry.compat?.supportsReasoningEffort).toBe(seededModel.compat?.supportsReasoningEffort);
+ });
+ }
+
+ // Absolute contract for the user-specified SuperGrok addition. The parity
+ // loop above can't catch a value typo (e.g. 2_000_000) or a flipped
+ // reasoning flag — both sides regenerate from the same seed together — so
+ // pin the literal attributes here.
+ it("exposes grok-composer-2.5-fast as a non-reasoning 200K text model", () => {
+ const composer = seed.find(model => model.id === "grok-composer-2.5-fast");
+ expect(composer, "grok-composer-2.5-fast must be in the SuperGrok curated seed").toBeDefined();
+ expect(composer!.reasoning).toBe(false);
+ expect(composer!.contextWindow).toBe(200_000);
+ expect(composer!.input).toEqual(["text"]);
+ // The bundled models.json entry is byte-identical to the generator's
+ // deterministic xai-oauth output: gen:models pushes
+ // buildXaiOAuthStaticSeed() (offline — xai-oauth has no upstream catalog
+ // source) and applyGeneratedModelPolicies(), so a regen reproduces these
+ // exact bytes; only unrelated other-provider network churn was excluded
+ // to keep the diff scoped. Pin its zero-cost invariant (overlay-stable
+ // for the SuperGrok subscription), which the parity loop above never
+ // compares. (maxTokens is pinned by the maxTokens-equals-contextWindow
+ // test below.)
+ expect(bundled["grok-composer-2.5-fast"]?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
+ });
+
+ // The OAuth surface's /v1/models reports no per-request output limit, so the
+ // curated catalog owns maxTokens — set to mirror each model's contextWindow
+ // (the openai-responses wire still clamps the actual request to
+ // OPENAI_MAX_OUTPUT_TOKENS). Pin maxTokens === contextWindow on both the
+ // static-seed and bundled paths so a null placeholder can
+ // never silently leak back into the bundle.
+ it("sets maxTokens equal to contextWindow for every xai-oauth model", () => {
+ for (const model of seed) {
+ expect(model.maxTokens, `seed ${model.id} maxTokens`).toBe(model.contextWindow);
+ expect(bundled[model.id]?.maxTokens, `bundled ${model.id} maxTokens`).toBe(model.contextWindow);
+ }
+ });
+});
diff --git a/packages/catalog/test/xai-responses-thinking-policy.test.ts b/packages/catalog/test/xai-responses-thinking-policy.test.ts
new file mode 100644
index 000000000..2843552dc
--- /dev/null
+++ b/packages/catalog/test/xai-responses-thinking-policy.test.ts
@@ -0,0 +1,172 @@
+import { describe, expect, it } from "bun:test";
+import { Effort } from "@oh-my-pi/pi-catalog/effort";
+import MODELS_JSON from "@oh-my-pi/pi-catalog/models.json" with { type: "json" };
+import {
+ MODELS_DEV_PROVIDER_DESCRIPTORS,
+ mapModelsDevToModels,
+} from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
+import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
+import { applyGeneratedModelPolicies } from "../scripts/generated-policies";
+
+const XAI_MODELS_DEV_FIXTURE = {
+ xai: {
+ models: {
+ "grok-4.5": {
+ name: "Grok 4.5",
+ tool_call: true,
+ reasoning: true,
+ modalities: { input: ["text", "image"] },
+ limit: { context: 500_000, output: 500_000 },
+ cost: { input: 2, output: 6, cache_read: 0.3 },
+ },
+ "grok-4.6": {
+ name: "Grok 4.6",
+ tool_call: true,
+ reasoning: true,
+ modalities: { input: ["text", "image"] },
+ limit: { context: 500_000, output: 500_000 },
+ cost: { input: 2, output: 6, cache_read: 0.5 },
+ },
+ "grok-code-fast-1": {
+ name: "Grok Code Fast 1",
+ tool_call: true,
+ reasoning: true,
+ modalities: { input: ["text"] },
+ limit: { context: 256_000, output: 10_000 },
+ cost: { input: 0.2, output: 1.5 },
+ },
+ "grok-build-0.1": {
+ name: "Grok Build 0.1",
+ tool_call: true,
+ reasoning: true,
+ modalities: { input: ["text", "image"] },
+ limit: { context: 256_000, output: 256_000 },
+ cost: { input: 0, output: 0 },
+ },
+ "grok-4.20-0309-reasoning": {
+ name: "Grok 4.20 (Reasoning)",
+ tool_call: true,
+ reasoning: true,
+ modalities: { input: ["text", "image"] },
+ limit: { context: 2_000_000, output: 64_000 },
+ cost: { input: 2, output: 6 },
+ },
+ "grok-2": {
+ name: "Grok 2",
+ tool_call: true,
+ reasoning: false,
+ modalities: { input: ["text"] },
+ limit: { context: 131_072, output: 8192 },
+ cost: { input: 2, output: 10 },
+ },
+ "grok-4.20-multi-agent-beta-latest": {
+ name: "Grok 4.20 (Multi-Agent)",
+ tool_call: true,
+ reasoning: true,
+ modalities: { input: ["text"] },
+ limit: { context: 2_000_000, output: 64_000 },
+ cost: { input: 2, output: 6 },
+ },
+ },
+ },
+};
+
+describe("paid xAI Responses thinking policy", () => {
+ it("bakes the effort-dial allowlist on stencil.so → openai-responses mapping", () => {
+ const mapped = mapModelsDevToModels(XAI_MODELS_DEV_FIXTURE, MODELS_DEV_PROVIDER_DESCRIPTORS).filter(
+ model => model.provider === "xai",
+ );
+ const byId = Object.fromEntries(mapped.map(model => [model.id, model]));
+
+ expect(byId["grok-4.5"]?.api).toBe("openai-responses");
+ expect(byId["grok-4.5"]?.compat).toMatchObject({
+ supportsReasoningEffort: true,
+ omitReasoningEffort: false,
+ reasoningEffortMap: { minimal: "low" },
+ });
+ for (const id of ["grok-code-fast-1", "grok-build-0.1", "grok-4.20-0309-reasoning"] as const) {
+ expect(byId[id]?.reasoning, id).toBe(true);
+ expect(byId[id]?.compat, id).toMatchObject({
+ supportsReasoningEffort: false,
+ omitReasoningEffort: true,
+ });
+ expect(byId[id]?.compat, id).not.toHaveProperty("reasoningEffortMap");
+ }
+ expect(byId["grok-2"]?.compat).toMatchObject({
+ supportsReasoningEffort: false,
+ omitReasoningEffort: true,
+ });
+ expect(byId["grok-2"]?.compat).not.toHaveProperty("reasoningEffortMap");
+ });
+
+ it("strips stale thinking dials from off-allowlist paid xAI reasoners during generation", () => {
+ const mapped = mapModelsDevToModels(XAI_MODELS_DEV_FIXTURE, MODELS_DEV_PROVIDER_DESCRIPTORS).filter(
+ model => model.provider === "xai",
+ );
+ // Snapshot-era Completions rows still carry a default effort ladder after the
+ // api flip; the generator must not re-emit that dial for Responses.
+ const snapshotStale = mapped.find(model => model.id === "grok-code-fast-1");
+ expect(snapshotStale).toBeDefined();
+ snapshotStale!.thinking = { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] };
+
+ applyGeneratedModelPolicies(mapped);
+ const byId = Object.fromEntries(mapped.map(model => [model.id, model]));
+
+ expect(byId["grok-4.5"]?.thinking).toEqual({
+ mode: "effort",
+ efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
+ effortMap: { minimal: "low" },
+ });
+ expect(byId["grok-4.6"]?.thinking).toEqual({
+ mode: "effort",
+ efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh],
+ effortMap: { minimal: "low" },
+ });
+ expect(byId["grok-4.6"]?.compat).toMatchObject({
+ supportsReasoningEffort: true,
+ reasoningEffortMap: { minimal: "low" },
+ });
+ expect(byId["grok-4.6"]?.compat).not.toMatchObject({
+ reasoningEffortMap: { xhigh: "high" },
+ });
+ expect(byId["grok-4.20-multi-agent-beta-latest"]?.thinking).toEqual({
+ mode: "effort",
+ efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh],
+ effortMap: { minimal: "low" },
+ });
+ expect(byId["grok-4.20-multi-agent-beta-latest"]?.compat).toMatchObject({
+ supportsReasoningEffort: true,
+ reasoningEffortMap: { minimal: "low" },
+ });
+ expect(byId["grok-4.20-multi-agent-beta-latest"]?.compat).not.toMatchObject({
+ reasoningEffortMap: { xhigh: "high" },
+ });
+ for (const id of ["grok-code-fast-1", "grok-build-0.1", "grok-4.20-0309-reasoning"] as const) {
+ expect(byId[id]?.reasoning, id).toBe(true);
+ expect(byId[id]?.thinking, id).toBeUndefined();
+ expect(byId[id]?.compat, id).toMatchObject({ supportsReasoningEffort: false });
+ }
+ });
+
+ it("exports no-dial rows in the bundled models.json snapshot", () => {
+ const bundled =
+ (MODELS_JSON as unknown as Record>>).xai ?? {};
+ for (const id of ["grok-code-fast-1", "grok-build-0.1", "grok-4.20-0309-reasoning"] as const) {
+ expect(bundled[id], `xai/${id} missing from models.json`).toBeDefined();
+ expect(bundled[id]?.reasoning, id).toBe(true);
+ expect(bundled[id]?.thinking, id).toBeUndefined();
+ expect(bundled[id]?.compat?.supportsReasoningEffort, id).toBe(false);
+ }
+ expect(bundled["grok-4.5"]?.thinking?.efforts).toEqual([Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]);
+ expect(bundled["grok-4.5"]?.thinking?.efforts).not.toContain(Effort.XHigh);
+ expect(bundled["grok-4.5"]?.compat?.supportsReasoningEffort).toBe(true);
+ expect(bundled["grok-4.6"]?.thinking?.efforts).toContain(Effort.XHigh);
+ expect(bundled["grok-4.6"]?.compat).not.toMatchObject({
+ reasoningEffortMap: { xhigh: "high" },
+ });
+ expect(bundled["grok-4.20-multi-agent-beta-latest"]?.thinking?.efforts).toContain(Effort.XHigh);
+ expect(bundled["grok-4.20-multi-agent-beta-latest"]?.compat).not.toMatchObject({
+ reasoningEffortMap: { xhigh: "high" },
+ });
+ });
+});
diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 5597d222b..f61f4fc3d 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -2,6 +2,16 @@
## [Unreleased]
+### Changed
+
+- Routed paid xAI models (`XAI_API_KEY` / `xai/…`) through the Responses API used by SuperGrok OAuth instead of Chat Completions.
+- Changed the default model for `XAI_API_KEY` (`xai`) from `grok-4-fast-non-reasoning` to `grok-4.5`.
+- 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`. Explicit `xai-oauth/grok-4.5` still works with that paid key.
+- Stopped sending presence/frequency penalties and stop sequences to xAI reasoning models such as `grok-4.5`, which reject them.
+
## [17.3.4] - 2026-08-14
### Changed
diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts
index f2dd6f1b5..50aa6e713 100644
--- a/packages/coding-agent/src/config/model-registry.ts
+++ b/packages/coding-agent/src/config/model-registry.ts
@@ -1667,17 +1667,22 @@ export class ModelRegistry {
*
* Side-effect-free and synchronous: a command-backed key (`!cmd`) counts as
* configured by its presence alone — the program is NOT executed — and OAuth
- * tokens are NOT refreshed (`authStorage.hasAuth`). This is what keeps the
+ * tokens are NOT refreshed (`authStorage.hasResolvableAuth`). This is what keeps the
* model-switch pre-flight off the event loop's hot path; the real key
* (command execution + OAuth refresh) is resolved lazily per request via
* {@link ModelRegistry.resolver}.
+ *
+ * Cross-provider env aliases count here (`xai-oauth` can borrow `XAI_API_KEY`)
+ * so an explicit `xai-oauth/…` selector does not fail with "No API key".
+ * Default-model availability still uses {@link AuthStorage.hasAuth}, which
+ * ignores that alias so SuperGrok is not auto-selected from a paid key.
*/
hasConfiguredAuth(model: Model): boolean {
const keyConfig = this.#customProviderApiKeys.get(model.provider);
return (
isCommandConfigValue(keyConfig) ||
this.#keylessProviders.has(model.provider) ||
- this.authStorage.hasAuth(model.provider)
+ this.authStorage.hasResolvableAuth(model.provider)
);
}
diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts
index f8f665441..6d0e2d86d 100644
--- a/packages/coding-agent/test/model-resolver.test.ts
+++ b/packages/coding-agent/test/model-resolver.test.ts
@@ -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", () => {