@diegopetrucci/pi-oracle 0.1.15 → 0.1.19

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.
@@ -1 +1 @@
1
- 0.79.10
1
+ 0.80.6
package/index.ts CHANGED
@@ -7,7 +7,7 @@ import { getAgentDir, getMarkdownTheme, type ExtensionAPI, type ExtensionContext
7
7
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
8
8
  import { Type } from "typebox";
9
9
 
10
- type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
10
+ type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
11
11
  type ThinkingLevelMap = Partial<Record<ThinkingLevel, unknown | null>>;
12
12
 
13
13
  type PiModel = {
@@ -67,7 +67,7 @@ interface OraclePreferences {
67
67
  const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
68
68
  const READ_ONLY_PLUS_BASH_TOOLS = [...READ_ONLY_TOOLS, "bash"];
69
69
  const DEFAULT_THINKING_LEVEL: ThinkingLevel = "xhigh";
70
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
70
+ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
71
71
  const COLLAPSED_LINE_LIMIT = 8;
72
72
  const ORACLE_STATUS_ID = "oracle";
73
73
  const ORACLE_WIDGET_ID = "oracle";
@@ -82,6 +82,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
82
82
  "claude-opus-4-5",
83
83
  "claude-opus-4-1",
84
84
  "claude-opus-4",
85
+ "claude-sonnet-5",
85
86
  "claude-sonnet-4-6",
86
87
  "claude-sonnet-4-5",
87
88
  "claude-sonnet-4",
@@ -95,37 +96,32 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
95
96
  anthropic: [
96
97
  "claude-fable-5",
97
98
  "claude-opus-4-8",
98
- "claude-opus-4.8",
99
99
  "claude-opus-4-7",
100
- "claude-opus-4.7",
101
100
  "claude-opus-4-6",
102
- "claude-opus-4.6",
103
101
  "claude-opus-4-5",
104
- "claude-opus-4.5",
105
102
  "claude-opus-4-1",
106
- "claude-opus-4.1",
107
- "claude-opus-4-0",
108
- "claude-opus-4",
103
+ "claude-sonnet-5",
109
104
  "claude-sonnet-4-6",
110
- "claude-sonnet-4.6",
111
105
  "claude-sonnet-4-5",
112
- "claude-sonnet-4.5",
113
- "claude-sonnet-4-0",
114
- "claude-sonnet-4",
115
- "claude-3-7-sonnet",
116
106
  ],
117
107
  "ant-ling": ["Ling-2.6-1T", "Ling-2.6-flash"],
118
108
  "azure-openai-responses": [
109
+ "gpt-5.6-sol",
110
+ "gpt-5.6-terra",
111
+ "gpt-5.6-luna",
119
112
  "gpt-5.5-pro",
120
113
  "gpt-5.5",
121
114
  "gpt-5.4-pro",
122
115
  "gpt-5.4",
123
116
  "gpt-5.3-codex",
124
117
  "gpt-5-pro",
118
+ "gpt-5-chat-latest",
125
119
  "gpt-5.2-pro",
126
120
  "gpt-5.2",
127
121
  "gpt-5.2-codex",
128
122
  "gpt-5.1-codex-max",
123
+ "gpt-5.1-codex",
124
+ "gpt-5.1",
129
125
  "gpt-5.1-chat-latest",
130
126
  "o3-pro",
131
127
  "o3-deep-research",
@@ -133,7 +129,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
133
129
  "gpt-5.4-mini",
134
130
  "gpt-5-mini",
135
131
  ],
136
- cerebras: ["gpt-oss-120b", "zai-glm-4.7", "llama3.1-8b"],
132
+ cerebras: ["gpt-oss-120b", "zai-glm-4.7", "gemma-4-31b"],
137
133
  "cloudflare-ai-gateway": [
138
134
  "claude-fable-5",
139
135
  "claude-opus-4-7",
@@ -151,8 +147,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
151
147
  "@cf/zai-org/glm-5.2",
152
148
  "@cf/moonshotai/kimi-k2.6",
153
149
  "@cf/nvidia/nemotron-3-120b-a12b",
154
- "@cf/moonshotai/kimi-k2.5",
155
150
  "@cf/openai/gpt-oss-120b",
151
+ "@cf/qwen/qwen3-30b-a3b-fp8",
156
152
  "@cf/zai-org/glm-4.7-flash",
157
153
  ],
158
154
  deepseek: ["deepseek-v4-pro", "deepseek-v4-flash"],
@@ -161,13 +157,17 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
161
157
  "accounts/fireworks/models/kimi-k2p7-code",
162
158
  "accounts/fireworks/routers/kimi-k2p7-code-fast",
163
159
  "accounts/fireworks/models/glm-5p2",
160
+ "accounts/fireworks/routers/glm-5p2-fast",
164
161
  "accounts/fireworks/models/minimax-m3",
165
162
  "accounts/fireworks/models/glm-5p1",
163
+ "accounts/fireworks/routers/glm-5p1-fast",
166
164
  "accounts/fireworks/models/kimi-k2p6",
165
+ "accounts/fireworks/routers/kimi-k2p6-turbo",
166
+ "accounts/fireworks/routers/kimi-k2p6-fast",
167
167
  "accounts/fireworks/models/minimax-m2p7",
168
168
  "accounts/fireworks/models/qwen3p7-plus",
169
- "accounts/fireworks/models/qwen3p6-plus",
170
169
  "accounts/fireworks/models/gpt-oss-120b",
170
+ "accounts/fireworks/models/gpt-oss-20b",
171
171
  ],
172
172
  "github-copilot": [
173
173
  "claude-fable-5",
@@ -178,40 +178,46 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
178
178
  "gpt-5.5",
179
179
  "gpt-5.4",
180
180
  "gpt-5.3-codex",
181
+ "gpt-5.2-codex",
181
182
  "gpt-5.2",
182
- "gpt-5.1-codex-max",
183
- "gpt-5.1",
184
- "gpt-5",
185
183
  "gemini-3.1-pro-preview",
186
- "gemini-3-pro-preview",
184
+ "gemini-3.5-flash",
185
+ "gemini-3-flash-preview",
186
+ "claude-sonnet-5",
187
187
  "claude-sonnet-4.6",
188
188
  "claude-sonnet-4.5",
189
189
  "gemini-2.5-pro",
190
+ "gpt-5-mini",
191
+ "kimi-k2.7-code",
190
192
  ],
191
193
  google: [
192
194
  "gemini-3.1-pro-preview-customtools",
193
195
  "gemini-3.1-pro-preview",
194
196
  "gemini-3-pro-preview",
195
- "gemini-2.5-pro-preview",
197
+ "gemini-3.5-flash",
196
198
  "gemini-2.5-pro",
197
- "gemini-2.5-flash-preview",
198
- "gemini-2.5-flash-lite-preview",
199
+ "gemini-2.5-flash",
200
+ "gemini-3.1-flash-lite-preview",
201
+ "gemini-3.1-flash-lite",
199
202
  "gemini-2.5-flash-lite",
203
+ "gemini-2.0-flash",
200
204
  ],
201
205
  "google-vertex": [
202
206
  "gemini-3.1-pro-preview-customtools",
203
207
  "gemini-3.1-pro-preview",
204
- "gemini-3-pro-preview",
208
+ "gemini-3.5-flash",
209
+ "gemini-3-flash-preview",
205
210
  "gemini-2.5-pro",
211
+ "gemini-2.5-flash",
212
+ "gemini-3.1-flash-lite",
206
213
  "gemini-2.5-flash-lite",
207
- "gemini-2.0-flash-lite",
208
214
  ],
209
215
  groq: [
210
216
  "openai/gpt-oss-120b",
211
- "groq/compound-mini",
212
217
  "qwen/qwen3-32b",
213
- "moonshotai/kimi-k2-instruct",
214
- "meta-llama/llama-4-scout",
218
+ "meta-llama/llama-4-scout-17b-16e-instruct",
219
+ "llama-3.3-70b-versatile",
220
+ "openai/gpt-oss-20b",
215
221
  ],
216
222
  huggingface: [
217
223
  "zai-org/GLM-5.1",
@@ -241,16 +247,22 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
241
247
  "devstral-2512",
242
248
  ],
243
249
  openai: [
250
+ "gpt-5.6-sol",
251
+ "gpt-5.6-terra",
252
+ "gpt-5.6-luna",
244
253
  "gpt-5.5-pro",
245
254
  "gpt-5.5",
246
255
  "gpt-5.4-pro",
247
256
  "gpt-5.4",
248
257
  "gpt-5.3-codex",
249
258
  "gpt-5-pro",
259
+ "gpt-5-chat-latest",
250
260
  "gpt-5.2-pro",
251
261
  "gpt-5.2",
252
262
  "gpt-5.2-codex",
253
263
  "gpt-5.1-codex-max",
264
+ "gpt-5.1-codex",
265
+ "gpt-5.1",
254
266
  "gpt-5.1-chat-latest",
255
267
  "o3-pro",
256
268
  "o3-deep-research",
@@ -259,14 +271,13 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
259
271
  "gpt-5-mini",
260
272
  ],
261
273
  "openai-codex": [
274
+ "gpt-5.6-sol",
275
+ "gpt-5.6-terra",
276
+ "gpt-5.6-luna",
262
277
  "gpt-5.5",
263
278
  "gpt-5.4",
264
- "gpt-5.3-codex",
265
- "gpt-5.2",
266
- "gpt-5.1-codex-max",
267
279
  "gpt-5.4-mini",
268
- "gpt-5.1-codex-mini",
269
- "big-pickle",
280
+ "gpt-5.3-codex-spark",
270
281
  ],
271
282
  opencode: [
272
283
  "claude-fable-5",
@@ -281,15 +292,19 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
281
292
  "gpt-5.3-codex",
282
293
  "gpt-5.2-codex",
283
294
  "gpt-5.1-codex",
295
+ "grok-4.5",
296
+ "gemini-3.5-flash",
284
297
  "gemini-3.1-pro",
298
+ "glm-5.2",
285
299
  "glm-5.1",
286
300
  "glm-5",
301
+ "kimi-k2.7-code",
287
302
  "kimi-k2.6",
288
303
  "kimi-k2.5",
289
304
  "qwen3.6-plus",
290
305
  "qwen3.5-plus",
306
+ "minimax-m3",
291
307
  "minimax-m2.7",
292
- "minimax-m2.5-free",
293
308
  ],
294
309
  "opencode-go": [
295
310
  "deepseek-v4-pro",
@@ -299,12 +314,11 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
299
314
  "qwen3.7-plus",
300
315
  "qwen3.6-plus",
301
316
  "mimo-v2.5-pro",
302
- "mimo-v2-pro",
317
+ "mimo-v2.5",
303
318
  "minimax-m3",
304
319
  "minimax-m2.7",
305
320
  "kimi-k2.7-code",
306
321
  "kimi-k2.6",
307
- "kimi-k2.5",
308
322
  ],
309
323
  openrouter: [
310
324
  "anthropic/claude-fable-5",
@@ -312,20 +326,30 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
312
326
  "anthropic/claude-opus-4.8",
313
327
  "anthropic/claude-opus-4.8-fast",
314
328
  "anthropic/claude-opus-4.7",
315
- "anthropic/claude-opus-4.6-fast",
329
+ "anthropic/claude-opus-4.7-fast",
316
330
  "anthropic/claude-opus-4.6",
317
331
  "anthropic/claude-opus-4.5",
318
- "anthropic/claude-opus-4",
332
+ "anthropic/claude-opus-4.1",
333
+ "openai/gpt-5.6-sol-pro",
334
+ "openai/gpt-5.6-sol",
335
+ "openai/gpt-5.6-terra-pro",
336
+ "openai/gpt-5.6-terra",
337
+ "openai/gpt-5.6-luna-pro",
338
+ "openai/gpt-5.6-luna",
319
339
  "openai/gpt-5.5-pro",
320
340
  "openai/gpt-5.5",
321
- "openai/gpt-5.4-pro",
322
- "openai/gpt-5.4",
341
+ "~openai/gpt-latest",
323
342
  "google/gemini-3.1-pro-preview-customtools",
324
343
  "google/gemini-3.1-pro-preview",
344
+ "~google/gemini-pro-latest",
345
+ "google/gemini-3.5-flash",
325
346
  "google/gemini-2.5-pro",
326
347
  "moonshotai/kimi-k2.7-code",
348
+ "~moonshotai/kimi-latest",
327
349
  "moonshotai/kimi-k2.6",
328
350
  "moonshotai/kimi-k2-thinking",
351
+ "x-ai/grok-4.5",
352
+ "~x-ai/grok-latest",
329
353
  "deepseek/deepseek-v4-pro",
330
354
  "deepseek/deepseek-r1",
331
355
  "deepseek/deepseek-v3.2",
@@ -337,6 +361,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
337
361
  ],
338
362
  together: [
339
363
  "deepseek-ai/DeepSeek-V4-Pro",
364
+ "zai-org/GLM-5.2",
340
365
  "zai-org/GLM-5.1",
341
366
  "moonshotai/Kimi-K2.7-Code",
342
367
  "moonshotai/Kimi-K2.6",
@@ -345,12 +370,10 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
345
370
  "MiniMaxAI/MiniMax-M3",
346
371
  "MiniMaxAI/MiniMax-M2.7",
347
372
  "Qwen/Qwen3.5-397B-A17B",
348
- "Qwen/Qwen3-Coder-Next-FP8",
349
- "Qwen/Qwen3-235B-A22B-Instruct-2507-tput",
350
373
  "openai/gpt-oss-120b",
351
- "moonshotai/Kimi-K2.5",
352
- "deepseek-ai/DeepSeek-V3-1",
353
- "MiniMaxAI/MiniMax-M2.5",
374
+ "openai/gpt-oss-20b",
375
+ "nvidia/nemotron-3-ultra-550b-a55b",
376
+ "google/gemma-4-31B-it",
354
377
  ],
355
378
  "vercel-ai-gateway": [
356
379
  "anthropic/claude-fable-5",
@@ -359,11 +382,17 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
359
382
  "anthropic/claude-opus-4.6",
360
383
  "anthropic/claude-opus-4.5",
361
384
  "anthropic/claude-opus-4.1",
385
+ "anthropic/claude-sonnet-5",
362
386
  "anthropic/claude-sonnet-4.6",
387
+ "openai/gpt-5.6-sol",
388
+ "openai/gpt-5.6-terra",
389
+ "openai/gpt-5.6-luna",
363
390
  "openai/gpt-5.5-pro",
364
391
  "openai/gpt-5.5",
365
392
  "openai/gpt-5.4-pro",
366
393
  "openai/gpt-5.4",
394
+ "openai/gpt-5.3-codex",
395
+ "openai/gpt-5.2-codex",
367
396
  "openai/gpt-5.1-codex",
368
397
  "openai/gpt-5-codex",
369
398
  "moonshotai/kimi-k2.7-code",
@@ -374,52 +403,37 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
374
403
  "deepseek/deepseek-v3.2-thinking",
375
404
  "alibaba/qwen3.7-max",
376
405
  "alibaba/qwen3.7-plus",
406
+ "alibaba/qwen3.6-plus",
377
407
  "alibaba/qwen3.5-plus",
378
408
  "alibaba/qwen3-max-thinking",
379
409
  "google/gemini-3.1-pro-preview",
410
+ "google/gemini-3.5-flash",
380
411
  "google/gemini-3-flash",
381
- "xai/grok-4.3",
412
+ "xai/grok-4.5",
413
+ "xai/grok-4.20-reasoning",
382
414
  "minimax/minimax-m3",
383
415
  "zai/glm-5.2",
384
416
  "zai/glm-5.1",
385
417
  ],
386
418
  xai: [
387
- "grok-4.3",
419
+ "grok-4.5",
388
420
  "grok-4.20-0309-reasoning",
389
- "grok-4-1-fast",
390
- "grok-4-fast",
391
- "grok-4",
392
- "grok-3-mini-latest",
393
- "grok-3-mini-fast",
394
- "grok-3-latest",
421
+ "grok-4.20-0309-non-reasoning",
422
+ "grok-4.3",
423
+ "grok-build-0.1",
424
+ "grok-code-fast-1",
425
+ "grok-3-fast",
426
+ "grok-3",
395
427
  ],
396
428
  xiaomi: ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "mimo-v2-omni", "mimo-v2-flash"],
397
- "xiaomi-token-plan-ams": [
398
- "mimo-v2.5-pro",
399
- "mimo-v2.5",
400
- "mimo-v2-pro",
401
- "mimo-v2-omni",
402
- "mimo-v2-flash",
403
- ],
404
- "xiaomi-token-plan-cn": [
405
- "mimo-v2.5-pro",
406
- "mimo-v2.5",
407
- "mimo-v2-pro",
408
- "mimo-v2-omni",
409
- "mimo-v2-flash",
410
- ],
411
- "xiaomi-token-plan-sgp": [
412
- "mimo-v2.5-pro",
413
- "mimo-v2.5",
414
- "mimo-v2-pro",
415
- "mimo-v2-omni",
416
- "mimo-v2-flash",
417
- ],
429
+ "xiaomi-token-plan-ams": ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro"],
430
+ "xiaomi-token-plan-cn": ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro"],
431
+ "xiaomi-token-plan-sgp": ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro"],
418
432
  nvidia: [
419
433
  "nvidia/nemotron-3-ultra-550b-a55b",
420
434
  "nvidia/nemotron-3-super-120b-a12b",
421
435
  "moonshotai/kimi-k2.6",
422
- "z-ai/glm-5.1",
436
+ "z-ai/glm-5.2",
423
437
  "qwen/qwen3.5-122b-a10b",
424
438
  "openai/gpt-oss-120b",
425
439
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
@@ -430,11 +444,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
430
444
  "glm-5.1",
431
445
  "glm-5-turbo",
432
446
  "glm-5v-turbo",
433
- "glm-5",
434
447
  "glm-4.7",
435
- "glm-4.7-flash",
436
- "glm-4.6v",
437
- "glm-4.5v",
438
448
  "glm-4.5-air",
439
449
  ],
440
450
  "zai-coding-cn": [
@@ -516,7 +526,7 @@ function normalizeModelPreference(value: unknown): string | undefined {
516
526
  function parseModelPreference(value: unknown): { model?: string; thinkingLevel?: ThinkingLevel } {
517
527
  const model = normalizeModelPreference(value);
518
528
  if (!model) return {};
519
- const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i);
529
+ const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh|max)$/i);
520
530
  if (!match?.[1]) return { model };
521
531
  return { model: match[1], thinkingLevel: parseThinkingLevel(match[2]) };
522
532
  }
