fix(llm-pi-ai): describing a model must not fail on a bad profile level

`resolveModel` validated the profile's reasoning level against the exact
model and threw when it did not fit. That call builds the model catalog,
and the catalog build catches per PROVIDER — so one mis-set field took the
whole provider out of every picker behind a single error row, hiding even
the models that do support the level. Measured: `anthropic` set to `max`
threw for six of its eight models.

Describing what a model can do now reports an unusable profile level as no
default rather than throwing; the request path still refuses it, which is
where a bad configuration belongs. The existing spec asserted the old
throw and now asserts both halves of that split.

Known gap, left deliberately: a model that cannot take the route's level
still fails its first request while the picker shows 「Default」 for it,
because the request path keeps using the profile level as the fallback.
Reaching that needs a hand-written `settings.yaml` — the Models page no
longer writes the field — and the error names the model and the level, so
selecting a supported level is a way out. Closing it properly means giving
`AgentOptions` a `reasoningEffort` so compositions without a model picker
keep an entry point, then dropping the provider-scoped field altogether;
that is its own change.
This commit is contained in:
Yichen Jiang
2026-08-07 18:07:15 +08:00
parent 2dc1406dfd
commit f3049e5663
5 changed files with 41 additions and 7 deletions
+13 -2
View File
@@ -372,13 +372,24 @@ describe('provider profile lifecycle', () => {
await expect(supported.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash'))
.resolves.toMatchObject({ reasoning: { defaultEffort: ReasoningEffortId('max') } })
// A profile level this model cannot take DESCRIBES as no default rather
// than failing: resolveModelInfo builds the model catalog, and a catalog
// that throws takes its whole provider out of every picker — one mis-set
// field would hide every model on the route, including the ones that do
// support the level. The request path below is where it is refused.
const unsupported = new Context()
await unsupported.plugin(LlmService)
await unsupported.plugin(LlmPiAi, {
providers: { deepseek: { reasoning: 'medium' } },
})
await expect(unsupported.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash'))
.rejects.toMatchObject({ code: 'UNSUPPORTED_REASONING_EFFORT' })
const described = await unsupported.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash')
expect(described.reasoning?.defaultEffort).toBeUndefined()
expect(described.reasoning?.efforts.length).toBeGreaterThan(0)
await expect(assemble(unsupported, {
provider: 'deepseek', model: 'deepseek-v4-flash', messages: [],
})).resolves.toMatchObject({
finish: { kind: 'error', failure: { code: 'UNSUPPORTED_REASONING_EFFORT' } },
})
const disabled = new Context()
await disabled.plugin(LlmService)