Skip to content
Open
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
67 changes: 67 additions & 0 deletions backend/internal/pkg/apicompat/chatcompletions_responses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,73 @@ func TestResponsesEventToChatChunks_ToolCallDelta(t *testing.T) {
assert.Equal(t, 0, *tc.Index, "first tool arg delta must still use index 0")
}

func TestResponsesEventToChatChunks_ToolCallBackfillsNameFromArgumentsDone(t *testing.T) {
state := NewResponsesEventToChatState()
state.Model = "gpt-4o"
state.SentRole = true

chunks := ResponsesEventToChatChunks(&ResponsesStreamEvent{
Type: "response.output_item.added",
OutputIndex: 1,
Item: &ResponsesOutput{
Type: "function_call",
CallID: "call_backfill_1",
},
}, state)
require.Len(t, chunks, 1)
require.Len(t, chunks[0].Choices[0].Delta.ToolCalls, 1)
assert.Equal(t, "", chunks[0].Choices[0].Delta.ToolCalls[0].Function.Name)

chunks = ResponsesEventToChatChunks(&ResponsesStreamEvent{
Type: "response.function_call_arguments.done",
OutputIndex: 1,
CallID: "call_backfill_1",
Name: "get_weather",
Arguments: `{"city":"NYC"}`,
}, state)
require.Len(t, chunks, 1)
tc := chunks[0].Choices[0].Delta.ToolCalls[0]
require.NotNil(t, tc.Index)
assert.Equal(t, 0, *tc.Index)
assert.Equal(t, "get_weather", tc.Function.Name)
assert.Equal(t, `{"city":"NYC"}`, tc.Function.Arguments)
}

func TestResponsesEventToChatChunks_ToolCallBackfillsNameFromOutputItemDone(t *testing.T) {
state := NewResponsesEventToChatState()
state.Model = "gpt-4o"
state.SentRole = true

chunks := ResponsesEventToChatChunks(&ResponsesStreamEvent{
Type: "response.output_item.added",
OutputIndex: 2,
Item: &ResponsesOutput{
Type: "function_call",
CallID: "call_backfill_2",
},
}, state)
require.Len(t, chunks, 1)
require.Len(t, chunks[0].Choices[0].Delta.ToolCalls, 1)
assert.Equal(t, "", chunks[0].Choices[0].Delta.ToolCalls[0].Function.Name)

chunks = ResponsesEventToChatChunks(&ResponsesStreamEvent{
Type: "response.output_item.done",
OutputIndex: 2,
Item: &ResponsesOutput{
Type: "function_call",
CallID: "call_backfill_2",
Name: "get_time",
Arguments: `{"tz":"UTC"}`,
},
}, state)
require.Len(t, chunks, 1)
tc := chunks[0].Choices[0].Delta.ToolCalls[0]
require.NotNil(t, tc.Index)
assert.Equal(t, 0, *tc.Index)
assert.Equal(t, "get_time", tc.Function.Name)
assert.Equal(t, `{"tz":"UTC"}`, tc.Function.Arguments)
}

func TestResponsesEventToChatChunks_Completed(t *testing.T) {
state := NewResponsesEventToChatState()
state.Model = "gpt-4o"
Expand Down
74 changes: 74 additions & 0 deletions backend/internal/pkg/apicompat/responses_to_chatcompletions.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ type ResponsesEventToChatState struct {
Finalized bool // true after finish chunk has been emitted
NextToolCallIndex int // next sequential tool_call index to assign
OutputIndexToToolIndex map[int]int // Responses output_index → Chat tool_calls index
OutputIndexNameSent map[int]bool
OutputIndexArgsSeen map[int]bool
IncludeUsage bool
Usage *ChatUsage
}
Expand All @@ -141,6 +143,8 @@ func NewResponsesEventToChatState() *ResponsesEventToChatState {
ID: generateChatCmplID(),
Created: time.Now().Unix(),
OutputIndexToToolIndex: make(map[int]int),
OutputIndexNameSent: make(map[int]bool),
OutputIndexArgsSeen: make(map[int]bool),
}
}

Expand All @@ -156,6 +160,10 @@ func ResponsesEventToChatChunks(evt *ResponsesStreamEvent, state *ResponsesEvent
return resToChatHandleOutputItemAdded(evt, state)
case "response.function_call_arguments.delta":
return resToChatHandleFuncArgsDelta(evt, state)
case "response.function_call_arguments.done":
return resToChatHandleFuncArgsDone(evt, state)
case "response.output_item.done":
return resToChatHandleOutputItemDone(evt, state)
case "response.reasoning_summary_text.delta":
return resToChatHandleReasoningDelta(evt, state)
case "response.reasoning_summary_text.done":
Expand Down Expand Up @@ -248,6 +256,7 @@ func resToChatHandleOutputItemAdded(evt *ResponsesStreamEvent, state *ResponsesE
idx := state.NextToolCallIndex
state.OutputIndexToToolIndex[evt.OutputIndex] = idx
state.NextToolCallIndex++
state.OutputIndexNameSent[evt.OutputIndex] = strings.TrimSpace(evt.Item.Name) != ""

return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
ToolCalls: []ChatToolCall{{
Expand All @@ -270,6 +279,7 @@ func resToChatHandleFuncArgsDelta(evt *ResponsesStreamEvent, state *ResponsesEve
if !ok {
return nil
}
state.OutputIndexArgsSeen[evt.OutputIndex] = true

return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
ToolCalls: []ChatToolCall{{
Expand All @@ -281,6 +291,70 @@ func resToChatHandleFuncArgsDelta(evt *ResponsesStreamEvent, state *ResponsesEve
})}
}

func resToChatHandleFuncArgsDone(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
idx, ok := state.OutputIndexToToolIndex[evt.OutputIndex]
if !ok {
return nil
}

delta := ChatToolCall{Index: &idx}
if evt.CallID != "" {
delta.ID = evt.CallID
}

if name := strings.TrimSpace(evt.Name); name != "" && !state.OutputIndexNameSent[evt.OutputIndex] {
delta.Function.Name = name
state.OutputIndexNameSent[evt.OutputIndex] = true
}

if evt.Arguments != "" && !state.OutputIndexArgsSeen[evt.OutputIndex] {
delta.Function.Arguments = evt.Arguments
state.OutputIndexArgsSeen[evt.OutputIndex] = true
}

if delta.ID == "" && delta.Function.Name == "" && delta.Function.Arguments == "" {
return nil
}

return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
ToolCalls: []ChatToolCall{delta},
})}
}

func resToChatHandleOutputItemDone(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
if evt.Item == nil || evt.Item.Type != "function_call" {
return nil
}

idx, ok := state.OutputIndexToToolIndex[evt.OutputIndex]
if !ok {
return nil
}

delta := ChatToolCall{Index: &idx}
if evt.Item.CallID != "" {
delta.ID = evt.Item.CallID
}

if name := strings.TrimSpace(evt.Item.Name); name != "" && !state.OutputIndexNameSent[evt.OutputIndex] {
delta.Function.Name = name
state.OutputIndexNameSent[evt.OutputIndex] = true
}

if evt.Item.Arguments != "" && !state.OutputIndexArgsSeen[evt.OutputIndex] {
delta.Function.Arguments = evt.Item.Arguments
state.OutputIndexArgsSeen[evt.OutputIndex] = true
}

if delta.ID == "" && delta.Function.Name == "" && delta.Function.Arguments == "" {
return nil
}

return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
ToolCalls: []ChatToolCall{delta},
})}
}

func resToChatHandleReasoningDelta(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
if evt.Delta == "" {
return nil
Expand Down
Loading