]> git.giacomolli.uk - onechat/blobdiff - responses.go
Send the cache key in the session_id header
[onechat] / responses.go
index f887e6d34a455a5da01700ee973e4056ce897b8b..096439f5cc9564d92f8fcfed7636ae60960c91a6 100644 (file)
@@ -10,16 +10,27 @@ import (
        "io"
        "net/http"
        "os"
        "io"
        "net/http"
        "os"
+       "path/filepath"
        "strings"
        "sync"
        "time"
 )
 
 const (
        "strings"
        "sync"
        "time"
 )
 
 const (
+       // chatgptURL is the Responses endpoint of the ChatGPT backend.
+       chatgptURL = "https://chatgpt.com/backend-api/codex/responses"
+
+       // apiURL is the Responses endpoint of the OpenAI API.
+       apiURL = "https://api.openai.com/v1/responses"
+
        // quotaPrefix starts the names of the quota headers of the ChatGPT
        // backend, in canonical form.
        quotaPrefix = "X-Codex-"
 
        // quotaPrefix starts the names of the quota headers of the ChatGPT
        // backend, in canonical form.
        quotaPrefix = "X-Codex-"
 
+       // turnStateHeader is an x-codex-* header that holds an opaque state of
+       // the backend, not quota.
+       turnStateHeader = "X-Codex-Turn-State"
+
        // errorBytes is the most bytes of an error response that go into the
        // error.
        errorBytes = 4096
        // errorBytes is the most bytes of an error response that go into the
        // error.
        errorBytes = 4096
@@ -37,8 +48,10 @@ type apiKey string
 // Client sends model calls to a Responses endpoint for one agent. The ChatGPT
 // backend and the OpenAI API take the same request body and differ only in the
 // URL and the credentials. Every call sends the fixed cache key of the agent,
 // Client sends model calls to a Responses endpoint for one agent. The ChatGPT
 // backend and the OpenAI API take the same request body and differ only in the
 // URL and the credentials. Every call sends the fixed cache key of the agent,
-// so the turns and the compactions of the agent share one prompt cache. Every
-// call is recorded in the usage file.
+// so the turns and the compactions of the agent share one prompt cache. The
+// ChatGPT backend caches only with the key in the session_id header, so a call
+// to it sends the key there and in the body. Every call is recorded in the
+// usage file.
 //
 // The request never holds max_output_tokens or metadata, because the ChatGPT
 // backend refuses them. It sets store to false, so the client sends the whole
 //
 // The request never holds max_output_tokens or metadata, because the ChatGPT
 // backend refuses them. It sets store to false, so the client sends the whole
@@ -50,7 +63,6 @@ type Client struct {
        cacheKey string
        usage    *usageLog
        http     *http.Client
        cacheKey string
        usage    *usageLog
        http     *http.Client
-       clock    func() time.Time
 }
 
 // Request is one model call. Input holds the input items in order, such as
 }
 
 // Request is one model call. Input holds the input items in order, such as
@@ -145,6 +157,23 @@ type outputItem struct {
        } `json:"content"`
 }
 
        } `json:"content"`
 }
 
+// modelEndpoint returns the URL and the authorizer of the endpoint in config.
+// The ChatGPT backend uses the token file in dir. The OpenAI API uses the key in
+// OPENAI_API_KEY, and there is no fallback from one to the other. All clients
+// share the authorizer, so one refresh runs at a time.
+func modelEndpoint(config Config, dir string) (string, authorizer, error) {
+       if config.Endpoint == endpointAPI {
+               key := os.Getenv("OPENAI_API_KEY")
+               if key == "" {
+                       return "", nil, errors.New("endpoint api needs OPENAI_API_KEY")
+               }
+
+               return apiURL, apiKey(key), nil
+       }
+
+       return chatgptURL, newChatGPTAuth(filepath.Join(dir, tokenFileName), authIssuer), nil
+}
+
 // newClient returns a client for the Responses endpoint at url. It records
 // each call in the usage file at usagePath.
 func newClient(url string, auth authorizer, cacheKey, usagePath string) *Client {
 // newClient returns a client for the Responses endpoint at url. It records
 // each call in the usage file at usagePath.
 func newClient(url string, auth authorizer, cacheKey, usagePath string) *Client {
@@ -155,8 +184,7 @@ func newClient(url string, auth authorizer, cacheKey, usagePath string) *Client
                usage: &usageLog{
                        path: usagePath,
                },
                usage: &usageLog{
                        path: usagePath,
                },
-               http:  &http.Client{},
-               clock: time.Now,
+               http: &http.Client{},
        }
 }
 
        }
 }
 
@@ -191,8 +219,13 @@ func (c *Client) Create(ctx context.Context, request Request) (Response, error)
                return Response{}, fmt.Errorf("authorize model call: %w", err)
        }
 
                return Response{}, fmt.Errorf("authorize model call: %w", err)
        }
 
+       _, chatgpt := c.auth.(*chatgptAuth)
+       if chatgpt {
+               httpRequest.Header.Set("session_id", c.cacheKey)
+       }
+
        record := usageRecord{
        record := usageRecord{
-               Date:  c.clock().Format(time.RFC3339Nano),
+               Date:  time.Now().Format(time.RFC3339Nano),
                Model: request.Model,
        }
 
                Model: request.Model,
        }
 
@@ -240,7 +273,8 @@ type usageLog struct {
 }
 
 // usageRecord is one line of usage.jsonl. Status is the HTTP status, or 0 when
 }
 
 // usageRecord is one line of usage.jsonl. Status is the HTTP status, or 0 when
-// no response came. Quota holds the x-codex-* headers of the response.
+// no response came. Quota holds the x-codex-* headers of the response, without
+// the turn state.
 type usageRecord struct {
        Date         string            `json:"date"`
        Model        string            `json:"model"`
 type usageRecord struct {
        Date         string            `json:"date"`
        Model        string            `json:"model"`
@@ -309,11 +343,12 @@ func userMessage(texts ...string) any {
        }
 }
 
        }
 }
 
-// quotaHeaders returns the x-codex-* headers, with lowercase names.
+// quotaHeaders returns the x-codex-* headers, with lowercase names, without the
+// turn state.
 func quotaHeaders(header http.Header) map[string]string {
        var quota map[string]string
        for name, values := range header {
 func quotaHeaders(header http.Header) map[string]string {
        var quota map[string]string
        for name, values := range header {
-               if !strings.HasPrefix(name, quotaPrefix) || len(values) == 0 {
+               if !strings.HasPrefix(name, quotaPrefix) || name == turnStateHeader || len(values) == 0 {
                        continue
                }
 
                        continue
                }