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")