From c64dc4b676a2f018b55378b36a82eb6ee503825c Mon Sep 17 00:00:00 2001 From: Haoqian Li Date: Sat, 20 Jun 2026 14:15:53 +0800 Subject: [PATCH] fix openai tool call request content --- src/model/providers/openai/request.ts | 4 +- tests/model/providers/openai/request.test.ts | 76 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/model/providers/openai/request.test.ts diff --git a/src/model/providers/openai/request.ts b/src/model/providers/openai/request.ts index 36c8f877..fb61554e 100644 --- a/src/model/providers/openai/request.ts +++ b/src/model/providers/openai/request.ts @@ -132,6 +132,8 @@ function toOpenAIMessages(message: CanonicalMessage, messageIndex: number): Open block.type !== "tool_call" && block.type !== "thinking", ); + const requiresEmptyAssistantContent = + message.role === "assistant" && (thinkingBlocks.length > 0 || assistantToolCalls.length > 0); const messages: OpenAIMessage[] = []; if (normalContent.length > 0 || assistantToolCalls.length > 0 || thinkingBlocks.length > 0) { @@ -139,7 +141,7 @@ function toOpenAIMessages(message: CanonicalMessage, messageIndex: number): Open role: message.role, content: normalContent.length > 0 ? toOpenAIContent(normalContent) - : (message.role === "assistant" && thinkingBlocks.length > 0 ? "" : undefined), + : (requiresEmptyAssistantContent ? "" : undefined), tool_calls: assistantToolCalls.length > 0 ? assistantToolCalls : undefined, }; // DeepSeek V4 requires reasoning_content to be passed back on assistant diff --git a/tests/model/providers/openai/request.test.ts b/tests/model/providers/openai/request.test.ts new file mode 100644 index 00000000..61944f70 --- /dev/null +++ b/tests/model/providers/openai/request.test.ts @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { buildOpenAIRequest } from "../../../../src/model/providers/openai/request.js"; +import type { + CanonicalModelRequest, + ModelDefinition, +} from "../../../../src/model/protocol/canonical.js"; + +const model: ModelDefinition = { + id: "test-model", + capabilities: { + supportsToolUse: true, + supportsStreaming: true, + supportsParallelToolCalls: true, + supportsThinking: true, + supportsJsonSchema: true, + supportsSystemPrompt: true, + supportsPromptCache: false, + maxContextTokens: 8192, + maxOutputTokens: 1024, + }, + multimodal: { + input: ["text"], + }, +}; + +test("serializes assistant tool calls with explicit empty content", () => { + const request: CanonicalModelRequest = { + model: "test-model", + provider: "openai", + stream: true, + messages: [ + { + role: "user", + content: [{ type: "text", text: "Call a tool." }], + }, + { + role: "assistant", + content: [ + { + type: "tool_call", + id: "call_test_1", + name: "noop", + input: {}, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + toolCallId: "call_test_1", + content: [{ type: "text", text: "ok" }], + }, + ], + }, + ], + }; + + const body = buildOpenAIRequest(request, model); + const assistantMessage = body.messages.find((message) => message.role === "assistant"); + + assert.equal(assistantMessage?.content, ""); + assert.deepEqual(assistantMessage?.tool_calls, [ + { + id: "call_test_1", + type: "function", + function: { + name: "noop", + arguments: "{}", + }, + }, + ]); +});