@diegopetrucci/pi-oracle 0.1.15 → 0.1.16

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 (2) hide show
  1. package/index.ts +126 -33
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -612,6 +612,19 @@ function isTransientErrorMessage(message: string): boolean {
612
612
  return TRANSIENT_ERROR_PATTERN.test(message);
613
613
  }
614
614
 
615
+ // A model the catalog advertises but the active provider/subscription cannot
616
+ // serve surfaces as a not-found/404-style error (legacy snapshots or
617
+ // access-gated tiers). These are NOT transient: the same model keeps failing,
618
+ // so callers should fall back to a different model instead of retrying it.
619
+ const MODEL_AVAILABILITY_ERROR_PATTERN =
620
+ /\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;
621
+
622
+ function isModelAvailabilityError(message: string | undefined): boolean {
623
+ if (!message) return false;
624
+ if (isTransientErrorMessage(message)) return false;
625
+ return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
626
+ }
627
+
615
628
  function formatOracleModelError(stopReason: "error" | "aborted", errorMessage: string | undefined): string {
616
629
  const trimmed = errorMessage?.trim();
617
630
  if (stopReason === "aborted") {
@@ -776,10 +789,48 @@ async function findAvailableModel(
776
789
  return undefined;
777
790
  }
778
791
 
792
+ function toOracleSelection(
793
+ model: PiModel,
794
+ thinkingLevelOverride: ThinkingLevel | undefined,
795
+ reason: string,
796
+ ): OracleSelection {
797
+ const thinking = resolveThinkingLevel(model, thinkingLevelOverride);
798
+ return {
799
+ modelRef: `${model.provider}/${model.id}`,
800
+ provider: model.provider,
801
+ modelId: model.id,
802
+ modelName: model.name,
803
+ thinkingLevel: thinking.effective,
804
+ ...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
805
+ autoSelected: true,
806
+ selectionReason: appendThinkingLevelClampReason(reason, thinking),
807
+ };
808
+ }
809
+
810
+ function buildSessionFallbackSelection(
811
+ ctx: { model?: PiModel },
812
+ thinkingLevelOverride: ThinkingLevel | undefined,
813
+ ): OracleSelection | undefined {
814
+ const model = ctx.model;
815
+ if (!model) return undefined;
816
+ return toOracleSelection(
817
+ model,
818
+ thinkingLevelOverride,
819
+ `Used the current session model ${model.provider}/${model.id} as a final fallback.`,
820
+ );
821
+ }
822
+
823
+ function withFallbackReason(selection: OracleSelection, previous: OracleSelection): OracleSelection {
824
+ return {
825
+ ...selection,
826
+ selectionReason: `${selection.selectionReason} (Fell back from ${previous.modelRef} after a model-availability error.)`,
827
+ };
828
+ }
829
+
779
830
  async function selectOracleModel(
780
831
  ctx: { model?: PiModel; modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> } },
781
832
  thinkingLevelOverride?: ThinkingLevel,
782
- ): Promise<{ ok: true; selection: OracleSelection } | { ok: false; error: string }> {
833
+ ): Promise<{ ok: true; selection: OracleSelection; ordered: OracleSelection[] } | { ok: false; error: string }> {
783
834
  const available = await ctx.modelRegistry.getAvailable();
784
835
  if (available.length === 0) {
785
836
  return {
@@ -810,27 +861,23 @@ async function selectOracleModel(
810
861
  reason = "No reasoning models were available, so the top-ranked model across all available providers was used.";
811
862
  }
812
863
 
864
+ const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
813
865
  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,
866
+ const orderedModels = preferred ? [preferred, ...sorted.filter((model) => model !== preferred)] : sorted;
867
+
868
+ const ordered = orderedModels.map((model, index) =>
869
+ toOracleSelection(
870
+ model,
871
+ thinkingLevelOverride,
872
+ index === 0
873
+ ? preferred
874
+ ? `Selected ${model.id} via the hardcoded preference list for ${model.provider}.`
875
+ : reason
876
+ : `Auto-selected ${model.provider}/${model.id} as a lower-ranked fallback.`,
877
+ ),
819
878
  );
820
879
 
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
- };
880
+ return { ok: true, selection: ordered[0], ordered };
834
881
  }
835
882
 
836
883
  function updateOracleUi(ctx: ExtensionContext, activeRuns: Map<string, OracleUiRun>): void {
@@ -1237,7 +1284,16 @@ export default function oracleExtension(pi: ExtensionAPI) {
1237
1284
  updateOracleUi(ctx, activeRuns);
1238
1285
 
1239
1286
  try {
1240
- let selection: OracleSelection;
1287
+ const attempts: OracleSelection[] = [];
1288
+ const seen = new Set<string>();
1289
+ const pushAttempt = (candidate: OracleSelection | undefined) => {
1290
+ if (!candidate) return;
1291
+ const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
1292
+ if (seen.has(key)) return;
1293
+ seen.add(key);
1294
+ attempts.push(candidate);
1295
+ };
1296
+
1241
1297
  if (configuredModel) {
1242
1298
  const modelRef = configuredModel;
1243
1299
  const matched = await findAvailableModel(ctx, modelRef);
@@ -1256,7 +1312,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
1256
1312
  : usedToolOverride
1257
1313
  ? "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
1314
  : "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 = {
1315
+ pushAttempt({
1260
1316
  modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
1261
1317
  provider,
1262
1318
  modelId,
@@ -1267,7 +1323,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
1267
1323
  : {}),
1268
1324
  autoSelected: false,
1269
1325
  selectionReason,
1270
- };
1326
+ });
1271
1327
  } else {
1272
1328
  const selectionResult = await selectOracleModel(ctx, configuredThinkingLevel);
1273
1329
  if (!selectionResult.ok) {
@@ -1290,11 +1346,14 @@ export default function oracleExtension(pi: ExtensionAPI) {
1290
1346
  },
1291
1347
  };
1292
1348
  }
1293
- selection = selectionResult.selection;
1349
+ for (const candidate of selectionResult.ordered) pushAttempt(candidate);
1294
1350
  }
1295
1351
 
1296
- uiRun.selection = selection;
1297
- updateOracleUi(ctx, activeRuns);
1352
+ // Keep the known-good session model as a final fallback: the catalog can
1353
+ // advertise models the active provider/subscription cannot actually serve
1354
+ // (legacy snapshots, access-gated tiers), which fail with a not-found/404
1355
+ // error. Falling back lets the run degrade gracefully instead of hard-failing.
1356
+ pushAttempt(buildSessionFallbackSelection(ctx, configuredThinkingLevel));
1298
1357
 
1299
1358
  const handleUpdate = (partial: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => {
1300
1359
  uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
@@ -1302,17 +1361,51 @@ export default function oracleExtension(pi: ExtensionAPI) {
1302
1361
  onUpdate?.(partial);
1303
1362
  };
1304
1363
 
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
- };
1364
+ let lastErrorResult: { ok: false; error: string; details: OracleDetails } | undefined;
1365
+ for (let index = 0; index < attempts.length; index++) {
1366
+ const previous = index > 0 ? attempts[index - 1] : undefined;
1367
+ const attempt = previous ? withFallbackReason(attempts[index], previous) : attempts[index];
1368
+ uiRun.selection = attempt;
1369
+ updateOracleUi(ctx, activeRuns);
1370
+
1371
+ const result = await runOracle(attempt, params, signal, handleUpdate, ctx.cwd);
1372
+ if (result.ok) {
1373
+ return {
1374
+ content: [{ type: "text", text: result.output }],
1375
+ details: result.details,
1376
+ };
1377
+ }
1378
+
1379
+ lastErrorResult = result;
1380
+ const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1381
+ if (!canFallBack) {
1382
+ return {
1383
+ content: [{ type: "text", text: result.error }],
1384
+ details: result.details,
1385
+ };
1386
+ }
1311
1387
  }
1312
1388
 
1313
1389
  return {
1314
- content: [{ type: "text", text: result.output }],
1315
- details: result.details,
1390
+ content: [
1391
+ { type: "text", text: lastErrorResult?.error ?? "Oracle could not select an available model." },
1392
+ ],
1393
+ details:
1394
+ lastErrorResult?.details ?? {
1395
+ modelRef: "",
1396
+ provider: ctx.model?.provider ?? "unknown",
1397
+ modelId: "",
1398
+ modelName: undefined,
1399
+ thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1400
+ autoSelected: true,
1401
+ selectionReason: "No authenticated models were available to run the oracle.",
1402
+ includeBash: params.includeBash ?? false,
1403
+ usage: createEmptyUsage(),
1404
+ stderr: "",
1405
+ exitCode: 1,
1406
+ durationMs: 0,
1407
+ cwd: params.cwd ?? ctx.cwd,
1408
+ },
1316
1409
  };
1317
1410
  } finally {
1318
1411
  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.16",
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",