@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 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.0",
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.0",
84
- "@oh-my-pi/pi-agent-core": "10.6.0",
85
- "@oh-my-pi/pi-ai": "10.6.0",
86
- "@oh-my-pi/pi-natives": "10.6.0",
87
- "@oh-my-pi/pi-tui": "10.6.0",
88
- "@oh-my-pi/pi-utils": "10.6.0",
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 { parseModelPattern, parseModelString, SMOL_MODEL_PRIORITY } from "../config/model-resolver";
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(settings, available, matchPreferences);
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 ? resolveModelFromString(role, available, matchPreferences) : undefined;
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 { ModelRegistry } from "./model-registry";
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
@@ -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<any> | undefined;
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<any>): Promise<boolean> {
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<any> | undefined = () => undefined;
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<any> | undefined;
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<any>): Promise<boolean>;
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<any>) => Promise<boolean>;
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<any> | undefined;
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<any> | undefined = () => undefined;
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<any> | undefined;
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<any> | undefined;
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
- const currentRoles = settings.get("modelRoles") as Record<string, string>;
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 { ModelRegistry } from "../../config/model-registry";
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<any>;
30
+ model: Model;
31
31
  }
32
32
 
33
33
  interface ScopedModelItem {
34
- model: Model<any>;
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 defaultModel?: Model<any>;
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<any>, role: string) => void;
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<any> | undefined,
92
+ _currentModel: Model | undefined,
103
93
  settings: Settings,
104
94
  modelRegistry: ModelRegistry,
105
95
  scopedModels: ReadonlyArray<ScopedModelItem>,
106
- onSelect: (model: Model<any>, role: string) => void,
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
- // Load default model
189
- const defaultStr = roles.default;
190
- if (defaultStr) {
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
- this.planModel = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
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
- // Tagged models first: default (0), smol (1), slow (2), plan (3), untagged (4)
235
- const aTag = modelsAreEqual(this.defaultModel, a.model)
236
- ? 0
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<any>) => ({
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
- if (isDefault) badges.push(makeInvertedBadge("DEFAULT", "success"));
403
- if (isSmol) badges.push(makeInvertedBadge("SMOL", "warning"));
404
- if (isSlow) badges.push(makeInvertedBadge("SLOW", "accent"));
405
- if (isPlan) badges.push(makeInvertedBadge("PLAN", "muted"));
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, "temporary");
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<any>, role: ModelRole): void {
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 === "temporary") {
591
- this.onSelectCallback(model, role);
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
- if (role === "default") {
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 === "temporary") {
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 roleLabel = role === "smol" ? "Smol" : role;
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<any> | undefined;
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<any>;
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<any>;
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<any>; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;
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<any>[] };
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<any>;
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<any>; thinkingLevel: ThinkingLevel }>;
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 { ModelRegistry } from "../config/model-registry";
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<any>; thinkingLevel: ThinkingLevel }>;
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<any>;
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<any>;
178
+ model: Model;
179
179
  thinkingLevel: ThinkingLevel;
180
- role: string;
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<any>; thinkingLevel: ThinkingLevel }>;
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<any> | undefined {
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<any>; thinkingLevel: ThinkingLevel }> {
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: string): Model<any> | undefined {
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<any>, role: string = "default"): Promise<void> {
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<any>): Promise<void> {
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: string[],
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: string; model: Model<any> }> = [];
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<any> | undefined;
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<any>[] {
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<any>): string {
2533
+ private _getModelKey(model: Model): string {
2534
2534
  return `${model.provider}/${model.id}`;
2535
2535
  }
2536
2536
 
2537
2537
  private _resolveRoleModel(
2538
- role: string,
2539
- availableModels: Model<any>[],
2540
- currentModel: Model<any> | undefined,
2541
- ): Model<any> | undefined {
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<any>[]): Model<any>[] {
2559
- const candidates: Model<any>[] = [];
2558
+ private _getCompactionModelCandidates(availableModels: Model[]): Model[] {
2559
+ const candidates: Model[] = [];
2560
2560
  const seen = new Set<string>();
2561
2561
 
2562
- const addCandidate = (model: Model<any> | undefined): void => {
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
- addCandidate(this._resolveRoleModel("default", availableModels, currentModel));
2572
- addCandidate(this._resolveRoleModel("slow", availableModels, currentModel));
2573
- addCandidate(this._resolveRoleModel("small", availableModels, currentModel));
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<any>;
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<any>,
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<any>,
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<any>,
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<any>,
825
+ model: Model,
826
826
  reserveTokens: number,
827
827
  apiKey: string,
828
828
  signal?: AbortSignal,
@@ -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 { parseModelPattern } from "../config/model-resolver";
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) {