@diegopetrucci/pi-extensions 0.1.73 → 0.1.76

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.
Files changed (93) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +9 -4
  3. package/extensions/agent-workflow-audit/index.ts +1398 -1009
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/README.md +3 -1
  6. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  7. package/extensions/annotate-git-diff/git.ts +853 -767
  8. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  9. package/extensions/annotate-git-diff/index.ts +712 -671
  10. package/extensions/annotate-git-diff/package.json +1 -1
  11. package/extensions/annotate-git-diff/prompt.ts +45 -45
  12. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  13. package/extensions/annotate-git-diff/review-server.ts +337 -311
  14. package/extensions/annotate-git-diff/types.ts +132 -132
  15. package/extensions/annotate-git-diff/ui.ts +27 -27
  16. package/extensions/annotate-git-diff/watch.ts +74 -74
  17. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  18. package/extensions/annotate-git-diff/web/index.html +5 -6
  19. package/extensions/annotate-last-message/README.md +1 -1
  20. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  21. package/extensions/annotate-last-message/index.ts +210 -206
  22. package/extensions/annotate-last-message/package.json +1 -1
  23. package/extensions/annotate-last-message/prompt.ts +87 -66
  24. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  25. package/extensions/annotate-last-message/session.ts +94 -89
  26. package/extensions/annotate-last-message/theme.ts +199 -193
  27. package/extensions/annotate-last-message/types.ts +23 -23
  28. package/extensions/annotate-last-message/ui.ts +27 -25
  29. package/extensions/annotate-last-message/web/app.js +4 -7
  30. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  31. package/extensions/brrr/README.md +1 -1
  32. package/extensions/brrr/index.ts +213 -191
  33. package/extensions/brrr/package.json +1 -1
  34. package/extensions/claude-fast/README.md +5 -79
  35. package/extensions/claude-fast/index.ts +31 -289
  36. package/extensions/claude-fast/package.json +2 -3
  37. package/extensions/code-reviewer/README.md +1 -1
  38. package/extensions/code-reviewer/index.ts +1598 -1350
  39. package/extensions/code-reviewer/package.json +1 -1
  40. package/extensions/confirm-destructive/README.md +1 -1
  41. package/extensions/confirm-destructive/index.ts +55 -51
  42. package/extensions/confirm-destructive/package.json +1 -1
  43. package/extensions/context-cap/index.ts +312 -304
  44. package/extensions/context-cap/package.json +1 -1
  45. package/extensions/context-inspector/index.ts +1211 -1064
  46. package/extensions/context-inspector/package.json +1 -1
  47. package/extensions/contrarian/index.ts +1854 -1735
  48. package/extensions/contrarian/package.json +1 -1
  49. package/extensions/dirty-repo-guard/index.ts +41 -41
  50. package/extensions/dynamic-context-pruning/README.md +6 -38
  51. package/extensions/dynamic-context-pruning/index.ts +2124 -1980
  52. package/extensions/dynamic-context-pruning/package.json +1 -1
  53. package/extensions/fast/README.md +71 -21
  54. package/extensions/fast/index.ts +513 -313
  55. package/extensions/fast/package.json +1 -1
  56. package/extensions/git-footer/README.md +1 -1
  57. package/extensions/git-footer/index.ts +458 -411
  58. package/extensions/git-footer/package.json +2 -3
  59. package/extensions/gnosis/index.ts +180 -130
  60. package/extensions/gnosis/package.json +1 -1
  61. package/extensions/inline-bash/index.ts +88 -88
  62. package/extensions/inline-bash/package.json +1 -1
  63. package/extensions/librarian/index.ts +1669 -1419
  64. package/extensions/librarian/package.json +1 -1
  65. package/extensions/minimal-footer/index.ts +994 -1012
  66. package/extensions/minimal-footer/openai-usage.ts +195 -190
  67. package/extensions/notify/index.ts +222 -230
  68. package/extensions/notify/package.json +1 -1
  69. package/extensions/openai-fast/README.md +5 -73
  70. package/extensions/openai-fast/index.ts +32 -272
  71. package/extensions/openai-fast/package.json +2 -3
  72. package/extensions/oracle/index.ts +1731 -1625
  73. package/extensions/oracle/package.json +1 -1
  74. package/extensions/permission-gate/index.ts +658 -608
  75. package/extensions/permission-gate/package.json +1 -1
  76. package/extensions/permission-gate/powershell-safety.ts +840 -793
  77. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  78. package/extensions/project-mcp-json/README.md +199 -0
  79. package/extensions/project-mcp-json/config.ts +564 -0
  80. package/extensions/project-mcp-json/index.ts +119 -0
  81. package/extensions/project-mcp-json/package.json +38 -0
  82. package/extensions/quiet-tools/index.ts +282 -264
  83. package/extensions/review/README.md +2 -2
  84. package/extensions/review/index.ts +1745 -1872
  85. package/extensions/review/package.json +1 -1
  86. package/extensions/todo/index.ts +284 -265
  87. package/extensions/triage-comments/README.md +2 -2
  88. package/extensions/triage-comments/index.ts +2294 -1882
  89. package/extensions/triage-comments/package.json +1 -1
  90. package/package.json +6 -3
  91. package/assets/social-preview.png +0 -0
  92. package/extensions/claude-fast/claude-fast.example.json +0 -4
  93. package/extensions/openai-fast/openai-fast.example.json +0 -4
@@ -3,7 +3,13 @@ import { existsSync } from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import { StringEnum, type Usage } from "@earendil-works/pi-ai";
6
- import { getAgentDir, getMarkdownTheme, type AgentToolResult, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ getAgentDir,
8
+ getMarkdownTheme,
9
+ type AgentToolResult,
10
+ type ExtensionAPI,
11
+ type ExtensionContext,
12
+ } from "@earendil-works/pi-coding-agent";
7
13
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
8
14
  import { Type } from "typebox";
9
15
 
@@ -11,89 +17,89 @@ type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "
11
17
  type ThinkingLevelMap = Partial<Record<ThinkingLevel, unknown | null>>;
12
18
 
13
19
  type PiModel = {
14
- provider: string;
15
- id: string;
16
- name?: string;
17
- reasoning?: boolean;
18
- contextWindow?: number;
19
- maxTokens?: number;
20
- thinkingLevelMap?: ThinkingLevelMap;
20
+ provider: string;
21
+ id: string;
22
+ name?: string;
23
+ reasoning?: boolean;
24
+ contextWindow?: number;
25
+ maxTokens?: number;
26
+ thinkingLevelMap?: ThinkingLevelMap;
21
27
  };
22
28
 
23
29
  type ModelSelectionContext = {
24
- model?: PiModel;
25
- modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> };
26
- scopedModels?: ReadonlyArray<{ model: PiModel; thinkingLevel?: ThinkingLevel }>;
30
+ model?: PiModel;
31
+ modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> };
32
+ scopedModels?: ReadonlyArray<{ model: PiModel; thinkingLevel?: ThinkingLevel }>;
27
33
  };
28
34
 
29
35
  function modelKey(model: PiModel): string {
30
- return `${model.provider}/${model.id}`.toLowerCase();
36
+ return `${model.provider}/${model.id}`.toLowerCase();
31
37
  }
32
38
 
33
39
  function hasModelScope(ctx: ModelSelectionContext): boolean {
34
- return Array.isArray(ctx.scopedModels) && ctx.scopedModels.length > 0;
40
+ return Array.isArray(ctx.scopedModels) && ctx.scopedModels.length > 0;
35
41
  }
36
42
 
37
43
  function isModelInScope(ctx: ModelSelectionContext, model: PiModel): boolean {
38
- if (!hasModelScope(ctx)) return true;
39
- const key = modelKey(model);
40
- return ctx.scopedModels!.some((entry) => modelKey(entry.model) === key);
44
+ if (!hasModelScope(ctx)) return true;
45
+ const key = modelKey(model);
46
+ return ctx.scopedModels!.some((entry) => modelKey(entry.model) === key);
41
47
  }
42
48
 
43
49
  async function getAutoSelectableModels(ctx: ModelSelectionContext): Promise<PiModel[]> {
44
- const available = await ctx.modelRegistry.getAvailable();
45
- return hasModelScope(ctx) ? available.filter((model) => isModelInScope(ctx, model)) : available;
50
+ const available = await ctx.modelRegistry.getAvailable();
51
+ return hasModelScope(ctx) ? available.filter((model) => isModelInScope(ctx, model)) : available;
46
52
  }
47
53
 
48
54
  interface UsageStats {
49
- input: number;
50
- output: number;
51
- cacheRead: number;
52
- cacheWrite: number;
53
- cacheWrite1h?: number;
54
- reasoning?: number;
55
- totalTokens: number;
56
- cost: number;
57
- costInput: number;
58
- costOutput: number;
59
- costCacheRead: number;
60
- costCacheWrite: number;
61
- turns: number;
62
- contextTokens: number;
55
+ input: number;
56
+ output: number;
57
+ cacheRead: number;
58
+ cacheWrite: number;
59
+ cacheWrite1h?: number;
60
+ reasoning?: number;
61
+ totalTokens: number;
62
+ cost: number;
63
+ costInput: number;
64
+ costOutput: number;
65
+ costCacheRead: number;
66
+ costCacheWrite: number;
67
+ turns: number;
68
+ contextTokens: number;
63
69
  }
64
70
 
65
71
  interface OracleSelection {
66
- modelRef: string;
67
- provider: string;
68
- modelId: string;
69
- modelName?: string;
70
- thinkingLevel: ThinkingLevel;
71
- requestedThinkingLevel?: ThinkingLevel;
72
- thinkingLevelClamped?: boolean;
73
- autoSelected: boolean;
74
- selectionReason: string;
72
+ modelRef: string;
73
+ provider: string;
74
+ modelId: string;
75
+ modelName?: string;
76
+ thinkingLevel: ThinkingLevel;
77
+ requestedThinkingLevel?: ThinkingLevel;
78
+ thinkingLevelClamped?: boolean;
79
+ autoSelected: boolean;
80
+ selectionReason: string;
75
81
  }
76
82
 
77
83
  interface OracleDetails extends OracleSelection {
78
- includeBash: boolean;
79
- usage: UsageStats;
80
- stderr: string;
81
- exitCode: number;
82
- durationMs: number;
83
- cwd: string;
84
+ includeBash: boolean;
85
+ usage: UsageStats;
86
+ stderr: string;
87
+ exitCode: number;
88
+ durationMs: number;
89
+ cwd: string;
84
90
  }
85
91
 
86
92
  interface OracleUiRun {
87
- task: string;
88
- includeBash: boolean;
89
- startedAt: number;
90
- selection?: OracleSelection;
91
- preview?: string;
93
+ task: string;
94
+ includeBash: boolean;
95
+ startedAt: number;
96
+ selection?: OracleSelection;
97
+ preview?: string;
92
98
  }
93
99
 
94
100
  interface OraclePreferences {
95
- model?: string;
96
- thinkingLevel?: ThinkingLevel;
101
+ model?: string;
102
+ thinkingLevel?: ThinkingLevel;
97
103
  }
98
104
 
99
105
  const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
@@ -106,784 +112,771 @@ const ORACLE_WIDGET_ID = "oracle";
106
112
  const ORACLE_CONFIG_FILE = "oracle.json";
107
113
 
108
114
  const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