@@ -612,6 +622,19 @@ function isTransientErrorMessage(message: string): boolean {
612
622
  return TRANSIENT_ERROR_PATTERN.test(message);
613
623
  }
614
624
 
625
+ // A model the catalog advertises but the active provider/subscription cannot
626
+ // serve surfaces as a not-found/404-style error (legacy snapshots or
627
+ // access-gated tiers). These are NOT transient: the same model keeps failing,
628
+ // so callers should fall back to a different model instead of retrying it.
629
+ const MODEL_AVAILABILITY_ERROR_PATTERN =
630
+ /\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;
631
+
632
+ function isModelAvailabilityError(message: string | undefined): boolean {
633
+ if (!message) return false;
634
+ if (isTransientErrorMessage(message)) return false;
635
+ return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
636
+ }
637
+
615
638
  function formatOracleModelError(stopReason: "error" | "aborted", errorMessage: string | undefined): string {
616
639
  const trimmed = errorMessage?.trim();
617
640
  if (stopReason === "aborted") {
@@ -700,7 +723,7 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
700
723
  }
701
724
 
702
725
  function withThinking(modelRef: string, thinkingLevel: ThinkingLevel): string {
703
- if (/(?:^|\/)[^:]+:(off|minimal|low|medium|high|xhigh)$/i.test(modelRef)) return modelRef;
726
+ if (/(?:^|\/)[^:]+:(off|minimal|low|medium|high|xhigh|max)$/i.test(modelRef)) return modelRef;
704
727
  return `${modelRef}:${thinkingLevel}`;
705
728
  }
706
729
 
@@ -709,8 +732,8 @@ function isThinkingLevelSupported(model: PiModel, level: ThinkingLevel): boolean
709
732
  if (!model.reasoning) return level === "off";
710
733
 
711
734
  const map = model.thinkingLevelMap;
712
- if (level === "xhigh") {
713
- return !!map && Object.prototype.hasOwnProperty.call(map, "xhigh") && map.xhigh != null;
735
+ if (level === "xhigh" || level === "max") {
736
+ return !!map && Object.prototype.hasOwnProperty.call(map, level) && map[level] != null;
714
737
  }
715
738
  return map?.[level] !== null;
716
739
  }
@@ -731,11 +754,16 @@ function clampThinkingLevel(model: PiModel, requested: ThinkingLevel): ThinkingL
731
754
  return "off";
732
755
  }
