xAI: normalize deprecated Grok 4.20 ids

This commit is contained in:
huntharo 2026-03-20 00:51:01 -04:00
parent a168cde6ee
commit 73079d1cb1
No known key found for this signature in database
11 changed files with 88 additions and 8 deletions

View File

@ -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;
}

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", () => {
expect(resolveXaiInlineCitations({})).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;
}
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",
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"],

View File

@ -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

View File

@ -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];

View File

@ -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");
});

View File

@ -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,

View File

@ -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}`;
}

View File

@ -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 {