diff --git a/llm/transformer/openai/responses/inbound_stream.go b/llm/transformer/openai/responses/inbound_stream.go index ca5ea32d1..0214bc972 100644 --- a/llm/transformer/openai/responses/inbound_stream.go +++ b/llm/transformer/openai/responses/inbound_stream.go @@ -621,6 +621,23 @@ func (s *responsesInboundStream) initToolCall(tc llm.ToolCall) error { }, } + // A Responses function_call must include its name in output_item.added for + // clients to route it. Some upstreams provide that identity only in a later + // arguments delta or done event, so retain the call until it is known. + if tc.ResponseCustomToolCall == nil && tc.Function.Name == "" { + return nil + } + + return s.startToolCallItem(toolCallIndex) +} + +func (s *responsesInboundStream) startToolCallItem(toolCallIndex int) error { + if s.toolCallItemStarted[toolCallIndex] { + return nil + } + + tc := s.toolCalls[toolCallIndex] + itemID := tc.ID if itemID == "" { itemID = generateItemID() @@ -676,10 +693,38 @@ func (s *responsesInboundStream) initToolCall(tc llm.ToolCall) error { func (s *responsesInboundStream) handleFunctionCallDelta(tc llm.ToolCall) error { toolCallIndex := tc.Index - s.toolCalls[toolCallIndex].Function.Arguments += tc.Function.Arguments + storedToolCall := s.toolCalls[toolCallIndex] + if tc.ID != "" { + storedToolCall.ID = tc.ID + } + if tc.Type != "" { + storedToolCall.Type = tc.Type + } + if tc.Function.Name != "" { + storedToolCall.Function.Name = tc.Function.Name + } + if tc.Function.Namespace != "" { + storedToolCall.Function.Namespace = tc.Function.Namespace + } + storedToolCall.Function.Arguments += tc.Function.Arguments - if tc.Function.Arguments != "" { - itemID := s.toolCalls[toolCallIndex].ID + argumentsToEmit := tc.Function.Arguments + if !s.toolCallItemStarted[toolCallIndex] { + if storedToolCall.Function.Name == "" { + return nil + } + + if err := s.startToolCallItem(toolCallIndex); err != nil { + return err + } + + // Arguments received before the identity became available have not been + // emitted. Replay the complete buffered payload after output_item.added. + argumentsToEmit = storedToolCall.Function.Arguments + } + + if argumentsToEmit != "" { + itemID := storedToolCall.ID if itemID == "" { itemID = s.currentItemID } @@ -689,7 +734,7 @@ func (s *responsesInboundStream) handleFunctionCallDelta(tc llm.ToolCall) error ItemID: &itemID, OutputIndex: s.toolCallOutputIndex[toolCallIndex], ContentIndex: lo.ToPtr(0), - Delta: tc.Function.Arguments, + Delta: argumentsToEmit, }) if err != nil { return fmt.Errorf("failed to enqueue function_call_arguments.delta event: %w", err) @@ -962,6 +1007,9 @@ func (s *responsesInboundStream) closeCurrentOutputItem() error { Type: StreamEventTypeFunctionCallArgumentsDone, ItemID: &itemID, OutputIndex: s.toolCallOutputIndex[idx], + CallID: tc.ID, + Name: tc.Function.Name, + Namespace: tc.Function.Namespace, Arguments: tc.Function.Arguments, }) if err != nil { diff --git a/llm/transformer/openai/responses/outbound_stream.go b/llm/transformer/openai/responses/outbound_stream.go index 5fb4ff69b..edd37a5ae 100644 --- a/llm/transformer/openai/responses/outbound_stream.go +++ b/llm/transformer/openai/responses/outbound_stream.go @@ -5,7 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "io" "log/slog" + "reflect" "strings" "github.com/samber/lo" @@ -332,20 +334,80 @@ 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. + } + + identityChanged := false + if streamEvent.Name != "" && streamEvent.Name != tc.Function.Name { + tc.Function.Name = streamEvent.Name + identityChanged = true + } + if streamEvent.Namespace != "" && streamEvent.Namespace != tc.Function.Namespace { + tc.Function.Namespace = streamEvent.Namespace + identityChanged = true + } + + // 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 + missingArgs := "" + if finalArgs == "" { + if !identityChanged { + return nil // An empty done event must not overwrite accumulated deltas. + } + } else { + forwardedArgs := tc.Function.Arguments + switch { + case forwardedArgs == "": + missingArgs = finalArgs + case strings.HasPrefix(finalArgs, forwardedArgs): + missingArgs = strings.TrimPrefix(finalArgs, forwardedArgs) + case equalJSONValues(forwardedArgs, finalArgs): + // The final payload may be reformatted without changing its meaning. + // The complete arguments were already forwarded, so do not emit a duplicate. + missingArgs = "" + default: + return fmt.Errorf("function call arguments mismatch for call_id %q", callID) + } + + tc.Function.Arguments = finalArgs + if missingArgs == "" && !identityChanged { + return nil + } + } + + toolCallIdx := s.state.toolCallIndex[callID] + resp.Choices = []llm.Choice{ + { + Index: 0, + Delta: &llm.Message{ + ToolCalls: []llm.ToolCall{ + { + ID: tc.ID, + Type: tc.Type, + Index: toolCallIdx, + Function: llm.FunctionCall{ + Name: tc.Function.Name, + Namespace: tc.Function.Namespace, + Arguments: missingArgs, + }, + }, + }, + }, + }, + } case StreamEventTypeCustomToolCallInputDelta: // Custom tool call input delta - accumulate and emit as tool call delta @@ -630,6 +692,39 @@ func (s *responsesOutboundStream) transformStreamChunk(event *httpclient.StreamE return nil } +func equalJSONValues(left, right string) bool { + leftValue, err := decodeJSONValue(left) + if err != nil { + return false + } + + rightValue, err := decodeJSONValue(right) + if err != nil { + return false + } + + return reflect.DeepEqual(leftValue, rightValue) +} + +// decodeJSONValue preserves numeric lexemes so semantic comparisons do not +// lose precision for integers that cannot be represented exactly as float64. +func decodeJSONValue(value string) (any, error) { + decoder := json.NewDecoder(strings.NewReader(value)) + decoder.UseNumber() + + var decoded any + if err := decoder.Decode(&decoded); err != nil { + return nil, err + } + + var trailing any + if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("unexpected trailing JSON value: %w", err) + } + + return decoded, nil +} + func (s *responsesOutboundStream) Current() *llm.Response { if s.queueIndex < len(s.eventQueue) { event := s.eventQueue[s.queueIndex] diff --git a/llm/transformer/openai/responses/outbound_stream_test.go b/llm/transformer/openai/responses/outbound_stream_test.go index 8a9063a24..14cd8ab8b 100644 --- a/llm/transformer/openai/responses/outbound_stream_test.go +++ b/llm/transformer/openai/responses/outbound_stream_test.go @@ -2,6 +2,7 @@ package responses import ( "context" + "encoding/json" "testing" "github.com/google/go-cmp/cmp" @@ -196,6 +197,193 @@ 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_AcceptsEquivalentFinalArguments(t *testing.T) { + trans, err := NewOutboundTransformer("https://api.openai.com", "test-api-key") + require.NoError(t, err) + + forwardedArguments := `{"task":"delegate this task","priority":1}` + finalArguments := `{ + "priority": 1, + "task": "delegate this task" +}` + events := []*httpclient.StreamEvent{ + {Data: []byte(`{"type":"response.created","response":{"id":"resp_equivalent_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_equivalent_arguments","type":"function_call","call_id":"call_equivalent_arguments","name":"spawn_agent","arguments":""}}`)}, + {Data: []byte(`{"type":"response.function_call_arguments.delta","item_id":"fc_equivalent_arguments","output_index":0,"delta":"{\"task\":\"delegate this task\",\"priority\":1}"}`)}, + {Data: []byte(`{"type":"response.function_call_arguments.done","item_id":"fc_equivalent_arguments","output_index":0,"arguments":"{\n \"priority\": 1,\n \"task\": \"delegate this task\"\n}"}`)}, + {Data: []byte(`{"type":"response.completed","response":{"id":"resp_equivalent_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.Equal(t, forwardedArguments, arguments) + require.JSONEq(t, finalArguments, arguments) +} + +func TestOutboundTransformer_TransformStream_RejectsDistinctLargeJSONNumbers(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_large_numbers","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_large_numbers","type":"function_call","call_id":"call_large_numbers","name":"spawn_agent","arguments":""}}`)}, + {Data: []byte(`{"type":"response.function_call_arguments.delta","item_id":"fc_large_numbers","output_index":0,"delta":"{\"value\":9007199254740992}"}`)}, + {Data: []byte(`{"type":"response.function_call_arguments.done","item_id":"fc_large_numbers","output_index":0,"arguments":"{\"value\":9007199254740993}"}`)}, + } + + stream, err := trans.TransformStream(t.Context(), nil, streams.SliceStream(events)) + require.NoError(t, err) + + _, err = streams.All(stream) + require.ErrorContains(t, err, `function call arguments mismatch for call_id "call_large_numbers"`) +} + +func TestResponsesStream_RoundTrip_PreservesToolIdentityProvidedOnlyInDone(t *testing.T) { + outbound, err := NewOutboundTransformer("https://api.openai.com", "test-api-key") + require.NoError(t, err) + + upstreamEvents := []*httpclient.StreamEvent{ + {Data: []byte(`{"type":"response.created","response":{"id":"resp_done_identity","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_identity","type":"function_call","call_id":"call_done_identity","arguments":""}}`)}, + {Data: []byte(`{"type":"response.function_call_arguments.done","item_id":"fc_done_identity","output_index":0,"name":"spawn_agent","namespace":"collaboration","arguments":"{\"task\":\"delegate this task\"}"}`)}, + {Data: []byte(`{"type":"response.completed","response":{"id":"resp_done_identity","object":"response","created_at":1700000000,"model":"gpt-5","status":"completed","output":[]}}`)}, + } + + unifiedStream, err := outbound.TransformStream(t.Context(), nil, streams.SliceStream(upstreamEvents)) + require.NoError(t, err) + + clientStream, err := NewInboundTransformer().TransformStream(t.Context(), unifiedStream) + require.NoError(t, err) + + clientEvents, err := streams.All(clientStream) + require.NoError(t, err) + + var addedItem *Item + var completedItem *Item + var completedArguments *StreamEvent + for _, clientEvent := range clientEvents { + if string(clientEvent.Data) == "[DONE]" { + continue + } + + var event StreamEvent + require.NoError(t, json.Unmarshal(clientEvent.Data, &event)) + if event.Type == StreamEventTypeOutputItemAdded && event.Item != nil && event.Item.Type == "function_call" { + addedItem = event.Item + } + if event.Type == StreamEventTypeOutputItemDone && event.Item != nil && event.Item.Type == "function_call" { + completedItem = event.Item + } + if event.Type == StreamEventTypeFunctionCallArgumentsDone { + completedArguments = &event + } + } + + require.NotNil(t, addedItem) + require.NotNil(t, completedItem) + require.NotNil(t, completedArguments) + require.Equal(t, "call_done_identity", completedArguments.CallID) + require.Equal(t, "spawn_agent", completedArguments.Name) + require.Equal(t, "collaboration", completedArguments.Namespace) + require.JSONEq(t, `{"task":"delegate this task"}`, completedArguments.Arguments) + require.Equal(t, "spawn_agent", addedItem.Name) + require.Equal(t, "collaboration", addedItem.Namespace) + require.Equal(t, "spawn_agent", completedItem.Name) + require.Equal(t, "collaboration", completedItem.Namespace) + require.JSONEq(t, `{"task":"delegate this task"}`, completedItem.Arguments) +} + +func TestResponsesStream_RoundTrip_DefersFunctionCallUntilLateIdentityArrives(t *testing.T) { + outbound, err := NewOutboundTransformer("https://api.openai.com", "test-api-key") + require.NoError(t, err) + + arguments := `{"task":"delegate this task"}` + upstreamEvents := []*httpclient.StreamEvent{ + {Data: []byte(`{"type":"response.created","response":{"id":"resp_late_identity","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_late_identity","type":"function_call","call_id":"call_late_identity","arguments":""}}`)}, + {Data: []byte(`{"type":"response.function_call_arguments.delta","item_id":"fc_late_identity","output_index":0,"delta":"{\"task\":\"delegate this task\"}"}`)}, + {Data: []byte(`{"type":"response.function_call_arguments.done","item_id":"fc_late_identity","output_index":0,"name":"spawn_agent","namespace":"collaboration","arguments":"{\"task\":\"delegate this task\"}"}`)}, + {Data: []byte(`{"type":"response.completed","response":{"id":"resp_late_identity","object":"response","created_at":1700000000,"model":"gpt-5","status":"completed","output":[]}}`)}, + } + + unifiedStream, err := outbound.TransformStream(t.Context(), nil, streams.SliceStream(upstreamEvents)) + require.NoError(t, err) + + clientStream, err := NewInboundTransformer().TransformStream(t.Context(), unifiedStream) + require.NoError(t, err) + + clientEvents, err := streams.All(clientStream) + require.NoError(t, err) + + var addedItems []*Item + var receivedArguments string + for _, clientEvent := range clientEvents { + if string(clientEvent.Data) == "[DONE]" { + continue + } + + var event StreamEvent + require.NoError(t, json.Unmarshal(clientEvent.Data, &event)) + if event.Type == StreamEventTypeOutputItemAdded && event.Item != nil && event.Item.Type == "function_call" { + addedItems = append(addedItems, event.Item) + } + if event.Type == StreamEventTypeFunctionCallArgumentsDelta { + receivedArguments += event.Delta + } + } + + require.Len(t, addedItems, 1) + require.Equal(t, "spawn_agent", addedItems[0].Name) + require.Equal(t, "collaboration", addedItems[0].Namespace) + require.JSONEq(t, arguments, receivedArguments) +} + func TestOutboundTransformer_TransformStream_PreservesFinalItemAnnotations(t *testing.T) { trans, err := NewOutboundTransformer("https://api.openai.com", "test-api-key") require.NoError(t, err) diff --git a/llm/transformer/openai/responses/testdata/tool-2.stream.jsonl b/llm/transformer/openai/responses/testdata/tool-2.stream.jsonl index 4c52cc313..1ea3df377 100644 --- a/llm/transformer/openai/responses/testdata/tool-2.stream.jsonl +++ b/llm/transformer/openai/responses/testdata/tool-2.stream.jsonl @@ -133,7 +133,7 @@ {"LastEventID": "", "Type": "response.function_call_arguments.delta", "Data": "{\"type\": \"response.function_call_arguments.delta\", \"sequence_number\": 132, \"item_id\": \"fc_020592949fb9ce090069355e9ec7c8819698be65c589e9e388\", \"output_index\": 2, \"delta\": \" \", \"obfuscation\": \"KQcWZMfb84zVXuo\"}"} {"LastEventID": "", "Type": "response.function_call_arguments.delta", "Data": "{\"type\": \"response.function_call_arguments.delta\", \"sequence_number\": 133, \"item_id\": \"fc_020592949fb9ce090069355e9ec7c8819698be65c589e9e388\", \"output_index\": 2, \"delta\": \"4\", \"obfuscation\": \"B6udkGzNwcN0usQ\"}"} {"LastEventID": "", "Type": "response.function_call_arguments.delta", "Data": "{\"type\": \"response.function_call_arguments.delta\", \"sequence_number\": 134, \"item_id\": \"fc_020592949fb9ce090069355e9ec7c8819698be65c589e9e388\", \"output_index\": 2, \"delta\": \"\\\"}\", \"obfuscation\": \"TpnVvhWItbjFMt\"}"} -{"LastEventID": "", "Type": "response.function_call_arguments.done", "Data": "{\"type\": \"response.function_call_arguments.done\", \"sequence_number\": 135, \"item_id\": \"fc_020592949fb9ce090069355e9ec7c8819698be65c589e9e388\", \"output_index\": 2, \"arguments\": \"{\\\"expression\\\":\\\"25 * 4\\\"}\"}"} +{"LastEventID": "", "Type": "response.function_call_arguments.done", "Data": "{\"type\": \"response.function_call_arguments.done\", \"sequence_number\": 135, \"item_id\": \"fc_020592949fb9ce090069355e9ec7c8819698be65c589e9e388\", \"output_index\": 2, \"call_id\": \"call_zfEfzIJcwVLuoHXhbdyXeb77\", \"name\": \"calculate\", \"arguments\": \"{\\\"expression\\\":\\\"25 * 4\\\"}\"}"} {"LastEventID": "", "Type": "response.output_item.done", "Data": "{\"type\": \"response.output_item.done\", \"sequence_number\": 136, \"output_index\": 2, \"item\": {\"id\": \"fc_020592949fb9ce090069355e9ec7c8819698be65c589e9e388\", \"type\": \"function_call\", \"status\": \"completed\", \"arguments\": \"{\\\"expression\\\":\\\"25 * 4\\\"}\", \"call_id\": \"call_zfEfzIJcwVLuoHXhbdyXeb77\", \"name\": \"calculate\"}}"} {"LastEventID": "", "Type": "response.output_item.added", "Data": "{\"type\": \"response.output_item.added\", \"sequence_number\": 137, \"output_index\": 3, \"item\": {\"id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"type\": \"function_call\", \"status\": \"in_progress\", \"arguments\": \"\", \"call_id\": \"call_QGIzrBsREQ0xDM1snIlUGG78\", \"name\": \"get_current_weather\"}}"} {"LastEventID": "", "Type": "response.function_call_arguments.delta", "Data": "{\"type\": \"response.function_call_arguments.delta\", \"sequence_number\": 138, \"item_id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"output_index\": 3, \"delta\": \"{\", \"obfuscation\": \"4I4Hgl6LAKpPHCj\"}"} @@ -141,6 +141,6 @@ {"LastEventID": "", "Type": "response.function_call_arguments.delta", "Data": "{\"type\": \"response.function_call_arguments.delta\", \"sequence_number\": 140, \"item_id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"output_index\": 3, \"delta\": \"\\\":\", \"obfuscation\": \"2Dr3Om6WN96A4W\"}"} {"LastEventID": "", "Type": "response.function_call_arguments.delta", "Data": "{\"type\": \"response.function_call_arguments.delta\", \"sequence_number\": 141, \"item_id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"output_index\": 3, \"delta\": \"\\\"Tokyo\", \"obfuscation\": \"S9ppEddpLF\"}"} {"LastEventID": "", "Type": "response.function_call_arguments.delta", "Data": "{\"type\": \"response.function_call_arguments.delta\", \"sequence_number\": 142, \"item_id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"output_index\": 3, \"delta\": \"\\\"}\", \"obfuscation\": \"dNh9WMs3MgXwIy\"}"} -{"LastEventID": "", "Type": "response.function_call_arguments.done", "Data": "{\"type\": \"response.function_call_arguments.done\", \"sequence_number\": 143, \"item_id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"output_index\": 3, \"arguments\": \"{\\\"location\\\":\\\"Tokyo\\\"}\"}"} +{"LastEventID": "", "Type": "response.function_call_arguments.done", "Data": "{\"type\": \"response.function_call_arguments.done\", \"sequence_number\": 143, \"item_id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"output_index\": 3, \"call_id\": \"call_QGIzrBsREQ0xDM1snIlUGG78\", \"name\": \"get_current_weather\", \"arguments\": \"{\\\"location\\\":\\\"Tokyo\\\"}\"}"} {"LastEventID": "", "Type": "response.output_item.done", "Data": "{\"type\": \"response.output_item.done\", \"sequence_number\": 144, \"output_index\": 3, \"item\": {\"id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"type\": \"function_call\", \"status\": \"completed\", \"arguments\": \"{\\\"location\\\":\\\"Tokyo\\\"}\", \"call_id\": \"call_QGIzrBsREQ0xDM1snIlUGG78\", \"name\": \"get_current_weather\"}}"} {"LastEventID": "", "Type": "response.completed", "Data": "{\"type\": \"response.completed\", \"sequence_number\": 145, \"response\": {\"id\": \"resp_020592949fb9ce090069355e9a54788196911d78a6360a88f2\", \"object\": \"response\", \"created_at\": 1765105306, \"status\": \"completed\", \"background\": false, \"content_filters\": null, \"error\": null, \"incomplete_details\": null, \"instructions\": null, \"max_output_tokens\": null, \"max_tool_calls\": null, \"model\": \"gpt-4o\", \"output\": [{\"id\": \"msg_020592949fb9ce090069355e9c05408196bb3d459b4ca2b073\", \"type\": \"message\", \"status\": \"completed\", \"content\": [{\"type\": \"output_text\", \"annotations\": [], \"logprobs\": [], \"text\": \"Hello! I'm an AI assistant designed to help with a wide range of tasks, including answering math questions and providing current weather information. For your project, I'll first calculate the product of 25 and 4, and then I'll retrieve the current weather conditions in Tokyo.\\n\\nI'll be using dedicated tools for these tasks:\\n\\n1. A calculation tool to find the product of 25 multiplied by 4.\\n2. A weather information tool to get the latest weather update for Tokyo.\\n\\nLet's start with the calculations and weather retrieval. I'll execute both tasks simultaneously to save time.\"}], \"role\": \"assistant\"}, {\"id\": \"fc_020592949fb9ce090069355e9ec7c8819698be65c589e9e388\", \"type\": \"function_call\", \"status\": \"completed\", \"arguments\": \"{\\\"expression\\\":\\\"25 * 4\\\"}\", \"call_id\": \"call_zfEfzIJcwVLuoHXhbdyXeb77\", \"name\": \"calculate\"}, {\"id\": \"fc_020592949fb9ce090069355e9ef2308196ae83c4ec1c19243b\", \"type\": \"function_call\", \"status\": \"completed\", \"arguments\": \"{\\\"location\\\":\\\"Tokyo\\\"}\", \"call_id\": \"call_QGIzrBsREQ0xDM1snIlUGG78\", \"name\": \"get_current_weather\"}], \"parallel_tool_calls\": true, \"previous_response_id\": null, \"prompt_cache_key\": null, \"prompt_cache_retention\": null, \"reasoning\": {\"effort\": null, \"summary\": null}, \"safety_identifier\": null, \"service_tier\": \"default\", \"store\": true, \"temperature\": 1.0, \"text\": {\"format\": {\"type\": \"text\"}, \"verbosity\": \"medium\"}, \"tool_choice\": \"auto\", \"tools\": [{\"type\": \"function\", \"description\": \"Perform mathematical calculations\", \"name\": \"calculate\", \"parameters\": {\"properties\": {\"expression\": {\"type\": \"string\"}}, \"required\": [\"expression\"], \"type\": \"object\", \"additionalProperties\": false}, \"strict\": true}, {\"type\": \"function\", \"description\": \"Get the current weather for a specified location\", \"name\": \"get_current_weather\", \"parameters\": {\"properties\": {\"location\": {\"type\": \"string\"}}, \"required\": [\"location\"], \"type\": \"object\", \"additionalProperties\": false}, \"strict\": true}], \"top_logprobs\": 0, \"top_p\": 1.0, \"truncation\": \"disabled\", \"usage\": {\"input_tokens\": 8, \"input_tokens_details\": {\"cached_tokens\": 0}, \"output_tokens\": 162, \"output_tokens_details\": {\"reasoning_tokens\": 0}, \"total_tokens\": 170}, \"user\": null, \"metadata\": {}}}"}