Configuration
Setup & providers
Configuration and setup¶
/setup is the configuration center: viewing, adding, editing, and removing
providers, credentials, models, and provider defaults. /model is fast live
switching only — it changes the running session's provider, model, and effort
and never edits configuration. A normal user configures a provider in four
choices plus Save:
Provider -> Credential -> Models -> Default model -> Save
Nothing else is required to get a known provider working. Context window, transport, reasoning replay, input modalities, effort wire mapping, token budgets, and aliases live under Advanced and are never part of the default path. Built-in provider and model facts come from the Latch catalog; user configuration stores selections and overrides, never a copy of the built-in metadata.
Configuration center¶
/setup opens on the provider list, not a step-by-step wizard. Each row shows
what the user needs to decide next:
- display name and stable provider id;
- status:
ready,missing credential, orunresolved models; - configured model count;
- the provider's default model.
A first run with no usable provider opens the same list with Add highlighted. The list is the navigation hub for per-provider surfaces: Credential, Models, Default model, and Advanced, plus Add provider, Edit provider, and Remove provider. Every list uses the windowed choice surface, so long catalogs scroll instead of clipping.
/setup
Providers ready · missing credential · unresolved
Add provider…
OpenCode Go | OpenCode Zen | OpenAI | Anthropic | DeepSeek | Custom Provider
Credential environment variable name | masked secret
Models catalog multi-select, add custom model, refresh
Default model
Save
Edit provider…
Credential / Models / Default model / Advanced
Remove provider… config only; stored credentials are kept
Simplified paths and Advanced¶
Known providers — OpenCode Go, OpenCode Zen, OpenAI, Anthropic, and DeepSeek — resolve base URL, transport, reasoning replay, input modalities, context window, and effort mapping from the catalog. Their setup asks only for a credential, the models to enable, and a default model. Only Custom Provider asks for protocol and base URL by default; transport, replay, modalities, context, aliases, and wire mapping stay under Advanced.
Adding a custom model normally asks only for the request/model id and a display name. Everything else about it — transport, context window, replay, modalities, effort mapping, token budgets — is Advanced, and a model with an unresolved transport is not offered for activation until it is filled in.
Metadata resolution¶
Effective model metadata resolves in exactly one order:
builtin catalog -> discovered metadata -> user overrides
- Builtin catalog is Latch-owned truth for known providers and models:
transport, context window, effort set and default, replay policy, and input
modalities. It ships with Latch and is never copied into
config.toml. - Discovered metadata is a bounded, allowlisted service catalog: ids, labels and explicitly declared transport/context/effort/image facts. Unknown gateway ids without protocol metadata remain unresolved; DeepSeek/Codex use their fixed family protocols. Remote prompts, URLs, prices and authorization claims are ignored.
- User overrides win field by field over both discovery and builtins.
config.toml keeps explicit model overrides in models and sanitized discovered
facts in a separate catalog section. The latter retains historical descriptors
for resume, while its available list drives the picker. A regenerable cache
must never be the only source of a selected model's wire metadata.
Models, selections, and defaults¶
- The catalog defines the known model set for a provider. Custom models extend it through overrides.
providers.<id>.enabled_modelsoptionally selects which models the provider offers; absent means the full catalog plus custom entries. Selections are not metadata.providers.<id>.models.<request>holds per-model overrides only, and is written only when a user changes a field. Existing override entries are preserved field by field.providers.<id>.default_modelis the model selected when switching to that provider.[inference]is the new-session default profile.- The current session's
/modelchoice is live runtime state, durable for that session and never written back to configuration.
These are three different defaults and none may stand in for another:
providers.<id>.default_model— the model/modelselects when it switches to that provider. It must not fall back to catalog index 0.[inference]— the new-session default profile: the provider/model/effort a fresh session starts with.- The running session's
/modelselection — live state only.
The first successful setup of the first usable provider may seed
[inference]. Later provider edits never change it. Changing it afterwards
requires an explicit Set as new-session default action in the configuration
center. The running session's live /model state stays separate: /model
never rewrites [inference], a provider default, or an override.
Thinking strength¶
The provider-neutral effort vocabulary stays none/minimal/low/medium/
high/xhigh/max. Built-in models map efforts to the wire automatically
through their transport adapter; the default flow only chooses which levels to
expose and which one is the default.
Wire mapping is a Custom/Advanced concern. effort_map maps each exposed level
to the transport's documented form:
[providers.acme.models."acme-pro"]
efforts = ["low", "medium", "high"]
default_effort = "medium"
[providers.acme.models."acme-pro".effort_map]
low = { value = "1" }
medium = { value = "2" }
high = { budget_tokens = 32768 }
value = "…"— the transport's effort field (reasoning_effort,reasoning.effort,output_config.effort,thinkingConfig.thinkingLevel).budget_tokens = N— the transport's token budget (thinking.budget_tokens,thinkingConfig.thinkingBudget).disabled = true— the transport's documented off switch (thinking.type = "disabled",thinkingConfig.thinkingBudget = 0).
A map covers every exposed level or is absent (absent means the neutral
identity for transports that speak neutral names); default_effort must be
one of efforts; budget and disabled forms are rejected for transports
without such a field, with an actionable setup message. Mapping is
serialization: it lives in the adapter, and the kernel loop and memory never
see it.
Gemini thinking is model-specific, not transport-wide: one transport carries
level models (thinkingConfig.thinkingLevel), budget models
(thinkingConfig.thinkingBudget), models where zero disables thinking, and
models that cannot disable thinking at all. Each Gemini model therefore
declares a gemini_thinking capability — levels with the documented level
set and optional off level, or budget with zero_allowed. Built-in catalog
rows carry their documented capability automatically; a custom Gemini model
declares it explicitly rather than inheriting a transport-wide default.
Validation rejects a value map on a budget model, a budget_tokens map on
a level model, and a disabled map when the model documents no off switch.
Discovery¶
Go, Zen and DeepSeek expose GET /models; ChatGPT subscriptions use the
authenticated Codex catalog. Listings can be global service metadata, not proof
that every key/account is entitled to every model. Setup/login refresh supported
services automatically. /model refresh and setup's Refresh action refresh them
later; startup/doctor remain offline. All calls are bounded and read-only.
Discovery is conservative:
- explicit protocol metadata can resolve new gateway models; an id alone cannot unless the provider family has a fixed supported protocol;
- model facts merge as
builtin catalog -> discovered metadata -> user overrides; - no remote endpoint, system instruction or permission claim is adopted;
- a failed/empty response leaves the saved catalog intact;
- picker availability updates without deleting old descriptors needed by resume;
- one credential covers all models on that configured service. New setup enables
all catalog models, while deliberate subsets remain supported.
/model allclears local model filters without changing credentials or active profiles.
Zen and Gemini¶
Zen serves some models over Google's Generative Language API. Supporting them
adds a Gemini transport: streaming generateContent requests
(models/{model}:streamGenerateContent?alt=sse), contents with
functionCall/functionResponse parts, inlineData images, and
generationConfig.thinkingConfig for thinking (includeThoughts, plus the
model's documented thinkingLevel or thinkingBudget). Multimodal tool
results travel as functionResponse.parts, not as sibling parts.
The transport must not assume the API omits call ids: provider-native call ids are preserved whenever they are present, and deterministic ids are synthesized only as a compatibility fallback for APIs or models that genuinely omit them. Tool responses are associated by call id, with the function name used only as a sanity check, never as the sole correlation key.
Thought signatures are reasoning state, not decoration: Gemini thought and function-call parts can carry signatures that must be preserved and replayed when the API requires them for multi-turn reasoning. Before P3 lands, the durable reasoning representation is reassessed for positional thought parts: the current provider-neutral artifacts must be able to store every thought part, its order, and its opaque signature losslessly, or the representation is extended. A design that drops or reorders thought signatures is not acceptable; replay is capability metadata, exactly as it is for every other transport.
ResolvedPaths and the storage root¶
One abstraction, ResolvedPaths, owns path resolution; no other module calls
XDG or default-path APIs, and no other module constructs a config, state,
secret, database, artifact, or cache path independently. It resolves:
config_pathstate_rootsecrets_pathdatabase_pathartifacts_rootcache_root- the path source (
explicit,new, orlegacy) and the legacy source paths when one applies.
The new default layout is a single root:
~/.latch/
config.toml profiles, models, selections, overrides; never a secret
secrets.toml provider API keys, 0600, staged writes
latch.sqlite3 durable sessions and events
artifacts/ session artifacts
cache/ regenerable metadata: discovery snapshots, catalogs
cache/ holds only regenerable data and is safe to delete. The root is
created 0700 on first use. An explicit --config path wins; an explicit
state_dir in configuration overrides state_root. Otherwise
~/.latch/config.toml is the default. latch doctor reports every resolved
path and its source, and the setup review names the root it will write to.
Legacy compatibility and latch migrate¶
Existing XDG installations keep working without a rewrite: when
~/.latch/config.toml does not exist and a legacy config does
(~/.config/latch/config.toml, with state under ~/.local/state/latch/),
ResolvedPaths resolves legacy and Latch reads those files in place. No run
silently relocates sessions or credentials.
Migration is explicit and one-directional. latch migrate rewrites the
configuration under the new root, stages the secrets there, copies the session
database with SQLite backup semantics, copies artifacts, writes a migration
marker recording the source paths and timestamp, and renames the legacy
config.toml (and any legacy secrets file) to a non-discoverable backup name.
After migration:
- deleting
~/.latch/config.tomlyields fresh defaults and a fresh setup; it must not silently reactivate the stale legacy configuration; - backups may be kept, but migrated backups no longer participate in automatic discovery, because discovery matches only the exact live filenames;
- the marker keeps
latch doctorable to explain where the installation came from.
Crash-consistent persistence¶
Configuration and secrets are separate files and are not updated as one cross-file transaction. Saves are staged and ordered so a crash can never produce configuration that references a secret which was never committed:
- validate the whole change first; a validation failure writes nothing;
- write temporary files in the target directory;
- commit
secrets.tomlfirst (fsync, rename); - commit
config.tomllast (fsync, rename).
Failure semantics are defined rather than assumed: a crash before the secrets
commit leaves the previous configuration intact; a crash after the secrets
commit can leave an orphan secret, which is acceptable because nothing
references it; a crash can never leave a file:<id> reference without its
committed secret. Removing a provider writes configuration first and may leave
an orphan secret by design. On load, a file:<id> reference with no stored
value is reported as missing credential — a recoverable provider status, not
a startup failure — and /setup offers to enter the key again.
Security and validation¶
Secrets exist only in TUI memory until Save, are masked in display and debug
output, never enter config.toml, the event log, the transcript, or logs, and
are redacted from every error path. The storage root is 0700, secrets.toml
is 0600 and staged, a secrets file that is group- or world-readable is
refused on read, and the whole root is masked inside the mandatory sandbox so
sandboxed commands and extensions cannot read a key even when the workspace or
HOME changes. Every surface that names a credential prints only its symbolic
reference; deleting a stored secret is an explicit action, never a side effect
of removing a provider.
Validation is explicit and actionable: provider ids are unique and
[a-z0-9._-]; URLs parse as http(s); environment variable names are
identifiers; secrets and request names are non-empty; context windows are
positive token counts; a provider has at least one enabled model and a
default_model that is enabled; an override never widens a capability the
transport cannot express; and unresolved models cannot be activated.
Phases¶
- P0a — paths, storage, migration, permissions:
ResolvedPaths, the~/.latchlayout,cache/, legacy read-in-place discovery, the migration marker and non-discoverable backups,latch migrate, and the0700/0600permission rules. - P0b — config domain and persistence: multi-model provider config, selections vs overrides, the three defaults, and crash-consistent staged saves with defined recovery.
- P0c — configuration center: the
/setupprovider list with status, Add/Edit/Remove, simplified known-provider paths, Custom Provider basics, Advanced disclosure, and/modelas live switching only. - P0d — Zen with existing transports: the
opencode-zenkind for chat/responses/messages models, with built-in catalogs and no Gemini yet. - P1 — effort mapping:
effort_mapconfig, adapter emission rules, and the Custom/Advanced mapping editor. - P2 — discovery: the conservative refresh round trip, catalog merge, and
unresolvedhandling. - P3 — Gemini: the transport adapter with native call ids, call-id correlation, and lossless thought-signature replay, including any provider-neutral reasoning-representation extension it requires.
Each phase is independently releasable, keeps the provider-neutral boundaries
intact — P3 may extend the durable reasoning representation if lossless Gemini
thought-signature replay requires it — and updates README.md,
config.example.toml, and ARCHITECTURE.md where behavior or ownership
changes.