The model-provider credentials, one per provider, each pi's auth.json
entry as JSON in the credential store (Rho.CredentialStore) under
"auth:<provider>":
- a subscription sign-in (omp's
/login anthropicand/login openai-codex,Rho.Auth.Login):%{"type" => "oauth", "access", "refresh", "expires" (ms since the epoch), "accountId", "email"}, for:anthropic(Claude Pro/Max) and:openai_codex(ChatGPT Plus/Pro) - an API key (
put_api_key/2):%{"type" => "api_key", "key"}, for:anthropic,:openaiand:google(Gemini)
A provider holds one or the other: storing one replaces the other.
credential/1 hands out what a model call needs: the key, or the access
token, refreshing it first when it is within 5 minutes of expiry (or
empty: a login moved over from another device may carry only the
refresh token). Refreshes run in this process, one per provider
at a time: concurrent callers wait for the same refresh. A refresh spends
the old refresh token, so its result is saved before anyone gets the new
access token; if saving fails, the new credentials stay in this process
(retried every few seconds and on every call, never refreshed again from
the spent token) and callers get {:error, {:store_failed, reason}} until
they are saved. A put/2 or delete/1 takes effect only once written;
then it wins over a refresh in flight. Subscribers (subscribe/0) get
{:rho_auth, :changed} on every put/2, delete/1 and failed
refresh or save.
Reads (get/1, status/0, a key or a fresh token from credential/1)
don't go through the process. Secrets are never logged. Options (tests):
:respond answers the token endpoint instead of the network
(Rho.Auth.OAuthFlow), :save_retry_ms (5 s).
Summary
Functions
Returns a specification to start this module under a supervisor.
What a model call needs: %{type: :api_key, key}, or for a sign-in a
usable access token (and the ChatGPT account id for OpenAI Codex),
refreshed first when it's within 5 minutes of expiry.
{:error, :signed_out} when there is no credential;
{:error, {:refresh_failed, message}} when the
refresh didn't work (a dead grant needs signing in again);
{:error, {:store_failed, reason}} while a refreshed sign-in couldn't be
saved yet (it is retried; the old refresh token is spent, so it is never
used again).
Signs out of provider; an error means the sign-in is still stored.
What a store error means for the user.
provider's stored credentials as they are (no refresh: use
credential/1 for a model call); :error when there are none or the
store can't be read.
How provider can be signed in to: :oauth (a subscription) and/or :api_key.
The provider's name for the user, e.g. "Claude (Anthropic)".
The provider whose credential a req_llm model spec needs:
"anthropic:…", "openai_codex:…", "openai:…", "google:…".
Every provider rho can sign in to, in display order.
Stores provider's credentials (the shapes in the moduledoc), replacing
what it had. {:error, :invalid_credentials} when creds doesn't fit
the provider, {:error, :unknown_provider} for a provider rho doesn't
know, {:error, reason} when the store failed.
Stores an API key for provider (:anthropic, :openai or :google),
replacing its sign-in if it had one. Surrounding whitespace is dropped.
Does provider have stored credentials (get/1)?
Started by Rho.Supervisor (its :auth option holds opts, see the moduledoc).
Per provider: whether it has a credential, which kind, and for a sign-in its email and expiry.
Sends the caller {:rho_auth, :changed} after every change, until it exits.
Types
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec credential(provider()) :: {:ok, credential()} | {:error, :signed_out | {:refresh_failed, String.t()} | {:store_failed, term()}}
What a model call needs: %{type: :api_key, key}, or for a sign-in a
usable access token (and the ChatGPT account id for OpenAI Codex),
refreshed first when it's within 5 minutes of expiry.
{:error, :signed_out} when there is no credential;
{:error, {:refresh_failed, message}} when the
refresh didn't work (a dead grant needs signing in again);
{:error, {:store_failed, reason}} while a refreshed sign-in couldn't be
saved yet (it is retried; the old refresh token is spent, so it is never
used again).
Signs out of provider; an error means the sign-in is still stored.
What a store error means for the user.
provider's stored credentials as they are (no refresh: use
credential/1 for a model call); :error when there are none or the
store can't be read.
@spec kinds(provider()) :: [:oauth | :api_key]
How provider can be signed in to: :oauth (a subscription) and/or :api_key.
The provider's name for the user, e.g. "Claude (Anthropic)".
The provider whose credential a req_llm model spec needs:
"anthropic:…", "openai_codex:…", "openai:…", "google:…".
@spec providers() :: [provider()]
Every provider rho can sign in to, in display order.
Stores provider's credentials (the shapes in the moduledoc), replacing
what it had. {:error, :invalid_credentials} when creds doesn't fit
the provider, {:error, :unknown_provider} for a provider rho doesn't
know, {:error, reason} when the store failed.
Stores an API key for provider (:anthropic, :openai or :google),
replacing its sign-in if it had one. Surrounding whitespace is dropped.
Does provider have stored credentials (get/1)?
@spec start_link(keyword()) :: GenServer.on_start()
Started by Rho.Supervisor (its :auth option holds opts, see the moduledoc).
@spec status() :: %{ required(provider()) => %{ signed_in: boolean(), type: :oauth | :api_key | nil, email: String.t() | nil, expires: integer() | nil } }
Per provider: whether it has a credential, which kind, and for a sign-in its email and expiry.
@spec subscribe() :: :ok
Sends the caller {:rho_auth, :changed} after every change, until it exits.