20 // chatgptURL is the Responses endpoint of the ChatGPT backend.
21 chatgptURL = "https://chatgpt.com/backend-api/codex/responses"
23 // apiURL is the Responses endpoint of the OpenAI API.
24 apiURL = "https://api.openai.com/v1/responses"
26 // quotaPrefix starts the names of the quota headers of the ChatGPT
27 // backend, in canonical form.
28 quotaPrefix = "X-Codex-"
30 // turnStateHeader is an x-codex-* header that holds an opaque state of
31 // the backend, not quota.
32 turnStateHeader = "X-Codex-Turn-State"
34 // errorBytes is the most bytes of an error response that go into the
39 // authorizer adds the credentials of a model call to the headers of its
41 type authorizer interface {
42 Authorize(ctx context.Context, header http.Header) error
45 // apiKey authorizes calls to the OpenAI API with a key.
48 // Client sends model calls to a Responses endpoint for one agent. The ChatGPT
49 // backend and the OpenAI API take the same request body and differ only in the
50 // URL and the credentials. Every call sends the fixed cache key of the agent,
51 // so the turns and the compactions of the agent share one prompt cache. The
52 // ChatGPT backend caches only with the key in the session_id header, so a call
53 // to it sends the key there and in the body. Every call is recorded in the
56 // The request never holds max_output_tokens or metadata, because the ChatGPT
57 // backend refuses them. It sets store to false, so the client sends the whole
58 // input on each call, and it asks for the encrypted reasoning, so a later call
59 // can send the reasoning items back.
68 // Request is one model call. Input holds the input items in order, such as
69 // userMessage values or output items of an earlier response. Node names the
70 // node that a compaction builds, or is nil. It goes only to the usage file,
71 // never to the endpoint.
81 // Tool is the definition of a function tool.
83 Type string `json:"type"`
84 Name string `json:"name"`
85 Description string `json:"description"`
86 Parameters any `json:"parameters"`
87 Strict bool `json:"strict"`
90 // Response is the result of a model call. Output holds the output items as the
91 // endpoint sent them. Text is the text of the message items.
92 type Response struct {
93 Output []json.RawMessage
98 // Usage gives the token counts of a call. Cached is the part of the input
99 // tokens that came from the prompt cache.
106 // requestBody is the JSON body of a call.
107 type requestBody struct {
108 Model string `json:"model"`
109 Instructions string `json:"instructions"`
110 Input []any `json:"input"`
111 Tools []Tool `json:"tools"`
112 Reasoning reasoning `json:"reasoning"`
113 Store bool `json:"store"`
114 Stream bool `json:"stream"`
115 Include []string `json:"include"`
116 PromptCacheKey string `json:"prompt_cache_key"`
119 type reasoning struct {
120 Effort string `json:"effort"`
123 // streamEvent holds the fields of the stream events that the client reads.
124 type streamEvent struct {
125 Type string `json:"type"`
126 Item json.RawMessage `json:"item"`
127 Response *eventResponse `json:"response"`
128 Code string `json:"code"`
129 Message string `json:"message"`
132 type eventResponse struct {
133 Output []json.RawMessage `json:"output"`
134 Usage *wireUsage `json:"usage"`
135 Error *wireError `json:"error"`
136 IncompleteDetails *struct {
137 Reason string `json:"reason"`
138 } `json:"incomplete_details"`
141 type wireUsage struct {
142 InputTokens int `json:"input_tokens"`
143 OutputTokens int `json:"output_tokens"`
144 InputTokensDetails struct {
145 CachedTokens int `json:"cached_tokens"`
146 } `json:"input_tokens_details"`
149 type wireError struct {
150 Code string `json:"code"`
151 Message string `json:"message"`
154 // outputItem holds the fields of an output item that give its text.
155 type outputItem struct {
156 Type string `json:"type"`
158 Type string `json:"type"`
159 Text string `json:"text"`
163 // modelEndpoint returns the URL and the authorizer of the endpoint in config.
164 // The ChatGPT backend uses the token file in dir. The OpenAI API uses the key in
165 // OPENAI_API_KEY, and there is no fallback from one to the other. All clients
166 // share the authorizer, so one refresh runs at a time.
167 func modelEndpoint(config Config, dir string) (string, authorizer, error) {
168 if config.Endpoint == endpointAPI {
169 key := os.Getenv("OPENAI_API_KEY")
171 return "", nil, errors.New("endpoint api needs OPENAI_API_KEY")
174 return apiURL, apiKey(key), nil
177 return chatgptURL, newChatGPTAuth(filepath.Join(dir, tokenFileName), authIssuer), nil
180 // newClient returns a client for the Responses endpoint at url. It records
181 // each call in the usage file at usagePath.
182 func newClient(url string, auth authorizer, cacheKey, usagePath string) *Client {
190 http: &http.Client{},
194 // Create sends the call and reads its event stream to the end.
195 func (c *Client) Create(ctx context.Context, request Request) (Response, error) {
196 body, err := json.Marshal(requestBody{
197 Model: request.Model,
198 Instructions: request.Instructions,
199 Input: request.Input,
200 Tools: request.Tools,
201 Reasoning: reasoning{
202 Effort: request.Effort,
206 Include: []string{"reasoning.encrypted_content"},
207 PromptCacheKey: c.cacheKey,
210 return Response{}, fmt.Errorf("encode request: %w", err)
213 httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(body))
215 return Response{}, fmt.Errorf("create request: %w", err)
218 httpRequest.Header.Set("Content-Type", "application/json")
219 httpRequest.Header.Set("Accept", "text/event-stream")
220 err = c.auth.Authorize(ctx, httpRequest.Header)
222 return Response{}, fmt.Errorf("authorize model call: %w", err)
225 _, chatgpt := c.auth.(*chatgptAuth)
227 httpRequest.Header.Set("session_id", c.cacheKey)
231 record := usageRecord{
232 Date: start.Format(time.RFC3339Nano),
233 Model: request.Model,
236 if request.Node != nil {
237 record.Node = &[2]int{request.Node.level, request.Node.index}
240 response, err := c.send(httpRequest, &record)
242 record.Error = err.Error()
245 record.Seconds = time.Since(start).Seconds()
246 record.InputTokens = response.Usage.Input
247 record.CachedTokens = response.Usage.Cached
248 record.OutputTokens = response.Usage.Output
249 record.TextBytes = len(response.Text)
251 return response, errors.Join(err, c.usage.Write(record))
254 // send makes the HTTP call and reads its stream. It puts the status and the
255 // quota headers into record.
256 func (c *Client) send(httpRequest *http.Request, record *usageRecord) (response Response, err error) {
257 httpResponse, err := c.http.Do(httpRequest)
259 return Response{}, fmt.Errorf("send model call: %w", err)
263 err = errors.Join(err, httpResponse.Body.Close())
266 record.Status = httpResponse.StatusCode
267 record.Quota = quotaHeaders(httpResponse.Header)
268 if httpResponse.StatusCode != http.StatusOK {
269 text, readErr := io.ReadAll(io.LimitReader(httpResponse.Body, errorBytes))
270 err = fmt.Errorf("model call: HTTP %d: %s", httpResponse.StatusCode, strings.TrimSpace(string(text)))
272 return Response{}, errors.Join(err, readErr)
275 return readStream(httpResponse.Body)
278 // usageLog appends one JSON line for each model call to usage.jsonl. Calls can
279 // run at once, so a mutex serializes the writes.
280 type usageLog struct {
285 // usageRecord is one line of usage.jsonl. Date is the start of the call, and
286 // Seconds its length. Node is the node [l, i] of a compaction. Status is the
287 // HTTP status, or 0 when no response came. TextBytes is the size of the text of
288 // the reply. Quota holds the x-codex-* headers of the response, without the
290 type usageRecord struct {
291 Date string `json:"date"`
292 Seconds float64 `json:"seconds"`
293 Model string `json:"model"`
294 Node *[2]int `json:"node,omitempty"`
295 Status int `json:"status"`
296 InputTokens int `json:"input_tokens"`
297 CachedTokens int `json:"cached_tokens"`
298 OutputTokens int `json:"output_tokens"`
299 TextBytes int `json:"text_bytes"`
300 Quota map[string]string `json:"quota,omitempty"`
301 Error string `json:"error,omitempty"`
304 // Write appends record to the file with one write.
305 func (u *usageLog) Write(record usageRecord) (err error) {
306 line, err := json.Marshal(record)
308 return fmt.Errorf("encode usage: %w", err)
312 defer u.mutex.Unlock()
314 file, err := os.OpenFile(u.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
316 return fmt.Errorf("open usage file: %w", err)
320 err = errors.Join(err, file.Close())
323 _, err = file.Write(append(line, '\n'))
325 return fmt.Errorf("write usage: %w", err)
331 // Authorize sets the key as the bearer token.
332 func (k apiKey) Authorize(ctx context.Context, header http.Header) error {
333 header.Set("Authorization", "Bearer "+string(k))
337 // userMessage returns a user input item with one text part for each text.
338 func userMessage(texts ...string) any {
340 Type string `json:"type"`
341 Text string `json:"text"`
344 content := make([]part, len(texts))
345 for n, text := range texts {
353 Role string `json:"role"`
354 Content []part `json:"content"`
361 // quotaHeaders returns the x-codex-* headers, with lowercase names, without the
363 func quotaHeaders(header http.Header) map[string]string {
364 var quota map[string]string
365 for name, values := range header {
366 if !strings.HasPrefix(name, quotaPrefix) || name == turnStateHeader || len(values) == 0 {
371 quota = make(map[string]string)
374 quota[strings.ToLower(name)] = values[0]
380 // readStream reads the server-sent events of a call until the response
381 // completes. The output items come from the output_item.done events, or from
382 // the completed response when the stream sent none.
383 func readStream(stream io.Reader) (Response, error) {
384 var response Response
385 reader := bufio.NewReader(stream)
386 var data bytes.Buffer
388 line, err := reader.ReadBytes('\n')
389 if err != nil && !errors.Is(err, io.EOF) {
390 return response, fmt.Errorf("read event stream: %w", err)
393 line = bytes.TrimRight(line, "\r\n")
395 value, found := bytes.CutPrefix(line, []byte("data:"))
397 data.Write(bytes.TrimPrefix(value, []byte(" ")))
400 } else if data.Len() > 0 {
401 done, eventErr := readEvent(&response, data.Bytes())
403 if eventErr != nil || done {
404 return response, eventErr
408 if errors.Is(err, io.EOF) {
409 return response, errors.New("event stream ended before the response completed")
414 // readEvent adds one stream event to response, and tells if it ends the
416 func readEvent(response *Response, data []byte) (bool, error) {
417 var event streamEvent
418 err := json.Unmarshal(data, &event)
420 return true, fmt.Errorf("decode stream event: %w", err)
424 case "response.output_item.done":
425 response.Output = append(response.Output, event.Item)
426 case "response.completed":
427 if event.Response == nil {
428 return true, errors.New("completed event without a response")
431 if len(response.Output) == 0 {
432 response.Output = event.Response.Output
435 if event.Response.Usage != nil {
436 response.Usage = Usage{
437 Input: event.Response.Usage.InputTokens,
438 Cached: event.Response.Usage.InputTokensDetails.CachedTokens,
439 Output: event.Response.Usage.OutputTokens,
443 response.Text, err = outputText(response.Output)
446 case "response.incomplete":
448 if event.Response != nil && event.Response.IncompleteDetails != nil {
449 reason = event.Response.IncompleteDetails.Reason
452 return true, fmt.Errorf("response incomplete: %s", reason)
453 case "response.failed":
454 message := "no details"
455 if event.Response != nil && event.Response.Error != nil {
456 message = event.Response.Error.Code + ": " + event.Response.Error.Message
459 return true, fmt.Errorf("response failed: %s", message)
461 return true, fmt.Errorf("stream error: %s: %s", event.Code, event.Message)
467 // outputText returns the text of the message items of output.
468 func outputText(output []json.RawMessage) (string, error) {
469 var text strings.Builder
470 for _, raw := range output {
472 err := json.Unmarshal(raw, &item)
474 return "", fmt.Errorf("decode output item: %w", err)
477 if item.Type != "message" {
481 for _, part := range item.Content {
482 if part.Type == "output_text" {
483 text.WriteString(part.Text)
488 return text.String(), nil