fix(agents): avoid xAI web_search tool-name collisions

This commit is contained in:
Vignesh Natarajan
2026-03-05 21:37:33 -08:00
parent 9c86a9fd23
commit e11a0775e7
3 changed files with 63 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Models/openai-completions streaming compatibility: force `compat.supportsUsageInStreaming=false` for non-native OpenAI-compatible endpoints during model normalization, preventing usage-only stream chunks from triggering `choices[0]` parser crashes in provider streams. (#8714) Thanks @nonanon1.
- Tools/xAI native web-search collision guard: drop OpenClaw `web_search` from tool registration when routing to xAI/Grok model providers (including OpenRouter `x-ai/*`) to avoid duplicate tool-name request failures against provider-native `web_search`. (#14749) Thanks @realsamrat.
- TUI/token copy-safety rendering: treat long credential-like mixed alphanumeric tokens (including quoted forms) as copy-sensitive in render sanitization so formatter hard-wrap guards no longer inject visible spaces into auth-style values before display. (#26710) Thanks @jasonthane.
- WhatsApp/self-chat response prefix fallback: stop forcing `"[openclaw]"` as the implicit outbound response prefix when no identity name or response prefix is configured, so blank/default prefix settings no longer inject branding text unexpectedly in self-chat flows. (#27962) Thanks @ecanmor.
- Memory/QMD search result decoding: accept `qmd search` hits that only include `file` URIs (for example `qmd://collection/path.md`) without `docid`, resolve them through managed collection roots, and keep multi-collection results keyed by file fallback so valid QMD hits no longer collapse to empty `memory_search` output. (#28181) Thanks @0x76696265.

View File

@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { __testing } from "./pi-tools.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
const baseTools = [
{ name: "read" },
{ name: "web_search" },
{ name: "exec" },
] as unknown as AnyAgentTool[];
function toolNames(tools: AnyAgentTool[]): string[] {
return tools.map((tool) => tool.name);
}
describe("applyModelProviderToolPolicy", () => {
it("keeps web_search for non-xAI models", () => {
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
modelProvider: "openai",
modelId: "gpt-4o-mini",
});
expect(toolNames(filtered)).toEqual(["read", "web_search", "exec"]);
});
it("removes web_search for OpenRouter xAI model ids", () => {
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
modelProvider: "openrouter",
modelId: "x-ai/grok-4.1-fast",
});
expect(toolNames(filtered)).toEqual(["read", "exec"]);
});
it("removes web_search for direct xAI providers", () => {
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
modelProvider: "x-ai",
modelId: "grok-4.1",
});
expect(toolNames(filtered)).toEqual(["read", "exec"]);
});
});

View File

@@ -43,6 +43,7 @@ import {
import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
import type { SandboxContext } from "./sandbox.js";
import { isXaiProvider } from "./schema/clean-for-xai.js";
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
import { createToolFsPolicy, resolveToolFsConfig } from "./tool-fs-policy.js";
import {
@@ -65,6 +66,7 @@ function isOpenAIProvider(provider?: string) {
const TOOL_DENY_BY_MESSAGE_PROVIDER: Readonly<Record<string, readonly string[]>> = {
voice: ["tts"],
};
const TOOL_DENY_FOR_XAI_PROVIDERS = new Set(["web_search"]);
function normalizeMessageProvider(messageProvider?: string): string | undefined {
const normalized = messageProvider?.trim().toLowerCase();
@@ -87,6 +89,18 @@ function applyMessageProviderToolPolicy(
return tools.filter((tool) => !deniedSet.has(tool.name));
}
function applyModelProviderToolPolicy(
tools: AnyAgentTool[],
params?: { modelProvider?: string; modelId?: string },
): AnyAgentTool[] {
if (!isXaiProvider(params?.modelProvider, params?.modelId)) {
return tools;
}
// xAI/Grok providers expose a native web_search tool; sending OpenClaw's
// web_search alongside it causes duplicate-name request failures.
return tools.filter((tool) => !TOOL_DENY_FOR_XAI_PROVIDERS.has(tool.name));
}
function isApplyPatchAllowedForModel(params: {
modelProvider?: string;
modelId?: string;
@@ -177,6 +191,7 @@ export const __testing = {
patchToolSchemaForClaudeCompatibility,
wrapToolParamNormalization,
assertRequiredParams,
applyModelProviderToolPolicy,
} as const;
export function createOpenClawCodingTools(options?: {
@@ -501,9 +516,13 @@ export function createOpenClawCodingTools(options?: {
}),
];
const toolsForMessageProvider = applyMessageProviderToolPolicy(tools, options?.messageProvider);
const toolsForModelProvider = applyModelProviderToolPolicy(toolsForMessageProvider, {
modelProvider: options?.modelProvider,
modelId: options?.modelId,
});
// Security: treat unknown/undefined as unauthorized (opt-in, not opt-out)
const senderIsOwner = options?.senderIsOwner === true;
const toolsByAuthorization = applyOwnerOnlyToolPolicy(toolsForMessageProvider, senderIsOwner);
const toolsByAuthorization = applyOwnerOnlyToolPolicy(toolsForModelProvider, senderIsOwner);
const subagentFiltered = applyToolPolicyPipeline({
tools: toolsByAuthorization,
toolMeta: (tool) => getPluginToolMeta(tool),