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

@@ -718,6 +718,18 @@ export interface PiAiProviderProfile {
* unset fields from the installed model of the same id.
*/
models?: PiAiModelProfile[]
/**
* Context capacity for a model this route lists that neither the entry nor
* the installed catalog sizes (default 262,144). A guess by construction, so
* a deployment whose gateway serves smaller models corrects it here.
*/
defaultContextWindow?: number
/**
* Output capability for a model this route lists that neither the entry nor
* the installed catalog sizes (default 32,768). This sizes the model; it
* never becomes a per-request cap on its own.
*/
defaultMaxTokens?: number
/** Provider request headers; Harness attribution wins reserved names. */
headers?: Record<string, string>
/** Provider-neutral pi-ai reasoning level. */
@@ -748,18 +760,17 @@ export interface PiAiModelProfile {
contextWindow?: number
/**
* Maximum output tokens. Configuring one also makes it this model's
* per-request default; the value inherited from the installed catalog is the
* model's capability and never becomes a request default on its own.
* per-request default; a value inherited from the installed catalog, or the
* route's fallback, is the model's capability and never becomes a request
* default on its own.
*/
maxTokens?: number
/** Whether the model exposes reasoning; defaults to the catalog capability. */
reasoning?: boolean
}
```
Depends on: `CacheRetention` (`@earendil-works/pi-ai`) · `ModelThinkingLevel` (`@earendil-works/pi-ai`) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets` (`@earendil-works/pi-ai`) · `Transport` (`@earendil-works/pi-ai`)
Source: [`packages/llm/llm-pi-ai/src/config.ts:104`](../packages/llm/llm-pi-ai/src/config.ts)
Source: [`packages/llm/llm-pi-ai/src/config.ts:122`](../packages/llm/llm-pi-ai/src/config.ts)
## `@deepseek-ai/dsh-llm-replay`

View File

@@ -726,7 +726,7 @@ One registered namespace's RAW user section changed, whether or not the resolved
Types: [SettingsNamespace](../core-data-structures/settings.md)
Source: [`packages/settings/settings/src/index.ts:150`](../../packages/settings/settings/src/index.ts)
Source: [`packages/settings/settings/src/index.ts:167`](../../packages/settings/settings/src/index.ts)
### `settings/updated` — emit
@@ -753,7 +753,7 @@ Committed change to one registered namespace's resolved value. Emitted after the
Types: [SettingsNamespace](../core-data-structures/settings.md) · [SettingsUpdateSource](../core-data-structures/settings.md)
Source: [`packages/settings/settings/src/index.ts:137`](../../packages/settings/settings/src/index.ts)
Source: [`packages/settings/settings/src/index.ts:154`](../../packages/settings/settings/src/index.ts)
## `skills/*`

View File

@@ -1813,7 +1813,7 @@ async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevi
Types: [SettingsDescribeOptions](../core-data-structures/settings.md) · [SettingsDescriptor](../core-data-structures/settings.md) · [SettingsNamespace](../core-data-structures/settings.md) · [SettingsPathOp](../core-data-structures/settings.md) · [SettingsRegisterOptions](../core-data-structures/settings.md) · [SettingsScope](../core-data-structures/settings.md)
Source: [`packages/settings/settings/src/index.ts:365`](../../packages/settings/settings/src/index.ts)
Source: [`packages/settings/settings/src/index.ts:384`](../../packages/settings/settings/src/index.ts)
## `ctx.skills` — `SkillService`

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

View File

@@ -38,8 +38,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:81`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-title`](../packages/session-title/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:93`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`cli-demo`](../packages/examples/cli-demo), [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`session-projection-cache`](../packages/session-projection/session-projection-cache), [`session-telemetry`](../packages/telemetry/session-telemetry), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workspace-context`](../packages/context/workspace-context) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:102`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence), [`session-telemetry`](../packages/telemetry/session-telemetry) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:150`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:137`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:167`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:154`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:188`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:160`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:134`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |