Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 58 additions & 11 deletions llm/transformer/openai/responses/outbound_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,20 +332,67 @@ func (s *responsesOutboundStream) transformStreamChunk(event *httpclient.StreamE
}

case StreamEventTypeFunctionCallArgumentsDone:
// Function call completed - update state but don't emit an event
if streamEvent.CallID != "" {
if tc, ok := s.state.toolCalls[streamEvent.CallID]; ok {
if streamEvent.Name != "" {
tc.Function.Name = streamEvent.Name
}
if streamEvent.Namespace != "" {
tc.Function.Namespace = streamEvent.Namespace
}
tc.Function.Arguments = streamEvent.Arguments
callID := streamEvent.CallID
if callID == "" && streamEvent.ItemID != nil {
callID = s.state.itemToCallID[*streamEvent.ItemID]
if callID == "" {
// Fallback: item_id might be the call_id itself.
callID = *streamEvent.ItemID
}
}

return nil // Intentionally skip this event
tc, ok := s.state.toolCalls[callID]
if !ok {
return nil // Intentionally skip an unknown tool call.
}

if streamEvent.Name != "" {
tc.Function.Name = streamEvent.Name
}
if streamEvent.Namespace != "" {
tc.Function.Namespace = streamEvent.Namespace
}

// Some upstreams provide the complete JSON arguments only in the done event.
// Preserve arguments already emitted through delta events and forward only the
// missing suffix so downstream Responses streams receive the full value once.
finalArgs := streamEvent.Arguments
if finalArgs == "" {
return nil // An empty done event must not overwrite accumulated deltas.
}

forwardedArgs := tc.Function.Arguments
var missingArgs string
switch {
case forwardedArgs == "":
missingArgs = finalArgs
case strings.HasPrefix(finalArgs, forwardedArgs):
missingArgs = strings.TrimPrefix(finalArgs, forwardedArgs)
default:
return fmt.Errorf("function call arguments mismatch for call_id %q", callID)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
}

tc.Function.Arguments = finalArgs
if missingArgs == "" {
return nil
}

toolCallIdx := s.state.toolCallIndex[callID]
resp.Choices = []llm.Choice{
{
Index: 0,
Delta: &llm.Message{
ToolCalls: []llm.ToolCall{
{
Index: toolCallIdx,
Function: llm.FunctionCall{
Arguments: missingArgs,
},
},
},
},
},
}

case StreamEventTypeCustomToolCallInputDelta:
// Custom tool call input delta - accumulate and emit as tool call delta
Expand Down
31 changes: 31 additions & 0 deletions llm/transformer/openai/responses/outbound_stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,37 @@ func TestOutboundTransformer_TransformStream_ResponseCancelledCompletes(t *testi
require.Equal(t, "cancelled", *responses[1].Choices[0].FinishReason)
}

func TestOutboundTransformer_TransformStream_EmitsArgumentsProvidedOnlyInDone(t *testing.T) {
trans, err := NewOutboundTransformer("https://api.openai.com", "test-api-key")
require.NoError(t, err)

events := []*httpclient.StreamEvent{
{Data: []byte(`{"type":"response.created","response":{"id":"resp_done_arguments","object":"response","created_at":1700000000,"model":"gpt-5","status":"in_progress","output":[]}}`)},
{Data: []byte(`{"type":"response.output_item.added","output_index":0,"item":{"id":"fc_done_arguments","type":"function_call","call_id":"call_done_arguments","name":"collaboration.spawn_agent","arguments":""}}`)},
{Data: []byte(`{"type":"response.function_call_arguments.done","item_id":"fc_done_arguments","output_index":0,"name":"collaboration.spawn_agent","arguments":"{\"task\":\"delegate this task\"}"}`)},
{Data: []byte(`{"type":"response.completed","response":{"id":"resp_done_arguments","object":"response","created_at":1700000000,"model":"gpt-5","status":"completed","output":[]}}`)},
}

stream, err := trans.TransformStream(t.Context(), nil, streams.SliceStream(events))
require.NoError(t, err)

responses, err := streams.All(stream)
require.NoError(t, err)

var arguments string
for _, response := range responses {
if response == llm.DoneResponse || len(response.Choices) == 0 || response.Choices[0].Delta == nil {
continue
}

for _, toolCall := range response.Choices[0].Delta.ToolCalls {
arguments += toolCall.Function.Arguments
}
}

require.JSONEq(t, `{"task":"delegate this task"}`, arguments)
}

func TestOutboundTransformer_TransformStream_PreservesFinalItemAnnotations(t *testing.T) {
trans, err := NewOutboundTransformer("https://api.openai.com", "test-api-key")
require.NoError(t, err)
Expand Down