19 // quotaPrefix starts the names of the quota headers of the ChatGPT
20 // backend, in canonical form.
21 quotaPrefix = "X-Codex-"
23 // errorBytes is the most bytes of an error response that go into the
28 // authorizer adds the credentials of a model call to the headers of its
30 type authorizer interface {
31 Authorize(ctx context.Context, header http.Header) error
34 // apiKey authorizes calls to the OpenAI API with a key.
37 // Client sends model calls to a Responses endpoint for one agent. The ChatGPT
38 // backend and the OpenAI API take the same request body and differ only in the
39 // URL and the credentials. Every call sends the fixed cache key of the agent,
40 // so the turns and the compactions of the agent share one prompt cache. Every
41 // call is recorded in the usage file.
43 // The request never holds max_output_tokens or metadata, because the ChatGPT
44 // backend refuses them. It sets store to false, so the client sends the whole
45 // input on each call, and it asks for the encrypted reasoning, so a later call
46 // can send the reasoning items back.
53 clock func() time.Time
56 // Request is one model call. Input holds the input items in order, such as
57 // userMessage values or output items of an earlier response.
66 // Tool is the definition of a function tool.
68 Type string `json:"type"`
69 Name string `json:"name"`
70 Description string `json:"description"`
71 Parameters any `json:"parameters"`
72 Strict bool `json:"strict"`
75 // Response is the result of a model call. Output holds the output items as the
76 // endpoint sent them. Text is the text of the message items.
77 type Response struct {
78 Output []json.RawMessage
83 // Usage gives the token counts of a call. Cached is the part of the input
84 // tokens that came from the prompt cache.
91 // requestBody is the JSON body of a call.
92 type requestBody struct {
93 Model string `json:"model"`
94 Instructions string `json:"instructions"`
95 Input []any `json:"input"`
96 Tools []Tool `json:"tools"`
97 Reasoning reasoning `json:"reasoning"`
98 Store bool `json:"store"`
99 Stream bool `json:"stream"`
100 Include []string `json:"include"`
101 PromptCacheKey string `json:"prompt_cache_key"`
104 type reasoning struct {
105 Effort string `json:"effort"`
108 // streamEvent holds the fields of the stream events that the client reads.
109 type streamEvent struct {
110 Type string `json:"type"`
111 Item json.RawMessage `json:"item"`
112 Response *eventResponse `json:"response"`
113 Code string `json:"code"`
114 Message string `json:"message"`
117 type eventResponse struct {
118 Output []json.RawMessage `json:"output"`
119 Usage *wireUsage `json:"usage"`
120 Error *wireError `json:"error"`
121 IncompleteDetails *struct {
122 Reason string `json:"reason"`
123 } `json:"incomplete_details"`
126 type wireUsage struct {
127 InputTokens int `json:"input_tokens"`
128 OutputTokens int `json:"output_tokens"`
129 InputTokensDetails struct {
130 CachedTokens int `json:"cached_tokens"`
131 } `json:"input_tokens_details"`
134 type wireError struct {
135 Code string `json:"code"`
136 Message string `json:"message"`
139 // outputItem holds the fields of an output item that give its text.
140 type outputItem struct {
141 Type string `json:"type"`
143 Type string `json:"type"`
144 Text string `json:"text"`
148 // newClient returns a client for the Responses endpoint at url. It records
149 // each call in the usage file at usagePath.
150 func newClient(url string, auth authorizer, cacheKey, usagePath string) *Client {
158 http: &http.Client{},
163 // Create sends the call and reads its event stream to the end.
164 func (c *Client) Create(ctx context.Context, request Request) (Response, error) {
165 body, err := json.Marshal(requestBody{
166 Model: request.Model,
167 Instructions: request.Instructions,
168 Input: request.Input,
169 Tools: request.Tools,
170 Reasoning: reasoning{
171 Effort: request.Effort,
175 Include: []string{"reasoning.encrypted_content"},
176 PromptCacheKey: c.cacheKey,
179 return Response{}, fmt.Errorf("encode request: %w", err)
182 httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(body))
184 return Response{}, fmt.Errorf("create request: %w", err)
187 httpRequest.Header.Set("Content-Type", "application/json")
188 httpRequest.Header.Set("Accept", "text/event-stream")
189 err = c.auth.Authorize(ctx, httpRequest.Header)
191 return Response{}, fmt.Errorf("authorize model call: %w", err)
194 record := usageRecord{
195 Date: c.clock().Format(time.RFC3339Nano),
196 Model: request.Model,
199 response, err := c.send(httpRequest, &record)
201 record.Error = err.Error()
204 record.InputTokens = response.Usage.Input
205 record.CachedTokens = response.Usage.Cached
206 record.OutputTokens = response.Usage.Output
208 return response, errors.Join(err, c.usage.Write(record))
211 // send makes the HTTP call and reads its stream. It puts the status and the
212 // quota headers into record.
213 func (c *Client) send(httpRequest *http.Request, record *usageRecord) (response Response, err error) {
214 httpResponse, err := c.http.Do(httpRequest)
216 return Response{}, fmt.Errorf("send model call: %w", err)
220 err = errors.Join(err, httpResponse.Body.Close())
223 record.Status = httpResponse.StatusCode
224 record.Quota = quotaHeaders(httpResponse.Header)
225 if httpResponse.StatusCode != http.StatusOK {
226 text, readErr := io.ReadAll(io.LimitReader(httpResponse.Body, errorBytes))
227 err = fmt.Errorf("model call: HTTP %d: %s", httpResponse.StatusCode, strings.TrimSpace(string(text)))
229 return Response{}, errors.Join(err, readErr)
232 return readStream(httpResponse.Body)
235 // usageLog appends one JSON line for each model call to usage.jsonl. Calls can
236 // run at once, so a mutex serializes the writes.
237 type usageLog struct {
242 // usageRecord is one line of usage.jsonl. Status is the HTTP status, or 0 when
243 // no response came. Quota holds the x-codex-* headers of the response.
244 type usageRecord struct {
245 Date string `json:"date"`
246 Model string `json:"model"`
247 Status int `json:"status"`
248 InputTokens int `json:"input_tokens"`
249 CachedTokens int `json:"cached_tokens"`
250 OutputTokens int `json:"output_tokens"`
251 Quota map[string]string `json:"quota,omitempty"`
252 Error string `json:"error,omitempty"`
255 // Write appends record to the file with one write.
256 func (u *usageLog) Write(record usageRecord) (err error) {
257 line, err := json.Marshal(record)
259 return fmt.Errorf("encode usage: %w", err)
263 defer u.mutex.Unlock()
265 file, err := os.OpenFile(u.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
267 return fmt.Errorf("open usage file: %w", err)
271 err = errors.Join(err, file.Close())
274 _, err = file.Write(append(line, '\n'))
276 return fmt.Errorf("write usage: %w", err)
282 // Authorize sets the key as the bearer token.
283 func (k apiKey) Authorize(ctx context.Context, header http.Header) error {
284 header.Set("Authorization", "Bearer "+string(k))
288 // userMessage returns a user input item with one text part for each text.
289 func userMessage(texts ...string) any {
291 Type string `json:"type"`
292 Text string `json:"text"`
295 content := make([]part, len(texts))
296 for n, text := range texts {
304 Role string `json:"role"`
305 Content []part `json:"content"`
312 // quotaHeaders returns the x-codex-* headers, with lowercase names.
313 func quotaHeaders(header http.Header) map[string]string {
314 var quota map[string]string
315 for name, values := range header {
316 if !strings.HasPrefix(name, quotaPrefix) || len(values) == 0 {
321 quota = make(map[string]string)
324 quota[strings.ToLower(name)] = values[0]
330 // readStream reads the server-sent events of a call until the response
331 // completes. The output items come from the output_item.done events, or from
332 // the completed response when the stream sent none.
333 func readStream(stream io.Reader) (Response, error) {
334 var response Response
335 reader := bufio.NewReader(stream)
336 var data bytes.Buffer
338 line, err := reader.ReadBytes('\n')
339 if err != nil && !errors.Is(err, io.EOF) {
340 return response, fmt.Errorf("read event stream: %w", err)
343 line = bytes.TrimRight(line, "\r\n")
345 value, found := bytes.CutPrefix(line, []byte("data:"))
347 data.Write(bytes.TrimPrefix(value, []byte(" ")))
350 } else if data.Len() > 0 {
351 done, eventErr := readEvent(&response, data.Bytes())
353 if eventErr != nil || done {
354 return response, eventErr
358 if errors.Is(err, io.EOF) {
359 return response, errors.New("event stream ended before the response completed")
364 // readEvent adds one stream event to response, and tells if it ends the
366 func readEvent(response *Response, data []byte) (bool, error) {
367 var event streamEvent
368 err := json.Unmarshal(data, &event)
370 return true, fmt.Errorf("decode stream event: %w", err)
374 case "response.output_item.done":
375 response.Output = append(response.Output, event.Item)
376 case "response.completed":
377 if event.Response == nil {
378 return true, errors.New("completed event without a response")
381 if len(response.Output) == 0 {
382 response.Output = event.Response.Output
385 if event.Response.Usage != nil {
386 response.Usage = Usage{
387 Input: event.Response.Usage.InputTokens,
388 Cached: event.Response.Usage.InputTokensDetails.CachedTokens,
389 Output: event.Response.Usage.OutputTokens,
393 response.Text, err = outputText(response.Output)
396 case "response.incomplete":
398 if event.Response != nil && event.Response.IncompleteDetails != nil {
399 reason = event.Response.IncompleteDetails.Reason
402 return true, fmt.Errorf("response incomplete: %s", reason)
403 case "response.failed":
404 message := "no details"
405 if event.Response != nil && event.Response.Error != nil {
406 message = event.Response.Error.Code + ": " + event.Response.Error.Message
409 return true, fmt.Errorf("response failed: %s", message)
411 return true, fmt.Errorf("stream error: %s: %s", event.Code, event.Message)
417 // outputText returns the text of the message items of output.
418 func outputText(output []json.RawMessage) (string, error) {
419 var text strings.Builder
420 for _, raw := range output {
422 err := json.Unmarshal(raw, &item)
424 return "", fmt.Errorf("decode output item: %w", err)
427 if item.Type != "message" {
431 for _, part := range item.Content {
432 if part.Type == "output_text" {
433 text.WriteString(part.Text)
438 return text.String(), nil