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