@diegopetrucci/pi-extensions 0.1.74 → 0.1.78

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