From aabf0b8375cba13705778b634e2724ae98ac2e55 Mon Sep 17 00:00:00 2001 From: Rafael Moreira Date: Wed, 9 Sep 2026 22:37:39 -0300 Subject: [PATCH] fix(responses): gracefully handle code-mode view_image calls via unified exec When running under Code Mode (tool_mode: code_mode_only / features.code_mode_host=true), routed models (such as Gemini 3.8 Flash or Claude) occasionally emit undeclared top-level client tool calls to view_image when encountering visual assets. Previously, responses-undeclared-tool-guard.ts failed closed with HTTP 502 / response.failed, abruptly terminating the turn. This patch: 1. Adds view_image to CODE_MODE_HELPER_TOOL_NAMES in src/types/tools.ts so the guard normalizes it to exec. 2. Synthesizes a friendly code-mode informative message via text() inside compileCodeModeHelperInput in src/responses/code-mode-helper-compat.ts, allowing the model to self-correct and proceed via exec_command/code without aborting the session. 3. Adds regression tests in tests/responses/legacy-shell-compat.test.ts and tests/responses/responses-undeclared-tool-guard.test.ts. --- src/responses/code-mode-helper-compat.ts | 10 ++++++++++ src/types/tools.ts | 1 + tests/responses/legacy-shell-compat.test.ts | 12 ++++++++++++ .../responses-undeclared-tool-guard.test.ts | 10 ++++++++++ 4 files changed, 33 insertions(+) diff --git a/src/responses/code-mode-helper-compat.ts b/src/responses/code-mode-helper-compat.ts index 726c5dcf19..f0c86f1e07 100644 --- a/src/responses/code-mode-helper-compat.ts +++ b/src/responses/code-mode-helper-compat.ts @@ -54,6 +54,16 @@ export function compileCodeModeHelperInput(argumentsText: unknown, toolName: str if (toolName === "write_stdin") { return `const result = await tools.write_stdin(${JSON.stringify(args)});\ntext(result);`; } + if (toolName === "view_image") { + let target = ""; + if (isPlainObject(args)) { + if (typeof args.path === "string") target = ` for ${JSON.stringify(args.path)}`; + else if (typeof args.file_path === "string") target = ` for ${JSON.stringify(args.file_path)}`; + else if (typeof args.file === "string") target = ` for ${JSON.stringify(args.file)}`; + } + const msg = `Error: 'view_image' is not available in Code Mode${target}. Inspect files or visual assets programmatically using tools.exec_command or code.`; + return `text(${JSON.stringify(msg)});\n`; + } return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`; } diff --git a/src/types/tools.ts b/src/types/tools.ts index bb91ebe63f..3678bcbc98 100644 --- a/src/types/tools.ts +++ b/src/types/tools.ts @@ -57,6 +57,7 @@ const CODE_MODE_HELPER_TOOL_NAMES = [ ...LEGACY_SHELL_BRIDGE_TOOL_NAMES, "write_stdin", "apply_patch", + "view_image", ] as const; /** diff --git a/tests/responses/legacy-shell-compat.test.ts b/tests/responses/legacy-shell-compat.test.ts index 6503c3e6dd..0598bced7c 100644 --- a/tests/responses/legacy-shell-compat.test.ts +++ b/tests/responses/legacy-shell-compat.test.ts @@ -112,4 +112,16 @@ describe("code-mode helper compatibility", () => { expect(received).toEqual(input === "[]" ? [] : input); } }); + + test("view_image emits a friendly code-mode error message via text()", async () => { + const source = compileCodeModeHelperInput( + JSON.stringify({ path: "/root/banner.png" }), + "view_image", + ); + let output: unknown; + const run = new AsyncFunction("tools", "text", source); + await run({}, (value: unknown) => { output = value; }); + expect(output).toContain("Error: 'view_image' is not available in Code Mode for \"/root/banner.png\""); + expect(output).toContain("tools.exec_command"); + }); }); diff --git a/tests/responses/responses-undeclared-tool-guard.test.ts b/tests/responses/responses-undeclared-tool-guard.test.ts index 41d8383a6c..27c577b118 100644 --- a/tests/responses/responses-undeclared-tool-guard.test.ts +++ b/tests/responses/responses-undeclared-tool-guard.test.ts @@ -1640,6 +1640,16 @@ describe("undeclaredToolCallNameInResponse", () => { expect(undeclaredToolCallNameInResponse(response, new Set())).toBe("write_stdin"); }); + test("accepts view_image through a bare unified exec declaration", () => { + const response = { + output: [{ type: "function_call", name: "view_image" }], + }; + + expect(undeclaredToolCallNameInResponse(response, new Set(["exec"]))).toBeUndefined(); + expect(undeclaredToolCallNameInResponse(response, new Set(["view_image"]))).toBeUndefined(); + expect(undeclaredToolCallNameInResponse(response, new Set())).toBe("view_image"); + }); + test("never legacy-normalizes a namespaced shell bridge call", () => { // A namespaced call (e.g. an MCP server advertising its own exec_command) must be // matched by its full wire name only — never normalized to bare `exec`.