fix(host): pin model discovery to loopback and drop its unread wire field
llm.discoverModels was reachable from any declared trusted host. The method takes a caller-supplied baseURL and makes the host issue a GET to it, then reports the status or the parsed body — so on a LAN deployment an anonymous caller had a probe for whatever the host can reach and the browser cannot, plus a path that carries a draft credential. The PRIVILEGED_METHODS doc already states the rule this broke: trustedHosts is a DNS-rebinding fence, not authentication, so the configuration plane stays loopback-same-origin. It is in that set now, asserted both against the hand-built fence and over real HTTP beside the catalog reads that deliberately stay reachable. supportsDiscovery and listModelDiscoveryNamespaces are gone. The field was required on the wire and read by nobody: its own contract said a surface should offer the action "instead of naming an adapter family it would have to hardcode", while the surface hardcodes llm-pi-ai in two places and gates the button on whether there is anything to probe. Its shape did not fit the second caller either — the create card has no row to read a per-row field from. Keeping a required field alive for a consumer that may never arrive costs every producer and fixture a value nobody consults, which is exactly how the fixtures drifted. The registry that fed it had no other production consumer, so registration and disposal are now observed through the offer itself. The Agent Note claimed the key is never logged, which the wire schema beside it already contradicts, and predated both the provider field and the catalog-answer path. The two new public types pointed at core.md without a type-equiv block or manifest entry, so the generated service catalog named documentation that did not exist.
This commit is contained in:
@@ -79,6 +79,16 @@ Supported profile fields are `apiKey`, `apiKeyEnv`, `displayName`, `api`, `baseU
|
||||
|
||||
The adapter forces pi-ai's SDK `maxRetries` to zero so one `stream()` call makes one provider request. The removed profile fields `maxRetries` and `maxRetryDelayMs` fail load instead of silently multiplying or hiding the separately composed agent-level retry budget. Idle expiry aborts the SDK's stable request signal and surfaces `TIMEOUT`; an earlier caller abort remains `ABORTED`.
|
||||
|
||||
## Endpoint interrogation
|
||||
|
||||
The plugin offers `ctx.llm.registerModelDiscovery('llm-pi-ai', …)`, which answers "which models can this provider serve?" for a route a configuration surface is editing or drafting. It is deliberately *not* a catalog refresh: nothing is stored, and the reply is candidates the surface offers for adoption. `settings.yaml` remains the only thing that decides what a route serves.
|
||||
|
||||
A request naming a route the **installed catalog ships is answered from that catalog**, with no network call: pi-ai's registry is the authoritative list for its own providers, and it carries the context windows and output caps a listing endpoint would not disclose. Such a route needs no `baseURL` at all. Only a route the catalog does not describe — a gateway, a self-hosted server — is interrogated over the wire, and one that names no endpoint is told to set one or enter its models by hand.
|
||||
|
||||
Interrogation reads `openai-completions` and `openai-responses`, whose `GET /models` shape with bearer auth is the one a gateway, a self-hosted server, and the official endpoints all agree on. Azure is excluded despite its OpenAI lineage — it authenticates with an `api-key` header and requires an `api-version` query — and Codex uses OAuth; every other protocol answers `DISCOVERY_UNSUPPORTED` so the surface falls back to hand-entry instead of an authentication failure being reported as a provider with no models. The `baseURL` is treated as a prefix rather than a URL to resolve against, so a deployment path such as `https://gateway.example/openai/v1` keeps its segments.
|
||||
|
||||
Most listings disclose an id and nothing else; `context_window`/`context_length` and `max_output_tokens`/`max_tokens` are read when a gateway supplies them, entries without a usable id are skipped rather than failing the whole listing, and everything else the adopting surface still owes. The reply is read under a four-megabyte ceiling enforced on the bytes actually received — the endpoint is a URL the user typed, so a declared length is checked first but never trusted as the bound. An unreachable endpoint, a refused credential, a non-JSON body, and a body with no `data` array all fail with `DISCOVERY_FAILED` and a message naming the endpoint and, for a 401 or 403 alone, the credential. Cancellation during the body read surfaces as `ABORTED`, like a cancellation before the request went out.
|
||||
|
||||
## Provider/model routing and replay
|
||||
|
||||
Each resolution produces one **immutable** snapshot — the profiles plus a `createModels()` collection holding the `Provider` each route built — and every operation captures a whole snapshot before its first `await`. A configuration change builds a *new* collection rather than mutating the one in use: `Models.streamSimple()` resolves its provider lazily, when the stream is first consumed, which is after the credential await, so a mutated collection would let a request that started under one configuration finish under another or fail on a provider that no longer exists. This is what makes the seam's per-step call freeze (`llm.prepareCall()`) hold end to end — switching models mid-reply takes effect on the next step, never inside the one in flight. Requests reach their provider through `Models.streamSimple()`. A catalog route that keeps its catalog protocol **reuses** the installed provider with its model list replaced, because that provider owns API implementations this package cannot reconstruct — Bedrock loads its Smithy module through a separate entry point — so rebuilding it from parts would silently narrow which providers work. Every other route is built by `createProvider()` over the protocol table behind `supportedProtocols()`, whose entries are the same factories pi-ai's own provider factories use.
|
||||
|
||||
Reference in New Issue
Block a user