diff --git a/relaykit/dto/openai_response.go b/relaykit/dto/openai_response.go
index 945d0a869904..7a0e8ded9d95 100644
--- a/relaykit/dto/openai_response.go
+++ b/relaykit/dto/openai_response.go
@@ -123,7 +123,7 @@ type ToolCallResponse struct {
// Index is not nil only in chat completion chunk object
Index *int `json:"index,omitempty"`
ID string `json:"id,omitempty"`
- Type any `json:"type"`
+ Type any `json:"type,omitempty"`
Function FunctionResponse `json:"function"`
}
diff --git a/relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go
index 49efa07d163e..eaec34d55ecc 100644
--- a/relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go
+++ b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go
@@ -1,6 +1,7 @@
package oairesponses
import (
+ "encoding/json"
"testing"
"github.com/QuantumNous/new-api/relaykit/dto"
@@ -246,6 +247,135 @@ func TestResponsesStreamEventToChatChunksCustomToolAndReasoning(t *testing.T) {
assert.Equal(t, "content_filter", *chunks[4].Choices[0].FinishReason)
}
+func TestResponsesStreamEventToChatChunksEmitsToolMetadataOnlyOnce(t *testing.T) {
+ state := newTestResponsesStreamState()
+ outputIndex := 0
+
+ itemChunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
+ Type: responsesEventOutputItemAdded,
+ OutputIndex: &outputIndex,
+ Item: &dto.ResponsesOutput{
+ Type: responsesOutputTypeFunctionCall,
+ ID: "fc_1",
+ CallId: "call_1",
+ Name: "show_widget",
+ },
+ })
+ firstArgsChunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
+ Type: responsesEventFunctionArgsDelta,
+ OutputIndex: &outputIndex,
+ Delta: `{"widget_code":""}`,
+ })
+
+ require.Len(t, itemChunks, 2)
+ require.Len(t, firstArgsChunks, 1)
+ require.Len(t, secondArgsChunks, 1)
+
+ first := itemChunks[1].Choices[0].Delta.ToolCalls[0]
+ assert.Equal(t, "call_1", first.ID)
+ assert.Equal(t, "function", first.Type)
+ assert.Equal(t, "show_widget", first.Function.Name)
+
+ for i, chunks := range [][]dto.ChatCompletionsStreamResponse{firstArgsChunks, secondArgsChunks} {
+ continuation := chunks[0].Choices[0].Delta.ToolCalls[0]
+ require.NotNil(t, continuation.Index)
+ assert.Equal(t, 0, *continuation.Index)
+ assert.Equal(t, []string{`{"widget_code":""}`}[i], continuation.Function.Arguments)
+ assert.Empty(t, continuation.ID)
+ assert.Nil(t, continuation.Type)
+ assert.Empty(t, continuation.Function.Name)
+
+ encoded, err := json.Marshal(continuation)
+ require.NoError(t, err)
+ assert.NotContains(t, string(encoded), `"id"`)
+ assert.NotContains(t, string(encoded), `"type"`)
+ assert.NotContains(t, string(encoded), `"name"`)
+ }
+}
+
+func TestResponsesStreamEventToChatChunksEmitsMetadataForEachParallelTool(t *testing.T) {
+ state := newTestResponsesStreamState()
+ firstOutputIndex := 0
+ secondOutputIndex := 1
+
+ firstChunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
+ Type: responsesEventOutputItemAdded,
+ OutputIndex: &firstOutputIndex,
+ Item: &dto.ResponsesOutput{
+ Type: responsesOutputTypeFunctionCall,
+ CallId: "call_1",
+ Name: "first_tool",
+ },
+ })
+ secondChunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
+ Type: responsesEventOutputItemAdded,
+ OutputIndex: &secondOutputIndex,
+ Item: &dto.ResponsesOutput{
+ Type: responsesOutputTypeFunctionCall,
+ CallId: "call_2",
+ Name: "second_tool",
+ },
+ })
+ firstArgsChunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
+ Type: responsesEventFunctionArgsDelta,
+ OutputIndex: &firstOutputIndex,
+ Delta: `{"first":`,
+ })
+ secondArgsChunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
+ Type: responsesEventFunctionArgsDelta,
+ OutputIndex: &secondOutputIndex,
+ Delta: `{"second":`,
+ })
+ firstArgsEndChunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
+ Type: responsesEventFunctionArgsDelta,
+ OutputIndex: &firstOutputIndex,
+ Delta: `"one"}`,
+ })
+ secondArgsEndChunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
+ Type: responsesEventFunctionArgsDelta,
+ OutputIndex: &secondOutputIndex,
+ Delta: `"two"}`,
+ })
+
+ require.Len(t, firstChunks, 2)
+ require.Len(t, secondChunks, 1)
+
+ first := firstChunks[1].Choices[0].Delta.ToolCalls[0]
+ second := secondChunks[0].Choices[0].Delta.ToolCalls[0]
+ assert.Equal(t, "call_1", first.ID)
+ assert.Equal(t, "function", first.Type)
+ assert.Equal(t, 0, *first.Index)
+ assert.Equal(t, "call_2", second.ID)
+ assert.Equal(t, "function", second.Type)
+ assert.Equal(t, 1, *second.Index)
+
+ continuations := []struct {
+ chunks []dto.ChatCompletionsStreamResponse
+ index int
+ arguments string
+ }{
+ {firstArgsChunks, 0, `{"first":`},
+ {secondArgsChunks, 1, `{"second":`},
+ {firstArgsEndChunks, 0, `"one"}`},
+ {secondArgsEndChunks, 1, `"two"}`},
+ }
+ for _, tt := range continuations {
+ require.Len(t, tt.chunks, 1)
+ continuation := tt.chunks[0].Choices[0].Delta.ToolCalls[0]
+ require.NotNil(t, continuation.Index)
+ assert.Equal(t, tt.index, *continuation.Index)
+ assert.Equal(t, tt.arguments, continuation.Function.Arguments)
+ assert.Empty(t, continuation.ID)
+ assert.Nil(t, continuation.Type)
+ assert.Empty(t, continuation.Function.Name)
+ }
+}
+
func TestResponsesStreamEventToChatChunksUsesTerminalDoneOutput(t *testing.T) {
state := newTestResponsesStreamState()
chunks := mustStreamChunks(t, state, &dto.ResponsesStreamResponse{
diff --git a/relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go
index 6026e3899eeb..769ec4c27f57 100644
--- a/relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go
+++ b/relaykit/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go
@@ -397,25 +397,25 @@ func (s *ResponsesToChatStreamState) toolDelta(tool *responsesStreamTool, explic
}
chunks := s.ensureStart()
- callID := strings.TrimSpace(tool.CallID)
- if callID == "" {
- callID = tool.Key
- }
responseTool := dto.ToolCallResponse{
- ID: callID,
- Type: "function",
Function: dto.FunctionResponse{
Arguments: argsDelta,
},
}
responseTool.SetIndex(tool.Index)
+ if !tool.Sent {
+ callID := strings.TrimSpace(tool.CallID)
+ if callID == "" {
+ callID = tool.Key
+ }
+ responseTool.ID = callID
+ responseTool.Type = "function"
+ tool.Sent = true
+ }
if !tool.NameSent && tool.Name != "" {
responseTool.Function.Name = tool.Name
tool.NameSent = true
}
- if !tool.Sent {
- tool.Sent = true
- }
if argsDelta != "" {
tool.ArgsSentAt += len(argsDelta)
s.usageText.WriteString(argsDelta)