@oh-my-pi/pi-coding-agent 10.5.0 → 10.6.1

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.
@@ -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
@@ -6,17 +6,13 @@ Powerful search tool built on ripgrep.
6
6
  - Supports full regex syntax (e.g., `log.*Error`, `function\\s+\\w+`)
7
7
  - Filter files with `glob` (e.g., `*.js`, `**/*.tsx`) or `type` (e.g., `js`, `py`, `rust`)
8
8
  - Pattern syntax uses ripgrep—literal braces need escaping (`interface\\{\\}` to find `interface{}` in Go)
9
- - For cross-line patterns like `struct \\{[\\s\\S]*?field`, use `multiline: true`
9
+ - For cross-line patterns like `struct \\{[\\s\\S]*?field`, set `multiline: true` if needed
10
+ - If the pattern contains a literal `\n`, multiline defaults to true
10
11
  </instruction>
11
12
 
12
13
  <output>
13
- Results depend on `output_mode`:
14
- - `content`: Matching lines with file paths and line numbers
15
- - `files_with_matches`: File paths only (one per line)
16
- - `count`: Match counts per file
17
-
18
- In `content` mode, truncated at 100 matches default (configurable via `limit`).
19
- For `files_with_matches` and `count` modes, use `limit` truncate results.
14
+ Results are always content mode: matching lines with file paths and line numbers.
15
+ Truncated at 100 matches by default (configurable via `limit`).
20
16
  </output>
21
17
 
22
18
  <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) {