@diegopetrucci/pi-oracle 0.1.7 → 0.1.9

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 (3) hide show
  1. package/README.md +23 -6
  2. package/index.ts +299 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,7 +11,7 @@ It adds an `oracle` tool that spins up a separate read-only pi subprocess and se
11
11
  - creates an isolated read-only subprocess
12
12
  - auto-picks the strongest reasoning model on the current provider
13
13
  - uses provider-specific hardcoded rankings first, then a heuristic fallback
14
- - sets reasoning/thinking to `xhigh` by default for reasoning models
14
+ - requests `xhigh` by default for reasoning models, then clamps to the model-supported thinking level
15
15
  - defaults to `read,grep,find,ls`
16
16
  - can optionally allow non-mutating `bash` inspection
17
17
  - shows a live oracle status line and widget while the subprocess is running
@@ -27,7 +27,7 @@ By default, the extension:
27
27
  4. tries a provider-specific hardcoded priority list first
28
28
  5. falls back to a heuristic that favors stronger tiers like `opus`, `pro`, newer versions, and penalizes `mini`, `flash`, `haiku`, `spark`, etc.
29
29
 
30
- The hardcoded rankings now cover pi's built-in provider set, including OpenAI/Codex, Anthropic, Google variants, GitHub Copilot, Bedrock, Azure OpenAI Responses, Cloudflare, DeepSeek, Fireworks, Groq, Hugging Face, Kimi/Moonshot, MiniMax, Mistral, OpenCode, OpenRouter, Vercel AI Gateway, xAI, ZAI, and Cerebras.
30
+ The hardcoded rankings now cover pi's built-in provider set, including Together; see the provider matrix for the current provider-by-provider top picks.
31
31
 
32
32
  If no reasoning model exists on the current provider, it falls back to the best available model on that provider.
33
33
 
@@ -35,9 +35,10 @@ If no reasoning model exists on the current provider, it falls back to the best
35
35
 
36
36
  Yes — the extension explicitly sets the oracle reasoning level.
37
37
 
38
- - reasoning models default to `xhigh`
38
+ - reasoning models request `xhigh` by default, then use the Pi-compatible effective thinking level supported by the matched model
39
39
  - non-reasoning models default to `off`
40
- - you can override it with the tool's optional `thinkingLevel` parameter
40
+ - you can override it with the tool's optional `thinkingLevel` parameter; matched models still clamp unsupported overrides and report the effective level
41
+ - you can persist a default thinking level with `/oracle thinking <level>` so future automatic oracle tool calls use it when the agent does not pass a per-call override
41
42
 
42
43
  Use `/oracle-model` inside pi to see what it would pick right now.
43
44
 
@@ -81,12 +82,28 @@ Ask pi normally, for example:
81
82
 
82
83
  The main agent can call the tool directly.
83
84
 
85
+ ## User defaults
86
+
87
+ Use `/oracle` to set persisted defaults that apply to future oracle tool calls, including calls the agent launches automatically without per-call overrides.
88
+
89
+ ```text
90
+ /oracle status
91
+ /oracle model anthropic/claude-opus-4-5
92
+ /oracle thinking high
93
+ /oracle thinking auto
94
+ /oracle clear model
95
+ /oracle clear thinking
96
+ /oracle clear
97
+ ```
98
+
99
+ Tool-call parameters still win over these defaults. `auto` clears the configured default and restores the built-in selection behavior. Preferences are saved under pi's agent directory in `extensions/oracle.json`.
100
+
84
101
  ## Tool parameters
85
102
 
86
103
  - `task` - required prompt for the oracle
87
104
  - `includeBash` - optional, adds `bash` for non-mutating inspection
88
- - `model` - optional explicit model override
89
- - `thinkingLevel` - optional reasoning/thinking override
105
+ - `model` - optional explicit model override; falls back to the `/oracle model` default, then auto-selection
106
+ - `thinkingLevel` - optional reasoning/thinking override; falls back to the `/oracle thinking` default, then built-in defaults
90
107
  - `cwd` - optional working directory override
91
108
 
92
109
  ## Notes
package/index.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { basename } from "node:path";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
4
5
  import { StringEnum } from "@earendil-works/pi-ai";
5
- import { getMarkdownTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { getAgentDir, getMarkdownTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
7
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
7
8
  import { Type } from "typebox";
8
9
 
9
10
  type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
11
+ type ThinkingLevelMap = Partial<Record<ThinkingLevel, unknown | null>>;
10
12
 
11
13
  type PiModel = {
12
14
  provider: string;
@@ -15,6 +17,7 @@ type PiModel = {
15
17
  reasoning?: boolean;
16
18
  contextWindow?: number;
17
19
  maxTokens?: number;
20
+ thinkingLevelMap?: ThinkingLevelMap;
18
21
  };
19
22
 
20
23
  interface UsageStats {
@@ -33,6 +36,8 @@ interface OracleSelection {
33
36
  modelId: string;
34
37
  modelName?: string;
35
38
  thinkingLevel: ThinkingLevel;
39
+ requestedThinkingLevel?: ThinkingLevel;
40
+ thinkingLevelClamped?: boolean;
36
41
  autoSelected: boolean;
37
42
  selectionReason: string;
38
43
  }
@@ -54,6 +59,11 @@ interface OracleUiRun {
54
59
  preview?: string;
55
60
  }
56
61
 
62
+ interface OraclePreferences {
63
+ model?: string;
64
+ thinkingLevel?: ThinkingLevel;
65
+ }
66
+
57
67
  const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
58
68
  const READ_ONLY_PLUS_BASH_TOOLS = [...READ_ONLY_TOOLS, "bash"];
59
69
  const DEFAULT_THINKING_LEVEL: ThinkingLevel = "xhigh";
@@ -61,6 +71,7 @@ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as
61
71
  const COLLAPSED_LINE_LIMIT = 8;
62
72
  const ORACLE_STATUS_ID = "oracle";
63
73
  const ORACLE_WIDGET_ID = "oracle";
74
+ const ORACLE_CONFIG_FILE = "oracle.json";
64
75
 
65
76
  const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
66
77
  "amazon-bedrock": [
@@ -294,6 +305,20 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
294
305
  "minimax/minimax-m2.1",
295
306
  "z-ai/glm-5.1",
296
307
  ],
308
+ together: [
309
+ "deepseek-ai/DeepSeek-V4-Pro",
310
+ "zai-org/GLM-5.1",
311
+ "moonshotai/Kimi-K2.6",
312
+ "Qwen/Qwen3.6-Plus",
313
+ "MiniMaxAI/MiniMax-M2.7",
314
+ "Qwen/Qwen3.5-397B-A17B",
315
+ "Qwen/Qwen3-Coder-Next-FP8",
316
+ "Qwen/Qwen3-235B-A22B-Instruct-2507-tput",
317
+ "openai/gpt-oss-120b",
318
+ "moonshotai/Kimi-K2.5",
319
+ "deepseek-ai/DeepSeek-V3-1",
320
+ "MiniMaxAI/MiniMax-M2.5",
321
+ ],
297
322
  "vercel-ai-gateway": [
298
323
  "anthropic/claude-opus-4.7",
299
324
  "anthropic/claude-opus-4.6",
@@ -393,7 +418,7 @@ const OracleParams = Type.Object({
393
418
  thinkingLevel: Type.Optional(
394
419
  StringEnum(THINKING_LEVELS, {
395
420
  description:
396
- "Optional reasoning level override for the oracle subprocess. Default: xhigh for reasoning models, off for non-reasoning models.",
421
+ "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.",
397
422
  }),
398
423
  ),
399
424
  cwd: Type.Optional(Type.String({ description: "Optional working directory for the oracle subprocess." })),
@@ -411,6 +436,74 @@ function createEmptyUsage(): UsageStats {
411
436
  };
412
437
  }
413
438
 
439
+ function getOracleConfigPath(): string {
440
+ return path.join(getAgentDir(), "extensions", ORACLE_CONFIG_FILE);
441
+ }
442
+
443
+ function parseThinkingLevel(value: unknown): ThinkingLevel | undefined {
444
+ if (typeof value !== "string") return undefined;
445
+ const normalized = value.trim().toLowerCase();
446
+ return (THINKING_LEVELS as readonly string[]).includes(normalized) ? (normalized as ThinkingLevel) : undefined;
447
+ }
448
+
449
+ function normalizeModelPreference(value: unknown): string | undefined {
450
+ if (typeof value !== "string") return undefined;
451
+ const trimmed = value.trim();
452
+ return trimmed ? trimmed : undefined;
453
+ }
454
+
455
+ function parseModelPreference(value: unknown): { model?: string; thinkingLevel?: ThinkingLevel } {
456
+ const model = normalizeModelPreference(value);
457
+ if (!model) return {};
458
+ const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i);
459
+ if (!match?.[1]) return { model };
460
+ return { model: match[1], thinkingLevel: parseThinkingLevel(match[2]) };
461
+ }
462
+
463
+ async function readOraclePreferences(): Promise<OraclePreferences> {
464
+ try {
465
+ const raw = await fs.readFile(getOracleConfigPath(), "utf8");
466
+ const parsed = JSON.parse(raw) as {
467
+ model?: unknown;
468
+ defaultModel?: unknown;
469
+ thinkingLevel?: unknown;
470
+ defaultThinkingLevel?: unknown;
471
+ };
472
+ return {
473
+ model: parseModelPreference(parsed.model).model ?? parseModelPreference(parsed.defaultModel).model,
474
+ thinkingLevel:
475
+ parseThinkingLevel(parsed.thinkingLevel) ??
476
+ parseThinkingLevel(parsed.defaultThinkingLevel) ??
477
+ parseModelPreference(parsed.model).thinkingLevel ??
478
+ parseModelPreference(parsed.defaultModel).thinkingLevel,
479
+ };
480
+ } catch {
481
+ return {};
482
+ }
483
+ }
484
+
485
+ async function writeOraclePreferences(preferences: OraclePreferences): Promise<void> {
486
+ const configPath = getOracleConfigPath();
487
+ const config = {
488
+ ...(preferences.model ? { model: preferences.model } : {}),
489
+ ...(preferences.thinkingLevel ? { thinkingLevel: preferences.thinkingLevel } : {}),
490
+ updatedAt: new Date().toISOString(),
491
+ };
492
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
493
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
494
+ }
495
+
496
+ function formatOraclePreferences(preferences: OraclePreferences): string {
497
+ const model = preferences.model ?? "auto";
498
+ const thinkingLevel = preferences.thinkingLevel ?? "auto";
499
+ return `Oracle defaults: model=${model}, thinkingLevel=${thinkingLevel}. Config: ${getOracleConfigPath()}`;
500
+ }
501
+
502
+ function notifyCommand(ctx: any, message: string, kind = "info"): void {
503
+ if (ctx.hasUI && ctx.ui) ctx.ui.notify(message, kind);
504
+ else console.log(message);
505
+ }
506
+
414
507
  function formatTokens(count: number): string {
415
508
  if (count < 1000) return count.toString();
416
509
  if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
@@ -516,7 +609,7 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
516
609
  return { command: process.execPath, args: [currentScript, ...args] };
517
610
  }
518
611
 
519
- const execName = basename(process.execPath).toLowerCase();
612
+ const execName = path.basename(process.execPath).toLowerCase();
520
613
  const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
521
614
  if (!isGenericRuntime) {
522
615
  return { command: process.execPath, args };
@@ -530,9 +623,48 @@ function withThinking(modelRef: string, thinkingLevel: ThinkingLevel): string {
530
623
  return `${modelRef}:${thinkingLevel}`;
531
624
  }
532
625
 
533
- function resolveThinkingLevel(model: PiModel | undefined, override: ThinkingLevel | undefined): ThinkingLevel {
534
- if (override) return override;
535
- return model?.reasoning ? DEFAULT_THINKING_LEVEL : "off";
626
+ // Keep these local so the extension stays compatible with older pi peer installs that do not export clamp helpers.
627
+ function isThinkingLevelSupported(model: PiModel, level: ThinkingLevel): boolean {
628
+ if (!model.reasoning) return level === "off";
629
+
630
+ const map = model.thinkingLevelMap;
631
+ if (level === "xhigh") {
632
+ return !!map && Object.prototype.hasOwnProperty.call(map, "xhigh") && map.xhigh != null;
633
+ }
634
+ return map?.[level] !== null;
635
+ }
636
+
637
+ function clampThinkingLevel(model: PiModel, requested: ThinkingLevel): ThinkingLevel {
638
+ if (isThinkingLevelSupported(model, requested)) return requested;
639
+
640
+ const requestedIndex = THINKING_LEVELS.indexOf(requested);
641
+ for (let index = requestedIndex + 1; index < THINKING_LEVELS.length; index++) {
642
+ const level = THINKING_LEVELS[index];
643
+ if (isThinkingLevelSupported(model, level)) return level;
644
+ }
645
+ for (let index = requestedIndex - 1; index >= 0; index--) {
646
+ const level = THINKING_LEVELS[index];
647
+ if (isThinkingLevelSupported(model, level)) return level;
648
+ }
649
+
650
+ return "off";
651
+ }
652
+
653
+ function resolveThinkingLevel(
654
+ model: PiModel | undefined,
655
+ override: ThinkingLevel | undefined,
656
+ ): { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean } {
657
+ const requested = override ?? (model?.reasoning ? DEFAULT_THINKING_LEVEL : "off");
658
+ const effective = model ? clampThinkingLevel(model, requested) : requested;
659
+ return { requested, effective, clamped: effective !== requested };
660
+ }
661
+
662
+ function appendThinkingLevelClampReason(
663
+ reason: string,
664
+ resolution: { requested: ThinkingLevel; effective: ThinkingLevel; clamped: boolean },
665
+ ): string {
666
+ if (!resolution.clamped) return reason;
667
+ return `${reason} Requested thinking level ${resolution.requested} was clamped to ${resolution.effective} based on the matched model's capabilities.`;
536
668
  }
537
669
 
538
670
  async function findAvailableModel(
@@ -599,9 +731,11 @@ async function selectOracleModel(
599
731
 
600
732
  const preferred = selectPreferredModel(candidates, providerForPreferences);
601
733
  const winner = preferred ?? [...candidates].sort((a, b) => rankModel(b) - rankModel(a))[0];
602
- const selectionReason = preferred
603
- ? `Selected ${winner.id} via the hardcoded preference list for ${winner.provider}.`
604
- : reason;
734
+ const thinking = resolveThinkingLevel(winner, thinkingLevelOverride);
735
+ const selectionReason = appendThinkingLevelClampReason(
736
+ preferred ? `Selected ${winner.id} via the hardcoded preference list for ${winner.provider}.` : reason,
737
+ thinking,
738
+ );
605
739
 
606
740
  return {
607
741
  ok: true,
@@ -610,7 +744,8 @@ async function selectOracleModel(
610
744
  provider: winner.provider,
611
745
  modelId: winner.id,
612
746
  modelName: winner.name,
613
- thinkingLevel: resolveThinkingLevel(winner, thinkingLevelOverride),
747
+ thinkingLevel: thinking.effective,
748
+ ...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
614
749
  autoSelected: true,
615
750
  selectionReason,
616
751
  },
@@ -833,26 +968,147 @@ function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): st
833
968
 
834
969
  export default function oracleExtension(pi: ExtensionAPI) {
835
970
  const activeRuns = new Map<string, OracleUiRun>();
971
+ let preferences: OraclePreferences = {};
836
972
 
837
973
  pi.on("session_start", async (_event, ctx) => {
974
+ preferences = await readOraclePreferences();
838
975
  activeRuns.clear();
839
976
  updateOracleUi(ctx, activeRuns);
840
977
  });
841
978
 
979
+ pi.registerCommand("oracle", {
980
+ description: "Configure Oracle default model and thinking level for future oracle tool calls",
981
+ getArgumentCompletions: (prefix) => {
982
+ const parts = prefix.trim().toLowerCase().split(/\s+/).filter(Boolean);
983
+ const first = parts[0] ?? "";
984
+ if (parts.length <= 1) {
985
+ const commands = ["status", "model", "thinking", "clear"];
986
+ const matches = commands.filter((command) => command.startsWith(first));
987
+ return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
988
+ }
989
+ if (parts[0] === "thinking") {
990
+ const query = parts[1] ?? "";
991
+ const values = ["auto", ...THINKING_LEVELS];
992
+ const matches = values.filter((value) => value.startsWith(query));
993
+ return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
994
+ }
995
+ if (parts[0] === "clear") {
996
+ const query = parts[1] ?? "";
997
+ const values = ["all", "model", "thinking"];
998
+ const matches = values.filter((value) => value.startsWith(query));
999
+ return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
1000
+ }
1001
+ return null;
1002
+ },
1003
+ handler: async (args, ctx) => {
1004
+ const raw = args.trim();
1005
+ const tokens = raw ? raw.split(/\s+/) : [];
1006
+ const [command = "status", ...rest] = tokens;
1007
+ const action = command.toLowerCase();
1008
+ const configPath = getOracleConfigPath();
1009
+
1010
+ const save = async (next: OraclePreferences): Promise<string | undefined> => {
1011
+ preferences = next;
1012
+ try {
1013
+ await writeOraclePreferences(preferences);
1014
+ return undefined;
1015
+ } catch (error) {
1016
+ const message = error instanceof Error ? error.message : String(error);
1017
+ return `Preference changed for this process, but could not save ${configPath}: ${message}`;
1018
+ }
1019
+ };
1020
+
1021
+ if (action === "status" || action === "show") {
1022
+ notifyCommand(ctx, formatOraclePreferences(preferences));
1023
+ return;
1024
+ }
1025
+
1026
+ if (action === "model") {
1027
+ const model = rest.join(" ").trim();
1028
+ const normalizedModelAction = model.toLowerCase();
1029
+ if (!model) {
1030
+ notifyCommand(ctx, "Usage: /oracle model <provider/model|auto>", "warning");
1031
+ return;
1032
+ }
1033
+ if (normalizedModelAction === "auto" || normalizedModelAction === "clear" || normalizedModelAction === "default") {
1034
+ const warning = await save({ ...preferences, model: undefined });
1035
+ notifyCommand(ctx, `Oracle default model cleared; future oracle calls will auto-select. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1036
+ return;
1037
+ }
1038
+ const parsedModel = parseModelPreference(model);
1039
+ const next = {
1040
+ ...preferences,
1041
+ model: parsedModel.model,
1042
+ thinkingLevel: parsedModel.thinkingLevel ?? preferences.thinkingLevel,
1043
+ };
1044
+ const warning = await save(next);
1045
+ notifyCommand(ctx, `Oracle default model set to ${parsedModel.model}. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1046
+ return;
1047
+ }
1048
+
1049
+ if (action === "thinking" || action === "think" || action === "thinking-level") {
1050
+ const value = rest[0]?.trim().toLowerCase();
1051
+ if (!value) {
1052
+ notifyCommand(ctx, `Usage: /oracle thinking ${THINKING_LEVELS.join(" | ")} | auto`, "warning");
1053
+ return;
1054
+ }
1055
+ if (value === "auto" || value === "clear" || value === "default") {
1056
+ const warning = await save({ ...preferences, thinkingLevel: undefined });
1057
+ notifyCommand(ctx, `Oracle default thinking level cleared; future oracle calls will use built-in defaults. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1058
+ return;
1059
+ }
1060
+ const thinkingLevel = parseThinkingLevel(value);
1061
+ if (!thinkingLevel) {
1062
+ notifyCommand(ctx, `Usage: /oracle thinking ${THINKING_LEVELS.join(" | ")} | auto`, "warning");
1063
+ return;
1064
+ }
1065
+ const warning = await save({ ...preferences, thinkingLevel });
1066
+ notifyCommand(ctx, `Oracle default thinking level set to ${thinkingLevel}. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1067
+ return;
1068
+ }
1069
+
1070
+ if (action === "clear" || action === "reset") {
1071
+ const target = rest[0]?.trim().toLowerCase() || "all";
1072
+ let next: OraclePreferences;
1073
+ if (target === "all") next = {};
1074
+ else if (target === "model") next = { ...preferences, model: undefined };
1075
+ else if (target === "thinking" || target === "thinking-level") next = { ...preferences, thinkingLevel: undefined };
1076
+ else {
1077
+ notifyCommand(ctx, "Usage: /oracle clear [all|model|thinking]", "warning");
1078
+ return;
1079
+ }
1080
+ const warning = await save(next);
1081
+ notifyCommand(ctx, `Oracle defaults cleared (${target}). ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
1082
+ return;
1083
+ }
1084
+
1085
+ notifyCommand(ctx, "Usage: /oracle status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]", "warning");
1086
+ },
1087
+ });
1088
+
842
1089
  pi.registerCommand("oracle-model", {
843
1090
  description: "Show which model the oracle would use right now",
844
1091
  handler: async (_args, ctx) => {
845
- const selectionResult = await selectOracleModel(ctx);
1092
+ const defaultModel = parseModelPreference(preferences.model);
1093
+ if (defaultModel.model) {
1094
+ const matched = await findAvailableModel(ctx, defaultModel.model);
1095
+ const thinking = resolveThinkingLevel(matched, preferences.thinkingLevel ?? defaultModel.thinkingLevel);
1096
+ const modelRef = matched ? `${matched.provider}/${matched.id}` : defaultModel.model;
1097
+ const suffix = matched
1098
+ ? appendThinkingLevelClampReason("Configured default oracle model is active.", thinking)
1099
+ : "Configured default oracle model is active, but it was not matched against the authenticated model list.";
1100
+ notifyCommand(ctx, `Oracle: ${modelRef} (${thinking.effective}) — ${suffix}`);
1101
+ return;
1102
+ }
1103
+ const selectionResult = await selectOracleModel(ctx, preferences.thinkingLevel);
846
1104
  if (!selectionResult.ok) {
847
- if (ctx.hasUI) ctx.ui.notify(selectionResult.error, "error");
848
- else console.log(selectionResult.error);
1105
+ notifyCommand(ctx, selectionResult.error, "error");
849
1106
  return;
850
1107
  }
851
1108
 
852
1109
  const { selection } = selectionResult;
853
1110
  const message = `Oracle: ${selection.modelRef} (${selection.thinkingLevel}) — ${selection.selectionReason}`;
854
- if (ctx.hasUI) ctx.ui.notify(message, "info");
855
- else console.log(message);
1111
+ notifyCommand(ctx, message);
856
1112
  },
857
1113
  });
858
1114
 
@@ -867,11 +1123,16 @@ export default function oracleExtension(pi: ExtensionAPI) {
867
1123
  "Use this tool sparingly when you want a second opinion, deeper analysis, code review, debugging help, or a higher-reasoning pass.",
868
1124
  "Do not use it for routine low-value work; it is slower than the main agent.",
869
1125
  "The oracle is read-only by default. Set includeBash only when shell-based inspection is genuinely useful.",
870
- "The oracle sets thinking to xhigh by default for reasoning models, unless the tool call explicitly overrides thinkingLevel.",
1126
+ "The oracle requests xhigh by default for reasoning models; defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
871
1127
  ],
872
1128
  parameters: OracleParams,
873
1129
 
874
1130
  async execute(toolCallId, params, signal, onUpdate, ctx) {
1131
+ const explicitModel = parseModelPreference(params.model);
1132
+ const defaultModel = parseModelPreference(preferences.model);
1133
+ const configuredModel = explicitModel.model ?? defaultModel.model;
1134
+ const configuredThinkingLevel =
1135
+ params.thinkingLevel ?? explicitModel.thinkingLevel ?? preferences.thinkingLevel ?? defaultModel.thinkingLevel;
875
1136
  const uiRun: OracleUiRun = {
876
1137
  task: params.task,
877
1138
  includeBash: params.includeBash ?? false,
@@ -882,25 +1143,38 @@ export default function oracleExtension(pi: ExtensionAPI) {
882
1143
 
883
1144
  try {
884
1145
  let selection: OracleSelection;
885
- if (params.model?.trim()) {
886
- const modelRef = params.model.trim();
1146
+ if (configuredModel) {
1147
+ const modelRef = configuredModel;
887
1148
  const matched = await findAvailableModel(ctx, modelRef);
888
1149
  const provider =
889
1150
  matched?.provider ?? (modelRef.includes("/") ? modelRef.split("/")[0] : ctx.model?.provider ?? "unknown");
890
1151
  const modelId = matched?.id ?? (modelRef.includes("/") ? modelRef.split("/").slice(1).join("/") : modelRef);
1152
+ const thinking = resolveThinkingLevel(matched, configuredThinkingLevel);
1153
+ const usedToolOverride = !!explicitModel.model;
1154
+ const selectionReason = matched
1155
+ ? appendThinkingLevelClampReason(
1156
+ usedToolOverride
1157
+ ? "Used the explicit model override provided in the tool call."
1158
+ : "Used the configured default oracle model.",
1159
+ thinking,
1160
+ )
1161
+ : usedToolOverride
1162
+ ? "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."
1163
+ : "Used the configured default oracle model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
891
1164
  selection = {
892
1165
  modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
893
1166
  provider,
894
1167
  modelId,
895
1168
  modelName: matched?.name,
896
- thinkingLevel: resolveThinkingLevel(matched, params.thinkingLevel),
1169
+ thinkingLevel: thinking.effective,
1170
+ ...(matched && thinking.clamped
1171
+ ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true }
1172
+ : {}),
897
1173
  autoSelected: false,
898
- selectionReason: matched
899
- ? "Used the explicit model override provided in the tool call."
900
- : "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.",
1174
+ selectionReason,
901
1175
  };
902
1176
  } else {
903
- const selectionResult = await selectOracleModel(ctx, params.thinkingLevel);
1177
+ const selectionResult = await selectOracleModel(ctx, configuredThinkingLevel);
904
1178
  if (!selectionResult.ok) {
905
1179
  return {
906
1180
  content: [{ type: "text", text: selectionResult.error }],
@@ -909,7 +1183,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
909
1183
  provider: ctx.model?.provider ?? "unknown",
910
1184
  modelId: "",
911
1185
  modelName: undefined,
912
- thinkingLevel: params.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
1186
+ thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
913
1187
  autoSelected: true,
914
1188
  selectionReason: selectionResult.error,
915
1189
  includeBash: params.includeBash ?? false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
5
5
  "keywords": ["pi-package", "pi", "oracle", "reasoning", "subagent"],
6
6
  "license": "MIT",