Merge pull request #3007 from SebastianBoehler/codex/fix-codex-oauth-stream-tools

fix: preserve streamed Codex tool calls
This commit is contained in:
Mauro
2026-06-04 21:24:54 +02:00
committed by GitHub
2 changed files with 110 additions and 0 deletions
+10
View File
@@ -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 {
+100
View File
@@ -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")
}