@diegopetrucci/pi-extensions 0.1.67 → 0.1.69

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.
@@ -75,6 +75,54 @@ export interface MinimalSessionEntry {
75
75
  [key: string]: unknown;
76
76
  }
77
77
 
78
+ export interface MinimalProjectedSessionEntry {
79
+ sourceEntry: MinimalSessionEntry;
80
+ messages: MinimalMessage[];
81
+ }
82
+
83
+ export interface MinimalSessionProjection {
84
+ entries: MinimalProjectedSessionEntry[];
85
+ messages?: MinimalMessage[];
86
+ }
87
+
88
+ function isRecord(value: unknown): value is Record<string, unknown> {
89
+ return typeof value === "object" && value !== null;
90
+ }
91
+
92
+ function isMinimalMessage(value: unknown): value is MinimalMessage {
93
+ return isRecord(value) && typeof value.role === "string" && value.role.length > 0;
94
+ }
95
+
96
+ function isProjectionSourceEntry(value: unknown): value is MinimalSessionEntry {
97
+ return isRecord(value)
98
+ && typeof value.type === "string"
99
+ && value.type.length > 0
100
+ && typeof value.id === "string"
101
+ && value.id.length > 0;
102
+ }
103
+
104
+ export function normalizeSessionProjection(value: unknown): MinimalSessionProjection | undefined {
105
+ if (!isRecord(value) || !Array.isArray(value.entries)) return undefined;
106
+
107
+ const entries: MinimalProjectedSessionEntry[] = [];
108
+ for (const candidate of value.entries) {
109
+ if (!isRecord(candidate) || !isProjectionSourceEntry(candidate.sourceEntry) || !Array.isArray(candidate.messages)) return undefined;
110
+ if (!candidate.messages.every(isMinimalMessage)) return undefined;
111
+ entries.push({
112
+ sourceEntry: candidate.sourceEntry,
113
+ messages: candidate.messages,
114
+ });
115
+ }
116
+
117
+ if ("messages" in value && (!Array.isArray(value.messages) || !value.messages.every(isMinimalMessage))) return undefined;
118
+ const messages = Array.isArray(value.messages) ? value.messages : entries.flatMap((entry) => entry.messages);
119
+ return { entries, messages };
120
+ }
121
+
122
+ export function sessionProjectionToMessages(projection: MinimalSessionProjection): MinimalMessage[] {
123
+ return projection.messages ?? projection.entries.flatMap((entry) => entry.messages);
124
+ }
125
+
78
126
  // ============================================================================
79
127
  // Constants
80
128
  // ============================================================================
@@ -114,6 +162,22 @@ const DEFAULT_CACHED_PRICE_RATIO = 0.1;
114
162
  */
115
163
  const DEFAULT_BREAK_EVEN_THRESHOLD = 22;
116
164
 
165
+ /**
166
+ * Default mid_loop break-even threshold (pe-zy4s follow-up). The package's own
167
+ * representative-corpus benchmark (turn-START state definition — the same one
168
+ * the runtime `context` handler uses) favored rejecting mid_loop candidates
169
+ * at cachedPriceRatio r=0.1 (mid_loop-optimal T=1, total realized
170
+ * net benefit ~0.0). This supports a conservative default for the measured
171
+ * corpus, not a universal claim that mid_loop pruning cannot pay off.
172
+ * The reported 2026-09-08 byte-level capture (a mid_loop prune at T=22
173
+ * invalidating 81% of a warm request prefix) illustrates cache invalidation,
174
+ * not an inability to amortize it: enough subsequent calls can recoup a
175
+ * one-time bust. Idle keeps T=22; turn-start classification does not imply
176
+ * the provider cache is cold. Users who want the old behavior can override
177
+ * via gate.breakEvenThresholdByState.mid_loop.
178
+ */
179
+ const DEFAULT_MID_LOOP_BREAK_EVEN_THRESHOLD = 1;
180
+
117
181
  /** Tool names never pruned by default: orchestration/subagent-style tools and todo-like state. */
