Merge pull request #2967 from miruchigawa/main

fix(codex): preserve streamed output text deltas
This commit is contained in:
Mauro
2026-05-31 11:24:20 +02:00
committed by GitHub
2 changed files with 75 additions and 1 deletions
+9 -1
View File
@@ -104,8 +104,12 @@ func (p *CodexProvider) Chat(
defer stream.Close() defer stream.Close()
var resp *responses.Response var resp *responses.Response
var streamedText strings.Builder
for stream.Next() { for stream.Next() {
evt := stream.Current() evt := stream.Current()
if evt.Type == "response.output_text.delta" {
streamedText.WriteString(evt.Delta)
}
if evt.Type == "response.completed" || evt.Type == "response.failed" || evt.Type == "response.incomplete" { if evt.Type == "response.completed" || evt.Type == "response.failed" || evt.Type == "response.incomplete" {
evtResp := evt.Response evtResp := evt.Response
if evtResp.ID != "" { if evtResp.ID != "" {
@@ -153,7 +157,11 @@ func (p *CodexProvider) Chat(
return nil, fmt.Errorf("codex API call: stream ended without completed response") return nil, fmt.Errorf("codex API call: stream ended without completed response")
} }
return orc.ParseResponseFromStruct(resp), nil parsed := orc.ParseResponseFromStruct(resp)
if parsed.Content == "" && streamedText.Len() > 0 {
parsed.Content = streamedText.String()
}
return parsed, nil
} }
func (p *CodexProvider) GetDefaultModel() string { func (p *CodexProvider) GetDefaultModel() string {
@@ -374,6 +374,51 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
} }
} }
func TestCodexProvider_ChatRoundTrip_OutputTextDeltaFallback(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
}
resp := map[string]any{
"id": "resp_test",
"object": "response",
"status": "completed",
"output": nil,
}
writeOutputTextDeltaSSE(w, "OK", 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: "Hello"}},
nil,
"gpt-4o",
map[string]any{},
)
if err != nil {
t.Fatalf("Chat() error: %v", err)
}
if resp.Content != "OK" {
t.Errorf("Content = %q, want %q", resp.Content, "OK")
}
}
func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) { func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/responses" { if r.URL.Path != "/responses" {
@@ -647,3 +692,24 @@ func writeCompletedSSE(w http.ResponseWriter, response map[string]any) {
fmt.Fprintf(w, "data: %s\n\n", string(b)) fmt.Fprintf(w, "data: %s\n\n", string(b))
fmt.Fprintf(w, "data: [DONE]\n\n") fmt.Fprintf(w, "data: [DONE]\n\n")
} }
func writeOutputTextDeltaSSE(w http.ResponseWriter, delta string, response map[string]any) {
deltaEvent := map[string]any{
"type": "response.output_text.delta",
"sequence_number": 1,
"delta": delta,
}
completedEvent := map[string]any{
"type": "response.completed",
"sequence_number": 2,
"response": response,
}
deltaBytes, _ := json.Marshal(deltaEvent)
completedBytes, _ := json.Marshal(completedEvent)
w.Header().Set("Content-Type", "text/event-stream")
fmt.Fprintf(w, "event: response.output_text.delta\n")
fmt.Fprintf(w, "data: %s\n\n", string(deltaBytes))
fmt.Fprintf(w, "event: response.completed\n")
fmt.Fprintf(w, "data: %s\n\n", string(completedBytes))
fmt.Fprintf(w, "data: [DONE]\n\n")
}