Add Grok 4.20 reasoning and non-reasoning to xAI model catalog (#50772)

Merged via squash.

Prepared head SHA: 095e645ea58b2259b25c923aeaf11bbcb2990c8f
Co-authored-by: Jaaneek <25470423+Jaaneek@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo
This commit is contained in:
Jaaneek 2026-03-20 19:28:30 +00:00 committed by GitHub
parent f6b3245a7b
commit 916f496b51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 161 additions and 22 deletions

View File

@ -152,6 +152,7 @@ Docs: https://docs.openclaw.ai
- Telegram: stabilize pairing/session/forum routing and reply formatting tests (#50155) Thanks @joshavant. - Telegram: stabilize pairing/session/forum routing and reply formatting tests (#50155) Thanks @joshavant.
- Hardening: refresh stale device pairing requests and pending metadata (#50695) Thanks @smaeljaish771 and @joshavant. - Hardening: refresh stale device pairing requests and pending metadata (#50695) Thanks @smaeljaish771 and @joshavant.
- Gateway: harden OpenResponses file-context escaping (#50782) Thanks @YLChen-007 and @joshavant. - Gateway: harden OpenResponses file-context escaping (#50782) Thanks @YLChen-007 and @joshavant.
- xAI/models: rename the bundled Grok 4.20 catalog entries to the GA IDs and normalize saved deprecated beta IDs at runtime so existing configs and sessions keep resolving. (#50772) thanks @Jaaneek
### Fixes ### Fixes

View File

@ -34,8 +34,7 @@ OpenClaw now includes these xAI model families out of the box:
- `grok-4`, `grok-4-0709` - `grok-4`, `grok-4-0709`
- `grok-4-fast-reasoning`, `grok-4-fast-non-reasoning` - `grok-4-fast-reasoning`, `grok-4-fast-non-reasoning`
- `grok-4-1-fast-reasoning`, `grok-4-1-fast-non-reasoning` - `grok-4-1-fast-reasoning`, `grok-4-1-fast-non-reasoning`
- `grok-4.20-experimental-beta-0304-reasoning` - `grok-4.20-reasoning`, `grok-4.20-non-reasoning`
- `grok-4.20-experimental-beta-0304-non-reasoning`
- `grok-code-fast-1` - `grok-code-fast-1`
The plugin also forward-resolves newer `grok-4*` and `grok-code-fast*` ids when The plugin also forward-resolves newer `grok-4*` and `grok-code-fast*` ids when

View File

@ -59,14 +59,14 @@ const XAI_MODEL_CATALOG = [
contextWindow: XAI_LARGE_CONTEXT_WINDOW, contextWindow: XAI_LARGE_CONTEXT_WINDOW,
}, },
{ {
id: "grok-4.20-experimental-beta-0304-reasoning", id: "grok-4.20-reasoning",
name: "Grok 4.20 Experimental Beta 0304 (Reasoning)", name: "Grok 4.20 (Reasoning)",
reasoning: true, reasoning: true,
contextWindow: XAI_LARGE_CONTEXT_WINDOW, contextWindow: XAI_LARGE_CONTEXT_WINDOW,
}, },
{ {
id: "grok-4.20-experimental-beta-0304-non-reasoning", id: "grok-4.20-non-reasoning",
name: "Grok 4.20 Experimental Beta 0304 (Non-Reasoning)", name: "Grok 4.20 (Non-Reasoning)",
reasoning: false, reasoning: false,
contextWindow: XAI_LARGE_CONTEXT_WINDOW, contextWindow: XAI_LARGE_CONTEXT_WINDOW,
}, },

View File

@ -16,8 +16,21 @@ describe("xai provider models", () => {
}); });
}); });
it("publishes Grok 4.20 reasoning and non-reasoning models", () => {
expect(resolveXaiCatalogEntry("grok-4.20-reasoning")).toMatchObject({
id: "grok-4.20-reasoning",
reasoning: true,
contextWindow: 2_000_000,
});
expect(resolveXaiCatalogEntry("grok-4.20-non-reasoning")).toMatchObject({
id: "grok-4.20-non-reasoning",
reasoning: false,
contextWindow: 2_000_000,
});
});
it("marks current Grok families as modern while excluding multi-agent ids", () => { it("marks current Grok families as modern while excluding multi-agent ids", () => {
expect(isModernXaiModel("grok-4.20-experimental-beta-0304-reasoning")).toBe(true); expect(isModernXaiModel("grok-4.20-reasoning")).toBe(true);
expect(isModernXaiModel("grok-code-fast-1")).toBe(true); expect(isModernXaiModel("grok-code-fast-1")).toBe(true);
expect(isModernXaiModel("grok-3-mini-fast")).toBe(false); expect(isModernXaiModel("grok-3-mini-fast")).toBe(false);
expect(isModernXaiModel("grok-4.20-multi-agent-experimental-beta-0304")).toBe(false); expect(isModernXaiModel("grok-4.20-multi-agent-experimental-beta-0304")).toBe(false);
@ -40,7 +53,7 @@ describe("xai provider models", () => {
providerId: "xai", providerId: "xai",
ctx: { ctx: {
provider: "xai", provider: "xai",
modelId: "grok-4.20-experimental-beta-0304-reasoning", modelId: "grok-4.20-reasoning",
modelRegistry: { find: () => null } as never, modelRegistry: { find: () => null } as never,
providerConfig: { providerConfig: {
api: "openai-completions", api: "openai-completions",
@ -59,7 +72,7 @@ describe("xai provider models", () => {
}); });
expect(grok420).toMatchObject({ expect(grok420).toMatchObject({
provider: "xai", provider: "xai",
id: "grok-4.20-experimental-beta-0304-reasoning", id: "grok-4.20-reasoning",
api: "openai-completions", api: "openai-completions",
baseUrl: "https://api.x.ai/v1", baseUrl: "https://api.x.ai/v1",
reasoning: true, reasoning: true,

View File

@ -1,3 +1,4 @@
import { normalizeXaiModelId } from "openclaw/plugin-sdk/provider-models";
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search"; import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
export const XAI_WEB_SEARCH_ENDPOINT = "https://api.x.ai/v1/responses"; export const XAI_WEB_SEARCH_ENDPOINT = "https://api.x.ai/v1/responses";
@ -79,7 +80,7 @@ export function resolveXaiSearchConfig(searchConfig?: Record<string, unknown>):
export function resolveXaiWebSearchModel(searchConfig?: Record<string, unknown>): string { export function resolveXaiWebSearchModel(searchConfig?: Record<string, unknown>): string {
const config = resolveXaiSearchConfig(searchConfig); const config = resolveXaiSearchConfig(searchConfig);
return typeof config.model === "string" && config.model.trim() return typeof config.model === "string" && config.model.trim()
? config.model.trim() ? normalizeXaiModelId(config.model.trim())
: XAI_DEFAULT_WEB_SEARCH_MODEL; : XAI_DEFAULT_WEB_SEARCH_MODEL;
} }

View File

@ -44,6 +44,19 @@ describe("xai web search config resolution", () => {
); );
}); });
it("normalizes deprecated grok 4.20 beta model ids to GA ids", () => {
expect(
resolveXaiWebSearchModel({
grok: { model: "grok-4.20-experimental-beta-0304-reasoning" },
}),
).toBe("grok-4.20-reasoning");
expect(
resolveXaiWebSearchModel({
grok: { model: "grok-4.20-experimental-beta-0304-non-reasoning" },
}),
).toBe("grok-4.20-non-reasoning");
});
it("defaults inlineCitations to false", () => { it("defaults inlineCitations to false", () => {
expect(resolveXaiInlineCitations({})).toBe(false); expect(resolveXaiInlineCitations({})).toBe(false);
expect(resolveXaiInlineCitations(undefined)).toBe(false); expect(resolveXaiInlineCitations(undefined)).toBe(false);

View File

@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { normalizeXaiModelId } from "./model-id-normalization.js";
describe("normalizeXaiModelId", () => {
it("maps deprecated grok 4.20 beta ids to GA ids", () => {
expect(normalizeXaiModelId("grok-4.20-experimental-beta-0304-reasoning")).toBe(
"grok-4.20-reasoning",
);
expect(normalizeXaiModelId("grok-4.20-experimental-beta-0304-non-reasoning")).toBe(
"grok-4.20-non-reasoning",
);
});
it("leaves current xai model ids unchanged", () => {
expect(normalizeXaiModelId("grok-4.20-reasoning")).toBe("grok-4.20-reasoning");
expect(normalizeXaiModelId("grok-4")).toBe("grok-4");
});
});

View File

@ -21,3 +21,13 @@ export function normalizeGoogleModelId(id: string): string {
} }
return id; return id;
} }
export function normalizeXaiModelId(id: string): string {
if (id === "grok-4.20-experimental-beta-0304-reasoning") {
return "grok-4.20-reasoning";
}
if (id === "grok-4.20-experimental-beta-0304-non-reasoning") {
return "grok-4.20-non-reasoning";
}
return id;
}

View File

@ -194,6 +194,15 @@ describe("model-selection", () => {
defaultProvider: "google", defaultProvider: "google",
expected: { provider: "google", model: "gemini-3.1-flash-lite-preview" }, expected: { provider: "google", model: "gemini-3.1-flash-lite-preview" },
}, },
{
name: "normalizes deprecated xai grok 4.20 beta ids",
variants: [
"xai/grok-4.20-experimental-beta-0304-reasoning",
"grok-4.20-experimental-beta-0304-reasoning",
],
defaultProvider: "xai",
expected: { provider: "xai", model: "grok-4.20-reasoning" },
},
{ {
name: "keeps OpenAI codex refs on the openai provider", name: "keeps OpenAI codex refs on the openai provider",
variants: ["openai/gpt-5.3-codex", "gpt-5.3-codex"], variants: ["openai/gpt-5.3-codex", "gpt-5.3-codex"],

View File

@ -14,7 +14,7 @@ import {
} from "./agent-scope.js"; } from "./agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
import type { ModelCatalogEntry } from "./model-catalog.js"; import type { ModelCatalogEntry } from "./model-catalog.js";
import { normalizeGoogleModelId } from "./model-id-normalization.js"; import { normalizeGoogleModelId, normalizeXaiModelId } from "./model-id-normalization.js";
import { splitTrailingAuthProfile } from "./model-ref-profile.js"; import { splitTrailingAuthProfile } from "./model-ref-profile.js";
import { import {
findNormalizedProviderKey, findNormalizedProviderKey,
@ -121,6 +121,9 @@ function normalizeProviderModelId(provider: string, model: string): string {
if (provider === "google" || provider === "google-vertex") { if (provider === "google" || provider === "google-vertex") {
return normalizeGoogleModelId(model); return normalizeGoogleModelId(model);
} }
if (provider === "xai") {
return normalizeXaiModelId(model);
}
// OpenRouter-native models (e.g. "openrouter/aurora-alpha") need the full // OpenRouter-native models (e.g. "openrouter/aurora-alpha") need the full
// "openrouter/<name>" as the model ID sent to the API. Models from external // "openrouter/<name>" as the model ID sent to the API. Models from external
// providers already contain a slash (e.g. "anthropic/claude-sonnet-4-5") and // providers already contain a slash (e.g. "anthropic/claude-sonnet-4-5") and

View File

@ -9,7 +9,7 @@ import { isRecord } from "../utils.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { discoverBedrockModels } from "./bedrock-discovery.js"; import { discoverBedrockModels } from "./bedrock-discovery.js";
import { normalizeGoogleModelId } from "./model-id-normalization.js"; import { normalizeGoogleModelId, normalizeXaiModelId } from "./model-id-normalization.js";
import { resolveOllamaApiBase } from "./models-config.providers.discovery.js"; import { resolveOllamaApiBase } from "./models-config.providers.discovery.js";
export { buildKimiCodingProvider } from "../../extensions/kimi-coding/provider-catalog.js"; export { buildKimiCodingProvider } from "../../extensions/kimi-coding/provider-catalog.js";
export { buildKilocodeProvider } from "../../extensions/kilocode/provider-catalog.js"; export { buildKilocodeProvider } from "../../extensions/kilocode/provider-catalog.js";
@ -42,7 +42,7 @@ import {
} from "./model-auth-markers.js"; } from "./model-auth-markers.js";
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js"; import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
export { resolveOllamaApiBase } from "./models-config.providers.discovery.js"; export { resolveOllamaApiBase } from "./models-config.providers.discovery.js";
export { normalizeGoogleModelId }; export { normalizeGoogleModelId, normalizeXaiModelId };
type ModelsConfig = NonNullable<OpenClawConfig["models"]>; type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string]; export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];

View File

@ -341,6 +341,15 @@ describe("web_search grok config resolution", () => {
expect(resolveGrokModel({ model: "grok-4-fast" })).toBe("grok-4-fast"); expect(resolveGrokModel({ model: "grok-4-fast" })).toBe("grok-4-fast");
}); });
it("normalizes deprecated grok 4.20 beta ids to GA ids", () => {
expect(resolveGrokModel({ model: "grok-4.20-experimental-beta-0304-reasoning" })).toBe(
"grok-4.20-reasoning",
);
expect(resolveGrokModel({ model: "grok-4.20-experimental-beta-0304-non-reasoning" })).toBe(
"grok-4.20-non-reasoning",
);
});
it("falls back to default model", () => { it("falls back to default model", () => {
expect(resolveGrokModel({})).toBe("grok-4-1-fast"); expect(resolveGrokModel({})).toBe("grok-4-1-fast");
}); });

View File

@ -9,6 +9,8 @@ vi.mock("../../agents/model-catalog.js", () => ({
{ provider: "kimi", id: "kimi-code", name: "Kimi Code" }, { provider: "kimi", id: "kimi-code", name: "Kimi Code" },
{ provider: "openai", id: "gpt-4o-mini", name: "GPT-4o mini" }, { provider: "openai", id: "gpt-4o-mini", name: "GPT-4o mini" },
{ provider: "openai", id: "gpt-4o", name: "GPT-4o" }, { provider: "openai", id: "gpt-4o", name: "GPT-4o" },
{ provider: "xai", id: "grok-4", name: "Grok 4" },
{ provider: "xai", id: "grok-4.20-reasoning", name: "Grok 4.20 (Reasoning)" },
]), ]),
})); }));
@ -263,6 +265,45 @@ describe("createModelSelectionState respects session model override", () => {
expect(state.provider).toBe(defaultProvider); expect(state.provider).toBe(defaultProvider);
expect(state.model).toBe("deepseek-v3-4bit-mlx"); expect(state.model).toBe("deepseek-v3-4bit-mlx");
}); });
it("normalizes deprecated xai beta session overrides before allowlist checks", async () => {
const cfg = {
agents: {
defaults: {
model: {
primary: "xai/grok-4",
},
models: {
"xai/grok-4": {},
"xai/grok-4.20-experimental-beta-0304-reasoning": {},
},
},
},
} as OpenClawConfig;
const sessionKey = "agent:main:telegram:group:123:topic:99";
const sessionEntry = makeEntry({
providerOverride: "xai",
modelOverride: "grok-4.20-experimental-beta-0304-reasoning",
});
const sessionStore = { [sessionKey]: sessionEntry };
const state = await createModelSelectionState({
cfg,
agentCfg: cfg.agents?.defaults,
sessionEntry,
sessionStore,
sessionKey,
defaultProvider: "xai",
defaultModel: "grok-4",
provider: "xai",
model: "grok-4",
hasModelDirective: false,
});
expect(state.provider).toBe("xai");
expect(state.model).toBe("grok-4.20-reasoning");
expect(state.resetModelOverride).toBe(false);
});
}); });
describe("createModelSelectionState resolveDefaultReasoningLevel", () => { describe("createModelSelectionState resolveDefaultReasoningLevel", () => {

View File

@ -6,6 +6,7 @@ import {
buildAllowedModelSet, buildAllowedModelSet,
type ModelAliasIndex, type ModelAliasIndex,
modelKey, modelKey,
normalizeModelRef,
normalizeProviderId, normalizeProviderId,
resolveModelRefFromString, resolveModelRefFromString,
resolveReasoningDefault, resolveReasoningDefault,
@ -326,7 +327,8 @@ export async function createModelSelectionState(params: {
const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider; const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider;
const overrideModel = sessionEntry.modelOverride?.trim(); const overrideModel = sessionEntry.modelOverride?.trim();
if (overrideModel) { if (overrideModel) {
const key = modelKey(overrideProvider, overrideModel); const normalizedOverride = normalizeModelRef(overrideProvider, overrideModel);
const key = modelKey(normalizedOverride.provider, normalizedOverride.model);
if (allowedModelKeys.size > 0 && !allowedModelKeys.has(key)) { if (allowedModelKeys.size > 0 && !allowedModelKeys.has(key)) {
const { updated } = applyModelOverrideToSessionEntry({ const { updated } = applyModelOverrideToSessionEntry({
entry: sessionEntry, entry: sessionEntry,
@ -356,11 +358,14 @@ export async function createModelSelectionState(params: {
// the regular session/parent model override behavior. // the regular session/parent model override behavior.
const skipStoredOverride = params.hasResolvedHeartbeatModelOverride === true; const skipStoredOverride = params.hasResolvedHeartbeatModelOverride === true;
if (storedOverride?.model && !skipStoredOverride) { if (storedOverride?.model && !skipStoredOverride) {
const candidateProvider = storedOverride.provider || defaultProvider; const normalizedStoredOverride = normalizeModelRef(
const key = modelKey(candidateProvider, storedOverride.model); storedOverride.provider || defaultProvider,
storedOverride.model,
);
const key = modelKey(normalizedStoredOverride.provider, normalizedStoredOverride.model);
if (allowedModelKeys.size === 0 || allowedModelKeys.has(key)) { if (allowedModelKeys.size === 0 || allowedModelKeys.has(key)) {
provider = candidateProvider; provider = normalizedStoredOverride.provider;
model = storedOverride.model; model = normalizedStoredOverride.model;
} }
} }

View File

@ -101,7 +101,7 @@ describe("model-pricing-cache", () => {
], ],
}, },
hooks: { hooks: {
mappings: [{ model: "xai/grok-4" }], mappings: [{ model: "xai/grok-4.20-experimental-beta-0304-reasoning" }],
}, },
tools: { tools: {
subagents: { model: { primary: "zai/glm-5" } }, subagents: { model: { primary: "zai/glm-5" } },
@ -130,7 +130,7 @@ describe("model-pricing-cache", () => {
}, },
}, },
{ {
id: "x-ai/grok-4", id: "x-ai/grok-4.20-experimental-beta-0304-reasoning",
pricing: { pricing: {
prompt: "0.000002", prompt: "0.000002",
completion: "0.00001", completion: "0.00001",
@ -172,12 +172,25 @@ describe("model-pricing-cache", () => {
cacheRead: 0.3, cacheRead: 0.3,
cacheWrite: 0, cacheWrite: 0,
}); });
expect(getCachedGatewayModelPricing({ provider: "xai", model: "grok-4" })).toEqual({ expect(
getCachedGatewayModelPricing({
provider: "xai",
model: "grok-4.20-experimental-beta-0304-reasoning",
}),
).toEqual({
input: 2, input: 2,
output: 10, output: 10,
cacheRead: 0, cacheRead: 0,
cacheWrite: 0, cacheWrite: 0,
}); });
expect(getCachedGatewayModelPricing({ provider: "xai", model: "grok-4.20-reasoning" })).toEqual(
{
input: 2,
output: 10,
cacheRead: 0,
cacheWrite: 0,
},
);
expect(getCachedGatewayModelPricing({ provider: "zai", model: "glm-5" })).toEqual({ expect(getCachedGatewayModelPricing({ provider: "zai", model: "glm-5" })).toEqual({
input: 1, input: 1,
output: 4, output: 4,

View File

@ -7,7 +7,7 @@ import {
resolveModelRefFromString, resolveModelRefFromString,
type ModelRef, type ModelRef,
} from "../agents/model-selection.js"; } from "../agents/model-selection.js";
import { normalizeGoogleModelId } from "../agents/models-config.providers.js"; import { normalizeGoogleModelId, normalizeXaiModelId } from "../agents/models-config.providers.js";
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js"; import { createSubsystemLogger } from "../logging/subsystem.js";
@ -155,6 +155,9 @@ function canonicalizeOpenRouterLookupId(id: string): string {
if (provider === "google") { if (provider === "google") {
model = normalizeGoogleModelId(model); model = normalizeGoogleModelId(model);
} }
if (provider === "x-ai") {
model = normalizeXaiModelId(model);
}
return `${provider}/${model}`; return `${provider}/${model}`;
} }

View File

@ -24,6 +24,7 @@ export {
XAI_TOOL_SCHEMA_PROFILE, XAI_TOOL_SCHEMA_PROFILE,
} from "../agents/model-compat.js"; } from "../agents/model-compat.js";
export { normalizeProviderId } from "../agents/provider-id.js"; export { normalizeProviderId } from "../agents/provider-id.js";
export { normalizeXaiModelId } from "../agents/model-id-normalization.js";
export { cloneFirstTemplateModel } from "../plugins/provider-model-helpers.js"; export { cloneFirstTemplateModel } from "../plugins/provider-model-helpers.js";
export { export {