From 74422da4f818aa22879fdb1f33b83846c96dc7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 10 Jun 2026 23:11:36 -0400 Subject: [PATCH] Stop sending strict:null in Codex/Responses tool defs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gpt-account (Codex) turn built its function tools with strict:null. The Responses API expects strict to be a boolean or omitted; null is malformed, so the endpoint dropped the whole tool list — leaving the model with nothing to call. That is exactly the "model returned no tool calls" failure on the GPT-account path. Omit strict when null; keep the boolean otherwise. Locked with a test, alongside the Bedrock/Gemini forced-tool-choice encoding. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../provider-tool-turns/responses-format.ts | 11 +++++++++- .../tests/unit/provider-tool-turns.test.ts | 22 ++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/AgentCompany-New/src/main/services/provider-tool-turns/responses-format.ts b/AgentCompany-New/src/main/services/provider-tool-turns/responses-format.ts index 99943382..2d84e61f 100644 --- a/AgentCompany-New/src/main/services/provider-tool-turns/responses-format.ts +++ b/AgentCompany-New/src/main/services/provider-tool-turns/responses-format.ts @@ -7,7 +7,16 @@ import { isRecord, type JsonRecord, type ProviderEventStreamMessage } from "../p import { parseArguments, type AgentTurnMessage, type AgentTurnResult, type ToolCall, type ToolDefinition } from "../provider-tool-turn"; export function responsesToolDefs(tools: ToolDefinition[], strict: boolean | null = false): unknown[] { - return tools.map((tool) => ({ type: "function", name: tool.name, description: tool.description, parameters: tool.parameters, strict })); + // `strict` must be a boolean or omitted. Passing `strict: null` (the Codex + // path) is malformed and makes the endpoint drop the whole tool list, so the + // model has nothing to call. Omit the field instead of sending null. + return tools.map((tool) => ({ + type: "function", + name: tool.name, + description: tool.description, + parameters: tool.parameters, + ...(strict === null ? {} : { strict }), + })); } export function toResponsesInput(messages: AgentTurnMessage[]): unknown[] { diff --git a/AgentCompany-New/tests/unit/provider-tool-turns.test.ts b/AgentCompany-New/tests/unit/provider-tool-turns.test.ts index ce482fbd..723adc5f 100644 --- a/AgentCompany-New/tests/unit/provider-tool-turns.test.ts +++ b/AgentCompany-New/tests/unit/provider-tool-turns.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { describe, expect, test } from "vitest"; import type { JsonRecord, ProviderEventStreamMessage } from "../../src/main/services/provider-http"; import type { AgentTurnMessage } from "../../src/main/services/provider-tool-turn"; -import { accumulateResponsesStream, parseResponsesOutput, toResponsesInput } from "../../src/main/services/provider-tool-turns/responses-format"; +import { accumulateResponsesStream, parseResponsesOutput, responsesToolDefs, toResponsesInput } from "../../src/main/services/provider-tool-turns/responses-format"; import { parseChatChoice, toChatMessages } from "../../src/main/services/provider-tool-turns/chat-format"; import { parseAnthropicContent, toAnthropicMessages } from "../../src/main/services/provider-tool-turns/anthropic-format"; import { accumulateGeminiStream, antigravityRequestBody } from "../../src/main/services/provider-tool-turns/gemini-format"; @@ -209,3 +209,23 @@ describe("credential resolver", () => { })(); }); }); + +describe("tool definitions and forced tool choice", () => { + const tools = [{ name: "hire_agent", description: "Add a report.", parameters: { type: "object", properties: {} } }]; + + test("Responses tool defs omit strict when null, keep it when boolean", () => { + const codex = responsesToolDefs(tools, null)[0] as Record; + expect("strict" in codex).toBe(false); // null would make the endpoint drop the tools + expect(responsesToolDefs(tools)[0]).toMatchObject({ type: "function", name: "hire_agent", strict: false }); + expect(responsesToolDefs(tools, true)[0]).toMatchObject({ strict: true }); + }); + + test("Bedrock and Gemini encode a forced tool choice", () => { + expect(bedrockToolConfig(tools, "required").toolChoice).toEqual({ any: {} }); + expect(bedrockToolConfig(tools).toolChoice).toEqual({ auto: {} }); + const vertex = vertexRequestBody({ model: "m", instructions: null, messages: [], tools, toolChoice: "required" }); + expect(vertex.toolConfig).toEqual({ functionCallingConfig: { mode: "ANY" } }); + const anti = antigravityRequestBody({ model: "m", projectId: "p", instructions: null, messages: [], tools, requestId: "r" }); + expect((anti.request as Record).toolConfig).toEqual({ functionCallingConfig: { mode: "AUTO" } }); + }); +});