Repository navigation
fix(qoder): pair tool calls whose assistant turn has null content - #297
Merged
caigee-cmd merged 1 commit intoOct 15, 2026
Merged
Conversation
The Qoder gateway pairs each role:"tool" message with the tool_calls of the assistant turn ahead of it, and it only registers that pairing when the assistant message's `content` is a string. OpenAI clients send `"content": null` for a pure tool-call turn, which is what Claude Code and most SDKs emit when the model answers with tools and no prose. The gateway then has no registered call for the follow-up tool message and rejects the whole request: <400> InternalError.Algo.InvalidParameter: messages with role "tool" must be a response to a preceeding message with "tool_calls" An empty string is accepted and pairs correctly, so rewrite a nil content on a tool-call turn to "". Measured against the live gateway, holding everything else fixed: content=null id=call_1 -> 400 InvalidParameter content=null id=toolu_01 -> 400 InvalidParameter content="" id=call_1 -> 200 content="" id=toolu_01 -> 200 content="x" id=call_1 -> 200 content="x" id=toolu_01 -> 200 The call id is irrelevant; only the content shape decides it. Only messages that carry tool_calls are rewritten — a plain assistant message with null content keeps its original shape, and non-empty content passes through untouched.
12 tasks done
caigee-cmd
approved these changes
Oct 15, 2026
caigee-cmd
left a comment
Owner
There was a problem hiding this comment.
LGTM. 审查结论:
正确性 — 上游网关只在 assistant 消息 content 为字符串时登记 tool_calls 配对;OpenAI 客户端对纯工具调用轮次发送的正是 "content": null,导致配对未登记、后续 role:"tool" 消息被拒。把该轮次的 nil content 改写为 "" 是对症的修复。
范围 — normalizeMessageContent 只在 len(message.ToolCalls) > 0 && message.Content == nil 时改写,其他所有内容形状(非空字符串、空字符串、无 tool_calls 的 null)原样透传。我确认 BuildPlainChatBody 是 qoder 包唯一的 chat 请求构造路径,不存在第二条需要同步修改的分支。
验证 — 本地跑过:go build ./...、go test ./...(全量通过)、go vet ./...、release-notes.py validate(exit 0)、3 个新测试全通过。gofmt 仅 internal/store/migrations.go 一处,为 main 既有基线,与本 PR 无关。
合并。
8 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
中文
改动概要
content: null的 assistant 轮次被网关拒绝 #296。content为字符串时,才把它携带的tool_calls登记为可配对。OpenAI 客户端对纯工具调用轮次发送"content": null,导致每次重放工具历史都被拒:400 InvalidParameter: messages with role "tool" must be a response to a preceeding message with "tool_calls"(实测矩阵见 issue)。BuildPlainChatBody现在会把带tool_calls的消息上的 nil content 改写为""。调用 id、工具结果和其他一切内容形状原样透传;不带tool_calls的普通 assistant 消息保持原样。测试计划
go test ./internal/providers/qoder/ -run NullContent— 新增 3 个用例:null+tool_calls 改写为""、无 tool_calls 的 null 保持不动、非空 content 保持不动go vet对涉及包无告警changelog/unreleased/qoder-null-content-tool-pairing.mdEnglish
Summary
content: null的 assistant 轮次被网关拒绝 #296.contentis a string. OpenAI clients send"content": nullfor a pure tool-call turn, so every replayed tool history is rejected with400 InvalidParameter: messages with role "tool" must be a response to a preceeding message with "tool_calls"(measured matrix in the linked issue).BuildPlainChatBodynow rewrites a nil content to""on messages that carrytool_calls. Call ids, tool results, and every other content shape pass through untouched; a plain assistant message with null content keeps its shape.Test plan
go test ./internal/providers/qoder/ -run NullContent— 3 new cases: null+tool_calls becomes"", null without tool_calls is kept, non-empty content is keptgo vetclean on the touched packageschangelog/unreleased/qoder-null-content-tool-pairing.md