Stop sending strict:null in Codex/Responses tool defs

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) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.8 committed 2026-06-10 23:11:36 -04:00
1 parent f7543e506d
commit 74422da4f8
2 files changed
+31 -2

No files matched your search

@@ -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[] {
@@ -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<string, unknown>;
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<string, unknown>).toolConfig).toEqual({ functionCallingConfig: { mode: "AUTO" } });
});
});