From 8af6459b5222db5c198899f6a10fc16d5c4ac776 Mon Sep 17 00:00:00 2001 From: Thomas Beaudouin Date: Mon, 27 Apr 2026 16:45:35 +0800 Subject: [PATCH] Handle streamed function/tool calls in Codex provider Parse and assemble streamed function_call events in pkg/providers/oauth/codex_provider.go. Added handling for response.output_item.added, response.function_call_arguments.delta and .done events to accumulate partial tool call info (name, call ID, argument deltas) into an ordered map, then convert them into protocoltypes.ToolCall entries when no tool calls were present in the final parsed response. Introduced a streamedToolCall struct and rememberToolCall helper, added imports (encoding/json, protocoltypes), and fallbacks for argument JSON parsing and finish reason adjustment. --- pkg/providers/oauth/codex_provider.go | 77 +++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/pkg/providers/oauth/codex_provider.go b/pkg/providers/oauth/codex_provider.go index a4abcefa9..b0c13d175 100644 --- a/pkg/providers/oauth/codex_provider.go +++ b/pkg/providers/oauth/codex_provider.go @@ -2,6 +2,7 @@ package oauthprovider import ( "context" + "encoding/json" "errors" "fmt" "strings" @@ -13,6 +14,7 @@ import ( "github.com/sipeed/picoclaw/pkg/auth" "github.com/sipeed/picoclaw/pkg/logger" orc "github.com/sipeed/picoclaw/pkg/providers/openai_responses_common" + "github.com/sipeed/picoclaw/pkg/providers/protocoltypes" ) const ( @@ -119,11 +121,53 @@ func (p *CodexProvider) Chat( var resp *responses.Response var streamedText strings.Builder + streamedToolCalls := map[string]*streamedToolCall{} + streamedToolCallOrder := make([]string, 0, 4) + rememberToolCall := func(key string) *streamedToolCall { + if key == "" { + key = fmt.Sprintf("streamed_tool_call_%d", len(streamedToolCallOrder)) + } + if tc, ok := streamedToolCalls[key]; ok { + return tc + } + tc := &streamedToolCall{key: key} + streamedToolCalls[key] = tc + streamedToolCallOrder = append(streamedToolCallOrder, key) + return tc + } for stream.Next() { evt := stream.Current() switch evt.Type { case "response.output_text.delta": streamedText.WriteString(evt.Delta) + case "response.output_item.added": + item := evt.AsResponseOutputItemAdded().Item + if item.Type == "function_call" { + fc := item.AsFunctionCall() + st := rememberToolCall(fc.ID) + if fc.CallID != "" { + st.callID = fc.CallID + } + if fc.Name != "" { + st.name = fc.Name + } + if fc.Arguments != "" { + st.arguments = fc.Arguments + } + } + case "response.function_call_arguments.delta": + delta := evt.AsResponseFunctionCallArgumentsDelta() + st := rememberToolCall(delta.ItemID) + st.arguments += delta.Delta + case "response.function_call_arguments.done": + done := evt.AsResponseFunctionCallArgumentsDone() + st := rememberToolCall(done.ItemID) + if done.Name != "" { + st.name = done.Name + } + if done.Arguments != "" { + st.arguments = done.Arguments + } case "response.completed", "response.failed", "response.incomplete": evtResp := evt.Response if evtResp.ID != "" { @@ -172,6 +216,32 @@ func (p *CodexProvider) Chat( } parsed := orc.ParseResponseFromStruct(resp) + if len(parsed.ToolCalls) == 0 && len(streamedToolCalls) > 0 { + for _, key := range streamedToolCallOrder { + if tc := streamedToolCalls[key]; tc != nil && tc.name != "" { + arguments := strings.TrimSpace(tc.arguments) + if arguments == "" { + arguments = "{}" + } + var args map[string]any + if err := json.Unmarshal([]byte(arguments), &args); err != nil { + args = map[string]any{"raw": arguments} + } + callID := tc.callID + if callID == "" { + callID = tc.key + } + parsed.ToolCalls = append(parsed.ToolCalls, protocoltypes.ToolCall{ + ID: callID, + Name: tc.name, + Arguments: args, + }) + } + } + if len(parsed.ToolCalls) > 0 && parsed.FinishReason == "" { + parsed.FinishReason = "tool_calls" + } + } if parsed.Content == "" && streamedText.Len() > 0 { parsed.Content = streamedText.String() if parsed.FinishReason == "" { @@ -265,6 +335,13 @@ func buildCodexParams( return params } +type streamedToolCall struct { + key string + callID string + name string + arguments string +} + func CreateCodexTokenSource() func() (string, string, error) { return func() (string, string, error) { cred, err := auth.GetCredential("openai")