]> git.giacomolli.uk - onechat/commitdiff
Name and order the ChatGPT login code
authorAnderson A. Giacomolli <redacted>
Sat, 7 Nov 2026 16:29:06 +0000 (13:29 -0300)
committerAnderson A. Giacomolli <redacted>
Sat, 7 Nov 2026 16:30:22 +0000 (13:30 -0300)
Every package-level name of the ChatGPT login starts with chatgpt, and
its file is chatgpt.go. The login has an issuer, a client ID, a token
file, and a poll interval, and the prefix keeps these names apart from
the names of the Telegram bot and the model client. The client ID
constant does not name another program.

The declarations of the login files follow the order of the go-code
skill. The behavior does not change.

Issue: 947bddb

chatgpt.go [moved from auth.go with 59% similarity]
chatgpt_test.go [moved from auth_test.go with 72% similarity]
main.go
model.go
model_test.go

similarity index 59%
rename from auth.go
rename to chatgpt.go
index fda78d8ac709473f20a0a559506e0652b220e7ba..302e143bb732d029378ce5382f9e519eb8753c8f 100644 (file)
--- a/auth.go
@@ -20,86 +20,41 @@ import (
 )
 
 const (
 )
 
 const (
-       // authIssuer is the OpenAI login service.
-       authIssuer = "https://auth.openai.com"
+       // chatgptIssuer is the OpenAI login service.
+       chatgptIssuer = "https://auth.openai.com"
 
 
-       // codexClientID is the OAuth client ID of the Codex CLI. The ChatGPT
+       // chatgptClientID is the OAuth client ID of the Codex CLI. The ChatGPT
        // backend takes the tokens of this client only.
        // backend takes the tokens of this client only.
-       codexClientID = "app_EMoamEEZ73f0CkXaXp7hrann"
+       chatgptClientID = "app_EMoamEEZ73f0CkXaXp7hrann"
 
 
-       // authClaim is the JWT claim that holds the ChatGPT account ID.
-       authClaim = "https://api.openai.com/auth"
+       // chatgptAuthClaim is the JWT claim that holds the ChatGPT account ID.
+       chatgptAuthClaim = "https://api.openai.com/auth"
 
 
-       // refreshMargin is how long before its expiry an access token is
+       // chatgptRefreshMargin is how long before its expiry an access token is
        // refreshed.
        // refreshed.
-       refreshMargin = 60 * time.Second
+       chatgptRefreshMargin = 60 * time.Second
 
 
-       // loginTimeout is how long a device code login waits for the user.
-       loginTimeout = 15 * time.Minute
+       // chatgptLoginTimeout is how long a device code login waits for the
+       // user.
+       chatgptLoginTimeout = 15 * time.Minute
 
 
-       // tokenFileName is the name of the token file in the data directory.
-       tokenFileName = "auth.json"
+       // chatgptTokenFileName is the name of the token file in the data
+       // directory.
+       chatgptTokenFileName = "auth.json"
 
 
-       // defaultPollInterval is the poll interval of a device code login when
-       // the login service gives none.
-       defaultPollInterval = 5 * time.Second
+       // chatgptDefaultPollInterval is the poll interval of a device code
+       // login when the login service gives none.
+       chatgptDefaultPollInterval = 5 * time.Second
 )
 
 )
 
