From e0fe02fb0970bcffe8b89454f4c79d2a015bcc2b Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Fri, 1 May 2026 07:56:36 +0530 Subject: [PATCH] fix(tools): preserve tool availability contracts --- src/agents/tools/image-generate-tool.test.ts | 7 +-- src/agents/tools/image-generate-tool.ts | 28 ++++++------ src/agents/tools/image-tool.test.ts | 11 +---- src/agents/tools/image-tool.ts | 17 ++++---- src/agents/tools/music-generate-tool.test.ts | 7 +-- src/agents/tools/music-generate-tool.ts | 26 +++++------ src/agents/tools/pdf-tool.ts | 9 ++-- src/agents/tools/video-generate-tool.test.ts | 7 +-- src/agents/tools/video-generate-tool.ts | 28 ++++++------ src/agents/tools/web-search.ts | 45 ++++++++++++++++++++ 10 files changed, 108 insertions(+), 77 deletions(-) diff --git a/src/agents/tools/image-generate-tool.test.ts b/src/agents/tools/image-generate-tool.test.ts index 4620d5e8742..f03737d4fc8 100644 --- a/src/agents/tools/image-generate-tool.test.ts +++ b/src/agents/tools/image-generate-tool.test.ts @@ -230,12 +230,9 @@ describe("createImageGenerateTool", () => { vi.unstubAllEnvs(); }); - it("defers image-generation model resolution until execution", async () => { + it("returns null when no image-generation model can be inferred", () => { stubImageGenerationProviders(); - const tool = requireImageGenerateTool(createImageGenerateTool({ config: {} })); - await expect(tool.execute("tool-call-1", { prompt: "draw a chart" })).rejects.toThrow( - "No image-generation model configured.", - ); + expect(createImageGenerateTool({ config: {} })).toBeNull(); }); it("tells agents how to request transparent OpenAI backgrounds", () => { diff --git a/src/agents/tools/image-generate-tool.ts b/src/agents/tools/image-generate-tool.ts index a5da9e433a4..451256a1089 100644 --- a/src/agents/tools/image-generate-tool.ts +++ b/src/agents/tools/image-generate-tool.ts @@ -565,7 +565,18 @@ export function createImageGenerateTool(options?: { workspaceDir?: string; sandbox?: ImageGenerateSandboxConfig; fsPolicy?: ToolFsPolicy; -}): AnyAgentTool { +}): AnyAgentTool | null { + const cfg = options?.config ?? getRuntimeConfig(); + const imageGenerationModelConfig = resolveImageGenerationModelConfigForTool({ + cfg, + agentDir: options?.agentDir, + }); + if (!imageGenerationModelConfig) { + return null; + } + const effectiveCfg = + applyImageGenerationModelConfigDefaults(cfg, imageGenerationModelConfig) ?? cfg; + const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(effectiveCfg); const sandboxConfig = options?.sandbox && options.sandbox.root.trim() ? { @@ -584,9 +595,8 @@ export function createImageGenerateTool(options?: { execute: async (_toolCallId, args) => { const params = args as Record; const action = resolveAction(params); - const cfg = options?.config ?? getRuntimeConfig(); if (action === "list") { - const runtimeProviders = listRuntimeImageGenerationProviders({ config: cfg }); + const runtimeProviders = listRuntimeImageGenerationProviders({ config: effectiveCfg }); const providers = runtimeProviders.map((provider) => Object.assign( { id: provider.id }, @@ -597,7 +607,7 @@ export function createImageGenerateTool(options?: { configured: isCapabilityProviderConfigured({ providers: runtimeProviders, provider, - cfg, + cfg: effectiveCfg, agentDir: options?.agentDir, }), authEnvVars: getImageGenerationProviderAuthEnvVars(provider.id), @@ -647,16 +657,6 @@ export function createImageGenerateTool(options?: { }; } - const imageGenerationModelConfig = resolveImageGenerationModelConfigForTool({ - cfg, - agentDir: options?.agentDir, - }); - if (!imageGenerationModelConfig) { - throw new ToolInputError("No image-generation model configured."); - } - const effectiveCfg = - applyImageGenerationModelConfigDefaults(cfg, imageGenerationModelConfig) ?? cfg; - const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(effectiveCfg); const prompt = readStringParam(params, "prompt", { required: true }); const imageInputs = normalizeReferenceImages(params); const model = readStringParam(params, "model"); diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index 734808bde4f..4a8fddb1a91 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -618,20 +618,13 @@ describe("image tool implicit imageModel config", () => { __testing.setProviderDepsForTest(); }); - it("defers image model pairing until execution", async () => { + it("stays disabled without auth when no pairing is possible", async () => { await withTempAgentDir(async (agentDir) => { const cfg: OpenClawConfig = { agents: { defaults: { model: { primary: "openai/gpt-5.4" } } }, }; expect(resolveImageModelConfigForTool({ cfg, agentDir })).toBeNull(); - const tool = createImageTool({ config: cfg, agentDir }); - expect(tool).not.toBeNull(); - await expect( - tool?.execute("tool-call-1", { - image: `data:image/png;base64,${ONE_PIXEL_PNG_B64}`, - prompt: "describe it", - }), - ).rejects.toThrow("No image model configured."); + expect(createImageTool({ config: cfg, agentDir })).toBeNull(); }); }); diff --git a/src/agents/tools/image-tool.ts b/src/agents/tools/image-tool.ts index 0a69353fd16..8699c5a0e69 100644 --- a/src/agents/tools/image-tool.ts +++ b/src/agents/tools/image-tool.ts @@ -380,6 +380,15 @@ export function createImageTool(options?: { } return null; } + const imageModelConfig = resolveImageModelConfigForTool({ + cfg: options?.config, + agentDir, + }); + if (!imageModelConfig) { + return null; + } + const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(options?.config); + // If model has native vision, images in the prompt are auto-injected // so this tool is only needed when image wasn't provided in the prompt const description = options?.modelHasVision @@ -453,14 +462,6 @@ export function createImageTool(options?: { record, DEFAULT_PROMPT, ); - const imageModelConfig = resolveImageModelConfigForTool({ - cfg: options?.config, - agentDir, - }); - if (!imageModelConfig) { - throw new Error("No image model configured."); - } - const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(options?.config); const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined; const maxBytes = pickMaxBytes(options?.config, maxBytesMb); diff --git a/src/agents/tools/music-generate-tool.test.ts b/src/agents/tools/music-generate-tool.test.ts index a5353c40016..d5142676dd7 100644 --- a/src/agents/tools/music-generate-tool.test.ts +++ b/src/agents/tools/music-generate-tool.test.ts @@ -129,12 +129,9 @@ describe("createMusicGenerateTool", () => { vi.unstubAllEnvs(); }); - it("defers music-generation model resolution until execution", async () => { + it("returns null when no music-generation config or auth-backed provider is available", () => { vi.spyOn(musicGenerationRuntime, "listRuntimeMusicGenerationProviders").mockReturnValue([]); - const tool = createMusicGenerateTool({ config: asConfig({}) }); - await expect(tool.execute("tool-call-1", { prompt: "make a loop" })).rejects.toThrow( - "No music-generation model configured.", - ); + expect(createMusicGenerateTool({ config: asConfig({}) })).toBeNull(); }); it("registers when music-generation config is present", () => { diff --git a/src/agents/tools/music-generate-tool.ts b/src/agents/tools/music-generate-tool.ts index fadb4fd974e..f2052ab2b19 100644 --- a/src/agents/tools/music-generate-tool.ts +++ b/src/agents/tools/music-generate-tool.ts @@ -493,7 +493,16 @@ export function createMusicGenerateTool(options?: { sandbox?: MusicGenerateSandboxConfig; fsPolicy?: ToolFsPolicy; scheduleBackgroundWork?: MusicGenerateBackgroundScheduler; -}): AnyAgentTool { +}): AnyAgentTool | null { + const cfg: OpenClawConfig = options?.config ?? getRuntimeConfig(); + const musicGenerationModelConfig = resolveMusicGenerationModelConfigForTool({ + cfg, + agentDir: options?.agentDir, + }); + if (!musicGenerationModelConfig) { + return null; + } + const sandboxConfig = options?.sandbox ? { root: options.sandbox.root, @@ -514,26 +523,17 @@ export function createMusicGenerateTool(options?: { execute: async (_toolCallId, rawArgs) => { const args = rawArgs as Record; const action = resolveAction(args); - const cfg: OpenClawConfig = options?.config ?? getRuntimeConfig(); + const effectiveCfg = + applyMusicGenerationModelConfigDefaults(cfg, musicGenerationModelConfig) ?? cfg; if (action === "list") { - return createMusicGenerateListActionResult(cfg); + return createMusicGenerateListActionResult(effectiveCfg); } if (action === "status") { return createMusicGenerateStatusActionResult(options?.agentSessionKey); } - const musicGenerationModelConfig = resolveMusicGenerationModelConfigForTool({ - cfg, - agentDir: options?.agentDir, - }); - if (!musicGenerationModelConfig) { - throw new ToolInputError("No music-generation model configured."); - } - const effectiveCfg = - applyMusicGenerationModelConfigDefaults(cfg, musicGenerationModelConfig) ?? cfg; - const duplicateGuardResult = createMusicGenerateDuplicateGuardResult( options?.agentSessionKey, ); diff --git a/src/agents/tools/pdf-tool.ts b/src/agents/tools/pdf-tool.ts index 2f3ac1e92ef..88461446103 100644 --- a/src/agents/tools/pdf-tool.ts +++ b/src/agents/tools/pdf-tool.ts @@ -257,6 +257,11 @@ export function createPdfTool(options?: { return null; } + const pdfModelConfig = resolvePdfModelConfigForTool({ cfg: options?.config, agentDir }); + if (!pdfModelConfig) { + return null; + } + const maxBytesMbDefault = ( options?.config?.agents?.defaults as Record | undefined )?.pdfMaxBytesMb; @@ -303,10 +308,6 @@ export function createPdfTool(options?: { record, DEFAULT_PROMPT, ); - const pdfModelConfig = resolvePdfModelConfigForTool({ cfg: options?.config, agentDir }); - if (!pdfModelConfig) { - throw new Error("No PDF model configured."); - } const maxBytesMbRaw = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined; const maxBytesMb = typeof maxBytesMbRaw === "number" && Number.isFinite(maxBytesMbRaw) && maxBytesMbRaw > 0 diff --git a/src/agents/tools/video-generate-tool.test.ts b/src/agents/tools/video-generate-tool.test.ts index 76994a0d0d3..74512be2c7d 100644 --- a/src/agents/tools/video-generate-tool.test.ts +++ b/src/agents/tools/video-generate-tool.test.ts @@ -93,13 +93,10 @@ describe("createVideoGenerateTool", () => { vi.unstubAllEnvs(); }); - it("defers video-generation model resolution until execution", async () => { + it("returns null when no video-generation config or auth-backed provider is available", () => { vi.spyOn(videoGenerationRuntime, "listRuntimeVideoGenerationProviders").mockReturnValue([]); - const tool = createVideoGenerateTool({ config: asConfig({}) }); - await expect(tool.execute("tool-call-1", { prompt: "make a clip" })).rejects.toThrow( - "No video-generation model configured.", - ); + expect(createVideoGenerateTool({ config: asConfig({}) })).toBeNull(); }); it("registers when video-generation config is present", () => { diff --git a/src/agents/tools/video-generate-tool.ts b/src/agents/tools/video-generate-tool.ts index 3d0e7b9f43d..e18c85ebddf 100644 --- a/src/agents/tools/video-generate-tool.ts +++ b/src/agents/tools/video-generate-tool.ts @@ -800,7 +800,16 @@ export function createVideoGenerateTool(options?: { sandbox?: VideoGenerateSandboxConfig; fsPolicy?: ToolFsPolicy; scheduleBackgroundWork?: VideoGenerateBackgroundScheduler; -}): AnyAgentTool { +}): AnyAgentTool | null { + const cfg: OpenClawConfig = options?.config ?? getRuntimeConfig(); + const videoGenerationModelConfig = resolveVideoGenerationModelConfigForTool({ + cfg, + agentDir: options?.agentDir, + }); + if (!videoGenerationModelConfig) { + return null; + } + const sandboxConfig = options?.sandbox ? { root: options.sandbox.root, @@ -821,27 +830,18 @@ export function createVideoGenerateTool(options?: { execute: async (_toolCallId, rawArgs) => { const args = rawArgs as Record; const action = resolveAction(args); - const cfg: OpenClawConfig = options?.config ?? getRuntimeConfig(); + const effectiveCfg = + applyVideoGenerationModelConfigDefaults(cfg, videoGenerationModelConfig) ?? cfg; + const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(effectiveCfg); if (action === "list") { - return createVideoGenerateListActionResult(cfg); + return createVideoGenerateListActionResult(effectiveCfg); } if (action === "status") { return createVideoGenerateStatusActionResult(options?.agentSessionKey); } - const videoGenerationModelConfig = resolveVideoGenerationModelConfigForTool({ - cfg, - agentDir: options?.agentDir, - }); - if (!videoGenerationModelConfig) { - throw new ToolInputError("No video-generation model configured."); - } - const effectiveCfg = - applyVideoGenerationModelConfigDefaults(cfg, videoGenerationModelConfig) ?? cfg; - const remoteMediaSsrfPolicy = resolveRemoteMediaSsrfPolicy(effectiveCfg); - const duplicateGuardResult = createVideoGenerateDuplicateGuardResult( options?.agentSessionKey, ); diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index 16a9ea23b27..789b24dda2a 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -14,6 +14,51 @@ const WebSearchSchema = { type: "number", description: "Number of results to return.", minimum: 1, + maximum: 20, + }, + country: { + type: "string", + description: "2-letter country code for region-specific results.", + }, + language: { + type: "string", + description: "ISO 639-1 language code for results.", + }, + freshness: { + type: "string", + description: "Filter by time: day, week, month, or year.", + }, + date_after: { + type: "string", + description: "Only results published after this date (YYYY-MM-DD).", + }, + date_before: { + type: "string", + description: "Only results published before this date (YYYY-MM-DD).", + }, + search_lang: { + type: "string", + description: "Brave search result language code.", + }, + ui_lang: { + type: "string", + description: "Brave UI locale code in language-region format.", + }, + domain_filter: { + type: "array", + items: { type: "string" }, + description: "Perplexity native Search API domain filter.", + }, + max_tokens: { + type: "number", + description: "Perplexity native Search API total content budget.", + minimum: 1, + maximum: 1000000, + }, + max_tokens_per_page: { + type: "number", + description: "Perplexity native Search API max tokens extracted per page.", + minimum: 1, }, }, } satisfies Record;