fix(coding-agent): clamp thinking level to model capabilities

- setThinkingLevel() now clamps xhigh to high when model doesn't support it
- Model changes automatically re-clamp the current thinking level
- Fixed /model command to use session.setModel() instead of agent.setModel()
- Footer and editor border color update after model/thinking changes

Closes #253
This commit is contained in:
Mario Zechner 2025-12-20 09:50:56 +01:00
parent c712901eb2
commit b7c3cf9436
3 changed files with 42 additions and 20 deletions

View file

@ -543,6 +543,9 @@ export class AgentSession {
this.agent.setModel(model);
this.sessionManager.saveModelChange(model.provider, model.id);
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
// Re-clamp thinking level for new model's capabilities
this.setThinkingLevel(this.thinkingLevel);
}
/**
@ -580,13 +583,10 @@ export class AgentSession {
this.sessionManager.saveModelChange(next.model.provider, next.model.id);
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
// Apply thinking level (silently use "off" if not supported)
const effectiveThinking = next.model.reasoning ? next.thinkingLevel : "off";
this.agent.setThinkingLevel(effectiveThinking);
this.sessionManager.saveThinkingLevelChange(effectiveThinking);
this.settingsManager.setDefaultThinkingLevel(effectiveThinking);
// Apply thinking level (setThinkingLevel clamps to model capabilities)
this.setThinkingLevel(next.thinkingLevel);
return { model: next.model, thinkingLevel: effectiveThinking, isScoped: true };
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
}
private async _cycleAvailableModel(): Promise<ModelCycleResult | null> {
@ -612,6 +612,9 @@ export class AgentSession {
this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
// Re-clamp thinking level for new model's capabilities
this.setThinkingLevel(this.thinkingLevel);
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
}
@ -630,11 +633,16 @@ export class AgentSession {
/**
* Set thinking level.
* Silently uses "off" if model doesn't support thinking.
* Clamps to model capabilities: "off" if no reasoning, "high" if xhigh unsupported.
* Saves to session and settings.
*/
setThinkingLevel(level: ThinkingLevel): void {
const effectiveLevel = this.supportsThinking() ? level : "off";
let effectiveLevel = level;
if (!this.supportsThinking()) {
effectiveLevel = "off";
} else if (level === "xhigh" && !this.supportsXhighThinking()) {
effectiveLevel = "high";
}
this.agent.setThinkingLevel(effectiveLevel);
this.sessionManager.saveThinkingLevelChange(effectiveLevel);
this.settingsManager.setDefaultThinkingLevel(effectiveLevel);
@ -1183,10 +1191,10 @@ export class AgentSession {
}
}
// Restore thinking level if saved
// Restore thinking level if saved (setThinkingLevel clamps to model capabilities)
const savedThinking = this.sessionManager.loadThinkingLevel();
if (savedThinking) {
this.agent.setThinkingLevel(savedThinking as ThinkingLevel);
this.setThinkingLevel(savedThinking as ThinkingLevel);
}
this._reconnectToAgent();

View file

@ -3,6 +3,7 @@
*/
import { Agent, type Attachment, ProviderTransport, type ThinkingLevel } from "@mariozechner/pi-agent-core";
import { supportsXhigh } from "@mariozechner/pi-ai";
import chalk from "chalk";
import { type Args, parseArgs, printHelp } from "./cli/args.js";
import { processFileArguments } from "./cli/file-processor.js";
@ -294,6 +295,15 @@ export async function main(args: string[]) {
initialThinking = parsed.thinking;
}
// Clamp thinking level to model capabilities
if (initialModel) {
if (!initialModel.reasoning) {
initialThinking = "off";
} else if (initialThinking === "xhigh" && !supportsXhigh(initialModel)) {
initialThinking = "high";
}
}
// Determine which tools to use
let selectedTools = parsed.tools ? parsed.tools.map((name) => allTools[name]) : codingTools;
@ -378,11 +388,6 @@ export async function main(args: string[]) {
}),
});
// If initial thinking was requested but model doesn't support it, reset to off
if (initialThinking !== "off" && initialModel && !initialModel.reasoning) {
agent.setThinkingLevel("off");
}
// Load previous messages if continuing, resuming, or using --session
if (parsed.continue || parsed.resume || parsed.session) {
const messages = sessionManager.loadMessages();

View file

@ -1172,6 +1172,7 @@ export class InteractiveMode {
if (newLevel === null) {
this.showStatus("Current model does not support thinking");
} else {
this.footer.updateState(this.session.state);
this.updateEditorBorderColor();
this.showStatus(`Thinking level: ${newLevel}`);
}
@ -1184,6 +1185,7 @@ export class InteractiveMode {
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
this.showStatus(msg);
} else {
this.footer.updateState(this.session.state);
this.updateEditorBorderColor();
const thinkingStr =
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
@ -1310,6 +1312,7 @@ export class InteractiveMode {
this.session.getAvailableThinkingLevels(),
(level) => {
this.session.setThinkingLevel(level);
this.footer.updateState(this.session.state);
this.updateEditorBorderColor();
done();
this.showStatus(`Thinking level: ${level}`);
@ -1379,11 +1382,17 @@ export class InteractiveMode {
this.ui,
this.session.model,
this.settingsManager,
(model) => {
this.agent.setModel(model);
this.sessionManager.saveModelChange(model.provider, model.id);
done();
this.showStatus(`Model: ${model.id}`);
async (model) => {
try {
await this.session.setModel(model);
this.footer.updateState(this.session.state);
this.updateEditorBorderColor();
done();
this.showStatus(`Model: ${model.id}`);
} catch (error) {
done();
this.showError(error instanceof Error ? error.message : String(error));
}
},
() => {
done();