models catalog #
Persisted preferences are owned by the daemon and changed through the settings API. The CLI refreshes them on use and never writes settings files.
The models extension keeps a local copy of the models.dev catalog and answers one question for the rest of Albedo: what is actually known about this model and its provider. It never guesses a limit from a model name.
It is enabled by default and contributes no model tools and no prompt context.
cache #
The catalog is cached at $ALBEDO_HOME/models.json (default ~/.albedo/models.json). Albedo refreshes it when it is missing or older than the refresh window, in the background, and writes it by rename so a partial download is never readable. The warm runs in its own process, spawned by the models initialiser when the runtime starts and never waited on: its settings read may queue 20s on the home settings lock, which the runtime's start ack must not sit behind. A crash in it is not an install failure, so it does not quarantine the extension. Every lookup, list, and /reload models refreshes the cache again. A failed fetch keeps the previous cache. The cached JSON keeps only the fields used by lookups and lists. Fetching, revision caching, and the persisted reduced index stay in harness/extensions/models/albedo_models.erl.
{ "models": { "url": "https://models.dev/api.json", "refreshHours": 24 } }
Put this in $ALBEDO_HOME/extensions.json. refreshHours: 0 disables automatic fetching and uses only the cached file, which is what the test suite and air-gapped hosts do. Only https, or http on the loopback host, is fetched.
Run /reload models to fetch the configured catalog immediately, regardless of its age or refreshHours. The command returns only after the cache has been atomically replaced, so the next /model picker reads the new model list. A failed fetch leaves the previous cache in place and reports the failure.
lookup #
A lookup takes the session's model id and its configured provider base url. The endpoint decides between providers that publish the same model id. The ChatGPT endpoint uses the OpenAI provider identity. Without a host match, the smallest positive context and output limits win, and modalities and efforts are intersected across candidates that report them. Empty capability lists mean unknown and do not constrain the intersection. A successful lookup preserves an empty effort list, including an empty intersection. Reasoning efforts are inferred from the name only when a valid catalog does not contain the model, never when the catalog is unavailable or malformed. The answer carries no guessed provider, API endpoint, or environment. Exact provider lookups never borrow another provider's facts. These selection rules live in harness/extensions/models/catalog.gleam, which returns typed model facts directly to its callers. Both the catalog key and its trailing segment resolve, so vendor/model and model find the same entry.
An answer carries the context window, the maximum output, the input modalities, the provider id, its documented API endpoint, and its expected environment variable names. Everything is reported with its source: the cached file and how the model was matched.
fast mode #
/fast is a user-only, no-argument, per-session, default-off billing-sensitive
toggle. The UI indicator shows the saved setting; the command opens no picker or
status page and has no model binding. The provider extension owns capability
discovery and wire encoding; Albedo does not infer support from model names or
reasoning-effort metadata. The HTTP read reports support as supported,
unsupported, or unknown. Enabling is allowed only when the active provider
explicitly reports support; disabling is
always allowed. Switching model/provider clears it, subagents start off, and
human-created forks preserve it. Supported modes and their sources vary by
provider; mode names the provider's own advertised option, not a universal
OpenAI service tier. Adapters without a fast-mode implementation report
unsupported; an implemented capability check that cannot verify account/model
support reports unknown.
Provider mappings #
The additional native mappings below were checked against official documentation
on 2026-10-30 and tested offline, not against live provider accounts. Their
supported result means documented request/model compatibility, not proof of
account entitlement, preview enrollment, available capacity, or the actual tier
served. These providers offer no documented equivalent of Codex's account-specific
Fast tier advertisement. No paid inference is used as a capability probe. The
provider validates access on the real request; its refusal is surfaced, not
retried by Albedo as an ordinary-speed request. Some providers can themselves
serve standard processing at quota/capacity limits, so the indicator remains
saved intent, not a speed or billing guarantee.
| Adapter | Native fast control and safety boundary | Official source |
|---|---|---|
| Codex | Exact advertised Fast tier ID in service_tier, checked for the selected account and every rotated account; unchanged by the static mappings below. |
Codex models |
| OpenAI | service_tier: "fast", on Responses and Chat Completions, only at https://api.openai.com/v1 (one trailing slash allowed) and exact published Fast model IDs. Generic compatible endpoints, including Alibaba, do not inherit it. |
Fast mode, Fast models/pricing |
| Claude | Messages speed: "fast", Console API-key profiles only. Opus 5.5 is GA for eligible Console tiers; Opus 5 and Opus 4.8 require preview access and fast-mode-2026-02-01. Opus 4.6 is deliberately excluded because it can accept Fast but serve standard speed. Claude Code OAuth eligibility is unverified and not enabled. |
Fast mode |
| Perplexity | Agent API service_tier: "priority", only its own explicitly listed priority models; not inferred passthrough from an upstream trainer. Unsupported models can silently ignore this field upstream, so the adapter rejects them locally. |
Agent models and service tiers |
| Vertex | Priority PayGo headers X-Vertex-AI-LLM-Shared-Request-Type: priority and X-Vertex-AI-LLM-Request-Type: shared; exact documented model/location pairs only. This chooses Priority-only shared capacity, not purchased Provisioned Throughput. Ordinary regional locations are not implicitly eligible. |
Priority PayGo |
| Bedrock | Converse preview performanceConfig.latency: "optimized", for the US system cross-region profiles and source regions below. Not Anthropic speed, not a generic Priority service tier, and not Messages/Mantle. |
Latency-optimized inference |
Bedrock's exact implemented profile/region pairs are:
us.anthropic.claude-3-5-haiku-20241022-v1:0:us-east-2,us-west-2.us.meta.llama3-1-70b-instruct-v1:0:us-east-2,us-west-2.us.meta.llama3-1-405b-instruct-v1:0:us-east-2. AWS documents an 11K total input/output-token ceiling; Albedo refuses an output cap already above that, while AWS determines the combined total and may serve standard latency.us.amazon.nova-pro-v1:0:us-east-1,us-east-2. The AWS page's prose and table disagree about Oregon; this mapping uses the conservative table.
Opaque/application-profile ARNs and unlisted IDs fail closed. Bedrock's general Priority tier is a different product: the reviewed Claude model cards explicitly mark it unsupported, despite their shared introductory description of Priority.
Exact provider allowlists live in each adapter's fast.gleam; they must be
updated from those sources, not extended by model-name prefixes. Stream attempts
recheck their actual model, endpoint/location, and credential kind before adding
the control. Ordinary requests omit the new fields/headers, and background/search
callers keep their existing fast-off defaults. No fast mapping was established
in the reviewed official docs for Antigravity, OpenCode Go/Zen, or Alibaba; those
adapters/endpoints remain unavailable rather than guessing passthrough.
used by #
rolling compaction asks for the context window when contextWindowTokens is not configured, so compaction works without hand-configuring each model. An explicit setting still wins. /context shows the window and names where it came from; an unlisted model stays explicitly unknown.
Provider extensions consume the catalog's endpoint and environment metadata, so authentication and transport policy do not require the daemon to hardcode a model list.
Messages wire adapters #
messages_api/request owns the portable Anthropic Messages body and tool-schema
normalization; messages_api/stream owns its SSE parser. Providers build their
own openai_api.Exchange, retaining endpoint, authentication, headers, timeouts,
and model policy. Bedrock Messages and OpenCode Go use the shared portable body
and reducer, without Claude-native replay details or Claude Code tool-name
rewriting. Existing stored gateway replay remains readable; new gateway turns
retain text, reasoning, tool calls, and usage without attaching signed Claude
blocks. Bedrock Converse keeps its own codec.
Claude keeps its richer request encoder and a thin reducer policy adapter for
signed native replay and Claude Code tool names. Its Messages transport and
advertised endpoint share a fixed Anthropic base; Files and model discovery
remain provider-owned. Extracting the wire codec does not allow subscription
credentials to be redirected to compatible endpoints.
Upstream.protocol describes the stored replay shape, not every HTTP API:
Messages adapters still produce Chat Completions-shaped portable replay.
Generic OpenAI Responses is already independent of Codex subscription policy.
provider extensions #
The catalog also lists model ids by provider or endpoint. /login and /model read those lists from this cache, or from a provider's own list (see model lists). GET /models/{provider}?endpoint=… answers ids. With &details=1 it answers {id, efforts, context, maxContext, raised, output, input} objects: the catalog's facts, the window a raised cap gives and whether it is raised, and the reasoning efforts a session accepts for that model. The /model picker lists every saved profile this way. The openai model-provider extension depends on models and owns API-key configuration plus the shared Responses/Chat Completions client. Once a Chat Completions history carries reasoning_content, that client sends the field on every assistant message, empty where a turn had none: DeepSeek's thinking mode refuses a tools request without it, and a gateway that spreads one model over several backends (OpenCode Go) returns turns without reasoning that the next backend then rejects. It also contributes a catalog that lists an endpoint's models by asking <baseUrl>/models, sending a saved key account or fresh environment key for that endpoint (5s to connect, up to 45s to answer: OpenRouter's list is large). Non-chat entitlements (audio, TTS, realtime, image, Wan, ASR, translation, and auto routes) are excluded generically. Alibaba uses this same catalog through an openai preset. It stays out of the way for api.openai.com and chatgpt.com, and when the endpoint cannot be reached or answers badly, models.dev's list applies as before. The codex extension depends on openai; it reuses that request stack and adds only ChatGPT OAuth, account selection, and Codex wire policy. The dependency chain is therefore codex -> openai -> models. perplexity depends on models: it serves the Agent API's Responses transport with a key account, lists the model ids {baseUrl}/models publishes, and leaves lookups to models.dev, which already describes the trainer's side of an id such as openai/gpt-5.6-sol. antigravity has no dependencies: it brings its own catalog and sign-in, and its upstream drives openai_api.exchange with its own encoder and reducer. mistral depends on models: it lists the chat models Mistral's /models publishes and answers lookups at a Mistral endpoint from models.dev's mistral namespace, which names no endpoint of its own (see mistral). claude depends on models: it selects its current and fallback models from the cached Anthropic namespace, reads exact provider-scoped limits and efforts, and uses a Messages transport with Claude Code OAuth rather than the catalog's API-key credentials.
Saved provider profiles carry an extension tag. Existing profiles without one migrate as openai. Codex profiles use extension: "codex" and keep OAuth credentials separately in $ALBEDO_HOME/creds.json; see model authentication.
A saved Bedrock profile with api: "converse" uses the adapter's verified effort
allowlist, not another API's catalog effort list. Session effort validation and
the profile's detailed model picker share this answer. A model without a
verified mapping has no selectable effort, even if it emits reasoning. The
model catalog still supplies limits and modalities; a missing limit stays
unknown. Bedrock Messages profiles and other providers keep their existing
catalog behavior. Exact supported IDs, ARN rules, levels, and public AWS sources
are listed under Converse reasoning effort.
Mistral's wire preparation drops empty portable assistant inputs, as well as replayed assistant messages left without content or tool calls. Both sessions and proxy requests use this preparation. Mistral's message validator permits consecutive user messages. Tool calls and results retain their order, and a user input after tool results still receives the assistant bridge. Other Chat Completions providers keep their existing projection: OpenAI's assistant content contract requires content unless a call is present, but does not specify Mistral's non-empty-content restriction.
model lists #
A catalog's list answers Unlisted (not its provider), Local(ids) (ids it already holds, such as models.dev's), or Remote(key, fetch, fallback) for a list a provider must be asked for. The host owns the cache for every remote list (harness/model_lists.gleam, albedo_model_lists.erl); a catalog keeps no list cache of its own.
- Each list is saved per catalog and
keyin$ALBEDO_HOME/model-lists/<sha256>.jsonwith the time it was fetched.keynames everything the list depends on (the openai catalog uses the endpoint and its API key; Codex the account id); it is hashed, never written. - A saved list answers at once, also after a restart. Once it is 5 minutes old the next listing starts a fetch in the background, so the listing after that sees the new list. One fetch runs per list.
- A list never fetched is waited on for at most 8s, showing
fallback(models.dev's list, or a facts file the catalog keeps) when that runs out; the fetch goes on for up to 55s and saves its answer when it lands. Until then, or for a minute after a slow failure, later listings showfallbackwithout waiting. A failure within a second (no sign-in yet) is retried on the next listing. - A saved list no listing has refreshed for 30 days (a key or endpoint no longer in use) is deleted when another list is saved.
- A failed fetch keeps the saved list;
fetchreturns an error rather than a fallback, so a transient failure never replaces what was saved. /reload modelsrefetches the list of every saved profile, each list once, plus the files a catalog'sreloadkeeps (the models.dev download), and reports one outcome per catalog. A catalog no profile uses is not reported.
Claude, Codex and Antigravity also keep model facts (limits, efforts, labels) in their own files for lookups; their fetch rewrites that file, and Antigravity still refreshes its file on the request path, since requests need its labels. Every period here is divided by ALBEDO_TEST_TIME_SCALE.
codex models #
Codex asks the ChatGPT backend which models it offers each signed-in account: GET https://chatgpt.com/backend-api/codex/models?client_version=…. The backend shows a model only to clients at or past its minimal client version, so albedo sends the latest released Codex CLI version, read from npm (@openai/codex), rechecked whenever a stale list is refreshed; a forced reload always rechecks it, so a released client version unlocks its models the moment you reload. A model OpenAI releases to Codex appears in /model without an albedo update, and no model name is hardcoded. The facts are kept per account in $ALBEDO_HOME/codex-models.json: the picker's list fetch (see model lists) refetches them, and a Codex session start refreshes them in the background when over an hour old, conditionally with the backend's ETag. Lookups read only that file.
A Codex lookup answers for the ChatGPT endpoint with the backend's own facts: the default window (context_window), the maximum a raised cap gives (max_context_window, when larger), input kinds, and reasoning efforts, including levels models.dev does not list. Codex advertises ultra as a virtual tier that cannot be requested; the catalog excludes it from new responses and existing cached lists. models.dev's openai entry fills in what the backend leaves out, such as the output limit. A model the backend does not report falls back to that openai entry. /effort, session defaults, and model switches use this same selection.
choosing a model from the command line #
albedo models prints provider/model for every configured provider, the active provider first and each provider's default model first. albedo -p <prompt> --model <model> takes either spelling the agents' spawn(model=...) takes: provider/model, or a bare id, served by the active provider when it offers it and otherwise by the one provider that does. Only exact ids match; an id no provider lists, or one several list, is refused before a session is created. A new session starts on the model; with --session, PATCH /sessions/{session_id}?view=configuration with the observed validator switches that session without making the model the default for new sessions, which /model does.
A persisted session without an effort selects the catalog default when its actor starts. The actor must save that effort before accepting work. A failed write leaves the session unavailable, logs the initialization failure, and retries on its next activation.
raised caps #
A provider can report a larger window than a session uses by default; Codex reports 272k by default and 872k at most for GPT-5.6 and GPT-6. Models degrade over long contexts, and published long-context results differ by model, so albedo uses the default until you raise the cap. /raise-cap or tab in the /model picker raises it for one model, saved in $ALBEDO_HOME/extensions.json as soon as you press tab, without switching to the model:
{ "raisedCaps": { "gpt-6-astra": true } }
Compaction, /context, and every session on that model then use the maximum from their next request. A catalog reports the maximum as ModelInfo.max_context_tokens; a model without one has nothing to raise.
compact-at #
A model can have its own compaction window, saved for every session on it and always at or below the model's window (a larger saved value changes nothing). ctrl+e in the /model picker edits it for the highlighted model (an empty field clears it; 100k, 1.5m and 250000 all parse), and enter in the field saves it at once, like tab for the cap: neither waits for a model switch. A save the daemon refuses leaves the row as it was and shows why. It is stored in the compactAt section of $ALBEDO_HOME/extensions.json, keyed like raisedCaps:
{ "compactAt": { "gpt-5": 150000 } }
PATCH /settings?group=models writes it as compact_at (a count, or null to clear), and GET /models answers it as compact_at beside raised. selection.compact_at reads it and selection.window is the one answer everything uses: the lower of the saved window and the model's window (default, or the maximum once raised). /context, /compact-at and compaction all see it from the next request; source then names it (…; compact-at). It sits in the same layer as context-limits.json, so a session's own /compact-at outranks it, and so does a strategy's configured contextWindowTokens. See compaction.
context limits (context-limits.json) #
priv/context-limits.json and $ALBEDO_HOME/context-limits.json merge by entry id (with local overriding default, and new local ids placed first) using the same {extension?, host?, model?} glob matcher as cache-ttl.json:
{
"version": 1,
"entries": [
{
"id": "haiku-100k",
"match": { "model": "*haiku*" },
"contextTokens": 100000,
"note": "keep prompts under the 100k pricing and quality cliff"
}
]
}
When an entry matches a catalogued model, ModelInfo.context_tokens becomes contextTokens and, if the catalog reported a larger window, ModelInfo.max_context_tokens preserves that ceiling so /raise-cap and tab in /model can still raise the model to its full window.
antigravity models #
Antigravity is not on models.dev, and its model ids and required model_enum labels change server-side. The antigravity extension therefore contributes its own catalog. It asks Cloud Code Assist fetchAvailableModels which models the account can use, keeps the ids that the real client's agent picker shows, and caches them with their limits, labels, and the renames of retired ids in $ALBEDO_HOME/antigravity.json. The picker's list fetch (see model lists) rediscovers it, and requests refresh it in the background after 6 hours. A built-in table supplies each model's thinking control and serves as the list before the first discovery. A saved profile that names a retired id follows the backend's rename. The catalog answers lookups only for the Antigravity endpoint, so models.dev keeps answering for the same ids at other providers.
contribute another catalog #
extension.ModelsPlugin(ModelCatalog(lookup, list, reload)) is the catalog contract. lookup(model, endpoint) returns optional facts for one model; list(provider, endpoint) returns Unlisted to decline, Local(ids) for ids at hand, or Remote(key, fetch, fallback) for a provider list the host caches (see model lists). The first enabled catalog that claims the query wins, even when its local or remote list is empty; unavailable models must not leak in from later catalogs. reload refetches a file the catalog keeps besides its list, or is None.
A model transport contributes ModelProviderPlugin(ModelProvider(catalog_provider, resolve, fast_mode)). fast_mode is Option(fn(ModelContext) -> FastMode): use None when the adapter does not implement fast mode, or Some(check) to report verified support, unsupported, or unknown for that profile/account/model. None reports unsupported by this adapter, not a claim about the upstream service; a failed or inconclusive implemented check stays unknown. resolve receives the session's ModelContext.fast preference and owns its protocol-specific translation; fast mode does not change reasoning effort. catalog_provider is the models.dev namespace used by /login and /model; resolve returns the profile's extension.Upstream: its endpoint, the replay protocol it produces, a stream function over albedo's request types, explain, which turns a provider failure into an actionable message, and images, the largest image edge every request it sends accepts (extension.any_images, albedo's own 16384px bound, when the provider states none). openai builds upstreams from the shared openai_api client and uses harness/rotation over explicit key lists; a provider with another wire format supplies its own stream. Its extension should require the catalog or transport extension whose metadata and wire behavior it reuses. For example, openai declares the openai namespace, while codex also declares openai and inherits the shared transport through its dependency. New provider extensions get catalog-backed discovery by declaring their namespace. A provider whose backend reports its own models, as Codex does, can also answer from a ModelsPlugin of its own.
An openai profile declares albedo's own bound unless its config.json entry sets "imageEdge" (pixels), for an endpoint that takes smaller images; a login saves its profile through the daemon, which merges it into the stored one, so a hand-set value survives. claude declares 2000px, and so do bedrock and antigravity's Claude models (types.anthropic_images; its Gemini models declare albedo's bound), since the rule is Anthropic's, not the route's. Anthropic takes an 8000px image on its own, but once a request carries more than 20 images every image in it, earlier turns and tool results included, must fit in 2000px, and a working session passes 20 quickly. A tool image over the limit never reaches the model: each cell carries the edge to the kernel, so show_image raises ValueError: 2304x1212 image is over this model's 2000px edge limit at the call and the model can show a smaller one (the daemon checks again and names any it still refuses under image_errors), and a submitted image over it is refused before the transcript keeps it.
A model whose catalog entry lists its inputs without image (the provider's own catalog, else models.dev's, as for every other model fact) is sent no images at all: each request replaces a message's or tool result's images with a note saying how many were omitted, while the transcript keeps them for a model that reads them. Its upstream gets max_images: Some(0), which also reaches the prompt and the kernel. The loop's request_for appends a line to the instructions saying the model reads text only and to review with git diff and files.read, so turns, rebuild and summaries all carry the same prompt. Each cell is sent text_only, so show_image and api.attach_image raise ValueError: this model reads text only ... at the call, view_diff and view_code raise before they render (through api.require_images), and browser and cua screenshots return without attaching. A cell cells.run starts inherits the outer cell's limits. A model no catalog describes is sent images as before.
An image cut short is refused the same way, whatever the model: a PNG whose chunks stop before IEND, or a JPEG with no end-of-image marker after its frame header, still has a readable header, but the provider cannot decode it and would refuse every later request that carries it. show_image raises ValueError: this 49152-byte image/png stops before its IEND chunk: ... at the call, and image.inspect, which every attach path goes through, refuses it again; history loads check only headers, so an image already stored stays loadable.
History can still hold images over the limit: ones an earlier, looser provider accepted before a model switch, or ones from before limits were declared. At the start of each model step, before the request is prepared, the loop has albedo-render --fit EDGE scale each such image down (aspect kept, Lanczos; a JPEG stays JPEG, anything else becomes PNG) and appends an image fit row to the transcript (transcript.ImageFit): the copy, stored in images like any payload, the original's payload hash, and a user note from origin image scaled (An earlier image was scaled from WxH to WxH to fit this model's EDGEpx edge limit; the transcript keeps the original.). Loading the history applies each fit to every row before it, so the transcript alone says what any request carried: provider request rows, /context, forks, and restarts all see the copy, and a fork from before the fit row starts from the original. Readers that only show rows see the fit as its note, which the chat shows once. The original row is never rewritten. A fit is permanent: switching back to a looser provider keeps the copy (fitting per edge is a TODO in daemon/image_fit.gleam). Snapcompact frames never need fitting, because they are rendered to fit. Without albedo-render the turn fails before sending, naming the binary, rather than getting the provider's 400.