diff --git a/service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go b/service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go index 645827ea..39db54d0 100644 --- a/service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go +++ b/service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go @@ -281,6 +281,64 @@ func TestResponsesStreamEventToChatChunksUsesTerminalDoneOutput(t *testing.T) { assert.Equal(t, "tool_calls", *chunks[3].Choices[0].FinishReason) } +func TestResponsesStreamEventToChatChunksDoesNotResendToolOnTerminalOutput(t *testing.T) { + state := newTestResponsesStreamState() + outputIndex := 0 + + var chunks []dto.ChatCompletionsStreamResponse + chunks = append(chunks, mustStreamChunks(t, state, &dto.ResponsesStreamResponse{Type: responsesEventCreated})...) + chunks = append(chunks, mustStreamChunks(t, state, &dto.ResponsesStreamResponse{ + Type: responsesEventOutputItemAdded, + OutputIndex: &outputIndex, + Item: &dto.ResponsesOutput{ + Type: responsesOutputTypeFunctionCall, + ID: "fc_1", + CallId: "call_1", + Name: "lookup", + }, + })...) + chunks = append(chunks, mustStreamChunks(t, state, &dto.ResponsesStreamResponse{ + Type: responsesEventFunctionArgsDelta, + OutputIndex: &outputIndex, + Delta: `{"q":"x"}`, + })...) + chunks = append(chunks, mustStreamChunks(t, state, &dto.ResponsesStreamResponse{ + Type: responsesEventCompleted, + Response: &dto.OpenAIResponsesResponse{ + Status: []byte(`"completed"`), + Output: []dto.ResponsesOutput{ + { + Type: responsesOutputTypeFunctionCall, + ID: "fc_1", + CallId: "call_1", + Name: "lookup", + Arguments: []byte(`{"q":"x"}`), + }, + }, + }, + })...) + + totalArgs := "" + toolIndexes := map[int]bool{} + var finishReason string + for _, chunk := range chunks { + for _, choice := range chunk.Choices { + for _, tc := range choice.Delta.ToolCalls { + require.NotNil(t, tc.Index) + toolIndexes[*tc.Index] = true + totalArgs += tc.Function.Arguments + } + if choice.FinishReason != nil { + finishReason = *choice.FinishReason + } + } + } + + assert.Equal(t, map[int]bool{0: true}, toolIndexes) + assert.Equal(t, `{"q":"x"}`, totalArgs) + assert.Equal(t, "tool_calls", finishReason) +} + func TestFinalizeResponsesToChatStreamFlushesPendingDeltaOnlyArguments(t *testing.T) { state := newTestResponsesStreamState() outputIndex := 2 diff --git a/service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go b/service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go index 0b5f3533..675e42d9 100644 --- a/service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go +++ b/service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go @@ -269,6 +269,23 @@ func (s *ResponsesToChatStreamState) ensureToolForEvent(event *dto.ResponsesStre } tool := s.toolByKey[key] + if tool == nil { + if itemID := responseStreamEventItemID(event); itemID != "" { + if existingKey := s.itemIDToKey[itemID]; existingKey != "" { + tool = s.toolByKey[existingKey] + } + } + if tool == nil { + if callID := strings.TrimSpace(event.Item.CallId); callID != "" { + if existingKey := s.callIDToKey[callID]; existingKey != "" { + tool = s.toolByKey[existingKey] + } + } + } + if tool != nil { + s.toolByKey[key] = tool + } + } if tool == nil { tool = &responsesStreamTool{Key: key, Index: s.nextToolIndex} s.nextToolIndex++