Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions dto/openai_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,37 @@ type FunctionResponse struct {
Description string `json:"description,omitempty"`
Name string `json:"name,omitempty"`
// call function with arguments in JSON format
Parameters any `json:"parameters,omitempty"` // request
Arguments string `json:"arguments"` // response
Parameters any `json:"parameters,omitempty"` // request
Arguments ResponseArguments `json:"arguments"` // response
}

// ResponseArguments accepts both the canonical JSON string form and the object
// form occasionally emitted by Responses streaming events.
type ResponseArguments string

func (a *ResponseArguments) UnmarshalJSON(data []byte) error {
if len(data) == 0 || string(data) == "null" {
*a = ""
return nil
}
var s string
if err := json.Unmarshal(data, &s); err == nil {
*a = ResponseArguments(s)
return nil
}
if !json.Valid(data) {
return fmt.Errorf("invalid response arguments JSON")
}
*a = ResponseArguments(string(data))
return nil
}

func (a ResponseArguments) MarshalJSON() ([]byte, error) {
return json.Marshal(string(a))
}

func (a ResponseArguments) String() string {
return string(a)
}

type ChatCompletionsStreamResponse struct {
Expand Down Expand Up @@ -346,7 +375,7 @@ type ResponsesOutput struct {
Size string `json:"size"`
CallId string `json:"call_id,omitempty"`
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
Arguments ResponseArguments `json:"arguments,omitempty"`
}

type ResponsesOutputContent struct {
Expand Down
50 changes: 50 additions & 0 deletions dto/openai_response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dto

import (
"encoding/json"
"testing"
)

func TestResponsesOutputArgumentsAcceptsObject(t *testing.T) {
var output ResponsesOutput
err := json.Unmarshal([]byte(`{"type":"function_call","arguments":{"query":"hello","limit":3}}`), &output)
if err != nil {
t.Fatalf("Unmarshal ResponsesOutput failed: %v", err)
}
if got := output.Arguments.String(); got != `{"query":"hello","limit":3}` {
t.Fatalf("unexpected arguments: %s", got)
}
}

func TestResponsesOutputArgumentsAcceptsNull(t *testing.T) {
var output ResponsesOutput
err := json.Unmarshal([]byte(`{"type":"function_call","arguments":null}`), &output)
if err != nil {
t.Fatalf("Unmarshal ResponsesOutput failed: %v", err)
}
if got := output.Arguments.String(); got != "" {
t.Fatalf("unexpected arguments: %s", got)
}
}

func TestResponsesOutputArgumentsAcceptsArray(t *testing.T) {
var output ResponsesOutput
err := json.Unmarshal([]byte(`{"type":"function_call","arguments":["hello",3]}`), &output)
if err != nil {
t.Fatalf("Unmarshal ResponsesOutput failed: %v", err)
}
if got := output.Arguments.String(); got != `["hello",3]` {
t.Fatalf("unexpected arguments: %s", got)
}
}

func TestFunctionResponseArgumentsAcceptsString(t *testing.T) {
var fn FunctionResponse
err := json.Unmarshal([]byte(`{"name":"x","arguments":"{\"query\":\"hello\"}"}`), &fn)
if err != nil {
t.Fatalf("Unmarshal FunctionResponse failed: %v", err)
}
if got := fn.Arguments.String(); got != `{"query":"hello"}` {
t.Fatalf("unexpected arguments: %s", got)
}
}
4 changes: 2 additions & 2 deletions relay/channel/claude/relay-claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.ChatCo
Type: "function",
Index: common.GetPointer(fcIdx),
Function: dto.FunctionResponse{
Arguments: *claudeResponse.Delta.PartialJson,
Arguments: dto.ResponseArguments(*claudeResponse.Delta.PartialJson),
},
})
case "signature_delta":
Expand Down Expand Up @@ -546,7 +546,7 @@ func ResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse) *dto.OpenAITextRe
Type: "function", // compatible with other OpenAI derivative applications
Function: dto.FunctionResponse{
Name: message.Name,
Arguments: string(args),
Arguments: dto.ResponseArguments(string(args)),
},
})
case "thinking":
Expand Down
2 changes: 1 addition & 1 deletion relay/channel/gemini/relay-gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ func getResponseToolCall(item *dto.GeminiPart) *dto.ToolCallResponse {
ID: fmt.Sprintf("call_%s", common.GetUUID()),
Type: "function",
Function: dto.FunctionResponse{
Arguments: string(argsBytes),
Arguments: dto.ResponseArguments(string(argsBytes)),
Name: item.FunctionCall.FunctionName,
},
}
Expand Down
2 changes: 1 addition & 1 deletion relay/channel/ollama/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func ollamaStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
// arguments -> string
argBytes, _ := json.Marshal(tc.Function.Arguments)
toolId := fmt.Sprintf("call_%d", toolCallIndex)
tr := dto.ToolCallResponse{ID: toolId, Type: "function", Function: dto.FunctionResponse{Name: tc.Function.Name, Arguments: string(argBytes)}}
tr := dto.ToolCallResponse{ID: toolId, Type: "function", Function: dto.FunctionResponse{Name: tc.Function.Name, Arguments: dto.ResponseArguments(string(argBytes))}}
tr.SetIndex(toolCallIndex)
toolCallIndex++
delta.Choices[0].Delta.ToolCalls = append(delta.Choices[0].Delta.ToolCalls, tr)
Expand Down
4 changes: 2 additions & 2 deletions relay/channel/openai/chat_via_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
ID: callID,
Type: "function",
Function: dto.FunctionResponse{
Arguments: argsDelta,
Arguments: dto.ResponseArguments(argsDelta),
},
}
tool.SetIndex(idx)
Expand Down Expand Up @@ -408,7 +408,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
toolCallNameByID[callID] = name
}

