diff --git a/docs/providers/xai.md b/docs/providers/xai.md index 8640586c4530..473742d21e1f 100644 --- a/docs/providers/xai.md +++ b/docs/providers/xai.md @@ -187,6 +187,14 @@ metadata without joining the published inventory. Their pricing remains unknown, recorded as zero until the manifest includes them. Zero is an unavailable estimate, not a claim that the provider charges nothing. +Thinking levels follow xAI's +[documented release rule](https://docs.x.ai/developers/model-capabilities/text/reasoning) +rather than a fixed model list. Grok 4.5 and later accept `low`, `medium`, and +`high`; Grok 4.6 and later add `xhigh`. The default is `high`, and reasoning cannot +be turned off. A newer release id such as `grok-4.8`, its `-latest` alias, or a +dated snapshot gets these levels and image input before the manifest lists it. +Variant ids such as `-fast` keep reasoning effort off. + ## Feature coverage The bundled plugin maps supported xAI APIs onto OpenClaw's shared provider and diff --git a/extensions/xai/model-id.ts b/extensions/xai/model-id.ts index 9c190a37aae1..d81f6581979e 100644 --- a/extensions/xai/model-id.ts +++ b/extensions/xai/model-id.ts @@ -1,17 +1,33 @@ // Xai plugin module implements model id behavior. + +// A Grok release id: plain, `-latest`, or a dated snapshot (grok-4.7, grok-5, grok-4.8-0115). +// Other suffixes (fast, mini, reasoning variants) name models with their own contracts. +const XAI_GROK_RELEASE_ID = /^grok-(\d+)(?:\.(\d+))?(?:-(?:latest|\d{4}))?$/u; + +/** + * xAI documents reasoning effort from Grok 4.5 and xhigh "on grok-4.6 and later", so + * compare release numbers: new Grok releases work before the manifest lists them. + */ +export function isXaiGrokReleaseAtLeast(id: string, minimum: readonly [number, number]): boolean { + const match = XAI_GROK_RELEASE_ID.exec(normalizeXaiModelId(id.trim().toLowerCase())); + if (!match) { + return false; + } + const major = Number(match[1]); + const minor = Number(match[2] ?? 0); + // Grok 4.20 predates Grok 4.3 despite its number and has no reasoning effort control. + if (major === 4 && minor === 20) { + return false; + } + return major > minimum[0] || (major === minimum[0] && minor >= minimum[1]); +} + export function isXaiXhighModelId(id: string): boolean { - const normalized = normalizeXaiModelId(id.trim().toLowerCase()); - return normalized === "grok-4.7" || normalized === "grok-4.6"; + return isXaiGrokReleaseAtLeast(id, [4, 6]); } export function isXaiFrontierModelId(id: string): boolean { - const normalized = normalizeXaiModelId(id.trim().toLowerCase()); - return ( - normalized === "grok-4.7" || - normalized === "grok-4.6" || - normalized === "grok-4.5" || - normalized.startsWith("grok-4.5-") - ); + return isXaiGrokReleaseAtLeast(id, [4, 5]); } export function normalizeXaiModelId(id: string): string { diff --git a/extensions/xai/provider-policy-api.test.ts b/extensions/xai/provider-policy-api.test.ts index 63e334e78646..a4fbe5cd014b 100644 --- a/extensions/xai/provider-policy-api.test.ts +++ b/extensions/xai/provider-policy-api.test.ts @@ -50,6 +50,10 @@ describe("xai provider thinking policy", () => { ["x-ai", "grok-4.7"], ["xai", "grok-4.6"], ["x-ai", "grok-4.6"], + // Releases newer than the manifest follow xAI's "grok-4.6 and later" rule. + ["xai", "grok-4.8"], + ["xai", "grok-4.8-latest"], + ["xai", "grok-5"], ])("exposes xhigh reasoning for %s/%s", (provider, modelId) => { expect(resolveThinkingProfile({ provider, modelId })).toEqual({ levels: [{ id: "low" }, { id: "medium" }, { id: "high" }, { id: "xhigh" }], @@ -95,6 +99,10 @@ describe("xai provider thinking policy", () => { ["x-ai", "grok-build-0.1"], ["x-ai", "grok-4.20-0309-reasoning"], ["x-ai", "grok-4.20-beta-latest-reasoning"], + // Grok 4.20 predates 4.3, and variant suffixes are separate model contracts. + ["xai", "grok-4.20"], + ["xai", "grok-4-0709"], + ["xai", "grok-4.8-fast"], ])("does not advertise configurable reasoning for %s/%s", (provider, modelId) => { expect(resolveThinkingProfile({ provider, modelId })).toEqual({ levels: [{ id: "off" }], diff --git a/extensions/xai/responses-tool-policy.test.ts b/extensions/xai/responses-tool-policy.test.ts index 69aafe3ff5b6..615a8c1be172 100644 --- a/extensions/xai/responses-tool-policy.test.ts +++ b/extensions/xai/responses-tool-policy.test.ts @@ -39,25 +39,25 @@ const callers = [ }, ]; -it.each(callers)( - "keeps the $name default request within supported reasoning efforts", - async ({ run }) => { - const request = vi.fn(async () => - Response.json({ output_text: "fixture result" }), - ); - vi.stubGlobal("fetch", withFetchPreconnect(request)); - await run("grok-4.7"); - expect(request).toHaveBeenCalledOnce(); - const init = request.mock.calls[0]?.[1]; - expect(init).toEqual(expect.objectContaining({ body: expect.any(String) })); - const body = new Request("https://api.x.ai/v1/responses", init); - expect(await body.json()).toMatchObject({ - model: "grok-4.7", - store: false, - reasoning: { effort: "low" }, - }); - }, -); +// Releases newer than the setup default keep its tool effort. +it.each( + callers.flatMap(({ name, run }) => + ["grok-4.7", "grok-4.8"].map((model) => ({ name, run, model })), + ), +)("keeps the $name $model request within supported reasoning efforts", async ({ run, model }) => { + const request = vi.fn(async () => Response.json({ output_text: "fixture result" })); + vi.stubGlobal("fetch", withFetchPreconnect(request)); + await run(model); + expect(request).toHaveBeenCalledOnce(); + const init = request.mock.calls[0]?.[1]; + expect(init).toEqual(expect.objectContaining({ body: expect.any(String) })); + const body = new Request("https://api.x.ai/v1/responses", init); + expect(await body.json()).toMatchObject({ + model, + store: false, + reasoning: { effort: "low" }, + }); +}); it.each(callers)("preserves an explicit model and omitted effort for $name", async ({ run }) => { const request = vi.fn(async () => Response.json({ output_text: "fixture result" })); diff --git a/extensions/xai/runtime-model-compat.test.ts b/extensions/xai/runtime-model-compat.test.ts index 08e130c51382..d5e3c8e118cc 100644 --- a/extensions/xai/runtime-model-compat.test.ts +++ b/extensions/xai/runtime-model-compat.test.ts @@ -24,7 +24,7 @@ describe("xai runtime model compat", () => { }); }); - it.each(["grok-4.7", "grok-4.6"])("preserves %s xhigh reasoning", (id) => { + it.each(["grok-4.8", "grok-4.7", "grok-4.6"])("preserves %s xhigh reasoning", (id) => { const model = applyXaiRuntimeModelCompat({ id, provider: "xai", diff --git a/extensions/xai/src/responses-tool-shared.ts b/extensions/xai/src/responses-tool-shared.ts index 3c2e4929d7ed..05781e1044f5 100644 --- a/extensions/xai/src/responses-tool-shared.ts +++ b/extensions/xai/src/responses-tool-shared.ts @@ -8,6 +8,7 @@ import { } from "openclaw/plugin-sdk/string-coerce-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { resolveXaiCatalogEntry } from "../model-definitions.js"; +import { isXaiGrokReleaseAtLeast } from "../model-id.js"; import { applyXaiRuntimeModelCompat } from "../runtime-model-compat.js"; import type { XaiWebSearchResponse } from "./web-search-response.types.js"; @@ -64,9 +65,7 @@ export function resolveXaiToolDefaultReasoningEffort( preferred: "none" | "low", ): "none" | "low" | undefined { // Per-model tool defaults must survive changes to the setup default. - return model === "grok-4.3" || model === "grok-4.6" || model === "grok-4.7" - ? preferred - : undefined; + return model === "grok-4.3" || isXaiGrokReleaseAtLeast(model, [4, 6]) ? preferred : undefined; } function buildXaiResponsesToolBody(params: { diff --git a/extensions/xai/supported-capabilities.test.ts b/extensions/xai/supported-capabilities.test.ts index a28defe2dca4..f56849c0caf6 100644 --- a/extensions/xai/supported-capabilities.test.ts +++ b/extensions/xai/supported-capabilities.test.ts @@ -34,6 +34,7 @@ it.each([ { id: "grok-3-mini-fast", reasoning: true, input: ["text"], maxTokens: 64_000 }, { id: "grok-4.20-reasoning", reasoning: true, input: ["text", "image"], maxTokens: 30_000 }, { id: "grok-4.20-non-reasoning", reasoning: false, input: ["text", "image"], maxTokens: 30_000 }, + { id: "grok-4.8", reasoning: true, input: ["text", "image"], maxTokens: 64_000 }, ])( "keeps supported capabilities separate from inventory and pricing for $id", ({ id, ...capabilities }) => {