Repository navigation
feat(web): add Sign in with ChatGPT and an OpenAI BYOK connection (part 1/1) - #6255
Conversation
Code Review SummaryStatus: 3 Issues Found | Recommendation: Address before merge Executive SummaryA previously-fixed path regressed: a prefilled Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (9 files)
Resolved since the previous review: Fix these issues in Kilo Cloud Previous Review Summaries (8 snapshots, latest commit f5b6107)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit f5b6107)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryRenaming the delegated partner-key env var to Overview
Issue Details (click to expand)WARNING
Files Reviewed (3 files)
Fix these issues in Kilo Cloud Previous review (commit bae99ce)Status: No Issues Found | Recommendation: Merge Executive SummaryIncremental review at Files Reviewed (2 files)
Previous review (commit 9f2b643)Status: No Issues Found | Recommendation: Merge Incremental review at Files Reviewed (2 files)
Previous review (commit d0879e6)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (11 files)
Notes
Fix these issues in Kilo Cloud Previous review (commit d20e6a0)Status: 5 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (6 files)Changed in revision
Carried forward and re-verified at HEAD
Notes
Fix these issues in Kilo Cloud Previous review (commit 0cd12eb)Status: 5 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (16 files)
NotesMemory-leak check (requested): no new leaks. Fix these issues in Kilo Cloud Previous review (commit 28ff13c)Status: No New Issues Found | Recommendation: Merge Overview
Both findings from the previous review are resolved in the current revision. Previous findings, re-verified on changed code
Files Reviewed in this revision (7 files)
Previous review (commit 6596424)Status: 2 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)CRITICAL
SUGGESTION
Files Reviewed (44 files)
Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0 Review guidance: REVIEW.md from base branch |
d204d3e to
28ff13c
Compare
- Gate the ChatGPT sign-in option and the OpenAI BYOK card behind the PostHog flag `sign-in-with-chatgpt`, whose release condition holds the approved email domains. - Evaluate the flag against the email a signed-out visitor types, so the sign-in page honors the same allow-list without a hardcoded list. - Show the OpenAI logo beside the ChatGPT sign-in label. - Serve the OAuth callback at /auth/openai/callback.
pandemicsyn
left a comment
There was a problem hiding this comment.
lgmt, i think you could probably resolve most of these in a follow up PR if you want.
- Re-evaluate the ChatGPT access flag when PostHog finishes loading, and debounce the typed email so a multi-character edit issues one reload. - Assert the sign-in form passes the typed email to the hook; add hook tests. - Hold the BYOK card's reserved height while the flag resolves. - Exclude a row disabled through the BYOK toggle from delegated routing. - Write the terminal refresh failure inside the row lock so a concurrent reconnect is not erased.
openid-client builds the code exchange from a fixed field set and drops `resource`, so an authorization that requested a resource was redeemed without one and OpenAI rejected it with `invalid_grant`. Pass the resource back through `exchangeBody`. Also update the delegated-connection store mock in get-provider.local-fake.test.ts for the new enabled-flag read.
OpenAI requires `subscription_sharing_activity` and `subscription_sharing_purpose` in the request metadata of every delegated Responses call. Add them, take `subscription_sharing_activity_id` from the gateway session when there is one, and keep caller metadata within OpenAI's 16-entry limit. Also drop the OpenRouter-only `user` field before the request reaches api.openai.com.
`OPENAI_API_KEY` belongs to another project and doubles as the Vercel AI Gateway BYOK credential. Read the delegated partner key from `OPENAI_CHATGPT_API_KEY` instead, so the two paths cannot inherit each other's project, quota, or billing.
`get-provider.local-fake.test.ts` now overrides `OPENAI_CHATGPT_API_KEY`, which the renamed env var made necessary. Remove the `user` deletion: api.openai.com accepts the field on `/v1/responses`, so it was not the fix and is not needed.
The sign-in page evaluated the flag as the visitor typed, one flags request per keystroke. Evaluate it from the address they submit instead, so one submission costs one reload and the option is decided before the sign-in options render.
The catalog can list an OpenAI model the plain API does not serve, such as `openai/gpt-5.6-luna-pro`, which answers with a 404. Sending it to the delegated upstream fails the request instead of leaving it on its existing route. Eligibility now reads the project's `GET /v1/models` list, cached for an hour. A missing key or an unreadable list keeps the route, so a transient problem never hides it.
Changelog for users
Changelog for maintainers
OPENAI_CLIENT_IDandOPENAI_CLIENT_SECRETcome from the environment; the secret is server-side only and sent as HTTP Basic. OptionalOPENAI_DISCOVERY_URL,OPENAI_TOKEN_ENDPOINTandOPENAI_CHATGPT_API_URLoverrides exist, and no client-id literal is written into source.openaiNextAuth provider validates the ID token through discovery with PKCE, state and nonce checks, and uses the registered redirect<app origin>/testing/oai-redirect. A new route rewrites that path to the NextAuth callback and returns a declined or failed connect to/byokfor a signed-in person.<issuer>#<sub>. Re-linking the identical id is a no-op; a different id still returns PROVIDER-ALREADY-LINKED. Unlinking the provider deletes the stored connection.byok_api_keysunder provider idopenai-chatgpt, refreshes single-flight under a row lock, and a terminal refresh failure clears the tokens and disables the row. The status and disconnect procedures return only state and identity, never a token.getProviderprefers an enabled connection for eligible responses requests and keeps the normal balance and abuse checks; a terminally dead connection returnschatgpt-reconnectinstead of another billing path. The partner key isOPENAI_API_KEY, and the delegated token travels inOpenAI-On-Behalf-Of-Token.E2E proof
[e1] web: the sign-in page offers ChatGPT beside the existing methods - open /users/sign_in and the OAuth group shows 'Sign in with ChatGPT' together with 'Continue with Google' and 'Continue with Email'. — probe-auth.log: 'e1 url=http://localhost:6900/users/sign_in hasChatGPT=true hasGoogle=true hasEmail=true buttons=[... "Continue with Email", ... "Sign in with ChatGPT"]' (screenshot shots/e1-signin.png).
[e1] web: signing in returns the same person to their existing account, never a duplicate - precondition: a Kilo account exists whose email matches the ChatGPT account, needs:seed; complete the Sign in… — Consent completed in the browser and the session resolved to the account that already existed: e1-signin.log 'LANDED_URL http://localhost:6900/profile' with '"kiloUserId":"d72f6758-7d2d-4a61-bbe2-4fe6fe9f2f60"' and '"isNewUser":false'; e1-pre-db.log holds that pre-existing id ('d72f6758-7d2d-4a61-bbe2-4fe6fe9f2f60|verify-byok-1789588278700@example.com', count '1') and e1-post-db.log still shows one row for the email plus 'openai|https://auth.openai.com#vf-e1-sub|d72f6758-7d2d-4a61-bbe2-4fe6fe9f2f60'; no real ChatGPT account is supplied, so the consent ran against a local stub issuer mirroring…
[e2] web: the sign-up page offers the same login/signup option - open /users/sign_in?signup=true and the OAuth button group shows 'Sign in with ChatGPT' beside 'Continue with Google'. — /users/sign_in?signup=true renders 'Sign in with ChatGPT' between 'Continue with Discord' and 'Continue with Email', beside 'Continue with Google'.
[e2] web: a person with no account gets a new one - complete the Sign in with ChatGPT consent with a supplied ChatGPT account whose email has no Kilo user (needs the account +… — A ChatGPT identity with no Kilo account created one: e2-pre-db.log '0' users with the email before, e2-signin.log '"isNewUser":true' with '"kiloUserId":"6d4d26f5-84a9-4824-9abb-c30cfe87dfcc"', and e2-post-db.log '6d4d26f5-84a9-4824-9abb-c30cfe87dfcc|vf-oai-new-1789603494@example.com' plus 'openai|https://auth.openai.com#vf-e2-sub|6d4d26f5-84a9-4824-9abb-c30cfe87dfcc' (stub issuer as in e1); screenshots e2-signin-1-start.png, e2-signin-2-landed.png.
[e2] web: a person with no account gets a new one - complete the Sign in with ChatGPT consent with a supplied ChatGPT account whose email has no Kilo user (needs the account +…
[e4] web: an untrusted callback creates no session and no account - replay GET /testing/oai-redirect?code=not-a-real-code&state= in a context holding the transaction… — with no transaction cookies the replay lands on /users/sign_in?callbackUrl=…&error=OAuthCallback showing 'Error / Oops! Something went wrong trying to log in.', /api/auth/session returns {}, and db.sh gives user_auth_provider_openai=0 (db-checks.log).
E2E proof — log excerpts
/home/igor_kilocode_ai/.local/share/kwf/sections/owner-signin-with-chatgpt-dc3c/e2e-web/e1-refresh.log/home/igor_kilocode_ai/.local/share/kwf/sections/owner-signin-with-chatgpt-dc3c/e2e-web/stub-issuer.log/home/igor_kilocode_ai/.local/share/kwf/sections/owner-signin-with-chatgpt-dc3c/e2e-web/e2-gateway.log/home/igor_kilocode_ai/.local/share/kwf/sections/owner-signin-with-chatgpt-dc3c/e2e-web/e2-gateway-nextjs.log/home/igor_kilocode_ai/.local/share/kwf/sections/owner-signin-with-chatgpt-dc3c/e2e-web/e2-usage.log/home/igor_kilocode_ai/.local/share/kwf/sections/owner-signin-with-chatgpt-dc3c/e2e-web/e3-summary.logOwner request for the stack
[e20] ux-check: declining over a Needs reconnect state keeps the reconnect context — with a stored needs-reconnect row, the declined consent returns to /byok where the card still shows 'Needs reconnect' + the expired sentence + 'Reconnect with ChatGPT' and 'Disconnect'.
[e19] ux-check: exactly one CTA and a visible in-progress state — /byok with no connection has exactly 1 'Sign in with ChatGPT' button and, on click, its label becomes 'Redirecting to ChatGPT...' with disabled=true for the whole observation window (it never becomes clickable again).
[e21] ux-check: no two entries are both labelled exactly 'OpenAI' — the subscription card is labelled 'OpenAI (ChatGPT subscription)' and the page contains 0 elements whose text is exactly 'OpenAI'; the paste-key dialog's provider list renders only on open, so its item label was not observed.
[e15] ux-check: sign-in and sign-up both show 'Sign in with ChatGPT' and it reaches the consent screen — probe-e15.log: 'e15 signup clicked url= https://auth.openai.com/api/accounts/authorize?...response_type=code...' and probe-auth.log e1/p1 both show hasChatGPT=true with the OAuth group (screenshots shots/e15-signup-after-click.png, shots/e1-signin.png, shots/p1-signup.png).
[p8] ux-check: no two entries labelled exactly 'OpenAI'; the subscription card and the paste-key entry are distinguishable — The p8 probe printed 'p8 exactOpenaiLabels=0 cardTitleCount=1 cardText="OpenAI (ChatGPT subscription) Connect with your ChatGPT subscription to use OpenAI models in Kilo. No API key needed. Sign in with ChatGPT"'; no exact 'OpenAI' label exists and the card is qualified (screenshot shots/p8-labels.png).
[e7] web: the disconnected state is clear and the page does not shift - sign in with the test account, open /byok, the OpenAI card shows the connect explanation and a single 'Sign in with ChatGPT' button… — card-e7.log: 'e7 w=375 loadingY=396 resolvedY=396 delta=0 ... pageChatGptButtons=1' and 'e7 w=1440 loadingY=358 resolvedY=358 delta=0 ... pageChatGptButtons=1', card text 'Connect with your ChatGPT subscription to use OpenAI models in Kilo. No API key needed.' (screenshots shots/e7-loading-375.png, shots/e7-resolved-375.png).
[e15] ux-check: sign-in and sign-up both show 'Sign in with ChatGPT' and it reaches the consent screen
[e10] web: one click starts a connection, not the sign-in Turnstile bounce - signed in with the test account, open /byok and tap Sign in with ChatGPT once; assert the browser reaches… — probe-e10.log: signed-in /byok click lands on 'https://auth.openai.com/api/accounts/authorize?...scope=openid profile email offline_access resource.invoke chatpass.enable.request&response_type=code...' and 'e10 finalUrl= https://auth.openai.com/... turnstile= false'; screenshots shots/e10-byok-before.png, shots/e10-after-click.png.
[e10] ux-check: the 'BYOK API Keys' offset does not change across card states at 375px and 1440px — vfe-offset.log: 'e10 w=375 disconnected loadingY=396 resolvedY=396 delta=0', 'e10 w=375 connected ... delta=0', 'e10 w=1440 disconnected loadingY=358 resolvedY=358 delta=0', 'e10 w=1440 connected ... delta=0' with 3 skeletons during loading (shots/e10-disconnected-375-loading.png, shots/e10-connected-375.png, shots/e10-disconnected-1440.png).
[e10] ux-check: the 'BYOK API Keys' offset does not change across card states at 375px and 1440px
[e10] ux-check: the 'BYOK API Keys' offset does not change across card states at 375px and 1440px
[p6] web: disconnect is one click and returns the card to the disconnected state - with a connection present, tap 'Disconnect' once: the card shows the connect state again with no dialog, select count(*)… — Fixture connected row (placeholder tokens, is_enabled=true), one click on Disconnect: r3-disconnect.log 'r3p6 before="OpenAI (ChatGPT subscription) Connected Connected as vf-verify@example.com Connected on 9/16/2026 Disconnect" beforeY=362 after="OpenAI (ChatGPT subscription) Connect with your ChatGPT subscription to use OpenAI models in Kilo. No API key needed. Sign in with ChatGPT" afterButtons=["Sign in with ChatGPT"] afterY=362 delta=0 rows=0' - no dialog, the row is gone and nothing below the card moved; captures shots/r3-p6-before.png and shots/r3-p6-after.png.
[p6] web: disconnect is one click and returns the card to the disconnected state - with a connection present, tap 'Disconnect' once: the card shows the connect state again with no dialog, select count(*)…
[e6] web: an expired or failed connection is readable and offers one clear recovery - with a fixture connection row carrying is_enabled false, status 'error' and the message 'Your ChatGPT connection has… — Fixture row mirroring the real payload (is_enabled=false, status error, error_message, dumped in e2e-web/r3-fixture.log, encrypted with the app BYOK_ENCRYPTION_KEY): r3-states.log 'r3 w=375 discY=423 errLoadingY=423 errY=423 errDelta=0 errVsDisc=0 connY=423 connVsDisc=0 skeletons=3', 'errCard="OpenAI (ChatGPT subscription) Needs reconnect Your ChatGPT connection has expired. Reconnect to continue.', 'errButtons=["Reconnect with ChatGPT","Disconnect"]' and 'sentence=true hasReconnect=true hasDisconnect=true sameAuthorizationAsConnect=true'; captures shots/r3-e6-375-loading.png and…
[p7] web: a declined permission is reported readably and offers Try again - with a signed-in session, navigate to /testing/oai-redirect?error=access_denied&state=stub: the person lands on /byok (not the… — r3-declined.log 'noRowCard="OpenAI (ChatGPT subscription) ChatGPT was not connected. Try again. Try again" noRowButtons=["Try again"] noRowY=423 noRowBodyHasMessage=true' and 'noRowY=362' at 1440, reached by arriving at /byok with openai_error=access_denied; capture shots/r3-p7-declined-norow-375.png.
[p11] ux-check: declining consent on a stored needs-reconnect connection keeps the reconnect context and Disconnect — r3-declined.log 'storedBefore="OpenAI (ChatGPT subscription) Needs reconnect Your ChatGPT connection has expired. Reconnect to continue. Reconnect with ChatGPT Disconnect" beforeY=423 afterDecline="OpenAI (ChatGPT subscription) Needs reconnect ChatGPT was not connected. Try again. Reconnect with ChatGPT Disconnect"', 'declineDelta=0', 'rowsAfterDecline=1' and 'afterY=362' at 1440 - the card keeps the reconnect context and Disconnect, not only Try again; capture shots/r3-p11-declined-stored-375.png.
[p13] ux-check: the 'BYOK API Keys' offset does not change across card states at 375px and 1440px — r3-states.log 'connY=423 connVsDisc=0 skeletons=3' at 375px and 'r3 w=1440 discY=362' with the same run's errDelta=0/connVsDisc=0 - the loading skeleton, disconnected, expired and connected states all keep the heading at one offset, against a 23px shift before the fix; captures shots/r3-e6-375-loading.png, shots/r3-e6-375-error.png, shots/r3-e6-1440-loading.png, shots/r3-e6-1440-error.png, shots/r3-p13-connected-375.png, shots/r3-p5-disconnected-375.png.
[p12] ux-check: no two distinct entries on /byok are both labelled exactly 'OpenAI' — r3-labels.log 'r3p12 exactOpenaiLabels=0 cardTitleCount=1' - the ChatGPT-subscription card is the only OpenAI entry and no unqualified 'OpenAI' label exists on the page; capture shots/r3-p12-labels.png.
[p13] ux-check: the 'BYOK API Keys' offset does not change across card states at 375px and 1440px
[e6] web: an expired or failed connection is readable and offers one clear recovery - with a fixture connection row carrying is_enabled false, status 'error' and the message 'Your ChatGPT connection has…
[p9] web: a failed disconnect is readable and retryable - with a fixture connection row, make the disconnect mutation fail (network off or a 500) and tap Disconnect once: the card shows We couldn't… — Fault injected as a stubbed 500 on the disconnect mutation (the pack's e2e index lists no fault hook or test id for this surface, so the failing response is a stub): r3-disconnect-fail.log 'r3p9 before="OpenAI (ChatGPT subscription) Connected Connected as vf-verify@example.com Connected on 9/16/2026 Disconnect" beforeY=362 after="OpenAI (ChatGPT subscription) Connected We couldn't disconnect ChatGPT. Try again. Disconnect" afterButtons=["Disconnect"] afterY=362 delta=0 rows=1 blockedPosts=["openAiChatGpt.disconnect?batch=1"]' - readable failure, Disconnect kept, row intact and nothing below…
[p10] ux-check: one 'Sign in with ChatGPT' button with a visible in-progress state — r3-connect.log 'r3p10 pageChatGptButtons=1 cardButtons=["Sign in with ChatGPT"]' shows exactly one connect control with no connection, and the same click issues the authorization and leaves the page ('turnstileBounce=false'), so the control cannot be clicked twice before the browser leaves; the in-progress rendering itself is a visual claim and the pending navigation destroys the page context before any DOM read, which is left to the visual reviewer (captures shots/r3-p10-in-progress.png); the diff does not touch the button's click handler.
[p5] web: the disconnected state is clear and the page does not shift - sign in with the test account, open /byok, the OpenAI card shows the connect explanation and a single 'Sign in with ChatGPT' button… — r3-states.log 'discCard="OpenAI (ChatGPT subscription) Connect with your ChatGPT subscription to use OpenAI models in Kilo. No API key needed. Sign in with ChatGPT" discButtons=["Sign in with ChatGPT"]' with 'discY=423' equal to the held-skeleton offset measured in the same run, and the 1440 pair at 362 - one connect control, and the key-list heading is at the same offset before and after the card resolves; capture shots/r3-p5-disconnected-375.png.
[e6] web: the expired state keeps one recovery and one removal, and the page does not move - with a fixture connection row (is_enabled false, status error, message Your ChatGPT connection has expired. — Fixture row mirroring the real payload (is_enabled=false, status error, error_message, encrypted with the app BYOK_ENCRYPTION_KEY) inserted by the verifier driver: e6-375.log 'FIXTURE inserted is_enabled=false status=error rows=1', 'RESULT w=375 loadingHeadingY=423 resolvedHeadingY=423 delta=0', 'ASSERTS sentence=true hasReconnect=true hasDisconnect=true' and CARD carries 'Your ChatGPT connection has expired. Reconnect to continue.' with '"Reconnect with ChatGPT","Disconnect"'; e6-1440.log 'RESULT w=1440 loadingHeadingY=362 resolvedHeadingY=362 delta=0' - the 'BYOK API Keys' heading keeps one…
[e6] web: the expired state keeps one recovery and one removal, and the page does not move - with a fixture connection row (is_enabled false, status error, message Your ChatGPT connection has expired.
[e5] web: connecting with the real ChatGPT account shows the connected state - tap 'Sign in with ChatGPT', complete consent with the supplied account, return to /byok: the card shows the connected… — The card's own connect control was exercised after deleting the session's row: e5-card.log 'ROW_DELETED' follows 'CARD_BEFORE' with '"card":"OpenAI (ChatGPT subscription) Connect with your ChatGPT subscription to use OpenAI models in Kilo. No API key needed. Sign in with ChatGPT' and '"chatgptButtons":["Sign in with ChatGPT"]', and 'CARD_AFTER' shows '"card":"OpenAI (ChatGPT subscription) Connected Connected as vf-e5b-1789603892@example.com Connected on 9/17/2026 Disconnect' with '"chatgptButtons":[]'; e5-post-db.log records…
Owner manual verification
Owner verification is pending; these checks did not pass automatically.
PR stack (merge bottom to top)
kwf/owner-signin-with-chatgpt-dc3c-l1) ← this PR