condensate

Build and connect

Accounts and model routing

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

On this page

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.

What a model request needs
AccountCredentials for provider access
ModelThe selected provider model
EngineThe compatible session loop
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.

EngineWhat runs the sessionAccount typeModels
claudeClaude Code, through the Claude Agent SDKClaude subscription loginClaude models
codexCodex CLI, pinned to a tested releaseCodex subscription loginOpenAI models
Condensate's own agent (shown as "API")Condensate's agent, calling provider APIs directlyAPI key accounts (opencode, cerebras) for open and hosted models, and a Codex login for OpenAI modelsOpen 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.

StateMeaningRecovery
healthyReady to useNone needed
needs-loginLogin is missing or was revokedSign 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
exhaustedQuota is used up until a known timeWaits until exhaustedUntil. A Codex account can spend a reset credit (POST /accounts/:name/reset-credits/consume)
org-disabledThe provider turned the account offFix it with the provider
workspace-owner-usage-limitThe provider workspace owner has hit a usage limitRaise the limit with the provider, then re-enable the account (POST /accounts/:name/reenable)
workspace-member-usage-limitThis member has hit a usage limitFix 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/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:

HeaderPurposeDefault
x-condensate-workspaceWhich workspace's accounts to use. Required unless a default is setCONDENSATE_COMPLETIONS_DEFAULT_WORKSPACE. With neither set, the call is refused with 400 workspace_required
x-condensate-sessionGroups calls into one session. Up to 128 charactersNone
x-condensate-trafficwork for real calls. smoke for tests and sweeps: a model refusal is reported but not recorded on the loginwork

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 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

SymptomCheck
Account listed, but the provider refusesLogin state (needs-login) and the provider's response for that account
Account refused with account-not-in-rosterThe workspace's account list in ~/.condensate/workspaces.json
Every listed account is spentHealth states and reset times. The runtime returns NoSpendableAccount
Model rejected before a turn startsEngine and model compatibility, and workspace model policy
Another account becomes default, old task unchangedThe existing session's binding. Sessions keep the account they started with
Task changes models after a failureThe failure class and the configured fallback chain
Turn did not replay after an errorA tool had already run, so the session moved to the next account for its next turn
Completion returns 429Pool 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.