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:
@@ -2563,14 +2563,12 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const active = new Set(registered.map(provider => provider.id))
|
||||
const directory = ctx.llm.listConfigurableProviders()
|
||||
const declared = new Set(directory.map(entry => entry.provider))
|
||||
const discoverable = new Set(ctx.llm.listModelDiscoveryNamespaces())
|
||||
const views = directory.map(entry => ({
|
||||
provider: entry.provider,
|
||||
displayName: entry.displayName,
|
||||
settingsNs: entry.settingsNs,
|
||||
settingsPath: [...entry.settingsPath],
|
||||
active: active.has(entry.provider),
|
||||
supportsDiscovery: discoverable.has(entry.settingsNs),
|
||||
}))
|
||||
// Routes registered without a directory declaration still appear —
|
||||
// they exist and serve models — just with no settings address.
|
||||
@@ -2582,7 +2580,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
settingsNs: '',
|
||||
settingsPath: [],
|
||||
active: true,
|
||||
supportsDiscovery: false,
|
||||
})
|
||||
}
|
||||
return Promise.resolve(ok(request, { providers: views }))
|
||||
|
||||
@@ -16,7 +16,6 @@ export const configurableProviderViewSchema = z.object({
|
||||
settingsNs: z.string(),
|
||||
settingsPath: z.array(z.string()),
|
||||
active: z.boolean(),
|
||||
supportsDiscovery: z.boolean(),
|
||||
}) satisfies z.ZodType<Wire<ConfigurableProviderView>>
|
||||
|
||||
/** llm.providers request payload. */
|
||||
|
||||
@@ -22,12 +22,6 @@ export interface ConfigurableProviderView {
|
||||
settingsPath: string[]
|
||||
/** Whether the route is currently registered (its models are requestable). */
|
||||
active: boolean
|
||||
/**
|
||||
* Whether `llm.discoverModels` can answer for this entry's namespace. A
|
||||
* surface offers the action only where it works instead of naming an adapter
|
||||
* family it would have to hardcode.
|
||||
*/
|
||||
supportsDiscovery: boolean
|
||||
}
|
||||
|
||||
/** Llm-domain unary methods (the map keys llm.* of RpcMethodMap). */
|
||||
|
||||
@@ -529,11 +529,11 @@ describe('llm domain', () => {
|
||||
const api = createApiProxy(ctx, DEFAULTS)
|
||||
const value = expectOk(await api.llm.providers(request({})))
|
||||
expect(value.providers).toEqual([
|
||||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true, supportsDiscovery: false },
|
||||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: false, supportsDiscovery: true },
|
||||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true },
|
||||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: false },
|
||||
// An undeclared live route has no settings address, so nothing can be
|
||||
// interrogated on its behalf either.
|
||||
{ provider: 'undeclared', displayName: 'Undeclared', settingsNs: '', settingsPath: [], active: true, supportsDiscovery: false },
|
||||
{ provider: 'undeclared', displayName: 'Undeclared', settingsNs: '', settingsPath: [], active: true },
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -677,7 +677,6 @@ describe('config unary surface', () => {
|
||||
settingsNs: 'llm-pi-ai',
|
||||
settingsPath: ['providers', 'openai'],
|
||||
active: false,
|
||||
supportsDiscovery: true,
|
||||
}
|
||||
const group = { id: 'deepseek-official', name: 'DeepSeek', models: [{ id: 'deepseek-v4-flash', name: 'Flash' }] }
|
||||
const api = scriptedApi({
|
||||
|
||||
Reference in New Issue
Block a user