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

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/settings.md
settings.md: 1cabfae5d8dc72a9cd79341d250ee79820693872
settings.zh.md: d63a1384646fa38199e7d65e9f0504f0440597be
settings.md: 08f99b5e65ac4a57cdfff3323c0d9148d379bed8
settings.zh.md: 08903810b44c293944950a8f5ae2710f45678d60

View File

@@ -17,7 +17,7 @@ type SettingsNamespace = Branded<'SettingsNamespace'>
## Registration
Registration binds a schemastery schema to a namespace on the calling plugin's fiber — disposing that fiber removes the namespace and its observers. The options carry the composition layer and the owner's effect timing.
Registration binds a schemastery schema to a namespace on the calling plugin's fiber — disposing that fiber removes the namespace and its observers. The options carry the composition layer, the owner's effect timing, and an optional check for what the schema cannot express.
```ts type-equiv
/** Registration options beyond the namespace schema. */
@@ -26,9 +26,28 @@ interface SettingsRegisterOptions<T> {
base?: Partial<T>
/** Owner's effect timing, surfaced to configuration UIs; defaults to `live`. */
applies?: SettingsApplies
/**
* Reject a resolved section the owner could not act on, for constraints its
* schema cannot express — a cross-field requirement, or one field's validity
* depending on another's. Throwing here refuses the *write* that produced the
* value, so a caller learns at `update`/`replace`/`mutate` instead of storing
* something that would silently disable the owner.
*
* Kept separate from the schema because the schema is also what a
* configuration surface renders and what an absent section resolves through;
* folding a cross-field check into it would change both.
*
* A stored section that fails this keeps the namespace's last good value and
* warns, exactly as a schema failure does, so an externally edited document
* can never strand the owner.
* @param value - the resolved section, schema-valid by construction.
*/
validate?: (value: T) => void
}
```
`validate` runs after the schema admits a value, so it sees defaults and the composition base exactly as the owner will. `dsh-llm-pi-ai` uses it to refuse a provider profile it could not serve at the write that produced it, rather than storing one that would disable every route in its namespace.
`applies` is a UI hint, not a mechanism: a `restart` owner simply never watches, so its value is read once at construction and configuration surfaces can badge the pending change.
```ts type-equiv

View File

@@ -17,7 +17,7 @@ type SettingsNamespace = Branded<'SettingsNamespace'>
## 注册
注册把 schemastery schema 绑定到调用方插件 fiber 上的 namespace——dispose 该 fiber 即移除 namespace 及其观察者。options 携带组合层owner 的生效时机。
注册把 schemastery schema 绑定到调用方插件 fiber 上的 namespace——dispose 该 fiber 即移除 namespace 及其观察者。options 携带组合层owner 的生效时机,以及一个可选的、用于校验 schema 表达不了的约束的钩子
```ts type-equiv
/** Registration options beyond the namespace schema. */
@@ -26,9 +26,28 @@ interface SettingsRegisterOptions<T> {
base?: Partial<T>
/** Owner's effect timing, surfaced to configuration UIs; defaults to `live`. */
applies?: SettingsApplies
/**
* Reject a resolved section the owner could not act on, for constraints its
* schema cannot express — a cross-field requirement, or one field's validity
* depending on another's. Throwing here refuses the *write* that produced the
* value, so a caller learns at `update`/`replace`/`mutate` instead of storing
* something that would silently disable the owner.
*
* Kept separate from the schema because the schema is also what a
* configuration surface renders and what an absent section resolves through;
* folding a cross-field check into it would change both.
*
* A stored section that fails this keeps the namespace's last good value and
* warns, exactly as a schema failure does, so an externally edited document
* can never strand the owner.
* @param value - the resolved section, schema-valid by construction.
*/
validate?: (value: T) => void
}
```
`validate` 在 schema 接纳该值之后运行,因此它看到的默认值与组合 base 与 owner 将看到的完全一致。`dsh-llm-pi-ai` 用它在写入处拒绝自己无法服务的提供方 profile而不是先存下来、再让该 namespace 下每条路由失效。
`applies` 是 UI 提示而非机制:`restart` 的 owner 只是从不 watch其值在构造期读取一次配置界面可为待生效变更加标。
```ts type-equiv