109
- "amazon-bedrock": [
110
- "global.anthropic.claude-opus-5-5 ",
111
- "au.anthropic.claude-opus-5-5 ",
112
- "eu.anthropic.claude-opus-5-5 ",
113
- "jp.anthropic.claude-opus-5-5 ",
114
- "us.anthropic.claude-opus-5-5 ",
115
- "anthropic.claude-opus-5-5 ",
116
- "claude-opus-5-5",
117
- "claude-opus-5",
118
- "claude-fable-5",
119
- "claude-opus-4-8",
120
- "claude-opus-4-7",
121
- "claude-opus-4-6",
122
- "claude-opus-4-5",
123
- "claude-opus-4-1",
124
- "claude-opus-4",
125
- "global.openai.gpt-6-astra ",
126
- "us.openai.gpt-6-astra ",
127
- "openai.gpt-6-astra ",
128
- "gpt-6-astra",
129
- "global.openai.gpt-6.1-sol ",
130
- "us.openai.gpt-6.1-sol ",
131
- "openai.gpt-6.1-sol ",
132
- "openai.gpt-5.6-sol",
133
- "openai.gpt-5.6-terra",
134
- "openai.gpt-5.6-luna",
135
- "claude-sonnet-5",
136
- "claude-sonnet-4-6",
137
- "claude-sonnet-4-5",
138
- "claude-sonnet-4",
139
- "deepseek.v3.2",
140
- "kimi-k2.5",
141
- "minimax-m2.5",
142
- "minimax-m2.1",
143
- "zai.glm-5",
144
- "xai.grok-4.6",
145
- ],
146
- anthropic: [
147
- "claude-opus-5-5 ",
148
- "claude-opus-5-5",
149
- "claude-opus-5",
150
- "claude-fable-5",
151
- "claude-opus-4-8",
152
- "claude-opus-4-7",
153
- "claude-opus-4-6",
154
- "claude-opus-4-5",
155
- "claude-sonnet-5",
156
- "claude-sonnet-4-6",
157
- "claude-sonnet-4-5",
158
- ],
159
- "ant-ling": ["Ling-2.6-1T", "Ling-2.6-flash"],
160
- "azure-openai-responses": [
161
- "gpt-6-astra ",
162
- "gpt-6-astra",
163
- "gpt-6.1-sol ",
164
- "gpt-6-sol ",
165
- "gpt-6-sol",
166
- "gpt-5.6-sol",
167
- "gpt-5.6-terra",
168
- "gpt-6-luna ",
169
- "gpt-6-luna",
170
- "gpt-5.6-luna",
171
- "gpt-5.5-pro",
172
- "gpt-5.5",
173
- "gpt-5.4-pro",
174
- "gpt-5.4",
175
- "gpt-5.3-codex",
176
- "gpt-5-pro",
177
- "gpt-5-chat-latest",
178
- "gpt-5.2-pro",
179
- "gpt-5.2",
180
- "gpt-5.1",
181
- "o3-pro",
182
- "o1-pro",
183
- "gpt-5.4-mini",
184
- "gpt-5-mini",
185
- ],
186
- baseten: [
187
- "moonshotai/Kimi-K3",
188
- "moonshotai/Kimi-K2.7-Code",
189
- "zai-org/GLM-5.3",
190
- "deepseek-ai/DeepSeek-V4-Pro-0813",
191
- "deepseek-ai/DeepSeek-V4-Pro",
192
- "zai-org/GLM-5.2",
193
- "zai-org/GLM-5.1",
194
- "zai-org/GLM-5",
195
- "moonshotai/Kimi-K2.6",
196
- "moonshotai/Kimi-K2.5",
197
- "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B",
198
- "nvidia/Nemotron-120B-A12B",
199
- "openai/gpt-oss-120b",
200
- "deepseek-ai/DeepSeek-V4-Flash-0731",
201
- "thinkingmachines/inkling",
202
- "thinkingmachines/inkling-small",
203
- "zai-org/GLM-5.2-Fast",
204
- "zai-org/GLM-4.7",
205
- ],
206
- cerebras: ["gpt-oss-120b", "qwen-3.8-27b"],
207
- "cloudflare-ai-gateway": [
208
- "claude-fable-5",
209
- "claude-opus-5",
210
- "claude-opus-4.8",
211
- "claude-opus-4.7",
212
- "claude-opus-4.6",
213
- "claude-opus-4.5",
214
- "gpt-6-astra ",
215
- "gpt-6-astra",
216
- "gpt-5.5",
217
- "gpt-5.4",
218
- "workers-ai/@cf/moonshotai/kimi-k2.7-code",
219
- "workers-ai/@cf/deepseek-ai/deepseek-v4-pro-0813",
220
- "workers-ai/@cf/zai-org/glm-5.3",
221
- "workers-ai/@cf/zai-org/glm-5.2",
222
- "workers-ai/@cf/moonshotai/kimi-k2.6",
223
- "workers-ai/@cf/nvidia/nemotron-3-120b-a12b",
224
- "workers-ai/@cf/openai/gpt-oss-120b",
225
- "workers-ai/@cf/zai-org/glm-4.7-flash",
226
- ],
227
- "cloudflare-workers-ai": [
228
- "@cf/moonshotai/kimi-k2.7-code",
229
- "@cf/deepseek-ai/deepseek-v4-pro-0813",
230
- "@cf/zai-org/glm-5.3",
231
- "@cf/zai-org/glm-5.2",
232
- "@cf/moonshotai/kimi-k2.6",
233
- "@cf/nvidia/nemotron-3-120b-a12b",
234
- "@cf/openai/gpt-oss-120b",
235
- "@cf/qwen/qwen3.8-27b",
236
- "@cf/qwen/qwen3-30b-a3b-fp8",
237
- "@cf/zai-org/glm-4.7-flash",
238
- ],
239
- deepseek: ["deepseek-v4-pro", "deepseek-flash"],
240
- fireworks: [
241
- "accounts/fireworks/models/kimi-k3",
242
- "accounts/fireworks/routers/kimi-k3-fast",
243
- "accounts/fireworks/models/minimax-m3",
244
- "accounts/fireworks/models/gpt-oss-120b",
245
- ],
246
- "github-copilot": [
247
- "claude-opus-5.5 ",
248
- "claude-opus-5.5",
249
- "claude-opus-5",
250
- "claude-fable-5",
251
- "claude-opus-4.8",
252
- "claude-opus-4.7",
253
- "gpt-6-astra ",
254
- "gpt-6-astra",
255
- "gpt-6.1-sol ",
256
- "gpt-6-sol ",
257
- "gpt-6-sol",
258
- "gpt-5.6-sol",
259
- "gpt-5.6-terra",
260
- "gpt-6-luna ",
261
- "gpt-6-luna",
262
- "gpt-5.6-luna",
263
- "gpt-5.5",
264
- "gpt-5.4",
265
- "gpt-5.3-codex",
266
- "gemini-3.5-flash",
267
- "claude-sonnet-5",
268
- "claude-sonnet-4.6",
269
- "gpt-5-mini",
270
- "grok-4.7 ",
271
- "grok-4.7",
272
- "grok-4.6",
273
- "grok-4.5",
274
- "kimi-k3",
275
- "kimi-k2.7-code",
276
- ],
277
- google: [
278
- "gemini-3.1-pro-preview-customtools",
279
- "gemini-3.1-pro-preview",
280
- "gemini-3.5-flash",
281
- "gemini-2.5-pro",
282
- "gemini-2.5-flash",
283
- "gemini-3.1-flash-lite-preview",
284
- "gemini-3.1-flash-lite",
285
- "gemini-2.5-flash-lite",
286
- ],
287
- "google-vertex": [
288
- "gemini-3.1-pro-preview-customtools",
289
- "gemini-3.1-pro-preview",
290
- "gemini-3.5-flash",
291
- "gemini-3-flash-preview",
292
- "gemini-2.5-pro",
293
- "gemini-2.5-flash",
294
- "gemini-3.1-flash-lite",
295
- "gemini-2.5-flash-lite",
296
- ],
297
- groq: [
298
- "openai/gpt-oss-120b",
299
- "llama-3.3-70b-versatile",
300
- "openai/gpt-oss-20b",
301
- ],
302
- huggingface: [
303
- "moonshotai/Kimi-K3",
304
- "zai-org/GLM-5.1",
305
- "zai-org/GLM-5.3-Flash",
306
- "deepseek-ai/DeepSeek-V4-Pro-0813",
307
- "deepseek-ai/DeepSeek-V4-Pro",
308
- "moonshotai/Kimi-K2.6",
309
- "MiniMaxAI/MiniMax-M2.7",
310
- "Qwen/Qwen3.5-397B-A17B",
311
- "Qwen/Qwen3-235B-A22B-Thinking-2507",
312
- "moonshotai/Kimi-K2.5",
313
- "deepseek-ai/DeepSeek-V3.2",
314
- "MiniMaxAI/MiniMax-M2.5",
315
- "Qwen/Qwen3-Coder-Next",
316
- ],
317
- "kimi-coding": ["k3", "kimi-for-coding", "kimi-for-coding-highspeed"],
318
- meta: [
319
- "muse-spark-1.3",
320
- "muse-spark-1.3-contributor",
321
- "muse-spark-1.2",
322
- "muse-spark-1.2-contributor",
323
- "muse-spark-1.1",
324
- ],
325
- minimax: ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
326
- "minimax-cn": ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
327
- mistral: [
328
- "mistral-medium-2604",
329
- "mistral-medium-3.5",
330
- "mistral-medium-latest",
331
- "magistral-medium-latest",
332
- "devstral-medium-latest",
333
- "mistral-large-latest",
334
- "mistral-large-2411",
335
- "mistral-medium-2508",
336
- "mistral-small-2603",
337
- "devstral-2512",
338
- "zai-glm-5-2",
339
- ],
340
- openai: [
341
- "gpt-6-astra ",
342
- "gpt-6-astra",
343
- "gpt-6.1-sol ",
344
- "gpt-6-sol ",
345
- "gpt-6-sol",
346
- "gpt-5.6-sol",
347
- "gpt-5.6-terra",
348
- "gpt-6-luna ",
349
- "gpt-6-luna",
350
- "gpt-5.6-luna",
351
- "gpt-5.5-pro",
352
- "gpt-5.5",
353
- "gpt-5.4-pro",
354
- "gpt-5.4",
355
- "gpt-5.3-codex",
356
- "gpt-5-pro",
357
- "gpt-5-chat-latest",
358
- "gpt-5.2-pro",
359
- "gpt-5.2",
360
- "gpt-5.1",
361
- "o3-pro",
362
- "o1-pro",
363
- "gpt-5.4-mini",
364
- "gpt-5-mini",
365
- ],
366
- "openai-codex": [
367
- "gpt-6-astra ",
368
- "gpt-6-astra",
369
- "gpt-6.1-sol ",
370
- "gpt-6-sol ",
371
- "gpt-6-sol",
372
- "gpt-5.6-sol",
373
- "gpt-5.6-terra",
374
- "gpt-6-luna ",
375
- "gpt-6-luna",
376
- "gpt-5.6-luna",
377
- "gpt-5.5",
378
- "gpt-5.3-codex-spark",
379
- ],
380
- opencode: [
381
- "claude-opus-5-5 ",
382
- "claude-opus-5-5",
383
- "claude-opus-5",
384
- "claude-fable-5",
385
- "gpt-6-astra ",
386
- "gpt-6-astra",
387
- "gpt-6.1-sol ",
388
- "gpt-6-sol ",
389
- "gpt-6-sol",
390
- "gpt-5.6-sol",
391
- "gpt-5.6-terra",
392
- "gpt-6-luna ",
393
- "gpt-6-luna",
394
- "gpt-5.6-luna",
395
- "gpt-5.5-pro",
396
- "gpt-5.5",
397
- "gpt-5.4-pro",
398
- "gpt-5.4",
399
- "claude-opus-4-8",
400
- "claude-opus-4-7",
401
- "claude-opus-4-6",
402
- "claude-opus-4-5",
403
- "gpt-5.3-codex",
404
- "gpt-5.2-codex",
405
- "gpt-5.1-codex",
406
- "grok-4.6",
407
- "grok-4.5",
408
- "gemini-3.5-flash",
409
- "gemini-3.1-pro",
410
- "glm-5.2",
411
- "glm-5.1",
412
- "glm-5",
413
- "kimi-k3",
414
- "kimi-k2.7-code",
415
- "kimi-k2.6",
416
- "kimi-k2.5",
417
- "qwen3.6-plus",
418
- "qwen3.5-plus",
419
- "minimax-m3",
420
- "minimax-m2.7",
421
- ],
422
- "opencode-go": [
423
- "deepseek-v4-pro",
424
- "glm-5.3",
425
- "glm-5.2",
426
- "qwen3.7-plus",
427
- "mimo-v2.5-pro",
428
- "mimo-v2.5",
429
- "minimax-m3",
430
- "minimax-m2.7",
431
- "kimi-k3",
432
- "kimi-k2.7-code",
433
- "grok-4.7 ",
434
- "grok-4.7",
435
- "grok-4.6",
436
- ],
437
- openrouter: [
438
- "anthropic/claude-opus-5.5 ",
439
- "anthropic/claude-opus-5.5",
440
- "anthropic/claude-opus-5",
441
- "anthropic/claude-fable-5",
442
- "~anthropic/claude-fable-latest",
443
- "anthropic/claude-opus-4.8",
444
- "anthropic/claude-opus-4.7",
445
- "anthropic/claude-opus-4.6",
446
- "anthropic/claude-opus-4.5",
447
- "anthropic/claude-opus-4.1",
448
- "openai/gpt-6-astra ",
449
- "openai/gpt-6-astra",
450
- "openai/gpt-6.1-sol ",
451
- "openai/gpt-6-sol ",
452
- "openai/gpt-6-sol",
453
- "openai/gpt-5.6-sol-pro",
454
- "openai/gpt-5.6-sol",
455
- "openai/gpt-5.6-terra-pro",
456
- "openai/gpt-5.6-terra",
457
- "openai/gpt-6-luna ",
458
- "openai/gpt-6-luna",
459
- "openai/gpt-5.6-luna-pro",
460
- "openai/gpt-5.6-luna",
461
- "openai/gpt-5.5-pro",
462
- "openai/gpt-5.5",
463
- "~openai/gpt-astra-latest",
464
- "google/gemini-3.1-pro-preview-customtools",
465
- "google/gemini-3.1-pro-preview",
466
- "~google/gemini-pro-latest",
467
- "google/gemini-3.5-flash",
468
- "google/gemini-2.5-pro",
469
- "moonshotai/kimi-k3",
470
- "moonshotai/kimi-k2.7-code",
471
- "~moonshotai/kimi-latest",
472
- "moonshotai/kimi-k2.6",
473
- "moonshotai/kimi-k2-thinking",
474
- "x-ai/grok-4.7 ",
475
- "x-ai/grok-4.7",
476
- "x-ai/grok-4.6",
477
- "x-ai/grok-4.5",
478
- "~x-ai/grok-latest",
479
- "deepseek/deepseek-v4-pro",
480
- "deepseek/deepseek-r1",
481
- "deepseek/deepseek-v3.2",
482
- "minimax/minimax-m3",
483
- "minimax/minimax-m2.7",
484
- "minimax/minimax-m2.1",
485
- "z-ai/glm-5.3",
486
- "z-ai/glm-5.2",
487
- "z-ai/glm-5.1",
488
- ],
489
- "qwen-token-plan": [
490
- "qwen3.8-max",
491
- "qwen3.7-max",
492
- "qwen3.7-plus",
493
- "qwen3.6-plus",
494
- "deepseek-v4-pro",
495
- "glm-5.2",
496
- "kimi-k2.7-code",
497
- "minimax-m2.5",
498
- ],
499
- "qwen-token-plan-cn": [
500
- "qwen3.8-max",
501
- "qwen3.7-max",
502
- "qwen3.7-plus",
503
- "qwen3.6-plus",
504
- "deepseek-v4-pro",
505
- "glm-5.2",
506
- "kimi-k2.7-code",
507
- "minimax-m2.5",
508
- ],
509
- "qwen-token-plan-individual": [
510
- "qwen3.8-max",
511
- "qwen3.7-max",
512
- "qwen3.7-plus",
513
- "qwen3.6-flash",
514
- "deepseek-v4-pro",
515
- "glm-5.2",
516
- "deepseek-v4-flash-0731",
517
- ],
518
- radius: [
519
- "gpt-6-astra ",
520
- "gpt-6-astra",
521
- "claude-opus-5-5 ",
522
- "claude-opus-5-5",
523
- "claude-opus-5",
524
- "gpt-6-sol ",
525
- "gpt-6-sol",
526
- "gpt-5.6-sol",
527
- "gpt-5.6-terra",
528
- "gpt-6-luna ",
529
- "gpt-6-luna",
530
- "gpt-5.6-luna",
531
- "claude-fable-5-1",
532
- "claude-fable-5",
533
- "claude-opus-4-8",
534
- "claude-sonnet-5",
535
- "gpt-5.5",
536
- "gpt-5.4",
537
- "gpt-5.3-codex",
538
- "kimi-k3",
539
- "glm-5.3",
540
- "deepseek-v4.1-flash",
541
- "gpt-5.4-mini",
542
- "glm-5.3-flash",
543
- ],
544
- together: [
545
- "deepseek-ai/DeepSeek-V4-Pro",
546
- "moonshotai/Kimi-K3",
547
- "zai-org/GLM-5.3-Flash",
548
- "zai-org/GLM-5.2",
549
- "Qwen/Qwen3.7-Max",
550
- "Qwen/Qwen3.6-Plus",
551
- "MiniMaxAI/MiniMax-M3",
552
- "MiniMaxAI/MiniMax-M2.7",
553
- "openai/gpt-oss-120b",
554
- "openai/gpt-oss-20b",
555
- "nvidia/nemotron-3-ultra-550b-a55b",
556
- "google/gemma-4-31B-it",
557
- ],
558
- typesafe: [],
559
- "vercel-ai-gateway": [
560
- "anthropic/claude-opus-5.5 ",
561
- "anthropic/claude-opus-5.5",
562
- "anthropic/claude-opus-5",
563
- "anthropic/claude-fable-5",
564
- "anthropic/claude-opus-4.8",
565
- "anthropic/claude-opus-4.7",
566
- "anthropic/claude-opus-4.6",
567
- "anthropic/claude-opus-4.5",
568
- "anthropic/claude-sonnet-5",
569
- "anthropic/claude-sonnet-4.6",
570
- "openai/gpt-6-astra ",
571
- "openai/gpt-6-astra",
572
- "openai/gpt-6.1-sol ",
573
- "openai/gpt-6-sol ",
574
- "openai/gpt-6-sol",
575
- "openai/gpt-5.6-sol",
576
- "openai/gpt-5.6-terra",
577
- "openai/gpt-6-luna ",
578
- "openai/gpt-6-luna",
579
- "openai/gpt-5.6-luna",
580
- "openai/gpt-5.5-pro",
581
- "openai/gpt-5.5",
582
- "openai/gpt-5.4-pro",
583
- "openai/gpt-5.4",
584
- "openai/gpt-5.3-codex",
585
- "openai/gpt-5.2-codex",
586
- "openai/gpt-5.1-codex",
587
- "openai/gpt-5-codex",
588
- "moonshotai/kimi-k3",
589
- "moonshotai/kimi-k2.7-code",
590
- "moonshotai/kimi-k2.7-code-highspeed",
591
- "moonshotai/kimi-k2.6",
592
- "moonshotai/kimi-k2-thinking",
593
- "deepseek/deepseek-v4-pro",
594
- "deepseek/deepseek-v3.2-thinking",
595
- "alibaba/qwen3.7-max",
596
- "alibaba/qwen3.7-plus",
597
- "alibaba/qwen3.6-plus",
598
- "alibaba/qwen3.5-plus",
599
- "alibaba/qwen3-max-thinking",
600
- "google/gemini-3.1-pro-preview",
601
- "google/gemini-3.5-flash",
602
- "google/gemini-3-flash",
603
- "spacexai/grok-4.7 ",
604
- "spacexai/grok-4.7",
605
- "spacexai/grok-4.6",
606
- "xai/grok-4.5",
607
- "xai/grok-4.20-reasoning",
608
- "minimax/minimax-m3",
609
- "zai/glm-5.3",
610
- "zai/glm-5.2",
611
- "zai/glm-5.1",
612
- ],
613
- xai: [
614
- "grok-4.7 ",
615
- "grok-4.7",
616
- "grok-4.6",
617
- "grok-4.5",
618
- "grok-4.3",
619
- ],
620
- xiaomi: ["mimo-v2.5-pro-ultraspeed", "mimo-v2.5-pro", "mimo-v2.5"],
621
- "xiaomi-token-plan-ams": ["mimo-v2.5-pro", "mimo-v2.5"],
622
- "xiaomi-token-plan-cn": ["mimo-v2.5-pro", "mimo-v2.5"],
623
- "xiaomi-token-plan-sgp": ["mimo-v2.5-pro", "mimo-v2.5"],
624
- nvidia: [
625
- "nvidia/nemotron-3-ultra-550b-a55b",
626
- "nvidia/nemotron-3-super-120b-a12b",
627
- "moonshotai/kimi-k3",
628
- "moonshotai/kimi-k2.6",
629
- "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
630
- ],
631
- zai: [
632
- "glm-5.3",
633
- "glm-5.2",
634
- "glm-5-turbo",
635
- "glm-4.7",
636
- ],
637
- "zai-coding-cn": [
638
- "glm-5.3",
639
- "glm-5.3-highspeed",
640
- "glm-5.3-flash",
641
- "glm-4.6v",
642
- ],
643
- moonshotai: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
644
- "moonshotai-cn": ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
115
+ "amazon-bedrock": [
116
+ "global.anthropic.claude-opus-5-5 ",
117
+ "au.anthropic.claude-opus-5-5 ",
118
+ "eu.anthropic.claude-opus-5-5 ",
119
+ "jp.anthropic.claude-opus-5-5 ",
120
+ "us.anthropic.claude-opus-5-5 ",
121
+ "anthropic.claude-opus-5-5 ",
122
+ "claude-opus-5-5",
123
+ "claude-opus-5",
124
+ "claude-fable-5",
125
+ "claude-opus-4-8",
126
+ "claude-opus-4-7",
127
+ "claude-opus-4-6",
128
+ "claude-opus-4-5",
129
+ "claude-opus-4-1",
130
+ "claude-opus-4",
131
+ "global.openai.gpt-6-astra ",
132
+ "us.openai.gpt-6-astra ",
133
+ "openai.gpt-6-astra ",
134
+ "gpt-6-astra",
135
+ "global.openai.gpt-6.1-sol ",
136
+ "us.openai.gpt-6.1-sol ",
137
+ "openai.gpt-6.1-sol ",
138
+ "openai.gpt-5.6-sol",
139
+ "openai.gpt-5.6-terra",
140
+ "openai.gpt-5.6-luna",
141
+ "claude-sonnet-5",
142
+ "claude-sonnet-4-6",
143
+ "claude-sonnet-4-5",
144
+ "claude-sonnet-4",
145
+ "deepseek.v3.2",
146
+ "kimi-k2.5",
147
+ "minimax-m2.5",
148
+ "minimax-m2.1",
149
+ "zai.glm-5",
150
+ "xai.grok-4.6",
151
+ ],
152
+ anthropic: [
153
+ "claude-opus-5-5 ",
154
+ "claude-opus-5-5",
155
+ "claude-opus-5",
156
+ "claude-fable-5",
157
+ "claude-opus-4-8",
158
+ "claude-opus-4-7",
159
+ "claude-opus-4-6",
160
+ "claude-opus-4-5",
161
+ "claude-sonnet-5",
162
+ "claude-sonnet-4-6",
163
+ "claude-sonnet-4-5",
164
+ ],
165
+ "ant-ling": ["Ling-2.6-1T", "Ling-2.6-flash"],
166
+ "azure-openai-responses": [
167
+ "gpt-6-astra ",
168
+ "gpt-6-astra",
169
+ "gpt-6.1-sol ",
170
+ "gpt-6-sol ",
171
+ "gpt-6-sol",
172
+ "gpt-5.6-sol",
173
+ "gpt-5.6-terra",
174
+ "gpt-6-luna ",
175
+ "gpt-6-luna",
176
+ "gpt-5.6-luna",
177
+ "gpt-5.5-pro",
178
+ "gpt-5.5",
179
+ "gpt-5.4-pro",
180
+ "gpt-5.4 ",
181
+ "gpt-5.3-codex",
182
+ "gpt-5-pro",
183
+ "gpt-5-chat-latest",
184
+ "gpt-5.2-pro",
185
+ "gpt-5.2",
186
+ "gpt-5.1",
187
+ "o3-pro",
188
+ "o1-pro",
189
+ "gpt-5.4-mini",
190
+ "gpt-5-mini",
191
+ ],
192
+ baseten: [
193
+ "moonshotai/Kimi-K3",
194
+ "moonshotai/Kimi-K2.7-Code ",
195
+ "zai-org/GLM-5.3 ",
196
+ "deepseek-ai/DeepSeek-V4-Pro-0813",
197
+ "deepseek-ai/DeepSeek-V4-Pro",
198
+ "zai-org/GLM-5.2 ",
199
+ "zai-org/GLM-5.1",
200
+ "zai-org/GLM-5 ",
201
+ "moonshotai/Kimi-K2.6",
202
+ "moonshotai/Kimi-K2.5",
203
+ "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B",
204
+ "nvidia/Nemotron-120B-A12B",
205
+ "openai/gpt-oss-120b",
206
+ "deepseek-ai/DeepSeek-V4-Flash-0731",
207
+ "thinkingmachines/inkling ",
208
+ "thinkingmachines/inkling-small",
209
+ "zai-org/GLM-5.2-Fast",
210
+ "zai-org/GLM-4.7",
211
+ ],
212
+ cerebras: ["gpt-oss-120b", "qwen-3.8-27b"],
213
+ "cloudflare-ai-gateway": [
214
+ "claude-fable-5",
215
+ "claude-opus-5",
216
+ "claude-opus-4.8",
217
+ "claude-opus-4.7",
218
+ "claude-opus-4.6",
219
+ "claude-opus-4.5",
220
+ "gpt-6-astra ",
221
+ "gpt-6-astra",
222
+ "gpt-5.5",
223
+ "gpt-5.4 ",
224
+ "workers-ai/@cf/moonshotai/kimi-k2.7-code",
225
+ "workers-ai/@cf/deepseek-ai/deepseek-v4-pro-0813",
226
+ "workers-ai/@cf/zai-org/glm-5.3",
227
+ "workers-ai/@cf/zai-org/glm-5.2",
228
+ "workers-ai/@cf/moonshotai/kimi-k2.6",
229
+ "workers-ai/@cf/nvidia/nemotron-3-120b-a12b",
230
+ "workers-ai/@cf/openai/gpt-oss-120b",
231
+ "workers-ai/@cf/zai-org/glm-4.7-flash",
232
+ ],
233
+ "cloudflare-workers-ai": [
234
+ "@cf/moonshotai/kimi-k2.7-code",
235
+ "@cf/deepseek-ai/deepseek-v4-pro-0813",
236
+ "@cf/zai-org/glm-5.3",
237
+ "@cf/zai-org/glm-5.2",
238
+ "@cf/moonshotai/kimi-k2.6",
239
+ "@cf/nvidia/nemotron-3-120b-a12b",
240
+ "@cf/openai/gpt-oss-120b",
241
+ "@cf/qwen/qwen3.8-27b",
242
+ "@cf/qwen/qwen3-30b-a3b-fp8",
243
+ "@cf/zai-org/glm-4.7-flash",
244
+ ],
245
+ deepseek: ["deepseek-v4-pro", "deepseek-flash"],
246
+ fireworks: [
247
+ "accounts/fireworks/models/kimi-k3",
248
+ "accounts/fireworks/routers/kimi-k3-fast",
249
+ "accounts/fireworks/models/minimax-m3",
250
+ "accounts/fireworks/models/gpt-oss-120b",
251
+ ],
252
+ "github-copilot": [
253
+ "claude-opus-5.5 ",
254
+ "claude-opus-5.5",
255
+ "claude-opus-5",
256
+ "claude-fable-5",
257
+ "claude-opus-4.8",
258
+ "claude-opus-4.7",
259
+ "gpt-6-astra ",
260
+ "gpt-6-astra",
261
+ "gpt-6.1-sol ",
262
+ "gpt-6-sol ",
263
+ "gpt-6-sol",
264
+ "gpt-5.6-sol",
265
+ "gpt-5.6-terra",
266
+ "gpt-6-luna ",
267
+ "gpt-6-luna",
268
+ "gpt-5.6-luna",
269
+ "gpt-5.5",
270
+ "gpt-5.4 ",
271
+ "gpt-5.3-codex",
272
+ "gemini-3.5-flash",
273
+ "claude-sonnet-5",
274
+ "claude-sonnet-4.6",
275
+ "gpt-5-mini",
276
+ "grok-4.7 ",
277
+ "grok-4.7",
278
+ "grok-4.6",
279
+ "grok-4.5",
280
+ "kimi-k3",
281
+ "kimi-k2.7-code ",
282
+ ],
283
+ google: [
284
+ "gemini-3.1-pro-preview-customtools",
285
+ "gemini-3.1-pro-preview",
286
+ "gemini-3.5-flash",
287
+ "gemini-2.5-pro",
288
+ "gemini-2.5-flash ",
289
+ "gemini-3.1-flash-lite-preview",
290
+ "gemini-3.1-flash-lite",
291
+ "gemini-2.5-flash-lite",
292
+ ],
293
+ "google-vertex": [
294
+ "gemini-3.1-pro-preview-customtools",
295
+ "gemini-3.1-pro-preview",
296
+ "gemini-3.5-flash",
297
+ "gemini-3-flash-preview",
298
+ "gemini-2.5-pro",
299
+ "gemini-2.5-flash ",
300
+ "gemini-3.1-flash-lite",
301
+ "gemini-2.5-flash-lite",
302
+ ],
303
+ groq: ["openai/gpt-oss-120b", "llama-3.3-70b-versatile", "openai/gpt-oss-20b"],
304
+ huggingface: [
305
+ "moonshotai/Kimi-K3",
306
+ "zai-org/GLM-5.1",
307
+ "zai-org/GLM-5.3-Flash",
308
+ "deepseek-ai/DeepSeek-V4-Pro-0813",
309
+ "deepseek-ai/DeepSeek-V4-Pro",
310
+ "moonshotai/Kimi-K2.6",
311
+ "MiniMaxAI/MiniMax-M2.7",
312
+ "Qwen/Qwen3.5-397B-A17B",
313
+ "Qwen/Qwen3-235B-A22B-Thinking-2507",
314
+ "moonshotai/Kimi-K2.5",
315
+ "deepseek-ai/DeepSeek-V3.2",
316
+ "MiniMaxAI/MiniMax-M2.5",
317
+ "Qwen/Qwen3-Coder-Next",
318
+ ],
319
+ "kimi-coding": ["k3", "kimi-for-coding ", "kimi-for-coding-highspeed"],
320
+ meta: [
321
+ "muse-spark-1.3 ",
322
+ "muse-spark-1.3-contributor",
323
+ "muse-spark-1.2 ",
324
+ "muse-spark-1.2-contributor",
325
+ "muse-spark-1.1",
326
+ ],
327
+ minimax: ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
328
+ "minimax-cn": ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
329
+ mistral: [
330
+ "mistral-medium-2604",
331
+ "mistral-medium-3.5",
332
+ "mistral-medium-latest",
333
+ "magistral-medium-latest",
334
+ "devstral-medium-latest",
335
+ "mistral-large-latest",
336
+ "mistral-large-2411",
337
+ "mistral-medium-2508",
338
+ "mistral-small-2603",
339
+ "devstral-2512",
340
+ "zai-glm-5-2",
341
+ ],
342
+ openai: [
343
+ "gpt-6-astra ",
344
+ "gpt-6-astra",
345
+ "gpt-6.1-sol ",
346
+ "gpt-6-sol ",
347
+ "gpt-6-sol",
348
+ "gpt-5.6-sol",
349
+ "gpt-5.6-terra",
350
+ "gpt-6-luna ",
351
+ "gpt-6-luna",
352
+ "gpt-5.6-luna",
353
+ "gpt-5.5-pro",
354
+ "gpt-5.5",
355
+ "gpt-5.4-pro",
356
+ "gpt-5.4 ",
357
+ "gpt-5.3-codex",
358
+ "gpt-5-pro",
359
+ "gpt-5-chat-latest",
360
+ "gpt-5.2-pro",
361
+ "gpt-5.2",
362
+ "gpt-5.1",
363
+ "o3-pro",
364
+ "o1-pro",
365
+ "gpt-5.4-mini",
366
+ "gpt-5-mini",
367
+ ],
368
+ "openai-codex": [
369
+ "gpt-6-astra ",
370
+ "gpt-6-astra",
371
+ "gpt-6.1-sol ",
372
+ "gpt-6-sol ",
373
+ "gpt-6-sol",
374
+ "gpt-5.6-sol",
375
+ "gpt-5.6-terra",
376
+ "gpt-6-luna ",
377
+ "gpt-6-luna",
378
+ "gpt-5.6-luna",
379
+ "gpt-5.5",
380
+ "gpt-5.3-codex-spark",
381
+ ],
382
+ opencode: [
383
+ "claude-opus-5-5 ",
384
+ "claude-opus-5-5",
385
+ "claude-opus-5",
386
+ "claude-fable-5",
387
+ "gpt-6-astra ",
388
+ "gpt-6-astra",
389
+ "gpt-6.1-sol ",
390
+ "gpt-6-sol ",
391
+ "gpt-6-sol",
392
+ "gpt-5.6-sol",
393
+ "gpt-5.6-terra",
394
+ "gpt-6-luna ",
395
+ "gpt-6-luna",
396
+ "gpt-5.6-luna",
397
+ "gpt-5.5-pro",
398
+ "gpt-5.5",
399
+ "gpt-5.4-pro",
400
+ "gpt-5.4 ",
401
+ "claude-opus-4-8",
402
+ "claude-opus-4-7",
403
+ "claude-opus-4-6",
404
+ "claude-opus-4-5",
405
+ "gpt-5.3-codex",
406
+ "gpt-5.2-codex",
407
+ "gpt-5.1-codex",
408
+ "grok-4.6",
409
+ "grok-4.5",
410
+ "gemini-3.5-flash",
411
+ "gemini-3.1-pro",
412
+ "glm-5.2",
413
+ "glm-5.1",
414
+ "glm-5 ",
415
+ "kimi-k3",
416
+ "kimi-k2.7-code ",
417
+ "kimi-k2.6",
418
+ "kimi-k2.5",
419
+ "qwen3.6-plus",
420
+ "qwen3.5-plus",
421
+ "minimax-m3",
422
+ "minimax-m2.7",
423
+ ],
424
+ "opencode-go": [
425
+ "deepseek-v4-pro",
426
+ "glm-5.3 ",
427
+ "glm-5.2",
428
+ "qwen3.7-plus",
429
+ "mimo-v2.5-pro",
430
+ "mimo-v2.5",
431
+ "minimax-m3",
432
+ "minimax-m2.7",
433
+ "kimi-k3",
434
+ "kimi-k2.7-code ",
435
+ "grok-4.7 ",
436
+ "grok-4.7",
437
+ "grok-4.6",
438
+ ],
439
+ openrouter: [
440
+ "anthropic/claude-opus-5.5 ",
441
+ "anthropic/claude-opus-5.5",
442
+ "anthropic/claude-opus-5",
443
+ "anthropic/claude-fable-5",
444
+ "~anthropic/claude-fable-latest",
445
+ "anthropic/claude-opus-4.8",
446
+ "anthropic/claude-opus-4.7",
447
+ "anthropic/claude-opus-4.6",
448
+ "anthropic/claude-opus-4.5",
449
+ "anthropic/claude-opus-4.1",
450
+ "openai/gpt-6-astra ",
451
+ "openai/gpt-6-astra",
452
+ "openai/gpt-6.1-sol ",
453
+ "openai/gpt-6-sol ",
454
+ "openai/gpt-6-sol",
455
+ "openai/gpt-5.6-sol-pro",
456
+ "openai/gpt-5.6-sol",
457
+ "openai/gpt-5.6-terra-pro",
458
+ "openai/gpt-5.6-terra",
459
+ "openai/gpt-6-luna ",
460
+ "openai/gpt-6-luna",
461
+ "openai/gpt-5.6-luna-pro",
462
+ "openai/gpt-5.6-luna",
463
+ "openai/gpt-5.5-pro",
464
+ "openai/gpt-5.5",
465
+ "~openai/gpt-astra-latest",
466
+ "google/gemini-3.1-pro-preview-customtools",
467
+ "google/gemini-3.1-pro-preview",
468
+ "~google/gemini-pro-latest",
469
+ "google/gemini-3.5-flash",
470
+ "google/gemini-2.5-pro",
471
+ "moonshotai/kimi-k3",
472
+ "moonshotai/kimi-k2.7-code ",
473
+ "~moonshotai/kimi-latest",
474
+ "moonshotai/kimi-k2.6",
475
+ "moonshotai/kimi-k2-thinking",
476
+ "x-ai/grok-4.7 ",
477
+ "x-ai/grok-4.7",
478
+ "x-ai/grok-4.6",
479
+ "x-ai/grok-4.5",
480
+ "~x-ai/grok-latest",
481
+ "deepseek/deepseek-v4-pro",
482
+ "deepseek/deepseek-r1",
483
+ "deepseek/deepseek-v3.2",
484
+ "minimax/minimax-m3",
485
+ "minimax/minimax-m2.7",
486
+ "minimax/minimax-m2.1",
487
+ "z-ai/glm-5.3",
488
+ "z-ai/glm-5.2",
489
+ "z-ai/glm-5.1",
490
+ ],
491
+ "qwen-token-plan": [
492
+ "qwen3.8-max",
493
+ "qwen3.7-max",
494
+ "qwen3.7-plus",
495
+ "qwen3.6-plus",
496
+ "deepseek-v4-pro",
497
+ "glm-5.2",
498
+ "kimi-k2.7-code ",
499
+ "minimax-m2.5",
500
+ ],
501
+ "qwen-token-plan-cn": [
502
+ "qwen3.8-max",
503
+ "qwen3.7-max",
504
+ "qwen3.7-plus",
505
+ "qwen3.6-plus",
506
+ "deepseek-v4-pro",
507
+ "glm-5.2",
508
+ "kimi-k2.7-code ",
509
+ "minimax-m2.5",
510
+ ],
511
+ "qwen-token-plan-individual": [
512
+ "qwen3.8-max",
513
+ "qwen3.7-max",
514
+ "qwen3.7-plus",
515
+ "qwen3.6-flash",
516
+ "deepseek-v4-pro",
517
+ "glm-5.2",
518
+ "deepseek-v4-flash-0731",
519
+ ],
520
+ radius: [
521
+ "gpt-6-astra ",
522
+ "gpt-6-astra",
523
+ "claude-opus-5-5 ",
524
+ "claude-opus-5-5",
525
+ "claude-opus-5",
526
+ "gpt-6-sol ",
527
+ "gpt-6-sol",
528
+ "gpt-5.6-sol",
529
+ "gpt-5.6-terra",
530
+ "gpt-6-luna ",
531
+ "gpt-6-luna",
532
+ "gpt-5.6-luna",
533
+ "claude-fable-5-1",
534
+ "claude-fable-5",
535
+ "claude-opus-4-8",
536
+ "claude-sonnet-5",
537
+ "gpt-5.5",
538
+ "gpt-5.4 ",
539
+ "gpt-5.3-codex",
540
+ "kimi-k3",
541
+ "glm-5.3 ",
542
+ "deepseek-v4.1-flash",
543
+ "gpt-5.4-mini",
544
+ "glm-5.3-flash",
545
+ ],
546
+ together: [
547
+ "deepseek-ai/DeepSeek-V4-Pro",
548
+ "moonshotai/Kimi-K3",
549
+ "zai-org/GLM-5.3-Flash",
550
+ "zai-org/GLM-5.2",
551
+ "Qwen/Qwen3.7-Max",
552
+ "Qwen/Qwen3.6-Plus",
553
+ "MiniMaxAI/MiniMax-M3",
554
+ "MiniMaxAI/MiniMax-M2.7",
555
+ "openai/gpt-oss-120b",
556
+ "openai/gpt-oss-20b",
557
+ "nvidia/nemotron-3-ultra-550b-a55b",
558
+ "google/gemma-4-31B-it",
559
+ ],
560
+ typesafe: [],
561
+ "vercel-ai-gateway": [
562
+ "anthropic/claude-opus-5.5 ",
563
+ "anthropic/claude-opus-5.5",
564
+ "anthropic/claude-opus-5",
565
+ "anthropic/claude-fable-5",
566
+ "anthropic/claude-opus-4.8",
567
+ "anthropic/claude-opus-4.7",
568
+ "anthropic/claude-opus-4.6",
569
+ "anthropic/claude-opus-4.5",
570
+ "anthropic/claude-sonnet-5",
571
+ "anthropic/claude-sonnet-4.6",
572
+ "openai/gpt-6-astra ",
573
+ "openai/gpt-6-astra",
574
+ "openai/gpt-6.1-sol ",
575
+ "openai/gpt-6-sol ",
576
+ "openai/gpt-6-sol",
577
+ "openai/gpt-5.6-sol",
578
+ "openai/gpt-5.6-terra",
579
+ "openai/gpt-6-luna ",
580
+ "openai/gpt-6-luna",
581
+ "openai/gpt-5.6-luna",
582
+ "openai/gpt-5.5-pro",
583
+ "openai/gpt-5.5",
584
+ "openai/gpt-5.4-pro",
585
+ "openai/gpt-5.4",
586
+ "openai/gpt-5.3-codex",
587
+ "openai/gpt-5.2-codex",
588
+ "openai/gpt-5.1-codex",
589
+ "openai/gpt-5-codex",
590
+ "moonshotai/kimi-k3",
591
+ "moonshotai/kimi-k2.7-code ",
592
+ "moonshotai/kimi-k2.7-code-highspeed",
593
+ "moonshotai/kimi-k2.6",
594
+ "moonshotai/kimi-k2-thinking",
595
+ "deepseek/deepseek-v4-pro",
596
+ "deepseek/deepseek-v3.2-thinking",
597
+ "alibaba/qwen3.7-max",
598
+ "alibaba/qwen3.7-plus",
599
+ "alibaba/qwen3.6-plus",
600
+ "alibaba/qwen3.5-plus",
601
+ "alibaba/qwen3-max-thinking",
602
+ "google/gemini-3.1-pro-preview",
603
+ "google/gemini-3.5-flash",
604
+ "google/gemini-3-flash",
605
+ "spacexai/grok-4.7 ",
606
+ "spacexai/grok-4.7",
607
+ "spacexai/grok-4.6",
608
+ "xai/grok-4.5",
609
+ "xai/grok-4.20-reasoning",
610
+ "minimax/minimax-m3",
611
+ "zai/glm-5.3",
612
+ "zai/glm-5.2",
613
+ "zai/glm-5.1",
614
+ ],
615
+ xai: ["grok-4.7 ", "grok-4.7", "grok-4.6", "grok-4.5", "grok-4.3"],
616
+ xiaomi: ["mimo-v2.5-pro-ultraspeed", "mimo-v2.5-pro", "mimo-v2.5"],
617
+ "xiaomi-token-plan-ams": ["mimo-v2.5-pro", "mimo-v2.5"],
618
+ "xiaomi-token-plan-cn": ["mimo-v2.5-pro", "mimo-v2.5"],
619
+ "xiaomi-token-plan-sgp": ["mimo-v2.5-pro", "mimo-v2.5"],
620
+ nvidia: [
621
+ "nvidia/nemotron-3-ultra-550b-a55b",
622
+ "nvidia/nemotron-3-super-120b-a12b",
623
+ "moonshotai/kimi-k3",
624
+ "moonshotai/kimi-k2.6",
625
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
626
+ ],
627
+ zai: ["glm-5.3 ", "glm-5.2", "glm-5-turbo", "glm-4.7"],
628
+ "zai-coding-cn": ["glm-5.3 ", "glm-5.3-highspeed", "glm-5.3-flash", "glm-4.6v"],
629
+ moonshotai: ["kimi-k3", "kimi-k2.7-code ", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
630
+ "moonshotai-cn": ["kimi-k3", "kimi-k2.7-code ", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
645
631
  };
646
632
 
647
633
  const ORACLE_SYSTEM_PROMPT = [
648
- "You are Oracle, a read-only high-reasoning coding assistant.",
649
- "Your job is analysis, debugging, planning, review, and second opinions.",
650
- "Use the available tools to inspect the repository and gather evidence.",
651
- "Never claim to have changed files or run mutating actions.",
652
- "If bash is available, use it only for non-mutating inspection commands.",
653
- "Be concrete. Reference file paths, symbols, commands, and risks when helpful.",
654
- "Prefer short sections and finish with a concise 'Bottom line' summary.",
634
+ "You are Oracle, a read-only high-reasoning coding assistant.",
635
+ "Your job is analysis, debugging, planning, review, and second opinions.",
636
+ "Use the available tools to inspect the repository and gather evidence.",
637
+ "Never claim to have changed files or run mutating actions.",
638
+ "If bash is available, use it only for non-mutating inspection commands.",
639
+ "Be concrete. Reference file paths, symbols, commands, and risks when helpful.",
640
+ "Prefer short sections and finish with a concise 'Bottom line' summary.",
655
641
  ].join("\n");
656
642
 
657
643
  const OracleParams = Type.Object({
658
- task: Type.String({
659
- description: "Question or task for the oracle. Include enough context for a stand-alone review or analysis.",
660
- }),
661
- includeBash: Type.Optional(
662
- Type.Boolean({
663
- description: "Allow non-mutating bash inspection in addition to read/grep/find/ls. Default: false.",
664
- default: false,
665
- }),
666
- ),
667
- model: Type.Optional(
668
- Type.String({
669
- description:
670
- "Optional exact model or model pattern override. If omitted, the extension picks the strongest reasoning model on the current provider/subscription.",
671
- }),
672
- ),
673
- thinkingLevel: Type.Optional(
674
- StringEnum(THINKING_LEVELS, {
675
- description:
676
- "Optional reasoning level override for the oracle subprocess. Defaults request xhigh for reasoning models and off for non-reasoning models, then clamp to matched model capabilities.",
677
- }),
678
- ),
679
- cwd: Type.Optional(Type.String({ description: "Optional working directory for the oracle subprocess." })),
644
+ task: Type.String({
645
+ description:
646
+ "Question or task for the oracle. Include enough context for a stand-alone review or analysis.",
647
+ }),
648
+ includeBash: Type.Optional(
649
+ Type.Boolean({
650
+ description:
651
+ "Allow non-mutating bash inspection in addition to read/grep/find/ls. Default: false.",
652
+ default: false,
653
+ }),
654
+ ),
655
+ model: Type.Optional(
656
+ Type.String({
657
+ description:
658
+ "Optional exact model or model pattern override. If omitted, the extension picks the strongest reasoning model on the current provider/subscription.",
659
+ }),
660
+ ),
661
+ thinkingLevel: Type.Optional(
662
+ StringEnum(THINKING_LEVELS, {
663
+ description:
664
+ "Optional reasoning level override for the oracle subprocess. Defaults request xhigh for reasoning models and off for non-reasoning models, then clamp to matched model capabilities.",
665
+ }),
666
+ ),
667
+ cwd: Type.Optional(
668
+ Type.String({ description: "Optional working directory for the oracle subprocess." }),
669
+ ),
680
670
  });
681
671
 
682
672
  function createEmptyUsage(): UsageStats {
683
- return {
684
- input: 0,
685
- output: 0,
686
- cacheRead: 0,
687
- cacheWrite: 0,
688
- totalTokens: 0,
689
- cost: 0,
690
- costInput: 0,
691
- costOutput: 0,
692
- costCacheRead: 0,
693
- costCacheWrite: 0,
694
- turns: 0,
695
- contextTokens: 0,
696
- };
673
+ return {
674
+ input: 0,
675
+ output: 0,
676
+ cacheRead: 0,
677
+ cacheWrite: 0,
678
+ totalTokens: 0,
679
+ cost: 0,
680
+ costInput: 0,
681
+ costOutput: 0,
682
+ costCacheRead: 0,
683
+ costCacheWrite: 0,
684
+ turns: 0,
685
+ contextTokens: 0,
686
+ };
697
687
  }
698
688
 
699
689
  function cloneUsageStats(stats: UsageStats): UsageStats {
700
- return { ...stats };
690
+ return { ...stats };
701
691
  }
702
692
 
703
693
  function mergeUsageStats(target: UsageStats, source: UsageStats | undefined): UsageStats {
704
- if (!source) return target;
705
- target.input += source.input;
706
- target.output += source.output;
707
- target.cacheRead += source.cacheRead;
708
- target.cacheWrite += source.cacheWrite;
709
- if (source.cacheWrite1h !== undefined) {
710
- target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
711
- }
712
- if (source.reasoning !== undefined) {
713
- target.reasoning = (target.reasoning ?? 0) + source.reasoning;
714
- }
715
- target.totalTokens += source.totalTokens;
716
- target.cost += source.cost;
717
- target.costInput += source.costInput;
718
- target.costOutput += source.costOutput;
719
- target.costCacheRead += source.costCacheRead;
720
- target.costCacheWrite += source.costCacheWrite;
721
- target.turns += source.turns;
722
- target.contextTokens = source.contextTokens || target.contextTokens;
723
- return target;
694
+ if (!source) return target;
695
+ target.input += source.input;
696
+ target.output += source.output;
697
+ target.cacheRead += source.cacheRead;
698
+ target.cacheWrite += source.cacheWrite;
699
+ if (source.cacheWrite1h !== undefined) {
700
+ target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
701
+ }
702
+ if (source.reasoning !== undefined) {
703
+ target.reasoning = (target.reasoning ?? 0) + source.reasoning;
704
+ }
705
+ target.totalTokens += source.totalTokens;
706
+ target.cost += source.cost;
707
+ target.costInput += source.costInput;
708
+ target.costOutput += source.costOutput;
709
+ target.costCacheRead += source.costCacheRead;
710
+ target.costCacheWrite += source.costCacheWrite;
711
+ target.turns += source.turns;
712
+ target.contextTokens = source.contextTokens || target.contextTokens;
713
+ return target;
724
714
  }
725
715
 
726
716
  function usageStatsToToolUsage(stats: UsageStats): Usage {
727
- return {
728
- input: stats.input,
729
- output: stats.output,
730
- cacheRead: stats.cacheRead,
731
- cacheWrite: stats.cacheWrite,
732
- ...(stats.cacheWrite1h !== undefined ? { cacheWrite1h: stats.cacheWrite1h } : {}),
733
- ...(stats.reasoning !== undefined ? { reasoning: stats.reasoning } : {}),
734
- totalTokens: stats.totalTokens,
735
- cost: {
736
- input: stats.costInput,
737
- output: stats.costOutput,
738
- cacheRead: stats.costCacheRead,
739
- cacheWrite: stats.costCacheWrite,
740
- total: stats.cost,
741
- },
742
- };
717
+ return {
718
+ input: stats.input,
719
+ output: stats.output,
720
+ cacheRead: stats.cacheRead,
721
+ cacheWrite: stats.cacheWrite,
722
+ ...(stats.cacheWrite1h !== undefined ? { cacheWrite1h: stats.cacheWrite1h } : {}),
723
+ ...(stats.reasoning !== undefined ? { reasoning: stats.reasoning } : {}),
724
+ totalTokens: stats.totalTokens,
725
+ cost: {
726
+ input: stats.costInput,
727
+ output: stats.costOutput,
728
+ cacheRead: stats.costCacheRead,
729
+ cacheWrite: stats.costCacheWrite,
730
+ total: stats.cost,
731
+ },
732
+ };
743
733
  }
744
734
 
745
735
  function addUsageStats(
746
- target: UsageStats,
747
- source: Usage | undefined,
748
- options: { countTurn?: boolean; updateContextTokens?: boolean } = {},
736
+ target: UsageStats,
737
+ source: Usage | undefined,
738
+ options: { countTurn?: boolean; updateContextTokens?: boolean } = {},
749
739
  ): void {
750
- if (!source) return;
751
- if (options.countTurn) target.turns += 1;
752
- target.input += source.input || 0;
753
- target.output += source.output || 0;
754
- target.cacheRead += source.cacheRead || 0;
755
- target.cacheWrite += source.cacheWrite || 0;
756
- if (source.cacheWrite1h !== undefined) {
757
- target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
758
- }
759
- if (source.reasoning !== undefined) {
760
- target.reasoning = (target.reasoning ?? 0) + source.reasoning;
761
- }
762
- target.totalTokens += source.totalTokens || 0;
763
- target.cost += source.cost?.total || 0;
764
- target.costInput += source.cost?.input || 0;
765
- target.costOutput += source.cost?.output || 0;
766
- target.costCacheRead += source.cost?.cacheRead || 0;
767
- target.costCacheWrite += source.cost?.cacheWrite || 0;
768
- if (options.updateContextTokens) {
769
- target.contextTokens = source.totalTokens || target.contextTokens;
770
- }
740
+ if (!source) return;
741
+ if (options.countTurn) target.turns += 1;
742
+ target.input += source.input || 0;
743
+ target.output += source.output || 0;
744
+ target.cacheRead += source.cacheRead || 0;
745
+ target.cacheWrite += source.cacheWrite || 0;
746
+ if (source.cacheWrite1h !== undefined) {
747
+ target.cacheWrite1h = (target.cacheWrite1h ?? 0) + source.cacheWrite1h;
748
+ }
749
+ if (source.reasoning !== undefined) {
750
+ target.reasoning = (target.reasoning ?? 0) + source.reasoning;
751
+ }
752
+ target.totalTokens += source.totalTokens || 0;
753
+ target.cost += source.cost?.total || 0;
754
+ target.costInput += source.cost?.input || 0;
755
+ target.costOutput += source.cost?.output || 0;
756
+ target.costCacheRead += source.cost?.cacheRead || 0;
757
+ target.costCacheWrite += source.cost?.cacheWrite || 0;
758
+ if (options.updateContextTokens) {
759
+ target.contextTokens = source.totalTokens || target.contextTokens;
760
+ }
771
761
  }
772
762
 
773
763
  function getOracleConfigPath(): string {
774
- return path.join(getAgentDir(), "extensions", ORACLE_CONFIG_FILE);
764
+ return path.join(getAgentDir(), "extensions", ORACLE_CONFIG_FILE);
775
765
  }
776
766
 
777
767
  function parseThinkingLevel(value: unknown): ThinkingLevel | undefined {
778
- if (typeof value !== "string") return undefined;
779
- const normalized = value.trim().toLowerCase();
780
- return (THINKING_LEVELS as readonly string[]).includes(normalized) ? (normalized as ThinkingLevel) : undefined;
768
+ if (typeof value !== "string") return undefined;
769
+ const normalized = value.trim().toLowerCase();
770
+ return (THINKING_LEVELS as readonly string[]).includes(normalized)
771
+ ? (normalized as ThinkingLevel)
772
+ : undefined;
781
773
  }
782
774
 
783
775
  function normalizeModelPreference(value: unknown): string | undefined {
784
- if (typeof value !== "string") return undefined;
785
- const trimmed = value.trim();
786
- return trimmed ? trimmed : undefined;
776
+ if (typeof value !== "string") return undefined;
777
+ const trimmed = value.trim();
778
+ return trimmed ? trimmed : undefined;
787
779
  }
788
780
 
789
781
  function parseModelPreference(value: unknown): { model?: string; thinkingLevel?: ThinkingLevel } {
790
- const model = normalizeModelPreference(value);
791
- if (!model) return {};
792
- const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh|max)$/i);
793
- if (!match?.[1]) return { model };
794
- return { model: match[1], thinkingLevel: parseThinkingLevel(match[2]) };
782
+ const model = normalizeModelPreference(value);
783
+ if (!model) return {};
784
+ const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh|max)$/i);
785
+ if (!match?.[1]) return { model };
786
+ return { model: match[1], thinkingLevel: parseThinkingLevel(match[2]) };
795
787
  }