118
182
  const DEFAULT_PROTECTED_TOOL_NAMES = [
119
183
  "todo",
@@ -222,20 +286,19 @@ export type GateMode = "on" | "off" | "always-apply";
222
286
  * turn-START "idle" population (calls made at the start of a turn) carries
223
287
  * essentially ALL the realized net benefit (idle-optimal T=22, total realized
224
288
  * net benefit ~20,594 token-units), while "mid_loop" carries essentially NONE
225
- * (mid_loop-optimal T=1, total realized net benefit ~0.0 -- i.e. no mid_loop
226
- * candidate is ever worth accepting at r=0.1 under this definition). This is
289
+ * (mid_loop-optimal T=1, total realized net benefit ~0.0 on this corpus). This is
227
290
  * the OPPOSITE of what the pe-c5n9 evidence (measured under the old,
228
291
  * non-runtime-observable turn-END definition) suggested.
229
292
  *
230
293
  * Per the pe-zy4s decision rule (stricter idle default only if idle itself is
231
294
  * shown to be ~worthless): idle is NOT worthless here -- it is the ONLY state
232
- * carrying value at r=0.1 -- so no state-conditioned strictness is justified
233
- * in either direction. The per-state split is therefore kept at PARITY
234
- * (`breakEvenThresholdByState: { idle: DEFAULT_BREAK_EVEN_THRESHOLD, mid_loop:
235
- * DEFAULT_BREAK_EVEN_THRESHOLD }`), both config-overridable. A future ticket
236
- * could consider a stricter mid_loop default given this evidence, but that is
237
- * a new, unreviewed direction outside this ticket's authorized decision rule
238
- * and is left for a follow-up.
295
+ * carrying value at r=0.1 -- so the idle default stays at
296
+ * DEFAULT_BREAK_EVEN_THRESHOLD. The mid_loop default, by the same benchmark,
297
+ * is now DEFAULT_MID_LOOP_BREAK_EVEN_THRESHOLD (=1): the measured candidates
298
+ * did not justify a more permissive threshold at r=0.1. Mid_loop pruning can
299
+ * still amortize over enough subsequent calls; state alone does not establish
300
+ * cache warmth or profitability. Both defaults remain config-overridable via
301
+ * gate.breakEvenThresholdByState.
239
302
  */
240
303
  export type AgentState = "idle" | "mid_loop";
241
304
 
@@ -285,7 +348,7 @@ export function defaultConfig(): DynamicContextPruningConfig {
285
348
  mode: "on",
286
349
  cachedPriceRatio: DEFAULT_CACHED_PRICE_RATIO,
287
350
  breakEvenThreshold: DEFAULT_BREAK_EVEN_THRESHOLD,
288
- breakEvenThresholdByState: { idle: DEFAULT_BREAK_EVEN_THRESHOLD, mid_loop: DEFAULT_BREAK_EVEN_THRESHOLD },
351
+ breakEvenThresholdByState: { idle: DEFAULT_BREAK_EVEN_THRESHOLD, mid_loop: DEFAULT_MID_LOOP_BREAK_EVEN_THRESHOLD },
289
352
  },
290
353
  };
291
354
  }
@@ -684,13 +747,7 @@ export function findToolCallPairIndices(
684
747
  return pair;
685
748
  }
686
749
 
