Skip to content

feat(llm): add openai-chatgpt provider with ChatGPT subscription OAuth - #1639

Open
trongtrandp wants to merge 2 commits into
alibaba:mainfrom
trongtrandp:feat/openai-chatgpt
Open

trongtrandp wants to merge 2 commits into
alibaba:mainfrom
trongtrandp:feat/openai-chatgpt

Conversation

@trongtrandp

Copy link
Copy Markdown

Description

Adds a built-in openai-chatgpt provider so OCR can run reviews on a ChatGPT
subscription through OAuth instead of a platform API key.

  • Sign-in: ocr llm login openai-chatgpt runs an OAuth authorization-code
    flow with PKCE and OIDC ID-token validation (nonce, subject, audience), using
    a loopback callback on 127.0.0.1. Available models are discovered per
    account and written to the provider entry.
  • Credentials: stored in ~/.opencodereview/chatgpt/credentials.json
    (0600), written atomically under a file lock. Access tokens are refreshed on
    demand; a refresh response without refresh_token keeps the current one
    (RFC 6749 §6), and a token response without scope means the requested
    scopes were granted (RFC 6749 §5.1).
  • Account commands: logout, status, models and account <client-id>.
    Logout revokes the refresh token, or the access token when there is no
    refresh token, and keeps the registration.
  • Inference: goes through the Responses API with streaming. Tools are
    namespaced under ocr. Stream error / response.failed events keep the
    server's code and message, and subscription quota/eligibility errors map to
    actionable messages.
  • Config safety: the resolver rejects overrides that would leak or redirect
    the OAuth token (api_key, api_key_cmd, url, auth_header,
    extra_headers, protocol). extra_body is limited to reasoning, text
    and prompt_cache_key. OpenAI-Organization / OpenAI-Project headers from
    the environment are stripped on this route.
  • TUI and extension: choosing the provider in ocr config provider
    launches the login when no model is known yet. The VS Code config view shows
    a localized login hint. Generated provider catalogs are regenerated via
    go generate ./internal/llm.
  • Shared code: internal/viewer now exports OpenBrowser so the login
    reuses the existing browser opener ($BROWSER, exit-status check) instead of
    a second copy.
  • ocr llm test: the stricter "tool call plus visible answer" check applies
    only to this provider. Other providers keep the previous behavior.

Open design points for reviewers

These are deliberate choices I would like maintainer input on:

  1. WithMaxRetries(0) on the ChatGPT route: no SDK retries at all, including
    transient 5xx. This avoids hammering quota errors; a narrower policy (retry
    5xx and connection resets, skip quota codes) is possible.
  2. Token responses with expires_in > 86400 are rejected.
  3. retry_codes and OCR_LLM_EXTRA_HEADERS have no effect on this route, while
    extra_headers in the config is rejected. Silently ignoring them is
    inconsistent; I can reject or honor them instead.
  4. Each request takes the credential file lock and re-reads the file instead of
    caching the token in memory, so logout and account switches take effect
    immediately. This serializes concurrent requests.
  5. Models() decodes a {"models":[{"slug","visibility"}]} response. The unit
    tests use a mock, so this schema relies on manual verification.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring (no functional changes)
  • Documentation update
  • CI / Build / Tooling

How Has This Been Tested?

  • make check, make test (with -race) and make coverage (92.3%, threshold
    90%) pass locally; frontend jest/tsc pass.

  • New unit tests cover:

    • the login/refresh/revoke flows against a fake OIDC issuer;
    • a refresh response without rotation, and a token response without scope;
    • a re-login without plan permission keeping the existing registration;
    • logout revoking the access token when there is no refresh token;
    • stream error mapping and session-key expansion;
    • the TUI not carrying a previous provider's model into the OAuth login;
    • the preset metadata.
  • make test passes locally

  • Manual testing (describe below)

Checklist

  • My code follows the project's coding style (go fmt, go vet)
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the documentation accordingly (if applicable)
  • I have signed the CLA
  • I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

AI disclosure:

  • Claude Code (Claude Opus 5.5) was used for the implementation, for code
    review and for the fixes found in review.
  • ocr review (provider openai-chatgpt, model gpt-6.1-sol) was used for
    pre-commit review.

I have reviewed all generated code and can explain every change.

https://claude.ai/code/session_01DnWv6iXVSLhVRX343cw36k

Related Issues

None.

