@oh-my-pi/pi-coding-agent 10.6.0 → 10.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/package.json +7 -7
- package/src/commit/model-selection.ts +18 -39
- package/src/config/model-registry.ts +19 -0
- package/src/config/model-resolver.ts +92 -1
- package/src/config/settings.ts +23 -2
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +1 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +4 -4
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/main.ts +1 -2
- package/src/modes/components/model-selector.ts +43 -94
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +4 -4
- package/src/prompts/agents/explore.md +2 -2
- package/src/sdk.ts +2 -2
- package/src/session/agent-session.ts +26 -27
- package/src/session/compaction/branch-summarization.ts +1 -1
- package/src/session/compaction/compaction.ts +4 -4
- package/src/task/executor.ts +1 -35
- package/src/task/index.ts +1 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [10.6.1] - 2026-02-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `commit` model role for dedicated commit message generation
|
|
10
|
+
- Exported `resolveModelOverride` function from model resolver for external use
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Updated model role resolution to accept optional `roleOrder` parameter for custom role priority
|
|
15
|
+
- Made `tag` and `color` properties optional in `ModelRoleInfo` interface
|
|
16
|
+
- Updated model selector to safely handle roles without tag or color definitions
|
|
17
|
+
- Refactored role label display to use centralized `MODEL_ROLES` registry instead of hardcoded strings
|
|
18
|
+
- Refactored model role system to use centralized `MODEL_ROLES` registry with consistent tag, name, and color definitions
|
|
19
|
+
- Simplified model role resolution to use `MODEL_ROLE_IDS` array instead of hardcoded role checks
|
|
20
|
+
- Updated model selector to dynamically generate menu actions from `MODEL_ROLES` registry
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
|
|
24
|
+
- Removed support for `omp/` model role prefix; use `pi/` prefix instead
|
|
25
|
+
|
|
5
26
|
## [10.6.0] - 2026-02-04
|
|
6
27
|
### Breaking Changes
|
|
7
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "10.6.
|
|
3
|
+
"version": "10.6.2",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -80,12 +80,12 @@
|
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@mozilla/readability": "0.6.0",
|
|
83
|
-
"@oh-my-pi/omp-stats": "10.6.
|
|
84
|
-
"@oh-my-pi/pi-agent-core": "10.6.
|
|
85
|
-
"@oh-my-pi/pi-ai": "10.6.
|
|
86
|
-
"@oh-my-pi/pi-natives": "10.6.
|
|
87
|
-
"@oh-my-pi/pi-tui": "10.6.
|
|
88
|
-
"@oh-my-pi/pi-utils": "10.6.
|
|
83
|
+
"@oh-my-pi/omp-stats": "10.6.2",
|
|
84
|
+
"@oh-my-pi/pi-agent-core": "10.6.2",
|
|
85
|
+
"@oh-my-pi/pi-ai": "10.6.2",
|
|
86
|
+
"@oh-my-pi/pi-natives": "10.6.2",
|
|
87
|
+
"@oh-my-pi/pi-tui": "10.6.2",
|
|
88
|
+
"@oh-my-pi/pi-utils": "10.6.2",
|
|
89
89
|
"@openai/agents": "^0.4.5",
|
|
90
90
|
"@sinclair/typebox": "^0.34.48",
|
|
91
91
|
"ajv": "^8.17.1",
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import { MODEL_ROLE_IDS } from "../config/model-registry";
|
|
3
|
+
import {
|
|
4
|
+
expandRoleAlias,
|
|
5
|
+
parseModelPattern,
|
|
6
|
+
resolveModelFromSettings,
|
|
7
|
+
resolveModelFromString,
|
|
8
|
+
SMOL_MODEL_PRIORITY,
|
|
9
|
+
} from "../config/model-resolver";
|
|
3
10
|
import type { Settings } from "../config/settings";
|
|
4
11
|
|
|
5
12
|
export async function resolvePrimaryModel(
|
|
@@ -12,9 +19,15 @@ export async function resolvePrimaryModel(
|
|
|
12
19
|
): Promise<{ model: Model<Api>; apiKey: string }> {
|
|
13
20
|
const available = modelRegistry.getAvailable();
|
|
14
21
|
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
|
|
22
|
+
const roleOrder = ["commit", "smol", ...MODEL_ROLE_IDS] as const;
|
|
15
23
|
const model = override
|
|
16
24
|
? resolveModelFromString(expandRoleAlias(override, settings), available, matchPreferences)
|
|
17
|
-
: resolveModelFromSettings(
|
|
25
|
+
: resolveModelFromSettings({
|
|
26
|
+
settings,
|
|
27
|
+
availableModels: available,
|
|
28
|
+
matchPreferences,
|
|
29
|
+
roleOrder,
|
|
30
|
+
});
|
|
18
31
|
if (!model) {
|
|
19
32
|
throw new Error("No model available for commit generation");
|
|
20
33
|
}
|
|
@@ -37,7 +50,9 @@ export async function resolveSmolModel(
|
|
|
37
50
|
const available = modelRegistry.getAvailable();
|
|
38
51
|
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
|
|
39
52
|
const role = settings.getModelRole("smol");
|
|
40
|
-
const roleModel = role
|
|
53
|
+
const roleModel = role
|
|
54
|
+
? resolveModelFromString(expandRoleAlias(role, settings), available, matchPreferences)
|
|
55
|
+
: undefined;
|
|
41
56
|
if (roleModel) {
|
|
42
57
|
const apiKey = await modelRegistry.getApiKey(roleModel);
|
|
43
58
|
if (apiKey) return { model: roleModel, apiKey };
|
|
@@ -52,39 +67,3 @@ export async function resolveSmolModel(
|
|
|
52
67
|
|
|
53
68
|
return { model: fallbackModel, apiKey: fallbackApiKey };
|
|
54
69
|
}
|
|
55
|
-
|
|
56
|
-
function resolveModelFromSettings(
|
|
57
|
-
settings: Settings,
|
|
58
|
-
available: Model<Api>[],
|
|
59
|
-
matchPreferences: { usageOrder?: string[] },
|
|
60
|
-
): Model<Api> | undefined {
|
|
61
|
-
const roles = ["commit", "smol", "default"];
|
|
62
|
-
for (const role of roles) {
|
|
63
|
-
const configured = settings.getModelRole(role);
|
|
64
|
-
if (!configured) continue;
|
|
65
|
-
const resolved = resolveModelFromString(expandRoleAlias(configured, settings), available, matchPreferences);
|
|
66
|
-
if (resolved) return resolved;
|
|
67
|
-
}
|
|
68
|
-
return available[0];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function resolveModelFromString(
|
|
72
|
-
value: string,
|
|
73
|
-
available: Model<Api>[],
|
|
74
|
-
matchPreferences: { usageOrder?: string[] },
|
|
75
|
-
): Model<Api> | undefined {
|
|
76
|
-
const parsed = parseModelString(value);
|
|
77
|
-
if (parsed) {
|
|
78
|
-
return available.find(model => model.provider === parsed.provider && model.id === parsed.id);
|
|
79
|
-
}
|
|
80
|
-
return parseModelPattern(value, available, matchPreferences).model;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function expandRoleAlias(value: string, settings: Settings): string {
|
|
84
|
-
const lower = value.toLowerCase();
|
|
85
|
-
if (lower.startsWith("pi/") || lower.startsWith("omp/")) {
|
|
86
|
-
const role = lower.startsWith("pi/") ? value.slice(3) : value.slice(4);
|
|
87
|
-
return settings.getModelRole(role) ?? value;
|
|
88
|
-
}
|
|
89
|
-
return value;
|
|
90
|
-
}
|
|
@@ -15,8 +15,27 @@ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
|
15
15
|
import { type Static, Type } from "@sinclair/typebox";
|
|
16
16
|
import AjvModule from "ajv";
|
|
17
17
|
import { YAML } from "bun";
|
|
18
|
+
import type { ThemeColor } from "../modes/theme/theme";
|
|
18
19
|
import type { AuthStorage } from "../session/auth-storage";
|
|
19
20
|
|
|
21
|
+
export type ModelRole = "default" | "smol" | "slow" | "plan" | "commit";
|
|
22
|
+
|
|
23
|
+
export interface ModelRoleInfo {
|
|
24
|
+
tag?: string;
|
|
25
|
+
name: string;
|
|
26
|
+
color?: ThemeColor;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
30
|
+
default: { tag: "DEFAULT", name: "Default", color: "success" },
|
|
31
|
+
smol: { tag: "SMOL", name: "Fast", color: "warning" },
|
|
32
|
+
slow: { tag: "SLOW", name: "Thinking", color: "accent" },
|
|
33
|
+
plan: { tag: "PLAN", name: "Architect", color: "muted" },
|
|
34
|
+
commit: { name: "Commit" },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "plan", "commit"];
|
|
38
|
+
|
|
20
39
|
const Ajv = (AjvModule as any).default || AjvModule;
|
|
21
40
|
|
|
22
41
|
const OpenRouterRoutingSchema = Type.Object({
|
|
@@ -6,7 +6,8 @@ import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my
|
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { isValidThinkingLevel } from "../cli/args";
|
|
8
8
|
import { fuzzyMatch } from "../utils/fuzzy";
|
|
9
|
-
import type
|
|
9
|
+
import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
|
|
10
|
+
import type { Settings } from "./settings";
|
|
10
11
|
|
|
11
12
|
/** Default model IDs for each known provider */
|
|
12
13
|
export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|
@@ -310,6 +311,96 @@ export function parseModelPattern(
|
|
|
310
311
|
return parseModelPatternWithContext(pattern, availableModels, context);
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
const PREFIX_MODEL_ROLE = "pi/";
|
|
315
|
+
const DEFAULT_MODEL_ROLE = "default";
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check if a model override value is effectively the default role.
|
|
319
|
+
*/
|
|
320
|
+
export function isDefaultModelAlias(value: string | string[] | undefined): boolean {
|
|
321
|
+
if (!value) return true;
|
|
322
|
+
if (Array.isArray(value)) return value.every(entry => isDefaultModelAlias(entry));
|
|
323
|
+
if (value.startsWith(PREFIX_MODEL_ROLE)) {
|
|
324
|
+
value = value.slice(PREFIX_MODEL_ROLE.length);
|
|
325
|
+
}
|
|
326
|
+
return value === DEFAULT_MODEL_ROLE;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Expand a role alias like "pi/smol" to the configured model string.
|
|
331
|
+
*/
|
|
332
|
+
export function expandRoleAlias(value: string, settings?: Settings): string {
|
|
333
|
+
const normalized = value.trim();
|
|
334
|
+
if (normalized === "default") return settings?.getModelRole("default") ?? value;
|
|
335
|
+
if (!normalized.startsWith(PREFIX_MODEL_ROLE)) return value;
|
|
336
|
+
const role = normalized.slice(PREFIX_MODEL_ROLE.length) as ModelRole;
|
|
337
|
+
if (!MODEL_ROLE_IDS.includes(role)) return value;
|
|
338
|
+
return settings?.getModelRole(role) ?? value;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Resolve a model identifier or pattern to a Model instance.
|
|
343
|
+
*/
|
|
344
|
+
export function resolveModelFromString(
|
|
345
|
+
value: string,
|
|
346
|
+
available: Model<Api>[],
|
|
347
|
+
matchPreferences?: ModelMatchPreferences,
|
|
348
|
+
): Model<Api> | undefined {
|
|
349
|
+
const parsed = parseModelString(value);
|
|
350
|
+
if (parsed) {
|
|
351
|
+
return available.find(model => model.provider === parsed.provider && model.id === parsed.id);
|
|
352
|
+
}
|
|
353
|
+
return parseModelPattern(value, available, matchPreferences).model;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Resolve a model from configured roles, honoring order and overrides.
|
|
358
|
+
*/
|
|
359
|
+
export function resolveModelFromSettings(options: {
|
|
360
|
+
settings: Settings;
|
|
361
|
+
availableModels: Model<Api>[];
|
|
362
|
+
matchPreferences?: ModelMatchPreferences;
|
|
363
|
+
roleOrder?: readonly ModelRole[];
|
|
364
|
+
}): Model<Api> | undefined {
|
|
365
|
+
const { settings, availableModels, matchPreferences, roleOrder } = options;
|
|
366
|
+
const roles = roleOrder ?? MODEL_ROLE_IDS;
|
|
367
|
+
for (const role of roles) {
|
|
368
|
+
const configured = settings.getModelRole(role);
|
|
369
|
+
if (!configured) continue;
|
|
370
|
+
const resolved = resolveModelFromString(expandRoleAlias(configured, settings), availableModels, matchPreferences);
|
|
371
|
+
if (resolved) return resolved;
|
|
372
|
+
}
|
|
373
|
+
return availableModels[0];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Resolve a list of override patterns to the first matching model.
|
|
378
|
+
*/
|
|
379
|
+
export function resolveModelOverride(
|
|
380
|
+
modelPatterns: string[],
|
|
381
|
+
modelRegistry: ModelRegistry,
|
|
382
|
+
settings?: Settings,
|
|
383
|
+
): { model?: Model<Api>; thinkingLevel?: ThinkingLevel } {
|
|
384
|
+
if (modelPatterns.length === 0) return {};
|
|
385
|
+
const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
|
|
386
|
+
for (const pattern of modelPatterns) {
|
|
387
|
+
const normalized = pattern.trim();
|
|
388
|
+
if (!normalized || isDefaultModelAlias(normalized)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const effectivePattern = expandRoleAlias(pattern, settings);
|
|
392
|
+
const { model, thinkingLevel } = parseModelPattern(
|
|
393
|
+
effectivePattern,
|
|
394
|
+
modelRegistry.getAvailable(),
|
|
395
|
+
matchPreferences,
|
|
396
|
+
);
|
|
397
|
+
if (model) {
|
|
398
|
+
return { model, thinkingLevel: thinkingLevel !== "off" ? thinkingLevel : undefined };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
|
|
313
404
|
/**
|
|
314
405
|
* Resolve model patterns to actual Model objects with optional thinking levels
|
|
315
406
|
* Format: "pattern:level" where :level is optional
|
package/src/config/settings.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
+
import type { ModelRole } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
|
16
17
|
import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
|
|
17
18
|
import { YAML } from "bun";
|
|
18
19
|
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
|
|
@@ -387,7 +388,7 @@ export class Settings {
|
|
|
387
388
|
/**
|
|
388
389
|
* Set a model role (helper for modelRoles record).
|
|
389
390
|
*/
|
|
390
|
-
setModelRole(role: string, modelId: string): void {
|
|
391
|
+
setModelRole(role: ModelRole | string, modelId: string): void {
|
|
391
392
|
const current = this.get("modelRoles");
|
|
392
393
|
this.set("modelRoles", { ...current, [role]: modelId });
|
|
393
394
|
}
|
|
@@ -395,11 +396,31 @@ export class Settings {
|
|
|
395
396
|
/**
|
|
396
397
|
* Get a model role (helper for modelRoles record).
|
|
397
398
|
*/
|
|
398
|
-
getModelRole(role: string): string | undefined {
|
|
399
|
+
getModelRole(role: ModelRole | string): string | undefined {
|
|
399
400
|
const roles = this.get("modelRoles");
|
|
400
401
|
return roles[role];
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Get all model roles (helper for modelRoles record).
|
|
406
|
+
*/
|
|
407
|
+
getModelRoles(): ReadOnlyDict<string> {
|
|
408
|
+
return this.get("modelRoles");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/*
|
|
412
|
+
* Override model roles (helper for modelRoles record).
|
|
413
|
+
*/
|
|
414
|
+
overrideModelRoles(roles: ReadOnlyDict<string>): void {
|
|
415
|
+
const prev = this.get("modelRoles");
|
|
416
|
+
for (const [role, modelId] of Object.entries(roles)) {
|
|
417
|
+
if (modelId) {
|
|
418
|
+
prev[role] = modelId;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
this.set("modelRoles", prev);
|
|
422
|
+
}
|
|
423
|
+
|
|
403
424
|
/**
|
|
404
425
|
* Set disabled providers (for compatibility with discovery system).
|
|
405
426
|
*/
|
|
@@ -51,7 +51,7 @@ export interface CustomToolContext {
|
|
|
51
51
|
/** Model registry - use for API key resolution and model retrieval */
|
|
52
52
|
modelRegistry: ModelRegistry;
|
|
53
53
|
/** Current model (may be undefined if no model is selected yet) */
|
|
54
|
-
model: Model
|
|
54
|
+
model: Model | undefined;
|
|
55
55
|
/** Whether the agent is idle (not streaming) */
|
|
56
56
|
isIdle(): boolean;
|
|
57
57
|
/** Whether there are queued messages waiting to be processed */
|
|
@@ -206,7 +206,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
206
206
|
return this.runtime.setActiveTools(toolNames);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
setModel(model: Model
|
|
209
|
+
setModel(model: Model): Promise<boolean> {
|
|
210
210
|
return this.runtime.setModel(model);
|
|
211
211
|
}
|
|
212
212
|
|
|
@@ -112,7 +112,7 @@ export class ExtensionRunner {
|
|
|
112
112
|
private sessionManager: SessionManager;
|
|
113
113
|
private modelRegistry: ModelRegistry;
|
|
114
114
|
private errorListeners: Set<ExtensionErrorListener> = new Set();
|
|
115
|
-
private getModel: () => Model
|
|
115
|
+
private getModel: () => Model | undefined = () => undefined;
|
|
116
116
|
private isIdleFn: () => boolean = () => true;
|
|
117
117
|
private waitForIdleFn: () => Promise<void> = async () => {};
|
|
118
118
|
private abortFn: () => void = () => {};
|
|
@@ -162,7 +162,7 @@ export interface ExtensionContext {
|
|
|
162
162
|
/** Model registry for API key resolution */
|
|
163
163
|
modelRegistry: ModelRegistry;
|
|
164
164
|
/** Current model (may be undefined) */
|
|
165
|
-
model: Model
|
|
165
|
+
model: Model | undefined;
|
|
166
166
|
/** Whether the agent is idle (not streaming) */
|
|
167
167
|
isIdle(): boolean;
|
|
168
168
|
/** Abort the current agent operation */
|
|
@@ -776,7 +776,7 @@ export interface ExtensionAPI {
|
|
|
776
776
|
setActiveTools(toolNames: string[]): Promise<void>;
|
|
777
777
|
|
|
778
778
|
/** Set the current model. Returns false if no API key available. */
|
|
779
|
-
setModel(model: Model
|
|
779
|
+
setModel(model: Model): Promise<boolean>;
|
|
780
780
|
|
|
781
781
|
/** Get current thinking level. */
|
|
782
782
|
getThinkingLevel(): ThinkingLevel;
|
|
@@ -835,7 +835,7 @@ export type GetAllToolsHandler = () => string[];
|
|
|
835
835
|
|
|
836
836
|
export type SetActiveToolsHandler = (toolNames: string[]) => Promise<void>;
|
|
837
837
|
|
|
838
|
-
export type SetModelHandler = (model: Model
|
|
838
|
+
export type SetModelHandler = (model: Model) => Promise<boolean>;
|
|
839
839
|
|
|
840
840
|
export type GetThinkingLevelHandler = () => ThinkingLevel;
|
|
841
841
|
|
|
@@ -862,7 +862,7 @@ export interface ExtensionActions {
|
|
|
862
862
|
|
|
863
863
|
/** Actions for ExtensionContext (ctx.* in event handlers). */
|
|
864
864
|
export interface ExtensionContextActions {
|
|
865
|
-
getModel: () => Model
|
|
865
|
+
getModel: () => Model | undefined;
|
|
866
866
|
isIdle: () => boolean;
|
|
867
867
|
abort: () => void;
|
|
868
868
|
hasPendingMessages: () => boolean;
|
|
@@ -69,7 +69,7 @@ export class HookRunner {
|
|
|
69
69
|
private sessionManager: SessionManager;
|
|
70
70
|
private modelRegistry: ModelRegistry;
|
|
71
71
|
private errorListeners: Set<HookErrorListener> = new Set();
|
|
72
|
-
private getModel: () => Model
|
|
72
|
+
private getModel: () => Model | undefined = () => undefined;
|
|
73
73
|
private isIdleFn: () => boolean = () => true;
|
|
74
74
|
private waitForIdleFn: () => Promise<void> = async () => {};
|
|
75
75
|
private abortFn: () => void = () => {};
|
|
@@ -93,7 +93,7 @@ export class HookRunner {
|
|
|
93
93
|
*/
|
|
94
94
|
initialize(options: {
|
|
95
95
|
/** Function to get the current model */
|
|
96
|
-
getModel: () => Model
|
|
96
|
+
getModel: () => Model | undefined;
|
|
97
97
|
/** Handler for hooks to send messages */
|
|
98
98
|
sendMessageHandler: SendMessageHandler;
|
|
99
99
|
/** Handler for hooks to append entries */
|
|
@@ -147,7 +147,7 @@ export interface HookContext {
|
|
|
147
147
|
/** Model registry - use for API key resolution and model retrieval */
|
|
148
148
|
modelRegistry: ModelRegistry;
|
|
149
149
|
/** Current model (may be undefined if no model is selected yet) */
|
|
150
|
-
model: Model
|
|
150
|
+
model: Model | undefined;
|
|
151
151
|
/** Whether the agent is idle (not streaming) */
|
|
152
152
|
isIdle(): boolean;
|
|
153
153
|
/** Abort the current agent operation (fire-and-forget, does not wait) */
|
package/src/main.ts
CHANGED
|
@@ -391,8 +391,7 @@ async function buildSessionOptions(
|
|
|
391
391
|
process.exit(1);
|
|
392
392
|
}
|
|
393
393
|
options.model = model;
|
|
394
|
-
|
|
395
|
-
settings.override("modelRoles", { ...currentRoles, default: `${model.provider}/${model.id}` });
|
|
394
|
+
settings.overrideModelRoles({ default: `${model.provider}/${model.id}` });
|
|
396
395
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
|
397
396
|
const remembered = settings.getModelRole("default");
|
|
398
397
|
if (remembered) {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type TUI,
|
|
12
12
|
visibleWidth,
|
|
13
13
|
} from "@oh-my-pi/pi-tui";
|
|
14
|
-
import type
|
|
14
|
+
import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
|
|
15
15
|
import { parseModelString } from "../../config/model-resolver";
|
|
16
16
|
import type { Settings } from "../../config/settings";
|
|
17
17
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
@@ -27,27 +27,20 @@ function makeInvertedBadge(label: string, color: ThemeColor): string {
|
|
|
27
27
|
interface ModelItem {
|
|
28
28
|
provider: string;
|
|
29
29
|
id: string;
|
|
30
|
-
model: Model
|
|
30
|
+
model: Model;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
interface ScopedModelItem {
|
|
34
|
-
model: Model
|
|
34
|
+
model: Model;
|
|
35
35
|
thinkingLevel: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
type ModelRole = "default" | "smol" | "slow" | "plan" | "temporary";
|
|
39
|
-
|
|
40
38
|
interface MenuAction {
|
|
41
39
|
label: string;
|
|
42
40
|
role: ModelRole;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
const MENU_ACTIONS: MenuAction[] = [
|
|
46
|
-
{ label: "Set as Default", role: "default" },
|
|
47
|
-
{ label: "Set as Smol (Fast)", role: "smol" },
|
|
48
|
-
{ label: "Set as Slow (Thinking)", role: "slow" },
|
|
49
|
-
{ label: "Set as Plan (Architect)", role: "plan" },
|
|
50
|
-
];
|
|
43
|
+
const MENU_ACTIONS: MenuAction[] = MODEL_ROLE_IDS.map(role => ({ label: `Set as ${MODEL_ROLES[role].name}`, role }));
|
|
51
44
|
|
|
52
45
|
const ALL_TAB = "ALL";
|
|
53
46
|
|
|
@@ -76,13 +69,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
76
69
|
private allModels: ModelItem[] = [];
|
|
77
70
|
private filteredModels: ModelItem[] = [];
|
|
78
71
|
private selectedIndex: number = 0;
|
|
79
|
-
private
|
|
80
|
-
private smolModel?: Model<any>;
|
|
81
|
-
private slowModel?: Model<any>;
|
|
82
|
-
private planModel?: Model<any>;
|
|
72
|
+
private roles: { [key in ModelRole]?: Model } = {};
|
|
83
73
|
private settings: Settings;
|
|
84
74
|
private modelRegistry: ModelRegistry;
|
|
85
|
-
private onSelectCallback: (model: Model
|
|
75
|
+
private onSelectCallback: (model: Model, role: ModelRole | null) => void;
|
|
86
76
|
private onCancelCallback: () => void;
|
|
87
77
|
private errorMessage?: string;
|
|
88
78
|
private tui: TUI;
|
|
@@ -99,11 +89,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
99
89
|
|
|
100
90
|
constructor(
|
|
101
91
|
tui: TUI,
|
|
102
|
-
_currentModel: Model
|
|
92
|
+
_currentModel: Model | undefined,
|
|
103
93
|
settings: Settings,
|
|
104
94
|
modelRegistry: ModelRegistry,
|
|
105
95
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
106
|
-
onSelect: (model: Model
|
|
96
|
+
onSelect: (model: Model, role: ModelRole | null) => void,
|
|
107
97
|
onCancel: () => void,
|
|
108
98
|
options?: { temporaryOnly?: boolean; initialSearchInput?: string },
|
|
109
99
|
) {
|
|
@@ -182,42 +172,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
182
172
|
}
|
|
183
173
|
|
|
184
174
|
private _loadRoleModels(): void {
|
|
185
|
-
const roles = this.settings.get("modelRoles") as Record<string, string>;
|
|
186
175
|
const allModels = this.modelRegistry.getAll();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const parsed = parseModelString(defaultStr);
|
|
192
|
-
if (parsed) {
|
|
193
|
-
this.defaultModel = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Load smol model
|
|
198
|
-
const smolStr = roles.smol;
|
|
199
|
-
if (smolStr) {
|
|
200
|
-
const parsed = parseModelString(smolStr);
|
|
201
|
-
if (parsed) {
|
|
202
|
-
this.smolModel = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Load slow model
|
|
207
|
-
const slowStr = roles.slow;
|
|
208
|
-
if (slowStr) {
|
|
209
|
-
const parsed = parseModelString(slowStr);
|
|
210
|
-
if (parsed) {
|
|
211
|
-
this.slowModel = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Load plan model
|
|
216
|
-
const planStr = roles.plan;
|
|
217
|
-
if (planStr) {
|
|
218
|
-
const parsed = parseModelString(planStr);
|
|
176
|
+
for (const role of MODEL_ROLE_IDS) {
|
|
177
|
+
const modelId = this.settings.getModelRole(role);
|
|
178
|
+
if (!modelId) continue;
|
|
179
|
+
const parsed = parseModelString(modelId);
|
|
219
180
|
if (parsed) {
|
|
220
|
-
|
|
181
|
+
const model = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
182
|
+
if (model) {
|
|
183
|
+
this.roles[role] = model;
|
|
184
|
+
}
|
|
221
185
|
}
|
|
222
186
|
}
|
|
223
187
|
}
|
|
@@ -227,30 +191,25 @@ export class ModelSelectorComponent extends Container {
|
|
|
227
191
|
const mruOrder = this.settings.getStorage()?.getModelUsageOrder() ?? [];
|
|
228
192
|
const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
|
|
229
193
|
|
|
194
|
+
const modelRank = (model: ModelItem) => {
|
|
195
|
+
let i = 0;
|
|
196
|
+
while (i < MODEL_ROLE_IDS.length) {
|
|
197
|
+
const role = MODEL_ROLE_IDS[i];
|
|
198
|
+
if (this.roles[role] && modelsAreEqual(this.roles[role], model.model)) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
return i;
|
|
204
|
+
};
|
|
205
|
+
|
|
230
206
|
models.sort((a, b) => {
|
|
231
207
|
const aKey = `${a.provider}/${a.id}`;
|
|
232
208
|
const bKey = `${b.provider}/${b.id}`;
|
|
233
209
|
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
: modelsAreEqual(this.smolModel, a.model)
|
|
238
|
-
? 1
|
|
239
|
-
: modelsAreEqual(this.slowModel, a.model)
|
|
240
|
-
? 2
|
|
241
|
-
: modelsAreEqual(this.planModel, a.model)
|
|
242
|
-
? 3
|
|
243
|
-
: 4;
|
|
244
|
-
const bTag = modelsAreEqual(this.defaultModel, b.model)
|
|
245
|
-
? 0
|
|
246
|
-
: modelsAreEqual(this.smolModel, b.model)
|
|
247
|
-
? 1
|
|
248
|
-
: modelsAreEqual(this.slowModel, b.model)
|
|
249
|
-
? 2
|
|
250
|
-
: modelsAreEqual(this.planModel, b.model)
|
|
251
|
-
? 3
|
|
252
|
-
: 4;
|
|
253
|
-
if (aTag !== bTag) return aTag - bTag;
|
|
210
|
+
const aRank = modelRank(a);
|
|
211
|
+
const bRank = modelRank(b);
|
|
212
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
254
213
|
|
|
255
214
|
// Then MRU order (models in mruIndex come before those not in it)
|
|
256
215
|
const aMru = mruIndex.get(aKey) ?? Number.MAX_SAFE_INTEGER;
|
|
@@ -287,7 +246,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
287
246
|
// Load available models (built-in models still work even if models.json failed)
|
|
288
247
|
try {
|
|
289
248
|
const availableModels = this.modelRegistry.getAvailable();
|
|
290
|
-
models = availableModels.map((model: Model
|
|
249
|
+
models = availableModels.map((model: Model) => ({
|
|
291
250
|
provider: model.provider,
|
|
292
251
|
id: model.id,
|
|
293
252
|
model,
|
|
@@ -392,17 +351,15 @@ export class ModelSelectorComponent extends Container {
|
|
|
392
351
|
if (!item) continue;
|
|
393
352
|
|
|
394
353
|
const isSelected = i === this.selectedIndex;
|
|
395
|
-
const isDefault = modelsAreEqual(this.defaultModel, item.model);
|
|
396
|
-
const isSmol = modelsAreEqual(this.smolModel, item.model);
|
|
397
|
-
const isSlow = modelsAreEqual(this.slowModel, item.model);
|
|
398
|
-
const isPlan = modelsAreEqual(this.planModel, item.model);
|
|
399
354
|
|
|
400
355
|
// Build role badges (inverted: color as background, black text)
|
|
401
356
|
const badges: string[] = [];
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
357
|
+
for (const role of MODEL_ROLE_IDS) {
|
|
358
|
+
const { tag, color } = MODEL_ROLES[role];
|
|
359
|
+
if (tag && modelsAreEqual(this.roles[role], item.model)) {
|
|
360
|
+
badges.push(makeInvertedBadge(tag, color ?? "success"));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
406
363
|
const badgeText = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
407
364
|
|
|
408
365
|
let line = "";
|
|
@@ -533,7 +490,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
533
490
|
if (selectedModel) {
|
|
534
491
|
if (this.temporaryOnly) {
|
|
535
492
|
// In temporary mode, skip menu and select directly
|
|
536
|
-
this.handleSelect(selectedModel.model,
|
|
493
|
+
this.handleSelect(selectedModel.model, null);
|
|
537
494
|
} else {
|
|
538
495
|
this.openMenu();
|
|
539
496
|
}
|
|
@@ -585,10 +542,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
585
542
|
}
|
|
586
543
|
}
|
|
587
544
|
|
|
588
|
-
private handleSelect(model: Model
|
|
545
|
+
private handleSelect(model: Model, role: ModelRole | null): void {
|
|
589
546
|
// For temporary role, don't save to settings - just notify caller
|
|
590
|
-
if (role ===
|
|
591
|
-
this.onSelectCallback(model,
|
|
547
|
+
if (role === null) {
|
|
548
|
+
this.onSelectCallback(model, null);
|
|
592
549
|
return;
|
|
593
550
|
}
|
|
594
551
|
|
|
@@ -596,15 +553,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
596
553
|
this.settings.setModelRole(role, `${model.provider}/${model.id}`);
|
|
597
554
|
|
|
598
555
|
// Update local state for UI
|
|
599
|
-
|
|
600
|
-
this.defaultModel = model;
|
|
601
|
-
} else if (role === "smol") {
|
|
602
|
-
this.smolModel = model;
|
|
603
|
-
} else if (role === "slow") {
|
|
604
|
-
this.slowModel = model;
|
|
605
|
-
} else if (role === "plan") {
|
|
606
|
-
this.planModel = model;
|
|
607
|
-
}
|
|
556
|
+
this.roles[role] = model;
|
|
608
557
|
|
|
609
558
|
// Notify caller (for updating agent state if needed)
|
|
610
559
|
this.onSelectCallback(model, role);
|
|
@@ -649,7 +649,7 @@ export class InputController {
|
|
|
649
649
|
|
|
650
650
|
async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
|
|
651
651
|
try {
|
|
652
|
-
const roleOrder = ["slow", "default", "smol"];
|
|
652
|
+
const roleOrder = ["slow", "default", "smol"] as const;
|
|
653
653
|
const result = await this.ctx.session.cycleRoleModels(roleOrder, options);
|
|
654
654
|
if (!result) {
|
|
655
655
|
this.ctx.showStatus("Only one role model available");
|
|
@@ -3,6 +3,7 @@ import type { OAuthProvider } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { getAgentDbPath } from "../../config";
|
|
6
|
+
import { MODEL_ROLES } from "../../config/model-registry";
|
|
6
7
|
import { settings } from "../../config/settings";
|
|
7
8
|
import { DebugSelectorComponent } from "../../debug";
|
|
8
9
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
@@ -277,7 +278,7 @@ export class SelectorController {
|
|
|
277
278
|
this.ctx.session.scopedModels,
|
|
278
279
|
async (model, role) => {
|
|
279
280
|
try {
|
|
280
|
-
if (role ===
|
|
281
|
+
if (role === null) {
|
|
281
282
|
// Temporary: update agent state but don't persist to settings
|
|
282
283
|
await this.ctx.session.setModelTemporary(model);
|
|
283
284
|
this.ctx.statusLine.invalidate();
|
|
@@ -294,7 +295,8 @@ export class SelectorController {
|
|
|
294
295
|
// Don't call done() - selector stays open for role assignment
|
|
295
296
|
} else {
|
|
296
297
|
// Other roles (smol, slow): just update settings, not current model
|
|
297
|
-
const
|
|
298
|
+
const roleInfo = MODEL_ROLES[role];
|
|
299
|
+
const roleLabel = roleInfo?.name ?? role;
|
|
298
300
|
this.ctx.showStatus(`${roleLabel} model: ${model.id}`);
|
|
299
301
|
// Don't call done() - selector stays open
|
|
300
302
|
}
|
|
@@ -137,7 +137,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
137
137
|
private readonly version: string;
|
|
138
138
|
private readonly changelogMarkdown: string | undefined;
|
|
139
139
|
private planModePreviousTools: string[] | undefined;
|
|
140
|
-
private planModePreviousModel: Model
|
|
140
|
+
private planModePreviousModel: Model | undefined;
|
|
141
141
|
private planModeHasEntered = false;
|
|
142
142
|
public readonly lspServers:
|
|
143
143
|
| Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>
|
|
@@ -68,7 +68,7 @@ export type RpcCommand =
|
|
|
68
68
|
// ============================================================================
|
|
69
69
|
|
|
70
70
|
export interface RpcSessionState {
|
|
71
|
-
model?: Model
|
|
71
|
+
model?: Model;
|
|
72
72
|
thinkingLevel: ThinkingLevel;
|
|
73
73
|
isStreaming: boolean;
|
|
74
74
|
isCompacting: boolean;
|
|
@@ -105,21 +105,21 @@ export type RpcResponse =
|
|
|
105
105
|
type: "response";
|
|
106
106
|
command: "set_model";
|
|
107
107
|
success: true;
|
|
108
|
-
data: Model
|
|
108
|
+
data: Model;
|
|
109
109
|
}
|
|
110
110
|
| {
|
|
111
111
|
id?: string;
|
|
112
112
|
type: "response";
|
|
113
113
|
command: "cycle_model";
|
|
114
114
|
success: true;
|
|
115
|
-
data: { model: Model
|
|
115
|
+
data: { model: Model; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;
|
|
116
116
|
}
|
|
117
117
|
| {
|
|
118
118
|
id?: string;
|
|
119
119
|
type: "response";
|
|
120
120
|
command: "get_available_models";
|
|
121
121
|
success: true;
|
|
122
|
-
data: { models: Model
|
|
122
|
+
data: { models: Model[] };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Thinking
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: explore
|
|
3
3
|
description: Fast read-only codebase scout returning compressed context for handoff
|
|
4
4
|
tools: read, grep, find, ls, bash
|
|
5
|
-
model: pi/smol, haiku, flash, mini
|
|
5
|
+
model: pi/smol, haiku-4.5, haiku-4-5, gemini-flash-latest, gemini-3-flash, zai-glm-4.7, glm-4.7-flash, glm-4.5-flash, gpt-5.1-codex-mini, haiku, flash, mini
|
|
6
6
|
output:
|
|
7
7
|
properties:
|
|
8
8
|
query:
|
|
@@ -108,4 +108,4 @@ Infer from task; default medium:
|
|
|
108
108
|
|
|
109
109
|
<critical>
|
|
110
110
|
Call `submit_result` with findings when done.
|
|
111
|
-
</critical>
|
|
111
|
+
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -131,11 +131,11 @@ export interface CreateAgentSessionOptions {
|
|
|
131
131
|
modelRegistry?: ModelRegistry;
|
|
132
132
|
|
|
133
133
|
/** Model to use. Default: from settings, else first available */
|
|
134
|
-
model?: Model
|
|
134
|
+
model?: Model;
|
|
135
135
|
/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */
|
|
136
136
|
thinkingLevel?: ThinkingLevel;
|
|
137
137
|
/** Models available for cycling (Ctrl+P in interactive mode) */
|
|
138
|
-
scopedModels?: Array<{ model: Model
|
|
138
|
+
scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>;
|
|
139
139
|
|
|
140
140
|
/** System prompt. String replaces default, function receives default and returns final. */
|
|
141
141
|
systemPrompt?: string | ((defaultPrompt: string) => string);
|
|
@@ -32,7 +32,7 @@ import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
|
32
32
|
import { YAML } from "bun";
|
|
33
33
|
import type { Rule } from "../capability/rule";
|
|
34
34
|
import { getAgentDbPath } from "../config";
|
|
35
|
-
import type
|
|
35
|
+
import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "../config/model-registry";
|
|
36
36
|
import { parseModelString } from "../config/model-resolver";
|
|
37
37
|
import {
|
|
38
38
|
expandPromptTemplate,
|
|
@@ -127,7 +127,7 @@ export interface AgentSessionConfig {
|
|
|
127
127
|
sessionManager: SessionManager;
|
|
128
128
|
settings: Settings;
|
|
129
129
|
/** Models to cycle through with Ctrl+P (from --models flag) */
|
|
130
|
-
scopedModels?: Array<{ model: Model
|
|
130
|
+
scopedModels?: Array<{ model: Model; thinkingLevel: ThinkingLevel }>;
|
|
131
131
|
/** Prompt templates for expansion */
|
|
132
132
|
promptTemplates?: PromptTemplate[];
|
|
133
133
|
/** File-based slash commands for expansion */
|
|
@@ -167,7 +167,7 @@ export interface PromptOptions {
|
|
|
167
167
|
|
|
168
168
|
/** Result from cycleModel() */
|
|
169
169
|
export interface ModelCycleResult {
|
|
170
|
-
model: Model
|
|
170
|
+
model: Model;
|
|
171
171
|
thinkingLevel: ThinkingLevel;
|
|
172
172
|
/** Whether cycling through scoped models (--models flag) or all available */
|
|
173
173
|
isScoped: boolean;
|
|
@@ -175,9 +175,9 @@ export interface ModelCycleResult {
|
|
|
175
175
|
|
|
176
176
|
/** Result from cycleRoleModels() */
|
|
177
177
|
export interface RoleModelCycleResult {
|
|
178
|
-
model: Model
|
|
178
|
+
model: Model;
|
|
179
179
|
thinkingLevel: ThinkingLevel;
|
|
180
|
-
role:
|
|
180
|
+
role: ModelRole;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/** Session statistics for /session command */
|
|
@@ -257,7 +257,7 @@ export class AgentSession {
|
|
|
257
257
|
readonly sessionManager: SessionManager;
|
|
258
258
|
readonly settings: Settings;
|
|
259
259
|
|
|
260
|
-
private _scopedModels: Array<{ model: Model
|
|
260
|
+
private _scopedModels: Array<{ model: Model; thinkingLevel: ThinkingLevel }>;
|
|
261
261
|
private _promptTemplates: PromptTemplate[];
|
|
262
262
|
private _slashCommands: FileSlashCommand[];
|
|
263
263
|
|
|
@@ -887,7 +887,7 @@ export class AgentSession {
|
|
|
887
887
|
}
|
|
888
888
|
|
|
889
889
|
/** Current model (may be undefined if not yet selected) */
|
|
890
|
-
get model(): Model
|
|
890
|
+
get model(): Model | undefined {
|
|
891
891
|
return this.agent.state.model;
|
|
892
892
|
}
|
|
893
893
|
|
|
@@ -994,7 +994,7 @@ export class AgentSession {
|
|
|
994
994
|
}
|
|
995
995
|
|
|
996
996
|
/** Scoped models for cycling (from --models flag) */
|
|
997
|
-
get scopedModels(): ReadonlyArray<{ model: Model
|
|
997
|
+
get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel: ThinkingLevel }> {
|
|
998
998
|
return this._scopedModels;
|
|
999
999
|
}
|
|
1000
1000
|
|
|
@@ -1014,7 +1014,7 @@ export class AgentSession {
|
|
|
1014
1014
|
this._planReferenceSent = true;
|
|
1015
1015
|
}
|
|
1016
1016
|
|
|
1017
|
-
resolveRoleModel(role:
|
|
1017
|
+
resolveRoleModel(role: ModelRole): Model | undefined {
|
|
1018
1018
|
return this._resolveRoleModel(role, this._modelRegistry.getAvailable(), this.model);
|
|
1019
1019
|
}
|
|
1020
1020
|
|
|
@@ -1800,7 +1800,7 @@ export class AgentSession {
|
|
|
1800
1800
|
* Validates API key, saves to session and settings.
|
|
1801
1801
|
* @throws Error if no API key available for the model
|
|
1802
1802
|
*/
|
|
1803
|
-
async setModel(model: Model
|
|
1803
|
+
async setModel(model: Model, role: ModelRole = "default"): Promise<void> {
|
|
1804
1804
|
const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId);
|
|
1805
1805
|
if (!apiKey) {
|
|
1806
1806
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -1820,7 +1820,7 @@ export class AgentSession {
|
|
|
1820
1820
|
* Validates API key, saves to session log but NOT to settings.
|
|
1821
1821
|
* @throws Error if no API key available for the model
|
|
1822
1822
|
*/
|
|
1823
|
-
async setModelTemporary(model: Model
|
|
1823
|
+
async setModelTemporary(model: Model): Promise<void> {
|
|
1824
1824
|
const apiKey = await this._modelRegistry.getApiKey(model, this.sessionId);
|
|
1825
1825
|
if (!apiKey) {
|
|
1826
1826
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -1854,7 +1854,7 @@ export class AgentSession {
|
|
|
1854
1854
|
* @param options - Optional settings: `temporary` to not persist to settings
|
|
1855
1855
|
*/
|
|
1856
1856
|
async cycleRoleModels(
|
|
1857
|
-
roleOrder:
|
|
1857
|
+
roleOrder: readonly ModelRole[],
|
|
1858
1858
|
options?: { temporary?: boolean },
|
|
1859
1859
|
): Promise<RoleModelCycleResult | undefined> {
|
|
1860
1860
|
const availableModels = this._modelRegistry.getAvailable();
|
|
@@ -1862,7 +1862,7 @@ export class AgentSession {
|
|
|
1862
1862
|
|
|
1863
1863
|
const currentModel = this.model;
|
|
1864
1864
|
if (!currentModel) return undefined;
|
|
1865
|
-
const roleModels: Array<{ role:
|
|
1865
|
+
const roleModels: Array<{ role: ModelRole; model: Model }> = [];
|
|
1866
1866
|
|
|
1867
1867
|
for (const role of roleOrder) {
|
|
1868
1868
|
const roleModelStr =
|
|
@@ -1872,7 +1872,7 @@ export class AgentSession {
|
|
|
1872
1872
|
if (!roleModelStr) continue;
|
|
1873
1873
|
|
|
1874
1874
|
const parsed = parseModelString(roleModelStr);
|
|
1875
|
-
let match: Model
|
|
1875
|
+
let match: Model | undefined;
|
|
1876
1876
|
if (parsed) {
|
|
1877
1877
|
match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
1878
1878
|
}
|
|
@@ -1964,7 +1964,7 @@ export class AgentSession {
|
|
|
1964
1964
|
/**
|
|
1965
1965
|
* Get all available models with valid API keys.
|
|
1966
1966
|
*/
|
|
1967
|
-
getAvailableModels(): Model
|
|
1967
|
+
getAvailableModels(): Model[] {
|
|
1968
1968
|
return this._modelRegistry.getAvailable();
|
|
1969
1969
|
}
|
|
1970
1970
|
|
|
@@ -2530,15 +2530,15 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2530
2530
|
this.agent.continue().catch(() => {});
|
|
2531
2531
|
}
|
|
2532
2532
|
|
|
2533
|
-
private _getModelKey(model: Model
|
|
2533
|
+
private _getModelKey(model: Model): string {
|
|
2534
2534
|
return `${model.provider}/${model.id}`;
|
|
2535
2535
|
}
|
|
2536
2536
|
|
|
2537
2537
|
private _resolveRoleModel(
|
|
2538
|
-
role:
|
|
2539
|
-
availableModels: Model
|
|
2540
|
-
currentModel: Model
|
|
2541
|
-
): Model
|
|
2538
|
+
role: ModelRole,
|
|
2539
|
+
availableModels: Model[],
|
|
2540
|
+
currentModel: Model | undefined,
|
|
2541
|
+
): Model | undefined {
|
|
2542
2542
|
const roleModelStr =
|
|
2543
2543
|
role === "default"
|
|
2544
2544
|
? (this.settings.getModelRole("default") ??
|
|
@@ -2555,11 +2555,11 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2555
2555
|
return availableModels.find(m => m.id.toLowerCase() === roleLower);
|
|
2556
2556
|
}
|
|
2557
2557
|
|
|
2558
|
-
private _getCompactionModelCandidates(availableModels: Model
|
|
2559
|
-
const candidates: Model
|
|
2558
|
+
private _getCompactionModelCandidates(availableModels: Model[]): Model[] {
|
|
2559
|
+
const candidates: Model[] = [];
|
|
2560
2560
|
const seen = new Set<string>();
|
|
2561
2561
|
|
|
2562
|
-
const addCandidate = (model: Model
|
|
2562
|
+
const addCandidate = (model: Model | undefined): void => {
|
|
2563
2563
|
if (!model) return;
|
|
2564
2564
|
const key = this._getModelKey(model);
|
|
2565
2565
|
if (seen.has(key)) return;
|
|
@@ -2568,10 +2568,9 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2568
2568
|
};
|
|
2569
2569
|
|
|
2570
2570
|
const currentModel = this.model;
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
addCandidate(this._resolveRoleModel("smol", availableModels, currentModel));
|
|
2571
|
+
for (const role of MODEL_ROLE_IDS) {
|
|
2572
|
+
addCandidate(this._resolveRoleModel(role, availableModels, currentModel));
|
|
2573
|
+
}
|
|
2575
2574
|
|
|
2576
2575
|
const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
|
|
2577
2576
|
for (const model of sortedByContext) {
|
|
@@ -66,7 +66,7 @@ export interface CollectEntriesResult {
|
|
|
66
66
|
|
|
67
67
|
export interface GenerateBranchSummaryOptions {
|
|
68
68
|
/** Model to use for summarization */
|
|
69
|
-
model: Model
|
|
69
|
+
model: Model;
|
|
70
70
|
/** API key for the model */
|
|
71
71
|
apiKey: string;
|
|
72
72
|
/** Abort signal for cancellation */
|
|
@@ -478,7 +478,7 @@ export interface SummaryOptions {
|
|
|
478
478
|
|
|
479
479
|
export async function generateSummary(
|
|
480
480
|
currentMessages: AgentMessage[],
|
|
481
|
-
model: Model
|
|
481
|
+
model: Model,
|
|
482
482
|
reserveTokens: number,
|
|
483
483
|
apiKey: string,
|
|
484
484
|
signal?: AbortSignal,
|
|
@@ -547,7 +547,7 @@ export async function generateSummary(
|
|
|
547
547
|
async function generateShortSummary(
|
|
548
548
|
recentMessages: AgentMessage[],
|
|
549
549
|
historySummary: string | undefined,
|
|
550
|
-
model: Model
|
|
550
|
+
model: Model,
|
|
551
551
|
reserveTokens: number,
|
|
552
552
|
apiKey: string,
|
|
553
553
|
signal?: AbortSignal,
|
|
@@ -724,7 +724,7 @@ const TURN_PREFIX_SUMMARIZATION_PROMPT = renderPromptTemplate(compactionTurnPref
|
|
|
724
724
|
*/
|
|
725
725
|
export async function compact(
|
|
726
726
|
preparation: CompactionPreparation,
|
|
727
|
-
model: Model
|
|
727
|
+
model: Model,
|
|
728
728
|
apiKey: string,
|
|
729
729
|
customInstructions?: string,
|
|
730
730
|
signal?: AbortSignal,
|
|
@@ -822,7 +822,7 @@ export async function compact(
|
|
|
822
822
|
*/
|
|
823
823
|
async function generateTurnPrefixSummary(
|
|
824
824
|
messages: AgentMessage[],
|
|
825
|
-
model: Model
|
|
825
|
+
model: Model,
|
|
826
826
|
reserveTokens: number,
|
|
827
827
|
apiKey: string,
|
|
828
828
|
signal?: AbortSignal,
|
package/src/task/executor.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
10
10
|
import type { TSchema } from "@sinclair/typebox";
|
|
11
11
|
import Ajv, { type ValidateFunction } from "ajv";
|
|
12
12
|
import type { ModelRegistry } from "../config/model-registry";
|
|
13
|
-
import {
|
|
13
|
+
import { resolveModelOverride } from "../config/model-resolver";
|
|
14
14
|
import { type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
|
|
15
15
|
import { Settings } from "../config/settings";
|
|
16
16
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
TASK_SUBAGENT_PROGRESS_CHANNEL,
|
|
40
40
|
} from "./types";
|
|
41
41
|
|
|
42
|
-
const DEFAULT_MODEL_ALIASES = new Set(["default", "pi/default", "omp/default"]);
|
|
43
42
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
44
43
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
45
44
|
|
|
@@ -129,39 +128,6 @@ function getReportFindingKey(value: unknown): string | null {
|
|
|
129
128
|
return `${filePath}:${lineStart}:${lineEnd}:${priority ?? ""}:${title}`;
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
function resolveModelOverride(
|
|
133
|
-
modelPatterns: string[],
|
|
134
|
-
modelRegistry: ModelRegistry,
|
|
135
|
-
settings?: Settings,
|
|
136
|
-
): { model?: Model<Api>; thinkingLevel?: ThinkingLevel } {
|
|
137
|
-
if (modelPatterns.length === 0) return {};
|
|
138
|
-
const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
|
|
139
|
-
const roles = settings?.getGroup("modelRoles");
|
|
140
|
-
for (const pattern of modelPatterns) {
|
|
141
|
-
const normalized = pattern.trim().toLowerCase();
|
|
142
|
-
if (!normalized || DEFAULT_MODEL_ALIASES.has(normalized)) {
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
let effectivePattern = pattern;
|
|
146
|
-
if (normalized.startsWith("omp/") || normalized.startsWith("pi/")) {
|
|
147
|
-
const role = normalized.startsWith("omp/") ? pattern.slice(4) : pattern.slice(3);
|
|
148
|
-
const configured = roles?.[role] ?? roles?.[role.toLowerCase()];
|
|
149
|
-
if (configured) {
|
|
150
|
-
effectivePattern = configured;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const { model, thinkingLevel } = parseModelPattern(
|
|
154
|
-
effectivePattern,
|
|
155
|
-
modelRegistry.getAvailable(),
|
|
156
|
-
matchPreferences,
|
|
157
|
-
);
|
|
158
|
-
if (model) {
|
|
159
|
-
return { model, thinkingLevel: thinkingLevel !== "off" ? thinkingLevel : undefined };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return {};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
131
|
function buildSubmitResultToolChoice(model?: Model<Api>): ToolChoice | undefined {
|
|
166
132
|
if (!model) return undefined;
|
|
167
133
|
if (
|
package/src/task/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { Usage } from "@oh-my-pi/pi-ai";
|
|
|
20
20
|
import { $ } from "bun";
|
|
21
21
|
import { nanoid } from "nanoid";
|
|
22
22
|
import type { ToolSession } from "..";
|
|
23
|
+
import { isDefaultModelAlias } from "../config/model-resolver";
|
|
23
24
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
24
25
|
import type { Theme } from "../modes/theme/theme";
|
|
25
26
|
import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
|
|
@@ -165,16 +166,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
165
166
|
const { agent: agentName, context, schema: outputSchema, isolated } = params;
|
|
166
167
|
const isIsolated = isolated === true;
|
|
167
168
|
|
|
168
|
-
const isDefaultModelAlias = (value: string | string[] | undefined): boolean => {
|
|
169
|
-
if (!value) return true;
|
|
170
|
-
const values = Array.isArray(value) ? value : [value];
|
|
171
|
-
if (values.length === 0) return true;
|
|
172
|
-
return values.every(entry => {
|
|
173
|
-
const normalized = entry.trim().toLowerCase();
|
|
174
|
-
return normalized === "default" || normalized === "pi/default" || normalized === "omp/default";
|
|
175
|
-
});
|
|
176
|
-
};
|
|
177
|
-
|
|
178
169
|
// Validate agent exists
|
|
179
170
|
const agent = getAgent(agents, agentName);
|
|
180
171
|
if (!agent) {
|