mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix: preserve streamed Codex tool calls
This commit is contained in:
@@ -105,11 +105,18 @@ func (p *CodexProvider) Chat(
|
||||
|
||||
var resp *responses.Response
|
||||
var streamedText strings.Builder
|
||||
streamedOutputItems := make([]responses.ResponseOutputItemUnion, 0)
|
||||
for stream.Next() {
|
||||
evt := stream.Current()
|
||||
if evt.Type == "response.output_text.delta" {
|
||||
streamedText.WriteString(evt.Delta)
|
||||
}
|
||||
if evt.Type == "response.output_item.done" {
|
||||
itemEvt := evt.AsResponseOutputItemDone()
|
||||
if itemEvt.Item.Type != "" {
|
||||
streamedOutputItems = append(streamedOutputItems, itemEvt.Item)
|
||||
}
|
||||
}
|
||||
if evt.Type == "response.completed" || evt.Type == "response.failed" || evt.Type == "response.incomplete" {
|
||||
evtResp := evt.Response
|
||||
if evtResp.ID != "" {
|
||||
@@ -156,6 +163,9 @@ func (p *CodexProvider) Chat(
|
||||
logger.ErrorCF("provider.codex", "Codex stream ended without completed response event", fields)
|
||||
return nil, fmt.Errorf("codex API call: stream ended without completed response")
|
||||
}
|
||||
if len(resp.Output) == 0 && len(streamedOutputItems) > 0 {
|
||||
resp.Output = streamedOutputItems
|
||||
}
|
||||
|
||||
parsed := orc.ParseResponseFromStruct(resp)
|
||||
if parsed.Content == "" && streamedText.Len() > 0 {
|
||||
|
||||
@@ -419,6 +419,84 @@ func TestCodexProvider_ChatRoundTrip_OutputTextDeltaFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexProvider_ChatRoundTrip_OutputItemDoneFallback(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/responses" {
|
||||
http.Error(w, "not found: "+r.URL.Path, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var reqBody map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if reqBody["stream"] != true {
|
||||
http.Error(w, "stream must be true", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
item := map[string]any{
|
||||
"id": "fc_1",
|
||||
"type": "function_call",
|
||||
"call_id": "call_abc",
|
||||
"name": "write_file",
|
||||
"arguments": `{"path":"x.txt","content":"ok"}`,
|
||||
"status": "completed",
|
||||
}
|
||||
resp := map[string]any{
|
||||
"id": "resp_test",
|
||||
"object": "response",
|
||||
"status": "completed",
|
||||
"output": []map[string]any{},
|
||||
}
|
||||
writeOutputItemDoneSSE(w, item, resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := NewCodexProvider("test-token", "acc-123")
|
||||
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
||||
|
||||
resp, err := provider.Chat(
|
||||
t.Context(),
|
||||
[]Message{{Role: "user", Content: "Create x.txt"}},
|
||||
[]ToolDefinition{
|
||||
{
|
||||
Type: "function",
|
||||
Function: ToolFunctionDefinition{
|
||||
Name: "write_file",
|
||||
Description: "write file",
|
||||
Parameters: map[string]any{"type": "object"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"gpt-5.5",
|
||||
map[string]any{},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error: %v", err)
|
||||
}
|
||||
if len(resp.ToolCalls) != 1 {
|
||||
t.Fatalf("len(ToolCalls) = %d, want 1", len(resp.ToolCalls))
|
||||
}
|
||||
tc := resp.ToolCalls[0]
|
||||
if tc.ID != "call_abc" {
|
||||
t.Errorf("ToolCall.ID = %q, want %q", tc.ID, "call_abc")
|
||||
}
|
||||
if tc.Name != "write_file" {
|
||||
t.Errorf("ToolCall.Name = %q, want %q", tc.Name, "write_file")
|
||||
}
|
||||
if tc.Arguments["path"] != "x.txt" {
|
||||
t.Errorf("ToolCall.Arguments[path] = %v, want x.txt", tc.Arguments["path"])
|
||||
}
|
||||
if tc.Arguments["content"] != "ok" {
|
||||
t.Errorf("ToolCall.Arguments[content] = %v, want ok", tc.Arguments["content"])
|
||||
}
|
||||
if resp.FinishReason != "tool_calls" {
|
||||
t.Errorf("FinishReason = %q, want %q", resp.FinishReason, "tool_calls")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/responses" {
|
||||
@@ -713,3 +791,25 @@ func writeOutputTextDeltaSSE(w http.ResponseWriter, delta string, response map[s
|
||||
fmt.Fprintf(w, "data: %s\n\n", string(completedBytes))
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
}
|
||||
|
||||
func writeOutputItemDoneSSE(w http.ResponseWriter, item map[string]any, response map[string]any) {
|
||||
itemEvent := map[string]any{
|
||||
"type": "response.output_item.done",
|
||||
"sequence_number": 1,
|
||||
"output_index": 0,
|
||||
"item": item,
|
||||
}
|
||||
completedEvent := map[string]any{
|
||||
"type": "response.completed",
|
||||
"sequence_number": 2,
|
||||
"response": response,
|
||||
}
|
||||
itemBytes, _ := json.Marshal(itemEvent)
|
||||
completedBytes, _ := json.Marshal(completedEvent)
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
fmt.Fprintf(w, "event: response.output_item.done\n")
|
||||
fmt.Fprintf(w, "data: %s\n\n", string(itemBytes))
|
||||
fmt.Fprintf(w, "event: response.completed\n")
|
||||
fmt.Fprintf(w, "data: %s\n\n", string(completedBytes))
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user