1package provider
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net/http"
11 "runtime"
12 "strings"
13 "time"
14)
15
16// Codex (ChatGPT subscription) client. Uses OpenAI's Responses API via
17// chatgpt.com/backend-api with the chatgpt-account-id handshake.
18//
19// Wire protocol notes:
20// - Endpoint: POST https://chatgpt.com/backend-api/codex/responses
21// - Headers: Authorization: Bearer <access_token>, chatgpt-account-id: <id>,
22// OpenAI-Beta: responses=experimental, originator: zot (required legacy value)
23// - Body: OpenAI Responses API shape (not chat/completions).
24// input: [{role, content: [{type: "input_text" | "input_image" | ... }]}]
25// instructions: <system prompt>
26// tools: [{type:"function", name, description, parameters, strict}]
27// stream: true
28// - SSE events: response.output_item.added, response.output_text.delta,
29// response.function_call_arguments.delta, response.output_item.done,
30// response.completed, response.failed, error
31//
32// The access_token comes from the OpenAI OAuth flow (auth.openai.com).
33// The accountID is parsed from the id_token JWT's `chatgpt_account_id`
34// claim; see auth/oauth.go.
35
36const codexDefaultBaseURL = "https://chatgpt.com/backend-api/codex/responses"
37
38type codexClient struct {
39 token string
40 accountID string
41 baseURL string
42 errorLabel string
43 providerName string
44 modelName func(string) string
45 disableCLIRouting bool
46 cliRoutingAll bool
47 capabilities responsesCapabilities
48 http *http.Client
49}
50
51// responsesCapabilities records extensions verified for the exact provider
52// client, rather than assuming every Responses-shaped endpoint accepts them.
53type responsesCapabilities struct {
54 StablePromptCacheKey bool
55 ExplicitPromptCacheBreakpoint bool
56 SessionHeader bool
57 ThreadHeader bool
58 ClientRequestHeader bool
59}
60
61// NewOpenAICodex creates a Responses client for ChatGPT's Codex endpoint
62// using a subscription OAuth access token and the user's ChatGPT account ID.
63// It prefers WebSocket mode and falls back to HTTP/SSE before visible output.
64// baseURL may be empty to use the default.
65func NewOpenAICodex(token, accountID, baseURL string) Client {
66 return newResponsesWebSocketClient(newOpenAICodexClient(token, accountID, baseURL))
67}
68
69func newOpenAICodexClient(token, accountID, baseURL string) *codexClient {
70 if baseURL == "" {
71 baseURL = codexDefaultBaseURL
72 }
73 return &codexClient{
74 token: token,
75 accountID: accountID,
76 baseURL: strings.TrimRight(baseURL, "/"),
77 errorLabel: "codex",
78 providerName: "openai-codex",
79 // The ChatGPT Codex backend load-sheds requests from client
80 // identities it does not recognize: unknown originator/user-agent
81 // pairs receive "Our servers are currently overloaded" stream
82 // errors near-deterministically while Codex CLI requests succeed.
83 // Send the Codex CLI request shape for every model on this
84 // backend.
85 cliRoutingAll: true,
86 capabilities: responsesCapabilities{
87 StablePromptCacheKey: true,
88 SessionHeader: true,
89 ThreadHeader: true,
90 ClientRequestHeader: true,
91 },
92 http: &http.Client{Timeout: 0},
93 }
94}
95
96func (c *codexClient) Name() string { return "openai-codex" }
97
98// ---- Responses API wire types (subset needed for zut's surface) ----
99
100type codexInputText struct {
101 Type string `json:"type"` // "input_text"
102 Text string `json:"text"`
103 PromptCacheBreakpoint *codexPromptCacheBreakpoint `json:"prompt_cache_breakpoint,omitempty"`
104}
105
106type codexInputImage struct {
107 Type string `json:"type"` // "input_image"
108 Detail string `json:"detail"`
109 ImageURL string `json:"image_url"`
110}
111
112type codexOutputText struct {
113 Type string `json:"type"` // "output_text"
114 Text string `json:"text"`
115 Annotations []any `json:"annotations"`
116}
117
118type codexPromptCacheOptions struct {
119 Mode string `json:"mode"`
120}
121
122type codexPromptCacheBreakpoint struct {
123 Mode string `json:"mode"`
124}
125
126type codexInputMessage struct {
127 Type string `json:"type,omitempty"` // "message" (optional for input)
128 Role string `json:"role"`
129 Content []any `json:"content"`
130}
131
132type codexOutputMessage struct {
133 Type string `json:"type"` // "message"
134 Role string `json:"role"`
135 Status string `json:"status,omitempty"`
136 ID string `json:"id,omitempty"`
137 Content []codexOutputText `json:"content"`
138}
139
140type codexFunctionCall struct {
141 Type string `json:"type"` // "function_call"
142 ID string `json:"id,omitempty"`
143 CallID string `json:"call_id"`
144 Name string `json:"name"`
145 Arguments string `json:"arguments"` // JSON string
146}
147
148type codexFunctionCallOutput struct {
149 Type string `json:"type"` // "function_call_output"
150 CallID string `json:"call_id"`
151 Output string `json:"output"` // string (or ResponseFunctionCallOutputItemList for images; v1 only uses string)
152}
153
154// codexReasoningItem mirrors the Responses API "reasoning" output item.
155// We capture it on incoming streams and replay it verbatim on follow-up
156// requests: the API rejects assistant tool-call replays without it when
157// thinking is enabled.
158type codexReasoningItem struct {
159 Type string `json:"type"` // "reasoning"
160 ID string `json:"id,omitempty"`
161 EncryptedContent string `json:"encrypted_content,omitempty"`
162 // Summary is required by the Responses API even when no summary text
163 // was streamed; encode an empty array rather than omitting the field.
164 Summary []codexReasoningSummary `json:"summary"`
165}
166
167type codexReasoningSummary struct {
168 Type string `json:"type"` // "summary_text"
169 Text string `json:"text"`
170}
171
172type codexTool struct {
173 Type string `json:"type"` // "function"
174 Name string `json:"name"`
175 Description string `json:"description,omitempty"`
176 Parameters json.RawMessage `json:"parameters"`
177 // Responses otherwise attempts to normalize schemas into strict mode,
178 // which makes non-nullable optional properties required model output.
179 Strict bool `json:"strict"`
180}
181
182type codexReasoningConfig struct {
183 Effort string `json:"effort,omitempty"`
184}
185
186type codexRequest struct {
187 Model string `json:"model"`
188 Store bool `json:"store"`
189 Stream bool `json:"stream,omitempty"`
190 Instructions string `json:"instructions,omitempty"`
191 Input []any `json:"input"`
192 Tools []codexTool `json:"tools,omitempty"`
193 ToolChoice string `json:"tool_choice,omitempty"`
194 ParallelToolCalls bool `json:"parallel_tool_calls"`
195 Include []string `json:"include,omitempty"`
196 Reasoning *codexReasoningConfig `json:"reasoning,omitempty"`
197 PromptCacheKey string `json:"prompt_cache_key,omitempty"`
198 PromptCacheOptions *codexPromptCacheOptions `json:"prompt_cache_options,omitempty"`
199 ServiceTier string `json:"service_tier,omitempty"`
200 PreviousResponseID string `json:"previous_response_id,omitempty"`
201}
202
203// ---- Request building ----
204
205func (c *codexClient) findModel(id string) (Model, error) {
206 if c.providerName != "" && c.providerName != "openai-codex" {
207 if m, err := FindModel(c.providerName, id); err == nil {
208 return m, nil
209 }
210 }
211 if m, err := FindModel("openai-codex", id); err == nil {
212 return m, nil
213 }
214 return FindModel("openai", id)
215}
216
217func supportsOpenAIExplicitPromptCache(model string) bool {
218 id := strings.ToLower(strings.TrimSpace(model))
219 return strings.HasPrefix(id, "gpt-5.6-") || id == "gpt-6-astra"
220}
221
222func (c *codexClient) buildRequest(req Request) (*codexRequest, error) {
223 providerName := c.providerName
224 if providerName == "" {
225 providerName = "openai-codex"
226 }
227 if err := ValidateFastMode(providerName, req.FastMode); err != nil {
228 return nil, err
229 }
230 m, err := c.findModel(req.Model)
231 if err != nil {
232 return nil, err
233 }
234 reasoning := ClampReasoningForModel(m, req.Reasoning)
235
236 body := &codexRequest{
237 Model: req.Model,
238 Store: false,
239 Stream: true,
240 Instructions: req.SystemPrompt(),
241 ParallelToolCalls: true,
242 Include: []string{"reasoning.encrypted_content"},
243 }
244 if req.FastMode {
245 body.ServiceTier = fastModeServiceTier
246 }
247 if c.capabilities.ExplicitPromptCacheBreakpoint && supportsOpenAIExplicitPromptCache(req.Model) {
248 // Keep implicit mode so OpenAI can retain later conversation
249 // checkpoints as well as this stable instruction/tool boundary.
250 body.PromptCacheOptions = &codexPromptCacheOptions{Mode: "implicit"}
251 body.Input = append(body.Input, codexInputMessage{
252 Role: "developer",
253 Content: []any{codexInputText{
254 Type: "input_text",
255 Text: "[Stable prompt boundary]",
256 PromptCacheBreakpoint: &codexPromptCacheBreakpoint{Mode: "explicit"},
257 }},
258 })
259 }
260 if m.Reasoning {
261 effort := OpenAICodexReasoningEffort(reasoning, req.Model)
262 if hasReasoningLevelOverride(m, req.Reasoning) {
263 effort = reasoning
264 }
265 if effort != "" {
266 body.Reasoning = &codexReasoningConfig{Effort: effort}
267 }
268 }
269 activeTools := activeToolDefinitions(req.Tools, req.Messages)
270 if len(activeTools) > 0 {
271 body.ToolChoice = "auto"
272 for _, t := range activeTools {
273 params := t.Schema
274 if len(params) == 0 {
275 params = json.RawMessage(`{"type":"object","properties":{}}`)
276 }
277 body.Tools = append(body.Tools, codexTool{
278 Type: "function",
279 Name: t.Name,
280 Description: t.Description,
281 Parameters: params,
282 })
283 }
284 }
285
286 msgIdx := 0
287 req.Messages = RepairOrphanedToolResults(req.Messages)
288 for _, msg := range req.Messages {
289 switch msg.Role {
290 case RoleDeveloper, RoleUser:
291 content := []any{}
292 for _, c := range msg.Content {
293 switch v := c.(type) {
294 case TextBlock:
295 if v.Text != "" {
296 content = append(content, codexInputText{Type: "input_text", Text: v.Text})
297 }
298 case ImageBlock:
299 url := "data:" + v.MimeType + ";base64," + base64.StdEncoding.EncodeToString(v.Data)
300 content = append(content, codexInputImage{Type: "input_image", Detail: "auto", ImageURL: url})
301 }
302 }
303 if len(content) == 0 {
304 continue
305 }
306 role := "user"
307 if msg.Role == RoleDeveloper {
308 role = "developer"
309 }
310 body.Input = append(body.Input, codexInputMessage{Role: role, Content: content})
311 case RoleAssistant:
312 // Emit one output_message per text block, one function_call per
313 // tool call, and one reasoning item per ReasoningBlock,
314 // preserving the order so the model sees the same interleaving
315 // we captured. The reasoning replay is what keeps OpenAI
316 // Codex from rejecting follow-up tool calls with
317 // "thinking is enabled but reasoning_content is missing".
318 for _, c := range msg.Content {
319 switch v := c.(type) {
320 case ReasoningBlock:
321 item := codexReasoningItem{
322 Type: "reasoning",
323 ID: v.ID,
324 EncryptedContent: v.Encrypted,
325 Summary: []codexReasoningSummary{},
326 }
327 if v.Summary != "" {
328 item.Summary = []codexReasoningSummary{{Type: "summary_text", Text: v.Summary}}
329 }
330 body.Input = append(body.Input, item)
331 case TextBlock:
332 if v.Text == "" {
333 continue
334 }
335 msgIdx++
336 body.Input = append(body.Input, codexOutputMessage{
337 Type: "message",
338 Role: "assistant",
339 Status: "completed",
340 ID: fmt.Sprintf("msg_%d", msgIdx),
341 Content: []codexOutputText{
342 {Type: "output_text", Text: v.Text, Annotations: []any{}},
343 },
344 })
345 case ToolCallBlock:
346 args := string(v.Arguments)
347 if args == "" || !json.Valid([]byte(args)) {
348 args = "{}"
349 }
350 callID, _ := splitCallID(v.ID)
351 body.Input = append(body.Input, codexFunctionCall{
352 Type: "function_call",
353 CallID: callID,
354 Name: v.Name,
355 Arguments: args,
356 })
357 }
358 }
359 case RoleTool:
360 for _, c := range msg.Content {
361 if tr, ok := c.(ToolResultBlock); ok {
362 var text strings.Builder
363 imageCount := 0
364 for _, inner := range tr.Content {
365 switch v := inner.(type) {
366 case TextBlock:
367 if text.Len() > 0 {
368 text.WriteString("\n")
369 }
370 text.WriteString(v.Text)
371 case ImageBlock:
372 imageCount++
373 }
374 }
375 // The Responses API function_call_output only carries a
376 // string, so image bytes cannot ride along here. The agent
377 // loop mirrors any tool-result images into the following
378 // user message (which this client does serialize as
379 // input_image). Leave a short text note so an image-only
380 // result is not an empty output the API may reject, and so
381 // the model knows the image arrives next.
382 out := text.String()
383 if out == "" && imageCount > 0 {
384 if imageCount == 1 {
385 out = "[image returned; see the following message]"
386 } else {
387 out = fmt.Sprintf("[%d images returned; see the following message]", imageCount)
388 }
389 }
390 callID, _ := splitCallID(tr.CallID)
391 body.Input = append(body.Input, codexFunctionCallOutput{
392 Type: "function_call_output",
393 CallID: callID,
394 Output: out,
395 })
396 }
397 }
398 }
399 }
400
401 return body, nil
402}
403
404func splitCallID(id string) (string, string) {
405 if i := strings.Index(id, "|"); i >= 0 {
406 return id[:i], id[i+1:]
407 }
408 return id, ""
409}
410
411const minOpenAIPromptCacheTokens = 1024
412
413func responsesCacheDiagnostics(wire *codexRequest, transport, continuation string) CacheDiagnostics {
414 diagnostics := CacheDiagnostics{Mode: "none", Transport: transport, Continuation: continuation}
415 if wire == nil || wire.PromptCacheOptions == nil {
416 return diagnostics
417 }
418 diagnostics.Mode = wire.PromptCacheOptions.Mode
419 diagnostics.Eligible = estimatedOpenAIPromptTokens(wire) >= minOpenAIPromptCacheTokens
420 return diagnostics
421}
422
423// estimatedOpenAIPromptTokens supplies a deliberately conservative local
424// eligibility signal. OpenAI makes the final tokenization and cache decision;
425// this estimate never leaves the process and is not persisted in diagnostics.
426func estimatedOpenAIPromptTokens(wire *codexRequest) int {
427 encoded, err := json.Marshal(wire)
428 if err != nil {
429 return 0
430 }
431 return len(encoded) / 4
432}
433
434func usesCodexCLIRouting(model string) bool {
435 switch model {
436 case "gpt-5.6-luna", "gpt-5.6-luna-pro", "gpt-5.6-terra", "gpt-5.6-terra-pro":
437 return true
438 default:
439 return false
440 }
441}
442
443// ---- Streaming ----
444
445func (c *codexClient) Stream(ctx context.Context, req Request) (<-chan Event, error) {
446 wire, err := c.buildRequest(req)
447 if err != nil {
448 return nil, err
449 }
450 ReportCacheDiagnostics(req.Lifecycle, responsesCacheDiagnostics(wire, "http_sse", "full"))
451 if c.modelName != nil {
452 wire.Model = c.modelName(wire.Model)
453 }
454 useCodexCLIRouting := !c.disableCLIRouting && (c.cliRoutingAll || usesCodexCLIRouting(wire.Model))
455 cacheSessionID := strings.TrimSpace(req.Context.CacheSessionID)
456 if c.capabilities.StablePromptCacheKey && cacheSessionID != "" {
457 wire.PromptCacheKey = cacheSessionID
458 }
459 body, err := json.Marshal(wire)
460 if err != nil {
461 return nil, err
462 }
463
464 newReq := func() (*http.Request, error) {
465 httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL, bytes.NewReader(body))
466 if err != nil {
467 return nil, err
468 }
469 httpReq.Header = c.responsesHeaders(req, useCodexCLIRouting, false)
470 httpReq.Header.Set("content-type", "application/json")
471 httpReq.Header.Set("accept", "text/event-stream")
472 return httpReq, nil
473 }
474
475 resp, err := doStreamWithRetry(ctx, c.http, newReq, req.Lifecycle)
476 if err != nil {
477 return nil, fmt.Errorf("openai-codex: %w", err)
478 }
479 if resp.StatusCode != http.StatusOK {
480 b, _ := io.ReadAll(resp.Body)
481 resp.Body.Close()
482 return nil, fmt.Errorf("openai-codex: http %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
483 }
484
485 out := make(chan Event, 16)
486 go c.runStream(ctx, resp, req, out)
487 return out, nil
488}
489
490func (c *codexClient) responsesHeaders(req Request, useCodexCLIRouting, websocketMode bool) http.Header {
491 headers := make(http.Header)
492 headers.Set("authorization", "Bearer "+c.token)
493 if !websocketMode || c.accountID != "" {
494 headers.Set("openai-beta", "responses=experimental")
495 }
496 if c.accountID != "" {
497 headers.Set("chatgpt-account-id", c.accountID)
498 }
499
500 cacheSessionID := strings.TrimSpace(req.Context.CacheSessionID)
501 threadID := strings.TrimSpace(req.Context.ThreadID)
502 if useCodexCLIRouting {
503 // The ChatGPT Codex backend only admits (and reliably serves)
504 // requests that follow Codex CLI routing metadata; other client
505 // identities are load-shed even when capacity is available.
506 headers.Set("originator", "codex_cli_rs")
507 if c.capabilities.SessionHeader && cacheSessionID != "" {
508 headers.Set("session-id", cacheSessionID)
509 }
510 if c.capabilities.ThreadHeader && threadID != "" {
511 headers.Set("thread-id", threadID)
512 }
513 if c.capabilities.ClientRequestHeader && threadID != "" {
514 headers.Set("x-client-request-id", threadID)
515 }
516 headers.Set("user-agent", "codex_cli_rs/0.0.0")
517 return headers
518 }
519
520 // ChatGPT's backend recognizes these established values. They are
521 // external provider metadata, not zut product identifiers.
522 if c.accountID != "" {
523 headers.Set("originator", "zot")
524 headers.Set("user-agent", fmt.Sprintf("zot (%s %s)", runtime.GOOS, runtime.GOARCH))
525 }
526 return headers
527}
528
529func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Request, out chan<- Event) {
530 defer close(out)
531 defer resp.Body.Close()
532
533 raw := make(chan sseEvent, 16)
534 go readSSE(resp.Body, raw)
535 c.runResponseEvents(ctx, req, out, raw, nil)
536}
537
538// runResponseEvents translates the Responses streaming event protocol into
539// provider events. Both HTTP/SSE and Responses WebSocket mode use the same
540// event vocabulary, so keeping the state machine here prevents the transports
541// from drifting in tool-call, reasoning, and usage handling.
542func (c *codexClient) runResponseEvents(ctx context.Context, req Request, out chan<- Event, raw <-chan sseEvent, completed func(string)) {
543 var observe func(string, Message)
544 if completed != nil {
545 observe = func(responseID string, _ Message) { completed(responseID) }
546 }
547 c.runResponseEventsWithFirst(ctx, req, out, raw, nil, observe)
548}
549
550func (c *codexClient) runResponseEventsWithFirst(ctx context.Context, req Request, out chan<- Event, raw <-chan sseEvent, first *sseEvent, completed func(string, Message)) {
551 model, _ := c.findModel(req.Model)
552 providerName := c.providerName
553 if providerName == "" {
554 providerName = "openai-codex"
555 }
556 out <- EventStart{Model: req.Model, Provider: providerName}
557
558 // Accumulators. The Responses API emits output_items in order; each
559 // item is either a "message" (text) or a "function_call". We track
560 // the in-flight item by its index.
561 type itemState struct {
562 kind string // "message" | "function_call" | "reasoning"
563 callID string
564 name string
565 argsBuf strings.Builder
566 textBuf strings.Builder
567 summary strings.Builder
568 rawID string
569 encrypted string
570 announced bool
571 }
572 var (
573 items = map[int]*itemState{}
574 order []int
575 usage Usage
576 stop = StopEnd
577 finalErr error
578 )
579
580 assemble := func() Message {
581 content := []Content{}
582 for _, idx := range order {
583 it := items[idx]
584 switch it.kind {
585 case "message":
586 if it.textBuf.Len() > 0 {
587 content = append(content, TextBlock{Text: it.textBuf.String()})
588 }
589 case "function_call":
590 args := it.argsBuf.String()
591 if args == "" || !json.Valid([]byte(args)) {
592 args = "{}"
593 }
594 content = append(content, ToolCallBlock{
595 ID: it.callID, Name: it.name, Arguments: json.RawMessage(args),
596 })
597 case "reasoning":
598 if it.encrypted == "" && it.summary.Len() == 0 && it.rawID == "" {
599 continue
600 }
601 content = append(content, ReasoningBlock{
602 ID: it.rawID,
603 Summary: it.summary.String(),
604 Encrypted: it.encrypted,
605 })
606 }
607 }
608 return Message{Role: RoleAssistant, Content: content, Time: time.Now()}
609 }
610
611 sendDone := func() {
612 usage.CostUSD = ComputeCost(model, usage)
613 out <- EventUsage{Usage: usage}
614 out <- EventDone{Stop: stop, Err: finalErr, Message: assemble()}
615 }
616
617 for {
618 var ev sseEvent
619 if first != nil {
620 ev = *first
621 first = nil
622 } else {
623 select {
624 case <-ctx.Done():
625 stop = StopAborted
626 finalErr = ctx.Err()
627 sendDone()
628 return
629 case next, ok := <-raw:
630 if !ok {
631 sendDone()
632 return
633 }
634 ev = next
635 }
636 }
637 if ev.Err != nil {
638 stop = StopError
639 finalErr = fmt.Errorf("read SSE: %w", ev.Err)
640 sendDone()
641 return
642 }
643 var head struct {
644 Type string `json:"type"`
645 }
646 if err := json.Unmarshal([]byte(ev.Data), &head); err != nil {
647 continue
648 }
649
650 switch head.Type {
651 case "response.output_item.added":
652 var p struct {
653 OutputIndex int `json:"output_index"`
654 Item struct {
655 Type string `json:"type"` // "message" | "function_call" | "reasoning"
656 ID string `json:"id"`
657 CallID string `json:"call_id"`
658 Name string `json:"name"`
659 EncryptedContent string `json:"encrypted_content"`
660 } `json:"item"`
661 }
662 _ = json.Unmarshal([]byte(ev.Data), &p)
663 it := &itemState{}
664 switch p.Item.Type {
665 case "message":
666 it.kind = "message"
667 case "function_call":
668 it.kind = "function_call"
669 it.callID = p.Item.CallID
670 it.name = p.Item.Name
671 if !it.announced {
672 it.announced = true
673 out <- EventToolStart{ID: it.callID, Name: it.name}
674 }
675 case "reasoning":
676 it.kind = "reasoning"
677 it.rawID = p.Item.ID
678 it.encrypted = p.Item.EncryptedContent
679 default:
680 continue
681 }
682 items[p.OutputIndex] = it
683 order = append(order, p.OutputIndex)
684 case "response.output_text.delta":
685 var p struct {
686 OutputIndex int `json:"output_index"`
687 Delta string `json:"delta"`
688 }
689 _ = json.Unmarshal([]byte(ev.Data), &p)
690 if it, ok := items[p.OutputIndex]; ok && it.kind == "message" {
691 it.textBuf.WriteString(p.Delta)
692 out <- EventTextDelta{Delta: p.Delta}
693 }
694 case "response.reasoning_summary_text.delta":
695 var p struct {
696 OutputIndex int `json:"output_index"`
697 Delta string `json:"delta"`
698 }
699 _ = json.Unmarshal([]byte(ev.Data), &p)
700 if it, ok := items[p.OutputIndex]; ok && it.kind == "reasoning" {
701 it.summary.WriteString(p.Delta)
702 }
703 case "response.reasoning_summary_text.done":
704 // summary text already accumulated via deltas
705 case "response.function_call_arguments.delta":
706 var p struct {
707 OutputIndex int `json:"output_index"`
708 Delta string `json:"delta"`
709 }
710 _ = json.Unmarshal([]byte(ev.Data), &p)
711 if it, ok := items[p.OutputIndex]; ok && it.kind == "function_call" {
712 it.argsBuf.WriteString(p.Delta)
713 out <- EventToolArgs{ID: it.callID, Delta: p.Delta}
714 }
715 case "response.output_item.done":
716 var p struct {
717 OutputIndex int `json:"output_index"`
718 Item struct {
719 Type string `json:"type"`
720 ID string `json:"id"`
721 EncryptedContent string `json:"encrypted_content"`
722 Summary []struct {
723 Type string `json:"type"`
724 Text string `json:"text"`
725 } `json:"summary"`
726 } `json:"item"`
727 }
728 _ = json.Unmarshal([]byte(ev.Data), &p)
729 if it, ok := items[p.OutputIndex]; ok {
730 switch it.kind {
731 case "function_call":
732 out <- EventToolEnd{ID: it.callID}
733 case "reasoning":
734 if p.Item.EncryptedContent != "" {
735 it.encrypted = p.Item.EncryptedContent
736 }
737 if it.rawID == "" && p.Item.ID != "" {
738 it.rawID = p.Item.ID
739 }
740 for _, s := range p.Item.Summary {
741 if s.Text == "" {
742 continue
743 }
744 if it.summary.Len() > 0 {
745 it.summary.WriteString("\n")
746 }
747 it.summary.WriteString(s.Text)
748 }
749 }
750 }
751 case "response.completed", "response.done":
752 var p struct {
753 Response struct {
754 ID string `json:"id"`
755 Usage struct {
756 InputTokens int `json:"input_tokens"`
757 OutputTokens int `json:"output_tokens"`
758 InputTokensDetails *openAIInputTokensDetails `json:"input_tokens_details"`
759 OutputTokensDetails *struct {
760 ReasoningTokens int `json:"reasoning_tokens"`
761 } `json:"output_tokens_details"`
762 } `json:"usage"`
763 Status string `json:"status"`
764 } `json:"response"`
765 }
766 _ = json.Unmarshal([]byte(ev.Data), &p)
767 usage = normalizeOpenAIUsage(p.Response.Usage.InputTokens, p.Response.Usage.OutputTokens, p.Response.Usage.InputTokensDetails)
768 if details := p.Response.Usage.OutputTokensDetails; details != nil {
769 usage.ReasoningTokens = details.ReasoningTokens
770 usage.ReasoningTokensKnown = true
771 }
772
773 hadTool := false
774 for _, it := range items {
775 if it.kind == "function_call" {
776 hadTool = true
777 break
778 }
779 }
780 if hadTool {
781 stop = StopToolUse
782 } else {
783 stop = StopEnd
784 }
785 if completed != nil && p.Response.ID != "" {
786 completed(p.Response.ID, assemble())
787 }
788 sendDone()
789 return
790 case "response.failed":
791 var p struct {
792 Response struct {
793 Error struct {
794 Message string `json:"message"`
795 } `json:"error"`
796 } `json:"response"`
797 }
798 _ = json.Unmarshal([]byte(ev.Data), &p)
799 stop = StopError
800 finalErr = fmt.Errorf("codex: %s", p.Response.Error.Message)
801 sendDone()
802 return
803 case "error":
804 var p struct {
805 Message string `json:"message"`
806 Code string `json:"code"`
807 Error struct {
808 Message string `json:"message"`
809 Code string `json:"code"`
810 } `json:"error"`
811 }
812 _ = json.Unmarshal([]byte(ev.Data), &p)
813 msg := p.Message
814 if msg == "" {
815 msg = p.Error.Message
816 }
817 if msg == "" {
818 msg = p.Code
819 }
820 if msg == "" {
821 msg = p.Error.Code
822 }
823 if msg == "" {
824 msg = strings.TrimSpace(ev.Data)
825 }
826 stop = StopError
827 label := c.errorLabel
828 if label == "" {
829 label = "codex"
830 }
831 finalErr = fmt.Errorf("%s error: %s", label, msg)
832 sendDone()
833 return
834 }
835 }
836}