733
756
 
757
+ function isGpt56SolModel(model: PiModel | undefined): boolean {
758
+ if (!model) return false;
759
+ return /\bgpt-5\.6-sol\b/i.test(`${model.id} ${model.name ?? ""}`);
760
+ }
761
+
734
762
  function resolveThinkingLevel(
735
763
  model: PiModel | undefined,
736
764
  override: ThinkingLevel | undefined,
737
765
  ): { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean } {
738
- const requested = override ?? (model?.reasoning ? DEFAULT_THINKING_LEVEL : "off");
766
+ const requested = override ?? (model?.reasoning ? (isGpt56SolModel(model) ? "high" : DEFAULT_THINKING_LEVEL) : "off");
739
767
  const effective = model ? clampThinkingLevel(model, requested) : requested;
740
768
  return { requested, effective, clamped: effective !== requested };
741
769
  }
@@ -776,10 +804,48 @@ async function findAvailableModel(
776
804
  return undefined;
777
805
  }
778
806
 
807
+ function toOracleSelection(
808
+ model: PiModel,
809
+ thinkingLevelOverride: ThinkingLevel | undefined,
810
+ reason: string,
811
+ ): OracleSelection {
812
+ const thinking = resolveThinkingLevel(model, thinkingLevelOverride);
813
+ return {
814
+ modelRef: `${model.provider}/${model.id}`,
815
+ provider: model.provider,
816
+ modelId: model.id,
817
+ modelName: model.name,
818
+ thinkingLevel: thinking.effective,
819
+ ...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
820
+ autoSelected: true,
821
+ selectionReason: appendThinkingLevelClampReason(reason, thinking),
822
+ };
823
+ }
824
+
825
+ function buildSessionFallbackSelection(
826
+ ctx: { model?: PiModel },
827
+ thinkingLevelOverride: ThinkingLevel | undefined,
828
+ ): OracleSelection | undefined {
829
+ const model = ctx.model;
830
+ if (!model) return undefined;
831
+ return toOracleSelection(
832
+ model,
833
+ thinkingLevelOverride,
834
+ `Used the current session model ${model.provider}/${model.id} as a final fallback.`,
835
+ );
836
+ }
837
+
838
+ function withFallbackReason(selection: OracleSelection, previous: OracleSelection): OracleSelection {
839
+ return {
840
+ ...selection,
841
+ selectionReason: `${selection.selectionReason} (Fell back from ${previous.modelRef} after a model-availability error.)`,
842
+ };
843
+ }
844
+
779
845
  async function selectOracleModel(
780
846
  ctx: { model?: PiModel; modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> } },