newArgs := streamResp.Item.Arguments
newArgs := streamResp.Item.Arguments.String()
prevArgs := toolCallArgsByID[callID]
argsDelta := ""
if newArgs != "" {
Expand Down
4 changes: 2 additions & 2 deletions relay/channel/openai/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func ProcessStreamResponse(streamResponse dto.ChatCompletionsStreamResponse, res
}
for _, tool := range choice.Delta.ToolCalls {
responseTextBuilder.WriteString(tool.Function.Name)
responseTextBuilder.WriteString(tool.Function.Arguments)
responseTextBuilder.WriteString(tool.Function.Arguments.String())
}
}
}
Expand Down Expand Up @@ -132,7 +132,7 @@ func processChatCompletions(streamResp string, streamItems []string, responseTex
}
for _, tool := range choice.Delta.ToolCalls {
responseTextBuilder.WriteString(tool.Function.Name)
responseTextBuilder.WriteString(tool.Function.Arguments)
responseTextBuilder.WriteString(tool.Function.Arguments.String())
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions service/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,14 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
claudeResponses = append(claudeResponses, resp)
// 首块包含工具 delta,则追加 input_json_delta
if toolCall.Function.Arguments != "" {
partialJSON := toolCall.Function.Arguments.String()
idx := 0
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &idx,
Type: "content_block_delta",
Delta: &dto.ClaudeMediaMessage{
Type: "input_json_delta",
PartialJson: &toolCall.Function.Arguments,
PartialJson: &partialJSON,
},
})
}
Expand Down Expand Up @@ -515,12 +516,13 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon
}

if len(toolCall.Function.Arguments) > 0 {
partialJSON := toolCall.Function.Arguments.String()
claudeResponses = append(claudeResponses, &dto.ClaudeResponse{
Index: &idx,
Type: "content_block_delta",
Delta: &dto.ClaudeMediaMessage{
Type: "input_json_delta",
PartialJson: &toolCall.Function.Arguments,
PartialJson: &partialJSON,
},
})
}
Expand Down