687
- /**
688
- * Fallback correlation for decisions that reference a session entry rather
689
- * than a toolCallId (reserved for future non-tool-call strategies). Zips
690
- * message-producing session entries against the current context projection
691
- * by position; entry/message counts are expected to match in the common
692
- * case, but this degrades gracefully otherwise.
693
- */
750
+ /** Map raw session-entry provenance to projected message positions when Pi exposes it. */
694
751
  function expectedRoleForEntry(entry: MinimalSessionEntry): string | undefined {
695
752
  if (entry.type === "message") return entry.message?.role;
696
753
  if (entry.type === "custom_message") return "custom";
@@ -699,15 +756,53 @@ function expectedRoleForEntry(entry: MinimalSessionEntry): string | undefined {
699
756
  return undefined;
700
757
  }
701
758
 
702
- export function buildEntryIdToMessageIndexMap(
759
+ function buildProjectedEntryIdToMessageIndexMap(
703
760
  entries: MinimalSessionEntry[],
704
761
  messages: MinimalMessage[],
762
+ projection: MinimalSessionProjection,
705
763
  ): Map<string, number> {
706
764
  const map = new Map<string, number>();
765
+ const entryIds = new Set(entries.map((entry) => entry.id));
766
+ let cursor = 0;
767
+
768
+ for (const projectedEntry of projection.entries) {
769
+ let firstMessageIndex: number | undefined;
770
+ for (const projectedMessage of projectedEntry.messages) {
771
+ let messageIndex = messages.indexOf(projectedMessage, cursor);
772
+ if (messageIndex < cursor) messageIndex = -1;
773
+
774
+ if (messageIndex < 0 && typeof projectedMessage.timestamp === "number") {
775
+ messageIndex = messages.findIndex(
776
+ (candidate, index) =>
777
+ index >= cursor && candidate.role === projectedMessage.role && candidate.timestamp === projectedMessage.timestamp,
778
+ );
779
+ }
780
+ if (messageIndex < 0) continue;
781
+ firstMessageIndex ??= messageIndex;
782
+ cursor = messageIndex + 1;
783
+ }
784
+
785
+ if (firstMessageIndex !== undefined && entryIds.has(projectedEntry.sourceEntry.id)) {
786
+ map.set(projectedEntry.sourceEntry.id, firstMessageIndex);
787
+ }
788
+ }
789
+ return map;
790
+ }
791
+
792
+ export function buildEntryIdToMessageIndexMap(
793
+ entries: MinimalSessionEntry[],
794
+ messages: MinimalMessage[],
795
+ projection?: MinimalSessionProjection,
796
+ ): Map<string, number> {
797
+ if (projection) return buildProjectedEntryIdToMessageIndexMap(entries, messages, projection);
798
+
799
+ // Older runtimes expose only raw branch entries; preserve their positional
800
+ // correlation and use role/timestamp only when the counts diverge.
707
801
  const producing = entries.filter((entry) =>
708
802
  entry.type === "message" || entry.type === "custom_message" || entry.type === "branch_summary" || entry.type === "compaction",
709
803
  );
710
804
  const length = Math.min(producing.length, messages.length);
805
+ const map = new Map<string, number>();
711
806
  for (let i = 0; i < length; i++) {
712
807
  const entry = producing[i];
713
808
  const expectedRole = expectedRoleForEntry(entry);
@@ -732,15 +827,21 @@ export function findMessageIndexByRoleAndTimestamp(
732
827
  return index >= 0 ? index : undefined;
733
828
  }
734
829
 
735
- /** Resolve a session entry id to a message index using order-zip, then role+timestamp fallback. */
830
+ /** Resolve a session entry id using projection provenance, then legacy role+timestamp fallback. */
736
831
  export function resolveMessageIndexForEntry(
737
832
  entryId: string,
738
833
  entries: MinimalSessionEntry[],
739
834
  messages: MinimalMessage[],
835
+ projection?: MinimalSessionProjection,
740
836
  ): number | undefined {
741
- const zipMap = buildEntryIdToMessageIndexMap(entries, messages);
837
+ const zipMap = buildEntryIdToMessageIndexMap(entries, messages, projection);
742
838
  const zipped = zipMap.get(entryId);
743
839
  if (zipped !== undefined) return zipped;
840
+
841
+ // An empty projected contribution is an explicit omission/replacement state;
842
+ // only a validated projection may make that context-edit-aware decision.
843
+ if (projection?.entries.some((entry) => entry.sourceEntry.id === entryId && entry.messages.length === 0)) return undefined;
844
+
744
845
  const entry = entries.find((candidate) => candidate.id === entryId);
745
846
  if (!entry) return undefined;
746
847
  return findMessageIndexByRoleAndTimestamp(entry, messages);
@@ -2157,8 +2258,22 @@ export function rebuildDecisionStateFromEntries(entries: MinimalSessionEntry[]):
2157
2258
  // /prune picker helpers (pe-8re9)
2158
2259
  // ============================================================================
2159
2260
 
2160
- /** Convert branch entries into the plain message array the pipeline/pickers correlate against. */
2161
- export function sessionEntriesToMessages(entries: MinimalSessionEntry[]): MinimalMessage[] {
2261
+ function resolveSessionProjection(ctx: ExtensionContext): MinimalSessionProjection | undefined {
2262
+ try {
2263
+ const sessionManager = ctx.sessionManager as unknown as { buildSessionProjection?: () => unknown };
2264
+ const buildProjection = sessionManager.buildSessionProjection;
2265
+ if (typeof buildProjection !== "function") return undefined;
2266
+ return normalizeSessionProjection(buildProjection.call(sessionManager));
2267
+ } catch {
2268
+ return undefined;
2269
+ }
2270
+ }
2271
+
2272
+ export function sessionEntriesToMessages(
2273
+ entries: MinimalSessionEntry[],
2274
+ projection?: MinimalSessionProjection,
2275
+ ): MinimalMessage[] {
2276
+ if (projection) return sessionProjectionToMessages(projection);
2162
2277
  const messages: MinimalMessage[] = [];
2163
2278
  for (const entry of entries) {
2164
2279
  if (entry.type === "message" && entry.message) messages.push(entry.message);
@@ -2273,7 +2388,7 @@ export function formatPrunableItemDetail(item: PrunableItem, cost?: CacheCostMod
2273
2388
 
2274
2389
  /** Plain-text report used both by the non-UI /prune fallback and as a debugging aid. */
2275
2390
  export function formatPrunableItemsReport(items: PrunableItem[]): string[] {
2276
- if (items.length === 0) return ["No prunable tool results found in this session."];
2391
+ if (items.length === 0) return ["No prunable tool results found in the current model context."];
2277
2392
  return items.map((item, index) => `${index + 1}. ${formatPrunableItemOption(item)}`);
2278
2393
  }
2279
2394
 
@@ -2410,7 +2525,7 @@ export function formatStatusReport(input: StatusReportInput): string[] {
2410
2525
  const lines: string[] = [];
2411
2526
  lines.push(`Dynamic context pruning: ${config.enabled ? "ENABLED" : "disabled"}`);
2412
2527
  lines.push(
2413
- `Gate: mode=${config.gate.mode} threshold=${config.gate.breakEvenThreshold} cachedPriceRatio=${config.gate.cachedPriceRatio}`,
2528
+ `Gate: mode=${config.gate.mode} thresholds={idle:${resolveBreakEvenThreshold(config.gate, "idle")}, mid_loop:${resolveBreakEvenThreshold(config.gate, "mid_loop")}} cachedPriceRatio=${config.gate.cachedPriceRatio}`,
2414
2529
  );
2415
2530
  lines.push("Strategies:");
2416
2531
  lines.push(` dedupe: ${config.strategies.dedupe.enabled ? "on" : "off"}`);
@@ -2581,7 +2696,8 @@ export default function dynamicContextPruningExtension(pi: ExtensionAPI) {
2581
2696
  description: "Review, manually prune, and restore prunable tool results (interactive picker in TUI/RPC)",
2582
2697
  handler: async (_args, ctx) => {
2583
2698
  const branchEntries = ctx.sessionManager.getBranch() as unknown as MinimalSessionEntry[];
2584
- const messages = sessionEntriesToMessages(branchEntries);
2699
+ const projection = resolveSessionProjection(ctx);
2700
+ const messages = sessionEntriesToMessages(branchEntries, projection);
2585
2701
  const activeByToolCallId = buildActiveResultDecisionMap(persistedDecisions);
2586
2702
  const restoredToolCallIds = new Set(
2587
2703
  Array.from(lastDecisionByKey.values())
@@ -2591,19 +2707,20 @@ export default function dynamicContextPruningExtension(pi: ExtensionAPI) {
2591
2707
  const items = buildPrunableItems(messages, activeByToolCallId, restoredToolCallIds);
2592
2708
 
2593
2709
  if (!ctx.hasUI) {
2594
- notify(ctx, ["/prune: no interactive UI available. Prunable tool results in this session:", "", ...formatPrunableItemsReport(items)].join("\n"), "info");
2710
+ notify(ctx, ["/prune: no interactive UI available. Prunable tool results in the current model context:", "", ...formatPrunableItemsReport(items)].join("\n"), "info");
2595
2711
  return;
2596
2712
  }
2597
2713
 
2598
2714
  if (items.length === 0) {
2599
- notify(ctx, "No prunable tool results found in this session.", "info");
2715
+ notify(ctx, "No prunable tool results found in the current model context.", "info");
2600
2716
  return;
2601
2717
  }
2602
2718
 
2603
2719
  const doneLabel = "Done";
2604
2720
  while (true) {
2605
2721
  const latestBranch = ctx.sessionManager.getBranch() as unknown as MinimalSessionEntry[];
2606
- const latestMessages = sessionEntriesToMessages(latestBranch);
2722
+ const latestProjection = resolveSessionProjection(ctx);
2723
+ const latestMessages = sessionEntriesToMessages(latestBranch, latestProjection);
2607
2724
  const latestActive = buildActiveResultDecisionMap(persistedDecisions);
2608
2725
  const latestRestoredToolCallIds = new Set(
2609
2726
  Array.from(lastDecisionByKey.values())
@@ -2612,7 +2729,7 @@ export default function dynamicContextPruningExtension(pi: ExtensionAPI) {
2612
2729
  );
2613
2730
  const latestItems = buildPrunableItems(latestMessages, latestActive, latestRestoredToolCallIds);
2614
2731
  if (latestItems.length === 0) {
2615
- notify(ctx, "No prunable tool results found in this session.", "info");
2732
+ notify(ctx, "No prunable tool results found in the current model context.", "info");
2616
2733
  return;
2617
2734
  }
2618
2735
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-dynamic-context-pruning",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "A pi extension that dynamically prunes stale context to keep sessions lean.",
6
6
  "keywords": [
@@ -107,6 +107,13 @@ const ORACLE_CONFIG_FILE = "oracle.json";
107
107
 
108
108
  const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
109
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",
110
117
  "claude-opus-5",
111
118
  "claude-fable-5",
112
119
  "claude-opus-4-8",
@@ -115,6 +122,10 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
115
122
  "claude-opus-4-5",
116
123
  "claude-opus-4-1",
117
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",
118
129
  "openai.gpt-5.6-sol",
119
130
  "openai.gpt-5.6-terra",
120
131
  "openai.gpt-5.6-luna",
@@ -123,7 +134,6 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
123
134
  "claude-sonnet-4-5",
124
135
  "claude-sonnet-4",
125
136
  "deepseek.v3.2",
126
- "deepseek.r1",
127
137
  "kimi-k2.5",
128
138
  "minimax-m2.5",
129
139
  "minimax-m2.1",
@@ -131,6 +141,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
131
141
  "xai.grok-4.6",
132
142
  ],
133
143
  anthropic: [
144
+ "claude-opus-5-5 ",
145
+ "claude-opus-5-5",
134
146
  "claude-opus-5",
135
147
  "claude-fable-5",
136
148
  "claude-opus-4-8",
@@ -143,8 +155,14 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
143
155
  ],
144
156
  "ant-ling": ["Ling-2.6-1T", "Ling-2.6-flash"],
145
157
  "azure-openai-responses": [
158
+ "gpt-6-astra ",
159
+ "gpt-6-astra",
160
+ "gpt-6-sol ",
161
+ "gpt-6-sol",
146
162
  "gpt-5.6-sol",
147
163
  "gpt-5.6-terra",
164
+ "gpt-6-luna ",
165
+ "gpt-6-luna",
148
166
  "gpt-5.6-luna",
149
167
  "gpt-5.5-pro",
150
168
  "gpt-5.5",
@@ -181,7 +199,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
181
199
  "zai-org/GLM-5.2-Fast",
182
200
  "zai-org/GLM-4.7",
183
201
  ],
184
- cerebras: ["gpt-oss-120b", "gemma-4-31b"],
202
+ cerebras: ["gpt-oss-120b", "qwen-3.8-27b"],
185
203
  "cloudflare-ai-gateway": [
186
204
  "claude-fable-5",
187
205
  "claude-opus-5",
@@ -189,6 +207,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
189
207
  "claude-opus-4.7",
190
208
  "claude-opus-4.6",
191
209
  "claude-opus-4.5",
210
+ "gpt-6-astra ",
211
+ "gpt-6-astra",
192
212
  "gpt-5.5",
193
213
  "gpt-5.4",
194
214
  "workers-ai/@cf/moonshotai/kimi-k2.7-code",
@@ -212,7 +232,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
212
232
  "@cf/qwen/qwen3-30b-a3b-fp8",
213
233
  "@cf/zai-org/glm-4.7-flash",
214
234
  ],
215
- deepseek: ["deepseek-v4-pro", "deepseek-v4-flash"],
235
+ deepseek: ["deepseek-v4-pro", "deepseek-flash"],
216
236
  fireworks: [
217
237
  "accounts/fireworks/models/kimi-k3",
218
238
  "accounts/fireworks/routers/kimi-k3-fast",
@@ -226,13 +246,20 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
226
246
  "accounts/fireworks/models/gpt-oss-120b",
227
247
  ],
228
248
  "github-copilot": [
249
+ "claude-opus-5.5 ",
250
+ "claude-opus-5.5",
229
251
  "claude-opus-5",
230
252
  "claude-fable-5",
231
253
  "claude-opus-4.8",
232
254
  "claude-opus-4.7",
255
+ "gpt-6-astra ",
233
256
  "gpt-6-astra",
257
+ "gpt-6-sol ",
258
+ "gpt-6-sol",
234
259
  "gpt-5.6-sol",
235
260
  "gpt-5.6-terra",
261
+ "gpt-6-luna ",
262
+ "gpt-6-luna",
236
263
  "gpt-5.6-luna",
237
264
  "gpt-5.5",
238
265
  "gpt-5.4",
@@ -241,6 +268,10 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
241
268
  "claude-sonnet-5",
242
269
  "claude-sonnet-4.6",
243
270
  "gpt-5-mini",
271
+ "grok-4.7 ",
272
+ "grok-4.7",
273
+ "grok-4.6",
274
+ "grok-4.5",
244
275
  "kimi-k3",
245
276
  "kimi-k2.7-code",
246
277
  ],
@@ -285,6 +316,13 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
285
316
  "Qwen/Qwen3-Coder-Next",
286
317
  ],
287
318
  "kimi-coding": ["k3", "kimi-for-coding", "kimi-for-coding-highspeed"],
319
+ meta: [
320
+ "muse-spark-1.3",
321
+ "muse-spark-1.3-contributor",
322
+ "muse-spark-1.2",
323
+ "muse-spark-1.2-contributor",
324
+ "muse-spark-1.1",
325
+ ],
288
326
  minimax: ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
289
327
  "minimax-cn": ["MiniMax-M3", "MiniMax-M2.7-highspeed", "MiniMax-M2.7"],
290
328
  mistral: [
@@ -301,9 +339,14 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
301
339
  "zai-glm-5-2",
302
340
  ],
303
341
  openai: [
342
+ "gpt-6-astra ",
304
343
  "gpt-6-astra",
344
+ "gpt-6-sol ",
345
+ "gpt-6-sol",
305
346
  "gpt-5.6-sol",
306
347
  "gpt-5.6-terra",
348
+ "gpt-6-luna ",
349
+ "gpt-6-luna",
307
350
  "gpt-5.6-luna",
308
351
  "gpt-5.5-pro",
309
352
  "gpt-5.5",
@@ -321,18 +364,32 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
321
364
  "gpt-5-mini",
322
365
  ],
323
366
  "openai-codex": [
367
+ "gpt-6-astra ",
324
368
  "gpt-6-astra",
369
+ "gpt-6-sol ",
370
+ "gpt-6-sol",
325
371
  "gpt-5.6-sol",
326
372
  "gpt-5.6-terra",
373
+ "gpt-6-luna ",
374
+ "gpt-6-luna",
327
375
  "gpt-5.6-luna",
328
376
  "gpt-5.5",
329
- "gpt-5.4",
330
- "gpt-5.4-mini",
331
377
  "gpt-5.3-codex-spark",
332
378
  ],
333
379
  opencode: [
380
+ "claude-opus-5-5 ",
381
+ "claude-opus-5-5",
334
382
  "claude-opus-5",
335
383
  "claude-fable-5",
384
+ "gpt-6-astra ",
385
+ "gpt-6-astra",
386
+ "gpt-6-sol ",
387
+ "gpt-6-sol",
388
+ "gpt-5.6-sol",
389
+ "gpt-5.6-terra",
390
+ "gpt-6-luna ",
391
+ "gpt-6-luna",
392
+ "gpt-5.6-luna",
336
393
  "gpt-5.5-pro",
337
394
  "gpt-5.5",
338
395
  "gpt-5.4-pro",
@@ -344,6 +401,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
344
401
  "gpt-5.3-codex",
345
402
  "gpt-5.2-codex",
346
403
  "gpt-5.1-codex",
404
+ "grok-4.6",
347
405
  "grok-4.5",
348
406
  "gemini-3.5-flash",
349
407
  "gemini-3.1-pro",
@@ -374,8 +432,13 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
374
432
  "kimi-k3",
375
433
  "kimi-k2.7-code",
376
434
  "kimi-k2.6",
435
+ "grok-4.7 ",
436
+ "grok-4.7",
437
+ "grok-4.6",
377
438
  ],
378
439
  openrouter: [
440
+ "anthropic/claude-opus-5.5 ",
441
+ "anthropic/claude-opus-5.5",
379
442
  "anthropic/claude-opus-5",
380
443
  "anthropic/claude-fable-5",
381
444
  "~anthropic/claude-fable-latest",
@@ -384,15 +447,21 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
384
447
  "anthropic/claude-opus-4.6",
385
448
  "anthropic/claude-opus-4.5",
386
449
  "anthropic/claude-opus-4.1",
450
+ "openai/gpt-6-astra ",
451
+ "openai/gpt-6-astra",
452
+ "openai/gpt-6-sol ",
453
+ "openai/gpt-6-sol",
387
454
  "openai/gpt-5.6-sol-pro",
388
455
  "openai/gpt-5.6-sol",
389
456
  "openai/gpt-5.6-terra-pro",
390
457
  "openai/gpt-5.6-terra",
458
+ "openai/gpt-6-luna ",
459
+ "openai/gpt-6-luna",
391
460
  "openai/gpt-5.6-luna-pro",
392
461
  "openai/gpt-5.6-luna",
393
462
  "openai/gpt-5.5-pro",
394
463
  "openai/gpt-5.5",
395
- "~openai/gpt-latest",
464
+ "~openai/gpt-astra-latest",
396
465
  "google/gemini-3.1-pro-preview-customtools",
397
466
  "google/gemini-3.1-pro-preview",
398
467
  "~google/gemini-pro-latest",
@@ -403,6 +472,9 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
403
472
  "~moonshotai/kimi-latest",
404
473
  "moonshotai/kimi-k2.6",
405
474
  "moonshotai/kimi-k2-thinking",
475
+ "x-ai/grok-4.7 ",
476
+ "x-ai/grok-4.7",
477
+ "x-ai/grok-4.6",
406
478
  "x-ai/grok-4.5",
407
479
  "~x-ai/grok-latest",
408
480
  "deepseek/deepseek-v4-pro",
@@ -444,6 +516,34 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
444
516
  "glm-5.2",
445
517
  "deepseek-v4-flash-0731",
446
518
  ],
519
+ radius: [
520
+ "gpt-6-astra ",
521
+ "gpt-6-astra",
522
+ "claude-opus-5-5 ",
523
+ "claude-opus-5-5",
524
+ "claude-opus-5",
525
+ "gpt-6-sol ",
526
+ "gpt-6-sol",
527
+ "gpt-5.6-sol",
528
+ "gpt-5.6-terra",
529
+ "gpt-6-luna ",
530
+ "gpt-6-luna",
531
+ "gpt-5.6-luna",
532
+ "claude-fable-5-1",
533
+ "claude-fable-5",
534
+ "claude-opus-4-8",
535
+ "claude-sonnet-5",
536
+ "gpt-5.5",
537
+ "gpt-5.4",
538
+ "gpt-5.3-codex",
539
+ "kimi-k3",
540
+ "deepseek-v4-pro",
541
+ "glm-5.3",
542
+ "kimi-k2.7-code",
543
+ "deepseek-v4.1-flash",
544
+ "gpt-5.4-mini",
545
+ "glm-5.3-flash",
546
+ ],
447
547
  together: [
448
548
  "deepseek-ai/DeepSeek-V4-Pro",
449
549
  "moonshotai/Kimi-K3",
@@ -461,6 +561,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
461
561
  "google/gemma-4-31B-it",
462
562
  ],
463
563
  "vercel-ai-gateway": [
564
+ "anthropic/claude-opus-5.5 ",
565
+ "anthropic/claude-opus-5.5",
464
566
  "anthropic/claude-opus-5",
465
567
  "anthropic/claude-fable-5",
466
568
  "anthropic/claude-opus-4.8",
@@ -469,8 +571,14 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
469
571
  "anthropic/claude-opus-4.5",
470
572
  "anthropic/claude-sonnet-5",
471
573
  "anthropic/claude-sonnet-4.6",
574
+ "openai/gpt-6-astra ",
575
+ "openai/gpt-6-astra",
576
+ "openai/gpt-6-sol ",
577
+ "openai/gpt-6-sol",
472
578
  "openai/gpt-5.6-sol",
473
579
  "openai/gpt-5.6-terra",
580
+ "openai/gpt-6-luna ",
581
+ "openai/gpt-6-luna",
474
582
  "openai/gpt-5.6-luna",
475
583
  "openai/gpt-5.5-pro",
476
584
  "openai/gpt-5.5",
@@ -495,6 +603,9 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
495
603
  "google/gemini-3.1-pro-preview",
496
604
  "google/gemini-3.5-flash",
497
605
  "google/gemini-3-flash",
606
+ "spacexai/grok-4.7 ",
607
+ "spacexai/grok-4.7",
608
+ "spacexai/grok-4.6",
498
609
  "xai/grok-4.5",
499
610
  "xai/grok-4.20-reasoning",
500
611
  "minimax/minimax-m3",
@@ -503,6 +614,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
503
614
  "zai/glm-5.1",
504
615
  ],
505
616
  xai: [
617
+ "grok-4.7 ",
618
+ "grok-4.7",
506
619
  "grok-4.6",
507
620
  "grok-4.5",
508
621
  "grok-4.3",
@@ -515,9 +628,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
515
628
  "nvidia/nemotron-3-ultra-550b-a55b",
516
629
  "nvidia/nemotron-3-super-120b-a12b",
517
630
  "moonshotai/kimi-k3",
518
- "deepseek-ai/deepseek-v4-pro-0813",
519
631
  "moonshotai/kimi-k2.6",
520
- "minimaxai/minimax-m3",
521
632
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
522
633
  ],
523
634
  zai: [
@@ -528,13 +639,12 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
528
639
  ],
529
640
  "zai-coding-cn": [
530
641
  "glm-5.3",
531
- "glm-5.2",
532
- "glm-5-turbo",
533
- "glm-5",
534
- "glm-4.7",
642
+ "glm-5.3-highspeed",
643
+ "glm-5.3-flash",
644
+ "glm-4.6v",
535
645
  ],
536
- moonshotai: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2-thinking-turbo", "kimi-k2-thinking", "kimi-k2.5"],
537
- "moonshotai-cn": ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2-thinking-turbo", "kimi-k2-thinking", "kimi-k2.5"],
646
+ moonshotai: ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
647
+ "moonshotai-cn": ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
538
648
  };
539
649
 
540
650
  const ORACLE_SYSTEM_PROMPT = [
@@ -913,7 +1023,7 @@ function clampThinkingLevel(model: PiModel, requested: ThinkingLevel): ThinkingL
913
1023
 
914
1024
  function isGpt56SolModel(model: PiModel | undefined): boolean {
915
1025
  if (!model) return false;
916
- return /\bgpt-5\.6-sol\b/i.test(`${model.id} ${model.name ?? ""}`);
1026
+ return /\bgpt-(?:5\.6|6)-sol\b/i.test(`${model.id} ${model.name ?? ""}`);
917
1027
  }
918
1028
 
919
1029
  function resolveThinkingLevel(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, contrarian review, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, model/provider helpers, and Xiaohei-style article illustrations.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -71,9 +71,9 @@
71
71
  ]
72
72
  },
73
73
  "devDependencies": {
74
- "@earendil-works/pi-ai": "^0.85.1",
75
- "@earendil-works/pi-coding-agent": "^0.85.1",
76
- "@earendil-works/pi-tui": "^0.85.1",
74
+ "@earendil-works/pi-ai": "^0.87.1",
75
+ "@earendil-works/pi-coding-agent": "^0.87.1",
76
+ "@earendil-works/pi-tui": "^0.87.1",
77
77
  "@types/node": "^25.9.1",
78
78
  "husky": "^9.1.7",
79
79
  "typebox": "^1.3.6",