fix(llm): size unknown models and refuse a section that cannot be served

Three defects surfaced while driving the Models page.

A hand-declared model needed an explicit contextWindow and maxTokens,
but a provider listing usually returns ids and nothing else — so the
page happily wrote a profile the adapter then rejected, which took the
whole namespace down silently. Capacities now fall back to the route's
`defaultContextWindow` (262,144) and `defaultMaxTokens` (32,768). Both
are guesses by construction, which is why they are route fields a
deployment corrects once rather than constants buried in the adapter;
the fallback sizes the model and never becomes a per-request cap.

That silent failure was the second defect. A schema-valid profile the
adapter could not serve was stored and only rejected later, disabling
every route in the namespace with nothing said. `dsh-settings` gains an
optional `validate` on registration — a check for what a schema cannot
express — and `llm-pi-ai` refuses an unserviceable section at the write
that produced it. A stored section that fails keeps the namespace's last
good value, as a schema failure already did, so an externally edited
document still cannot strand the owner. The plugin's own last-good
fallback goes with it: nothing reaching it can fail any more.

Third, a model with no reasoning metadata advertised the single level
`off`, which pi-ai translates to *omitting* the reasoning option — the
same request naming no effort produces. Selecting it disabled nothing,
so a provider whose default is to think kept thinking with `off` shown
as selected. Such a model now reports no reasoning capability at all,
which is the seam's way of saying the control is unavailable, and the
per-model `reasoning` flag is gone: without a thinkingLevelMap to spell
levels it could only invent them.

The protocol table narrows to the three a hand-declared route reaches
today, most-reached first so a surface offering a choice defaults to the
one gateways actually speak.
This commit is contained in:
Yichen Jiang
2026-08-04 13:32:56 +08:00
parent 4c80cab108
commit f376ee23d1
22 changed files with 368 additions and 108 deletions
+20 -16
View File
@@ -48,7 +48,7 @@ import type { AdapterRegistrationHandle, DirectoryRegistrationHandle, LlmConfigu
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
import { PiAiAdapter } from './adapter.ts'
import { catalogProviderIds } from './catalog.ts'
import { Config, resolveProfiles } from './config.ts'
import { assertServiceable, Config, resolveProfiles } from './config.ts'
import type { ResolvedPiAiProviderProfile } from './config.ts'
export { PiAiAdapter } from './adapter.ts'
@@ -98,24 +98,24 @@ export function apply(ctx: Context, config: Config): void {
let current: () => Config = () => config
let lastRaw: Config | undefined
let lastGood: ReadonlyMap<string, ResolvedPiAiProviderProfile> | undefined
/**
* The resolved profiles for the current configuration, memoized by the raw
* snapshot's identity — which is also what makes the adapter's own snapshot
* stable across operations that observe no change.
*
* No fallback for an unserviceable snapshot lives here: the section schema
* resolves the whole profile set, so a write that could not be served is
* refused where it is written, and the settings seam keeps a namespace's
* last good value for a stored section that fails. Anything reaching this
* point has already resolved once.
*/
const profiles = (): ReadonlyMap<string, ResolvedPiAiProviderProfile> => {
const raw = current()
if (raw === lastRaw && lastGood !== undefined) return lastGood
try {
const next = resolveProfiles(raw.providers)
lastRaw = raw
lastGood = next
return next
} catch (error) {
// Static composition resolves before anything registers, so this branch
// only sees a live settings snapshot failing catalog or bound checks:
// keep serving the last good profiles and say so once per bad snapshot.
if (lastGood === undefined) throw error
lastRaw = raw
ctx.logger.error('llm-pi-ai: keeping the last good profiles after an invalid settings section')
ctx.logger.error(error)
return lastGood
}
const next = resolveProfiles(raw.providers)
lastRaw = raw
lastGood = next
return next
}
profiles()
@@ -201,6 +201,10 @@ export function apply(ctx: Context, config: Config): void {
ensureRegistrationFacts()
installSettingsSection(ctx, NS, Config, config, {
// Refuse an unserviceable section where it is written: without this a
// schema-valid profile the adapter cannot serve would be stored and then
// silently disable every route in this namespace.
validate: assertServiceable,
setSource: (source) => {
current = source
},