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:
parent
f6b3245a7b
commit
916f496b51
@ -152,6 +152,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
||||
@ -34,8 +34,7 @@ OpenClaw now includes these xAI model families out of the box:
|
||||
- `grok-4`, `grok-4-0709`
|
||||
- `grok-4-fast-reasoning`, `grok-4-fast-non-reasoning`
|
||||
- `grok-4-1-fast-reasoning`, `grok-4-1-fast-non-reasoning`
|
||||
- `grok-4.20-experimental-beta-0304-reasoning`
|
||||
- `grok-4.20-experimental-beta-0304-non-reasoning`
|
||||
- `grok-4.20-reasoning`, `grok-4.20-non-reasoning`
|
||||
- `grok-code-fast-1`
|
||||
|
||||
The plugin also forward-resolves newer `grok-4*` and `grok-code-fast*` ids when
|
||||
|
||||
@ -59,14 +59,14 @@ const XAI_MODEL_CATALOG = [
|
||||
contextWindow: XAI_LARGE_CONTEXT_WINDOW,
|
||||
},
|
||||
{
|
||||
id: "grok-4.20-experimental-beta-0304-reasoning",
|
||||
name: "Grok 4.20 Experimental Beta 0304 (Reasoning)",
|
||||
id: "grok-4.20-reasoning",
|
||||
name: "Grok 4.20 (Reasoning)",
|
||||
reasoning: true,
|
||||
contextWindow: XAI_LARGE_CONTEXT_WINDOW,
|
||||
},
|
||||
{
|
||||
id: "grok-4.20-experimental-beta-0304-non-reasoning",
|
||||
name: "Grok 4.20 Experimental Beta 0304 (Non-Reasoning)",
|
||||
id: "grok-4.20-non-reasoning",
|
||||
name: "Grok 4.20 (Non-Reasoning)",
|
||||
reasoning: false,
|
||||
contextWindow: XAI_LARGE_CONTEXT_WINDOW,
|
||||
},
|
||||
|
||||
@ -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", () => {
|
||||
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-3-mini-fast")).toBe(false);
|
||||
expect(isModernXaiModel("grok-4.20-multi-agent-experimental-beta-0304")).toBe(false);
|
||||
@ -40,7 +53,7 @@ describe("xai provider models", () => {
|
||||
providerId: "xai",
|
||||
ctx: {
|
||||
provider: "xai",
|
||||
modelId: "grok-4.20-experimental-beta-0304-reasoning",
|
||||
modelId: "grok-4.20-reasoning",
|
||||
modelRegistry: { find: () => null } as never,
|
||||
providerConfig: {
|
||||
api: "openai-completions",
|
||||
@ -59,7 +72,7 @@ describe("xai provider models", () => {
|
||||
});
|
||||
expect(grok420).toMatchObject({
|
||||
provider: "xai",
|
||||
id: "grok-4.20-experimental-beta-0304-reasoning",
|
||||
id: "grok-4.20-reasoning",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
reasoning: true,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { normalizeXaiModelId } from "openclaw/plugin-sdk/provider-models";
|
||||
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
|
||||
|
||||
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 {
|
||||
const config = resolveXaiSearchConfig(searchConfig);
|
||||
return typeof config.model === "string" && config.model.trim()
|
||||
? config.model.trim()
|
||||
? normalizeXaiModelId(config.model.trim())
|
||||
: XAI_DEFAULT_WEB_SEARCH_MODEL;
|
||||
}
|
||||
|
||||
|
||||
@ -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", () => {
|
||||
expect(resolveXaiInlineCitations({})).toBe(false);
|
||||
expect(resolveXaiInlineCitations(undefined)).toBe(false);
|
||||
|
||||
18
src/agents/model-id-normalization.test.ts
Normal file
18
src/agents/model-id-normalization.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@ -21,3 +21,13 @@ export function normalizeGoogleModelId(id: string): string {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@ -194,6 +194,15 @@ describe("model-selection", () => {
|
||||
defaultProvider: "google",
|
||||
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",
|
||||
variants: ["openai/gpt-5.3-codex", "gpt-5.3-codex"],
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from "./agent-scope.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.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 {
|
||||
findNormalizedProviderKey,
|
||||
@ -121,6 +121,9 @@ function normalizeProviderModelId(provider: string, model: string): string {
|
||||
if (provider === "google" || provider === "google-vertex") {
|
||||
return normalizeGoogleModelId(model);
|
||||
}
|
||||
if (provider === "xai") {
|
||||
return normalizeXaiModelId(model);
|
||||
}
|
||||
// OpenRouter-native models (e.g. "openrouter/aurora-alpha") need the full
|
||||
// "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
|
||||
|
||||
@ -9,7 +9,7 @@ import { isRecord } from "../utils.js";
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.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";
|
||||
export { buildKimiCodingProvider } from "../../extensions/kimi-coding/provider-catalog.js";
|
||||
export { buildKilocodeProvider } from "../../extensions/kilocode/provider-catalog.js";
|
||||
@ -42,7 +42,7 @@ import {
|
||||
} from "./model-auth-markers.js";
|
||||
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
|
||||
export { resolveOllamaApiBase } from "./models-config.providers.discovery.js";
|
||||
export { normalizeGoogleModelId };
|
||||
export { normalizeGoogleModelId, normalizeXaiModelId };
|
||||
|
||||
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
|
||||
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
||||
|
||||
@ -341,6 +341,15 @@ describe("web_search grok config resolution", () => {
|
||||
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", () => {
|
||||
expect(resolveGrokModel({})).toBe("grok-4-1-fast");
|
||||
});
|
||||
|
||||
@ -9,6 +9,8 @@ vi.mock("../../agents/model-catalog.js", () => ({
|
||||
{ provider: "kimi", id: "kimi-code", name: "Kimi Code" },
|
||||
{ provider: "openai", id: "gpt-4o-mini", name: "GPT-4o mini" },
|
||||
{ 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.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", () => {
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
buildAllowedModelSet,
|
||||
type ModelAliasIndex,
|
||||
modelKey,
|
||||
normalizeModelRef,
|
||||
normalizeProviderId,
|
||||
resolveModelRefFromString,
|
||||
resolveReasoningDefault,
|
||||
@ -326,7 +327,8 @@ export async function createModelSelectionState(params: {
|
||||
const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider;
|
||||
const overrideModel = sessionEntry.modelOverride?.trim();
|
||||
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)) {
|
||||
const { updated } = applyModelOverrideToSessionEntry({
|
||||
entry: sessionEntry,
|
||||
@ -356,11 +358,14 @@ export async function createModelSelectionState(params: {
|
||||
// the regular session/parent model override behavior.
|
||||
const skipStoredOverride = params.hasResolvedHeartbeatModelOverride === true;
|
||||
if (storedOverride?.model && !skipStoredOverride) {
|
||||
const candidateProvider = storedOverride.provider || defaultProvider;
|
||||
const key = modelKey(candidateProvider, storedOverride.model);
|
||||
const normalizedStoredOverride = normalizeModelRef(
|
||||
storedOverride.provider || defaultProvider,
|
||||
storedOverride.model,
|
||||
);
|
||||
const key = modelKey(normalizedStoredOverride.provider, normalizedStoredOverride.model);
|
||||
if (allowedModelKeys.size === 0 || allowedModelKeys.has(key)) {
|
||||
provider = candidateProvider;
|
||||
model = storedOverride.model;
|
||||
provider = normalizedStoredOverride.provider;
|
||||
model = normalizedStoredOverride.model;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -101,7 +101,7 @@ describe("model-pricing-cache", () => {
|
||||
],
|
||||
},
|
||||
hooks: {
|
||||
mappings: [{ model: "xai/grok-4" }],
|
||||
mappings: [{ model: "xai/grok-4.20-experimental-beta-0304-reasoning" }],
|
||||
},
|
||||
tools: {
|
||||
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: {
|
||||
prompt: "0.000002",
|
||||
completion: "0.00001",
|
||||
@ -172,12 +172,25 @@ describe("model-pricing-cache", () => {
|
||||
cacheRead: 0.3,
|
||||
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,
|
||||
output: 10,
|
||||
cacheRead: 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({
|
||||
input: 1,
|
||||
output: 4,
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
resolveModelRefFromString,
|
||||
type ModelRef,
|
||||
} 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 { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
|
||||
@ -155,6 +155,9 @@ function canonicalizeOpenRouterLookupId(id: string): string {
|
||||
if (provider === "google") {
|
||||
model = normalizeGoogleModelId(model);
|
||||
}
|
||||
if (provider === "x-ai") {
|
||||
model = normalizeXaiModelId(model);
|
||||
}
|
||||
return `${provider}/${model}`;
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ export {
|
||||
XAI_TOOL_SCHEMA_PROFILE,
|
||||
} from "../agents/model-compat.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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user