OpenGenie
Repository on GitHub

005 · Every model, one picker

On this page

A specification and implementation plan for making the Coding Agent offer every model declared on the Databricks AI Gateway, plus external providers (OpenAI, Anthropic, Google Gemini on Vertex AI) whose keys live in Lakebase — without weakening the write fence, and without a code change per model.

Status: specification · 7 November 2026 Scope: apps/coding-agent/api (FastAPI: model catalogue, resolution, credentials), apps/coding-agent/web (React: picker, settings, health), and a new llm_* schema in Lakebase. Relation to earlier specs: builds on 001 · Databricks coding agents (ACP runtime, write fence, evidence), 001 · Perfect shell (topbar, chips, CLS discipline) and 003 · Dev UX/UI (the editing surface whose model chip this spec replaces). Does not replace any of them. Audience: product, full-stack, database, UX/UI, front-end, AI engineering, platform security.


The one-paragraph version

Today the app can talk to five models, and their names are a Python list. The list is compiled in at api/config.py:32-41, turned into an endpoint by string surgery at api/adapters/acp/agent_config.py:180-189, and written into a single-provider opencode.json whose only credential is DATABRICKS_TOKEN (agent_config.py:37-66). Adding a model is therefore a code change and a deploy; an external provider is impossible, because the config schema has one provider slot and the agent process receives exactly one secret. Meanwhile the evidence in this repository says the model is the single largest lever on outcome — the same tools and rules scored 39% with the gateway model and 86% with Gemini (README finding #2) — and the gateway roster is gated: Claude and Gemini return MODEL is not enabled until an account admin flips a beta (original spec §3.2). So the product is one allow-list entry away from being unable to use the best model it has measured.

This spec replaces the list with a registry: a table of providers and models, seeded from a file and refreshed from live discovery, plus a resolver that turns one canonical model id into (provider block, credential, limits, capabilities) and a credential vault in Lakebase with envelope encryption. Adding a gateway model becomes a discovery pass. Adding OpenAI, Anthropic or Vertex Gemini becomes a row plus a key. The agent still receives its secret only as an environment variable — the invariant at agent_config.py:55-56 ("a config on disk is a secret on disk") is kept and generalised, so one injection mechanism serves every provider.


What is wrong today, in one table

#ProblemEvidence
E1The catalogue is source code. Five ids, hardcoded.api/config.py:32-41
E2A model id is converted into an endpoint by string surgery, so an id outside the convention resolves to nonsense instead of an error.agent_config.py:180-189
E3Exactly one provider is expressible. as_opencode_json() emits a single dbx block; there is no second slot and no per-provider credential.agent_config.py:37-66
E4The agent process receives one secret. SessionSpec.env={"DATABRICKS_TOKEN": token}. A second provider has nowhere to put its key.orchestrator.py:227-229
E5Two id dialects already coexist. The app writes agent ids as dbx/<endpoint> but stores sessions as databricks/<something>, so the switch and the advertised list disagree.agent_config.py:40 vs config.py:32-41; orchestrator.py:264-282
E6Model switching sends an id the agent does not know. set_model passes the app id straight to ACP; the probe recorded exactly this failure class (-32603 on an unresolvable provider).orchestrator.py:279-280; docs/e2e/20_acp_config_probe.txt
E7Output caps are a hardcoded dict, and a wrong cap makes a model unusable ("max_tokens 24000 cannot exceed 8192").agent_config.py:84-91; test at tests/test_genie_features.py:512-519
E8The picker is two <select> elements with no grouping, search, capability or state.web/src/App.tsx:433-444; web/src/components/Chrome.tsx:184-188
E9There is no durable store at all. The event store is in memory; a registry needs the database 001 planned but did not ship.api/adapters/memory/store.py:1-55
E10A rewritten config is not a model switch. set_model writes opencode.json at runtime, where nothing re-reads it; the real switch is the ACP call beside it.orchestrator.py:271-282

The full audit, with line-level citations and the measured consequences, is 01 · Current state.


The design in one picture

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E8F0FE','primaryTextColor':'#1F2937','primaryBorderColor':'#A8C0E8','lineColor':'#94A3B8','clusterBkg':'#F8FAFC','clusterBorder':'#CBD5E1'}}}%%
flowchart LR
    subgraph SRC["Sources of truth"]
        SEED["seed file<br/>llm_models.yaml"]:::green
        DISC["live discovery<br/>serving-endpoints"]:::green
        DB["Lakebase<br/>llm_provider · llm_model"]:::blue
    end
    subgraph CORE["Domain (pure)"]
        CAT["ModelCatalogue<br/>merge + precedence"]:::yellow
        RES["ModelResolver<br/>id to descriptor"]:::yellow
        POL["ModelPolicy<br/>limits + grants"]:::yellow
    end
    subgraph ADAPTERS["Adapters (I/O)"]
        ADBX["DatabricksGateway"]:::pink
        AOA["OpenAI-compatible"]:::pink
        AAN["Anthropic"]:::pink
        AGV["Google Vertex / Gemini"]:::pink
        VAULT["CredentialVault<br/>envelope encryption"]:::orange
    end
    AGENT["opencode acp<br/>one JSON config + env"]:::purple
    UI["React picker<br/>grouped · searchable"]:::purple

    SEED --> CAT
    DB --> CAT
    DISC --> CAT
    CAT --> RES
    RES --> POL
    RES --> ADBX
    RES --> AOA
    RES --> AAN
    RES --> AGV
    RES --> VAULT
    POL --> RES
    RES --> AGENT
    CAT --> UI
    UI --> RES
    classDef green fill:#E6F4EA,stroke:#A8D5B5,color:#1F2937
    classDef blue fill:#E8F0FE,stroke:#A8C0E8,color:#1F2937
    classDef yellow fill:#FFF8E1,stroke:#F2D98D,color:#1F2937
    classDef pink fill:#FDE7EF,stroke:#F2B5CB,color:#1F2937
    classDef orange fill:#FFEDE0,stroke:#F5C29C,color:#1F2937
    classDef purple fill:#EFE7FB,stroke:#C9B6EC,color:#1F2937

Adding a gateway model = discovery writes a row. Adding a provider = one adapter class + one row. Adding a user's own key = one row in the vault. No path requires editing the resolver.


Scope

In scope

Out of scope, and why

Not doingReason
A model router that picks a model per turn from cost/latencyValuable, but it needs the registry first. The seam is the resolver's single entry point (11 D7).
Fine-tuning, embeddings and image endpointsThe agent needs chat with tool calling. Embeddings are a different consumer with different limits.
Azure OpenAI / AWS Bedrock / on-prem gatewaysThe adapter shape supports them; each needs its own auth dance and a workspace we can test against. Named as a follow-on, not silently included.
Storing prompts or completionsUnchanged from 001 08: the app stores what happened, not the data.
Letting the browser hold a provider keyThe key never leaves the server; the browser sees last4 and a fingerprint. 06
Trusting a model's self-reported context windowLimits are measurements with provenance, or they are defaults marked unverified. 05

Reading map

Read in this order the first time. Every document is self-contained and cross-linked both ways.

#DocumentThe question it answersLead lens
00Why, from first principlesWhy does this matter, and what is irreducibly true?PO + AI engineer
01Current state auditWhat exactly exists today, measured, with line numbers?Full-stack
02RequirementsWhat must be true when we are done?PO + UX
03ArchitectureHow is it built, and where does each piece live?Full-stack + compiler
04Provider integrationWhat is different about each wire protocol?AI engineer
05Data model in LakebaseWhat do we store, and how is it shaped?Database
06Security and credentialsWhere do keys live, and what can go wrong?Security + DB
07UX, wireframes, navigationWhat does the user see, in ASCII, at every state?UX + front-end
08Six lensesWhat does each discipline refuse to ship without?all six
09Implementation planIn what order, with what acceptance, DRY and SOLID?Full-stack
10Edge cases and testingWhat can go wrong, what stops it, and which test fails if it does?compiler + AI
11Decisions and risksWhat did we choose, reject, and fear?PO
12Operations runbookHow is it run, rotated, and debugged at 3 a.m.?platform

Tooling in tools/ generates the ASCII diagrams and repairs anchors. The four checks below are what keep this spec set honest, and they run in CI:

cd apps/coding-agent/web
npm run verify:spec-005          # internal links + anchor slugs + ASCII box alignment
node scripts/verify-spec-mermaid.mjs ../../../specs/005-llm-models   # every diagram parses
python3 ../../../specs/005-llm-models/tools/diagrams.py --check      # markers well-formed

Cross-reference matrix

Who depends on what. A change in a row means re-reading the columns marked ●. This matrix is the contract that keeps fourteen documents from drifting apart.

↓ depends on →00010203040506070809101112
00 Why—●●●●●
01 Current state●—●●●●●●●●
02 Requirements●●—●●●●●●●●
03 Architecture●●●—●●●●●●●●●
04 Providers●●●—●●●●●●
05 Data model●●●●—●●●●●●
06 Security●●●●●●—●●●●●●
07 UX●●●●—●●●●
08 Lenses●●●●●●●●—●●●
09 Plan●●●●●●●●●—●●●
10 Edge cases●●●●●●●●●—●●
11 Decisions●●●●●●●●●●●—●
12 Operations●●●●●●●●—

If you only read one section

Read 03 §The resolution pipeline. It is seven steps from a string a user clicked to an environment variable in a child process, and every other document is either an input to one of those steps or a check on it.


The five sentences this spec must satisfy

If any of these is false, the design is wrong and this document is amended before code.

  1. Every model the workspace can serve is selectable, and the list is produced by discovery, not by editing Python.
  2. A user can add their own provider key and use it, without that key ever being written to a file, returned by an API, or logged.
  3. No offered model can be asked for more than it allows — the output cap is data with provenance, and a mismatch is refused before the request leaves the app, not after the endpoint rejects it.
  4. The model a turn used is the model that answered, pinned, auditable, and shown — never a value silently rewritten under a running session.
  5. When a provider is broken, the user sees the provider's own words and a next action, not "internal error" and a spinner.

Provenance of the claims in this spec

Every factual statement is labelled:

LabelMeaning
[verified]Read in this repository's code, or reproduced in an evidence file under docs/e2e/
[documented]Taken from vendor documentation, with a link
[proposal]A design decision of this spec, not yet built or measured
[unverified]Believed true, not checked; listed again in 12 §What we have not verified

The gateway's own discovery response shape is [verified] only for the fields the current code reads (name, task, state.ready — api/adapters/databricks/serving.py:29-31). Any field beyond those is [proposal] and must be probed before it is depended on.