From 6479883ddcc8b7792216bcc82940ce9098817adb Mon Sep 17 00:00:00 2001 From: fengsilin Date: Fri, 29 May 2026 15:38:34 +0800 Subject: [PATCH] fix: handle object-type arguments in Responses API stream gpt-5.4 returns `arguments` as a JSON object for some tools (e.g. apply_patch, tool_search), but ResponsesOutput.Arguments was typed as string, causing json.Unmarshal to fail on every such SSE event. The failure silently dropped all output_item events and the response.completed event from the forwarded stream, so: - the client never received tool call content - usage/token counts could not be extracted (billed as 0) - client reported "stream closed before response.completed" Fix: change Arguments to json.RawMessage and add GetArguments() which normalises both forms to a plain string (unescapes JSON strings, returns raw bytes for objects). Co-Authored-By: Claude Sonnet 4.6 --- dto/openai_response.go | 19 ++++++++++++++++++- relay/channel/openai/chat_via_responses.go | 2 +- service/openaicompat/responses_to_chat.go | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/dto/openai_response.go b/dto/openai_response.go index 37ea17b..7bd38bb 100644 --- a/dto/openai_response.go +++ b/dto/openai_response.go @@ -343,7 +343,24 @@ type ResponsesOutput struct { Size string `json:"size"` CallId string `json:"call_id,omitempty"` Name string `json:"name,omitempty"` - Arguments string `json:"arguments,omitempty"` + // Arguments can be a JSON string or a JSON object (e.g. gpt-5.4 returns objects for some tools) + Arguments json.RawMessage `json:"arguments,omitempty"` +} + +// GetArguments returns the arguments as a plain string. +// If the upstream sent a JSON string value, the inner string is returned (unescaped). +// If the upstream sent a JSON object, the raw JSON bytes are returned as a string. +func (r *ResponsesOutput) GetArguments() string { + if len(r.Arguments) == 0 { + return "" + } + if r.Arguments[0] == '"' { + var s string + if err := json.Unmarshal(r.Arguments, &s); err == nil { + return s + } + } + return string(r.Arguments) } type ResponsesOutputContent struct { diff --git a/relay/channel/openai/chat_via_responses.go b/relay/channel/openai/chat_via_responses.go index 8c4be24..c084e13 100644 --- a/relay/channel/openai/chat_via_responses.go +++ b/relay/channel/openai/chat_via_responses.go @@ -409,7 +409,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo toolCallNameByID[callID] = name } - newArgs := streamResp.Item.Arguments + newArgs := streamResp.Item.GetArguments() prevArgs := toolCallArgsByID[callID] argsDelta := "" if newArgs != "" { diff --git a/service/openaicompat/responses_to_chat.go b/service/openaicompat/responses_to_chat.go index abd0359..5ce894c 100644 --- a/service/openaicompat/responses_to_chat.go +++ b/service/openaicompat/responses_to_chat.go @@ -60,7 +60,7 @@ func ResponsesResponseToChatCompletionsResponse(resp *dto.OpenAIResponsesRespons Type: "function", Function: dto.FunctionResponse{ Name: name, - Arguments: out.Arguments, + Arguments: out.GetArguments(), }, }) }