fix(coding-agent): keep the auto ceiling out of the model clamp

Capping the classifier result before `clampAutoThinkingEffort` was not
enough. The clamp seeds `chosen` with `pool[0]`, so a sparse ladder whose
tiers all sit above the request snaps upward instead of down: on
`thinking.efforts: ["max"]` an `xhigh` request returned `max`, letting the
default setting bill the top tier with no opt-in. The same upward snap made
`resolveProvisionalAutoLevel` hand back `max`, breaking the invariant its
doc comment had just claimed.

`clampAutoThinkingEffort` now takes the ceiling and intersects it with the
model's supported tiers, returning `undefined` when nothing is eligible so
auto leaves the current level alone instead of billing an excluded tier.
The classifier passes its configured ceiling and the provisional level
passes XHigh.
This commit is contained in:
Éverton Toffanetto
2026-07-26 04:04:07 -03:00
parent e3e4fd3a31
commit 3933bbd0f4
3 changed files with 53 additions and 12 deletions
@@ -15,7 +15,6 @@
* the caller falls back to a concrete level and continues the turn.
*/
import { type AssistantMessage, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
import { THINKING_EFFORTS } from "@oh-my-pi/pi-catalog/effort";
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
import { prompt } from "@oh-my-pi/pi-utils";
@@ -96,10 +95,9 @@ export async function classifyDifficulty(
backend === ONLINE_AUTO_THINKING_MODEL_KEY
? await classifyOnline(input, deps, ceiling)
: await classifyLocal(input, backend, deps);
// Policy clamp before the model clamp: a hallucinated `max` must not cross a
// ceiling the user did not opt into, even on a model that supports the tier.
const capped = THINKING_EFFORTS.indexOf(effort) > THINKING_EFFORTS.indexOf(ceiling) ? ceiling : effort;
return clampAutoThinkingEffort(deps.model, capped);
// The ceiling goes into the clamp itself: capping the request alone is not
// enough, because a sparse ladder snaps an excluded request back up.
return clampAutoThinkingEffort(deps.model, effort, ceiling);
}
async function classifyOnline(input: string, deps: ClassifyDifficultyDeps, ceiling: Effort): Promise<Effort> {