# Accounts and model routing (https://condensate.dev/guides/accounts/)

> Add model logins, and how refresh, failover and the credential proxy work.

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.

[Diagram: What a model request needs. These are separate configuration choices that must agree. The arrows show a configuration checklist, not a credential transmission path.]

## 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](https://condensate.dev/guides/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/completions` sends a request through the workspace's accounts.
- `GET /v1/models` lists 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:8080`
- `CONDENSATE_PROXY_CA`, the CA file the session trusts: `~/.config/condensate/iron-proxy/ca.crt`
- `CONDENSATE_PROXY_MANAGEMENT_URL`, the address of the proxy's management API: `http://127.0.0.1:9092`
- `CONDENSATE_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](https://condensate.dev/concepts/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](https://condensate.dev/concepts/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:

```sh
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](https://condensate.dev/guides/sdk/) for setup.

For the HTTP routes behind these actions, see [HTTP reference](https://condensate.dev/reference/http/). For the apps that draw on these accounts, see [dex](https://condensate.dev/apps/dex/) and [dojo](https://condensate.dev/apps/dojo/).
