@oh-my-pi/pi-catalog 18.3.0 → 18.3.1

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.
@@ -0,0 +1,13 @@
1
+ import type { Model } from "../types";
2
+ import { resolveCatalogPolicy } from "./catalog-policy";
3
+
4
+ /**
5
+ * Ordered model ids for Anthropic's server-side refusal `fallbacks` chain,
6
+ * authored on the `server-side-fallback-models` catalog axis. Empty when no
7
+ * rule assigns a chain (the model or host does not support the feature).
8
+ */
9
+ export function serverSideFallbackModels(model: Model): readonly string[] {
10
+ const value = resolveCatalogPolicy(model).serverSideFallbackModels;
11
+ if (!Array.isArray(value)) return [];
12
+ return value.filter((entry): entry is string => typeof entry === "string");
13
+ }
@@ -453,6 +453,51 @@ function devinModelSpec(
453
453
  return spec;
454
454
  }
455
455
 
456
+ /**
457
+ * Lead chat uid of a Fusion pairing `fusion-<lead>[-fast]-sidekick-<sidekick>`.
458
+ * An exact live lead uid wins, so leads whose own uid ends in `-fast`
459
+ * (`swe-1-6-fast`) route as written. Otherwise `-fast` selects the lead's
460
+ * priority lane when the server lists one, then the standard lane.
461
+ *
462
+ * Returns `undefined` for uids that are not pairings and `null` for pairings
463
+ * whose lead is not live: the composite uid itself is never servable.
464
+ */
465
+ function devinFusionLeadUid(uid: string, liveUids: ReadonlyMap<string, unknown>): string | null | undefined {
466
+ if (!uid.startsWith("fusion-")) return undefined;
467
+ const cut = uid.indexOf("-sidekick-");
468
+ if (cut <= "fusion-".length) return undefined;
469
+ const lead = uid.slice("fusion-".length, cut);
470
+ if (liveUids.has(lead)) return lead;
471
+ if (lead.endsWith("-fast")) {
472
+ const base = lead.slice(0, -"-fast".length);
473
+ if (liveUids.has(`${base}-priority`)) return `${base}-priority`;
474
+ if (liveUids.has(base)) return base;
475
+ }
476
+ return null;
477
+ }
478
+
479
+ /**
480
+ * Point a Fusion pairing at its lead. omp runs only the lead (the sidekick is
481
+ * paired by the native client), so the limits and pricing a caller budgets
482
+ * against are the lead's, not the composite card's.
483
+ */
484
+ function routeDevinFusionLead(spec: ModelSpec<"devin-agent">, lead: ModelSpec<"devin-agent">): void {
485
+ spec.requestModelId = lead.id;
486
+ spec.reasoning = lead.reasoning;
487
+ spec.input = lead.input;
488
+ spec.supportsTools = lead.supportsTools;
489
+ spec.cost = lead.cost;
490
+ spec.contextWindow = lead.contextWindow;
491
+ spec.maxTokens = lead.maxTokens;
492
+ if (lead.compat?.supportsParallelToolCalls) {
493
+ spec.compat = { ...spec.compat, supportsParallelToolCalls: true };
494
+ } else if (spec.compat?.supportsParallelToolCalls) {
495
+ const { supportsParallelToolCalls: _, ...rest } = spec.compat;
496
+ if (Object.keys(rest).length > 0) spec.compat = rest;
497
+ else delete spec.compat;
498
+ }
499
+ }
500
+
456
501
  function normalizeDevinModels(
457
502
  configs: readonly ClientModelConfig[],
458
503
  baseUrlOverride: string | undefined,
@@ -461,6 +506,11 @@ function normalizeDevinModels(
461
506
  const specs: ModelSpec<"devin-agent">[] = [];
462
507
  const seen = new Set<string>();
463
508
  const lanes = new Map<string, DevinFamilyLane>();
509
+ const liveConfigs = new Map<string, ClientModelConfig>();
510
+ for (const config of configs) {
511
+ const uid = config.modelUid.trim();
512
+ if (!config.disabled && uid && !liveConfigs.has(uid)) liveConfigs.set(uid, config);
513
+ }
464
514
 
465
515
  for (const config of configs) {
466
516
  if (config.disabled) {
@@ -482,7 +532,16 @@ function normalizeDevinModels(
482
532
  // `fusion-sidekick-*`) that are themselves valid chat uids. Only the
483
533
  // former take the `AssignModel` path — sending a composite uid there 404s.
484
534
  const isAssignModelRouter = isRouter && (config.modelInfo?.harnessUids.length ?? 0) === 0;
485
- specs.push(devinModelSpec(config, uid, baseUrl, isAssignModelRouter));
535
+ // Fusion pairings are orchestrated by the native client: it runs the lead
536
+ // model as an ordinary chat uid and pairs a sidekick locally. The server
537
+ // has no provider for the composite uid itself (`permission_denied: no API
538
+ // providers are available`), so the chat request carries the lead uid and
539
+ // a pairing without a live lead is not listed.
540
+ const lead = devinFusionLeadUid(uid, liveConfigs);
541
+ if (lead === null) continue;
542
+ const spec = devinModelSpec(config, uid, baseUrl, isAssignModelRouter);
543
+ if (lead !== undefined) routeDevinFusionLead(spec, devinModelSpec(liveConfigs.get(lead)!, lead, baseUrl, false));
544
+ specs.push(spec);
486
545
  // A router is a server-side dispatcher, not an effort tier: it stays a
487
546
  // standalone model even when upstream files it under a family.
488
547
  if (!isRouter) {