diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6683b9a8b56..58e5c966fb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,8 @@ Docs: https://docs.openclaw.ai
- CLI/config: add `--dry-run` support to `openclaw config unset`, with `--json` output and allow-exec validation parity with `config set`/`config patch` dry-run handling. (#81895) Thanks @giodl73-repo.
- Memory-core: retry disabled dreaming cron cleanup until cron is available after startup, so persisted managed dreaming jobs are removed after restart. Fixes #82383. (#82389) Thanks @neeravmakwana.
+- Providers/xAI: keep retired Grok 3, Grok 4 Fast, Grok 4.1 Fast, and Grok Code slugs out of model pickers while preserving compatibility resolution for existing configs.
+- Providers/OAuth: let browser-hosted identity provider pages read successful localhost callback responses, preventing xAI Grok OAuth from showing a false connection failure after OpenClaw completes login.
- Gateway/diagnostics: redact credential-bearing gateway target URLs and client diagnostics while preserving raw connection URLs for programmatic use, so connect-failure logs no longer surface embedded tokens.
- Gateway/auth: honor `OPENCLAW_GATEWAY_TOKEN` as the remote interactive fallback when no remote token is configured, keeping remote TUI setup aligned with documented auth precedence.
- Logs: redact raw Basic auth and named security headers from `logs.tail` output before returning lines to read-scoped clients. Fixes #66832. Thanks @Magicray1217.
diff --git a/docs/providers/xai.md b/docs/providers/xai.md
index e4c53adba6e..728d9211af0 100644
--- a/docs/providers/xai.md
+++ b/docs/providers/xai.md
@@ -50,24 +50,21 @@ and, by default, `x_search` through an operator xAI Responses proxy.
## Built-in catalog
-OpenClaw includes these xAI model families out of the box:
+OpenClaw includes the current xAI chat models out of the box, ordered newest
+first in model pickers:
| Family | Model ids |
| -------------- | ------------------------------------------------------------------------ |
-| Grok 3 | `grok-3`, `grok-3-fast`, `grok-3-mini`, `grok-3-mini-fast` |
| Grok 4.3 | `grok-4.3` |
-| Grok 4 | `grok-4`, `grok-4-0709` |
-| Grok 4 Fast | `grok-4-fast`, `grok-4-fast-non-reasoning` |
-| Grok 4.1 Fast | `grok-4-1-fast`, `grok-4-1-fast-non-reasoning` |
| Grok 4.20 Beta | `grok-4.20-beta-latest-reasoning`, `grok-4.20-beta-latest-non-reasoning` |
-| Grok Code | `grok-code-fast-1` |
-The plugin also forward-resolves newer `grok-4*` and `grok-code-fast*` ids when
-they follow the same API shape.
+The plugin still forward-resolves older Grok 3, Grok 4, Grok 4 Fast, Grok 4.1
+Fast, and Grok Code slugs for existing configs, but OpenClaw no longer shows
+those retired upstream slugs in the selectable catalog.
-`grok-4.3`, `grok-4-fast`, `grok-4-1-fast`, and the `grok-4.20-beta-*`
-variants are the current image-capable Grok refs in the bundled catalog.
+Use `grok-4.3` for new chat and coding workloads unless you explicitly need a
+Grok 4.20 beta alias.
## OpenClaw feature coverage
diff --git a/extensions/xai/model-definitions.ts b/extensions/xai/model-definitions.ts
index 0b43a996806..e777c062e04 100644
--- a/extensions/xai/model-definitions.ts
+++ b/extensions/xai/model-definitions.ts
@@ -41,8 +41,8 @@ const XAI_FAST_COST = {
} satisfies XaiCost;
const XAI_GROK_420_COST = {
- input: 2,
- output: 6,
+ input: 1.25,
+ output: 2.5,
cacheRead: 0.2,
cacheWrite: 0,
} satisfies XaiCost;
@@ -190,6 +190,25 @@ const XAI_MODEL_CATALOG = [
},
] as const satisfies readonly XaiCatalogEntry[];
+const XAI_SELECTABLE_MODEL_IDS = new Set([
+ "grok-4.3",
+ "grok-4.20-beta-latest-reasoning",
+ "grok-4.20-beta-latest-non-reasoning",
+]);
+
+const XAI_RETIRED_BUILTIN_MODEL_IDS = new Set(
+ XAI_MODEL_CATALOG.map((entry) => entry.id).filter((id) => !XAI_SELECTABLE_MODEL_IDS.has(id)),
+);
+
+function normalizeXaiCatalogModelId(modelId: string): string {
+ const lower = normalizeOptionalLowercaseString(modelId) ?? "";
+ return lower.startsWith("xai/") ? lower.slice("xai/".length) : lower;
+}
+
+export function isRetiredXaiBuiltinModelId(modelId: string): boolean {
+ return XAI_RETIRED_BUILTIN_MODEL_IDS.has(normalizeXaiCatalogModelId(modelId));
+}
+
function toModelDefinition(entry: XaiCatalogEntry): ModelDefinitionConfig {
return {
id: entry.id,
@@ -217,7 +236,9 @@ export function buildXaiModelDefinition(): ModelDefinitionConfig {
}
export function buildXaiCatalogModels(): ModelDefinitionConfig[] {
- return XAI_MODEL_CATALOG.map((entry) => toModelDefinition(entry));
+ return XAI_MODEL_CATALOG.filter((entry) => XAI_SELECTABLE_MODEL_IDS.has(entry.id)).map((entry) =>
+ toModelDefinition(entry),
+ );
}
export function resolveXaiCatalogEntry(modelId: string) {
diff --git a/extensions/xai/onboard.test.ts b/extensions/xai/onboard.test.ts
index df5d1e3205c..20684565683 100644
--- a/extensions/xai/onboard.test.ts
+++ b/extensions/xai/onboard.test.ts
@@ -19,25 +19,56 @@ describe("xai onboard", () => {
});
it("merges xAI models and keeps existing provider overrides", () => {
- const cfg = applyXaiProviderConfig(
- createLegacyProviderConfig({
- providerId: "xai",
- api: "anthropic-messages",
- modelId: "custom-model",
- modelName: "Custom",
- }),
+ const legacy = createLegacyProviderConfig({
+ providerId: "xai",
+ api: "anthropic-messages",
+ modelId: "custom-model",
+ modelName: "Custom",
+ });
+ legacy.models!.providers!.xai!.models.push(
+ {
+ id: "grok-3",
+ name: "Grok 3",
+ reasoning: false,
+ input: ["text"],
+ cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 1000,
+ maxTokens: 100,
+ },
+ {
+ id: "grok-code-fast-1",
+ name: "Grok Code Fast 1",
+ reasoning: true,
+ input: ["text"],
+ cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 1000,
+ maxTokens: 100,
+ },
);
+ const cfg = applyXaiProviderConfig(legacy);
+
expect(cfg.models?.providers?.xai?.baseUrl).toBe("https://api.x.ai/v1");
expect(cfg.models?.providers?.xai?.api).toBe("openai-responses");
expect(cfg.models?.providers?.xai?.apiKey).toBe("old-key");
- const modelIds = cfg.models?.providers?.xai?.models.map((m) => m.id) ?? [];
- expect(modelIds).toContain("custom-model");
- expect(modelIds).toContain("grok-4.3");
- expect(modelIds).toContain("grok-4");
- expect(modelIds).toContain("grok-4-1-fast");
- expect(modelIds).toContain("grok-4.20-beta-latest-reasoning");
- expect(modelIds).toContain("grok-code-fast-1");
+ expect(cfg.models?.providers?.xai?.models.map((m) => m.id)).toEqual([
+ "custom-model",
+ "grok-4.3",
+ "grok-4.20-beta-latest-reasoning",
+ "grok-4.20-beta-latest-non-reasoning",
+ ]);
+ });
+
+ it("publishes current xAI models newest first for fresh setup", () => {
+ const cfg = applyXaiProviderConfig({});
+
+ expect(cfg.models?.providers?.xai?.baseUrl).toBe("https://api.x.ai/v1");
+ expect(cfg.models?.providers?.xai?.api).toBe("openai-responses");
+ expect(cfg.models?.providers?.xai?.models.map((m) => m.id)).toEqual([
+ "grok-4.3",
+ "grok-4.20-beta-latest-reasoning",
+ "grok-4.20-beta-latest-non-reasoning",
+ ]);
});
it("adds expected alias for the default model", () => {
diff --git a/extensions/xai/onboard.ts b/extensions/xai/onboard.ts
index 5d71bdc56a5..cb6da4a458c 100644
--- a/extensions/xai/onboard.ts
+++ b/extensions/xai/onboard.ts
@@ -1,13 +1,13 @@
import {
- createDefaultModelsPresetAppliers,
+ createModelCatalogPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { XAI_BASE_URL, XAI_DEFAULT_MODEL_ID } from "./model-definitions.js";
-import { buildXaiCatalogModels } from "./model-definitions.js";
+import { buildXaiCatalogModels, isRetiredXaiBuiltinModelId } from "./model-definitions.js";
export const XAI_DEFAULT_MODEL_REF = `xai/${XAI_DEFAULT_MODEL_ID}`;
-const xaiPresetAppliers = createDefaultModelsPresetAppliers<
+const xaiPresetAppliers = createModelCatalogPresetAppliers<
["openai-completions" | "openai-responses"]
>({
primaryModelRef: XAI_DEFAULT_MODEL_REF,
@@ -15,16 +15,42 @@ const xaiPresetAppliers = createDefaultModelsPresetAppliers<
providerId: "xai",
api,
baseUrl: XAI_BASE_URL,
- defaultModels: buildXaiCatalogModels(),
- defaultModelId: XAI_DEFAULT_MODEL_ID,
+ catalogModels: buildXaiCatalogModels(),
aliases: [{ modelRef: XAI_DEFAULT_MODEL_REF, alias: "Grok" }],
}),
});
+function pruneRetiredXaiBuiltinModels(cfg: OpenClawConfig): OpenClawConfig {
+ const provider = cfg.models?.providers?.xai;
+ if (!provider || !Array.isArray(provider.models)) {
+ return cfg;
+ }
+ const models = provider.models.filter((model) => !isRetiredXaiBuiltinModelId(model.id));
+ if (models.length === provider.models.length) {
+ return cfg;
+ }
+ return {
+ ...cfg,
+ models: {
+ ...cfg.models,
+ providers: {
+ ...cfg.models?.providers,
+ xai: {
+ ...provider,
+ models,
+ },
+ },
+ },
+ };
+}
+
export function applyXaiProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
- return xaiPresetAppliers.applyProviderConfig(cfg, "openai-responses");
+ return xaiPresetAppliers.applyProviderConfig(
+ pruneRetiredXaiBuiltinModels(cfg),
+ "openai-responses",
+ );
}
export function applyXaiConfig(cfg: OpenClawConfig): OpenClawConfig {
- return xaiPresetAppliers.applyConfig(cfg, "openai-responses");
+ return xaiPresetAppliers.applyConfig(pruneRetiredXaiBuiltinModels(cfg), "openai-responses");
}
diff --git a/extensions/xai/provider-models.ts b/extensions/xai/provider-models.ts
index e4b9a414633..2258998db45 100644
--- a/extensions/xai/provider-models.ts
+++ b/extensions/xai/provider-models.ts
@@ -7,7 +7,7 @@ import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coe
import { resolveXaiCatalogEntry, XAI_BASE_URL } from "./model-definitions.js";
import { applyXaiRuntimeModelCompat } from "./runtime-model-compat.js";
-const XAI_MODERN_MODEL_PREFIXES = ["grok-3", "grok-4", "grok-code-fast"] as const;
+const XAI_MODERN_MODEL_PREFIXES = ["grok-4.3", "grok-4.20"] as const;
export function isModernXaiModel(modelId: string): boolean {
const lower = normalizeOptionalLowercaseString(modelId) ?? "";
diff --git a/extensions/xai/web-search.test.ts b/extensions/xai/web-search.test.ts
index 6e46a1576a7..a541ccd93f8 100644
--- a/extensions/xai/web-search.test.ts
+++ b/extensions/xai/web-search.test.ts
@@ -3,7 +3,7 @@ import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runt
import { createNonExitingRuntime } from "openclaw/plugin-sdk/runtime-env";
import { withEnv, withEnvAsync, withFetchPreconnect } from "openclaw/plugin-sdk/test-env";
import { afterEach, describe, expect, it, vi } from "vitest";
-import { resolveXaiCatalogEntry } from "./model-definitions.js";
+import { buildXaiCatalogModels, resolveXaiCatalogEntry } from "./model-definitions.js";
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
import { resolveFallbackXaiAuth } from "./src/tool-auth-shared.js";
import { wrapXaiWebSearchError } from "./src/web-search-shared.js";
@@ -574,6 +574,14 @@ describe("xai web search response parsing", () => {
});
describe("xai provider models", () => {
+ it("publishes only current selectable chat models newest first", () => {
+ expect(buildXaiCatalogModels().map((model) => model.id)).toEqual([
+ "grok-4.3",
+ "grok-4.20-beta-latest-reasoning",
+ "grok-4.20-beta-latest-non-reasoning",
+ ]);
+ });
+
it("publishes Grok 4.3 as the default chat model", () => {
expectCatalogEntry("grok-4.3", {
id: "grok-4.3",
@@ -585,7 +593,7 @@ describe("xai provider models", () => {
});
});
- it("publishes the newer Grok fast and code models in the bundled catalog", () => {
+ it("keeps retired Grok fast and code slugs resolving for compatibility", () => {
expectCatalogEntry("grok-4-1-fast", {
id: "grok-4-1-fast",
reasoning: true,
@@ -648,8 +656,8 @@ describe("xai provider models", () => {
it("marks current Grok families as modern while excluding multi-agent ids", () => {
expect(isModernXaiModel("grok-4.3")).toBe(true);
expect(isModernXaiModel("grok-4.20-beta-latest-reasoning")).toBe(true);
- expect(isModernXaiModel("grok-code-fast-1")).toBe(true);
- expect(isModernXaiModel("grok-3-mini-fast")).toBe(true);
+ expect(isModernXaiModel("grok-code-fast-1")).toBe(false);
+ expect(isModernXaiModel("grok-3-mini-fast")).toBe(false);
expect(isModernXaiModel("grok-4.20-multi-agent-experimental-beta-0304")).toBe(false);
});
diff --git a/extensions/xai/xai-oauth.test.ts b/extensions/xai/xai-oauth.test.ts
index cd56ef99980..7388be164d2 100644
--- a/extensions/xai/xai-oauth.test.ts
+++ b/extensions/xai/xai-oauth.test.ts
@@ -27,7 +27,7 @@ describe("xAI OAuth", () => {
expect(isTrustedXaiOAuthEndpoint("not a url")).toBe(false);
});
- it("builds the Hermes-compatible authorize URL for OpenClaw", () => {
+ it("builds the xAI authorize URL for OpenClaw", () => {
const url = new URL(
buildXaiOAuthAuthorizeUrl({
authorizationEndpoint: "https://auth.x.ai/oauth2/authorize",
diff --git a/src/plugin-sdk/provider-auth-runtime.test.ts b/src/plugin-sdk/provider-auth-runtime.test.ts
index cc0ef4a38ce..b7f87b19fda 100644
--- a/src/plugin-sdk/provider-auth-runtime.test.ts
+++ b/src/plugin-sdk/provider-auth-runtime.test.ts
@@ -1,6 +1,23 @@
+import { createServer } from "node:net";
import { describe, expect, it } from "vitest";
import * as providerAuthRuntime from "./provider-auth-runtime.js";
+async function getFreePort(): Promise {
+ return await new Promise((resolve, reject) => {
+ const server = createServer();
+ server.once("error", reject);
+ server.listen(0, "127.0.0.1", () => {
+ const address = server.address();
+ if (!address || typeof address === "string") {
+ server.close(() => reject(new Error("Failed to allocate a local port")));
+ return;
+ }
+ const { port } = address;
+ server.close((err) => (err ? reject(err) : resolve(port)));
+ });
+ });
+}
+
describe("plugin-sdk provider-auth-runtime", () => {
it("exports the runtime-ready auth helper", () => {
expect(providerAuthRuntime.getRuntimeAuthForModel).toBeTypeOf("function");
@@ -25,4 +42,42 @@ describe("plugin-sdk provider-auth-runtime", () => {
error: "Paste the full redirect URL, not just the code.",
});
});
+
+ it("allows browser IdP pages to probe the localhost callback with CORS", async () => {
+ const port = await getFreePort();
+ const callback = providerAuthRuntime.waitForLocalOAuthCallback({
+ expectedState: "state-1",
+ timeoutMs: 5_000,
+ port,
+ callbackPath: "/callback",
+ redirectUri: `http://127.0.0.1:${port}/callback`,
+ hostname: "127.0.0.1",
+ successTitle: "OAuth complete",
+ });
+
+ const preflight = await fetch(`http://127.0.0.1:${port}/callback`, {
+ method: "OPTIONS",
+ headers: {
+ Origin: "https://auth.x.ai",
+ "Access-Control-Request-Method": "GET",
+ "Access-Control-Request-Headers": "content-type",
+ "Access-Control-Request-Private-Network": "true",
+ },
+ });
+
+ expect(preflight.status).toBe(204);
+ expect(preflight.headers.get("access-control-allow-origin")).toBe("https://auth.x.ai");
+ expect(preflight.headers.get("access-control-allow-methods")).toContain("GET");
+ expect(preflight.headers.get("access-control-allow-private-network")).toBe("true");
+
+ const response = await fetch(`http://127.0.0.1:${port}/callback?code=code-1&state=state-1`, {
+ headers: {
+ Origin: "https://auth.x.ai",
+ },
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.headers.get("access-control-allow-origin")).toBe("https://auth.x.ai");
+ await expect(callback).resolves.toEqual({ code: "code-1", state: "state-1" });
+ });
});
diff --git a/src/plugin-sdk/provider-auth-runtime.ts b/src/plugin-sdk/provider-auth-runtime.ts
index a220e2db125..1dac0a4a08e 100644
--- a/src/plugin-sdk/provider-auth-runtime.ts
+++ b/src/plugin-sdk/provider-auth-runtime.ts
@@ -74,6 +74,7 @@ export async function waitForLocalOAuthCallback(params: {
let timeout: NodeJS.Timeout | null = null;
const server = createServer((req, res) => {
try {
+ applyOAuthCallbackCorsHeaders(req, res);
const requestUrl = new URL(req.url ?? "/", `http://${hostname}:${params.port}`);
if (requestUrl.pathname !== params.callbackPath) {
res.statusCode = 404;
@@ -82,6 +83,19 @@ export async function waitForLocalOAuthCallback(params: {
return;
}
+ if (req.method === "OPTIONS") {
+ res.statusCode = 204;
+ res.end();
+ return;
+ }
+
+ if (req.method !== "GET") {
+ res.statusCode = 405;
+ res.setHeader("Content-Type", "text/plain");
+ res.end("Method not allowed");
+ return;
+ }
+
const error = requestUrl.searchParams.get("error");
const code = requestUrl.searchParams.get("code")?.trim();
const state = requestUrl.searchParams.get("state")?.trim();
@@ -160,6 +174,37 @@ export async function waitForLocalOAuthCallback(params: {
});
}
+function applyOAuthCallbackCorsHeaders(
+ req: import("node:http").IncomingMessage,
+ res: import("node:http").ServerResponse,
+): void {
+ const origin = req.headers.origin;
+ if (typeof origin === "string" && isHttpOrigin(origin)) {
+ res.setHeader("Access-Control-Allow-Origin", origin);
+ res.setHeader("Vary", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers");
+ }
+
+ const requestedHeaders = req.headers["access-control-request-headers"];
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
+ res.setHeader(
+ "Access-Control-Allow-Headers",
+ typeof requestedHeaders === "string" && requestedHeaders.trim().length > 0
+ ? requestedHeaders
+ : "content-type",
+ );
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
+ res.setHeader("Access-Control-Max-Age", "600");
+}
+
+function isHttpOrigin(value: string): boolean {
+ try {
+ const url = new URL(value);
+ return (url.protocol === "http:" || url.protocol === "https:") && url.origin === value;
+ } catch {
+ return false;
+ }
+}
+
function escapeHtmlText(value: string): string {
return value
.replace(/&/g, "&")