openai_responses_continuation_test.go RawHistoryFolder
1package provider
2
3import (
4 "context"
5 "strings"
6 "testing"
7)
8
9// runResponsesCompletionFixture feeds scripted Responses SSE payloads
10// through the shared completion parser and returns the terminal event
11// plus the reported usage.
12func runResponsesCompletionFixture(t *testing.T, payloads ...string) (EventDone, Usage) {
13 t.Helper()
14 c := newOpenAICodexClient("token", "acct", "")
15 raw := make(chan sseEvent, len(payloads)+1)
16 for _, payload := range payloads {
17 raw <- sseEvent{Data: payload}
18 }
19 close(raw)
20 out := make(chan Event, 32)
21 go c.runResponseEventsWithFirst(context.Background(), Request{Model: "gpt-5.2"}, out, raw, nil, nil)
22 var usage Usage
23 for ev := range out {
24 switch e := ev.(type) {
25 case EventUsage:
26 usage = e.Usage
27 case EventDone:
28 return e, usage
29 }
30 }
31 t.Fatal("parser stream ended without EventDone")
32 return EventDone{}, Usage{}
33}
34
35func completedPayload(event, response string) string {
36 return `{"type":"` + event + `","response":{` + response + `}}`
37}
38
39func TestResponsesContinuation(t *testing.T) {
40 textItem := []string{
41 `{"type":"response.output_item.added","output_index":0,"item":{"type":"message"}}`,
42 `{"type":"response.output_text.delta","output_index":0,"delta":"part one"}`,
43 }
44 usage := `"id":"resp-1","usage":{"input_tokens":3,"output_tokens":4}`
45
46 for _, tt := range []struct {
47 name string
48 event string
49 endTurn string // raw end_turn field, empty when absent
50 want StopReason
51 }{
52 {name: "explicit false continues", event: "response.completed", endTurn: `"end_turn":false`, want: StopContinue},
53 {name: "done alias continues", event: "response.done", endTurn: `"end_turn":false`, want: StopContinue},
54 {name: "explicit true ends", event: "response.completed", endTurn: `"end_turn":true`, want: StopEnd},
55 {name: "absent ends", event: "response.completed", endTurn: ``, want: StopEnd},
56 {name: "null ends", event: "response.completed", endTurn: `"end_turn":null`, want: StopEnd},
57 } {
58 t.Run(tt.name, func(t *testing.T) {
59 response := usage
60 if tt.endTurn != "" {
61 response += "," + tt.endTurn
62 }
63 payloads := append(append([]string{}, textItem...), completedPayload(tt.event, response))
64 done, gotUsage := runResponsesCompletionFixture(t, payloads...)
65 if done.Stop != tt.want {
66 t.Fatalf("stop = %q, want %q", done.Stop, tt.want)
67 }
68 if done.Err != nil {
69 t.Fatalf("err = %v, want nil", done.Err)
70 }
71 if gotUsage.InputTokens != 3 || gotUsage.OutputTokens != 4 {
72 t.Fatalf("usage = %+v, want input 3 output 4", gotUsage)
73 }
74 var text strings.Builder
75 for _, c := range done.Message.Content {
76 if tb, ok := c.(TextBlock); ok {
77 text.WriteString(tb.Text)
78 }
79 }
80 if text.String() != "part one" {
81 t.Fatalf("text = %q, want %q", text.String(), "part one")
82 }
83 })
84 }
85}
86
87func TestResponsesContinuationReasoningOnly(t *testing.T) {
88 done, _ := runResponsesCompletionFixture(t,
89 `{"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs_1","encrypted_content":"blob"}}`,
90 completedPayload("response.completed", `"id":"resp-1","end_turn":false,"usage":{"input_tokens":1,"output_tokens":1}`),
91 )
92 if done.Stop != StopContinue {
93 t.Fatalf("stop = %q, want %q", done.Stop, StopContinue)
94 }
95 var sawReasoning bool
96 for _, c := range done.Message.Content {
97 if rb, ok := c.(ReasoningBlock); ok && rb.ID == "rs_1" && rb.Encrypted == "blob" {
98 sawReasoning = true
99 }
100 }
101 if !sawReasoning {
102 t.Fatalf("reasoning not preserved: %#v", done.Message.Content)
103 }
104}
105
106func TestResponsesContinuationToolCallPrecedence(t *testing.T) {
107 toolItem := []string{
108 `{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","call_id":"call-1","name":"echo"}}`,
109 `{"type":"response.function_call_arguments.delta","output_index":0,"delta":"{}"}`,
110 `{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call"}}`,
111 }
112 for _, endTurn := range []string{`"end_turn":false`, `"end_turn":true`, ``} {
113 response := `"id":"resp-1","usage":{"input_tokens":1,"output_tokens":1}`
114 if endTurn != "" {
115 response += "," + endTurn
116 }
117 payloads := append(append([]string{}, toolItem...),
118 completedPayload("response.completed", response))
119 done, _ := runResponsesCompletionFixture(t, payloads...)
120 if done.Stop != StopToolUse {
121 t.Fatalf("end_turn %s with tool call: stop = %q, want %q", endTurn, done.Stop, StopToolUse)
122 }
123 var sawCall bool
124 for _, c := range done.Message.Content {
125 if tc, ok := c.(ToolCallBlock); ok && tc.ID == "call-1" && tc.Name == "echo" {
126 sawCall = true
127 }
128 }
129 if !sawCall {
130 t.Fatalf("tool call not preserved: %#v", done.Message.Content)
131 }
132 }
133}
134
135func TestResponsesContinuationPreservesErrorPaths(t *testing.T) {
136 done, _ := runResponsesCompletionFixture(t,
137 `{"type":"response.failed","response":{"error":{"message":"boom"}}}`,
138 )
139 if done.Stop != StopError || done.Err == nil {
140 t.Fatalf("failed response: stop = %q err = %v, want error", done.Stop, done.Err)
141 }
142}