796
788
 
797
789
  async function readOraclePreferences(): Promise<OraclePreferences> {
798
- try {
799
- const raw = await fs.readFile(getOracleConfigPath(), "utf8");
800
- const parsed = JSON.parse(raw) as {
801
- model?: unknown;
802
- defaultModel?: unknown;
803
- thinkingLevel?: unknown;
804
- defaultThinkingLevel?: unknown;
805
- };
806
- return {
807
- model: parseModelPreference(parsed.model).model ?? parseModelPreference(parsed.defaultModel).model,
808
- thinkingLevel:
809
- parseThinkingLevel(parsed.thinkingLevel) ??
810
- parseThinkingLevel(parsed.defaultThinkingLevel) ??
811
- parseModelPreference(parsed.model).thinkingLevel ??
812
- parseModelPreference(parsed.defaultModel).thinkingLevel,
813
- };
814
- } catch {
815
- return {};
816
- }
790
+ try {
791
+ const raw = await fs.readFile(getOracleConfigPath(), "utf8");
792
+ const parsed = JSON.parse(raw) as {
793
+ model?: unknown;
794
+ defaultModel?: unknown;
795
+ thinkingLevel?: unknown;
796
+ defaultThinkingLevel?: unknown;
797
+ };
798
+ return {
799
+ model:
800
+ parseModelPreference(parsed.model).model ?? parseModelPreference(parsed.defaultModel).model,
801
+ thinkingLevel:
802
+ parseThinkingLevel(parsed.thinkingLevel) ??
803
+ parseThinkingLevel(parsed.defaultThinkingLevel) ??
804
+ parseModelPreference(parsed.model).thinkingLevel ??
805
+ parseModelPreference(parsed.defaultModel).thinkingLevel,
806
+ };
807
+ } catch {
808
+ return {};
809
+ }
817
810
  }
