An account is a login that Condensate uses to reach a model provider. This page covers adding accounts, token refresh, health and failover, the completions endpoint and the credential proxy.
The model writes the answer. The engine is the program that runs the session. The account is the login that is allowed to use the provider. You pick the model, Condensate picks the engine from the model, and the account comes from a pool you manage.
Engines and account types
Condensate has three engines. Each one runs a session in its own way and needs a different kind of account.
| Engine | What runs the session | Account type | Models |
|---|---|---|---|
claude | Claude Code, through the Claude Agent SDK | Claude subscription login | Claude models |
codex | Codex CLI, pinned to a tested release | Codex subscription login | OpenAI models |
| Condensate's own agent (shown as "API") | Condensate's agent, calling provider APIs directly | API key accounts (opencode, cerebras) for open and hosted models, and a Codex login for OpenAI models | Open and hosted models, plus OpenAI models (the default engine for OpenAI ids) |
Subscription accounts sign in with a browser flow. API key accounts store a key once and never show it again. Condensate keeps the key in a file outside the account registry and returns only a short suffix, so you can tell keys apart.
Add an account
Accounts are added through the application, through the runtime's HTTP routes, or through the SDK. The sign-in steps depend on the account type.
Claude accounts (paste code). Start a login, open the URL it returns, sign in, and paste the code shown back into Condensate. The code can be given as code#state or as the bare code. Condensate checks the state and stores the result.
Codex accounts (device code). Start a login and Condensate returns a short user code and a verification page. Enter the code on that page. The runtime polls in the background until the login completes, so you do not paste anything.
API key accounts. Create an OpenCode Go account with its key in one request (POST /accounts/opencode). The key must be at least 8 characters. Condensate stores it and from then on reports only its last 4 characters.
Each login is single use. It expires 10 minutes after it starts, and the runtime keeps at most 64 logins pending at once. A login that expires or fails can be started again under the same name.
Account names are 1 to 32 characters: letters, digits, dashes, and underscores, starting with a letter or digit. A name that is a prefix or extension of an existing account's name for the same engine is refused (placeholder-clash).
Where accounts live
Account records live in a registry file at ~/.config/condensate/accounts.json. Set CONDENSATE_ACCOUNTS_FILE to move it. Each subscription account keeps its own config directory, so logins never share state. API key accounts point at their key file instead.
If the registry file is missing, unreadable, or empty, the runtime falls back to an account named default for Claude and one for Codex, backed by the standard directories ~/.claude and ~/.codex.
Records hold account names, engines, health, and file locations. They never hold token values.
Token refresh
Subscription logins use refresh tokens that work once. The broker is the only process that refreshes them. It refreshes a token within 60 seconds of expiry, so a session is not caught with a dead token mid-turn. Concurrent requests for the same account share one refresh, and a lock across processes stops two runtimes from refreshing the same login at once. Each new token is written with a compare-and-swap, so a stale refresh cannot overwrite a newer one.
Sessions never see refresh tokens. A session asks the runtime for a short-lived access token, and the runtime returns one of those and nothing more.
Health and failover
Each account has one health state. The runtime reads these states when it chooses an account for a turn.
| State | Meaning | Recovery |
|---|---|---|
healthy | Ready to use | None needed |
needs-login | Login is missing or was revoked | Sign in again. A changed credential store clears the mark. A login with no credential file, such as a macOS keychain login, gets one probe turn after 5 minutes |
exhausted | Quota is used up until a known time | Waits until exhaustedUntil. A Codex account can spend a reset credit (POST /accounts/:name/reset-credits/consume) |
org-disabled | The provider turned the account off | Fix it with the provider |
workspace-owner-usage-limit | The provider workspace owner has hit a usage limit | Raise the limit with the provider, then re-enable the account (POST /accounts/:name/reenable) |
workspace-member-usage-limit | This member has hit a usage limit | Fix it with the provider, then re-enable the account |
The three block states (org-disabled and the two usage limits) have no timer. They stay until you re-enable the account.
Quota is also tracked per model family. An account out of Fable quota can still run Opus.
Failover depends on why a turn failed. Quota and authentication failures can move work to another account or model. Other failures stay where they are, because moving them would hide the real problem. Those include a model that does not exist, a model the engine does not support, an unavailable engine, a closed or interrupted session, tool errors, permission errors, and stalls.
Account failover follows one rule. If no tool has run yet, the runtime replays the turn on the next account in the workspace's list. If a tool has already run, the turn is not replayed, because a repeated write could happen twice. The session moves to the next account for its next turn instead. The HTTP completions endpoint moves accounts only before it has streamed any output.
Model failover uses an ordered chain set by CONDENSATE_AGENTS_FALLBACK_CHAIN, written as engine:model entries separated by commas (for example pi:gpt-6.1-sol, where pi is the engine id). Authentication failures use the same chain unless CONDENSATE_ROUTING_FAILOVER_AUTH sets a separate one in the same format. Both chains can also live in the routing document under failover.quota and failover.auth. A caller can pass its own chain, and an empty chain turns failover off for that session. When no chain is named, the runtime builds one from the models the session may run. It leaves out Sonnet ids, and for a workspace model list it tries Opus 5.5 first, then Fable, then other engines' models in the listed order. When a model refuses a request, the runtime remembers the refusal for an hour by default (CONDENSATE_MODEL_REFUSAL_TTL_MS). Clear the remembered refusals with POST /accounts/model-refusals/clear.
Pacing Claude accounts
Pacing applies to Claude accounts only. It spreads work across the pool so one account does not run dry early. The runtime reads each account's 5-hour and 7-day usage windows and prefers the account with the most headroom for the time left in each window. It needs a fresh usage reading to pace. Without one, it uses the workspace's account order.
Pacing is enforced by default. Set CONDENSATE_ACCOUNT_PACER=shadow to log its choices without acting on them.
Workspace accounts and refusals
A workspace lists the accounts it may use in ~/.condensate/workspaces.json, set by CONDENSATE_WORKSPACES_FILE. The order of the list is the order of preference. See Workspaces for the file format.
A spawn that names an account outside the workspace's list is refused with HTTP 400 and the code account-not-in-roster. The response includes the list, so the caller can pick a valid account. When every listed account for the engine is spent, or the list holds none, the runtime returns NoSpendableAccount. It never falls back to an account outside the list. If policy.failover names an engine and model, the turn moves there and spends the workspace's accounts for that engine.
Account use fails closed. A session never spends an account that belongs to another workspace, and a session with no workspace cannot name an account that a workspace claims. If the workspace lists no usable account for the engine, the spawn is refused instead of borrowing one.
Workspace policy can also set model rules (policy.models), a Fable pool (policy.pools.fable), and opt-in engine failover (policy.failover). Engine failover is off unless the policy turns it on. Git commits can be attributed to the app (gitIdentity: "app") or to the account (gitIdentity: "account"), and a githubUser login can be set for GitHub actions.
Completions endpoint
The runtime serves an OpenAI-compatible completions API on port 4741 (CONDENSATE_AGENTS_PORT changes it). Two routes are available:
POST /v1/chat/completionssends a request through the workspace's accounts.GET /v1/modelslists the models available through the endpoint. It omits deprecated ids and shows the engine and provider for each model.
Three headers control how a request is handled:
| Header | Purpose | Default |
|---|---|---|
x-condensate-workspace | Which workspace's accounts to use. Required unless a default is set | CONDENSATE_COMPLETIONS_DEFAULT_WORKSPACE. With neither set, the call is refused with 400 workspace_required |
x-condensate-session | Groups calls into one session. Up to 128 characters | None |
x-condensate-traffic | work for real calls. smoke for tests and sweeps: a model refusal is reported but not recorded on the login | work |
Each call writes one receipt. The receipt records token counts and the account and model names from the registry. It never records keys or tokens. When the provider rate-limits a call, the endpoint returns 429 with code rate_limited, plus a Retry-After header when the provider said how long to wait. When every account for the model is out of quota, it returns 429 with code quota_exhausted. Other failures use their own codes: 404 model_not_found, 409 account_access_blocked, 503 account_needs_login, and 502 upstream_failed.
Credential proxy
The credential proxy is on by default. It keeps real credentials out of the sessions: a session holds placeholder values, and an outbound proxy swaps in the real credential for each provider call.
With nothing set, the runtime uses these defaults:
CONDENSATE_PROXY_URL, the proxy address:http://127.0.0.1:8080CONDENSATE_PROXY_CA, the CA file the session trusts:~/.config/condensate/iron-proxy/ca.crtCONDENSATE_PROXY_MANAGEMENT_URL, the address of the proxy's management API:http://127.0.0.1:9092CONDENSATE_PROXY_MANAGEMENT_KEY_FILE, a file holding the management key:~/.config/condensate/iron-proxy/management.env
Set a variable to name another value. The runtime refuses to boot if a proxy setting is empty, if the proxy does not answer, or if the CA file or the key file cannot be read. The error names what is missing. It never falls back to real credentials. With the proxy on, a session that still holds a real credential is refused with SeatProxyRefused. To run without the proxy, set CONDENSATE_PROXY=off; sessions then hold real credentials.
Proxy secrets are kept in CONDENSATE_PROXY_SECRETS_DIR, which defaults to ~/.config/condensate/iron-proxy/secrets. Each file is named proxy-<engine>-<account> and has mode 0600. A Claude session holds proxy-claude-<account>. A Codex session holds a placeholder auth.json. GitHub uses placeholders named proxy-gh-token, proxy-gh-app-token, and proxy-gh-user-<login>. The runtime writes each secret file itself and then calls the proxy's management API (POST /v1/reload) so the proxy picks it up. Every 30 seconds it asks the broker for each account's current token and pushes any change.
See Harnesses for how sessions use these credentials.
Check a real turn
Reading an account record or starting a server confirms configuration, not model access. Verify the selected account with a small real turn and inspect its result. That check uses provider quota.
Route a model request
The engine's ModelRouter resolves configured profiles into engine, model, effort, thinking, and failover settings. Overrides come first, then the profile, then the routing document. The routing document is set by CONDENSATE_ROUTING_FILE and is schema-validated. The defaults come from CONDENSATE_AGENTS_DEFAULT_ENGINE, CONDENSATE_AGENTS_DEFAULT_MODEL, and CONDENSATE_AGENTS_THINKING_TOKENS.
Claude-only model ids, such as fable, opus, sonnet, and claude-*, run only on claude accounts. OpenAI model ids run on the API engine or on codex. A model and engine that do not match are rejected before the session is built.
Failover is conditional on the classified failure and the configured policy. Do not assume every error will switch models, and do not assume that changing the default account changes a session that has already started. See Runtime for how sessions bind to accounts.
Diagnose account problems
| Symptom | Check |
|---|---|
| Account listed, but the provider refuses | Login state (needs-login) and the provider's response for that account |
Account refused with account-not-in-roster | The workspace's account list in ~/.condensate/workspaces.json |
| Every listed account is spent | Health states and reset times. The runtime returns NoSpendableAccount |
| Model rejected before a turn starts | Engine and model compatibility, and workspace model policy |
| Another account becomes default, old task unchanged | The existing session's binding. Sessions keep the account they started with |
| Task changes models after a failure | The failure class and the configured fallback chain |
| Turn did not replay after an error | A tool had already run, so the session moved to the next account for its next turn |
Completion returns 429 | Pool capacity and Retry-After on the completions endpoint |
Inspect the available models
The runtime exposes its completion model catalog:
curl -fsS http://127.0.0.1:4741/v1/models
The returned catalog describes this runtime's completion surface. Access still depends on the configured account. Use the application's model picker and inspect an actual turn when configuring durable agent tasks.
SDK access
The SDK's Accounts service lists accounts, issues tokens, creates Claude, Codex, and API key accounts, and runs the sign-in steps. It is in early access and is not published to npm. See SDK for setup.
For the HTTP routes behind these actions, see HTTP reference. For the apps that draw on these accounts, see dex and dojo.