781
847
  thinkingLevelOverride?: ThinkingLevel,
782
- ): Promise<{ ok: true; selection: OracleSelection } | { ok: false; error: string }> {
848
+ ): Promise<{ ok: true; selection: OracleSelection; ordered: OracleSelection[] } | { ok: false; error: string }> {
783
849
  const available = await ctx.modelRegistry.getAvailable();
784
850
  if (available.length === 0) {
785
851
  return {
@@ -810,27 +876,23 @@ async function selectOracleModel(
810
876
  reason = "No reasoning models were available, so the top-ranked model across all available providers was used.";
811
877
  }
812
878
 
879
+ const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
813
880
  const preferred = selectPreferredModel(candidates, providerForPreferences);
814
- const winner = preferred ?? [...candidates].sort((a, b) => rankModel(b) - rankModel(a))[0];
815
- const thinking = resolveThinkingLevel(winner, thinkingLevelOverride);
816
- const selectionReason = appendThinkingLevelClampReason(
817
- preferred ? `Selected ${winner.id} via the hardcoded preference list for ${winner.provider}.` : reason,
818
- thinking,
881
+ const orderedModels = preferred ? [preferred, ...sorted.filter((model) => model !== preferred)] : sorted;
882
+
883
+ const ordered = orderedModels.map((model, index) =>
884
+ toOracleSelection(
885
+ model,
886
+ thinkingLevelOverride,
887
+ index === 0
888
+ ? preferred
889
+ ? `Selected ${model.id} via the hardcoded preference list for ${model.provider}.`
890
+ : reason
891
+ : `Auto-selected ${model.provider}/${model.id} as a lower-ranked fallback.`,
892
+ ),
819
893
  );
820
894
 
821
- return {
822
- ok: true,
823
- selection: {
824
- modelRef: `${winner.provider}/${winner.id}`,
825
- provider: winner.provider,
826
- modelId: winner.id,
827
- modelName: winner.name,
828
- thinkingLevel: thinking.effective,
829
- ...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
830
- autoSelected: true,
831
- selectionReason,
832
- },
833
- };
895
+ return { ok: true, selection: ordered[0], ordered };
834
896
  }
835
897
 
836
898
  function updateOracleUi(ctx: ExtensionContext, activeRuns: Map<string, OracleUiRun>): void {
@@ -1061,6 +1123,13 @@ function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): st
1061
1123
  return [...lines.slice(0, lineLimit), `... (${lines.length - lineLimit} more lines)`].join("\n");
1062
1124
  }
1063
1125
 
1126
+ export const __test__ = {
1127
+ findAvailableModel,
1128
+ parseModelPreference,
1129
+ resolveThinkingLevel,
1130
+ selectOracleModel,
1131
+ };
1132
+
1064
1133
  export default function oracleExtension(pi: ExtensionAPI) {
1065
1134
  const activeRuns = new Map<string, OracleUiRun>();
1066
1135
  let preferences: OraclePreferences = {};
@@ -1177,7 +1246,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
1177
1246
  return;
1178
1247
  }
1179
1248
 
1180
- notifyCommand(ctx, "Usage: /oracle status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]", "warning");
1249
+ notifyCommand(ctx, "Usage: /oracle status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|max|auto> | clear [all|model|thinking]", "warning");
1181
1250
  },
1182
1251
  });
1183
1252
 
@@ -1223,13 +1292,17 @@ export default function oracleExtension(pi: ExtensionAPI) {
1223
1292
  parameters: OracleParams,
1224
1293
 
1225
1294
  async execute(toolCallId, params, signal, onUpdate, ctx) {
1295
+ const rawTask = (params as { task?: unknown }).task;
1296
+ const task = typeof rawTask === "string" ? rawTask.trim() : "";
1297
+ if (!task) throw new Error("Invalid parameters: expected task to be a non-empty string.");
1298
+
1226
1299
  const explicitModel = parseModelPreference(params.model);
1227
1300
  const defaultModel = parseModelPreference(preferences.model);
1228
1301
  const configuredModel = explicitModel.model ?? defaultModel.model;
1229
1302
  const configuredThinkingLevel =
1230
1303
  params.thinkingLevel ?? explicitModel.thinkingLevel ?? preferences.thinkingLevel ?? defaultModel.thinkingLevel;
1231
1304
  const uiRun: OracleUiRun = {
1232
- task: params.task,
1305
+ task,
1233
1306
  includeBash: params.includeBash ?? false,
1234
1307
  startedAt: Date.now(),
1235
1308
  };
@@ -1237,7 +1310,16 @@ export default function oracleExtension(pi: ExtensionAPI) {
1237
1310
  updateOracleUi(ctx, activeRuns);
1238
1311
 
1239
1312
  try {
1240
- let selection: OracleSelection;
1313
+ const attempts: OracleSelection[] = [];
1314
+ const seen = new Set<string>();
1315
+ const pushAttempt = (candidate: OracleSelection | undefined) => {
1316
+ if (!candidate) return;
1317
+ const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
1318
+ if (seen.has(key)) return;
1319
+ seen.add(key);
1320
+ attempts.push(candidate);
1321
+ };
1322
+
1241
1323
  if (configuredModel) {
1242
1324
  const modelRef = configuredModel;
1243
1325
  const matched = await findAvailableModel(ctx, modelRef);
@@ -1256,7 +1338,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
1256
1338
  : usedToolOverride
1257
1339
  ? "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."
1258
1340
  : "Used the configured default oracle model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
1259
- selection = {
1341
+ pushAttempt({
1260
1342
  modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
1261
1343
  provider,
1262
1344
  modelId,
@@ -1267,7 +1349,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
1267
1349
  : {}),
1268
1350
  autoSelected: false,
1269
1351
  selectionReason,
1270
- };
1352
+ });
1271
1353
  } else {
1272
1354
  const selectionResult = await selectOracleModel(ctx, configuredThinkingLevel);
1273
1355
  if (!selectionResult.ok) {
@@ -1290,11 +1372,14 @@ export default function oracleExtension(pi: ExtensionAPI) {
1290
1372
  },
1291
1373
  };
1292
1374
  }
1293
- selection = selectionResult.selection;
1375
+ for (const candidate of selectionResult.ordered) pushAttempt(candidate);
1294
1376
  }
1295
1377
 
1296
- uiRun.selection = selection;
1297
- updateOracleUi(ctx, activeRuns);
1378
+ // Keep the known-good session model as a final fallback: the catalog can
1379
+ // advertise models the active provider/subscription cannot actually serve
1380
+ // (legacy snapshots, access-gated tiers), which fail with a not-found/404
1381
+ // error. Falling back lets the run degrade gracefully instead of hard-failing.
1382
+ pushAttempt(buildSessionFallbackSelection(ctx, configuredThinkingLevel));
1298
1383
 
1299
1384
  const handleUpdate = (partial: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => {
1300
1385
  uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
@@ -1302,17 +1387,51 @@ export default function oracleExtension(pi: ExtensionAPI) {
1302
1387
  onUpdate?.(partial);
1303
1388
  };
1304
1389
 
1305
- const result = await runOracle(selection, params, signal, handleUpdate, ctx.cwd);
1306
- if (!result.ok) {
1307
- return {
1308
- content: [{ type: "text", text: result.error }],
1309
- details: result.details,
1310
- };
1390
+ let lastErrorResult: { ok: false; error: string; details: OracleDetails } | undefined;
1391
+ for (let index = 0; index < attempts.length; index++) {
1392
+ const previous = index > 0 ? attempts[index - 1] : undefined;
1393
+ const attempt = previous ? withFallbackReason(attempts[index], previous) : attempts[index];
1394
+ uiRun.selection = attempt;
1395
+ updateOracleUi(ctx, activeRuns);
1396
+
1397
+ const result = await runOracle(attempt, { ...params, task }, signal, handleUpdate, ctx.cwd);
1398
+ if (result.ok) {
1399
+ return {
1400
+ content: [{ type: "text", text: result.output }],
1401
+ details: result.details,
1402
+ };
1403
+ }
1404
+
1405
+ lastErrorResult = result;
1406
+ const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1407
+ if (!canFallBack) {
1408
+ return {
1409
+ content: [{ type: "text", text: result.error }],
1410
+ details: result.details,
1411
+ };
1412
+ }
1311
1413
  }
1312
1414
 
1313
1415
  return {
1314
- content: [{ type: "text", text: result.output }],
1315
- details: result.details,
1416
+ content: [
1417
+ { type: "text", text: lastErrorResult?.error ?? "Oracle could not select an available model." },
1418
+ ],
1419
+ details:
1420
+ lastErrorResult?.details ?? {
1421
+ modelRef: "",
1422
+ provider: ctx.model?.provider ?? "unknown",
1423
+ modelId: "",
1424
+ modelName: undefined,
1425
+ thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1426
+ autoSelected: true,
1427
+ selectionReason: "No authenticated models were available to run the oracle.",
1428
+ includeBash: params.includeBash ?? false,
1429
+ usage: createEmptyUsage(),
1430
+ stderr: "",
1431
+ exitCode: 1,
1432
+ durationMs: 0,
1433
+ cwd: params.cwd ?? ctx.cwd,
1434
+ },
1316
1435
  };
1317
1436
  } finally {
1318
1437
  activeRuns.delete(toolCallId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.15",
3
+ "version": "0.1.19",
4
4
  "description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
5
5
  "keywords": [
6
6
  "pi-package",