Sign in with 'ocr llm login openai-chatgpt'; requests go through the Responses API using the account's OAuth token instead of an API key.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 5 issue(s) in this PR.

  • ✅ Successfully posted inline: 5 comment(s)

Comment on lines +140 to +146
profile, err := c.Login(ctx, accountID, func(authURL string) error {
fmt.Printf("Continue with ChatGPT:\n%s\n", authURL)
if err := openChatGPTBrowser(authURL); err != nil {
fmt.Println("Could not open a browser automatically. Open the sign-in URL above.")
}
return nil
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
runChatGPTLogin uses fmt.Printf / fmt.Println directly (lines 141, 143, 176) while every cobra subcommand handler in init() consistently writes to cmd.OutOrStdout(). This breaks output redirection and testability for the login flow. Consider passing io.Writer (or *cobra.Command) into runChatGPTLogin so all user-facing output goes through the same writer.

Comment on lines +158 to +165
if ep.ChatGPT {
if len(tools) > 0 && !toolCalled {
return fmt.Errorf("ChatGPT did not call the required self-test tool; tool-call round trip could not be verified")
}
if strings.TrimSpace(resp.VisibleContent()) == "" {
return fmt.Errorf("LLM connectivity test returned no visible answer; connection verification failed")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
When ep.ChatGPT is true but len(tools) == 0, firstChoice remains "" and the test sends the request without forcing a tool call. The subsequent ChatGPT-specific validation block (lines 158-164) then checks len(tools) > 0 && !toolCalled, which correctly skips the tool-call assertion. However, it still enforces strings.TrimSpace(resp.VisibleContent()) == "" even when no tools were offered. If the ChatGPT plan route legitimately returns an empty visible response for a connectivity probe without tools (e.g., a reasoning-only reply), this would produce a false failure.

Consider whether the empty-visible-content check should also be gated on len(tools) > 0, or whether the intent is that ChatGPT must always produce visible text regardless of tool presence. If the latter, adding a brief comment explaining why would help future maintainers.


{!ambientAuth && (
{preset.oauth && (
<p class="form-hint">{t('view.config.chatgptLoginHint')} <code>ocr llm login openai-chatgpt</code></p>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
The provider name openai-chatgpt is hardcoded in the command string. If additional OAuth providers are added in the future, this hint won't dynamically reflect the correct provider name. Consider using preset.name to make it generic:

<code>ocr llm login {preset.name}</code>

This way the hint remains accurate for any OAuth provider.

if err := f.Close(); err != nil {
return err
}
return os.Rename(f.Name(), c.path)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · low
The credential file is written via os.CreateTemp + os.Rename, which creates the temp file with mode 0600. However, there is no explicit os.Chmod on the final file after rename. While os.CreateTemp uses 0600 on Unix, this is an implementation detail rather than a documented guarantee, and behavior may differ on non-Unix platforms. Since this file contains OAuth tokens and refresh tokens, explicitly setting restrictive permissions would be more robust.

Consider adding os.Chmod(c.path, 0o600) after the rename (or as part of a follow-up hardening step).

Comment on lines +343 to +352
if !c.now().Add(time.Minute).Before(a.ExpiresAt) && !c.now().Before(a.EarliestRefreshAt) {
if a.RefreshToken == "" {
return errors.New("ChatGPT session is not renewable; sign in again")
}
p, _, err := c.discovery(ctx)
if err != nil {
return err
}
var t tokenResponse
err = c.request(ctx, http.MethodPost, p.Endpoint().TokenURL, url.Values{"grant_type": {"refresh_token"}, "client_id": {a.ClientID}, "refresh_token": {a.RefreshToken}, "resource": {c.resource}}, "", &t)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · medium
Holding a file lock across network I/O can block other processes (or concurrent goroutines in the same process via TokenSource) for up to the HTTP client timeout (30s per request). During token refresh, two HTTP calls are made (discovery + token endpoint) while the lock is held. Similarly, Logout makes up to 3 retry attempts against the revocation endpoint under the same lock.

Consider restructuring to read/update the store under the lock, release it for the HTTP call, then re-acquire to persist the result. Alternatively, use a shorter per-request timeout for the refresh path or document that callers should expect contention during refresh.

Also clarify why the ChatGPT self-test requires a visible answer, and derive the extension login hint from the preset name.
@trongtrandp
trongtrandp marked this pull request as ready for review October 3, 2026 13:45

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants