Merge pull request #3481 from visa2/fix/gateway-glm-codex-tool-args-doubled

fix(apicompat): avoid doubling tool_call arguments from single-chunk upstreams (Codex + GLM)
This commit is contained in:
Wesley Liddick
2026-06-26 09:42:13 +08:00
committed by GitHub
2 changed files with 42 additions and 0 deletions
@@ -737,6 +737,13 @@ func ChatCompletionsChunkToResponsesEvents(
copyCall.ID = generateItemID()
}
copyCall.Type = "function"
// Arguments are accumulated by the shared block below so the
// emitted delta and the stored value stay in sync. Some upstreams
// (e.g. GLM/Zhipu) pack id+name+arguments into the first tool_call
// chunk; without this reset the first chunk's arguments would be
// counted twice (once from this copy, once from the += below),
// producing a doubled, invalid JSON like {"a":1}{"a":1}.
copyCall.Function.Arguments = ""
state.ToolCalls[idx] = &copyCall
stored = &copyCall
itemID := generateItemID()
@@ -179,6 +179,41 @@ func TestStream_ToolCallLifecycleComplete(t *testing.T) {
require.True(t, sawItemDone, "function_call output_item.done missing")
}
// TestStream_ToolCallArgumentsInFirstChunkNotDoubled guards the GLM/Zhipu shape
// where a single tool_call delta chunk carries id+name+arguments together.
// Earlier code copied the whole tool_call (including arguments) into state and
// then accumulated the same chunk's arguments again, producing a doubled,
// invalid JSON like {"cmd":"ls"}{"cmd":"ls"} that breaks Codex tool parsing
// ("trailing characters").
func TestStream_ToolCallArgumentsInFirstChunkNotDoubled(t *testing.T) {
events := collectStreamEvents(t, []string{
`{"choices":[{"index":0,"delta":{"role":"assistant"}}]}`,
`{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_a","type":"function","function":{"name":"exec","arguments":"{\"cmd\":\"ls\"}"}}]}}]}`,
`{"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}`,
})
var argsDelta strings.Builder
var sawArgsDone, sawItemDone bool
for _, e := range events {
switch e.Type {
case "response.function_call_arguments.delta":
_, _ = argsDelta.WriteString(e.Delta)
case "response.function_call_arguments.done":
sawArgsDone = true
require.Equal(t, `{"cmd":"ls"}`, e.Arguments)
case "response.output_item.done":
if e.Item != nil && e.Item.Type == "function_call" {
sawItemDone = true
require.Equal(t, `{"cmd":"ls"}`, e.Item.Arguments)
}
}
}
require.True(t, sawArgsDone, "function_call_arguments.done missing")
require.True(t, sawItemDone, "function_call output_item.done missing")
// Accumulated deltas must equal the final arguments exactly (no duplication).
require.Equal(t, `{"cmd":"ls"}`, argsDelta.String())
}
// TestStream_SSEWireComplete drives the full stream through SSE encoding and
// asserts the function_call events carry complete fields on the wire.
func TestStream_SSEWireComplete(t *testing.T) {