fix: honor per-role thinking in modelRoles helpers

Fixes #186
This commit is contained in:
can1357
2026-03-09 14:16:37 +01:00
parent 36d2fed5d9
commit 4ed4cfb27c
19 changed files with 283 additions and 88 deletions
@@ -1,4 +1,4 @@
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
import { INTENT_FIELD, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { Markdown } from "@oh-my-pi/pi-tui";
import chalk from "chalk";
@@ -20,6 +20,7 @@ export interface CommitAgentInput {
cwd: string;
git: ControlledGit;
model: Model<Api>;
thinkingLevel?: ThinkingLevel;
settings: Settings;
modelRegistry: ModelRegistry;
authStorage: AuthStorage;
@@ -61,6 +62,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
modelRegistry: input.modelRegistry,
settings: input.settings,
model: input.model,
thinkingLevel: input.thinkingLevel,
systemPrompt,
customTools: tools,
enableLsp: false,
@@ -48,7 +48,12 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
const { model: primaryModel, apiKey: primaryApiKey } = primaryModelResult;
process.stdout.write(` └─ ${primaryModel.name}\n`);
const { model: agentModel } = await resolveSmolModel(settings, modelRegistry, primaryModel, primaryApiKey);
const { model: agentModel, thinkingLevel: agentThinkingLevel } = await resolveSmolModel(
settings,
modelRegistry,
primaryModel,
primaryApiKey,
);
if (stagedFiles.length === 0) {
process.stderr.write("No changes to commit.\n");
@@ -126,6 +131,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
cwd,
git,
model: agentModel,
thinkingLevel: agentThinkingLevel,
settings,
modelRegistry,
authStorage,
@@ -1,3 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { Type } from "@sinclair/typebox";
@@ -5,6 +6,7 @@ import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with
import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
import type { ChangelogCategory, ConventionalAnalysis } from "../../commit/types";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
const ConventionalAnalysisTool = {
@@ -49,6 +51,7 @@ const ConventionalAnalysisTool = {
export interface ConventionalAnalysisInput {
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
contextFiles?: Array<{ path: string; content: string }>;
userContext?: string;
typesDescription?: string;
@@ -64,6 +67,7 @@ export interface ConventionalAnalysisInput {
export async function generateConventionalAnalysis({
model,
apiKey,
thinkingLevel,
contextFiles,
userContext,
typesDescription,
@@ -89,7 +93,7 @@ export async function generateConventionalAnalysis({
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
tools: [ConventionalAnalysisTool],
},
{ apiKey, maxTokens: 2400 },
{ apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
);
return parseAnalysisFromResponse(response);
@@ -1,3 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { Type } from "@sinclair/typebox";
@@ -5,6 +6,7 @@ import summarySystemPrompt from "../../commit/prompts/summary-system.md" with {
import summaryUserPrompt from "../../commit/prompts/summary-user.md" with { type: "text" };
import type { CommitSummary } from "../../commit/types";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall } from "../utils";
const SummaryTool = {
@@ -18,6 +20,7 @@ const SummaryTool = {
export interface SummaryInput {
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
commitType: string;
scope: string | null;
details: string[];
@@ -32,6 +35,7 @@ export interface SummaryInput {
export async function generateSummary({
model,
apiKey,
thinkingLevel,
commitType,
scope,
details,
@@ -53,7 +57,7 @@ export async function generateSummary({
messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }],
tools: [SummaryTool],
},
{ apiKey, maxTokens: 200 },
{ apiKey, maxTokens: 200, reasoning: toReasoningEffort(thinkingLevel) },
);
return parseSummaryFromResponse(response, commitType, scope);
@@ -1,3 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { type TSchema, Type } from "@sinclair/typebox";
@@ -5,6 +6,7 @@ import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" wit
import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
import { CHANGELOG_CATEGORIES, type ChangelogCategory, type ChangelogGenerationResult } from "../../commit/types";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall, parseJsonPayload } from "../utils";
const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce<Record<ChangelogCategory, TSchema>>(
@@ -28,6 +30,7 @@ export const changelogTool = {
export interface ChangelogPromptInput {
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
changelogPath: string;
isPackageChangelog: boolean;
existingEntries?: string;
@@ -38,6 +41,7 @@ export interface ChangelogPromptInput {
export async function generateChangelogEntries({
model,
apiKey,
thinkingLevel,
changelogPath,
isPackageChangelog,
existingEntries,
@@ -58,7 +62,7 @@ export async function generateChangelogEntries({
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
tools: [changelogTool],
},
{ apiKey, maxTokens: 1200 },
{ apiKey, maxTokens: 1200, reasoning: toReasoningEffort(thinkingLevel) },
);
const parsed = parseChangelogResponse(response);
@@ -1,4 +1,5 @@
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { logger } from "@oh-my-pi/pi-utils";
import type { ControlledGit } from "../../commit/git";
@@ -16,6 +17,7 @@ export interface ChangelogFlowInput {
cwd: string;
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
stagedFiles: string[];
dryRun: boolean;
maxDiffChars?: number;
@@ -42,6 +44,7 @@ export async function runChangelogFlow({
cwd,
model,
apiKey,
thinkingLevel,
stagedFiles,
dryRun,
maxDiffChars,
@@ -72,6 +75,7 @@ export async function runChangelogFlow({
const generated = await generateChangelogEntries({
model,
apiKey,
thinkingLevel,
changelogPath: boundary.changelogPath,
isPackageChangelog,
existingEntries: existingEntries || undefined,
@@ -1,3 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { $env } from "@oh-my-pi/pi-utils";
import { parseFileDiffs } from "../../commit/git/diff";
@@ -21,8 +22,10 @@ export interface MapReduceSettings {
export interface MapReduceInput {
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
smolModel: Model<Api>;
smolApiKey: string;
smolThinkingLevel?: ThinkingLevel;
diff: string;
stat: string;
scopeCandidates: string;
@@ -50,12 +53,14 @@ export async function runMapReduceAnalysis(input: MapReduceInput): Promise<Conve
const observations = await runMapPhase({
model: input.smolModel,
apiKey: input.smolApiKey,
thinkingLevel: input.smolThinkingLevel,
files: fileDiffs,
config: input.settings,
});
return runReducePhase({
model: input.model,
apiKey: input.apiKey,
thinkingLevel: input.thinkingLevel,
observations,
stat: input.stat,
scopeCandidates: input.scopeCandidates,
@@ -1,3 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
import { completeSimple } from "@oh-my-pi/pi-ai";
import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
@@ -5,6 +6,7 @@ import fileObserverUserPrompt from "../../commit/prompts/file-observer-user.md"
import type { FileDiff, FileObservation } from "../../commit/types";
import { isExcludedFile } from "../../commit/utils/exclusions";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { truncateToTokenLimit } from "./utils";
const MAX_FILE_TOKENS = 50_000;
@@ -17,6 +19,7 @@ const RETRY_BACKOFF_MS = 1000;
export interface MapPhaseInput {
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
files: FileDiff[];
config?: {
maxFileTokens?: number;
@@ -27,7 +30,7 @@ export interface MapPhaseInput {
};
}
export async function runMapPhase({ model, apiKey, files, config }: MapPhaseInput): Promise<FileObservation[]> {
export async function runMapPhase({ model, apiKey, thinkingLevel, files, config }: MapPhaseInput): Promise<FileObservation[]> {
const filtered = files.filter(file => !isExcludedFile(file.filename));
const systemPrompt = renderPromptTemplate(fileObserverSystemPrompt);
const maxFileTokens = config?.maxFileTokens ?? MAX_FILE_TOKENS;
@@ -58,7 +61,7 @@ export async function runMapPhase({ model, apiKey, files, config }: MapPhaseInpu
};
const response = await withRetry(
() => completeSimple(model, request, { apiKey, maxTokens: 400, signal: AbortSignal.timeout(timeoutMs) }),
() => completeSimple(model, request, { apiKey, maxTokens: 400, reasoning: toReasoningEffort(thinkingLevel), signal: AbortSignal.timeout(timeoutMs) }),
maxRetries,
retryBackoffMs,
);
@@ -1,3 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
import { Type } from "@sinclair/typebox";
@@ -5,6 +6,7 @@ import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { ty
import reduceUserPrompt from "../../commit/prompts/reduce-user.md" with { type: "text" };
import type { ChangelogCategory, ConventionalAnalysis, FileObservation } from "../../commit/types";
import { renderPromptTemplate } from "../../config/prompt-templates";
import { toReasoningEffort } from "../../thinking";
import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
const ReduceTool = {
@@ -49,6 +51,7 @@ const ReduceTool = {
export interface ReducePhaseInput {
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
observations: FileObservation[];
stat: string;
scopeCandidates: string;
@@ -58,6 +61,7 @@ export interface ReducePhaseInput {
export async function runReducePhase({
model,
apiKey,
thinkingLevel,
observations,
stat,
scopeCandidates,
@@ -76,7 +80,7 @@ export async function runReducePhase({
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
tools: [ReduceTool],
},
{ apiKey, maxTokens: 2400 },
{ apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
);
return parseAnalysisResponse(response);
@@ -1,14 +1,34 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { MODEL_ROLE_IDS } from "../config/model-registry";
import {
expandRoleAlias,
parseModelPattern,
resolveModelFromSettings,
resolveModelFromString,
} from "../config/model-resolver";
import { expandRoleAlias, parseModelPattern, resolveModelRoleValue } from "../config/model-resolver";
import type { Settings } from "../config/settings";
import MODEL_PRIO from "../priority.json" with { type: "json" };
export interface ResolvedCommitModel {
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
}
function resolveRoleSelection(
roles: readonly string[],
settings: Settings,
availableModels: Model<Api>[],
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
for (const role of roles) {
const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
settings,
matchPreferences,
});
if (resolved.model) {
return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
}
}
return undefined;
}
export async function resolvePrimaryModel(
override: string | undefined,
settings: Settings,
@@ -16,18 +36,13 @@ export async function resolvePrimaryModel(
getAvailable: () => Model<Api>[];
getApiKey: (model: Model<Api>) => Promise<string | undefined>;
},
): Promise<{ model: Model<Api>; apiKey: string }> {
): Promise<ResolvedCommitModel> {
const available = modelRegistry.getAvailable();
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
const roleOrder = ["commit", "smol", ...MODEL_ROLE_IDS] as const;
const model = override
? resolveModelFromString(expandRoleAlias(override, settings), available, matchPreferences)
: resolveModelFromSettings({
settings,
availableModels: available,
matchPreferences,
roleOrder,
});
const resolved = override
? resolveModelRoleValue(override, available, { settings, matchPreferences })
: resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available);
const model = resolved?.model;
if (!model) {
throw new Error("No model available for commit generation");
}
@@ -35,7 +50,7 @@ export async function resolvePrimaryModel(
if (!apiKey) {
throw new Error(`No API key available for model ${model.provider}/${model.id}`);
}
return { model, apiKey };
return { model, apiKey, thinkingLevel: resolved?.thinkingLevel };
}
export async function resolveSmolModel(
@@ -46,18 +61,15 @@ export async function resolveSmolModel(
},
fallbackModel: Model<Api>,
fallbackApiKey: string,
): Promise<{ model: Model<Api>; apiKey: string }> {
): Promise<ResolvedCommitModel> {
const available = modelRegistry.getAvailable();
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
const role = settings.getModelRole("smol");
const roleModel = role
? resolveModelFromString(expandRoleAlias(role, settings), available, matchPreferences)
: undefined;
if (roleModel) {
const apiKey = await modelRegistry.getApiKey(roleModel);
if (apiKey) return { model: roleModel, apiKey };
const resolvedSmol = resolveRoleSelection(["smol"], settings, available);
if (resolvedSmol?.model) {
const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
}
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
for (const pattern of MODEL_PRIO.smol) {
const candidate = parseModelPattern(pattern, available, matchPreferences).model;
if (!candidate) continue;
+14 -2
View File
@@ -1,4 +1,5 @@
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
import { ModelRegistry } from "../config/model-registry";
@@ -45,12 +46,12 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
const modelRegistry = new ModelRegistry(authStorage);
await modelRegistry.refresh();
const { model: primaryModel, apiKey: primaryApiKey } = await resolvePrimaryModel(
const { model: primaryModel, apiKey: primaryApiKey, thinkingLevel: primaryThinkingLevel } = await resolvePrimaryModel(
args.model,
settings,
modelRegistry,
);
const { model: smolModel, apiKey: smolApiKey } = await resolveSmolModel(
const { model: smolModel, apiKey: smolApiKey, thinkingLevel: smolThinkingLevel } = await resolveSmolModel(
settings,
modelRegistry,
primaryModel,
@@ -75,6 +76,7 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
cwd,
model: primaryModel,
apiKey: primaryApiKey,
thinkingLevel: primaryThinkingLevel,
stagedFiles,
dryRun: args.dryRun,
maxDiffChars: commitSettings.changelogMaxDiffChars,
@@ -101,8 +103,10 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
userContext: args.context,
primaryModel,
primaryApiKey,
primaryThinkingLevel,
smolModel,
smolApiKey,
smolThinkingLevel,
commitSettings,
});
@@ -116,6 +120,7 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
stat,
model: primaryModel,
apiKey: primaryApiKey,
thinkingLevel: primaryThinkingLevel,
userContext: args.context,
});
@@ -144,8 +149,10 @@ async function generateAnalysis(input: {
userContext?: string;
primaryModel: Model<Api>;
primaryApiKey: string;
primaryThinkingLevel?: ThinkingLevel;
smolModel: Model<Api>;
smolApiKey: string;
smolThinkingLevel?: ThinkingLevel;
commitSettings: {
mapReduceEnabled: boolean;
mapReduceMinFiles: number;
@@ -166,8 +173,10 @@ async function generateAnalysis(input: {
return runMapReduceAnalysis({
model: input.primaryModel,
apiKey: input.primaryApiKey,
thinkingLevel: input.primaryThinkingLevel,
smolModel: input.smolModel,
smolApiKey: input.smolApiKey,
smolThinkingLevel: input.smolThinkingLevel,
diff: input.diff,
stat: input.stat,
scopeCandidates: input.scopeCandidates,
@@ -185,6 +194,7 @@ async function generateAnalysis(input: {
return generateConventionalAnalysis({
model: input.primaryModel,
apiKey: input.primaryApiKey,
thinkingLevel: input.primaryThinkingLevel,
contextFiles: input.contextFiles,
userContext: input.userContext,
typesDescription: TYPES_DESCRIPTION,
@@ -200,6 +210,7 @@ async function generateSummaryWithRetry(input: {
stat: string;
model: Model<Api>;
apiKey: string;
thinkingLevel?: ThinkingLevel;
userContext?: string;
}): Promise<{ summary: string }> {
let context = input.userContext;
@@ -207,6 +218,7 @@ async function generateSummaryWithRetry(input: {
const result = await generateSummary({
model: input.model,
apiKey: input.apiKey,
thinkingLevel: input.thinkingLevel,
commitType: input.analysis.type,
scope: input.analysis.scope,
details: input.analysis.details.map(detail => detail.text),