okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
e9df547
main
model authentication
model authentication is supplied by model-provider extensions rather than the daemon configuration loader.
credentials
Every secret albedo keeps lives in $ALBEDO_HOME/creds.json, mode 0600, and only the daemon reads or writes it. It has three sections: accounts holds OAuth accounts (and an optional alibaba key) by store key, one object or an array; providers holds each profile's apiKey by profile name; mcp holds each MCP server's bearerToken, headers and env by server name. All credential and settings mutations share one per-home daemon lock. OAuth refreshes retain their re-read under that lock and preserve other sections. config.json keeps no keys: an apiKey still written there by hand is used and wins over the saved one until the next daemon start moves it.
Account limit marks under accounts are recorded per model-family meter for Google Antigravity (limitedMeters: {"claude": <epoch ms>, ...} with meter keys gemini, claude, gpt-oss, falling back conservatively to legacy limitedUntil when limitedMeters is absent), whereas Codex and Alibaba record account-wide marks in limitedUntil.
At start the daemon moves secrets still kept elsewhere into creds.json: auth.json's accounts, mcp-credentials.json's servers, and each config.json profile's apiKey. It writes creds.json first, then moves the old files into backups/ as <name>-before-creds-<ms> with every mode bit cleared, so an interrupted start loses nothing and the next one finishes. Not even their owner can read the backups without chmod 600 first, so tools and agents running as the user never read them by accident; rm -f deletes them.
Clients save provider profiles and MCP configuration with credentials through the settings API. Reads disclose saved credential presence, never secret values. Provider keys belong to PATCH /settings?group=providers; MCP secrets belong to PATCH /settings?group=mcp. Both use the observed group validator and commit public and private changes together.
GET /server includes credential migration notices for files moved during this daemon's start. Reading a notice does not consume it. Clients dismiss notices through ui.dismissed_notices. Original files remain sealed in backups/.
openai-compatible api keys
The enabled-by-default openai extension requires models. It handles existing named OpenAI-compatible profiles and both Responses and Chat Completions. The Responses stream decoder accepts servers that leave output_index off their events (llama.cpp's llama-server) by placing each event with its item_id, and treats a missing content_index as 0. Model discovery comes from the models.dev cache, not the configured API endpoint. Older config.json profiles without an extension field remain OpenAI profiles; newly saved profiles include "extension": "openai".
chatgpt codex oauth
The enabled-by-default codex extension requires openai, so it reuses the OpenAI Responses encoder, SSE transport, replay format, and model metadata. Its picker shows the models the ChatGPT backend offers the signed-in account (see codex models); API-key OpenAI profiles still show the full OpenAI catalog. /login can add ChatGPT Plus/Pro accounts. A fresh install can enter codex as the provider name; an existing install can choose add chatgpt codex account.
The daemon runs the login (see sign-in api) with the Codex CLI browser flow: PKCE S256, the allowlisted http://localhost:1455/auth/callback, state validation, and a manual callback-url/code input that races the local callback. The token exchange uses OpenAI's public Codex client id and stores access, rotating refresh, expiry, ChatGPT workspace id, seat id when present, and email when present.
Credentials live in creds.json's accounts under the openai-codex key. One account uses the legacy object shape; two or more use an array. A login replaces the same seat/account identity and appends a different account. /login lists saved providers and each stored account; d (or delete) on either row asks to remove it, and enter on an account does the same. Removing the active provider makes the first remaining provider by name active. A Codex provider with no accounts left reads signed out and starts a sign-in when chosen. Access tokens refresh under the credential lock 60 seconds before expiry, with a re-read under the lock so concurrent sessions do not overwrite a newer refresh.
Each session hashes its id into the account pool and stays on that account. If that account is invalid or cannot refresh, selection continues through its siblings. When the Codex backend answers a request with 401 (for example token_revoked), that account is removed from creds.json and the turn fails with an error ending in run /login; the TUI opens /login when that error follows a turn sent from the open chat. Requests use https://chatgpt.com/backend-api/codex/responses with Bearer auth, chatgpt-account-id, Codex experimental Responses headers, and the session request id. Codex request bodies force store: false, automatic parallel tools, low text verbosity, encrypted reasoning replay, and nullable tool strictness.
The oauth callback binds only to 127.0.0.1. Port 1455 is fixed because OpenAI allowlists the exact redirect URI; a busy port fails instead of silently changing redirects.
sign-in api
Browser sign-ins run in the daemon, so every client shares one OAuth implementation. An extension contributes extension.LoginPlugin(oauth.Login(...)): the profile extension it serves, a chooser label and detail, the protocol its profiles use, its creds.json accounts key, its loopback callback (host, port, path, and whether the port is fixed), and three functions. authorize builds the provider url from a Grant (redirect, state, PKCE verifier and challenge). exchange trades the code for the credential object to store and may report progress. account names a stored credential; equal ids are the same account. albedo_oauth owns the rest: the callback listener, the race with a pasted code, a 5-minute timeout, and locked writes. A sign-in replaces the stored account with the same id and appends a new one. One account is stored as an object, two or more as an array.
Clients use these resources with the daemon bearer token:
GET /authreturns advertised providers and forms, saved account descriptors, and incomplete login resources.PUT /auth/logins/<id>creates a login at a client-generated UUIDv7 with{provider, flow, values}. Browser and manual flows use an empty values object. A known identical intent recovers the same resource; another intent conflicts. Creation returns201; recovery returns200.GET /auth/logins/<id>returns retained state, browser URL, progress, completion account, and validator. States arewaiting,exchanging,complete,failed, orcancelled.PATCH /auth/logins/<id>with{response}andIf-Matchdelivers a pasted callback URL,code#state, query string, or code. Callback and manual response compete for one exchange.DELETE /auth/logins/<id>cancels the login and closes its listener.DELETE /auth/accounts/<account-id>removes a saved account. A profile binding prevents removal until that binding changes.
Login identity and intent survive restart. Callback and PKCE material remain private. The daemon admits at most eight unfinished logins and retains terminal decisions for fifteen minutes. Browser launch remains client policy and respects ALBEDO_NO_BROWSER.
Clients list models through GET /models?provider_profile=<profile>. Profile defaults and account selection belong to conditional providers settings patches. account_id: null uses existing account rotation; an explicit ID binds runtime authentication to that account.
claude code oauth
The enabled-by-default claude extension adds Claude Pro/Max accounts through /login using Anthropic’s Claude Code OAuth client. It stores rotating access and refresh tokens in creds.json's accounts under anthropic, refreshed under the credential lock. An account stored without a refresh token (a legacy or hand-edited one) is never refreshed: the request path skips it, and the quota poll keeps it so its auth failure is recorded. The fixed http://localhost:53692/callback loopback redirect must be available; a pasted callback URL also works. Choose a Claude model after signing in; profiles use extension: "claude" and protocol: "chat_completions". The picker shows Opus 5.5, Fable 5.1, Sonnet 5, Opus 5, Opus 4.8, and Haiku 4.5 only when the cached models.dev Anthropic catalog contains them. /reload models refreshes this availability and the models’ limits. New model families still require a picker-policy update.
Requests go to Anthropic’s Messages endpoint with Claude Code’s OAuth beta, client headers, billing attribution block, identity system block, and tool names. The billing marker is text in system[0], not an HTTP header; its CCH attestation hashes the exact outgoing JSON body. Top-level tool-schema combiners are flattened into an object with their property alternatives and common required fields; nested schemas remain intact because Anthropic rejects top-level oneOf/allOf/anyOf. This transformation lives beside the request encoder in harness/extensions/claude/schema.gleam and preserves unrelated schema fields, including definitions and custom metadata. Replay is saved as provider-neutral chat messages so changing models retains conversation and tool history. This is subscription access, not an Anthropic API-key profile. Anthropic can change or restrict this unofficial client behavior at any time.
google antigravity oauth
The enabled-by-default antigravity extension signs in with the Antigravity IDE's Google OAuth client and sends requests to Cloud Code Assist (https://daily-cloudcode-pa.googleapis.com) for Gemini 3, Claude, and GPT-OSS models. /login offers add google antigravity account; a fresh install can also enter antigravity as the provider name. The daemon runs the sign-in through the sign-in api. Its callback is 127.0.0.1:51121/oauth-callback, which moves to an ephemeral port when 51121 is busy. After the token exchange it finds or provisions the account's Cloud Code Assist project: loadCodeAssist, a free-tier eligibility check, and onboardUser when the account has no tier yet, with progress shown in the client. An account that Google wants verified fails with the verification url.
antigravity/errors.gleam decodes error.details from the raw response before display truncation. Both streaming and sign-in use that decoder; never extract JSON fields from a formatted error with a regex. Sign-in uses the Google Accounts fallback only when VALIDATION_REQUIRED has no usable URL.
Credentials live in creds.json's accounts under google-antigravity, in the one-object-or-array shape: {type: "oauth", access, refresh, expires, projectId, email?, selected?}. The daemon tries the selected account first, then the others in order. Access tokens refresh under the credential lock 60 seconds before they expire. A 401 ends the turn with an error ending in run /login and marks that access token expired, so the next turn refreshes it; if the refresh token was revoked too, the account is skipped and reads as signed out.
Antigravity profiles use extension: "antigravity" and must use protocol: "chat_completions". The extension's upstream translates albedo requests into the Cloud Code Assist envelope, sends it through openai_api.exchange (the shared HTTP and SSE runner), and reduces Gemini's stream back into albedo events. Each assistant turn is stored as a Chat Completions message, so history, projection, and other providers read it unchanged. The exact Gemini parts, including thought signatures, travel in reasoning_details as an antigravity.parts entry and are replayed only to the model that produced them. Output from other models replays as plain text and tool calls; Gemini function calls without a signature get Google's documented skip_thought_signature_validator placeholder.
Requests use the antigravity/hub/<version> user agent. The version comes from the Antigravity update manifest, or from ALBEDO_ANTIGRAVITY_VERSION when it is set. Tool schemas are flattened to the subset Cloud Code Assist accepts: references are resolved, unions keep their first non-null member, const and enums become string enums, and dropped validation keywords move into the description.
amazon bedrock
The enabled-by-default bedrock extension serves Claude models through Bedrock's Anthropic-shaped Messages route ({baseUrl}/anthropic/v1/messages), identical on the bedrock-runtime and bedrock-mantle endpoints — baseUrl picks which host. There is no sign-in flow. /login offers add or update an Amazon Bedrock provider, which asks for an optional base url and optional api key and then lists the models.dev amazon-bedrock catalog; a profile can also be written with extension: "bedrock" and a Claude model id. baseUrl is optional: left unset, it defaults to bedrock-runtime in whatever region AWS_REGION, else AWS_DEFAULT_REGION, else the active AWS_PROFILE's own region key names (extension.effective_base_url); set it explicitly for bedrock-mantle or a non-default partition.
Authentication tries, in order: the profile's own apiKey (sent as x-api-key, no signing); AWS_BEARER_TOKEN_BEDROCK; AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN, signed with AWS SigV4 (harness/extensions/bedrock/sigv4.gleam); and finally AWS_PROFILE's own resolution from ~/.aws/config/~/.aws/credentials (AWS_CONFIG_FILE/AWS_SHARED_CREDENTIALS_FILE override the paths) — static keys from the shared credentials file, else static keys from the config file, else the config's credential_process. A credentials-file-only profile needs no config section. Credential sources are not mixed. Nothing is cached: each outbound model step re-reads the files and, when selected, re-runs the helper, so an external credential refresher takes effect within a tool-running turn. SigV4's service name follows each endpoint's own IAM action namespace (bedrock:* on bedrock-runtime, bedrock-mantle:* on bedrock-mantle); only bedrock-runtime has been exercised against a live account.
Only HTTPS AWS bedrock-runtime/bedrock-mantle hosts are accepted; embedded credentials, ports, non-root paths, query strings, and fragments are rejected before authentication. A credential_process is a trusted local executable: quoted arguments are parsed without a shell, and it inherits the daemon's environment. Its output and runtime remain bounded, and failure messages never include its stdout. The usage-feed command runner retains its separate environment allowlist. This is a limited credential chain, not the AWS SDK: native SSO, assume-role chains, and instance/container metadata are not resolved.
Replay keeps assistant text and tool calls only; signed extended-thinking blocks are not carried across turns, so multi-turn thinking context starts fresh each turn. The SSE reducer is claude's own (harness/extensions/claude/stream.gleam): Bedrock's Messages route emits the identical event shape.
google vertex
The enabled-by-default vertex extension serves Gemini models through Vertex AI's own generateContent/streamGenerateContent route — the same request and response body Google documents for the public Gemini API, not Cloud Code Assist's envelope. There is no sign-in flow: /login offers add or update a Google Vertex AI provider, which asks for the project and location and lists the models.dev google-vertex catalog. The extension reads Application Default Credentials from GOOGLE_APPLICATION_CREDENTIALS (default ~/.config/gcloud/application_default_credentials.json) fresh on every call, and the profile's project and location name the project and region, falling back to GOOGLE_VERTEX_PROJECT/GOOGLE_VERTEX_LOCATION when blank. A profile needs extension: "vertex" and a Gemini model id.
The credentials file is either a service-account key (private_key/client_email; the daemon signs its own RS256 JWT and exchanges it at token_uri) or a refresh-token credential (client_id/client_secret/refresh_token) — gcloud auth application-default login's own shape or Workforce Identity Federation's external_account_authorized_user. The latter's STS token endpoint (its token_url field) takes client credentials as HTTP Basic, not body fields; the plain authorized_user shape takes them in the body. There is no token cache: each call re-reads the credentials file and exchanges a fresh access token, so an external refresher that rewrites the file in place just works.
Vertex passes tool schemas as parametersJsonSchema and structured response schemas as responseJsonSchema, without Cloud Code Assist's schema flattening. Supported explicit reasoning efforts map to Gemini 2.5 token budgets or Gemini 3 thinking levels; unknown family/effort combinations fail locally instead of being ignored. Unspecified effort keeps the provider default. The global location uses the bare aiplatform.googleapis.com host for both transport and upstream identity.
Malformed present stream fields fail decoding, and EOF without a terminal finishReason is an incomplete response, not success. Only a normal STOP completion publishes executable tool calls; limited or filtered responses cannot execute partial calls. Output token totals include thinking tokens, with reasoning also reported separately.
Replay keeps text, thinking text, and tool calls only, each replayed call marked with Google's documented placeholder signature (skip_thought_signature_validator) rather than the native one antigravity preserves for its own Gemini calls.
Account ordering
harness/accounts.gleam owns pure pool ordering and the 32-bit FNV-1a session spread. The native account owner captures wall time and process-local affinity before calling it. Selected usable accounts come first, followed by the sticky or hashed order; limited accounts come last in reset order. Equal reset times retain their input order. albedo_accounts.erl keeps credential publication, session affinity, transient limits, and local-time display.
1# model authentication 2 3model authentication is supplied by model-provider extensions rather than the daemon configuration loader. 4 5## credentials 6 7Every secret albedo keeps lives in `$ALBEDO_HOME/creds.json`, mode `0600`, and only the daemon reads or writes it. It has three sections: `accounts` holds OAuth accounts (and an optional `alibaba` key) by store key, one object or an array; `providers` holds each profile's `apiKey` by profile name; `mcp` holds each MCP server's `bearerToken`, `headers` and `env` by server name. All credential and settings mutations share one per-home daemon lock. OAuth refreshes retain their re-read under that lock and preserve other sections. `config.json` keeps no keys: an `apiKey` still written there by hand is used and wins over the saved one until the next daemon start moves it. 8 9Account limit marks under `accounts` are recorded per model-family meter for Google Antigravity (`limitedMeters: {"claude": <epoch ms>, ...}` with meter keys `gemini`, `claude`, `gpt-oss`, falling back conservatively to legacy `limitedUntil` when `limitedMeters` is absent), whereas Codex and Alibaba record account-wide marks in `limitedUntil`. 10 11At start the daemon moves secrets still kept elsewhere into `creds.json`: `auth.json`'s accounts, `mcp-credentials.json`'s servers, and each `config.json` profile's `apiKey`. It writes `creds.json` first, then moves the old files into `backups/` as `<name>-before-creds-<ms>` with every mode bit cleared, so an interrupted start loses nothing and the next one finishes. Not even their owner can read the backups without `chmod 600` first, so tools and agents running as the user never read them by accident; `rm -f` deletes them. 12 13Clients save provider profiles and MCP configuration with credentials through the [settings API](settings.md). Reads disclose saved credential presence, never secret values. Provider keys belong to `PATCH /settings?group=providers`; MCP secrets belong to `PATCH /settings?group=mcp`. Both use the observed group validator and commit public and private changes together. 14 15`GET /server` includes credential migration notices for files moved during this daemon's start. Reading a notice does not consume it. Clients dismiss notices through `ui.dismissed_notices`. Original files remain sealed in `backups/`. 16 17## openai-compatible api keys 18 19The enabled-by-default `openai` extension requires `models`. It handles existing named OpenAI-compatible profiles and both Responses and Chat Completions. The Responses stream decoder accepts servers that leave `output_index` off their events (llama.cpp's llama-server) by placing each event with its `item_id`, and treats a missing `content_index` as 0. Model discovery comes from the models.dev cache, not the configured API endpoint. Older `config.json` profiles without an `extension` field remain OpenAI profiles; newly saved profiles include `"extension": "openai"`. 20 21## chatgpt codex oauth 22 23The enabled-by-default `codex` extension requires `openai`, so it reuses the OpenAI Responses encoder, SSE transport, replay format, and model metadata. Its picker shows the models the ChatGPT backend offers the signed-in account (see [codex models](models.md#codex-models)); API-key OpenAI profiles still show the full OpenAI catalog. `/login` can add ChatGPT Plus/Pro accounts. A fresh install can enter `codex` as the provider name; an existing install can choose **add chatgpt codex account**. 24 25The daemon runs the login (see [sign-in api](#sign-in-api)) with the Codex CLI browser flow: PKCE S256, the allowlisted `http://localhost:1455/auth/callback`, state validation, and a manual callback-url/code input that races the local callback. The token exchange uses OpenAI's public Codex client id and stores access, rotating refresh, expiry, ChatGPT workspace id, seat id when present, and email when present. 26 27Credentials live in `creds.json`'s accounts under the `openai-codex` key. One account uses the legacy object shape; two or more use an array. A login replaces the same seat/account identity and appends a different account. `/login` lists saved providers and each stored account; `d` (or delete) on either row asks to remove it, and enter on an account does the same. Removing the active provider makes the first remaining provider by name active. A Codex provider with no accounts left reads `signed out` and starts a sign-in when chosen. Access tokens refresh under the credential lock 60 seconds before expiry, with a re-read under the lock so concurrent sessions do not overwrite a newer refresh. 28 29Each session hashes its id into the account pool and stays on that account. If that account is invalid or cannot refresh, selection continues through its siblings. When the Codex backend answers a request with 401 (for example `token_revoked`), that account is removed from `creds.json` and the turn fails with an error ending in `run /login`; the TUI opens `/login` when that error follows a turn sent from the open chat. Requests use `https://chatgpt.com/backend-api/codex/responses` with Bearer auth, `chatgpt-account-id`, Codex experimental Responses headers, and the session request id. Codex request bodies force `store: false`, automatic parallel tools, low text verbosity, encrypted reasoning replay, and nullable tool strictness. 30 31The oauth callback binds only to `127.0.0.1`. Port 1455 is fixed because OpenAI allowlists the exact redirect URI; a busy port fails instead of silently changing redirects. 32 33## sign-in api 34 35Browser sign-ins run in the daemon, so every client shares one OAuth implementation. An extension contributes `extension.LoginPlugin(oauth.Login(...))`: the profile extension it serves, a chooser label and detail, the protocol its profiles use, its `creds.json` accounts key, its loopback callback (host, port, path, and whether the port is fixed), and three functions. `authorize` builds the provider url from a `Grant` (redirect, state, PKCE verifier and challenge). `exchange` trades the code for the credential object to store and may report progress. `account` names a stored credential; equal ids are the same account. `albedo_oauth` owns the rest: the callback listener, the race with a pasted code, a 5-minute timeout, and locked writes. A sign-in replaces the stored account with the same id and appends a new one. One account is stored as an object, two or more as an array. 36 37Clients use these resources with the daemon bearer token: 38 39- `GET /auth` returns advertised providers and forms, saved account descriptors, and incomplete login resources. 40- `PUT /auth/logins/<id>` creates a login at a client-generated UUIDv7 with `{provider, flow, values}`. Browser and manual flows use an empty values object. A known identical intent recovers the same resource; another intent conflicts. Creation returns `201`; recovery returns `200`. 41- `GET /auth/logins/<id>` returns retained state, browser URL, progress, completion account, and validator. States are `waiting`, `exchanging`, `complete`, `failed`, or `cancelled`. 42- `PATCH /auth/logins/<id>` with `{response}` and `If-Match` delivers a pasted callback URL, `code#state`, query string, or code. Callback and manual response compete for one exchange. 43- `DELETE /auth/logins/<id>` cancels the login and closes its listener. 44- `DELETE /auth/accounts/<account-id>` removes a saved account. A profile binding prevents removal until that binding changes. 45 46Login identity and intent survive restart. Callback and PKCE material remain private. The daemon admits at most eight unfinished logins and retains terminal decisions for fifteen minutes. Browser launch remains client policy and respects `ALBEDO_NO_BROWSER`. 47 48Clients list models through `GET /models?provider_profile=<profile>`. Profile defaults and account selection belong to conditional providers settings patches. `account_id: null` uses existing account rotation; an explicit ID binds runtime authentication to that account. 49 50## claude code oauth 51 52The enabled-by-default `claude` extension adds Claude Pro/Max accounts through `/login` using Anthropic’s Claude Code OAuth client. It stores rotating access and refresh tokens in `creds.json`'s accounts under `anthropic`, refreshed under the credential lock. An account stored without a refresh token (a legacy or hand-edited one) is never refreshed: the request path skips it, and the quota poll keeps it so its auth failure is recorded. The fixed `http://localhost:53692/callback` loopback redirect must be available; a pasted callback URL also works. Choose a Claude model after signing in; profiles use `extension: "claude"` and `protocol: "chat_completions"`. The picker shows Opus 5.5, Fable 5.1, Sonnet 5, Opus 5, Opus 4.8, and Haiku 4.5 only when the cached models.dev Anthropic catalog contains them. `/reload models` refreshes this availability and the models’ limits. New model families still require a picker-policy update. 53 54Requests go to Anthropic’s Messages endpoint with Claude Code’s OAuth beta, client headers, billing attribution block, identity system block, and tool names. The billing marker is text in `system[0]`, not an HTTP header; its CCH attestation hashes the exact outgoing JSON body. Top-level tool-schema combiners are flattened into an object with their property alternatives and common required fields; nested schemas remain intact because Anthropic rejects top-level `oneOf`/`allOf`/`anyOf`. This transformation lives beside the request encoder in `harness/extensions/claude/schema.gleam` and preserves unrelated schema fields, including definitions and custom metadata. Replay is saved as provider-neutral chat messages so changing models retains conversation and tool history. This is subscription access, not an Anthropic API-key profile. Anthropic can change or restrict this unofficial client behavior at any time. 55 56## google antigravity oauth 57 58The enabled-by-default `antigravity` extension signs in with the Antigravity IDE's Google OAuth client and sends requests to Cloud Code Assist (`https://daily-cloudcode-pa.googleapis.com`) for Gemini 3, Claude, and GPT-OSS models. `/login` offers **add google antigravity account**; a fresh install can also enter `antigravity` as the provider name. The daemon runs the sign-in through the [sign-in api](#sign-in-api). Its callback is `127.0.0.1:51121/oauth-callback`, which moves to an ephemeral port when 51121 is busy. After the token exchange it finds or provisions the account's Cloud Code Assist project: `loadCodeAssist`, a free-tier eligibility check, and `onboardUser` when the account has no tier yet, with progress shown in the client. An account that Google wants verified fails with the verification url. 59 60`antigravity/errors.gleam` decodes `error.details` from the raw response before display truncation. Both streaming and sign-in use that decoder; never extract JSON fields from a formatted error with a regex. Sign-in uses the Google Accounts fallback only when `VALIDATION_REQUIRED` has no usable URL. 61 62Credentials live in `creds.json`'s accounts under `google-antigravity`, in the one-object-or-array shape: `{type: "oauth", access, refresh, expires, projectId, email?, selected?}`. The daemon tries the selected account first, then the others in order. Access tokens refresh under the credential lock 60 seconds before they expire. A 401 ends the turn with an error ending in `run /login` and marks that access token expired, so the next turn refreshes it; if the refresh token was revoked too, the account is skipped and reads as signed out. 63 64Antigravity profiles use `extension: "antigravity"` and must use `protocol: "chat_completions"`. The extension's upstream translates albedo requests into the Cloud Code Assist envelope, sends it through `openai_api.exchange` (the shared HTTP and SSE runner), and reduces Gemini's stream back into albedo events. Each assistant turn is stored as a Chat Completions message, so history, projection, and other providers read it unchanged. The exact Gemini parts, including thought signatures, travel in `reasoning_details` as an `antigravity.parts` entry and are replayed only to the model that produced them. Output from other models replays as plain text and tool calls; Gemini function calls without a signature get Google's documented `skip_thought_signature_validator` placeholder. 65 66Requests use the `antigravity/hub/<version>` user agent. The version comes from the Antigravity update manifest, or from `ALBEDO_ANTIGRAVITY_VERSION` when it is set. Tool schemas are flattened to the subset Cloud Code Assist accepts: references are resolved, unions keep their first non-null member, `const` and enums become string enums, and dropped validation keywords move into the description. 67 68## amazon bedrock 69 70The enabled-by-default `bedrock` extension serves Claude models through Bedrock's Anthropic-shaped Messages route (`{baseUrl}/anthropic/v1/messages`), identical on the `bedrock-runtime` and `bedrock-mantle` endpoints — `baseUrl` picks which host. There is no sign-in flow. `/login` offers **add or update an Amazon Bedrock provider**, which asks for an optional base url and optional api key and then lists the models.dev `amazon-bedrock` catalog; a profile can also be written with `extension: "bedrock"` and a Claude model id. `baseUrl` is optional: left unset, it defaults to `bedrock-runtime` in whatever region `AWS_REGION`, else `AWS_DEFAULT_REGION`, else the active `AWS_PROFILE`'s own `region` key names (`extension.effective_base_url`); set it explicitly for `bedrock-mantle` or a non-default partition. 71 72Authentication tries, in order: the profile's own `apiKey` (sent as `x-api-key`, no signing); `AWS_BEARER_TOKEN_BEDROCK`; `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN`, signed with AWS SigV4 (`harness/extensions/bedrock/sigv4.gleam`); and finally `AWS_PROFILE`'s own resolution from `~/.aws/config`/`~/.aws/credentials` (`AWS_CONFIG_FILE`/`AWS_SHARED_CREDENTIALS_FILE` override the paths) — static keys from the shared credentials file, else static keys from the config file, else the config's `credential_process`. A credentials-file-only profile needs no config section. Credential sources are not mixed. Nothing is cached: each outbound model step re-reads the files and, when selected, re-runs the helper, so an external credential refresher takes effect within a tool-running turn. SigV4's service name follows each endpoint's own IAM action namespace (`bedrock:*` on bedrock-runtime, `bedrock-mantle:*` on bedrock-mantle); only bedrock-runtime has been exercised against a live account. 73 74Only HTTPS AWS `bedrock-runtime`/`bedrock-mantle` hosts are accepted; embedded credentials, ports, non-root paths, query strings, and fragments are rejected before authentication. A `credential_process` is a trusted local executable: quoted arguments are parsed without a shell, and it inherits the daemon's environment. Its output and runtime remain bounded, and failure messages never include its stdout. The usage-feed command runner retains its separate environment allowlist. This is a limited credential chain, not the AWS SDK: native SSO, assume-role chains, and instance/container metadata are not resolved. 75 76Replay keeps assistant text and tool calls only; signed extended-thinking blocks are not carried across turns, so multi-turn thinking context starts fresh each turn. The SSE reducer is `claude`'s own (`harness/extensions/claude/stream.gleam`): Bedrock's Messages route emits the identical event shape. 77 78## google vertex 79 80The enabled-by-default `vertex` extension serves Gemini models through Vertex AI's own `generateContent`/`streamGenerateContent` route — the same request and response body Google documents for the public Gemini API, not Cloud Code Assist's envelope. There is no sign-in flow: `/login` offers **add or update a Google Vertex AI provider**, which asks for the project and location and lists the models.dev `google-vertex` catalog. The extension reads Application Default Credentials from `GOOGLE_APPLICATION_CREDENTIALS` (default `~/.config/gcloud/application_default_credentials.json`) fresh on every call, and the profile's `project` and `location` name the project and region, falling back to `GOOGLE_VERTEX_PROJECT`/`GOOGLE_VERTEX_LOCATION` when blank. A profile needs `extension: "vertex"` and a Gemini model id. 81 82The credentials file is either a service-account key (`private_key`/`client_email`; the daemon signs its own RS256 JWT and exchanges it at `token_uri`) or a refresh-token credential (`client_id`/`client_secret`/`refresh_token`) — `gcloud auth application-default login`'s own shape or Workforce Identity Federation's `external_account_authorized_user`. The latter's STS token endpoint (its `token_url` field) takes client credentials as HTTP Basic, not body fields; the plain `authorized_user` shape takes them in the body. There is no token cache: each call re-reads the credentials file and exchanges a fresh access token, so an external refresher that rewrites the file in place just works. 83 84Vertex passes tool schemas as `parametersJsonSchema` and structured response schemas as `responseJsonSchema`, without Cloud Code Assist's schema flattening. Supported explicit reasoning efforts map to Gemini 2.5 token budgets or Gemini 3 thinking levels; unknown family/effort combinations fail locally instead of being ignored. Unspecified effort keeps the provider default. The `global` location uses the bare `aiplatform.googleapis.com` host for both transport and upstream identity. 85 86Malformed present stream fields fail decoding, and EOF without a terminal `finishReason` is an incomplete response, not success. Only a normal `STOP` completion publishes executable tool calls; limited or filtered responses cannot execute partial calls. Output token totals include thinking tokens, with reasoning also reported separately. 87 88Replay keeps text, thinking text, and tool calls only, each replayed call marked with Google's documented placeholder signature (`skip_thought_signature_validator`) rather than the native one `antigravity` preserves for its own Gemini calls. 89 90## Account ordering 91 92`harness/accounts.gleam` owns pure pool ordering and the 32-bit FNV-1a session spread. The native account owner captures wall time and process-local affinity before calling it. Selected usable accounts come first, followed by the sticky or hashed order; limited accounts come last in reset order. Equal reset times retain their input order. `albedo_accounts.erl` keeps credential publication, session affinity, transient limits, and local-time display.