mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
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:
@@ -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] = ©Call
|
||||
stored = ©Call
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user