feat(llm): interrogate a draft provider endpoint for its models

Once a pi-ai route became a declaration rather than a catalog lookup,
adding an OpenAI-compatible gateway meant knowing its model ids up
front. Most such endpoints publish that list at `GET /models`, but no
seam operation could ask: every one is keyed by a registered provider
route, and the provider being added has no route, no stored profile,
and no stored credential — the endpoint and key are values in a form.

Interrogation is therefore keyed by settings namespace, which a
configuration surface already holds from the configurable-provider
directory. `registerModelDiscovery` offers it per namespace,
`discoverModels` asks, and the request carries the draft itself. The
reply is candidates, not a catalog: every field but the id is optional
because most listings disclose nothing else, and adopting one is a
settings write like any other. Nothing here reads or writes settings or
credentials, so `settings.yaml` still decides what a route serves.

`llm.discoverModels` carries the same draft over the wire. Its apiKey is
the third and last payload a secret may ride, and it is never stored,
logged, or echoed; every refusal folds into `model-discovery-failed`,
naming the endpoint asked but never the credential offered.

The pi-ai side is a plain GET for OpenAI-compatible protocols only —
their listing shape is the one gateways, self-hosted servers, and the
official endpoints agree on. Others say so, sending the user to
hand-entry rather than reporting a guessed shape as an empty provider.
The reply is read under a four-megabyte ceiling held on the bytes
actually received, because the endpoint is a URL the user typed.
This commit is contained in:
Yichen Jiang
2026-08-04 10:14:46 +08:00
parent 9948a37cbc
commit ecee93ec26
34 changed files with 985 additions and 18 deletions

View File

@@ -40,4 +40,38 @@ export interface LlmApi {
* failures ride `failures` without failing the sound groups.
*/
models(request: RpcRequest<{}>): Promise<RpcResponse<{ groups: ModelProviderGroup[]; failures: ModelCatalogFailure[] }>>
/**
* Interrogate a provider endpoint the configuration surface is still
* drafting, and return the models it advertises for the user to adopt.
*
* The payload is the draft, not a stored route: `settingsNs` selects the
* adapter family that knows how to read the listing, and the endpoint,
* protocol, and key come from the form. Nothing is written — the reply is
* candidates, and only a later `settings.mutate` decides what a route
* serves. `apiKey` is therefore accepted here but never stored, logged, or
* echoed back; a provider whose key is already stored omits it and the
* endpoint answers unauthenticated or refuses.
*/
discoverModels(
request: RpcRequest<{
settingsNs: string
baseURL: string
api?: string
apiKey?: string
}>,
signal?: AbortSignal,
): Promise<RpcResponse<{ models: DiscoveredModelView[] }>>
}
/** Wire view of one model an interrogated endpoint advertises. */
export interface DiscoveredModelView {
/** Model id the endpoint accepts. */
id: string
/** Human-readable name when the endpoint supplies one. */
name?: string
/** Maximum combined request and response context, when disclosed. */
contextWindow?: number
/** Maximum output tokens, when disclosed. */
maxTokens?: number
}