-// errLoginNeeded tells that no valid token is available, so the user must run
-// "onechat login".
-var errLoginNeeded = errors.New(`login needed: run "onechat login"`)
+// errChatGPTLoginNeeded tells that no valid token is available, so the user
+// must run "onechat login".
+var errChatGPTLoginNeeded = errors.New(`login needed: run "onechat login"`)
 
 
-// chatgptAuth authorizes model calls to the ChatGPT backend with the tokens in
-// the token file. The tokens belong to OneChat only. The Codex CLI rotates the
-// refresh token on each refresh, so two programs with one token file log each
-// other out.
-//
-// Before each call it reads the expiry from the access token. Within
-// refreshMargin of the expiry it gets new tokens with the refresh token and
-// replaces the file atomically. A mutex lets one refresh run at a time, and a
-// call that waits for it uses the new tokens.
-type chatgptAuth struct {
-       mutex  sync.Mutex
-       path   string
-       issuer string
-       http   *http.Client
-}
-
-// tokenFile is the content of the token file.
-type tokenFile struct {
-       AccessToken  string `json:"access_token"`
-       RefreshToken string `json:"refresh_token"`
-       IDToken      string `json:"id_token"`
-       AccountID    string `json:"account_id"`
-}
-
-// tokenResponse is the answer of the token endpoint.
-type tokenResponse struct {
-       AccessToken  string `json:"access_token"`
-       RefreshToken string `json:"refresh_token"`
-       IDToken      string `json:"id_token"`
-}
-
-// deviceCode is the answer of the user code endpoint. The interval is a number
-// or a string of seconds.
-type deviceCode struct {
-       DeviceAuthID string          `json:"device_auth_id"`
-       UserCode     string          `json:"user_code"`
-       UserCodeAlt  string          `json:"usercode"`
-       Interval     json.RawMessage `json:"interval"`
-}
-
-// deviceGrant is the answer of the device token endpoint after the user
-// approves the code.
-type deviceGrant struct {
-       AuthorizationCode string `json:"authorization_code"`
-       CodeVerifier      string `json:"code_verifier"`
-}
-
-// login runs "onechat login": the device code login into the token file of the
-// data directory. It holds the lock of the token file, so it does not run while
-// the daemon runs.
-func login(ctx context.Context, out io.Writer) (err error) {
+// chatgptLogin runs "onechat login": the device code login into the token file
+// of the data directory. It holds the lock of the token file, so it does not
+// run while the daemon runs.
+func chatgptLogin(ctx context.Context, out io.Writer) (err error) {
        dir, err := dataDir()
        if err != nil {
                return err
        dir, err := dataDir()
        if err != nil {
                return err
@@ -114,29 +69,70 @@ func login(ctx context.Context, out io.Writer) (err error) {
                err = errors.Join(err, tokens.Close())
        }()
 
                err = errors.Join(err, tokens.Close())
        }()
 
-       return newChatGPTAuth(filepath.Join(dir, tokenFileName), authIssuer).Login(ctx, out)
+       return newChatGPTAuth(filepath.Join(dir, chatgptTokenFileName), chatgptIssuer).Login(ctx, out)
 }
 
 // newChatGPTAuth returns the authorizer for the token file at path, with the
 // login service at issuer.
 }
 
 // newChatGPTAuth returns the authorizer for the token file at path, with the
 // login service at issuer.
-func newChatGPTAuth(path, issuer string) *chatgptAuth {
-       return &chatgptAuth{
+func newChatGPTAuth(path, issuer string) *ChatGPTAuth {
+       return &ChatGPTAuth{
                path:   path,
                issuer: issuer,
                http:   &http.Client{},
        }
 }
 
                path:   path,
                issuer: issuer,
                http:   &http.Client{},
        }
 }
 
+// ChatGPTAuth authorizes model calls to the ChatGPT backend with the tokens in
+// the token file. The tokens belong to OneChat only. The Codex CLI rotates the
+// refresh token on each refresh, so two programs with one token file log each
+// other out.
+//
+// Before each call it reads the expiry from the access token. Within
+// chatgptRefreshMargin of the expiry it gets new tokens with the refresh token
+// and replaces the file atomically. A mutex lets one refresh run at a time, and
+// a call that waits for it uses the new tokens.
+type ChatGPTAuth struct {
+       mutex  sync.Mutex
+       path   string
+       issuer string
+       http   *http.Client
+}
+
+// Authorize sets the access token and the account ID, after a refresh when the
+// token expires within chatgptRefreshMargin.
+func (a *ChatGPTAuth) Authorize(ctx context.Context, header http.Header) error {
+       a.mutex.Lock()
+       defer a.mutex.Unlock()
+
+       tokens, err := a.read()
+       if err != nil {
+               return err
+       }
+
+       expiry, found := chatgptTokenExpiry(tokens.AccessToken)
+       if !found || !time.Now().Before(expiry.Add(-chatgptRefreshMargin)) {
+               tokens, err = a.refresh(ctx, tokens)
+               if err != nil {
+                       return err
+               }
+       }
+
+       header.Set("Authorization", "Bearer "+tokens.AccessToken)
+       header.Set("ChatGPT-Account-Id", tokens.AccountID)
+
+       return nil
+}
+
 // Login runs the device code login. It writes the link and the code for the
 // user to out, waits until the user approves the code, and writes the tokens
 // to the token file.
 // Login runs the device code login. It writes the link and the code for the
 // user to out, waits until the user approves the code, and writes the tokens
 // to the token file.
-func (a *chatgptAuth) Login(ctx context.Context, out io.Writer) error {
+func (a *ChatGPTAuth) Login(ctx context.Context, out io.Writer) error {
        a.mutex.Lock()
        defer a.mutex.Unlock()
 
        a.mutex.Lock()
        defer a.mutex.Unlock()
 
-       var code deviceCode
+       var code ChatGPTDeviceCode
        err := a.postJSON(ctx, "/api/accounts/deviceauth/usercode", map[string]string{
        err := a.postJSON(ctx, "/api/accounts/deviceauth/usercode", map[string]string{
-               "client_id": codexClientID,
+               "client_id": chatgptClientID,
        }, &code)
        if err != nil {
                return fmt.Errorf("request device code (device code login must be allowed in the ChatGPT security settings): %w", err)
        }, &code)
        if err != nil {
                return fmt.Errorf("request device code (device code login must be allowed in the ChatGPT security settings): %w", err)
@@ -152,7 +148,7 @@ func (a *chatgptAuth) Login(ctx context.Context, out io.Writer) error {
                return err
        }
 
                return err
        }
 
-       grant, err := a.poll(ctx, code.DeviceAuthID, userCode, pollInterval(code.Interval))
+       grant, err := a.poll(ctx, code.DeviceAuthID, userCode, chatgptPollInterval(code.Interval))
        if err != nil {
                return err
        }
        if err != nil {
                return err
        }
@@ -172,54 +168,78 @@ func (a *chatgptAuth) Login(ctx context.Context, out io.Writer) error {
        return err
 }
 
        return err
 }
 
-// postJSON posts body as JSON to the path of the login service and decodes the
-// answer into result.
-func (a *chatgptAuth) postJSON(ctx context.Context, path string, body, result any) error {
-       data, err := json.Marshal(body)
+// read reads the token file.
+func (a *ChatGPTAuth) read() (ChatGPTTokenFile, error) {
+       data, err := os.ReadFile(a.path)
+       if errors.Is(err, os.ErrNotExist) {
+               return ChatGPTTokenFile{}, errChatGPTLoginNeeded
+       }
+
        if err != nil {
        if err != nil {
-               return fmt.Errorf("encode request: %w", err)
+               return ChatGPTTokenFile{}, fmt.Errorf("read token file: %w", err)
        }
 
        }
 
-       _, err = a.post(ctx, path, "application/json", data, result)
+       var tokens ChatGPTTokenFile
+       err = json.Unmarshal(data, &tokens)
+       if err == nil && (tokens.AccessToken == "" || tokens.RefreshToken == "" || tokens.AccountID == "") {
+               err = errors.New("a token or the account ID is missing")
+       }
 
 
-       return err
+       if err != nil {
+               return ChatGPTTokenFile{}, fmt.Errorf("token file: %w: %w", err, errChatGPTLoginNeeded)
+       }
+
+       return tokens, nil
 }
 
 }
 
-// post posts body to the path of the login service and decodes a 200 answer
-// into result. It returns the HTTP status, or 0 when no answer came. The error
-// holds the status, but not the body, which can hold a token.
-func (a *chatgptAuth) post(ctx context.Context, path, contentType string, body []byte, result any) (status int, err error) {
-       request, err := http.NewRequestWithContext(ctx, http.MethodPost, a.issuer+path, bytes.NewReader(body))
-       if err != nil {
-               return 0, fmt.Errorf("create request: %w", err)
+// refresh gets new tokens with the refresh token and replaces the token file.
+// It keeps the old refresh token and ID token when the answer has none.
+func (a *ChatGPTAuth) refresh(ctx context.Context, tokens ChatGPTTokenFile) (ChatGPTTokenFile, error) {
+       var response ChatGPTTokenResponse
+       err := a.postJSON(ctx, "/oauth/token", map[string]string{
+               "client_id":     chatgptClientID,
+               "grant_type":    "refresh_token",
+               "refresh_token": tokens.RefreshToken,
+               "scope":         "openid profile email",
+       }, &response)
+       if err == nil && response.AccessToken == "" {
+               err = errors.New("refresh response has no access token")
        }
 
        }
 
-       request.Header.Set("Content-Type", contentType)
-       response, err := a.http.Do(request)
        if err != nil {
        if err != nil {
-               return 0, fmt.Errorf("send request: %w", err)
+               return ChatGPTTokenFile{}, fmt.Errorf("refresh tokens: %w: %w", err, errChatGPTLoginNeeded)
        }
 
        }
 
-       defer func() {
-               err = errors.Join(err, response.Body.Close())
-       }()
+       tokens.AccessToken = response.AccessToken
+       tokens.RefreshToken = cmp.Or(response.RefreshToken, tokens.RefreshToken)
+       tokens.IDToken = cmp.Or(response.IDToken, tokens.IDToken)
+       tokens.AccountID = cmp.Or(chatgptAccountID(tokens.AccessToken), chatgptAccountID(tokens.IDToken), tokens.AccountID)
 
 
-       if response.StatusCode != http.StatusOK {
-               return response.StatusCode, fmt.Errorf("HTTP %d", response.StatusCode)
+       err = a.write(tokens)
+       if err != nil {
+               return ChatGPTTokenFile{}, err
        }
 
        }
 
-       err = json.NewDecoder(response.Body).Decode(result)
+       return tokens, nil
+}
+
+// postJSON posts body as JSON to the path of the login service and decodes the
+// answer into result.
+func (a *ChatGPTAuth) postJSON(ctx context.Context, path string, body, result any) error {
+       data, err := json.Marshal(body)
        if err != nil {
        if err != nil {
-               return response.StatusCode, fmt.Errorf("decode answer: %w", err)
+               return fmt.Errorf("encode request: %w", err)
        }
 
        }
 
-       return response.StatusCode, nil
+       _, err = a.post(ctx, path, "application/json", data, result)
+
+       return err
 }
 
 // poll asks for the grant of the device code until the user approves it or the
 // login times out.
 }
 
 // poll asks for the grant of the device code until the user approves it or the
 // login times out.
-func (a *chatgptAuth) poll(ctx context.Context, deviceID, userCode string, interval time.Duration) (deviceGrant, error) {
-       ctx, cancel := context.WithTimeout(ctx, loginTimeout)
+func (a *ChatGPTAuth) poll(ctx context.Context, deviceID, userCode string, interval time.Duration) (ChatGPTDeviceGrant, error) {
+       ctx, cancel := context.WithTimeout(ctx, chatgptLoginTimeout)
        defer cancel()
 
        body, err := json.Marshal(map[string]string{
        defer cancel()
 
        body, err := json.Marshal(map[string]string{
@@ -227,11 +247,11 @@ func (a *chatgptAuth) poll(ctx context.Context, deviceID, userCode string, inter
                "user_code":      userCode,
        })
        if err != nil {
                "user_code":      userCode,
        })
        if err != nil {
-               return deviceGrant{}, fmt.Errorf("encode poll request: %w", err)
+               return ChatGPTDeviceGrant{}, fmt.Errorf("encode poll request: %w", err)
        }
 
        for {
        }
 
        for {
-               var grant deviceGrant
+               var grant ChatGPTDeviceGrant
                status, err := a.post(ctx, "/api/accounts/deviceauth/token", "application/json", body, &grant)
                if err == nil && (grant.AuthorizationCode == "" || grant.CodeVerifier == "") {
                        err = errors.New("device grant has no authorization code or code verifier")
                status, err := a.post(ctx, "/api/accounts/deviceauth/token", "application/json", body, &grant)
                if err == nil && (grant.AuthorizationCode == "" || grant.CodeVerifier == "") {
                        err = errors.New("device grant has no authorization code or code verifier")
@@ -242,55 +262,55 @@ func (a *chatgptAuth) poll(ctx context.Context, deviceID, userCode string, inter
                }
 
                if status != http.StatusForbidden && status != http.StatusNotFound {
                }
 
                if status != http.StatusForbidden && status != http.StatusNotFound {
-                       return deviceGrant{}, fmt.Errorf("poll device code: %w", err)
+                       return ChatGPTDeviceGrant{}, fmt.Errorf("poll device code: %w", err)
                }
 
                timer := time.NewTimer(interval)
                select {
                case <-ctx.Done():
                        timer.Stop()
                }
 
                timer := time.NewTimer(interval)
                select {
                case <-ctx.Done():
                        timer.Stop()
-                       return deviceGrant{}, fmt.Errorf("wait for approval: %w", ctx.Err())
+                       return ChatGPTDeviceGrant{}, fmt.Errorf("wait for approval: %w", ctx.Err())
                case <-timer.C:
                }
        }
 }
 
 // exchange trades the grant of the device code for tokens.
                case <-timer.C:
                }
        }
 }
 
 // exchange trades the grant of the device code for tokens.
-func (a *chatgptAuth) exchange(ctx context.Context, grant deviceGrant) (tokenFile, error) {
+func (a *ChatGPTAuth) exchange(ctx context.Context, grant ChatGPTDeviceGrant) (ChatGPTTokenFile, error) {
        form := url.Values{
                "grant_type":    {"authorization_code"},
                "code":          {grant.AuthorizationCode},
                "redirect_uri":  {a.issuer + "/deviceauth/callback"},
        form := url.Values{
                "grant_type":    {"authorization_code"},
                "code":          {grant.AuthorizationCode},
                "redirect_uri":  {a.issuer + "/deviceauth/callback"},
-               "client_id":     {codexClientID},
+               "client_id":     {chatgptClientID},
                "code_verifier": {grant.CodeVerifier},
        }
 
                "code_verifier": {grant.CodeVerifier},
        }
 
-       var response tokenResponse
+       var response ChatGPTTokenResponse
        _, err := a.post(ctx, "/oauth/token", "application/x-www-form-urlencoded", []byte(form.Encode()), &response)
        if err != nil {
        _, err := a.post(ctx, "/oauth/token", "application/x-www-form-urlencoded", []byte(form.Encode()), &response)
        if err != nil {
-               return tokenFile{}, fmt.Errorf("exchange code for tokens: %w", err)
+               return ChatGPTTokenFile{}, fmt.Errorf("exchange code for tokens: %w", err)
        }
 
        if response.AccessToken == "" || response.RefreshToken == "" || response.IDToken == "" {
        }
 
        if response.AccessToken == "" || response.RefreshToken == "" || response.IDToken == "" {
-               return tokenFile{}, errors.New("token response has no access, refresh, or ID token")
+               return ChatGPTTokenFile{}, errors.New("token response has no access, refresh, or ID token")
        }
 
        }
 
-       tokens := tokenFile{
+       tokens := ChatGPTTokenFile{
                AccessToken:  response.AccessToken,
                RefreshToken: response.RefreshToken,
                IDToken:      response.IDToken,
        }
 
                AccessToken:  response.AccessToken,
                RefreshToken: response.RefreshToken,
                IDToken:      response.IDToken,
        }
 
-       tokens.AccountID = cmp.Or(accountID(tokens.AccessToken), accountID(tokens.IDToken))
+       tokens.AccountID = cmp.Or(chatgptAccountID(tokens.AccessToken), chatgptAccountID(tokens.IDToken))
        if tokens.AccountID == "" {
        if tokens.AccountID == "" {
-               return tokenFile{}, errors.New("tokens have no ChatGPT account ID")
+               return ChatGPTTokenFile{}, errors.New("tokens have no ChatGPT account ID")
        }
 
        return tokens, nil
 }
 
 // write replaces the token file atomically. The file has mode 0600.
        }
 
        return tokens, nil
 }
 
 // write replaces the token file atomically. The file has mode 0600.
-func (a *chatgptAuth) write(tokens tokenFile) error {
+func (a *ChatGPTAuth) write(tokens ChatGPTTokenFile) error {
        data, err := json.Marshal(tokens)
        if err != nil {
                return fmt.Errorf("encode tokens: %w", err)
        data, err := json.Marshal(tokens)
        if err != nil {
                return fmt.Errorf("encode tokens: %w", err)
@@ -304,101 +324,97 @@ func (a *chatgptAuth) write(tokens tokenFile) error {
        return nil
 }
 
        return nil
 }
 
-// Authorize sets the access token and the account ID, after a refresh when the
-// token expires within refreshMargin.
-func (a *chatgptAuth) Authorize(ctx context.Context, header http.Header) error {
-       a.mutex.Lock()
-       defer a.mutex.Unlock()
-
-       tokens, err := a.read()
+// post posts body to the path of the login service and decodes a 200 answer
+// into result. It returns the HTTP status, or 0 when no answer came. The error
+// holds the status, but not the body, which can hold a token.
+func (a *ChatGPTAuth) post(ctx context.Context, path, contentType string, body []byte, result any) (status int, err error) {
+       request, err := http.NewRequestWithContext(ctx, http.MethodPost, a.issuer+path, bytes.NewReader(body))
        if err != nil {
        if err != nil {
-               return err
+               return 0, fmt.Errorf("create request: %w", err)
        }
 
        }
 
-       expiry, found := tokenExpiry(tokens.AccessToken)
-       if !found || !time.Now().Before(expiry.Add(-refreshMargin)) {
-               tokens, err = a.refresh(ctx, tokens)
-               if err != nil {
-                       return err
-               }
+       request.Header.Set("Content-Type", contentType)
+       response, err := a.http.Do(request)
+       if err != nil {
+               return 0, fmt.Errorf("send request: %w", err)
        }
 
        }
 
-       header.Set("Authorization", "Bearer "+tokens.AccessToken)
-       header.Set("ChatGPT-Account-Id", tokens.AccountID)
-
-       return nil
-}
+       defer func() {
+               err = errors.Join(err, response.Body.Close())
+       }()
 
 
-// read reads the token file.
-func (a *chatgptAuth) read() (tokenFile, error) {
-       data, err := os.ReadFile(a.path)
-       if errors.Is(err, os.ErrNotExist) {
-               return tokenFile{}, errLoginNeeded
+       if response.StatusCode != http.StatusOK {
+               return response.StatusCode, fmt.Errorf("HTTP %d", response.StatusCode)
        }
 
        }
 
+       err = json.NewDecoder(response.Body).Decode(result)
        if err != nil {
        if err != nil {
-               return tokenFile{}, fmt.Errorf("read token file: %w", err)
+               return response.StatusCode, fmt.Errorf("decode answer: %w", err)
        }
 
        }
 
-       var tokens tokenFile
-       err = json.Unmarshal(data, &tokens)
-       if err == nil && (tokens.AccessToken == "" || tokens.RefreshToken == "" || tokens.AccountID == "") {
-               err = errors.New("a token or the account ID is missing")
-       }
+       return response.StatusCode, nil
+}
 
 
-       if err != nil {
-               return tokenFile{}, fmt.Errorf("token file: %w: %w", err, errLoginNeeded)
-       }
+// ChatGPTDeviceCode is the answer of the user code endpoint. The interval is a
+// number or a string of seconds.
+type ChatGPTDeviceCode struct {
+       DeviceAuthID string          `json:"device_auth_id"`
+       UserCode     string          `json:"user_code"`
+       UserCodeAlt  string          `json:"usercode"`
+       Interval     json.RawMessage `json:"interval"`
+}
 
 
-       return tokens, nil
+// ChatGPTTokenFile is the content of the token file.
+type ChatGPTTokenFile struct {
+       AccessToken  string `json:"access_token"`
+       RefreshToken string `json:"refresh_token"`
+       IDToken      string `json:"id_token"`
+       AccountID    string `json:"account_id"`
 }
 
 }
 
-// refresh gets new tokens with the refresh token and replaces the token file.
-// It keeps the old refresh token and ID token when the answer has none.
-func (a *chatgptAuth) refresh(ctx context.Context, tokens tokenFile) (tokenFile, error) {
-       var response tokenResponse
-       err := a.postJSON(ctx, "/oauth/token", map[string]string{
-               "client_id":     codexClientID,
-               "grant_type":    "refresh_token",
-               "refresh_token": tokens.RefreshToken,
-               "scope":         "openid profile email",
-       }, &response)
-       if err == nil && response.AccessToken == "" {
-               err = errors.New("refresh response has no access token")
-       }
+// ChatGPTTokenResponse is the answer of the token endpoint.
+type ChatGPTTokenResponse struct {
+       AccessToken  string `json:"access_token"`
+       RefreshToken string `json:"refresh_token"`
+       IDToken      string `json:"id_token"`
+}
 
 
-       if err != nil {
-               return tokenFile{}, fmt.Errorf("refresh tokens: %w: %w", err, errLoginNeeded)
-       }
+// ChatGPTDeviceGrant is the answer of the device token endpoint after the user
+// approves the code.
+type ChatGPTDeviceGrant struct {
+       AuthorizationCode string `json:"authorization_code"`
+       CodeVerifier      string `json:"code_verifier"`
+}
 
 
-       tokens.AccessToken = response.AccessToken
-       tokens.RefreshToken = cmp.Or(response.RefreshToken, tokens.RefreshToken)
-       tokens.IDToken = cmp.Or(response.IDToken, tokens.IDToken)
-       tokens.AccountID = cmp.Or(accountID(tokens.AccessToken), accountID(tokens.IDToken), tokens.AccountID)
+// chatgptTokenExpiry returns the expiry of a JWT from its exp claim.
+func chatgptTokenExpiry(token string) (time.Time, bool) {
+       var claims struct {
+               Exp *float64 `json:"exp"`
+       }
 
 
-       err = a.write(tokens)
-       if err != nil {
-               return tokenFile{}, err
+       if !chatgptDecodeClaims(token, &claims) || claims.Exp == nil {
+               return time.Time{}, false
        }
 
        }
 
-       return tokens, nil
+       return time.Unix(int64(*claims.Exp), 0), true
 }
 
 }
 
-// pollInterval returns the poll interval of a device code, given in seconds as
-// a number or a string.
-func pollInterval(raw json.RawMessage) time.Duration {
+// chatgptPollInterval returns the poll interval of a device code, given in
+// seconds as a number or a string.
+func chatgptPollInterval(raw json.RawMessage) time.Duration {
        seconds, err := strconv.ParseFloat(strings.Trim(string(raw), `"`), 64)
        if err != nil || seconds < 0 {
        seconds, err := strconv.ParseFloat(strings.Trim(string(raw), `"`), 64)
        if err != nil || seconds < 0 {
-               return defaultPollInterval
+               return chatgptDefaultPollInterval
        }
 
        return time.Duration(seconds * float64(time.Second))
 }
 
        }
 
        return time.Duration(seconds * float64(time.Second))
 }
 
-// accountID returns the ChatGPT account ID of a JWT, or "" when it has none.
-func accountID(token string) string {
+// chatgptAccountID returns the ChatGPT account ID of a JWT, or "" when it has
+// none.
+func chatgptAccountID(token string) string {
        var claims map[string]json.RawMessage
        var claims map[string]json.RawMessage
-       if !decodeClaims(token, &claims) {
+       if !chatgptDecodeClaims(token, &claims) {
                return ""
        }
 
                return ""
        }
 
@@ -406,7 +422,7 @@ func accountID(token string) string {
                AccountID string `json:"chatgpt_account_id"`
        }
 
                AccountID string `json:"chatgpt_account_id"`
        }
 
-       err := json.Unmarshal(claims[authClaim], &auth)
+       err := json.Unmarshal(claims[chatgptAuthClaim], &auth)
        if err != nil {
                return ""
        }
        if err != nil {
                return ""
        }
@@ -414,10 +430,10 @@ func accountID(token string) string {
        return auth.AccountID
 }
 
        return auth.AccountID
 }
 
-// decodeClaims decodes the payload of a JWT into claims, without a check of
-// the signature. The token comes from the login service over HTTPS, and the
+// chatgptDecodeClaims decodes the payload of a JWT into claims, without a check
+// of the signature. The token comes from the login service over HTTPS, and the
 // client reads only the expiry and the account ID from it.
 // client reads only the expiry and the account ID from it.
-func decodeClaims(token string, claims any) bool {
+func chatgptDecodeClaims(token string, claims any) bool {
        parts := strings.Split(token, ".")
        if len(parts) != 3 {
                return false
        parts := strings.Split(token, ".")
        if len(parts) != 3 {
                return false
@@ -430,16 +446,3 @@ func decodeClaims(token string, claims any) bool {
 
        return json.Unmarshal(payload, claims) == nil
 }
 
        return json.Unmarshal(payload, claims) == nil
 }
-
-// tokenExpiry returns the expiry of a JWT from its exp claim.
-func tokenExpiry(token string) (time.Time, bool) {
-       var claims struct {
-               Exp *float64 `json:"exp"`
-       }
-
-       if !decodeClaims(token, &claims) || claims.Exp == nil {
-               return time.Time{}, false
-       }
-
-       return time.Unix(int64(*claims.Exp), 0), true
-}
similarity index 72%
rename from auth_test.go
rename to chatgpt_test.go
index 06216057cd64a3e9e8dd3ff2195c63738a3bbf93..982a6ff70e51d08164515a4f315f1c11e3ca54fd 100644 (file)
@@ -18,13 +18,13 @@ import (
        "github.com/stretchr/testify/require"
 )
 
        "github.com/stretchr/testify/require"
 )
 
-// testAccount is the fake ChatGPT account ID in the fake tokens.
-const testAccount = "account-test"
+// chatgptTestAccount is the fake ChatGPT account ID in the fake tokens.
+const chatgptTestAccount = "account-test"
 
 func TestLoginWritesTheTokenFile(t *testing.T) {
 
 func TestLoginWritesTheTokenFile(t *testing.T) {
-       server := newFakeAuth(t)
+       server := newChatGPTFake(t)
        server.pending = 2
        server.pending = 2
-       auth := newTestAuth(t, server)
+       auth := newTestChatGPTAuth(t, server)
 
        var out strings.Builder
        err := auth.Login(t.Context(), &out)
 
        var out strings.Builder
        err := auth.Login(t.Context(), &out)
@@ -38,12 +38,12 @@ func TestLoginWritesTheTokenFile(t *testing.T) {
        require.Equal(t, "authorization_code", form.Get("grant_type"))
        require.Equal(t, "auth-code", form.Get("code"))
        require.Equal(t, "verifier", form.Get("code_verifier"))
        require.Equal(t, "authorization_code", form.Get("grant_type"))
        require.Equal(t, "auth-code", form.Get("code"))
        require.Equal(t, "verifier", form.Get("code_verifier"))
-       require.Equal(t, codexClientID, form.Get("client_id"))
+       require.Equal(t, chatgptClientID, form.Get("client_id"))
        require.Equal(t, server.URL+"/deviceauth/callback", form.Get("redirect_uri"))
 
        require.Equal(t, server.URL+"/deviceauth/callback", form.Get("redirect_uri"))
 
-       tokens := readTokenFile(t, auth.path)
+       tokens := chatgptReadTokenFile(t, auth.path)
        require.Equal(t, "refresh-1", tokens.RefreshToken)
        require.Equal(t, "refresh-1", tokens.RefreshToken)
-       require.Equal(t, testAccount, tokens.AccountID)
+       require.Equal(t, chatgptTestAccount, tokens.AccountID)
        require.Equal(t, server.access[0], tokens.AccessToken)
        require.NotContains(t, out.String(), tokens.AccessToken)
        require.NotContains(t, out.String(), tokens.RefreshToken)
        require.Equal(t, server.access[0], tokens.AccessToken)
        require.NotContains(t, out.String(), tokens.AccessToken)
        require.NotContains(t, out.String(), tokens.RefreshToken)
@@ -54,9 +54,9 @@ func TestLoginWritesTheTokenFile(t *testing.T) {
 }
 
 func TestLoginFailsWhenTheCodeIsDenied(t *testing.T) {
 }
 
 func TestLoginFailsWhenTheCodeIsDenied(t *testing.T) {
-       server := newFakeAuth(t)
+       server := newChatGPTFake(t)
        server.deny = true
        server.deny = true
-       auth := newTestAuth(t, server)
+       auth := newTestChatGPTAuth(t, server)
 
        err := auth.Login(t.Context(), io.Discard)
        require.ErrorContains(t, err, "HTTP 400")
 
        err := auth.Login(t.Context(), io.Discard)
        require.ErrorContains(t, err, "HTTP 400")
@@ -64,15 +64,15 @@ func TestLoginFailsWhenTheCodeIsDenied(t *testing.T) {
 }
 
 func TestAuthorizeUsesAFreshTokenWithoutRefresh(t *testing.T) {
 }
 
 func TestAuthorizeUsesAFreshTokenWithoutRefresh(t *testing.T) {
-       server := newFakeAuth(t)
-       auth := newTestAuth(t, server)
-       writeTestTokens(t, auth, server.token(time.Hour), "refresh-0")
+       server := newChatGPTFake(t)
+       auth := newTestChatGPTAuth(t, server)
+       chatgptWriteTestTokens(t, auth, server.token(time.Hour), "refresh-0")
 
        header := http.Header{}
        err := auth.Authorize(t.Context(), header)
        require.NoError(t, err)
        require.Equal(t, "Bearer "+server.access[0], header.Get("Authorization"))
 
        header := http.Header{}
        err := auth.Authorize(t.Context(), header)
        require.NoError(t, err)
        require.Equal(t, "Bearer "+server.access[0], header.Get("Authorization"))
-       require.Equal(t, testAccount, header.Get("ChatGPT-Account-Id"))
+       require.Equal(t, chatgptTestAccount, header.Get("ChatGPT-Account-Id"))
        require.Equal(t, 0, server.refreshes)
 }
 
        require.Equal(t, 0, server.refreshes)
 }
 
@@ -96,10 +96,10 @@ func TestAuthorizeRefreshesNearTheExpiry(t *testing.T) {
 
        for _, tc := range testCases {
                t.Run(tc.name, func(t *testing.T) {
 
        for _, tc := range testCases {
                t.Run(tc.name, func(t *testing.T) {
-                       server := newFakeAuth(t)
+                       server := newChatGPTFake(t)
                        server.newRefresh = tc.newRefresh
                        server.newRefresh = tc.newRefresh
-                       auth := newTestAuth(t, server)
-                       writeTestTokens(t, auth, server.token(59*time.Second), "refresh-0")
+                       auth := newTestChatGPTAuth(t, server)
+                       chatgptWriteTestTokens(t, auth, server.token(59*time.Second), "refresh-0")
 
                        header := http.Header{}
                        err := auth.Authorize(t.Context(), header)
 
                        header := http.Header{}
                        err := auth.Authorize(t.Context(), header)
@@ -107,10 +107,10 @@ func TestAuthorizeRefreshesNearTheExpiry(t *testing.T) {
                        require.Equal(t, 1, server.refreshes)
                        require.Equal(t, "refresh-0", server.refreshed)
 
                        require.Equal(t, 1, server.refreshes)
                        require.Equal(t, "refresh-0", server.refreshed)
 
-                       tokens := readTokenFile(t, auth.path)
+                       tokens := chatgptReadTokenFile(t, auth.path)
                        require.Equal(t, server.access[1], tokens.AccessToken)
                        require.Equal(t, tc.wantRefresh, tokens.RefreshToken)
                        require.Equal(t, server.access[1], tokens.AccessToken)
                        require.Equal(t, tc.wantRefresh, tokens.RefreshToken)
-                       require.Equal(t, testAccount, tokens.AccountID)
+                       require.Equal(t, chatgptTestAccount, tokens.AccountID)
                        require.Equal(t, "Bearer "+server.access[1], header.Get("Authorization"))
 
                        err = auth.Authorize(t.Context(), header)
                        require.Equal(t, "Bearer "+server.access[1], header.Get("Authorization"))
 
                        err = auth.Authorize(t.Context(), header)
@@ -121,10 +121,10 @@ func TestAuthorizeRefreshesNearTheExpiry(t *testing.T) {
 }
 
 func TestAuthorizeRunsOneRefreshAtATime(t *testing.T) {
 }
 
 func TestAuthorizeRunsOneRefreshAtATime(t *testing.T) {
-       server := newFakeAuth(t)
+       server := newChatGPTFake(t)
        server.newRefresh = "refresh-new"
        server.newRefresh = "refresh-new"
-       auth := newTestAuth(t, server)
-       writeTestTokens(t, auth, server.token(time.Second), "refresh-0")
+       auth := newTestChatGPTAuth(t, server)
+       chatgptWriteTestTokens(t, auth, server.token(time.Second), "refresh-0")
 
        var group sync.WaitGroup
        errs := make(chan error, 8)
 
        var group sync.WaitGroup
        errs := make(chan error, 8)
@@ -144,19 +144,19 @@ func TestAuthorizeRunsOneRefreshAtATime(t *testing.T) {
 }
 
 func TestAuthorizeNeedsALogin(t *testing.T) {
 }
 
 func TestAuthorizeNeedsALogin(t *testing.T) {
-       server := newFakeAuth(t)
-       auth := newTestAuth(t, server)
+       server := newChatGPTFake(t)
+       auth := newTestChatGPTAuth(t, server)
 
        err := auth.Authorize(t.Context(), http.Header{})
 
        err := auth.Authorize(t.Context(), http.Header{})
-       require.ErrorIs(t, err, errLoginNeeded)
+       require.ErrorIs(t, err, errChatGPTLoginNeeded)
 
        server.failRefresh = true
 
        server.failRefresh = true
-       writeTestTokens(t, auth, server.token(-time.Minute), "refresh-0")
+       chatgptWriteTestTokens(t, auth, server.token(-time.Minute), "refresh-0")
        before, err := os.ReadFile(auth.path)
        require.NoError(t, err)
 
        err = auth.Authorize(t.Context(), http.Header{})
        before, err := os.ReadFile(auth.path)
        require.NoError(t, err)
 
        err = auth.Authorize(t.Context(), http.Header{})
-       require.ErrorIs(t, err, errLoginNeeded)
+       require.ErrorIs(t, err, errChatGPTLoginNeeded)
        require.NotContains(t, err.Error(), "refresh-0")
 
        after, err := os.ReadFile(auth.path)
        require.NotContains(t, err.Error(), "refresh-0")
 
        after, err := os.ReadFile(auth.path)
@@ -165,9 +165,9 @@ func TestAuthorizeNeedsALogin(t *testing.T) {
 }
 
 func TestForcedRefreshLetsTheNextCallWork(t *testing.T) {
 }
 
 func TestForcedRefreshLetsTheNextCallWork(t *testing.T) {
-       server := newFakeAuth(t)
-       auth := newTestAuth(t, server)
-       writeTestTokens(t, auth, server.token(-time.Hour), "refresh-0")
+       server := newChatGPTFake(t)
+       auth := newTestChatGPTAuth(t, server)
+       chatgptWriteTestTokens(t, auth, server.token(-time.Hour), "refresh-0")
 
        responses := newModelFake(t)
        client := newModelClient(responses.URL, auth, "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
 
        responses := newModelFake(t)
        client := newModelClient(responses.URL, auth, "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
@@ -177,8 +177,8 @@ func TestForcedRefreshLetsTheNextCallWork(t *testing.T) {
        require.Equal(t, 1, server.refreshes)
        call := responses.Call(0)
        require.Equal(t, "Bearer "+server.access[1], call.header.Get("Authorization"))
        require.Equal(t, 1, server.refreshes)
        call := responses.Call(0)
        require.Equal(t, "Bearer "+server.access[1], call.header.Get("Authorization"))
-       require.Equal(t, testAccount, call.header.Get("ChatGPT-Account-Id"))
-       require.Equal(t, server.access[1], readTokenFile(t, auth.path).AccessToken)
+       require.Equal(t, chatgptTestAccount, call.header.Get("ChatGPT-Account-Id"))
+       require.Equal(t, server.access[1], chatgptReadTokenFile(t, auth.path).AccessToken)
 }
 
 func TestModelEndpointFollowsTheConfig(t *testing.T) {
 }
 
 func TestModelEndpointFollowsTheConfig(t *testing.T) {
@@ -189,8 +189,8 @@ func TestModelEndpointFollowsTheConfig(t *testing.T) {
        address, auth, err := modelEndpoint(config, dir)
        require.NoError(t, err)
        require.Equal(t, modelChatGPTURL, address)
        address, auth, err := modelEndpoint(config, dir)
        require.NoError(t, err)
        require.Equal(t, modelChatGPTURL, address)
-       require.IsType(t, &chatgptAuth{}, auth)
-       require.Equal(t, filepath.Join(dir, tokenFileName), auth.(*chatgptAuth).path)
+       require.IsType(t, &ChatGPTAuth{}, auth)
+       require.Equal(t, filepath.Join(dir, chatgptTokenFileName), auth.(*ChatGPTAuth).path)
 
        config.Endpoint = endpointAPI
        address, auth, err = modelEndpoint(config, dir)
 
        config.Endpoint = endpointAPI
        address, auth, err = modelEndpoint(config, dir)
@@ -203,10 +203,10 @@ func TestModelEndpointFollowsTheConfig(t *testing.T) {
        require.ErrorContains(t, err, "OPENAI_API_KEY")
 }
 
        require.ErrorContains(t, err, "OPENAI_API_KEY")
 }
 
-// fakeAuth is an httptest login service. The device token endpoint answers
+// ChatGPTFake is an httptest login service. The device token endpoint answers
 // 403 pending times before it grants the code. Each token answer has a new
 // access token, kept in access.
 // 403 pending times before it grants the code. Each token answer has a new
 // access token, kept in access.
-type fakeAuth struct {
+type ChatGPTFake struct {
        *httptest.Server
        mutex       sync.Mutex
        pending     int
        *httptest.Server
        mutex       sync.Mutex
        pending     int
@@ -220,36 +220,52 @@ type fakeAuth struct {
        access      []string
 }
 
        access      []string
 }
 
-func newFakeAuth(t *testing.T) *fakeAuth {
-       t.Helper()
+// token returns a new access token that expires after ttl, for a token file
+// that a test writes.
+func (f *ChatGPTFake) token(ttl time.Duration) string {
+       f.mutex.Lock()
+       defer f.mutex.Unlock()
 
 
-       f := &fakeAuth{}
-       mux := http.NewServeMux()
-       mux.HandleFunc("POST /api/accounts/deviceauth/usercode", f.userCode)
-       mux.HandleFunc("POST /api/accounts/deviceauth/token", f.deviceToken)
-       mux.HandleFunc("POST /oauth/token", f.oauthToken)
-       f.Server = httptest.NewServer(mux)
-       t.Cleanup(f.Close)
+       return f.newToken(ttl)
+}
 
 
-       return f
+// newToken returns a new fake JWT with the expiry and the account claim, and
+// keeps it in access. The caller holds the mutex.
+func (f *ChatGPTFake) newToken(ttl time.Duration) string {
+       claims, err := json.Marshal(map[string]any{
+               "exp": time.Now().Add(ttl).Unix(),
+               chatgptAuthClaim: map[string]any{
+                       "chatgpt_account_id": chatgptTestAccount,
+               },
+               "serial": len(f.access),
+       })
+       if err != nil {
+               panic(err)
+       }
+
+       encode := base64.RawURLEncoding.EncodeToString
+       token := encode([]byte(`{"alg":"none"}`)) + "." + encode(claims) + "." + encode([]byte("fake"))
+       f.access = append(f.access, token)
+
+       return token
 }
 
 }
 
-func (f *fakeAuth) userCode(w http.ResponseWriter, r *http.Request) {
+func (f *ChatGPTFake) userCode(w http.ResponseWriter, r *http.Request) {
        var body map[string]string
        err := json.NewDecoder(r.Body).Decode(&body)
        var body map[string]string
        err := json.NewDecoder(r.Body).Decode(&body)
-       if err != nil || body["client_id"] != codexClientID {
+       if err != nil || body["client_id"] != chatgptClientID {
                http.Error(w, "bad client", http.StatusBadRequest)
                return
        }
 
                http.Error(w, "bad client", http.StatusBadRequest)
                return
        }
 
-       writeJSON(w, map[string]any{
+       chatgptWriteJSON(w, map[string]any{
                "device_auth_id": "device-1",
                "user_code":      "CODE-1234",
                "interval":       "0",
        })
 }
 
                "device_auth_id": "device-1",
                "user_code":      "CODE-1234",
                "interval":       "0",
        })
 }
 
-func (f *fakeAuth) deviceToken(w http.ResponseWriter, r *http.Request) {
+func (f *ChatGPTFake) deviceToken(w http.ResponseWriter, r *http.Request) {
        f.mutex.Lock()
        defer f.mutex.Unlock()
 
        f.mutex.Lock()
        defer f.mutex.Unlock()
 
@@ -271,14 +287,14 @@ func (f *fakeAuth) deviceToken(w http.ResponseWriter, r *http.Request) {
                return
        }
 
                return
        }
 
-       writeJSON(w, map[string]any{
+       chatgptWriteJSON(w, map[string]any{
                "authorization_code": "auth-code",
                "code_challenge":     "challenge",
                "code_verifier":      "verifier",
        })
 }
 
                "authorization_code": "auth-code",
                "code_challenge":     "challenge",
                "code_verifier":      "verifier",
        })
 }
 
-func (f *fakeAuth) oauthToken(w http.ResponseWriter, r *http.Request) {
+func (f *ChatGPTFake) oauthToken(w http.ResponseWriter, r *http.Request) {
        f.mutex.Lock()
        defer f.mutex.Unlock()
 
        f.mutex.Lock()
        defer f.mutex.Unlock()
 
@@ -290,7 +306,7 @@ func (f *fakeAuth) oauthToken(w http.ResponseWriter, r *http.Request) {
                }
 
                f.exchange = r.PostForm
                }
 
                f.exchange = r.PostForm
-               writeJSON(w, map[string]any{
+               chatgptWriteJSON(w, map[string]any{
                        "access_token":  f.newToken(time.Hour),
                        "refresh_token": "refresh-1",
                        "id_token":      f.newToken(time.Hour),
                        "access_token":  f.newToken(time.Hour),
                        "refresh_token": "refresh-1",
                        "id_token":      f.newToken(time.Hour),
@@ -301,7 +317,7 @@ func (f *fakeAuth) oauthToken(w http.ResponseWriter, r *http.Request) {
 
        var body map[string]string
        err := json.NewDecoder(r.Body).Decode(&body)
 
        var body map[string]string
        err := json.NewDecoder(r.Body).Decode(&body)
-       if err != nil || body["grant_type"] != "refresh_token" || body["client_id"] != codexClientID {
+       if err != nil || body["grant_type"] != "refresh_token" || body["client_id"] != chatgptClientID {
                http.Error(w, "bad refresh", http.StatusBadRequest)
                return
        }
                http.Error(w, "bad refresh", http.StatusBadRequest)
                return
        }
@@ -322,70 +338,54 @@ func (f *fakeAuth) oauthToken(w http.ResponseWriter, r *http.Request) {
                answer["refresh_token"] = f.newRefresh
        }
 
                answer["refresh_token"] = f.newRefresh
        }
 
-       writeJSON(w, answer)
+       chatgptWriteJSON(w, answer)
 }
 
 }
 
-// token returns a new access token that expires after ttl, for a token file
-// that a test writes.
-func (f *fakeAuth) token(ttl time.Duration) string {
-       f.mutex.Lock()
-       defer f.mutex.Unlock()
+func newChatGPTFake(t *testing.T) *ChatGPTFake {
+       t.Helper()
 
 
-       return f.newToken(ttl)
-}
+       f := &ChatGPTFake{}
+       mux := http.NewServeMux()
+       mux.HandleFunc("POST /api/accounts/deviceauth/usercode", f.userCode)
+       mux.HandleFunc("POST /api/accounts/deviceauth/token", f.deviceToken)
+       mux.HandleFunc("POST /oauth/token", f.oauthToken)
+       f.Server = httptest.NewServer(mux)
+       t.Cleanup(f.Close)
 
 
-// newToken returns a new fake JWT with the expiry and the account claim, and
-// keeps it in access. The caller holds the mutex.
-func (f *fakeAuth) newToken(ttl time.Duration) string {
-       claims, err := json.Marshal(map[string]any{
-               "exp": time.Now().Add(ttl).Unix(),
-               authClaim: map[string]any{
-                       "chatgpt_account_id": testAccount,
-               },
-               "serial": len(f.access),
-       })
-       if err != nil {
-               panic(err)
-       }
+       return f
+}
 
 
-       encode := base64.RawURLEncoding.EncodeToString
-       token := encode([]byte(`{"alg":"none"}`)) + "." + encode(claims) + "." + encode([]byte("fake"))
-       f.access = append(f.access, token)
+func newTestChatGPTAuth(t *testing.T, server *ChatGPTFake) *ChatGPTAuth {
+       t.Helper()
 
 
-       return token
+       return newChatGPTAuth(filepath.Join(t.TempDir(), chatgptTokenFileName), server.URL)
 }
 
 }
 
-func newTestAuth(t *testing.T, server *fakeAuth) *chatgptAuth {
+func chatgptReadTokenFile(t *testing.T, path string) ChatGPTTokenFile {
        t.Helper()
 
        t.Helper()
 
-       return newChatGPTAuth(filepath.Join(t.TempDir(), tokenFileName), server.URL)
+       data, err := os.ReadFile(path)
+       require.NoError(t, err)
+
+       var tokens ChatGPTTokenFile
+       require.NoError(t, json.Unmarshal(data, &tokens))
+
+       return tokens
 }
 
 }
 
-func writeTestTokens(t *testing.T, auth *chatgptAuth, access, refresh string) {
+func chatgptWriteTestTokens(t *testing.T, auth *ChatGPTAuth, access, refresh string) {
        t.Helper()
 
        t.Helper()
 
-       err := auth.write(tokenFile{
+       err := auth.write(ChatGPTTokenFile{
                AccessToken:  access,
                RefreshToken: refresh,
                IDToken:      access,
                AccessToken:  access,
                RefreshToken: refresh,
                IDToken:      access,
-               AccountID:    testAccount,
+               AccountID:    chatgptTestAccount,
        })
        require.NoError(t, err)
 }
 
        })
        require.NoError(t, err)
 }
 
-func readTokenFile(t *testing.T, path string) tokenFile {
-       t.Helper()
-
-       data, err := os.ReadFile(path)
-       require.NoError(t, err)
-
-       var tokens tokenFile
-       require.NoError(t, json.Unmarshal(data, &tokens))
-
-       return tokens
-}
-
-func writeJSON(w http.ResponseWriter, value any) {
+func chatgptWriteJSON(w http.ResponseWriter, value any) {
        w.Header().Set("Content-Type", "application/json")
        err := json.NewEncoder(w).Encode(value)
        if err != nil {
        w.Header().Set("Content-Type", "application/json")
        err := json.NewEncoder(w).Encode(value)
        if err != nil {
diff --git a/main.go b/main.go
index 9bc69d1dcdfb40213805d98920bc4b007efd3210..bdbcb722420b9dd8d8100c8ea1d7363a3160aaad 100644 (file)
--- a/main.go
+++ b/main.go
@@ -29,7 +29,7 @@ func run() error {
                        return err
                }
        case len(args) == 1 && args[0] == "login":
                        return err
                }
        case len(args) == 1 && args[0] == "login":
-               err := login(ctx, os.Stdout)
+               err := chatgptLogin(ctx, os.Stdout)
                if err != nil {
                        return err
                }
                if err != nil {
                        return err
                }
index bd1258b80d586aeece9bd174e8cc1baaf2e65ba6..3f3eccd8fb908ae1e796ea855bd6e77dfba17588 100644 (file)
--- a/model.go
+++ b/model.go
@@ -54,7 +54,7 @@ func modelEndpoint(config Config, dir string) (string, ModelAuthorizer, error) {
                return modelAPIURL, ModelAPIKey(key), nil
        }
 
                return modelAPIURL, ModelAPIKey(key), nil
        }
 
-       return modelChatGPTURL, newChatGPTAuth(filepath.Join(dir, tokenFileName), authIssuer), nil
+       return modelChatGPTURL, newChatGPTAuth(filepath.Join(dir, chatgptTokenFileName), chatgptIssuer), nil
 }
 
 // newModelClient returns a client for the Responses endpoint at url. It records
 }
 
 // newModelClient returns a client for the Responses endpoint at url. It records
@@ -202,7 +202,7 @@ func (c *ModelClient) Create(ctx context.Context, request ModelRequest) (ModelRe
                return ModelResponse{}, fmt.Errorf("authorize model call: %w", err)
        }
 
                return ModelResponse{}, fmt.Errorf("authorize model call: %w", err)
        }
 
-       _, chatgpt := c.auth.(*chatgptAuth)
+       _, chatgpt := c.auth.(*ChatGPTAuth)
        if chatgpt {
                httpRequest.Header.Set("session_id", c.cacheKey)
        }
        if chatgpt {
                httpRequest.Header.Set("session_id", c.cacheKey)
        }
index b90eaddca24bec8012cee558e4194585cc6942e2..2197eca170e1dde91a3183726d7e5fc5019dbfe1 100644 (file)
@@ -62,9 +62,9 @@ func TestClientSendsTheFixedRequestFields(t *testing.T) {
 }
 
 func TestClientSendsTheSessionHeaderToChatGPT(t *testing.T) {
 }
 
 func TestClientSendsTheSessionHeaderToChatGPT(t *testing.T) {
-       login := newFakeAuth(t)
-       auth := newTestAuth(t, login)
-       writeTestTokens(t, auth, login.token(time.Hour), "refresh-0")
+       login := newChatGPTFake(t)
+       auth := newTestChatGPTAuth(t, login)
+       chatgptWriteTestTokens(t, auth, login.token(time.Hour), "refresh-0")
 
        testCases := []struct {
                name string
 
        testCases := []struct {
                name string