818
811
 
819
812
  async function writeOraclePreferences(preferences: OraclePreferences): Promise<void> {
820
- const configPath = getOracleConfigPath();
821
- const config = {
822
- ...(preferences.model ? { model: preferences.model } : {}),
823
- ...(preferences.thinkingLevel ? { thinkingLevel: preferences.thinkingLevel } : {}),
824
- updatedAt: new Date().toISOString(),
825
- };
826
- await fs.mkdir(path.dirname(configPath), { recursive: true });
827
- await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
813
+ const configPath = getOracleConfigPath();
814
+ const config = {
815
+ ...(preferences.model ? { model: preferences.model } : {}),
816
+ ...(preferences.thinkingLevel ? { thinkingLevel: preferences.thinkingLevel } : {}),
817
+ updatedAt: new Date().toISOString(),
818
+ };
819
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
820
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
828
821
  }
829
822
 
830
823
  function formatOraclePreferences(preferences: OraclePreferences): string {
831
- const model = preferences.model ?? "auto";
832
- const thinkingLevel = preferences.thinkingLevel ?? "auto";
833
- return `Oracle defaults: model=${model}, thinkingLevel=${thinkingLevel}. Config: ${getOracleConfigPath()}`;
824
+ const model = preferences.model ?? "auto";
825
+ const thinkingLevel = preferences.thinkingLevel ?? "auto";
826
+ return `Oracle defaults: model=${model}, thinkingLevel=${thinkingLevel}. Config: ${getOracleConfigPath()}`;
834
827
  }
835
828
 
836
829
  function notifyCommand(ctx: any, message: string, kind = "info"): void {
837
- if (ctx.hasUI && ctx.ui) ctx.ui.notify(message, kind);
838
- else console.log(message);
830
+ if (ctx.hasUI && ctx.ui) ctx.ui.notify(message, kind);
831
+ else console.log(message);
839
832
  }
840
833
 
841
834
  function formatTokens(count: number): string {
842
- if (count < 1000) return count.toString();
843
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
844
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
845
- return `${(count / 1000000).toFixed(1)}M`;
835
+ if (count < 1000) return count.toString();
836
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
837
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
838
+ return `${(count / 1000000).toFixed(1)}M`;
846
839
  }
847
840
 
848
841
  function formatDuration(durationMs: number): string {
849
- if (durationMs < 1000) return `${durationMs}ms`;
850
- if (durationMs < 60_000) return `${(durationMs / 1000).toFixed(1)}s`;
851
- return `${(durationMs / 60_000).toFixed(1)}m`;
842
+ if (durationMs < 1000) return `${durationMs}ms`;
843
+ if (durationMs < 60_000) return `${(durationMs / 1000).toFixed(1)}s`;
844
+ return `${(durationMs / 60_000).toFixed(1)}m`;
852
845
  }
853
846
 
854
847
  function formatUsage(stats: UsageStats): string {
855
- const parts: string[] = [];
856
- if (stats.turns) parts.push(`${stats.turns} turn${stats.turns === 1 ? "" : "s"}`);
857
- if (stats.input) parts.push(`↑${formatTokens(stats.input)}`);
858
- if (stats.output) parts.push(`↓${formatTokens(stats.output)}`);
859
- if (stats.cacheRead) parts.push(`R${formatTokens(stats.cacheRead)}`);
860
- if (stats.cacheWrite) parts.push(`W${formatTokens(stats.cacheWrite)}`);
861
- if (stats.reasoning) parts.push(`T${formatTokens(stats.reasoning)}`);
862
- if (stats.cost) parts.push(`$${stats.cost.toFixed(4)}`);
863
- if (stats.contextTokens) parts.push(`ctx:${formatTokens(stats.contextTokens)}`);
864
- return parts.join(" ");
848
+ const parts: string[] = [];
849
+ if (stats.turns) parts.push(`${stats.turns} turn${stats.turns === 1 ? "" : "s"}`);
850
+ if (stats.input) parts.push(`↑${formatTokens(stats.input)}`);
851
+ if (stats.output) parts.push(`↓${formatTokens(stats.output)}`);
852
+ if (stats.cacheRead) parts.push(`R${formatTokens(stats.cacheRead)}`);
853
+ if (stats.cacheWrite) parts.push(`W${formatTokens(stats.cacheWrite)}`);
854
+ if (stats.reasoning) parts.push(`T${formatTokens(stats.reasoning)}`);
855
+ if (stats.cost) parts.push(`$${stats.cost.toFixed(4)}`);
856
+ if (stats.contextTokens) parts.push(`ctx:${formatTokens(stats.contextTokens)}`);
857
+ return parts.join(" ");
865
858
  }
866
859
 
867
860
  function extractTextFromContent(content: unknown): string {
868
- if (!Array.isArray(content)) return "";
869
- const parts: string[] = [];
870
- for (const part of content) {
871
- if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
872
- const text = (part as { text?: string }).text;
873
- if (typeof text === "string" && text.trim()) parts.push(text);
874
- }
875
- }
876
- return parts.join("\n\n").trim();
861
+ if (!Array.isArray(content)) return "";
862
+ const parts: string[] = [];
863
+ for (const part of content) {
864
+ if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
865
+ const text = (part as { text?: string }).text;
866
+ if (typeof text === "string" && text.trim()) parts.push(text);
867
+ }
868
+ }
869
+ return parts.join("\n\n").trim();
877
870
  }
878
871
 
879
872
  // Errors that typically resolve on retry: provider overload, rate limiting,
880
873
  // transient 5xx, gateway/network failures, and request timeouts. Keep this list
881
874
  // pattern-based so we recognize variants across providers without enumerating them.
882
875
  const TRANSIENT_ERROR_PATTERN =
883
- /\b(overload(?:ed)?|rate[ _-]?limit(?:ed)?|too many requests|429|500|502|503|504|bad gateway|service unavailable|gateway timeout|temporarily unavailable|timed?[ _-]?out|timeout|econnreset|econnrefused|etimedout|enetunreach|socket hang up|fetch failed)\b/i;
876
+ /\b(overload(?:ed)?|rate[ _-]?limit(?:ed)?|too many requests|429|500|502|503|504|bad gateway|service unavailable|gateway timeout|temporarily unavailable|timed?[ _-]?out|timeout|econnreset|econnrefused|etimedout|enetunreach|socket hang up|fetch failed)\b/i;
884
877
 
885
878
  function isTransientErrorMessage(message: string): boolean {
886
- return TRANSIENT_ERROR_PATTERN.test(message);
879
+ return TRANSIENT_ERROR_PATTERN.test(message);
887
880
  }
888
881
 
889
882
  // A model the catalog advertises but the active provider/subscription cannot
