Skip to content

Commit 678040a

Browse files
authored
fix: responses argument type (#605)
1 parent 449859f commit 678040a

4 files changed

Lines changed: 99 additions & 24 deletions

File tree

core/relay/adaptor/openai/chat.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1397,7 +1397,7 @@ func ConvertResponsesToChatCompletionResponse(
13971397
Type: relaymodel.ToolChoiceTypeFunction,
13981398
Function: relaymodel.Function{
13991399
Name: outputItem.Name,
1400-
Arguments: outputItem.Arguments,
1400+
Arguments: outputItem.Arguments.String(),
14011401
},
14021402
},
14031403
},

core/relay/adaptor/openai/gemini.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@ func ConvertResponsesToGeminiResponse(
10101010
if outputItem.Name != "" {
10111011
var args map[string]any
10121012
if outputItem.Arguments != "" {
1013-
err := sonic.Unmarshal([]byte(outputItem.Arguments), &args)
1013+
err := sonic.UnmarshalString(outputItem.Arguments.String(), &args)
10141014
if err == nil {
10151015
candidate.Content.Parts = append(
10161016
candidate.Content.Parts,
@@ -1257,7 +1257,7 @@ func (s *geminiStreamState) handleFunctionCallArgumentsDone(
12571257

12581258
// Parse arguments
12591259
var args map[string]any
1260-
if err := sonic.UnmarshalString(event.Arguments, &args); err != nil {
1260+
if err := sonic.UnmarshalString(event.Arguments.String(), &args); err != nil {
12611261
return false
12621262
}
12631263

core/relay/adaptor/openai/response_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,49 @@ func TestResponseStreamHandlerFlushesLifecycleEventsOnOfficialTextStreamOrder(t
309309
assert.Contains(t, output, "response.completed")
310310
}
311311

312+
func TestResponseStreamHandlerAcceptsObjectFunctionCallArguments(t *testing.T) {
313+
t.Parallel()
314+
gin.SetMode(gin.TestMode)
315+
316+
recorder := httptest.NewRecorder()
317+
c, _ := gin.CreateTestContext(recorder)
318+
c.Request = httptest.NewRequestWithContext(
319+
t.Context(),
320+
http.MethodPost,
321+
"/v1/responses",
322+
nil,
323+
)
324+
325+
body := strings.Join([]string{
326+
"event: response.output_item.added",
327+
`data: {"type":"response.output_item.added","output_index":0,"item":{"id":"fc_123","type":"function_call","call_id":"call_123","name":"search","arguments":{},"status":"in_progress"}}`,
328+
"",
329+
"event: response.function_call_arguments.done",
330+
`data: {"type":"response.function_call_arguments.done","item_id":"fc_123","output_index":0,"arguments":{"query":"spawn tool"},"sequence_number":1}`,
331+
"",
332+
"event: response.output_item.done",
333+
`data: {"type":"response.output_item.done","output_index":0,"item":{"id":"fc_123","type":"function_call","call_id":"call_123","name":"search","arguments":{"query":"spawn tool"},"status":"completed"}}`,
334+
"",
335+
"event: response.completed",
336+
`data: {"type":"response.completed","response":{"id":"resp_tool","object":"response","created_at":1,"status":"completed","model":"gpt-5.5","output":[{"id":"fc_123","type":"function_call","call_id":"call_123","name":"search","arguments":{"query":"spawn tool"},"status":"completed"}],"parallel_tool_calls":true,"store":false,"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`,
337+
"",
338+
}, "\n")
339+
resp := &http.Response{
340+
StatusCode: http.StatusOK,
341+
Body: io.NopCloser(bytes.NewBufferString(body)),
342+
Header: make(http.Header),
343+
}
344+
345+
result, err := ResponseStreamHandler(&meta.Meta{}, &responseTestStore{}, c, resp)
346+
require.Nil(t, err)
347+
assert.Equal(t, "resp_tool", result.UpstreamID)
348+
assert.Equal(t, model.ZeroNullInt64(2), result.Usage.TotalTokens)
349+
350+
output := recorder.Body.String()
351+
assert.Contains(t, output, `"arguments":{"query":"spawn tool"}`)
352+
assert.Contains(t, output, `"type":"response.completed"`)
353+
}
354+
312355
func TestResponseStreamHandlerStartsBufferTimeoutFromFirstDelayedEvent(t *testing.T) {
313356
responseStreamInitialBufferTimeoutTestMu.Lock()
314357
defer responseStreamInitialBufferTimeoutTestMu.Unlock()

core/relay/model/response.go

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,41 @@
11
package model
22

33
import (
4+
"slices"
5+
6+
"github.com/bytedance/sonic"
47
"github.com/labring/aiproxy/core/model"
58
)
69

10+
var nullBytes = []byte("null")
11+
12+
type ResponseArguments string
13+
14+
func (a *ResponseArguments) UnmarshalJSON(data []byte) error {
15+
var s string
16+
if err := sonic.ConfigDefault.Unmarshal(data, &s); err == nil {
17+
*a = ResponseArguments(s)
18+
return nil
19+
}
20+
21+
if slices.Equal(data, nullBytes) {
22+
*a = ""
23+
return nil
24+
}
25+
26+
*a = ResponseArguments(data)
27+
28+
return nil
29+
}
30+
31+
func (a ResponseArguments) MarshalJSON() ([]byte, error) {
32+
return sonic.ConfigDefault.Marshal(string(a))
33+
}
34+
35+
func (a ResponseArguments) String() string {
36+
return string(a)
37+
}
38+
739
// InputItemType represents the type of an input item
840
type InputItemType = string
941

@@ -179,15 +211,15 @@ type OutputContent struct {
179211

180212
// OutputItem represents an output item in a response
181213
type OutputItem struct {
182-
ID string `json:"id"`
183-
Type string `json:"type"`
184-
Status ResponseStatus `json:"status,omitempty"`
185-
Role string `json:"role,omitempty"`
186-
Content []OutputContent `json:"content,omitempty"`
187-
Arguments string `json:"arguments,omitempty"` // For function_call type
188-
CallID string `json:"call_id,omitempty"` // For function_call type
189-
Name string `json:"name,omitempty"` // For function_call type
190-
Summary any `json:"summary,omitempty"` // For reasoning type: []SummaryPart or string
214+
ID string `json:"id"`
215+
Type string `json:"type"`
216+
Status ResponseStatus `json:"status,omitempty"`
217+
Role string `json:"role,omitempty"`
218+
Content []OutputContent `json:"content,omitempty"`
219+
Arguments ResponseArguments `json:"arguments,omitempty"` // For function_call type
220+
CallID string `json:"call_id,omitempty"` // For function_call type
221+
Name string `json:"name,omitempty"` // For function_call type
222+
Summary any `json:"summary,omitempty"` // For reasoning type: []SummaryPart or string
191223
}
192224

193225
// InputContent represents content in an input item
@@ -333,18 +365,18 @@ type InputItemList struct {
333365

334366
// ResponseStreamEvent represents a server-sent event for response streaming
335367
type ResponseStreamEvent struct {
336-
Type string `json:"type"`
337-
Response *Response `json:"response,omitempty"`
338-
Error *OpenAIError `json:"error,omitempty"`
339-
OutputIndex *int `json:"output_index,omitempty"`
340-
Item *OutputItem `json:"item,omitempty"`
341-
ItemID string `json:"item_id,omitempty"`
342-
ContentIndex *int `json:"content_index,omitempty"`
343-
Part *OutputContent `json:"part,omitempty"` // For content_part events
344-
Delta string `json:"delta,omitempty"` // For text.delta, function_call_arguments.delta
345-
Text string `json:"text,omitempty"` // For text content
346-
Arguments string `json:"arguments,omitempty"` // For function_call_arguments.done
347-
SequenceNumber int `json:"sequence_number,omitempty"`
368+
Type string `json:"type"`
369+
Response *Response `json:"response,omitempty"`
370+
Error *OpenAIError `json:"error,omitempty"`
371+
OutputIndex *int `json:"output_index,omitempty"`
372+
Item *OutputItem `json:"item,omitempty"`
373+
ItemID string `json:"item_id,omitempty"`
374+
ContentIndex *int `json:"content_index,omitempty"`
375+
Part *OutputContent `json:"part,omitempty"` // For content_part events
376+
Delta string `json:"delta,omitempty"` // For text.delta, function_call_arguments.delta
377+
Text string `json:"text,omitempty"` // For text content
378+
Arguments ResponseArguments `json:"arguments,omitempty"` // For function_call_arguments.done
379+
SequenceNumber int `json:"sequence_number,omitempty"`
348380
}
349381

350382
func (r *Response) ToolUsageWebSearchCallCount() int64 {

0 commit comments

Comments
 (0)