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.
This commit is contained in:
Thomas Beaudouin 2026-04-27 16:45:35 +08:00
parent d9e643f472
commit 8af6459b52

View file

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