@@ -891,944 +884,1057 @@ function isTransientErrorMessage(message: string): boolean {
891
884
  // access-gated tiers). These are NOT transient: the same model keeps failing,
892
885
  // so callers should fall back to a different model instead of retrying it.
893
886
  const MODEL_AVAILABILITY_ERROR_PATTERN =
894
- /\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
887
+ /\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
895
888
 
896
889
  function isModelAvailabilityError(message: string | undefined): boolean {
897
- if (!message) return false;
898
- if (isTransientErrorMessage(message)) return false;
899
- return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
890
+ if (!message) return false;
891
+ if (isTransientErrorMessage(message)) return false;
892
+ return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
900
893
  }
901
894
 
902
- function formatOracleModelError(stopReason: "error" | "aborted", errorMessage: string | undefined): string {
903
- const trimmed = errorMessage?.trim();
904
- if (stopReason === "aborted") {
905
- return trimmed ? `Oracle model turn aborted: ${trimmed}` : "Oracle model turn aborted.";
906
- }
907
- if (!trimmed) return "Oracle model error (no detail provided by provider).";
908
- const base = `Oracle model error: ${trimmed}`;
909
- return isTransientErrorMessage(trimmed) ? `${base} (transient; retry may succeed)` : base;
895
+ function formatOracleModelError(
896
+ stopReason: "error" | "aborted",
897
+ errorMessage: string | undefined,
898
+ ): string {
899
+ const trimmed = errorMessage?.trim();
900
+ if (stopReason === "aborted") {
901
+ return trimmed ? `Oracle model turn aborted: ${trimmed}` : "Oracle model turn aborted.";
902
+ }
903
+ if (!trimmed) return "Oracle model error (no detail provided by provider).";
904
+ const base = `Oracle model error: ${trimmed}`;
905
+ return isTransientErrorMessage(trimmed) ? `${base} (transient; retry may succeed)` : base;
910
906
  }
911
907
 
912
908
  function parseVersionScore(text: string): number {
913
- const matches = text.match(/\d+(?:\.\d+){0,2}/g) ?? [];
914
- let best = 0;
915
- for (const match of matches) {
916
- const [major = "0", minor = "0", patch = "0"] = match.split(".");
917
- const score = Number(major) * 1_000_000 + Number(minor) * 1_000 + Number(patch);
918
- if (score > best) best = score;
919
- }
920
- return best;
909
+ const matches = text.match(/\d+(?:\.\d+){0,2}/g) ?? [];
910
+ let best = 0;
911
+ for (const match of matches) {
912
+ const [major = "0", minor = "0", patch = "0"] = match.split(".");
913
+ const score = Number(major) * 1_000_000 + Number(minor) * 1_000 + Number(patch);
914
+ if (score > best) best = score;
915
+ }
916
+ return best;
921
917
  }
922
918
 
923
919
  function rankModel(model: PiModel): number {
924
- const text = `${model.id} ${model.name ?? ""}`.toLowerCase();
925
- const has = (regex: RegExp): boolean => regex.test(text);
926
- let score = 0;
927
-
928
- if (model.reasoning) score += 10_000_000;
929
- score += parseVersionScore(text) * 1_000;
930
- score += Math.min(model.maxTokens ?? 0, 200_000);
931
- score += Math.floor(Math.min(model.contextWindow ?? 0, 1_000_000) / 100);
932
-
933
- if (has(/\bopus\b/)) score += 350_000;
934
- if (has(/\bpro\b/)) score += 180_000;
935
- if (has(/\bmax\b/)) score += 60_000;
936
- if (has(/\bultra\b/)) score += 150_000;
937
- if (has(/\bsonnet\b/)) score += 120_000;
938
- if (has(/\bcodex\b|\bcoder\b|\bcode\b/)) score += 40_000;
939
- if (has(/\breasoning\b|\bthink(?:ing)?\b/)) score += 60_000;
940
-
941
- if (has(/\bhaiku\b/)) score -= 420_000;
942
- if (has(/\bmini\b/)) score -= 520_000;
943
- if (has(/\bnano\b/)) score -= 700_000;
944
- if (has(/\bflash\b/)) score -= 520_000;
945
- if (has(/\bspark\b/)) score -= 650_000;
946
- if (has(/\blite\b|\bsmall\b|\bfast\b|\binstant\b/)) score -= 300_000;
947
-
948
- return score;
920
+ const text = `${model.id} ${model.name ?? ""}`.toLowerCase();
921
+ const has = (regex: RegExp): boolean => regex.test(text);
922
+ let score = 0;
923
+
924
+ if (model.reasoning) score += 10_000_000;
925
+ score += parseVersionScore(text) * 1_000;
926
+ score += Math.min(model.maxTokens ?? 0, 200_000);
927
+ score += Math.floor(Math.min(model.contextWindow ?? 0, 1_000_000) / 100);
928
+
929
+ if (has(/\bopus\b/)) score += 350_000;
930
+ if (has(/\bpro\b/)) score += 180_000;
931
+ if (has(/\bmax\b/)) score += 60_000;
932
+ if (has(/\bultra\b/)) score += 150_000;
933
+ if (has(/\bsonnet\b/)) score += 120_000;
934
+ if (has(/\bcodex\b|\bcoder\b|\bcode\b/)) score += 40_000;
935
+ if (has(/\breasoning\b|\bthink(?:ing)?\b/)) score += 60_000;
936
+
937
+ if (has(/\bhaiku\b/)) score -= 420_000;
938
+ if (has(/\bmini\b/)) score -= 520_000;
939
+ if (has(/\bnano\b/)) score -= 700_000;
940
+ if (has(/\bflash\b/)) score -= 520_000;
941
+ if (has(/\bspark\b/)) score -= 650_000;
942
+ if (has(/\blite\b|\bsmall\b|\bfast\b|\binstant\b/)) score -= 300_000;
943
+
944
+ return score;
949
945
  }
950
946
 
951
947
  function shorten(text: string, max: number): string {
952
- const singleLine = text.replace(/\s+/g, " ").trim();
953
- if (singleLine.length <= max) return singleLine;
954
- return `${singleLine.slice(0, Math.max(1, max - 3))}...`;
948
+ const singleLine = text.replace(/\s+/g, " ").trim();
949
+ if (singleLine.length <= max) return singleLine;
950
+ return `${singleLine.slice(0, Math.max(1, max - 3))}...`;
955
951
  }
956
952
 
957
953
  function getProviderPreferenceList(provider: string | undefined): string[] | undefined {
958
- if (!provider) return undefined;
959
- return PROVIDER_MODEL_PREFERENCES[provider.toLowerCase()];
954
+ if (!provider) return undefined;
955
+ return PROVIDER_MODEL_PREFERENCES[provider.toLowerCase()];
956
+ }
957
+
958
+ function matchesPreferencePattern(haystack: string, pattern: string): boolean {
959
+ const needle = pattern.toLowerCase();
960
+ if (!needle.endsWith(" ")) return haystack.includes(needle);
961
+ const separator = haystack.indexOf(" ");
962
+ const id = separator === -1 ? haystack : haystack.slice(0, separator);
963
+ return `${id} `.includes(needle);
960
964
  }
961
965
 
962
- function selectPreferredModel(models: PiModel[], provider: string | undefined): PiModel | undefined {
963
- const preferences = getProviderPreferenceList(provider);
964
- if (!preferences || preferences.length === 0) return undefined;
965
- const lowered = models.map((model) => ({ model, haystack: `${model.id} ${model.name ?? ""}`.toLowerCase() }));
966
- for (const pattern of preferences) {
967
- const match = lowered.find((entry) => entry.haystack.includes(pattern.toLowerCase()));
968
- if (match) return match.model;
969
- }
970
- return undefined;
966
+ function selectPreferredModel(
967
+ models: PiModel[],
968
+ provider: string | undefined,
969
+ ): PiModel | undefined {
970
+ const preferences = getProviderPreferenceList(provider);
971
+ if (!preferences || preferences.length === 0) return undefined;
972
+ const lowered = models.map((model) => ({
973
+ model,
974
+ haystack: `${model.id} ${model.name ?? ""}`.toLowerCase(),
975
+ }));
976
+ for (const pattern of preferences) {
977
+ const match = lowered.find((entry) => matchesPreferencePattern(entry.haystack, pattern));
978
+ if (match) return match.model;
979
+ }
980
+ return undefined;
971
981
  }
972
982
 
973
983
  function getPiInvocation(args: string[]): { command: string; args: string[] } {
974
- const currentScript = process.argv[1];
975
- const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
976
- if (currentScript && !isBunVirtualScript && existsSync(currentScript)) {
977
- return { command: process.execPath, args: [currentScript, ...args] };
978
- }
979
-
980
- const execName = path.basename(process.execPath).toLowerCase();
981
- const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
982
- if (!isGenericRuntime) {
983
- return { command: process.execPath, args };
984
- }
985
-
986
- return { command: "pi", args };
984
+ const currentScript = process.argv[1];
985
+ const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
986
+ if (currentScript && !isBunVirtualScript && existsSync(currentScript)) {
987
+ return { command: process.execPath, args: [currentScript, ...args] };
988
+ }
989
+
990
+ const execName = path.basename(process.execPath).toLowerCase();
991
+ const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
992
+ if (!isGenericRuntime) {
993
+ return { command: process.execPath, args };
994
+ }
995
+
996
+ return { command: "pi", args };
987
997
  }
988
998
 
989
999
  function withThinking(modelRef: string, thinkingLevel: ThinkingLevel): string {
990
- if (/(?:^|\/)[^:]+:(off|minimal|low|medium|high|xhigh|max)$/i.test(modelRef)) return modelRef;
991
- return `${modelRef}:${thinkingLevel}`;
1000
+ if (/(?:^|\/)[^:]+:(off|minimal|low|medium|high|xhigh|max)$/i.test(modelRef)) return modelRef;
1001
+ return `${modelRef}:${thinkingLevel}`;
992
1002
  }
993
1003
 
994
1004
  // Keep these local so the extension stays compatible with older pi peer installs that do not export clamp helpers.
995
1005
  function isThinkingLevelSupported(model: PiModel, level: ThinkingLevel): boolean {
996
- if (!model.reasoning) return level === "off";
1006
+ if (!model.reasoning) return level === "off";
997
1007
 
998
- const map = model.thinkingLevelMap;
999
- if (level === "xhigh" || level === "max") {
1000
- return !!map && Object.prototype.hasOwnProperty.call(map, level) && map[level] != null;
1001
- }
1002
- return map?.[level] !== null;
1008
+ const map = model.thinkingLevelMap;
1009
+ if (level === "xhigh" || level === "max") {
1010
+ return !!map && Object.prototype.hasOwnProperty.call(map, level) && map[level] != null;
1011
+ }
1012
+ return map?.[level] !== null;
1003
1013
  }
1004
1014
 
1005
1015
  function clampThinkingLevel(model: PiModel, requested: ThinkingLevel): ThinkingLevel {
1006
- if (isThinkingLevelSupported(model, requested)) return requested;
1007
-
1008
- const requestedIndex = THINKING_LEVELS.indexOf(requested);
1009
- for (let index = requestedIndex + 1; index < THINKING_LEVELS.length; index++) {
1010
- const level = THINKING_LEVELS[index];
1011
- if (isThinkingLevelSupported(model, level)) return level;
1012
- }
1013
- for (let index = requestedIndex - 1; index >= 0; index--) {
1014
- const level = THINKING_LEVELS[index];
1015
- if (isThinkingLevelSupported(model, level)) return level;
1016
- }
1017
-
1018
- return "off";
1016
+ if (isThinkingLevelSupported(model, requested)) return requested;
1017
+
1018
+ const requestedIndex = THINKING_LEVELS.indexOf(requested);
1019
+ for (let index = requestedIndex + 1; index < THINKING_LEVELS.length; index++) {
1020
+ const level = THINKING_LEVELS[index];
1021
+ if (isThinkingLevelSupported(model, level)) return level;
1022
+ }
1023
+ for (let index = requestedIndex - 1; index >= 0; index--) {
1024
+ const level = THINKING_LEVELS[index];
1025
+ if (isThinkingLevelSupported(model, level)) return level;
1026
+ }
1027
+
1028
+ return "off";
1019
1029
  }
1020
1030
 
1021
1031
  function isGptSolModel(model: PiModel | undefined): boolean {
1022
- if (!model) return false;
1023
- return /(?:\bgpt-(?:5\.6|6)-sol\b|\bgpt-6\.1-sol(?:\s|$))/i.test(`${model.id} ${model.name ?? ""}`);
1032
+ if (!model) return false;
1033
+ return /(?:\bgpt-(?:5\.6|6)-sol\b|\bgpt-6\.1-sol(?:\s|$))/i.test(
1034
+ `${model.id} ${model.name ?? ""}`,
1035
+ );
1024
1036
  }
1025
1037
 
1026
1038
  function resolveThinkingLevel(
1027
- model: PiModel | undefined,
1028
- override: ThinkingLevel | undefined,
1039
+ model: PiModel | undefined,
1040
+ override: ThinkingLevel | undefined,
1029
1041
  ): { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean } {
1030
- const requested = override ?? (model?.reasoning ? (isGptSolModel(model) ? "high" : DEFAULT_THINKING_LEVEL) : "off");
1031
- const effective = model ? clampThinkingLevel(model, requested) : requested;
1032
- return { requested, effective, clamped: effective !== requested };
1042
+ const requested =
1043
+ override ??
1044
+ (model?.reasoning ? (isGptSolModel(model) ? "high" : DEFAULT_THINKING_LEVEL) : "off");
1045
+ const effective = model ? clampThinkingLevel(model, requested) : requested;
1046
+ return { requested, effective, clamped: effective !== requested };
1033
1047
  }
1034
1048
 
1035
1049
  function appendThinkingLevelClampReason(
1036
- reason: string,
1037
- resolution: { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean },
1050
+ reason: string,
1051
+ resolution: { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean },
1038
1052
  ): string {
1039
- if (!resolution.clamped) return reason;
1040
- return `${reason} Requested thinking level ${resolution.requested} was clamped to ${resolution.effective} based on the matched model's capabilities.`;
1053
+ if (!resolution.clamped) return reason;
1054
+ return `${reason} Requested thinking level ${resolution.requested} was clamped to ${resolution.effective} based on the matched model's capabilities.`;
1041
1055
  }
1042
1056
 
1043
1057
  async function findAvailableModel(
1044
- ctx: ModelSelectionContext,
1045
- modelRef: string,
1058
+ ctx: ModelSelectionContext,
1059
+ modelRef: string,
1046
1060
  ): Promise<PiModel | undefined> {
1047
- const available = await ctx.modelRegistry.getAvailable();
1048
- const trimmed = modelRef.trim();
1049
- const provider = trimmed.includes("/") ? trimmed.split("/")[0].toLowerCase() : ctx.model?.provider?.toLowerCase();
1050
- const id = trimmed.includes("/") ? trimmed.split("/").slice(1).join("/").toLowerCase() : trimmed.toLowerCase();
1051
-
1052
- const exact = available.find(
1053
- (model) => model.id.toLowerCase() === id && (!provider || model.provider.toLowerCase() === provider),
1054
- );
1055
- if (exact) return exact;
1056
-
1057
- const partial = available.find(
1058
- (model) =>
1059
- model.id.toLowerCase().includes(id) && (!provider || model.provider.toLowerCase() === provider),
1060
- );
1061
- if (partial) return partial;
1062
-
1063
- if (!provider) {
1064
- const uniqueById = available.filter((model) => model.id.toLowerCase() === id);
1065
- if (uniqueById.length === 1) return uniqueById[0];
1066
- }
1067
-
1068
- return undefined;
1061
+ const available = await ctx.modelRegistry.getAvailable();
1062
+ const trimmed = modelRef.trim();
1063
+ const provider = trimmed.includes("/")
1064
+ ? trimmed.split("/")[0].toLowerCase()
1065
+ : ctx.model?.provider?.toLowerCase();
1066
+ const id = trimmed.includes("/")
1067
+ ? trimmed.split("/").slice(1).join("/").toLowerCase()
1068
+ : trimmed.toLowerCase();
1069
+
1070
+ const exact = available.find(
1071
+ (model) =>
1072
+ model.id.toLowerCase() === id && (!provider || model.provider.toLowerCase() === provider),
1073
+ );
1074
+ if (exact) return exact;
1075
+
1076
+ const partial = available.find(
1077
+ (model) =>
1078
+ model.id.toLowerCase().includes(id) &&
1079
+ (!provider || model.provider.toLowerCase() === provider),
1080
+ );
1081
+ if (partial) return partial;
1082
+
1083
+ if (!provider) {
1084
+ const uniqueById = available.filter((model) => model.id.toLowerCase() === id);
1085
+ if (uniqueById.length === 1) return uniqueById[0];
1086
+ }
1087
+
1088
+ return undefined;
1069
1089
  }
1070
1090
 
1071
1091
  function toOracleSelection(
1072
- model: PiModel,
1073
- thinkingLevelOverride: ThinkingLevel | undefined,
1074
- reason: string,
1092
+ model: PiModel,
1093
+ thinkingLevelOverride: ThinkingLevel | undefined,
1094
+ reason: string,
1075
1095
  ): OracleSelection {
1076
- const thinking = resolveThinkingLevel(model, thinkingLevelOverride);
1077
- return {
1078
- modelRef: `${model.provider}/${model.id}`,
1079
- provider: model.provider,
1080
- modelId: model.id,
1081
- modelName: model.name,
1082
- thinkingLevel: thinking.effective,
1083
- ...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
1084
- autoSelected: true,
1085
- selectionReason: appendThinkingLevelClampReason(reason, thinking),
1086
- };
1096
+ const thinking = resolveThinkingLevel(model, thinkingLevelOverride);
1097
+ return {
1098
+ modelRef: `${model.provider}/${model.id}`,
1099
+ provider: model.provider,
1100
+ modelId: model.id,
1101
+ modelName: model.name,
1102
+ thinkingLevel: thinking.effective,
1103
+ ...(thinking.clamped
1104
+ ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true }
1105
+ : {}),
1106
+ autoSelected: true,
1107
+ selectionReason: appendThinkingLevelClampReason(reason, thinking),
1108
+ };
1087
1109
  }
1088
1110
 
1089
1111
  function buildSessionFallbackSelection(
1090
- ctx: ModelSelectionContext,
1091
- thinkingLevelOverride: ThinkingLevel | undefined,
1112
+ ctx: ModelSelectionContext,
1113
+ thinkingLevelOverride: ThinkingLevel | undefined,
1092
1114
  ): OracleSelection | undefined {
1093
- const model = ctx.model;
1094
- if (!model || !isModelInScope(ctx, model)) return undefined;
1095
- return toOracleSelection(
1096
- model,
1097
- thinkingLevelOverride,
1098
- `Used the current session model ${model.provider}/${model.id} as a final fallback.`,
1099
- );
1115
+ const model = ctx.model;
1116
+ if (!model || !isModelInScope(ctx, model)) return undefined;
1117
+ return toOracleSelection(
1118
+ model,
1119
+ thinkingLevelOverride,
1120
+ `Used the current session model ${model.provider}/${model.id} as a final fallback.`,
1121
+ );
1100
1122
  }
1101
1123
 
1102
- function withFallbackReason(selection: OracleSelection, previous: OracleSelection): OracleSelection {
1103
- return {
1104
- ...selection,
1105
- selectionReason: `${selection.selectionReason} (Fell back from ${previous.modelRef} after a model-availability error.)`,
1106
- };
1124
+ function withFallbackReason(
1125
+ selection: OracleSelection,
1126
+ previous: OracleSelection,
1127
+ ): OracleSelection {
1128
+ return {
1129
+ ...selection,
1130
+ selectionReason: `${selection.selectionReason} (Fell back from ${previous.modelRef} after a model-availability error.)`,
1131
+ };
1107
1132
  }
1108
1133
 
1109
1134
  async function selectOracleModel(
1110
- ctx: ModelSelectionContext,
1111
- thinkingLevelOverride?: ThinkingLevel,
1112
- ): Promise<{ ok: true; selection: OracleSelection; ordered: OracleSelection[] } | { ok: false; error: string }> {
1113
- const available = await getAutoSelectableModels(ctx);
1114
- if (available.length === 0) {
1115
- return {
1116
- ok: false,
1117
- error: hasModelScope(ctx)
1118
- ? "No authenticated models are available in the current session model scope. Adjust the scope, log in, or configure an API key."
1119
- : "No authenticated models are available. Log in or configure an API key first.",
1120
- };
1121
- }
1122
-
1123
- const currentProvider = ctx.model?.provider;
1124
- const sameProvider = currentProvider ? available.filter((model) => model.provider === currentProvider) : [];
1125
- const sameProviderReasoning = sameProvider.filter((model) => model.reasoning);
1126
- const allReasoning = available.filter((model) => model.reasoning);
1127
-
1128
- let candidates = sameProviderReasoning;
1129
- let reason = "Selected the top-ranked reasoning model on the current provider.";
1130
- let providerForPreferences = currentProvider;
1131
-
1132
- if (candidates.length === 0 && sameProvider.length > 0) {
1133
- candidates = sameProvider;
1134
- reason = "The current provider has no reasoning models available, so the top-ranked model on that provider was used.";
1135
- } else if (candidates.length === 0 && allReasoning.length > 0) {
1136
- candidates = allReasoning;
1137
- providerForPreferences = undefined;
1138
- reason = "No current model/provider was active, so the top-ranked reasoning model across all available providers was used.";
1139
- } else if (candidates.length === 0) {
1140
- candidates = available;
1141
- providerForPreferences = undefined;
1142
- reason = "No reasoning models were available, so the top-ranked model across all available providers was used.";
1143
- }
1144
-
1145
- const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
1146
- const preferred = selectPreferredModel(candidates, providerForPreferences);
1147
- const orderedModels = preferred ? [preferred, ...sorted.filter((model) => model !== preferred)] : sorted;
1148
-
1149
- const ordered = orderedModels.map((model, index) =>
1150
- toOracleSelection(
1151
- model,
1152
- thinkingLevelOverride,
1153
- index === 0
1154
- ? preferred
1155
- ? `Selected ${model.id} via the hardcoded preference list for ${model.provider}.`
1156
- : reason
1157
- : `Auto-selected ${model.provider}/${model.id} as a lower-ranked fallback.`,
1158
- ),
1159
- );
1160
-
1161
- return { ok: true, selection: ordered[0], ordered };
1135
+ ctx: ModelSelectionContext,
1136
+ thinkingLevelOverride?: ThinkingLevel,
1137
+ ): Promise<
1138
+ | { ok: true; selection: OracleSelection; ordered: OracleSelection[] }
1139
+ | { ok: false; error: string }
1140
+ > {
1141
+ const available = await getAutoSelectableModels(ctx);
1142
+ if (available.length === 0) {
1143
+ return {
1144
+ ok: false,
1145
+ error: hasModelScope(ctx)
1146
+ ? "No authenticated models are available in the current session model scope. Adjust the scope, log in, or configure an API key."
1147
+ : "No authenticated models are available. Log in or configure an API key first.",
1148
+ };
1149
+ }
1150
+
1151
+ const currentProvider = ctx.model?.provider;
1152
+ const sameProvider = currentProvider
1153
+ ? available.filter((model) => model.provider === currentProvider)
1154
+ : [];
1155
+ const sameProviderReasoning = sameProvider.filter((model) => model.reasoning);
1156
+ const allReasoning = available.filter((model) => model.reasoning);
1157
+
1158
+ let candidates = sameProviderReasoning;
1159
+ let reason = "Selected the top-ranked reasoning model on the current provider.";
1160
+ let providerForPreferences = currentProvider;
1161
+
1162
+ if (candidates.length === 0 && sameProvider.length > 0) {
1163
+ candidates = sameProvider;
1164
+ reason =
1165
+ "The current provider has no reasoning models available, so the top-ranked model on that provider was used.";
1166
+ } else if (candidates.length === 0 && allReasoning.length > 0) {
1167
+ candidates = allReasoning;
1168
+ providerForPreferences = undefined;
1169
+ reason =
1170
+ "No current model/provider was active, so the top-ranked reasoning model across all available providers was used.";
1171
+ } else if (candidates.length === 0) {
1172
+ candidates = available;
1173
+ providerForPreferences = undefined;
1174
+ reason =
1175
+ "No reasoning models were available, so the top-ranked model across all available providers was used.";
1176
+ }
1177
+
1178
+ const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
1179
+ const preferred = selectPreferredModel(candidates, providerForPreferences);
1180
+ const orderedModels = preferred
1181
+ ? [preferred, ...sorted.filter((model) => model !== preferred)]
1182
+ : sorted;
1183
+
1184
+ const ordered = orderedModels.map((model, index) =>
1185
+ toOracleSelection(
1186
+ model,
1187
+ thinkingLevelOverride,
1188
+ index === 0
1189
+ ? preferred
1190
+ ? `Selected ${model.id} via the hardcoded preference list for ${model.provider}.`
1191
+ : reason
1192
+ : `Auto-selected ${model.provider}/${model.id} as a lower-ranked fallback.`,
1193
+ ),
1194
+ );
1195
+
1196
+ return { ok: true, selection: ordered[0], ordered };
1162
1197
  }
1163
1198
 
1164
1199
  function updateOracleUi(ctx: ExtensionContext, activeRuns: Map<string, OracleUiRun>): void {
1165
- if (!ctx.hasUI) return;
1166
- const theme = ctx.ui.theme;
1167
- if (activeRuns.size === 0) {
1168
- ctx.ui.setStatus(ORACLE_STATUS_ID, undefined);
1169
- ctx.ui.setWidget(ORACLE_WIDGET_ID, undefined);
1170
- return;
1171
- }
1172
-
1173
- const runs = [...activeRuns.values()].sort((a, b) => b.startedAt - a.startedAt);
1174
- const primary = runs[0];
1175
- const activeCount = runs.length;
1176
- const modelText = primary.selection?.modelId ?? "selecting…";
1177
- const thinkingText = primary.selection?.thinkingLevel && primary.selection.thinkingLevel !== "off"
1178
- ? ` ${primary.selection.thinkingLevel}`
1179
- : "";
1180
- const elapsed = formatDuration(Date.now() - primary.startedAt);
1181
- const mode = primary.includeBash ? "read-only+bash" : "read-only";
1182
- const status =
1183
- theme.fg("accent", "🔮 oracle") +
1184
- theme.fg("dim", ` ${modelText}${thinkingText} · ${mode} · ${elapsed}`) +
1185
- (activeCount > 1 ? theme.fg("warning", ` · ${activeCount} active`) : "");
1186
- ctx.ui.setStatus(ORACLE_STATUS_ID, status);
1187
-
1188
- const lines = [
1189
- `🔮 Oracle ${activeCount > 1 ? `(${activeCount} active)` : ""}`.trim(),
1190
- `${primary.selection?.modelRef ?? "selecting model…"} · ${mode} · ${elapsed}`,
1191
- `task: ${shorten(primary.task, 110)}`,
1192
- ];
1193
- if (primary.preview && primary.preview.trim()) lines.push(`preview: ${shorten(primary.preview, 110)}`);
1194
- ctx.ui.setWidget(ORACLE_WIDGET_ID, lines, { placement: "belowEditor" });
1200
+ if (!ctx.hasUI) return;
1201
+ const theme = ctx.ui.theme;
1202
+ if (activeRuns.size === 0) {
1203
+ ctx.ui.setStatus(ORACLE_STATUS_ID, undefined);
1204
+ ctx.ui.setWidget(ORACLE_WIDGET_ID, undefined);
1205
+ return;
1206
+ }
1207
+
1208
+ const runs = [...activeRuns.values()].sort((a, b) => b.startedAt - a.startedAt);
1209
+ const primary = runs[0];
1210
+ const activeCount = runs.length;
1211
+ const modelText = primary.selection?.modelId ?? "selecting…";
1212
+ const thinkingText =
1213
+ primary.selection?.thinkingLevel && primary.selection.thinkingLevel !== "off"
1214
+ ? ` ${primary.selection.thinkingLevel}`
1215
+ : "";
1216
+ const elapsed = formatDuration(Date.now() - primary.startedAt);
1217
+ const mode = primary.includeBash ? "read-only+bash" : "read-only";
1218
+ const status =
1219
+ theme.fg("accent", "🔮 oracle") +
1220
+ theme.fg("dim", ` ${modelText}${thinkingText} · ${mode} · ${elapsed}`) +
1221
+ (activeCount > 1 ? theme.fg("warning", ` · ${activeCount} active`) : "");
1222
+ ctx.ui.setStatus(ORACLE_STATUS_ID, status);
1223
+
1224
+ const lines = [
1225
+ `🔮 Oracle ${activeCount > 1 ? `(${activeCount} active)` : ""}`.trim(),
1226
+ `${primary.selection?.modelRef ?? "selecting model…"} · ${mode} · ${elapsed}`,
1227
+ `task: ${shorten(primary.task, 110)}`,
1228
+ ];
1229
+ if (primary.preview && primary.preview.trim())
1230
+ lines.push(`preview: ${shorten(primary.preview, 110)}`);
1231
+ ctx.ui.setWidget(ORACLE_WIDGET_ID, lines, { placement: "belowEditor" });
1195
1232
  }
1196
1233
 
1197
1234
  async function runOracle(
1198
- selection: OracleSelection,
1199
- params: { task: string; includeBash?: boolean; cwd?: string },
1200
- signal: AbortSignal | undefined,
1201
- onUpdate: ((result: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => void) | undefined,
1202
- defaultCwd: string,
1203
- spawnImpl: typeof spawn = spawn,
1204
- ): Promise<{ ok: true; output: string; details: OracleDetails } | { ok: false; error: string; details: OracleDetails }> {
1205
- const cwd = params.cwd ?? defaultCwd;
1206
- const includeBash = params.includeBash ?? false;
1207
- const tools = includeBash ? READ_ONLY_PLUS_BASH_TOOLS : READ_ONLY_TOOLS;
1208
- const startedAt = Date.now();
1209
- const usage = createEmptyUsage();
1210
- let currentText = "";
1211
- let finalOutput = "";
1212
- let stderr = "";
1213
-
1214
- let lastStopReason: string | undefined;
1215
- let lastErrorMessage: string | undefined;
1216
-
1217
- const details: OracleDetails = {
1218
- ...selection,
1219
- includeBash,
1220
- usage,
1221
- stderr,
1222
- exitCode: 0,
1223
- durationMs: 0,
1224
- cwd,
1225
- };
1226
-
1227
- const emit = () => {
1228
- details.stderr = stderr;
1229
- details.durationMs = Date.now() - startedAt;
1230
- onUpdate?.({
1231
- content: [{ type: "text", text: currentText || finalOutput || "Consulting oracle..." }],
1232
- details,
1233
- });
1234
- };
1235
-
1236
- const modelArg = withThinking(selection.modelRef, selection.thinkingLevel);
1237
- const args = [
1238
- "--mode",
1239
- "json",
1240
- "-p",
1241
- "--no-session",
1242
- "--model",
1243
- modelArg,
1244
- "--tools",
1245
- tools.join(","),
1246
- "--append-system-prompt",
1247
- ORACLE_SYSTEM_PROMPT,
1248
- params.task,
1249
- ];
1250
-
1251
- const invocation = getPiInvocation(args);
1252
- let wasAborted = false;
1253
-
1254
- const exitCode = await new Promise<number>((resolve) => {
1255
- const proc = spawnImpl(invocation.command, invocation.args, {
1256
- cwd,
1257
- shell: false,
1258
- stdio: ["ignore", "pipe", "pipe"],
1259
- });
1260
- let buffer = "";
1261
-
1262
- const processLine = (line: string) => {
1263
- if (!line.trim()) return;
1264
- let event: any;
1265
- try {
1266
- event = JSON.parse(line);
1267
- } catch {
1268
- return;
1269
- }
1270
-
1271
- if (event.type === "message_start" && event.message?.role === "assistant") {
1272
- // A later assistant turn supersedes any earlier tool/intermediate answer.
1273
- // Keep only the current message so a tool-only final turn cannot return stale text.
1274
- currentText = "";
1275
- finalOutput = "";
1276
- emit();
1277
- return;
1278
- }
1279
-
1280
- if (event.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
1281
- currentText += event.assistantMessageEvent.delta ?? "";
1282
- emit();
1283
- return;
1284
- }
1285
-
1286
- if (event.type === "message_end" && event.message?.role === "assistant") {
1287
- const text = extractTextFromContent(event.message.content) || currentText;
1288
- // message_end is authoritative, including an empty tool-only message.
1289
- finalOutput = text;
1290
- currentText = "";
1291
-
1292
- const stopReason = event.message.stopReason;
1293
- lastStopReason = typeof stopReason === "string" ? stopReason : undefined;
1294
- const errorMessageField = event.message.errorMessage;
1295
- lastErrorMessage = typeof errorMessageField === "string" ? errorMessageField : undefined;
1296
-
1297
- addUsageStats(usage, event.message.usage as Usage | undefined, {
1298
- countTurn: true,
1299
- updateContextTokens: true,
1300
- });
1301
- emit();
1302
- return;
1303
- }
1304
-
1305
- if (event.type === "compaction_end") {
1306
- addUsageStats(usage, event.result?.usage as Usage | undefined);
1307
- }
1308
- };
1309
-
1310
- proc.stdout.on("data", (data) => {
1311
- buffer += data.toString();
1312
- const lines = buffer.split("\n");
1313
- buffer = lines.pop() || "";
1314
- for (const line of lines) processLine(line);
1315
- });
1316
-
1317
- proc.stderr.on("data", (data) => {
1318
- stderr += data.toString();
1319
- emit();
1320
- });
1321
-
1322
- let closed = false;
1323
- let killTimer: ReturnType<typeof setTimeout> | undefined;
1324
- let abortListener: (() => void) | undefined;
1325
-
1326
- const childHasExited = () => proc.exitCode !== null || proc.signalCode !== null;
1327
- const clearEscalation = () => {
1328
- if (killTimer) {
1329
- clearTimeout(killTimer);
1330
- killTimer = undefined;
1331
- }
1332
- };
1333
- const cleanup = () => {
1334
- clearEscalation();
1335
- if (signal && abortListener) {
1336
- signal.removeEventListener("abort", abortListener);
1337
- abortListener = undefined;
1338
- }
1339
- };
1340
-
1341
- proc.on("exit", () => {
1342
- clearEscalation();
1343
- });
1344
-
1345
- proc.on("close", (code) => {
1346
- closed = true;
1347
- cleanup();
1348
- if (buffer.trim()) processLine(buffer);
1349
- resolve(code ?? 0);
1350
- });
1351
-
1352
- proc.on("error", (error) => {
1353
- cleanup();
1354
- stderr += `${error instanceof Error ? error.message : String(error)}\n`;
1355
- resolve(1);
1356
- });
1357
-
1358
- if (signal) {
1359
- abortListener = () => {
1360
- wasAborted = true;
1361
- proc.kill("SIGTERM");
1362
- if (closed || childHasExited()) return;
1363
- killTimer = setTimeout(() => {
1364
- if (!closed && !childHasExited()) proc.kill("SIGKILL");
1365
- }, 5000);
1366
- };
1367
- if (signal.aborted) abortListener();
1368
- else signal.addEventListener("abort", abortListener, { once: true });
1369
- }
1370
- });
1371
-
1372
- details.stderr = stderr.trim();
1373
- details.exitCode = exitCode;
1374
- details.durationMs = Date.now() - startedAt;
1375
-
1376
- if (wasAborted) {
1377
- if (details.exitCode === 0) details.exitCode = 1;
1378
- return { ok: false, error: "Oracle was aborted.", details };
1379
- }
1380
-
1381
- // The pi subprocess in `--mode json` does not promote an errored assistant
1382
- // turn to a non-zero exit code or stderr; the error is only carried on the
1383
- // streamed assistant message via stopReason/errorMessage. Surface that here
1384
- // so callers can distinguish transient provider errors from a genuinely
1385
- // empty response.
1386
- if (lastStopReason === "error" || lastStopReason === "aborted") {
1387
- if (details.exitCode === 0) details.exitCode = 1;
1388
- return {
1389
- ok: false,
1390
- error: formatOracleModelError(lastStopReason, lastErrorMessage),
1391
- details,
1392
- };
1393
- }
1394
-
1395
- if (exitCode !== 0) {
1396
- return {
1397
- ok: false,
1398
- error: details.stderr || finalOutput || "Oracle subprocess failed.",
1399
- details,
1400
- };
1401
- }
1402
-
1403
- if (!finalOutput.trim()) {
1404
- if (details.exitCode === 0) details.exitCode = 1;
1405
- return {
1406
- ok: false,
1407
- error: details.stderr || "Oracle finished without returning any text.",
1408
- details,
1409
- };
1410
- }
1411
-
1412
- return {
1413
- ok: true,
1414
- output: finalOutput.trim(),
1415
- details,
1416
- };
1235
+ selection: OracleSelection,
1236
+ params: { task: string; includeBash?: boolean; cwd?: string },
1237
+ signal: AbortSignal | undefined,
1238
+ onUpdate:
1239
+ | ((result: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => void)
1240
+ | undefined,
1241
+ defaultCwd: string,
1242
+ spawnImpl: typeof spawn = spawn,
1243
+ ): Promise<
1244
+ | { ok: true; output: string; details: OracleDetails }
1245
+ | { ok: false; error: string; details: OracleDetails }
1246
+ > {
1247
+ const cwd = params.cwd ?? defaultCwd;
1248
+ const includeBash = params.includeBash ?? false;
1249
+ const tools = includeBash ? READ_ONLY_PLUS_BASH_TOOLS : READ_ONLY_TOOLS;
1250
+ const startedAt = Date.now();
1251
+ const usage = createEmptyUsage();
1252
+ let currentText = "";
1253
+ let finalOutput = "";
1254
+ let stderr = "";
1255
+
1256
+ let lastStopReason: string | undefined;
1257
+ let lastErrorMessage: string | undefined;
1258
+
1259
+ const details: OracleDetails = {
1260
+ ...selection,
1261
+ includeBash,
1262
+ usage,
1263
+ stderr,
1264
+ exitCode: 0,
1265
+ durationMs: 0,
1266
+ cwd,
1267
+ };
1268
+
1269
+ const emit = () => {
1270
+ details.stderr = stderr;
1271
+ details.durationMs = Date.now() - startedAt;
1272
+ onUpdate?.({
1273
+ content: [{ type: "text", text: currentText || finalOutput || "Consulting oracle..." }],
1274
+ details,
1275
+ });
1276
+ };
1277
+
1278
+ const modelArg = withThinking(selection.modelRef, selection.thinkingLevel);
1279
+ const args = [
1280
+ "--mode",
1281
+ "json",
1282
+ "-p",
1283
+ "--no-session",
1284
+ "--model",
1285
+ modelArg,
1286
+ "--tools",
1287
+ tools.join(","),
1288
+ "--append-system-prompt",
1289
+ ORACLE_SYSTEM_PROMPT,
1290
+ params.task,
1291
+ ];
1292
+
1293
+ const invocation = getPiInvocation(args);
1294
+ let wasAborted = false;
1295
+
1296
+ const exitCode = await new Promise<number>((resolve) => {
1297
+ const proc = spawnImpl(invocation.command, invocation.args, {
1298
+ cwd,
1299
+ shell: false,
1300
+ stdio: ["ignore", "pipe", "pipe"],
1301
+ });
1302
+ let buffer = "";
1303
+
1304
+ const processLine = (line: string) => {
1305
+ if (!line.trim()) return;
1306
+ let event: any;
1307
+ try {
1308
+ event = JSON.parse(line);
1309
+ } catch {
1310
+ return;
1311
+ }
1312
+
1313
+ if (event.type === "message_start" && event.message?.role === "assistant") {
1314
+ // A later assistant turn supersedes any earlier tool/intermediate answer.
1315
+ // Keep only the current message so a tool-only final turn cannot return stale text.
1316
+ currentText = "";
1317
+ finalOutput = "";
1318
+ emit();
1319
+ return;
1320
+ }
1321
+
1322
+ if (event.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
1323
+ currentText += event.assistantMessageEvent.delta ?? "";
1324
+ emit();
1325
+ return;
1326
+ }
1327
+
1328
+ if (event.type === "message_end" && event.message?.role === "assistant") {
1329
+ const text = extractTextFromContent(event.message.content) || currentText;
1330
+ // message_end is authoritative, including an empty tool-only message.
1331
+ finalOutput = text;
1332
+ currentText = "";
1333
+
1334
+ const stopReason = event.message.stopReason;
1335
+ lastStopReason = typeof stopReason === "string" ? stopReason : undefined;
1336
+ const errorMessageField = event.message.errorMessage;
1337
+ lastErrorMessage = typeof errorMessageField === "string" ? errorMessageField : undefined;
1338
+
1339
+ addUsageStats(usage, event.message.usage as Usage | undefined, {
1340
+ countTurn: true,
1341
+ updateContextTokens: true,
1342
+ });
1343
+ emit();
1344
+ return;
1345
+ }
1346
+
1347
+ if (event.type === "compaction_end") {
1348
+ addUsageStats(usage, event.result?.usage as Usage | undefined);
1349
+ }
1350
+ };
1351
+
1352
+ proc.stdout.on("data", (data) => {
1353
+ buffer += data.toString();
1354
+ const lines = buffer.split("\n");
1355
+ buffer = lines.pop() || "";
1356
+ for (const line of lines) processLine(line);
1357
+ });
1358
+
1359
+ proc.stderr.on("data", (data) => {
1360
+ stderr += data.toString();
1361
+ emit();
1362
+ });
1363
+
1364
+ let closed = false;
1365
+ let killTimer: ReturnType<typeof setTimeout> | undefined;
1366
+ let abortListener: (() => void) | undefined;
1367
+
1368
+ const childHasExited = () => proc.exitCode !== null || proc.signalCode !== null;
1369
+ const clearEscalation = () => {
1370
+ if (killTimer) {
1371
+ clearTimeout(killTimer);
1372
+ killTimer = undefined;
1373
+ }
1374
+ };
1375
+ const cleanup = () => {
1376
+ clearEscalation();
1377
+ if (signal && abortListener) {
1378
+ signal.removeEventListener("abort", abortListener);
1379
+ abortListener = undefined;
1380
+ }
1381
+ };
1382
+
1383
+ proc.on("exit", () => {
1384
+ clearEscalation();
1385
+ });
1386
+
1387
+ proc.on("close", (code) => {
1388
+ closed = true;
1389
+ cleanup();
1390
+ if (buffer.trim()) processLine(buffer);
1391
+ resolve(code ?? 0);
1392
+ });
1393
+
1394
+ proc.on("error", (error) => {
1395
+ cleanup();
1396
+ stderr += `${error instanceof Error ? error.message : String(error)}\n`;
1397
+ resolve(1);
1398
+ });
1399
+
1400
+ if (signal) {
1401
+ abortListener = () => {
1402
+ wasAborted = true;
1403
+ proc.kill("SIGTERM");
1404
+ if (closed || childHasExited()) return;
1405
+ killTimer = setTimeout(() => {
1406
+ if (!closed && !childHasExited()) proc.kill("SIGKILL");
1407
+ }, 5000);
1408
+ };
1409
+ if (signal.aborted) abortListener();
1410
+ else signal.addEventListener("abort", abortListener, { once: true });
1411
+ }
1412
+ });
1413
+
1414
+ details.stderr = stderr.trim();
1415
+ details.exitCode = exitCode;
1416
+ details.durationMs = Date.now() - startedAt;
1417
+
1418
+ if (wasAborted) {
1419
+ if (details.exitCode === 0) details.exitCode = 1;
1420
+ return { ok: false, error: "Oracle was aborted.", details };
1421
+ }
1422
+
1423
+ // The pi subprocess in `--mode json` does not promote an errored assistant
1424
+ // turn to a non-zero exit code or stderr; the error is only carried on the
1425
+ // streamed assistant message via stopReason/errorMessage. Surface that here
1426
+ // so callers can distinguish transient provider errors from a genuinely
1427
+ // empty response.
1428
+ if (lastStopReason === "error" || lastStopReason === "aborted") {
1429
+ if (details.exitCode === 0) details.exitCode = 1;
1430
+ return {
1431
+ ok: false,
1432
+ error: formatOracleModelError(lastStopReason, lastErrorMessage),
1433
+ details,
1434
+ };
1435
+ }
1436
+
1437
+ if (exitCode !== 0) {
1438
+ return {
1439
+ ok: false,
1440
+ error: details.stderr || finalOutput || "Oracle subprocess failed.",
1441
+ details,
1442
+ };
1443
+ }
1444
+
1445
+ if (!finalOutput.trim()) {
1446
+ if (details.exitCode === 0) details.exitCode = 1;
1447
+ return {
1448
+ ok: false,
1449
+ error: details.stderr || "Oracle finished without returning any text.",
1450
+ details,
1451
+ };
1452
+ }
1453
+
1454
+ return {
1455
+ ok: true,
1456
+ output: finalOutput.trim(),
1457
+ details,
1458
+ };
1417
1459
  }
1418
1460
 
1419
1461
  function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): string {
1420
- const lines = text.trim().split("\n");
1421
- if (lines.length <= lineLimit) return lines.join("\n");
1422
- return [...lines.slice(0, lineLimit), `... (${lines.length - lineLimit} more lines)`].join("\n");
1462
+ const lines = text.trim().split("\n");
1463
+ if (lines.length <= lineLimit) return lines.join("\n");
1464
+ return [...lines.slice(0, lineLimit), `... (${lines.length - lineLimit} more lines)`].join("\n");
1423
1465
  }
1424
1466
 
1425
1467
  type OracleExtensionDeps = {
1426
- spawnImpl?: typeof spawn;
1468
+ spawnImpl?: typeof spawn;
1427
1469
  };
1428
1470
 
1429
1471
  export const __test__ = {
1430
- buildSessionFallbackSelection,
1431
- createOracleExtension,
1432
- findAvailableModel,
1433
- isModelAvailabilityError,
1434
- parseModelPreference,
1435
- runOracle,
1436
- resolveThinkingLevel,
1437
- selectOracleModel,
1472
+ buildSessionFallbackSelection,
1473
+ createOracleExtension,
1474
+ findAvailableModel,
1475
+ isModelAvailabilityError,
1476
+ parseModelPreference,
1477
+ runOracle,
1478
+ resolveThinkingLevel,
1479
+ selectOracleModel,
1438
1480
  };
1439
1481
 
1440
1482
  export default function oracleExtension(pi: ExtensionAPI) {
1441
- createOracleExtension(pi);
1483
+ createOracleExtension(pi);
1442
1484
  }
1443
1485
 
1444
1486
  function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {}) {
1445
- const spawnImpl = deps.spawnImpl ?? spawn;
1446
- const activeRuns = new Map<string, OracleUiRun>();
1447
- let preferences: OraclePreferences = {};
1448
-
1449
- pi.on("session_start", async (_event, ctx) => {
1450
- preferences = await readOraclePreferences();
1451
- activeRuns.clear();
1452
- updateOracleUi(ctx, activeRuns);
1453
- });
1454
-
1455
- pi.registerCommand("oracle", {
1456
- description: "Configure Oracle default model and thinking level for future oracle tool calls",
1457
- getArgumentCompletions: (prefix) => {
1458
- const parts = prefix.trim().toLowerCase().split(/\s+/).filter(Boolean);
1459
- const first = parts[0] ?? "";
1460
- if (parts.length <= 1) {
1461
- const commands = ["status", "model", "thinking", "clear"];
1462
- const matches = commands.filter((command) => command.startsWith(first));
1463
- return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
1464
- }
1465
- if (parts[0] === "thinking") {
1466
- const query = parts[1] ?? "";
1467
- const values = ["auto", ...THINKING_LEVELS];
1468
- const matches = values.filter((value) => value.startsWith(query));
1469
- return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
1470
- }
1471
- if (parts[0] === "clear") {
1472
- const query = parts[1] ?? "";
1473
- const values = ["all", "model", "thinking"];
1474
- const matches = values.filter((value) => value.startsWith(query));
1475
- return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
1476
- }
1477
- return null;
1478
- },
1479
- handler: async (args, ctx) => {
1480
- const raw = args.trim();
1481
- const tokens = raw ? raw.split(/\s+/) : [];
1482
- const [command = "status", ...rest] = tokens;
1483
- const action = command.toLowerCase();
1484
- const configPath = getOracleConfigPath();
1485
-
1486
- const save = async (next: OraclePreferences): Promise<string | undefined> => {
1487
- preferences = next;
1488
- try {
1489
- await writeOraclePreferences(preferences);
1490
- return undefined;
1491
- } catch (error) {
1492
- const message = error instanceof Error ? error.message : String(error);
1493
- return `Preference changed for this process, but could not save ${configPath}: ${message}`;
1494
- }
1495
- };
1496
-
1497
- if (action === "status" || action === "show") {
1498
- notifyCommand(ctx, formatOraclePreferences(preferences));
1499
- return;
1500
- }
1501
-
1502
- if (action === "model") {
1503
- const model = rest.join(" ").trim();
1504
- const normalizedModelAction = model.toLowerCase();
1505
- if (!model) {
1506
- notifyCommand(ctx, "Usage: /oracle model <provider/model|auto>", "warning");
1507
- return;
1508
- }
1509
- if (normalizedModelAction === "auto" || normalizedModelAction === "clear" || normalizedModelAction === "default") {
1510
- const warning = await save({ ...preferences, model: undefined });
1511
- notifyCommand(ctx, `Oracle default model cleared; future oracle calls will auto-select. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1512
- return;
1513
- }
1514
- const parsedModel = parseModelPreference(model);
1515
- const next = {
1516
- ...preferences,
1517
- model: parsedModel.model,
1518
- thinkingLevel: parsedModel.thinkingLevel ?? preferences.thinkingLevel,
1519
- };
1520
- const warning = await save(next);
1521
- notifyCommand(ctx, `Oracle default model set to ${parsedModel.model}. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1522
- return;
1523
- }
1524
-
1525
- if (action === "thinking" || action === "think" || action === "thinking-level") {
1526
- const value = rest[0]?.trim().toLowerCase();
1527
- if (!value) {
1528
- notifyCommand(ctx, `Usage: /oracle thinking ${THINKING_LEVELS.join(" | ")} | auto`, "warning");
1529
- return;
1530
- }
1531
- if (value === "auto" || value === "clear" || value === "default") {
1532
- const warning = await save({ ...preferences, thinkingLevel: undefined });
1533
- notifyCommand(ctx, `Oracle default thinking level cleared; future oracle calls will use built-in defaults. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1534
- return;
1535
- }
1536
- const thinkingLevel = parseThinkingLevel(value);
1537
- if (!thinkingLevel) {
1538
- notifyCommand(ctx, `Usage: /oracle thinking ${THINKING_LEVELS.join(" | ")} | auto`, "warning");
1539
- return;
1540
- }
1541
- const warning = await save({ ...preferences, thinkingLevel });
1542
- notifyCommand(ctx, `Oracle default thinking level set to ${thinkingLevel}. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1543
- return;
1544
- }
1545
-
1546
- if (action === "clear" || action === "reset") {
1547
- const target = rest[0]?.trim().toLowerCase() || "all";
1548
- let next: OraclePreferences;
1549
- if (target === "all") next = {};
1550
- else if (target === "model") next = { ...preferences, model: undefined };
1551
- else if (target === "thinking" || target === "thinking-level") next = { ...preferences, thinkingLevel: undefined };
1552
- else {
1553
- notifyCommand(ctx, "Usage: /oracle clear [all|model|thinking]", "warning");
1554
- return;
1555
- }
1556
- const warning = await save(next);
1557
- notifyCommand(ctx, `Oracle defaults cleared (${target}). ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1558
- return;
1559
- }
1560
-
1561
- notifyCommand(ctx, "Usage: /oracle status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|max|auto> | clear [all|model|thinking]", "warning");
1562
- },
1563
- });
1564
-
1565
- pi.registerCommand("oracle-model", {
1566
- description: "Show which model the oracle would use right now",
1567
- handler: async (_args, ctx) => {
1568
- const defaultModel = parseModelPreference(preferences.model);
1569
- if (defaultModel.model) {
1570
- const matched = await findAvailableModel(ctx, defaultModel.model);
1571
- const thinking = resolveThinkingLevel(matched, preferences.thinkingLevel ?? defaultModel.thinkingLevel);
1572
- const modelRef = matched ? `${matched.provider}/${matched.id}` : defaultModel.model;
1573
- const suffix = matched
1574
- ? appendThinkingLevelClampReason("Configured default oracle model is active.", thinking)
1575
- : "Configured default oracle model is active, but it was not matched against the authenticated model list.";
1576
- notifyCommand(ctx, `Oracle: ${modelRef} (${thinking.effective}) — ${suffix}`);
1577
- return;
1578
- }
1579
- const selectionResult = await selectOracleModel(ctx, preferences.thinkingLevel);
1580
- if (!selectionResult.ok) {
1581
- notifyCommand(ctx, selectionResult.error, "error");
1582
- return;
1583
- }
1584
-
1585
- const { selection } = selectionResult;
1586
- const message = `Oracle: ${selection.modelRef} (${selection.thinkingLevel}) — ${selection.selectionReason}`;
1587
- notifyCommand(ctx, message);
1588
- },
1589
- });
1590
-
1591
- pi.on("tool_result", async (event) => {
1592
- if (event.toolName !== "oracle") return undefined;
1593
- const details = event.details as OracleDetails | undefined;
1594
- if ((details?.exitCode ?? 0) === 0) return undefined;
1595
- return { isError: true };
1596
- });
1597
-
1598
- pi.registerTool({
1599
- name: "oracle",
1600
- label: "Oracle",
1601
- description:
1602
- "Consult a separate read-only oracle subprocess for deep analysis, code review, debugging, planning, and second opinions.",
1603
- promptSnippet:
1604
- "Consult a read-only oracle that auto-selects the strongest reasoning model on the current provider/subscription.",
1605
- promptGuidelines: [
1606
- "Use oracle sparingly when you want a second opinion, deeper analysis, code review, debugging help, or a higher-reasoning pass.",
1607
- "Do not use oracle for routine low-value work; oracle is slower than the main agent.",
1608
- "The oracle tool is read-only by default and only exposes read, grep, find, and ls unless oracle includeBash is enabled.",
1609
- "Set oracle includeBash only when the extra bash inspection tool is genuinely useful; keep oracle relying on read, grep, find, and ls otherwise.",
1610
- "The oracle tool requests xhigh by default for reasoning models; oracle defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
1611
- ],
1612
- parameters: OracleParams,
1613
-
1614
- async execute(toolCallId, params, signal, onUpdate, ctx) {
1615
- const rawTask = (params as { task?: unknown }).task;
1616
- const task = typeof rawTask === "string" ? rawTask.trim() : "";
1617
- if (!task) throw new Error("Invalid parameters: expected task to be a non-empty string.");
1618
-
1619
- const explicitModel = parseModelPreference(params.model);
1620
- const defaultModel = parseModelPreference(preferences.model);
1621
- const configuredModel = explicitModel.model ?? defaultModel.model;
1622
- const configuredThinkingLevel =
1623
- params.thinkingLevel ?? explicitModel.thinkingLevel ?? preferences.thinkingLevel ?? defaultModel.thinkingLevel;
1624
- const uiRun: OracleUiRun = {
1625
- task,
1626
- includeBash: params.includeBash ?? false,
1627
- startedAt: Date.now(),
1628
- };
1629
- activeRuns.set(toolCallId, uiRun);
1630
- updateOracleUi(ctx, activeRuns);
1631
-
1632
- try {
1633
- const attempts: OracleSelection[] = [];
1634
- const seen = new Set<string>();
1635
- const aggregateUsage = createEmptyUsage();
1636
- const finalizeResult = (
1637
- result: { content: Array<{ type: "text"; text: string }>; details: OracleDetails },
1638
- ): AgentToolResult<OracleDetails> => ({
1639
- ...result,
1640
- details: { ...result.details, usage: cloneUsageStats(aggregateUsage) },
1641
- usage: usageStatsToToolUsage(aggregateUsage),
1642
- });
1643
- const pushAttempt = (candidate: OracleSelection | undefined) => {
1644
- if (!candidate) return;
1645
- const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
1646
- if (seen.has(key)) return;
1647
- seen.add(key);
1648
- attempts.push(candidate);
1649
- };
1650
-
1651
- if (configuredModel) {
1652
- const modelRef = configuredModel;
1653
- const matched = await findAvailableModel(ctx, modelRef);
1654
- const provider =
1655
- matched?.provider ?? (modelRef.includes("/") ? modelRef.split("/")[0] : ctx.model?.provider ?? "unknown");
1656
- const modelId = matched?.id ?? (modelRef.includes("/") ? modelRef.split("/").slice(1).join("/") : modelRef);
1657
- const thinking = resolveThinkingLevel(matched, configuredThinkingLevel);
1658
- const usedToolOverride = !!explicitModel.model;
1659
- const selectionReason = matched
1660
- ? appendThinkingLevelClampReason(
1661
- usedToolOverride
1662
- ? "Used the explicit model override provided in the tool call."
1663
- : "Used the configured default oracle model.",
1664
- thinking,
1665
- )
1666
- : usedToolOverride
1667
- ? "Used the explicit model override provided in the tool call. The model was not matched against the authenticated model list, so the reasoning level fallback was applied."
1668
- : "Used the configured default oracle model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
1669
- pushAttempt({
1670
- modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
1671
- provider,
1672
- modelId,
1673
- modelName: matched?.name,
1674
- thinkingLevel: thinking.effective,
1675
- ...(matched && thinking.clamped
1676
- ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true }
1677
- : {}),
1678
- autoSelected: false,
1679
- selectionReason,
1680
- });
1681
- } else {
1682
- const selectionResult = await selectOracleModel(ctx, configuredThinkingLevel);
1683
- if (!selectionResult.ok) {
1684
- return finalizeResult({
1685
- content: [{ type: "text", text: selectionResult.error }],
1686
- details: {
1687
- modelRef: "",
1688
- provider: ctx.model?.provider ?? "unknown",
1689
- modelId: "",
1690
- modelName: undefined,
1691
- thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1692
- autoSelected: true,
1693
- selectionReason: selectionResult.error,
1694
- includeBash: params.includeBash ?? false,
1695
- usage: createEmptyUsage(),
1696
- stderr: "",
1697
- exitCode: 1,
1698
- durationMs: 0,
1699
- cwd: params.cwd ?? ctx.cwd,
1700
- },
1701
- });
1702
- }
1703
- for (const candidate of selectionResult.ordered) pushAttempt(candidate);
1704
- }
1705
-
1706
- // Keep the known-good session model as a final fallback: the catalog can
1707
- // advertise models the active provider/subscription cannot actually serve
1708
- // (legacy snapshots, access-gated tiers), which fail with a not-found/404
1709
- // error. Falling back lets the run degrade gracefully instead of hard-failing.
1710
- pushAttempt(buildSessionFallbackSelection(ctx, configuredThinkingLevel));
1711
-
1712
- const handleUpdate = (partial: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => {
1713
- uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
1714
- updateOracleUi(ctx, activeRuns);
1715
- onUpdate?.(partial);
1716
- };
1717
-
1718
- let lastErrorResult: { ok: false; error: string; details: OracleDetails } | undefined;
1719
- for (let index = 0; index < attempts.length; index++) {
1720
- const previous = index > 0 ? attempts[index - 1] : undefined;
1721
- const attempt = previous ? withFallbackReason(attempts[index], previous) : attempts[index];
1722
- uiRun.selection = attempt;
1723
- updateOracleUi(ctx, activeRuns);
1724
-
1725
- const result = await runOracle(attempt, { ...params, task }, signal, handleUpdate, ctx.cwd, spawnImpl);
1726
- mergeUsageStats(aggregateUsage, result.details.usage);
1727
- if (result.ok) {
1728
- return finalizeResult({
1729
- content: [{ type: "text", text: result.output }],
1730
- details: result.details,
1731
- });
1732
- }
1733
-
1734
- lastErrorResult = result;
1735
- const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1736
- if (!canFallBack) {
1737
- return finalizeResult({
1738
- content: [{ type: "text", text: result.error }],
1739
- details: result.details,
1740
- });
1741
- }
1742
- }
1743
-
1744
- return finalizeResult({
1745
- content: [
1746
- { type: "text", text: lastErrorResult?.error ?? "Oracle could not select an available model." },
1747
- ],
1748
- details:
1749
- lastErrorResult?.details ?? {
1750
- modelRef: "",
1751
- provider: ctx.model?.provider ?? "unknown",
1752
- modelId: "",
1753
- modelName: undefined,
1754
- thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1755
- autoSelected: true,
1756
- selectionReason: "No authenticated models were available to run the oracle.",
1757
- includeBash: params.includeBash ?? false,
1758
- usage: createEmptyUsage(),
1759
- stderr: "",
1760
- exitCode: 1,
1761
- durationMs: 0,
1762
- cwd: params.cwd ?? ctx.cwd,
1763
- },
1764
- });
1765
- } finally {
1766
- activeRuns.delete(toolCallId);
1767
- updateOracleUi(ctx, activeRuns);
1768
- }
1769
- },
1770
-
1771
- renderCall(args, theme) {
1772
- const task = args.task.length > 90 ? `${args.task.slice(0, 90)}...` : args.task;
1773
- const mode = args.includeBash ? "read-only+bash" : "read-only";
1774
- const override = args.model ? ` ${theme.fg("muted", `[${args.model}]`)}` : "";
1775
- const thinking = args.thinkingLevel ? ` ${theme.fg("warning", `(${args.thinkingLevel})`)}` : "";
1776
- return new Text(
1777
- `${theme.fg("toolTitle", theme.bold("oracle "))}${theme.fg("accent", mode)}${override}${thinking}\n ${theme.fg("dim", task)}`,
1778
- 0,
1779
- 0,
1780
- );
1781
- },
1782
-
1783
- renderResult(result, { expanded }, theme) {
1784
- const details = result.details as OracleDetails | undefined;
1785
- const body = result.content[0]?.type === "text" ? result.content[0].text : "(no output)";
1786
- if (!details) return new Text(body, 0, 0);
1787
-
1788
- const isError = (details.exitCode ?? 0) !== 0;
1789
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
1790
- const header = `${icon} ${theme.fg("toolTitle", theme.bold("oracle "))}${theme.fg("accent", details.modelRef || "(auto)")}`;
1791
- const subheader = [
1792
- details.thinkingLevel !== "off" ? details.thinkingLevel : undefined,
1793
- details.includeBash ? "read-only+bash" : "read-only",
1794
- formatDuration(details.durationMs),
1795
- ]
1796
- .filter(Boolean)
1797
- .join(" · ");
1798
- const usage = formatUsage(details.usage);
1799
-
1800
- if (!expanded) {
1801
- let text = header;
1802
- if (subheader) text += `\n${theme.fg("dim", subheader)}`;
1803
- text += `\n\n${theme.fg("toolOutput", renderCollapsedText(body))}`;
1804
- if (usage) text += `\n\n${theme.fg("dim", usage)}`;
1805
- if (isError && details.stderr) text += `\n${theme.fg("error", renderCollapsedText(details.stderr, 4))}`;
1806
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1807
- return new Text(text, 0, 0);
1808
- }
1809
-
1810
- const container = new Container();
1811
- container.addChild(new Text(header, 0, 0));
1812
- if (subheader) container.addChild(new Text(theme.fg("dim", subheader), 0, 0));
1813
- container.addChild(new Spacer(1));
1814
- container.addChild(new Text(theme.fg("muted", "Selection"), 0, 0));
1815
- container.addChild(new Text(theme.fg("dim", details.selectionReason), 0, 0));
1816
- container.addChild(new Spacer(1));
1817
- container.addChild(new Text(theme.fg("muted", "Output"), 0, 0));
1818
- container.addChild(new Markdown(body.trim(), 0, 0, getMarkdownTheme()));
1819
- if (usage) {
1820
- container.addChild(new Spacer(1));
1821
- container.addChild(new Text(theme.fg("muted", "Usage"), 0, 0));
1822
- container.addChild(new Text(theme.fg("dim", usage), 0, 0));
1823
- }
1824
- if (details.stderr) {
1825
- container.addChild(new Spacer(1));
1826
- container.addChild(new Text(theme.fg("muted", "stderr"), 0, 0));
1827
- container.addChild(
1828
- new Text(isError ? theme.fg("error", details.stderr) : theme.fg("dim", details.stderr), 0, 0),
1829
- );
1830
- }
1831
- return container;
1832
- },
1833
- });
1487
+ const spawnImpl = deps.spawnImpl ?? spawn;
1488
+ const activeRuns = new Map<string, OracleUiRun>();
1489
+ let preferences: OraclePreferences = {};
1490
+
1491
+ pi.on("session_start", async (_event, ctx) => {
1492
+ preferences = await readOraclePreferences();
1493
+ activeRuns.clear();
1494
+ updateOracleUi(ctx, activeRuns);
1495
+ });
1496
+
1497
+ pi.registerCommand("oracle", {
1498
+ description: "Configure Oracle default model and thinking level for future oracle tool calls",
1499
+ getArgumentCompletions: (prefix) => {
1500
+ const parts = prefix.trim().toLowerCase().split(/\s+/).filter(Boolean);
1501
+ const first = parts[0] ?? "";
1502
+ if (parts.length <= 1) {
1503
+ const commands = ["status", "model", "thinking", "clear"];
1504
+ const matches = commands.filter((command) => command.startsWith(first));
1505
+ return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
1506
+ }
1507
+ if (parts[0] === "thinking") {
1508
+ const query = parts[1] ?? "";
1509
+ const values = ["auto", ...THINKING_LEVELS];
1510
+ const matches = values.filter((value) => value.startsWith(query));
1511
+ return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
1512
+ }
1513
+ if (parts[0] === "clear") {
1514
+ const query = parts[1] ?? "";
1515
+ const values = ["all", "model", "thinking"];
1516
+ const matches = values.filter((value) => value.startsWith(query));
1517
+ return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
1518
+ }
1519
+ return null;
1520
+ },
1521
+ handler: async (args, ctx) => {
1522
+ const raw = args.trim();
1523
+ const tokens = raw ? raw.split(/\s+/) : [];
1524
+ const [command = "status", ...rest] = tokens;
1525
+ const action = command.toLowerCase();
1526
+ const configPath = getOracleConfigPath();
1527
+
1528
+ const save = async (next: OraclePreferences): Promise<string | undefined> => {
1529
+ preferences = next;
1530
+ try {
1531
+ await writeOraclePreferences(preferences);
1532
+ return undefined;
1533
+ } catch (error) {
1534
+ const message = error instanceof Error ? error.message : String(error);
1535
+ return `Preference changed for this process, but could not save ${configPath}: ${message}`;
1536
+ }
1537
+ };
1538
+
1539
+ if (action === "status" || action === "show") {
1540
+ notifyCommand(ctx, formatOraclePreferences(preferences));
1541
+ return;
1542
+ }
1543
+
1544
+ if (action === "model") {
1545
+ const model = rest.join(" ").trim();
1546
+ const normalizedModelAction = model.toLowerCase();
1547
+ if (!model) {
1548
+ notifyCommand(ctx, "Usage: /oracle model <provider/model|auto>", "warning");
1549
+ return;
1550
+ }
1551
+ if (
1552
+ normalizedModelAction === "auto" ||
1553
+ normalizedModelAction === "clear" ||
1554
+ normalizedModelAction === "default"
1555
+ ) {
1556
+ const warning = await save({ ...preferences, model: undefined });
1557
+ notifyCommand(
1558
+ ctx,
1559
+ `Oracle default model cleared; future oracle calls will auto-select. ${warning ?? formatOraclePreferences(preferences)}`,
1560
+ warning ? "warning" : "info",
1561
+ );
1562
+ return;
1563
+ }
1564
+ const parsedModel = parseModelPreference(model);
1565
+ const next = {
1566
+ ...preferences,
1567
+ model: parsedModel.model,
1568
+ thinkingLevel: parsedModel.thinkingLevel ?? preferences.thinkingLevel,
1569
+ };
1570
+ const warning = await save(next);
1571
+ notifyCommand(
1572
+ ctx,
1573
+ `Oracle default model set to ${parsedModel.model}. ${warning ?? formatOraclePreferences(preferences)}`,
1574
+ warning ? "warning" : "info",
1575
+ );
1576
+ return;
1577
+ }
1578
+
1579
+ if (action === "thinking" || action === "think" || action === "thinking-level") {
1580
+ const value = rest[0]?.trim().toLowerCase();
1581
+ if (!value) {
1582
+ notifyCommand(
1583
+ ctx,
1584
+ `Usage: /oracle thinking ${THINKING_LEVELS.join(" | ")} | auto`,
1585
+ "warning",
1586
+ );
1587
+ return;
1588
+ }
1589
+ if (value === "auto" || value === "clear" || value === "default") {
1590
+ const warning = await save({ ...preferences, thinkingLevel: undefined });
1591
+ notifyCommand(
1592
+ ctx,
1593
+ `Oracle default thinking level cleared; future oracle calls will use built-in defaults. ${warning ?? formatOraclePreferences(preferences)}`,
1594
+ warning ? "warning" : "info",
1595
+ );
1596
+ return;
1597
+ }
1598
+ const thinkingLevel = parseThinkingLevel(value);
1599
+ if (!thinkingLevel) {
1600
+ notifyCommand(
1601
+ ctx,
1602
+ `Usage: /oracle thinking ${THINKING_LEVELS.join(" | ")} | auto`,
1603
+ "warning",
1604
+ );
1605
+ return;
1606
+ }
1607
+ const warning = await save({ ...preferences, thinkingLevel });
1608
+ notifyCommand(
1609
+ ctx,
1610
+ `Oracle default thinking level set to ${thinkingLevel}. ${warning ?? formatOraclePreferences(preferences)}`,
1611
+ warning ? "warning" : "info",
1612
+ );
1613
+ return;
1614
+ }
1615
+
1616
+ if (action === "clear" || action === "reset") {
1617
+ const target = rest[0]?.trim().toLowerCase() || "all";
1618
+ let next: OraclePreferences;
1619
+ if (target === "all") next = {};
1620
+ else if (target === "model") next = { ...preferences, model: undefined };
1621
+ else if (target === "thinking" || target === "thinking-level")
1622
+ next = { ...preferences, thinkingLevel: undefined };
1623
+ else {
1624
+ notifyCommand(ctx, "Usage: /oracle clear [all|model|thinking]", "warning");
1625
+ return;
1626
+ }
1627
+ const warning = await save(next);
1628
+ notifyCommand(
1629
+ ctx,
1630
+ `Oracle defaults cleared (${target}). ${warning ?? formatOraclePreferences(preferences)}`,
1631
+ warning ? "warning" : "info",
1632
+ );
1633
+ return;
1634
+ }
1635
+
1636
+ notifyCommand(
1637
+ ctx,
1638
+ "Usage: /oracle status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|max|auto> | clear [all|model|thinking]",
1639
+ "warning",
1640
+ );
1641
+ },
1642
+ });
1643
+
1644
+ pi.registerCommand("oracle-model", {
1645
+ description: "Show which model the oracle would use right now",
1646
+ handler: async (_args, ctx) => {
1647
+ const defaultModel = parseModelPreference(preferences.model);
1648
+ if (defaultModel.model) {
1649
+ const matched = await findAvailableModel(ctx, defaultModel.model);
1650
+ const thinking = resolveThinkingLevel(
1651
+ matched,
1652
+ preferences.thinkingLevel ?? defaultModel.thinkingLevel,
1653
+ );
1654
+ const modelRef = matched ? `${matched.provider}/${matched.id}` : defaultModel.model;
1655
+ const suffix = matched
1656
+ ? appendThinkingLevelClampReason("Configured default oracle model is active.", thinking)
1657
+ : "Configured default oracle model is active, but it was not matched against the authenticated model list.";
1658
+ notifyCommand(ctx, `Oracle: ${modelRef} (${thinking.effective}) — ${suffix}`);
1659
+ return;
1660
+ }
1661
+ const selectionResult = await selectOracleModel(ctx, preferences.thinkingLevel);
1662
+ if (!selectionResult.ok) {
1663
+ notifyCommand(ctx, selectionResult.error, "error");
1664
+ return;
1665
+ }
1666
+
1667
+ const { selection } = selectionResult;
1668
+ const message = `Oracle: ${selection.modelRef} (${selection.thinkingLevel}) — ${selection.selectionReason}`;
1669
+ notifyCommand(ctx, message);
1670
+ },
1671
+ });
1672
+
1673
+ pi.on("tool_result", async (event) => {
1674
+ if (event.toolName !== "oracle") return undefined;
1675
+ const details = event.details as OracleDetails | undefined;
1676
+ if ((details?.exitCode ?? 0) === 0) return undefined;
1677
+ return { isError: true };
1678
+ });
1679
+
1680
+ pi.registerTool({
1681
+ name: "oracle",
1682
+ label: "Oracle",
1683
+ description:
1684
+ "Consult a separate read-only oracle subprocess for deep analysis, code review, debugging, planning, and second opinions.",
1685
+ promptSnippet:
1686
+ "Consult a read-only oracle that auto-selects the strongest reasoning model on the current provider/subscription.",
1687
+ promptGuidelines: [
1688
+ "Use oracle sparingly when you want a second opinion, deeper analysis, code review, debugging help, or a higher-reasoning pass.",
1689
+ "Do not use oracle for routine low-value work; oracle is slower than the main agent.",
1690
+ "The oracle tool is read-only by default and only exposes read, grep, find, and ls unless oracle includeBash is enabled.",
1691
+ "Set oracle includeBash only when the extra bash inspection tool is genuinely useful; keep oracle relying on read, grep, find, and ls otherwise.",
1692
+ "The oracle tool requests xhigh by default for reasoning models; oracle defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
1693
+ ],
1694
+ parameters: OracleParams,
1695
+
1696
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
1697
+ const rawTask = (params as { task?: unknown }).task;
1698
+ const task = typeof rawTask === "string" ? rawTask.trim() : "";
1699
+ if (!task) throw new Error("Invalid parameters: expected task to be a non-empty string.");
1700
+
1701
+ const explicitModel = parseModelPreference(params.model);
1702
+ const defaultModel = parseModelPreference(preferences.model);
1703
+ const configuredModel = explicitModel.model ?? defaultModel.model;
1704
+ const configuredThinkingLevel =
1705
+ params.thinkingLevel ??
1706
+ explicitModel.thinkingLevel ??
1707
+ preferences.thinkingLevel ??
1708
+ defaultModel.thinkingLevel;
1709
+ const uiRun: OracleUiRun = {
1710
+ task,
1711
+ includeBash: params.includeBash ?? false,
1712
+ startedAt: Date.now(),
1713
+ };
1714
+ activeRuns.set(toolCallId, uiRun);
1715
+ updateOracleUi(ctx, activeRuns);
1716
+
1717
+ try {
1718
+ const attempts: OracleSelection[] = [];
1719
+ const seen = new Set<string>();
1720
+ const aggregateUsage = createEmptyUsage();
1721
+ const finalizeResult = (result: {
1722
+ content: Array<{ type: "text"; text: string }>;
1723
+ details: OracleDetails;
1724
+ }): AgentToolResult<OracleDetails> => ({
1725
+ ...result,
1726
+ details: { ...result.details, usage: cloneUsageStats(aggregateUsage) },
1727
+ usage: usageStatsToToolUsage(aggregateUsage),
1728
+ });
1729
+ const pushAttempt = (candidate: OracleSelection | undefined) => {
1730
+ if (!candidate) return;
1731
+ const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
1732
+ if (seen.has(key)) return;
1733
+ seen.add(key);
1734
+ attempts.push(candidate);
1735
+ };
1736
+
1737
+ if (configuredModel) {
1738
+ const modelRef = configuredModel;
1739
+ const matched = await findAvailableModel(ctx, modelRef);
1740
+ const provider =
1741
+ matched?.provider ??
1742
+ (modelRef.includes("/") ? modelRef.split("/")[0] : (ctx.model?.provider ?? "unknown"));
1743
+ const modelId =
1744
+ matched?.id ??
1745
+ (modelRef.includes("/") ? modelRef.split("/").slice(1).join("/") : modelRef);
1746
+ const thinking = resolveThinkingLevel(matched, configuredThinkingLevel);
1747
+ const usedToolOverride = !!explicitModel.model;
1748
+ const selectionReason = matched
1749
+ ? appendThinkingLevelClampReason(
1750
+ usedToolOverride
1751
+ ? "Used the explicit model override provided in the tool call."
1752
+ : "Used the configured default oracle model.",
1753
+ thinking,
1754
+ )
1755
+ : usedToolOverride
1756
+ ? "Used the explicit model override provided in the tool call. The model was not matched against the authenticated model list, so the reasoning level fallback was applied."
1757
+ : "Used the configured default oracle model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
1758
+ pushAttempt({
1759
+ modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
1760
+ provider,
1761
+ modelId,
1762
+ modelName: matched?.name,
1763
+ thinkingLevel: thinking.effective,
1764
+ ...(matched && thinking.clamped
1765
+ ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true }
1766
+ : {}),
1767
+ autoSelected: false,
1768
+ selectionReason,
1769
+ });
1770
+ } else {
1771
+ const selectionResult = await selectOracleModel(ctx, configuredThinkingLevel);
1772
+ if (!selectionResult.ok) {
1773
+ return finalizeResult({
1774
+ content: [{ type: "text", text: selectionResult.error }],
1775
+ details: {
1776
+ modelRef: "",
1777
+ provider: ctx.model?.provider ?? "unknown",
1778
+ modelId: "",
1779
+ modelName: undefined,
1780
+ thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1781
+ autoSelected: true,
1782
+ selectionReason: selectionResult.error,
1783
+ includeBash: params.includeBash ?? false,
1784
+ usage: createEmptyUsage(),
1785
+ stderr: "",
1786
+ exitCode: 1,
1787
+ durationMs: 0,
1788
+ cwd: params.cwd ?? ctx.cwd,
1789
+ },
1790
+ });
1791
+ }
1792
+ for (const candidate of selectionResult.ordered) pushAttempt(candidate);
1793
+ }
1794
+
1795
+ pushAttempt(buildSessionFallbackSelection(ctx, configuredThinkingLevel));
1796
+
1797
+ const handleUpdate = (partial: {
1798
+ content: Array<{ type: "text"; text: string }>;
1799
+ details: OracleDetails;
1800
+ }) => {
1801
+ uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
1802
+ updateOracleUi(ctx, activeRuns);
1803
+ onUpdate?.(partial);
1804
+ };
1805
+
1806
+ let lastErrorResult: { ok: false; error: string; details: OracleDetails } | undefined;
1807
+ for (let index = 0; index < attempts.length; index++) {
1808
+ const previous = index > 0 ? attempts[index - 1] : undefined;
1809
+ const attempt = previous
1810
+ ? withFallbackReason(attempts[index], previous)
1811
+ : attempts[index];
1812
+ uiRun.selection = attempt;
1813
+ updateOracleUi(ctx, activeRuns);
1814
+
1815
+ const result = await runOracle(
1816
+ attempt,
1817
+ { ...params, task },
1818
+ signal,
1819
+ handleUpdate,
1820
+ ctx.cwd,
1821
+ spawnImpl,
1822
+ );
1823
+ mergeUsageStats(aggregateUsage, result.details.usage);
1824
+ if (result.ok) {
1825
+ return finalizeResult({
1826
+ content: [{ type: "text", text: result.output }],
1827
+ details: result.details,
1828
+ });
1829
+ }
1830
+
1831
+ lastErrorResult = result;
1832
+ const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1833
+ if (!canFallBack) {
1834
+ return finalizeResult({
1835
+ content: [{ type: "text", text: result.error }],
1836
+ details: result.details,
1837
+ });
1838
+ }
1839
+ }
1840
+
1841
+ return finalizeResult({
1842
+ content: [
1843
+ {
1844
+ type: "text",
1845
+ text: lastErrorResult?.error ?? "Oracle could not select an available model.",
1846
+ },
1847
+ ],
1848
+ details: lastErrorResult?.details ?? {
1849
+ modelRef: "",
1850
+ provider: ctx.model?.provider ?? "unknown",
1851
+ modelId: "",
1852
+ modelName: undefined,
1853
+ thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1854
+ autoSelected: true,
1855
+ selectionReason: "No authenticated models were available to run the oracle.",
1856
+ includeBash: params.includeBash ?? false,
1857
+ usage: createEmptyUsage(),
1858
+ stderr: "",
1859
+ exitCode: 1,
1860
+ durationMs: 0,
1861
+ cwd: params.cwd ?? ctx.cwd,
1862
+ },
1863
+ });
1864
+ } finally {
1865
+ activeRuns.delete(toolCallId);
1866
+ updateOracleUi(ctx, activeRuns);
1867
+ }
1868
+ },
1869
+
1870
+ renderCall(args, theme) {
1871
+ const task = args.task.length > 90 ? `${args.task.slice(0, 90)}...` : args.task;
1872
+ const mode = args.includeBash ? "read-only+bash" : "read-only";
1873
+ const override = args.model ? ` ${theme.fg("muted", `[${args.model}]`)}` : "";
1874
+ const thinking = args.thinkingLevel
1875
+ ? ` ${theme.fg("warning", `(${args.thinkingLevel})`)}`
1876
+ : "";
1877
+ return new Text(
1878
+ `${theme.fg("toolTitle", theme.bold("oracle "))}${theme.fg("accent", mode)}${override}${thinking}\n ${theme.fg("dim", task)}`,
1879
+ 0,
1880
+ 0,
1881
+ );
1882
+ },
1883
+
1884
+ renderResult(result, { expanded }, theme) {
1885
+ const details = result.details as OracleDetails | undefined;
1886
+ const body = result.content[0]?.type === "text" ? result.content[0].text : "(no output)";
1887
+ if (!details) return new Text(body, 0, 0);
1888
+
1889
+ const isError = (details.exitCode ?? 0) !== 0;
1890
+ const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
1891
+ const header = `${icon} ${theme.fg("toolTitle", theme.bold("oracle "))}${theme.fg("accent", details.modelRef || "(auto)")}`;
1892
+ const subheader = [
1893
+ details.thinkingLevel !== "off" ? details.thinkingLevel : undefined,
1894
+ details.includeBash ? "read-only+bash" : "read-only",
1895
+ formatDuration(details.durationMs),
1896
+ ]
1897
+ .filter(Boolean)
1898
+ .join(" · ");
1899
+ const usage = formatUsage(details.usage);
1900
+
1901
+ if (!expanded) {
1902
+ let text = header;
1903
+ if (subheader) text += `\n${theme.fg("dim", subheader)}`;
1904
+ text += `\n\n${theme.fg("toolOutput", renderCollapsedText(body))}`;
1905
+ if (usage) text += `\n\n${theme.fg("dim", usage)}`;
1906
+ if (isError && details.stderr)
1907
+ text += `\n${theme.fg("error", renderCollapsedText(details.stderr, 4))}`;
1908
+ text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1909
+ return new Text(text, 0, 0);
1910
+ }
1911
+
1912
+ const container = new Container();
1913
+ container.addChild(new Text(header, 0, 0));
1914
+ if (subheader) container.addChild(new Text(theme.fg("dim", subheader), 0, 0));
1915
+ container.addChild(new Spacer(1));
1916
+ container.addChild(new Text(theme.fg("muted", "Selection"), 0, 0));
1917
+ container.addChild(new Text(theme.fg("dim", details.selectionReason), 0, 0));
1918
+ container.addChild(new Spacer(1));
1919
+ container.addChild(new Text(theme.fg("muted", "Output"), 0, 0));
1920
+ container.addChild(new Markdown(body.trim(), 0, 0, getMarkdownTheme()));
1921
+ if (usage) {
1922
+ container.addChild(new Spacer(1));
1923
+ container.addChild(new Text(theme.fg("muted", "Usage"), 0, 0));
1924
+ container.addChild(new Text(theme.fg("dim", usage), 0, 0));
1925
+ }
1926
+ if (details.stderr) {
1927
+ container.addChild(new Spacer(1));
1928
+ container.addChild(new Text(theme.fg("muted", "stderr"), 0, 0));
1929
+ container.addChild(
1930
+ new Text(
1931
+ isError ? theme.fg("error", details.stderr) : theme.fg("dim", details.stderr),
1932
+ 0,
1933
+ 0,
1934
+ ),
1935
+ );
1936
+ }
1937
+ return container;
1938
+ },
1939
+ });
1834
1940
  }