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

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