Sign in with ChatGPT token sharing
exarch’s chatgpt service speaks OpenAI’s published route for open-source
apps: dynamic-client OAuth at auth.openai.com, inference at
https://api.openai.com/v1/responses with the plan’s bearer token, and nothing
borrowed from the Codex CLI.
Context
exarch used to imitate the Codex CLI: its client id, originator, a
codex_cli_rs user-agent and client_version, chatgpt-account-id and
openai-beta headers, requests to chatgpt.com/backend-api, a device-code
flow, the wham/usage meter. OpenAI now publishes a token-sharing route for
open-source apps, and calls reuse of app-server auth by other services never
permitted. The imitation had no standing; the route does.
Decision
- One route.
ORIGINATOR, the user-agent,client_versionand itsEXARCH_CODEX_CLIENT_VERSIONvalve, the account and beta headers, thebackend-api/codex/modelslister, thewham/usagemeter and the Codex-onlyexternal_web_accesstool config are deleted. The resolver supplies the bearer; the endpoint is genai’s default. - One flow. OpenAI documents a loopback redirect to
http://127.0.0.1:<port>/auth/callback; the device-code flow,exarch login --device-authand the TUI’s browser/device selector are deleted. - Host id and issued clients.
oauth.jsonholds one host id (urn:uuid:<v4>, minted once, never user-identifying) and, per account, the issued client id (oaiapp_…) and the retained ID token, so a re-login presents the account’s own client andid_token_hint. The sign-in choice isSignIn::RegisterorSignIn::Reauthorize(token);exarch login <account>renews. - The ID token is decoded, not verified. It arrives from the issuer’s
token endpoint over TLS and names the account, nothing more.
finalizechecks thatnoncematches the attempt, thatsubis present (and equals the renewed account’s), and thatchatgpt.tokens.use.directwas granted. - No usage readout.
chatgptisMeter::Unpublished. A spent cap is a 429subscription_sharing_usage_limit_exceedednaming no reset, in the unwaitable set, surfaced at once; the user is pointed at ChatGPT Settings → Usage. - No sampling controls. The route refuses
temperature,top_pandmax_output_tokens;Service::shares_a_planmakesBureau’s selection carry reasoning effort alone. - No migration. Codex-era tokens were minted for another client and scope and are dead either way; a store of the old shape reads as no accounts.
Consequences
/limits shows nothing for a plan. A full logout deletes the store and with it
the host id; the next sign-in mints another, which OpenAI sees as a new host.
Web search remains subject to the account’s policy.
Known gap
genai 0.7.0-rc.8 still collapses a mid-stream response.failed to
Error::StreamParse(message), losing error.code, so such a failure surfaces
as ProviderError::Other with the message. The fix is upstream: surface it as
Error::ChatResponse { body }, as the error event already is.
See provider and provider-fault-recovery.