Files
hq/02-DECISIONS/0055-model-access-is-answered-by-a-licence-or-a-node.md
T
jschoubben cde00e1d5f 0054 and 0055 join the topic their subject already had
Both carried 'model access', which is not one of the six the reading order
knows, so neither had a place in it. 0050 — the record they extend, on the same
subject — is 'what runs on it'.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-11 01:06:59 +02:00

147 lines
9.9 KiB
Markdown

---
topic: what runs on it
status: accepted
date: 2026-09-07
deciders: jochen
reconstructed: false
extends: 0050-model-access-is-vendor-agnostic.md
---
# 55. Model access is answered by a licence, or by a node that hosts the model
## Context
**[ADR 0024](0024-model-access-is-a-provision.md) made model access a provision, and
[ADR 0050](0050-model-access-is-vendor-agnostic.md) fixed what answers it: a record — a licence — that
an adapter turns into a sealed vendor credential.** A consumer requires `model-access`, is put on a
licence, and is delivered a key (a static API key, or an access token a manager refreshes). Every
answer so far has been a credential to reach a vendor's API across the internet.
**But a model need not come from a vendor. A node in the mesh can host one.** An operator with a GPU
runs Ollama or vLLM, which serves an OpenAI-compatible API on that node. A consumer that wants that
model does not need a vendor credential — it needs the model server's **endpoint**: the base URL and
the model name, and a key only if the server is configured to want one. This is model access answered
by a **node**, not by a record.
**The mesh already knows how a node answers a provision — it is the ordinary provider/consumer path.**
A provider `provides` a provision at a scope, `serves` its connection facts, and the mesh fills the
consumer's bound facts with the provider's `at`/`port` and each served fact — exactly how a Postgres
consumer learns where its database is. Nothing reserved `model-access` to records: a node offering
`provides: ["model-access"]` resolves through this path, and the resolver already **prefers a local
answer over a licence** — its own comment names the case, "a model the mesh runs itself." So the
capability exists; what is missing is the decision to use it, and the statement of what a node-answer
delivers and where it stops.
**A node-answer and a record-answer are the same provision with two shapes of answer.** This is the
same move [ADR 0050](0050-model-access-is-vendor-agnostic.md) already made for shapes within the
vendor path (static-key vs refreshable-grant): one provision, more than one way it is answered. A
consumer written against `model-access` should not care whether the model behind it is a vendor's or
the mesh's own — it asks for model access and is given what reaches a model.
## Considered Options
1. **A separate provision for the local case (`local-model`, `model-endpoint`).** A node answers that;
`model-access` stays record-only. Rejected: it splits "where my model comes from" into two
provisions a consumer must choose between in its manifest, when the mesh already models a
record-answer and a node-answer to **one** provision. A consumer would have to know, at authoring
time, whether its model will be a vendor's or the mesh's — the exact coupling the provision was
meant to remove. It is the safer implementation (see the limitation below) but the worse interface.
2. **Model access answered by either a licence or a node, under the one provision.** A consumer
requires `model-access`; the operator answers it with a licence (a vendor) or by assigning a
node that hosts a model. Adopted: one interface, and the answer is an operator's deployment choice,
not a consumer's authoring choice.
## Decision
**Model access is one provision answered two ways: by a licence (a record, turned into a sealed vendor
credential by an adapter — [ADR 0050](0050-model-access-is-vendor-agnostic.md)) or by a node that hosts
the model (a provider that serves an endpoint).** A consumer requires `model-access` and is delivered
whichever the operator assigned; it does not name the kind.
**A node-answer delivers an endpoint, not a credential.** The provider `provides: ["model-access"]`
and `serves` its connection facts — the port it listens on and the model it runs — and the mesh fills
the consumer's bound facts with the provider node's `at`, the served `port`, and the served `model`,
the same way every provider consumer learns where its provider is. The consumer assembles a base URL
(`http://<at>:<port>/v1`) and points an OpenAI-compatible client at it. If the local server wants a
key, the provider mints one the ordinary way (a per-consumer secret, sealed and host-unsealed); if it
does not — the common Ollama case — the consumer lists `model-access` under `binds` and **not** under
`secrets`, and no key is delivered. Secret delivery and fact delivery are already independent, so a
keyless endpoint is expressed by asking for the facts and not a secret.
**A node-answer uses no adapter.** The adapter registry ([ADR 0050](0050-model-access-is-vendor-agnostic.md))
is the vendor-credential machinery — accept-and-seal, refresh, usage. A node-hosted model has no vendor
secret to seal; its endpoint is served, and its key (if any) is minted like any provider's. The adapter
is consulted only for the record/vendor answer. So the vendor-agnostic decision is untouched, and the
node-answer adds no vendor logic anywhere.
**The resolver prefers a local answer.** When a node's own set answers `model-access` — a model the
mesh runs itself — a licence for it is not consulted. This is already the resolver's behaviour and is
made a decision here: a mesh that runs a model uses it, and a licence is the answer for a consumer that
has no local model, not a competitor to one that does.
**One limitation, stated so it is not found as a bug.** The local-preference above is exact for a
**node-scope** provider co-located with its consumer, and for any node-answer in a mesh that holds no
`model-access` licence. It is *not* yet exact for a **mesh-scope** model server — one node serving the
model to others — **while a licence for `model-access` also exists in the same mesh**: the record pass
that turns a licence into an answer keys on same-node satisfaction and would still demand the licence be
used, double-answering. Until that pass is taught to stand down when a brokered node need already
answers, a mesh-scope local model and a vendor licence must not both answer `model-access` in one mesh.
A node-scope local model has no such constraint. This is named because an unstated limitation is
indistinguishable from a bug, and costs more.
This is a decision and not a patch because it settles **what may answer model access** — a question
every model-access consumer's meaning depends on — and because it lets the mesh's own hosted models sit
behind the same provision as the vendors', which is what makes "the mesh can run its own model" a
deployment choice rather than a second interface to build against.
### How each claim is checked
- **A node answers model access without a licence, and is preferred over one.** A resolver test
assigns a consumer and a module that `provides: ["model-access"]` at node scope on the one node, with
a licence also present, and asserts no resolved need is answered by the record — the local model
answers and the licence is ignored. (This test exists; the decision adopts what it proves.)
- **The consumer is delivered an endpoint, not a credential.** A mesh bed assigns a model-server
provider and a consumer that binds `model-access` and does not list it under `secrets`, and asserts
the consumer's config carries `OPENAI_BASE_URL` built from the provider's served `at`/`port`, and
that no key file was delivered to its secret path.
- **The local endpoint is reachable through what the consumer was given.** The bed makes a request to
the base URL the consumer wrote and asserts the model server answers — the wiring, not a model's
output, is what is proven (the server may be a stub; a real model is not needed to prove the mesh
routed the consumer to it).
- **A node-answer consults no adapter.** A node-answered `model-access` need is resolved with the
vendor registry never read — asserted by the absence of any vendor on a node-answered need and the
ordinary served-facts delivery.
- **The scope limitation holds where stated.** The node-scope case is what the bed and the resolver
test exercise; the mesh-scope-plus-licence collision is recorded here and left for the resolver
change that reconciles the two answer passes, not worked around in a module.
## Consequences
- **The mesh can run its own model, and a consumer reaches it through the same `model-access` it uses
for a vendor.** One interface, two answers; a consumer moves between a vendor and a local model by an
operator reassigning its provision, not by a code change.
- **A local model is keyless by default and keyed by the ordinary path when it must be.** Nothing new
is invented for the local server's credential: it either has none, or mints one the way every
provider does.
- **The vendor path is untouched.** Adapters, the refresh carve-out, and usage
([ADR 0054](0054-model-usage-is-recorded-at-two-grains.md)) are the record answer's business; a
node-answer neither uses nor changes them. A local model that exposes usage would serve it as facts,
not as an adapter's usage verb.
- **The two answer passes meet in one place, and must be reconciled there.** The mesh-scope limitation
is the single point where a node-answer and a record-answer to the one provision can collide; it is
named, and its fix is a resolver change, not a per-module workaround.
## References
- [ADR 0024](0024-model-access-is-a-provision.md) — model access is a provision; this decides a node
may answer it, not only a record
- [ADR 0050](0050-model-access-is-vendor-agnostic.md) — model access is vendor-agnostic; the record
answer and its adapters, which the node answer sits beside and does not use
- [ADR 0054](0054-model-usage-is-recorded-at-two-grains.md) — model usage; a vendor's business on the
record path, served as facts (if at all) on the node path
- [ADR 0008](0008-a-context-owns-its-store.md) — a context owns its store; a licence is a record in one,
a node-answer needs none
- [03-DESIGN/01-to-be/14-model-access.md](../03-DESIGN/01-to-be/14-model-access.md) — the model-access
design, which this extends with the node answer