Repository navigation
feat(oauth): paste a claude setup-token into the Claude Code provider - #15093
Merged
diegosouzapw merged 9 commits intoOct 7, 2026
Merged
diegosouzapw merged 9 commits into
diegosouzapw merged 9 commits into
Conversation
`claude setup-token` mints a one-year OAuth token (sk-ant-oat01-...) scoped to user:inference only: no refresh token, and bootstrap/profile answer 403, so it carries no email or account UUID. The request path and the health sweep already handle such a connection (diegosouzapw#14287), but nothing could create one: the Claude auth-file import requires a refresh token and the generic import-token action was not open to `claude`. - claude provider: importToken() checks the sk-ant-oat prefix, verifies the token with one max_tokens=1 request before anything is stored, and returns a refresh-less OAuth payload with a fresh cliUserID. - import-token route: accepts `claude`, awaits a provider's async importToken() when present, matches identity-less tokens on the token itself (re-paste updates instead of duplicating; connectionId reauth now works without an email), keeps an existing cliUserID (diegosouzapw#10143), and returns the error's own 4xx instead of a blanket 500. - OAuthModal: Claude Code gets a "Setup Token" tab next to Browser Login. The per-provider tab/description/placeholder keys move into oauthModal/pasteTokenCopy.ts, so the frozen modal shrinks by 2 lines. - i18n: 3 new oauthModal keys, translated in all 66 locales.
Adding a Claude Code connection used to start the browser OAuth flow on open, popping claude.ai before the user could reach the Setup Token tab. Claude now opens in paste mode like the import-only providers; Browser Login stays one tab away and starts the flow only when clicked. Setup-token connections are named "Setup token" so they are told apart from browser-login rows (they carry no email), and the provider guide documents the flow, the verification request and what the inference-only scope cannot do.
…ry open With Claude Code opening on the setup-token form, nothing restarted the modal on reopen: adding a second connection after a successful import showed the previous "Connection successful!" screen, and once that was fixed the token field still held the last pasted token. The render-time reset now restores the first step for every provider (GitLab Duo keeps its setup step) and clears the pasted token, which also covers the import-only Devin providers.
Contributor
Author
|
@diegosouzapw please CR, thanks of OAUTH api tokens we dont have to refresh them so often (they are valid fo 1 year). the regular oauth tokens expiring after few weeks |
…py table The source contract in devin-providers.test.ts grepped OAuthModal.tsx for devinDesktopPasteDescription; the per-provider paste keys now live in oauthModal/pasteTokenCopy.ts, so the test reads both files. The Devin Desktop copy itself is unchanged.
Four screenshots from a Docker build of this branch: the dialog opening on the Setup Token tab, a token Anthropic rejects, the saved connection row and the empty form on reopen. The provider guide shows the dialog.
…ocales Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Keep the tip's ICU-escaped catalogs and add the three setup-token message keys from the pull request. Register check:ui-icu in the gate manifest so the pre-commit hook accepts the script added earlier. Co-authored-by: Damian Pozimski <damian.pozimski@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
claude setup-tokenprints a one-year OAuth token (sk-ant-oat01-...) for a Claude Pro/Max subscription. It is the simplest way to connect a headless or Docker OmniRoute to Claude Code, because nobody has to finish a browser login on the server. Until this PR, OmniRoute could run such a connection but had no way to create one:POST /api/oauth/claude/import-tokenansweredimport-token not supported for provider: claude.missing_refresh_token, because a setup-token has no refresh token.The runtime was already ready for it. Since #14287 the health sweep leaves refresh-less Claude rows alone, and #14261 shows people were already running them. This PR adds the missing way in, on the existing
claudeprovider, so there is no new provider to maintain.What changes for the user:
sk-ant-oat, and OmniRoute sends onemax_tokens: 1request with it. If Anthropic rejects it, the user sees the reason and no connection is created.user:inferencescope, so Anthropic will not return an email for it.docs/providers/CLAUDE_CODE_SETUP_TOKEN.mdexplains the flow and what an inference-only token cannot do (no quota bars, no email).oauthModalstrings, translated in all 66 locales.Under the hood:
claudejoins the import-token allowlist, and a provider can now expose an asyncimportToken()for upstream verification.connectionIdre-auth now works without an email too.cliUserID, so Anthropic keeps seeing the same device (fix(oauth): send required CLI headers in claude-auth import bootstrap call #10143).Opening on the paste form exposed a small modal bug, fixed here as well. After a successful import, reopening the dialog showed the old "Connection successful!" screen and kept the previous token in the field. The dialog now resets its step and clears the field every time it opens, for every provider, including the import-only Devin ones.
Related Issues
Evidence: it works end to end
Everything below comes from this branch built with the repo
Dockerfile, started with a fresh volume andREQUIRE_API_KEY=true, using a realclaude setup-tokentoken (masked). There are two runs on the same image. Section 1 is a scripted run against the import API (connection50370877). Sections 2 and 3 are a second run where the token was pasted in the dashboard popup (connection50f6e853). Every model call went through an OmniRoute API key.1. Before and after
Before:
diegosouzapw/omniroute:latest(3.8.50)After: this branch (3.8.52)
2. Every Opus and Sonnet model answers through the setup-token connection
One
POST /v1/chat/completionsper model with the prompt "Compute 17*23. Reply with exactly: MODEL-OK ". The answer is checkable, so a 200 alone is not taken as proof.Streaming and the Anthropic Messages format both work too:
The call log confirms every request ran on the setup-token connection (
conn=50f6e853, the row named "Setup token"):3. The dashboard flow
Playwright against the same image, in a fresh browser profile: risk notice, then the dialog.
The four screenshots are part of this PR under
docs/screenshots/claude-setup-token-*.png, and the provider guide shows the first one.I also scripted 16 opens across reloads, re-logins and three fresh browser profiles. Every one landed on the paste form, and none opened a popup.
4. What an inference-only token cannot do
The token has one scope,
user:inference. Anthropic answers403 oauth_scope_insufficienton bootstrap, profile and usage (direct probe:GET /api/claude_cli/bootstrapasks foruser:ccr_inferenceoruser:profile). So the connection shows no email and no quota bars; the usage call returns "Claude connected. Usage API requires admin permissions." Requests are not affected.Fable returned 404 on the test account. That comes from Anthropic, not from this change: a direct call to
api.anthropic.comwith the same token givesnot_found_error: model: claude-fable-5-1(and the same forclaude-fable-5), whileclaude-opus-5returns 200 on the same call. The test subscription simply has no Fable access.Validation
npm run lint(lint-staged on every commit), plustypecheck:core,check:dashboard-typecheck,check:file-sizeandcheck:complexity-ratchetsin PR mode,check:cycles,check:dead-code,i18n:check-new-keys,check:docs-symbols,check:doc-links,check:fabricated-docs,check:env-doc-sync,check:changelog-integrity: all green locallyrelease/v3.8.52at666ea59b4c;release/v3.8.51is frozen, 🔒 release-freeze: v3.8.51 in progress #15080)Local test runs:
npm run test:vitest: 54 files, 493 tests passedclaude-setup-token-import.test.tsfail on the base (the other two guard existing behaviour). All 4 tests inui/claude-setup-token-oauth-modal.test.tsxfail against the modal as it was before this PR.CI on this PR: API Route Typecheck, No new ESLint warnings, Vitest, Merge integrity and semgrep pass.⚠️ base-red inherited on two gates:
check:docs-countsflagsREADME.mdandllm.txtstill saying 3.8.51 after the v3.8.52 bump. This PR touches neither file, and the same check fails on fix(codex): hand off live SSE after readiness event #15094, feat(resilience): expose live combo queue telemetry #15092, fix(proxy): preserve required egress for no-auth account proxy references #15087 and fix(capabilities): honor model-compat vision overrides in catalog and combos #15086.devin-providers.test.tsgrepped the modal for a key that moved intopasteTokenCopy.ts, fixed in504e211652. The other 34 (GLM, Qwen, Opencode geo rotation, Codex quota resets, combo lockout, GHE Copilot registry, token refresh mutex and others) also fail on those sibling PRs.Tests Added Or Updated
tests/unit/claude-setup-token-import.test.ts(new, route level, Anthropic stubbed): a valid token creates a refresh-less OAuth row; the health sweep leaves it alone; a wrong prefix returns 400 without any upstream call; a non-string token returns 400; an Anthropic 401 returns 400 and stores nothing; the same token twice gives one row with the samecliUserID;connectionIdre-auth replaces the token; a different token creates a second rowtests/unit/ui/claude-setup-token-oauth-modal.test.tsx(new, jsdom): the dialog opens on the form with no fetch and nowindow.open; Save posts the trimmed token; Browser Login starts the flow only when clicked; reopening after success shows an empty formtests/unit/oauth-modal-paste-token-copy.test.ts(new): per-provider tab, description and placeholder keys, and every key exists inen.jsontests/unit/devin-providers.test.ts(updated): same move for the Devin Desktop description key; the test now reads the modal andoauthModal/pasteTokenCopy.ts. The Devin copy is unchanged.tests/unit/oauth-modal-grok-cli-paste-7610.test.ts(updated): the grok copy keys moved from the modal intooauthModal/pasteTokenCopy.ts, so the source contract now reads both files. The grok-cli keys themselves are unchanged.Coverage Notes
src/lib/oauth/providers/claude.ts(importSetupToken) and the import-token branch pluskeepDeviceIdentityinsrc/app/api/oauth/[provider]/[action]/route.tsare covered by the route test.src/shared/components/OAuthModal.tsx(paste-first providers, on-open reset) andoauthModal/pasteTokenCopy.tsare covered by the jsdom and copy tests.Reviewer Notes
OAuthModal.tsxgets shorter (1194 to 1191 lines). The per-provider tab, description and placeholder ternaries moved into a small key table inoauthModal/pasteTokenCopy.ts, following the helper-module precedent for frozen files.connectionIdre-auth was ignored for tokens without one. The order is nowconnectionId, then email when there is one, then the token itself. For Devin and Grok the only visible difference is that pasting the same token again updates the row instead of adding a duplicate.connected. Hiding them for refresh-less rows is a separate change.claude-sonnet-5on the same connection for 120 s (model_cooldown), even though the logged upstream request was forclaude-fable-5only. Sonnet 4.6 is unaffected. The lock comes from the existingMODEL_NOT_FOUNDbranch inopen-sse/handlers/chatCore.ts. I can open a separate issue with the logs.npm run buildinside the Dockerfile runs out of V8 heap at the defaultOMNIROUTE_BUILD_MEMORY_MB=6144(and at 4096), even in a 16 GB Docker VM. It builds with--build-arg OMNIROUTE_BUILD_MEMORY_MB=10240. Unrelated to this change, but anyone reproducing the image will hit it.