feat(ai): route xAI models through Responses and default to Grok 4.6 (#8124)

* feat(ai): route xAI models through Responses and default to Grok 4.6

Send built-in xAI catalog models through the Responses API with
store: false and include reasoning.encrypted_content so encrypted
reasoning is requested and replayed, including future models.dev
additions. Thinking levels come from models.dev reasoning_options
(grok-4.6 exposes xhigh); models without verified options
(grok-build-0.1) keep reasoning always on and never send unsupported
"none"/"minimal" efforts. Narrow the xAI provider to the Responses API,
send pi's runtime User-Agent on xAI requests for provider-side
attribution, and make Grok 4.6 the default xAI model.

* fix(ai): drop duplicate xAI encrypted-reasoning tests

Azure already covers the shared replay path; keep the xAI-specific include-without-effort case next to the existing request tests.

---------

Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com>
This commit is contained in:
Milosz Jankiewicz 2026-08-16 16:47:38 +01:00 • committed by GitHub
parent 086c32e745
commit 70e878d4cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 185 additions and 25 deletions

View file

@ -794,7 +794,7 @@ Many models support thinking/reasoning capabilities where they can show their in
const model = models.getModel('anthropic', 'claude-sonnet-4-5')!;
// or models.getModel('openai', 'gpt-5-mini');
// or models.getModel('google', 'gemini-2.5-flash');
// or models.getModel('xai', 'grok-4.5');
// or models.getModel('xai', 'grok-4.6');
// Check if model supports reasoning
if (model.reasoning) {

View file

@ -410,7 +410,6 @@ const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([
"gpt-5.6-terra",
"gpt-5.6-luna",
]);
const XAI_RESPONSES_MODEL_ID = "grok-4.5";
const XAI_BUILTIN_EXCLUDED_MODEL_IDS = new Set([
"grok-3",
"grok-3-fast",
@ -418,10 +417,6 @@ const XAI_BUILTIN_EXCLUDED_MODEL_IDS = new Set([
"grok-4.20-0309-reasoning",
"grok-code-fast-1",
]);
const XAI_RESPONSES_EFFORT_LEVEL_MAP = {
off: null,
minimal: null,
} as const;
const XAI_RESPONSES_COMPAT: OpenAIResponsesCompat = {
supportsLongCacheRetention: false,
};
@ -823,8 +818,10 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
) {
mergeThinkingLevelMap(model, { off: "none" });
}
if (model.provider === "xai" && model.api === "openai-responses" && model.id === XAI_RESPONSES_MODEL_ID) {
mergeThinkingLevelMap(model, XAI_RESPONSES_EFFORT_LEVEL_MAP);
// xAI models without verified effort options (e.g. grok-build-0.1) must not
// send the undocumented "none"/"minimal" efforts.
if (model.provider === "xai" && model.api === "openai-responses" && model.thinkingLevelMap === undefined) {
mergeThinkingLevelMap(model, { off: null, minimal: null });
}
if (supportsOpenAiXhigh(model.id)) {
mergeThinkingLevelMap(model, { xhigh: "xhigh" });
@ -1632,15 +1629,14 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
for (const [modelId, model] of Object.entries(data.xai.models)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
const useResponsesApi = modelId === XAI_RESPONSES_MODEL_ID;
models.push({
id: modelId,
name: m.name || modelId,
api: useResponsesApi ? "openai-responses" : "openai-completions",
api: "openai-responses",
provider: "xai",
baseUrl: "https://api.x.ai/v1",
...(useResponsesApi ? { compat: { ...XAI_RESPONSES_COMPAT } } : {}),
compat: { ...XAI_RESPONSES_COMPAT },
reasoning: m.reasoning === true,
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: {
@ -1946,10 +1942,10 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
// Claude 4.x and 5.x models route to Anthropic Messages API
const isCopilotClaude = /^claude-(haiku|sonnet|opus)-[45]([.\-]|$)/.test(modelId);
// Grok 4.5, gpt-5, oswe, and MAI-Code models are only served through
// Grok, gpt-5, oswe, and MAI-Code models are only served through
// the Copilot /responses endpoint.
const needsResponsesApi =
modelId === "grok-4.5" ||
modelId.startsWith("grok-") ||
modelId.startsWith("gpt-5") ||
modelId.startsWith("oswe") ||
modelId.startsWith("mai-");

View file

@ -39,6 +39,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { shortHash } from "../utils/hash.ts";
import { headersToRecord } from "../utils/headers.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { forcePiUserAgent } from "../utils/pi-user-agent.ts";
import { getProviderEnvValue } from "../utils/provider-env.ts";
import { retryProviderRequest } from "../utils/provider-retry.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
@ -670,6 +671,10 @@ function createClient(
Object.assign(headers, optionsHeaders);
}
if (model.provider === "xai") {
forcePiUserAgent(headers);
}
return new OpenAI({
apiKey,
baseURL: model.baseUrl,

View file

@ -19,6 +19,7 @@ import { splitDeferredTools } from "../utils/deferred-tools.ts";
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { forcePiUserAgent } from "../utils/pi-user-agent.ts";
import { getProviderEnvValue } from "../utils/provider-env.ts";
import { retryProviderRequest } from "../utils/provider-retry.ts";
import { createGrammarToolInputProperties } from "./constrained-sampling.ts";
@ -246,6 +247,10 @@ function createClient(
Object.assign(headers, optionsHeaders);
}
if (model.provider === "xai") {
forcePiUserAgent(headers);
}
return new OpenAI({
apiKey,
baseURL: model.baseUrl,

View file

@ -1,11 +1,10 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
import { loadXaiOAuth } from "../auth/oauth/load.ts";
import { createProvider, type Provider } from "../models.ts";
import { XAI_MODELS } from "./xai.models.ts";
export function xaiProvider(): Provider<"openai-completions" | "openai-responses"> {
export function xaiProvider(): Provider<"openai-responses"> {
return createProvider({
id: "xai",
name: "xAI",
@ -20,9 +19,6 @@ export function xaiProvider(): Provider<"openai-completions" | "openai-responses
}),
},
models: Object.values(XAI_MODELS),
api: {
"openai-completions": openAICompletionsApi(),
"openai-responses": openAIResponsesApi(),
},
api: openAIResponsesApi(),
});
}

View file

@ -1,4 +1,5 @@
import type * as NodeOs from "node:os";
import type { ProviderHeaders } from "../types.ts";
type ProcessWithOsBuiltinModule = typeof process & {
getBuiltinModule?: (id: "node:os") => typeof NodeOs;
@ -17,3 +18,10 @@ const nodeOs = loadNodeOs();
export function getPiUserAgent(): string {
return nodeOs ? `pi (${nodeOs.platform()} ${nodeOs.release()}; ${nodeOs.arch()})` : "pi (browser)";
}
export function forcePiUserAgent(headers: ProviderHeaders): void {
for (const name of Object.keys(headers)) {
if (name.toLowerCase() === "user-agent") delete headers[name];
}
headers["User-Agent"] = getPiUserAgent();
}

View file

@ -6,7 +6,9 @@ it("derives model API, ID, and provider literals from grouped model data", () =>
expectTypeOf(XAI_MODELS["grok-4.5"].api).toEqualTypeOf<"openai-responses">();
expectTypeOf(XAI_MODELS["grok-4.5"].id).toEqualTypeOf<"grok-4.5">();
expectTypeOf(XAI_MODELS["grok-4.5"].provider).toEqualTypeOf<"xai">();
expectTypeOf(XAI_MODELS["grok-4.3"].api).toEqualTypeOf<"openai-completions">();
expectTypeOf(XAI_MODELS["grok-4.6"].api).toEqualTypeOf<"openai-responses">();
expectTypeOf(XAI_MODELS["grok-4.6"].id).toEqualTypeOf<"grok-4.6">();
expectTypeOf(XAI_MODELS["grok-4.3"].api).toEqualTypeOf<"openai-responses">();
});
it("routes GitHub Copilot Grok 4.5 through the Responses API", () => {

View file

@ -542,7 +542,7 @@ describe("Generate E2E Tests", () => {
});
});
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider (grok-4.3 via OpenAI Completions)", () => {
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider (grok-4.3 via OpenAI Responses)", () => {
const llm = getModel("xai", "grok-4.3");
it("should complete basic text generation", { retry: 3 }, async () => {

View file

@ -147,6 +147,12 @@ describe("getSupportedThinkingLevels", () => {
expect(getSupportedThinkingLevels(model!)).toContain("max");
});
it("includes xhigh but not off or max for xAI Grok 4.6", () => {
const model = getModel("xai", "grok-4.6");
expect(model).toBeDefined();
expect(getSupportedThinkingLevels(model!)).toEqual(["low", "medium", "high", "xhigh"]);
});
it("includes xhigh and max but not off for Bedrock Claude Fable 5", () => {
const model = getModel("amazon-bedrock", "global.anthropic.claude-fable-5");
expect(model).toBeDefined();

View file

@ -1,10 +1,15 @@
import { arch, platform, release } from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts";
import type { OpenAIResponsesOptions } from "../src/api/openai-responses.ts";
import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts";
import { getSupportedThinkingLevels } from "../src/models.ts";
import { XAI_MODELS } from "../src/providers/xai.models.ts";
import { xaiProvider } from "../src/providers/xai.ts";
import type { Context, Model } from "../src/types.ts";
const PI_USER_AGENT = `pi (${platform()} ${release()}; ${arch()})`;
type CapturedRequest = {
url: string;
headers: Headers;
@ -72,10 +77,14 @@ describe("xAI Responses provider", () => {
}
});
it("uses Responses with low/medium/high efforts only for Grok 4.5", () => {
expect(XAI_MODELS["grok-4.5"].api).toBe("openai-responses");
it("routes every built-in xAI model through Responses", () => {
for (const model of Object.values(XAI_MODELS)) {
expect(model.api, model.id).toBe("openai-responses");
}
expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.5"])).toEqual(["low", "medium", "high"]);
expect(XAI_MODELS["grok-4.3"].api).toBe("openai-completions");
expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.6"])).toEqual(["low", "medium", "high", "xhigh"]);
expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.3"])).toEqual(["off", "low", "medium", "high"]);
expect(getSupportedThinkingLevels(XAI_MODELS["grok-build-0.1"])).toEqual(["low", "medium", "high"]);
});
it("uses /responses with bearer auth and xAI-compatible request fields", async () => {
@ -95,6 +104,7 @@ describe("xAI Responses provider", () => {
expect(captured.url).toBe("https://api.x.ai/v1/responses");
expect(captured.headers.get("authorization")).toBe("Bearer xai-test-token");
expect(captured.headers.get("user-agent")).toBe(PI_USER_AGENT);
expect(captured.headers.get("session_id")).toBe("pi-session-123");
expect(captured.body).toMatchObject({
model: "grok-4.5",
@ -114,4 +124,132 @@ describe("xAI Responses provider", () => {
]),
);
});
it("requests encrypted reasoning without an effort override", async () => {
const captured = await captureRequest(
XAI_MODELS["grok-4.5"],
{ messages: [{ role: "user", content: "hello", timestamp: 1 }] },
{ apiKey: "xai-test-token" },
);
expect(captured.body).toMatchObject({
model: "grok-4.5",
store: false,
include: ["reasoning.encrypted_content"],
});
expect(captured.body).not.toHaveProperty("reasoning");
});
it("uses /responses for Grok 4.6 with xhigh effort and encrypted reasoning", async () => {
const captured = await captureRequest(
XAI_MODELS["grok-4.6"],
{
systemPrompt: "You are a careful coding assistant.",
messages: [{ role: "user", content: "hello", timestamp: 1 }],
},
{
apiKey: "xai-test-token",
reasoningEffort: "xhigh",
},
);
expect(captured.url).toBe("https://api.x.ai/v1/responses");
expect(captured.body).toMatchObject({
model: "grok-4.6",
store: false,
stream: true,
reasoning: { effort: "xhigh" },
include: ["reasoning.encrypted_content"],
});
});
it("uses /responses for Grok 4.3", async () => {
const captured = await captureRequest(
XAI_MODELS["grok-4.3"],
{
messages: [{ role: "user", content: "hello", timestamp: 1 }],
},
{
apiKey: "xai-test-token",
reasoningEffort: "low",
},
);
expect(captured.url).toBe("https://api.x.ai/v1/responses");
expect(captured.body).toMatchObject({
model: "grok-4.3",
store: false,
include: ["reasoning.encrypted_content"],
reasoning: { effort: "low" },
});
});
it("keeps the SDK User-Agent for non-xAI Responses requests", async () => {
let userAgent: string | null = null;
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
userAgent = new Request(input, init).headers.get("user-agent");
return completedResponse();
});
const openaiModel: Model<"openai-responses"> = {
...XAI_MODELS["grok-4.5"],
provider: "openai",
baseUrl: "https://api.openai.com/v1",
};
const result = await streamOpenAIResponses(
openaiModel,
{ messages: [{ role: "user", content: "hello", timestamp: 1 }] },
{ apiKey: "test-token" },
).result();
expect(result.stopReason, result.errorMessage).toBe("stop");
expect(userAgent).not.toBeNull();
expect(userAgent).not.toBe(PI_USER_AGENT);
});
it("forces pi's User-Agent on custom xAI Completions models over caller headers", async () => {
let userAgent: string | null = null;
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
userAgent = new Request(input, init).headers.get("user-agent");
const chunks = [
{ id: "chatcmpl-ua", choices: [{ delta: { content: "ok" }, finish_reason: null, index: 0 }] },
{
id: "chatcmpl-ua",
choices: [{ delta: {}, finish_reason: "stop", index: 0 }],
usage: {
prompt_tokens: 1,
completion_tokens: 1,
prompt_tokens_details: { cached_tokens: 0 },
completion_tokens_details: { reasoning_tokens: 0 },
},
},
];
const body = `${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}`).join("\n\n")}\n\ndata: [DONE]\n\n`;
return new Response(body, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
});
const customModel: Model<"openai-completions"> = {
id: "grok-custom",
name: "Grok Custom",
api: "openai-completions",
provider: "xai",
baseUrl: "https://api.x.ai/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384,
};
const result = await streamOpenAICompletions(
customModel,
{ messages: [{ role: "user", content: "hello", timestamp: 1 }] },
{ apiKey: "xai-test-token", headers: { "User-Agent": "custom-agent" } },
).result();
expect(result.stopReason, result.errorMessage).toBe("stop");
expect(userAgent).toBe(PI_USER_AGENT);
});
});

View file

@ -32,7 +32,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
"github-copilot": "gpt-5.4",
openrouter: "moonshotai/kimi-k2.6",
"vercel-ai-gateway": "zai/glm-5.1",
xai: "grok-4.5",
xai: "grok-4.6",
groq: "openai/gpt-oss-120b",
cerebras: "zai-glm-4.7",
zai: "glm-5.3",

View file

@ -723,6 +723,10 @@ describe("default model selection", () => {
expect(defaultModelPerProvider["vercel-ai-gateway"]).toBe("zai/glm-5.1");
});
test("xai default tracks current model", () => {
expect(defaultModelPerProvider.xai).toBe("grok-4.6");
});
test("qwen token plan individual default tracks current model", () => {
expect(defaultModelPerProvider["qwen-token-plan-individual"]).toBe("qwen3.8-max");
});