fix(*): sign in to an oauth provider from /model - #279
Conversation
Enter on an OAuth row ran no setter and sent no request, so nothing on screen changed and the row's own warning read the same before and after. The screen above it says "Enter to set one up". Enter now opens a sign-in screen that names the provider and the command, and Enter there hands the terminal to `raven provider login <slug>` for the browser flow, refetches the options when the child exits, and stays put so a failure has somewhere to be reported. Three keyboard holes came with it: - the new stage needs its own guard in useInput, or Enter falls through to the list handler and silently returns to the list -- the very symptom being fixed; - bare `q` closes the overlay, which on the key screen meant an API key containing a `q` could never be typed; - the handoff shares this process group, so Ctrl-C reached the parent's signal handler and ended the session instead of the sign-in that was waiting on a browser. Signals are deferred while a child owns the terminal, and the three hand-off sites go through one helper. `/setup` spawned `raven setup`, which is not a registered command, so it could only ever reach the non-zero exit path; it launches `raven onboard` now and keeps its place out of the palette. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
A credential was written by one client and read by another's guess at where it went: the kit names Codex's file for the Codex CLI while we read it under raven's slug, and Copilot's token belongs to LiteLLM. A provider the user had signed in to reported itself unauthenticated forever, and `d` deleted a file nothing had written. Every family now derives one path, and the one that writes the file is the one asked where it is -- the kit's storage object answers for Codex rather than a second derivation beside it, which is what disagreed in the first place. Credentials outside that directory are not read: the Codex CLI's token stays the Codex CLI's, so refreshing ours cannot invalidate the copy it still holds, and a disconnect can remove everything it reports. That costs one sign-in per provider after upgrading. Copilot keeps two files, and the API key outlived the access token it came from, so a disconnected provider went on answering until it expired. The suite could not have caught any of this: neither lookup goes through the patched home, so whether a provider reported itself authenticated depended on whether the developer running the tests had signed in. Also opens GitHub's device page for the Copilot login. LiteLLM owns that flow and only prints the code, so it was the one family with no browser hand-off. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…ir vendor files them Two things LiteLLM was asked for came back wrong. Its logs went to the terminal. It installs a stderr handler on its own loggers at import, and every import of it in raven is deferred -- so the handler lands after the CLI has stripped terminal handlers, and nothing removes it. With the stdlib intercept setting the root level to 0, every record including DEBUG was written over the Ink screen; a model LiteLLM cannot map produced enough of them to make the picker unusable. The handler is detached on import, and the records still reach the log file. Its metadata table is keyed by the vendor's own spelling, and we asked with the routing id. A provider reached by region or by subscription therefore missed every time, and each miss cost a second guess plus the live catalogue fetch behind it -- Codex and both MiniMax OAuth regions had no context window at all. The spec now names the entry that holds a model's price and window, and one function answers where it is filed. The picker no longer offers a provider's spec default when nothing else lists a model: for Codex that id is rejected by the account API, so offering it turned an honest dead end into a row that fails on use. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Two ways the reported figures were not the user's. A model routed straight to its vendor was asked about under an `openrouter/` alias first, and OpenRouter prices what it fronts under its own prefix: `deepseek/deepseek-chat` reported a 65,536-token window at $0.14 per million where the vendor's own row says 131,072 at $0.28. The id is asked as it routes now, with the alias behind it, so a model LiteLLM lists nowhere else is still covered. A plan is not billed per token. LiteLLM files those models at zero, which the tiers below read as "unknown" and answered with the pay-as-you-go rate the plan holder is not paying -- $2.50 per million for a Copilot seat, $1.75 for a ChatGPT one. The spec states what a provider is billed on and the estimate is absent for a plan; tokens and window occupancy, which do mean something on a subscription, are unaffected. Billing is declared rather than read off `is_oauth`: OAuth is how you authenticate, not how you are charged. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…-out `RAVEN_TUI_DISABLE_MOUSE` sets the store's default, and the hydrate then normalized an absent `mouse_tracking` to `true` -- so the opt-out came back on a moment after start. It is the one field here whose default does not come from this function, so it is the one field that has to be left alone when the config says nothing. The five-second `mtime` poll went with it. `config.get` answers with the four whitelisted dotted keys whatever params it is handed, so `mtime` was never in the response, the poll always took its early return, and the MCP reload it guarded never ran -- while still costing a round trip on the same stdio pipe that carries `prompt.submit`. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`configured` was `exists()`, so a truncated write, a hand-edited token or an empty file all reported the provider as ready -- to the picker, to `provider list`, and to the startup gate -- and then failed on the first request. Each family's own reader decides now: the kit's storage parses Codex's JSON, MiniMax already parsed its own, and Copilot's device token has to hold something. Expiry stays out of it: a token with a refresh token is usable and refreshing is the client's job. Only presence is claimed. Whether a credential is accepted takes a request -- Copilot can hold a valid device token and still be refused the API key exchange -- and that is what `provider test` is for. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
LiteLLM ships a ChatGPT driver for the same backend raven talks to, with the
same OAuth client id, and it already implements the device flow, the refresh
and the account-id derivation. Raven was carrying a second implementation of
all three through `oauth-cli-kit`, which is now dropped as a dependency.
`provider login openai-codex` asks that driver for a token, which is the
whole flow: it requests the device code, prints the page and the code, polls,
and writes the credential where the request path will look for it. The
directory is still raven's -- `CHATGPT_TOKEN_DIR` points at
`~/.raven/oauth/chatgpt` alongside Copilot's -- so one place still holds
every credential and a disconnect still removes them.
Reading it goes through one module, because the driver's own accessor opens a
device flow when it finds no token: correct for a login, wrong for a status
report or a request that should fail with something the user can act on.
The Responses call path stays raven's. The driver's chat transformation
inherits OpenAI's URL and would post to `{api_base}/chat/completions`, while
this backend serves `/responses` -- the endpoint its own metadata declares.
Moving the request path across is a separate change with a live-traffic
verification of its own.
Credentials written by the old client are not read: sign in once more.
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The question was answered in three places with three rules. The config resolves it -- honoring an explicit `agents.defaults.provider`, then prefix over keyword, and declining to fall back to an OAuth provider. The startup gate re-derived it from the model-id prefix, with a MiniMax special case standing in for the rule the resolver already states, which is how the gate came to disagree with `raven status` about a signed-in provider. The factory compared the resolved name against a chain of strings, plus one extra check for a model id spelled the old way. The gate asks the resolver and then asks whether that provider's credentials are on disk -- the one thing the resolver takes on trust for the OAuth families. Credentials for some other provider no longer make a model reachable: a key for one vendor used to let the session start on a model only another vendor could answer. The factory reads which client serves a provider off the spec, so a family is added by declaring it rather than by editing the chain. Resolving through the loader rather than validating the raw file: the config on disk carries sections the base model does not declare, and it forbids extras, so validating it directly fails on every real config. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`estimate_cost_usd` answers None for a plan-billed provider, and the caller collapsed it to 0.0 before anything downstream saw it -- so a Copilot seat reported a spend of zero rather than no spend, and the daily roll-up summed those zeros as if the calls had been free. The field is optional the whole way now: the snapshot carries None, the accumulator adds only the calls that had a price, and the status bar already renders the figure only when it is a number. Tokens and window occupancy -- the measures that mean something on a subscription -- are unchanged. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The key was a hash of the whole transcript, which grows every turn, so it was different on every request -- and grouping requests that share a cached prefix onto one cache is the only thing the field does. Keyed on the instructions now, which is what the shared prefix actually is, and omitted when there are none: one key across requests that share no prefix is worse than no key. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
LiteLLM reaches this backend and owns the credential, so moving the request there looks free. Its Responses transformation filters the body through an allow-list, and two of the fields this provider sends are not in it -- a migration would keep every test green while quietly changing what is sent. The guard reads that allow-list out of the installed LiteLLM and fails when it stops dropping them, with the follow-up action in the failure message. Nothing polls upstream: the signal arrives when the dependency is bumped, and until then the guard is what a reader finds when they wonder why this provider is still here. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The picker offered this provider nothing and `provider test` reported a
signed-in account as having a bad key. Both came from asking sources that
cannot answer for it: the registry default is refused by the backend, the
slugs LiteLLM's table carries are not the ones an account is entitled to,
and the generic probe requests `{api_base}/v1/models`, which this backend
does not serve.
Its own catalogue endpoint answers both questions, so ask that. Entries
marked `visibility: "hide"` are dropped -- reachable, but not meant to be
offered. The answer is cached briefly because the picker rebuilds its list
on every refresh, and a failure is an empty list rather than an error: a
provider list that cannot reach the network is still worth showing.
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Letting LiteLLM own the codex credential routed both driver-owned families through the same reader, and only one of them stores its token there. So a copilot probe read the ChatGPT credential: with neither signed in it named the wrong provider to log into, and with the account authenticator reached at all it would have opened a GitHub device flow underneath `provider test`, printing a code and waiting for someone to paste it. Check the credential this provider stores, then ask the authenticator that owns it. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Signing in from the picker spawns `raven provider login`, resolved through PATH. That names whatever install comes first, which need not be the one running: a second install writes the credential where its own version expects it, so the login reports success and the picker still shows the provider as unauthenticated. Before this the key was dead and the screen only printed the command for the user to run, where their shell picked the same raven they had just launched. Name the entry point in the child environment instead. An explicitly set RAVEN_BIN is left alone, and a session that cannot name its own entry sets nothing rather than an empty value the TUI would read as "fall back to PATH" while looking answered. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`import_litellm` publishes the credential directories so LiteLLM's drivers and raven agree on one location, and those variables outlive the test that triggered the import. A later test that fakes the home directory still reads whatever the first one resolved -- on a developer machine, a real signed-in credential. Providers then report themselves configured inside a sandboxed home, and the codex catalogue lookup goes to the network. Point them at a temp directory for every test. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…osts The guard said this provider exists because LiteLLM drops ``parallel_tool_calls`` and ``text``. Both are the backend's own defaults, so losing them changes nothing -- and the two failures cited alongside them are version-bound: 1.95.0 rebuilds the empty ``response.completed`` output its bridge used to choke on, and a ``responses/`` prefix forces the bridge for a model its table has never heard of. What survives is ``prompt_cache_key``, filtered out by the same allow-list in every version through 1.97.0.dev1. Guard that instead, and say plainly that keeping this file is a decision rather than a necessity. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The all-platforms case falls back to a foreground run, and nothing stubbed it: the step then read the real config for a memory backend and imported into whatever workspace the developer has configured. Without one it exited 1 and the test failed on that instead of on its own assertions, which is why this file had a failure on every machine that had not onboarded. Stub the runner, the same way `_memory_enabled` above it is stubbed and for the same reason. What the test asserts -- that the step does not detach, and says why -- is unchanged. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Checking that a credential file exists only covers having none. A stored refresh token the server has since revoked gets the driver a logged warning and then its own fallback: a device code printed to stdout and fifteen minutes of polling -- underneath an agent request, the `model.options` RPC, or `provider test`, none of which have anybody waiting to type a code. An interrupted attempt is worse: the driver treats its own timestamp as a flow in progress and waits five silent minutes for it. Stub both entry points on the instance so refresh is the only way the call can succeed, and give the login path a way to discard the leftover stamp. The stamp is also no longer mistaken for a credential -- alone in a file it is a provider that looks connected and cannot authenticate. None of this was pinned on this side. The same guard for copilot had a test and this one did not, so deleting the refusal kept every test green. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Asking the driver for a token hands back the stored one when it is still good, so signing in while already signed in did nothing -- after announcing a device flow and opening a browser tab, then reporting success. Somebody switching accounts had no way to tell it had not happened. Say so instead, and name what does work. When there is no credential, drop an unfinished attempt's timestamp first: the driver would otherwise wait for that attempt to land rather than start this one, printing nothing for as long as five minutes with the terminal handed over. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Backing out asked "did the set-up happen?" of the row under the cursor. A successful sign-in moves that provider off the unconfigured list, so the row answers for whoever slid into its index: the picker returned to the add list pointing at a provider the user never touched, and the re-anchor meant for exactly this case never ran. The screen title was already immune -- it reads its target from state -- and this is the other consumer of the same list. The screen also told the user Ctrl+C would end the session, which is what the same commit's `deferSignalExit` exists to prevent. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The probe dispatched on minimax and let everything else fall through to copilot's reader, which is how copilot came to read the ChatGPT credential. Naming each family closes the shape rather than the instance: a fifth one arriving without a check of its own now says so instead of quietly using a neighbour's, and a sweep over the registry fails when that happens. The suite-wide credential isolation had the same gap in the other direction: it named the two families LiteLLM reads by variable and left the two that derive their path from the home directory pointing at the real one. Also drops a docstring's promise to clean up credentials in pre-`~/.raven` locations and to guard against a read fallback picking them back up. Neither exists; both belong to an approach that was ruled out. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Every picker refresh rebuilds the candidate list, so an account that cannot be reached was asked again on each one, paying the full timeout every time. A failure is cached for a shorter window than an answer: being offline is a state that changes, and an account's entitlements are not. Two comments went with it. The RPC's own docstring still promised to touch no network, which this batch had just made false; and the client version carried a claim about what the endpoint does with an unrecognised one, which was never measured -- what a live account answered for the value we send is. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Two answers written an hour apart pointed at each other. A revoked refresh token is still a stored credential, so the request path raised "no longer valid -- run provider login", and provider login read the same file, called it present, and reported "already signed in". The only way out was a command neither message named. Ask whether the credential still works instead of whether it is there; that call refreshes but cannot start a login, so a dead one falls through to the flow the user came for. Signing in now also forgets a catalogue fetched while there was nobody to fetch it for -- otherwise the picker offers this provider no models for the first half-minute after signing in to it. The probe told a missing credential from an unreachable one by catching an exception the catalogue never raises, so both read as the same thing. What is on disk answers it for free, and "sign in" is a different instruction from "the account returned nothing". Also compresses two docstrings in the same file: presence-versus-validity is stated once rather than three times, and the sentinel branch says what a new family must do without narrating what the old one did. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
A review pass over every comment this branch added. Three kinds came out: Wrong -- a docstring promising cleanup of pre-`~/.raven` credentials that neither exists nor was ever going to, a module claiming `model.options` and its neighbour cache touch no network after this branch gave them a lookup and a failure cache, and two claims about how a token is fetched that missed the path where the stored one is simply still good. Redundant -- a field comment retelling the pricing incident that the test docstring and the commit that fixed it already tell, and a factory comment retelling what the field it dispatches on already says. Field comments state the contract; the story lives where it happened. Dead -- two fixtures clearing an environment variable no reader is left for, while the variables that do jump out of a patched home went unnamed. Named them, and dropped the one that does nothing. The rename is the same edit in code: two names for one derivation, the newer one reading as the opposite of what it means. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…resh fails The hint for someone already signed in told them to run `provider remove`, which is not a command -- the subcommand is `reset`. A test pinned the wrong spelling while the message printed it, so the one thing that would have caught it was asserting it instead. The renewal failure said the credential was no longer valid. LiteLLM's refresh wraps every failure in one error type, an unreachable network included, so this side cannot tell those apart and must not claim to: being told a credential is gone sends the user to re-authorise something that never expired. Say both, and let the user pick which one they are in. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Every picker row was built by asking, signed in or not. Nobody signed in has nothing to report, so the request could only fail -- and the failure is cached, which is what left this provider empty for the half-minute after signing in to it. The clearing added for that ran in the `provider login` child process, where it could not reach the cache the gateway holds; gating the question on the credential removes the failure instead of clearing it, and the clearing goes with it. The probe told a reachable-but-empty account from an unreachable one by catching an exception the forgiving reader never raises. It now asks strictly, because the two are not fixed by the same thing: a credential the account no longer honours is fixed by signing in again, and the recovery menu offers that on one branch and only Retry on the other. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
A sign-in that took left the user on the confirmation screen, and Esc from there went to the list -- while the provider had just moved off the list it was selected from. The key screen next door already solves this: on success it points the selection at the provider it just set up and opens its models. OAuth stayed put instead, which was a way of avoiding the index that moved rather than following it. Follow it. A sign-in that leaves no credentials behind, and a login that failed, still report on the screen they happened on. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Two were hard-coded, and both came back "not supported when using Codex with a ChatGPT account": the registry's default and the provider constructor's. Which slugs an account may use is only knowable by asking the account, which is now what the picker and the probe do. So neither place carries one. The wizard asks for a model instead of writing one that cannot answer, and the constructor requires it rather than handing a caller an id that will be refused at request time. The invariant that every curated provider carries a default excludes this one now, with the reason stated where it is excluded and a guard that fails if a static default comes back. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Three things the model step got wrong once its list came from the account, each found by running it: The id went in bare. A route prefix is what LiteLLM needs and codex needs none, so the wizard wrote "gpt-5.6-sol" -- which OpenAI's keywords claim, so the test message went to a provider that does not serve it. The prefix that names a provider is not always the one that goes on the wire, and it lives in the registry now, with the picker reading the same answer. Azure stays absent: it uses the id verbatim as a deployment name in a URL path. Enter tore the wizard down. The prompt fell back to a default on an empty submit and its comment said the no-default branch validated non-empty, which held only while every provider carried a static default. The list is newest-first, so its head is the default -- a better one than a hard-coded id could be. Clearing the prefill and pressing Enter substituted silently. The prompt has already echoed an empty answer and a test message follows, so the model it was sent with appeared nowhere. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
… for Signing in happens in another process, so there is no call the gateway can make to invalidate what it cached. Time alone was the only key, which left two ways to serve the wrong answer: the previous account's models after switching accounts, and an empty list for the rest of the failure window after signing in to a provider that had been signed out. The file the driver writes is the fact both processes share, so the cache belongs to it. A strict fetch also no longer records a failure. It raises for a caller whose job is to report on this moment; the picker asking next should not inherit a verdict from a report it did not ask for. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…eaches The comment said azure was deliberately left out of the providers that get a public prefix. It is left out, but that is not what keeps a prefix off its deployment name: an endpoint provider is locked in as custom and its model is persisted directly, so this function is never asked. Explaining a mechanism that does not run is worse than saying nothing -- the next reader trusts it. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Both fixtures put the unconfigured providers after the sign-in target, so the index into the configured list and the index into the whole response agreed -- and the two are the same number for every case the tests covered. Mutating the line they exist to hold down left all fifteen passing. One unconfigured provider before the target is enough to tell them apart, and the mutation now fails. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…rk fault A token that cannot be produced any more and a catalogue that cannot be reached were reported identically, and the recovery menu branches on that: one status offers signing in again, the other offers Retry. Somebody whose credential had been revoked landed on the screen that could only retry it. The two are already distinguishable by what raised them -- the credential lookup, or the request. Say which. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…ting to it Resetting a provider clears its credential and leaves the model id behind, so the config still names a provider that cannot answer. Neither door that does it said so -- and then the startup check read the result as "not set up" and ran the six-step wizard from the language screen, over a session with other providers ready. Both halves now. The two doors say which model they are about to strand and how to fix it. And the startup check separates the two things it was conflating: a config with nothing usable is a first run and the wizard is the answer, while a default naming a provider that has gone unusable is one wrong line -- so name the line. That decision lives in one place, because both entry points were asking the same question and only one answer can be right. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The add-model screen takes free text and the account's catalogue offers bare slugs, so "gpt-5.6-sol" is what a user types -- and stored bare it resolves to OpenAI, which does not serve it. The listed models already carried the prefix; the typed ones went in verbatim. Same hole, third door. Which providers need their own name in front is the registry's answer now, not a set of names kept in the wizard, and both surfaces spell an id through one function. The guard is a sweep over every provider rather than the three cases this batch happened to think of -- the same shape found the one that got away last time. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
A token the endpoint refuses and a catalogue that cannot be reached were both reported as a network fault, and the recovery menu branches on that: one status offers signing in again, the other offers Retry. Somebody whose credential had been revoked landed on the screen that could only retry it, and the HTTP status the endpoint had returned was dropped on the way. Sorted by what would fix it: the credential lookup raising, or the request coming back 401/403, both mean sign in; anything else means retry. The split is not clean and does not claim to be -- the driver wraps a network failure during refresh in the same error as a revoked token -- so both messages name both causes. Also translates a docstring that had been left in Chinese. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…bout Resetting a provider clears its credential and leaves the model id behind, so the config still names a provider that cannot answer. The notice said so but told everyone to run `provider login`, which exits 1 for anyone who is not one of the four OAuth families -- and a local deployment has no key to give, so the key flag it suggested instead would have done nothing. Each credential kind is set up by its own command and its own field; say that one. The recovery menu for a failed verify also offers Retry now on the branch that takes the failures which cannot be sorted: a credential the account refused and a refresh that could not reach the network arrive as the same thing, and only one of them is fixed by signing in again. And the startup notice says what was found rather than guessing why: the model may resolve to a provider with no credentials, or to one that never served it. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Its docstring promised to stay in sync with the branches of `make_provider`, and the version that compared provider names drifted the moment the factory stopped comparing them. Both ask `spec.client` now, so they stay in sync by construction rather than by a comment. The guard for it was a source scan, which passed while the branch it guarded was dead. Two behaviours replace it: an OAuth provider with no key in config is accepted, and an Azure endpoint missing half its pair is refused. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Each of these passed with the thing it was written to hold down deleted.
`deferSignalExit`'s release-once protection: the test asserted that a function
was a function. Removing the protection left it green. It now emits a signal
with a deferral outstanding and asserts the session survives.
`/setup`: the test read the help text while the argv beside it was the thing
that reached a shell. Changing the argv back to `raven setup`, a command that
has never existed, left four tests passing.
`OpenAICodexProvider`: the test named itself after the model being the caller's
to supply and then only checked the getter echoed it back.
A comment goes with them: the display block `useConfigSync` normalizes is never
served, because the request asks `config.get {key:'full'}` and the handler reads
`keys`. The conditional above it is what makes the mouse-tracking opt-out safe,
not the fetch -- and the mismatch predates this branch.
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
A comment ended in "see the PR", which a reader of the file cannot follow
and which AGENTS.md rules out. The sentence it pointed at was going to say
the `config.get {key:'full'}` / `keys` mismatch is older than this change;
that is context for review, not for the file, so the pointer goes and the
part that constrains the code stays.
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`ensure_ready_to_start` replaced `ensure_configured_or_onboard` at both entry points, and the old function stayed behind with only its own tests as callers. Four guards for the paths that must not reach the gate were patching that name, so `gate_called == []` had become a fact about a function nobody called: retarget them and they fail again when the caller drops the condition they guard. Two comments still described the gate as "wizard when a provider key or default model is missing". It runs the wizard only for a config with nothing usable at all, and answers a stale default model with a notice. The stale-model test parametrized over `tui` and `agent` and then called the gate directly in both, so it never showed either entry point wired to it. Invoke the commands. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Whether `provider login` opens the device page depends on having somewhere to open it. CI runs headless Linux, so the runner was answering these assertions instead of the code: the two expecting a page opened failed there while passing on a developer's machine, and the one expecting no page passed for the wrong reason. Declare a display in the fixture; the two headless cases drop it again. Verified by making every platform take the Linux branch in a throwaway worktree: without the declaration the two failures reproduce, with it the file is green, and opening a tab on the already-signed-in path turns the third assertion red. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Adversarial review (verified against the branch, not just the diff)Method: fetched 1. OAuth credential files lose the 0600 permission the removed stack applied (security regression)
Suggested fix: create 2.
|
Moving these under `~/.raven/oauth` dropped the mode the old stack set. It chmodded its token file to 0600 (`oauth_cli_kit/storage.py:70`); the drivers that replaced it write with a plain `open()`, so on a 022 umask `auth.json`, `access-token` and `api-key.json` landed world-readable, in the same directory where raven's own MiniMax writer keeps 0600. Three parts, because only some of the writers are ours. The directory is created 0700 and tightened if it is not, which holds for a file raven never sees -- including one a family added later writes. A sign-in then restricts what it left behind, once for every family rather than in each handler: a later write from the same driver truncates in place and keeps the mode, so this holds across every refresh after it. And the one rewrite that replaces the file instead of truncating it sets the mode on the replacement, where there is no mode left to keep. Which files a sign-in leaves behind was already answered once, for disconnect, so both callers ask it. That list derived MiniMax's path a second time rather than asking the module that writes it, which is wrong exactly when a user has set `MINIMAX_OAUTH_TOKEN_DIR` -- so it asks, like the other three families already did. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`provider test github-copilot` could not succeed. It fetched a real API key and then fell into the generic probe, which returned "api_base is empty and provider has no default" -- the endpoint is not in the registry, it arrives inside the API key file -- and the key was discarded. Only the bad-credential half of this family's check was reachable, and no test pinned the other half because it could not pass. Asking its own way would not have worked either: the backend refuses a request carrying only an `Authorization` header. Its driver sends a set of editor headers on every call, so a valid seat would have come back refused and been reported as a bad credential. So this family gets its own probe, like Codex, taking both the endpoint and the headers from the driver rather than guessing them. The request and how its answer is reported are shared with the generic path -- one vocabulary, two callers deciding what to ask. Unverified: that GitHub answers 200 to this shape. It needs a live seat, and what is pinned here is the request that goes out. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Ctrl+C during a sign-in showed `exited with code null`. A child killed by a signal reports no exit code, and the screen promises that Ctrl+C cancels and comes back -- so it told the user their own key press was a fault. Cancelling is now what it looks like. The boot banner still opened at $0.00 for a plan-billed session. Every other counter there is genuinely zero before the first turn; this one was reporting a price for a session whose every turn answers "no per-token price". The predicate that decides is now asked wherever a dollar figure is reported rather than only inside the estimate. Also: `reload.mcp`'s docstring justified the handler by a five-second poll this branch deleted, in the module and again in its test file. The handler is still reached by `/reload-mcp`, so what changes is the reason. And a duplicate copy of `test_resetting_an_unrelated_provider_stays_quiet` goes -- identical body, shadowing the first, invisible to ruff because tests ignore F811. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
`test_provider` no longer times anything itself, and deleting the duplicate test left two blank lines at the end of its file. Both from the pre-commit ruff hooks, which see every changed file rather than the lint targets. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
|
Thank you -- all six verified independently, all six real, nothing waved away. Fixed 1. 0600 -- confirmed, and fixed as a class rather than at the three named sitesEvery citation checked out. I pulled the 0.1.6 wheel to read the removed One correction to the attribution: this is a regression for Fixed in three parts, because only some of these writers are ours:
Your suggestion to do it in While in there: that shared list derived MiniMax's path a second time instead of 2. Copilot probe -- confirmed, and it needed more than filling
|
Summary
/modellisted the four OAuth providers and did nothing when Enter was pressed onone. Enter now hands the terminal to
raven provider login <family>and lands onthat provider's model list when the sign-in succeeds. Everything the fix walked
through on the way is here too, because a working key was not enough to reach a
model:
Where a credential lives. Each OAuth family resolved its own path -- Codex
through
oauth_cli_kit, Copilot under~/.config/litellm, MiniMax underplatformdirs -- and the code that wrote one was not always the code that read it,
so a completed sign-in could report itself unauthenticated forever. One function
now answers where a credential lives, under
~/.raven/oauth, and "configured"means the file parses as a credential rather than that a path exists. LiteLLM's
driver owns the Codex flow end to end, which retires the
oauth-cli-kitdependency.
Signing in.
provider loginis now the driver's own flow, which is what makesthe credential it writes the one the request path reads. Two hazards come with
handing the flow over, and both are closed here rather than left for a user to
find: asking LiteLLM for a token falls through to starting a device flow when a
refresh fails, so a
model.optionsRPC or aprovider testcould print a devicecode and poll for fifteen minutes with nobody waiting to type it -- both entry
points are refused on the instance that only reads. And the driver treats its own
unfinished-attempt timestamp as a flow in progress, so an interrupted sign-in would
make the next one wait five silent minutes; the login path discards it. Signing in
while already signed in says so instead of announcing a flow it skips, a revoked
credential falls through to a real sign-in, and the failure text names both reasons
a refresh can fail -- LiteLLM wraps a revoked token and an unreachable network in
one error, so claiming either one would send half the users to fix the wrong thing.
Which Codex models exist. Two ids were hard-coded, in the registry and in the
provider constructor, and a live account refused both with "not supported when
using Codex with a ChatGPT account". Which slugs an account may use is only
knowable by asking it, so the account's own catalogue answers -- cached per
credential, with failures cached too so being offline costs one timeout rather than
one per keystroke, and never asked while signed out.
Probes.
provider testfetched a ChatGPT token for every non-MiniMax OAuthfamily, so Copilot was probed with somebody else's credential. Each family is
dispatched explicitly now -- and one with no check written yet says so, rather than
borrowing the nearest one. Codex and Copilot each get a probe that asks the way
their backend accepts being asked: Codex's account catalogue, and for Copilot the
endpoint out of its own API key file with the editor headers its driver sends,
because the generic
{api_base}/v1/modelsrequest could reach neither -- forCopilot it answered "api_base is empty" without asking, and asking would have had
a valid seat refused. A refused credential reads as a credential problem and an
unreachable host as a network fault.
Not verified: that GitHub answers 200 to the Copilot request. That needs a live
seat; what is pinned by test is the request that goes out.
Cost. A call on a subscription was costed at $0.00, indistinguishable from a
free model. Plan billing is declared in the registry, "no per-token price" is a
distinct answer from zero, the boot banner no longer opens a plan-billed session at
$0.00, and the invented subscription prices are gone.
The wizard and the terminal. The wizard's model step defaults to the newest
model the account offers, and says which one it used when the answer was empty --
Codex has no static default to fall back on any more, and a substitution nobody
mentions is how a test message ends up sent with a model the user did not pick.
Three more: importing LiteLLM printed over the TUI's rendering,
RAVEN_TUI_DISABLE_MOUSEwas turned back on a moment after start, and the TUIspawned whatever
ravencame first onPATHrather than the one that started thesession -- which is how a sign-in writes a credential the running process will
never read.
Type
Verification
The suite was reading the developer's real OAuth credentials before this branch:
import_litellmpublishes the token-directory variables process-wide and theyoutlive the test that triggered the import, so a later test that faked
$HOMEstill resolved a live credential -- which made providers report themselves
configured inside a sandboxed home and sent the Codex catalogue lookup to the
network. On a signed-in machine that was a red suite; it is green now, and the
variables point at a temp directory for every test.
Review on this branch kept turning up tests that passed either way, so the guards
were re-checked by mutation: break the implementation in an isolated worktree, run
the test, confirm it goes red.
docs/orREADME.mdnames a credential location or the wizard's startupcondition, so there was nothing to update
Risk
Five changes are visible to someone who upgrades:
~/.raven/oauthnow, so a signed-in user signs in once more. The old files areleft where they are, untouched.
~/.raven/oauthis created 0700, and an existing one is tightened to it. Auser who widened that directory on purpose will find it narrowed; the files in it
are credentials, and the drivers that write them do not set a mode themselves.
provider loginover a credential that already works reports and exitsinstead of re-running the browser flow.
raven provider reset <name>is the wayto sign in as somebody else, and the command says so.
it did not before: it asks the config which provider serves the model rather than
accepting any configured key as proof. The answer is a notice naming the model
and pointing at
/model-- not the wizard, which would restart at the languagescreen to fix one line. The wizard still runs for a config with nothing usable.
$0.00and are left out of the sessiontotal rather than adding zero to it.
Removed: a five-second
config.get {key: 'mtime'}poll in the TUI.config.getanswers with its four whitelisted keys whatever it is handed, so
mtimewas neverin the response, the poll always took its early return, and the MCP reload it
guarded never ran -- while costing a round trip on the same stdio pipe that carries
prompt.submit./reload-mcpis unaffected.Rollback is a revert of the merge: the credential move is a read-path change, so
reverting reads the old locations again and a user signed in before the upgrade is
signed in after the revert.
Related Issues
Fixes #263, #212, #218, #269, #275, #276, #277, #278