Skip to content

fix(responses): merge parallel function calls into one assistant message - #9

Open
gkd2323c wants to merge 1 commit into
leookun:mainfrom
gkd2323c:fix/merge-parallel-tool-calls
Open

gkd2323c wants to merge 1 commit into
leookun:mainfrom
gkd2323c:fix/merge-parallel-tool-calls

Conversation

@gkd2323c

@gkd2323c gkd2323c commented Oct 6, 2026

Copy link
Copy Markdown

问题

当一轮里模型并行调用多个工具时,第二次及以后的请求会被 Devin 上游拒绝:

invalid_argument: There is an issue with this request, please try a different model.

上游给出的提示具有误导性,它并不是真的建议换个模型。

根因

两侧对「一次生成发出的多个工具调用」的消息结构要求不同:

结构
OpenAI Responses 协议 每个调用是独立的 function_call item
Devin 要求 全部调用放在同一条 assistant 消息的 toolCalls 数组里

appendInputItem 的 function_call 分支为每个 item新建一条 AssistantMessage,于是并行调用被转成了「多条 assistant 各带一个 toolCall,后面跟多个结果」这种序列,上游判定为非法。

串行调用不受影响,所以问题只在并行时暴露——这也是它不容易被发现的原因。

复现

// input 片段:一次生成发出的两个并行调用
{"type":"function_call","call_id":"call-1","name":"get_time","arguments":"{}"},
{"type":"function_call","call_id":"call-2","name":"get_weather","arguments":"{\"city\":\"Beijing\"}"},
{"type":"function_call_output","call_id":"call-1","output":"18:30"},
{"type":"function_call_output","call_id":"call-2","output":"22C"}

修复前返回 400 invalid_argument,修复后返回 200,且模型能正确读到两条工具结果。

修复

在 function_call 分支里,若上一条消息是 tool-use 轮次的 assistant 消息,就把本次调用并入其 Content,而不是新建一条消息。

守卫条件 assistant.StopReason == llm.StopReasonToolUse 是关键:纯文本 assistant 消息(StopReason 为零值)不会被后续调用吞并,因此串行历史完全不变。reasoning item 走 default 分支被忽略,不参与合并。

验证

  • 新增 TestDecodeRequestMergesParallelFunctionCalls:断言并行调用合并成一条 assistant 消息、Content 持有全部调用、且每个 call_id 仍能解析出工具名(function_call_output 的处理依赖它)
  • go test ./... 全部 12 个包通过,无回归
  • 实测四个场景:串行(无回归)、两路并行、真实会话形状(call,out,call,call,out,out)、三路并行,修复后全部 200
  • 端到端由真实 agent 在一轮内并行读两个文件确认通过,抓包可见上游收到的是一条带 2 个 toolCalls 的消息

未运行 gofmt -w:HEAD 版本即不符合 gofmt(Windows checkout 的 CRLF 所致),全量格式化会重写整个文件并污染 diff。本次改动自身格式干净。

Fixes #7

Devin rejects a history sequence where several assistant messages each
carry a single toolCall followed by their results. The OpenAI Responses
protocol emits each call as its own function_call item, and the adapter
turned every item into a separate AssistantMessage, so a turn with
parallel tool calls produced exactly that illegal sequence and upstream
answered:

  invalid_argument: There is an issue with this request, please try a
  different model.

Devin expects all tool calls issued in one generation to sit in a single
assistant message with a toolCalls array.

Append a function_call to the preceding assistant message when that
message is a tool-use turn, instead of starting a new one. The guard on
StopReason == StopReasonToolUse keeps text-only assistant turns from
absorbing a later call, so serial tool-call history is unchanged.

Fixes leookun#7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

【pi 使用遇到的 bug】

1 participant