@linxiraos/pi-catalog 1.1.27 → 1.2.0

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 (100) hide show
  1. package/CHANGELOG.md +4 -17
  2. package/dist/types/compat/auth-ids.d.ts +2 -2
  3. package/dist/types/compat/judgment-questions.d.ts +7 -0
  4. package/dist/types/compat/prompt-cache-lookback.d.ts +7 -0
  5. package/dist/types/compat/provider-ids.d.ts +1 -1
  6. package/dist/types/compat/reasoning-carry.d.ts +21 -0
  7. package/dist/types/compat/request-size.d.ts +7 -0
  8. package/dist/types/compat/taxonomy.d.ts +0 -10
  9. package/dist/types/compat/types.d.ts +4 -0
  10. package/dist/types/discovery/antigravity.d.ts +13 -3
  11. package/dist/types/model-manager.d.ts +16 -0
  12. package/dist/types/provider-models/account-access.d.ts +11 -0
  13. package/dist/types/provider-models/google.d.ts +23 -1
  14. package/dist/types/provider-models/openai-compat.d.ts +19 -1
  15. package/dist/types/provider-models/special.d.ts +2 -0
  16. package/dist/types/types.d.ts +106 -20
  17. package/dist/types/wire/cloudflare-ai-gateway.d.ts +0 -10
  18. package/dist/types/wire/cloudflare.d.ts +10 -0
  19. package/dist/types/wire/factory-droid.d.ts +1 -1
  20. package/package.json +4 -4
  21. package/src/build.ts +39 -35
  22. package/src/compat/auth-ids.ts +4 -0
  23. package/src/compat/axes.ts +37 -3
  24. package/src/compat/cascade.ts +6 -1
  25. package/src/compat/judgment-questions.ts +12 -0
  26. package/src/compat/prompt-cache-lookback.ts +23 -0
  27. package/src/compat/provider-ids.d.ts +1 -1
  28. package/src/compat/provider-ids.ts +2 -0
  29. package/src/compat/reasoning-carry.ts +64 -0
  30. package/src/compat/request-size.ts +25 -0
  31. package/src/compat/resolve.ts +16 -2
  32. package/src/compat/rules/README.md +12 -2
  33. package/src/compat/rules/auth/_order.kdl +2 -2
  34. package/src/compat/rules/auth/cloudflare-workers-ai.kdl +9 -0
  35. package/src/compat/rules/auth/coralbricks.kdl +13 -0
  36. package/src/compat/rules/auth/minimax-code-cn.kdl +1 -1
  37. package/src/compat/rules/auth/minimax-code.kdl +1 -1
  38. package/src/compat/rules/auth/openai-codex.kdl +1 -1
  39. package/src/compat/rules/auth/stencil.kdl +1 -1
  40. package/src/compat/rules/classes/anthropic.kdl +87 -1
  41. package/src/compat/rules/classes/deepseek.kdl +6 -0
  42. package/src/compat/rules/classes/glm.kdl +16 -7
  43. package/src/compat/rules/classes/kimi.kdl +8 -0
  44. package/src/compat/rules/classes/minimax.kdl +31 -1
  45. package/src/compat/rules/classes/openai.kdl +5 -3
  46. package/src/compat/rules/classes/qwen.kdl +2 -1
  47. package/src/compat/rules/providers/abliteration.kdl +7 -0
  48. package/src/compat/rules/providers/anthropic.kdl +13 -0
  49. package/src/compat/rules/providers/apple.kdl +1 -0
  50. package/src/compat/rules/providers/cloudflare-workers-ai.kdl +34 -0
  51. package/src/compat/rules/providers/coralbricks.kdl +62 -0
  52. package/src/compat/rules/providers/cursor.kdl +22 -0
  53. package/src/compat/rules/providers/devin.kdl +2 -2
  54. package/src/compat/rules/providers/factory-droid.kdl +77 -13
  55. package/src/compat/rules/providers/github-copilot.kdl +29 -0
  56. package/src/compat/rules/providers/groq.kdl +21 -0
  57. package/src/compat/rules/providers/judgment-questions.kdl +53 -0
  58. package/src/compat/rules/providers/litellm.kdl +4 -1
  59. package/src/compat/rules/providers/minimax-code-cn.kdl +10 -5
  60. package/src/compat/rules/providers/minimax-code.kdl +10 -5
  61. package/src/compat/rules/providers/mistral.kdl +25 -1
  62. package/src/compat/rules/providers/moonshot.kdl +11 -0
  63. package/src/compat/rules/providers/openai-codex.kdl +17 -13
  64. package/src/compat/rules/providers/openai.kdl +22 -0
  65. package/src/compat/rules/providers/opencode-zen.kdl +20 -0
  66. package/src/compat/rules/providers/openrouter.kdl +6 -0
  67. package/src/compat/rules/providers/zhipu-coding-plan.kdl +10 -0
  68. package/src/compat/rules/runtime/behavior.kdl +7 -4
  69. package/src/compat/rules/taxonomy/_collapse.kdl +18 -0
  70. package/src/compat/rules.json +1 -1
  71. package/src/compat/taxonomy.d.ts +0 -10
  72. package/src/compat/taxonomy.ts +3 -3
  73. package/src/compat/types.d.ts +4 -0
  74. package/src/compat/types.ts +4 -0
  75. package/src/discovery/antigravity.d.ts +13 -3
  76. package/src/discovery/antigravity.ts +33 -17
  77. package/src/discovery/cursor.ts +2 -1
  78. package/src/discovery/devin.ts +20 -2
  79. package/src/model-cache.ts +113 -47
  80. package/src/model-manager.d.ts +16 -0
  81. package/src/model-manager.ts +107 -41
  82. package/src/model-tokenizer.ts +3 -0
  83. package/src/models.json +1 -1
  84. package/src/provider-models/account-access.d.ts +11 -0
  85. package/src/provider-models/account-access.ts +30 -0
  86. package/src/provider-models/cache-provider-id.ts +3 -2
  87. package/src/provider-models/descriptors.ts +4 -0
  88. package/src/provider-models/google.d.ts +23 -1
  89. package/src/provider-models/google.ts +70 -14
  90. package/src/provider-models/openai-compat.d.ts +19 -1
  91. package/src/provider-models/openai-compat.ts +215 -31
  92. package/src/provider-models/special.d.ts +2 -0
  93. package/src/provider-models/special.ts +15 -18
  94. package/src/types.d.ts +106 -20
  95. package/src/types.ts +149 -17
  96. package/src/wire/cloudflare-ai-gateway.d.ts +0 -10
  97. package/src/wire/cloudflare-ai-gateway.ts +0 -36
  98. package/src/wire/cloudflare.ts +40 -0
  99. package/src/wire/factory-droid.d.ts +1 -1
  100. package/src/wire/factory-droid.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { VERSION } from "@linxiraos/pi-utils";
1
+ import { logger, VERSION } from "@linxiraos/pi-utils";
2
2
  import { buildModel } from "./build";
3
3
  import { collapseBuiltVariants } from "./compat/collapse";
4
4
  import { applyCatalogMetrics, CatalogMetricsIndex } from "./identity/metrics";
@@ -57,6 +57,22 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
57
57
  cacheTtlMs?: number;
58
58
  /** When true, a successful dynamic fetch is the complete provider catalog and prunes static-only models. */
59
59
  dynamicModelsAuthoritative?: boolean;
60
+ /**
61
+ * When true, a provider's dynamic rows are the deployment's whole truth
62
+ * for input modality: a live text-only row strips the bundled reference's
63
+ * image support instead of OR-merging it back. Declared by providers whose
64
+ * capability flags are authoritative and whose endpoints reject
65
+ * unsupported content (`github-copilot`, `deepinfra`, `coralbricks`).
66
+ */
67
+ dynamicInputAuthoritative?: boolean;
68
+ /** When true, live reasoning capability replaces the fallback, including an explicit false. */
69
+ dynamicReasoningAuthoritative?: boolean;
70
+ /**
71
+ * When true, a live per-million price replaces the fallback even when it is
72
+ * an explicit `0` (a free model or a dropped cache-write charge). Without it,
73
+ * a zero discovery price is treated as unknown and the bundled rate is kept.
74
+ */
75
+ dynamicCostAuthoritative?: boolean;
60
76
  /**
61
77
  * When true, a fresh cache never satisfies an online-eligible refresh: the
62
78
  * dynamic fetch always runs (an explicit `"offline"` strategy is still
@@ -255,6 +271,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
255
271
  const usableCachedModels = restoredCache.models.filter(model => !restoredCache.unresolvedModelIds.has(model.id));
256
272
  const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
257
273
  const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
274
+ const mergeAuthority: DynamicMergeAuthority = {
275
+ input: options.dynamicInputAuthoritative ?? false,
276
+ reasoning: options.dynamicReasoningAuthoritative ?? false,
277
+ cost: options.dynamicCostAuthoritative ?? false,
278
+ };
258
279
  const cacheDropIds = options.dropCachedModelIdsOnStaticMismatch;
259
280
  const staticCatalogFingerprint = fingerprintStaticModels(staticModels, dynamicModelsAuthoritative);
260
281
  // Endpoint-migration policy is cache identity: adding an id must invalidate
@@ -298,7 +319,10 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
298
319
  ? restoredCache.models.filter(model => !additiveStaticModelIds.has(model.id))
299
320
  : restoredCache.models;
300
321
  const cachedModels = additiveStaticModelIds
301
- ? mergeCatalogMetrics(mergeDynamicModels(staticModels, cacheContribution), restoredCache.models)
322
+ ? mergeCatalogMetrics(
323
+ mergeDynamicModels(staticModels, cacheContribution, mergeAuthority),
324
+ restoredCache.models,
325
+ )
302
326
  : restoredCache.models;
303
327
  const source: ModelResolutionSource = cacheContribution.length > 0 ? "cache" : "bundled";
304
328
  return {
@@ -351,10 +375,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
351
375
  dynamicModels.length > 0 &&
352
376
  (dynamicModelsAuthoritative || !hasModelsDevFetcher || modelsDevFetchSucceeded)
353
377
  : modelsDevFetchSucceeded;
354
- const mergedWithCache = mergeDynamicModels(staticModels, cacheModels);
378
+ const mergedWithCache = mergeDynamicModels(staticModels, cacheModels, mergeAuthority);
355
379
  const mergedWithModelsDev = mergeDynamicModels(
356
380
  mergedWithCache,
357
381
  modelsDevModels,
382
+ mergeAuthority,
358
383
  fetchedModelsDevModels?.explicitKindModels,
359
384
  );
360
385
  const catalogMetricsSource = modelsDevFetchSucceeded ? normalizedModelsDevModels : preparedCacheModels;
@@ -364,6 +389,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
364
389
  const mergedModels = mergeDynamicModels(
365
390
  mergedWithCatalogMetrics,
366
391
  dynamicModels,
392
+ mergeAuthority,
367
393
  fetchedDynamicModels?.explicitKindModels,
368
394
  );
369
395
  const models = collapseBuiltVariants(
@@ -416,7 +442,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
416
442
  ? preparedLatestCacheModels.filter(model => !additiveStaticModelIds.has(model.id))
417
443
  : preparedLatestCacheModels;
418
444
  const fallbackSnapshotModels = collapseBuiltVariants(
419
- mergeDynamicModels(mergeDynamicModels(staticModels, latestCacheModels), modelsDevModels),
445
+ mergeDynamicModels(
446
+ mergeDynamicModels(staticModels, latestCacheModels, mergeAuthority),
447
+ modelsDevModels,
448
+ mergeAuthority,
449
+ ),
420
450
  );
421
451
  if (fallbackSnapshotModels.length > 0 || latestCache !== null || cache !== null) {
422
452
  writeModelCache(
@@ -544,9 +574,20 @@ function mergeCatalogMetrics<TApi extends Api>(
544
574
  return applyCatalogMetrics(models, new CatalogMetricsIndex(catalogModels));
545
575
  }
546
576
 
577
+ /**
578
+ * Which dynamic-row fields replace the existing row outright instead of
579
+ * merging additively. Set per provider by `ModelManagerOptions`.
580
+ */
581
+ interface DynamicMergeAuthority {
582
+ readonly input: boolean;
583
+ readonly reasoning: boolean;
584
+ readonly cost: boolean;
585
+ }
586
+
547
587
  function mergeDynamicModels<TApi extends Api>(
548
588
  baseModels: readonly Model<TApi>[],
549
589
  dynamicModels: readonly Model<TApi>[],
590
+ authority: DynamicMergeAuthority,
550
591
  explicitKindModels?: ReadonlySet<Model<TApi>>,
551
592
  ): Model<TApi>[] {
552
593
  // Empty-side fast paths: `mergeDynamicModels(base, [])` is the common shape
@@ -567,7 +608,7 @@ function mergeDynamicModels<TApi extends Api>(
567
608
  // A policy-derived kind on a chat row is not permission to replace an
568
609
  // authored runner. Only a kind present before materialization can do so.
569
610
  if (modelKind(existingModel) !== "chat" && !explicitKindModels?.has(dynamicModel)) continue;
570
- merged.set(dynamicModel.id, mergeDynamicModel(existingModel, dynamicModel));
611
+ merged.set(dynamicModel.id, mergeDynamicModel(existingModel, dynamicModel, authority));
571
612
  }
572
613
  return Array.from(merged.values());
573
614
  }
@@ -598,39 +639,30 @@ export function fingerprintStaticModels<TApi extends Api>(
598
639
  return `${MODEL_CACHE_FINGERPRINT_VERSION}:${Bun.hash(JSON.stringify(models)).toString(36)}`;
599
640
  }
600
641
 
601
- function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamicModel: Model<TApi>): Model<TApi> {
642
+ function mergeDynamicModel<TApi extends Api>(
643
+ existingModel: Model<TApi>,
644
+ dynamicModel: Model<TApi>,
645
+ authority: DynamicMergeAuthority,
646
+ ): Model<TApi> {
602
647
  // When discovery resolves the same model id to a different endpoint (e.g.
603
648
  // a GitHub Copilot business/enterprise host), the bundled reference's
604
- // capabilities are pinned to another endpoint and no longer apply. Copilot
605
- // dynamic discovery also pre-applies the correct image fallback for omitted
606
- // `supports.vision`, so its explicit `false` must not be OR-upgraded by the
607
- // canonical bundled model.
608
- // DeepInfra's discovery is authoritative (`dynamicModelsAuthoritative`) and
609
- // its `vision`/`vlm` tags are the catalog's whole truth for modality. Every
610
- // row shares the single DeepInfra endpoint, so `endpointChanged` never fires
611
- // there: without this carve-out a model that dropped those tags would keep
612
- // the bundled reference's image support and the agent would go on sending
649
+ // capabilities are pinned to another endpoint and no longer apply.
650
+ // `authority.input` carries the descriptor's declaration that a
651
+ // provider's discovery rows are the deployment's whole truth for input
652
+ // modality (Copilot's pre-applied `supports.vision` fallback, DeepInfra's
653
+ // `vision`/`vlm` tags, CoralBricks' authoritative `supports_image_input`
654
+ // flag): without it, a live row that dropped image support would keep the
655
+ // bundled reference's image support and the agent would go on sending
613
656
  // images to a now text-only route.
614
657
  const endpointChanged = existingModel.baseUrl !== dynamicModel.baseUrl;
615
- const dynamicInputAuthoritative =
616
- endpointChanged ||
617
- (existingModel.provider === "github-copilot" && dynamicModel.provider === "github-copilot") ||
618
- (existingModel.provider === "deepinfra" && dynamicModel.provider === "deepinfra");
658
+ const dynamicInputAuthoritative = endpointChanged || authority.input;
619
659
  const supportsImage = dynamicInputAuthoritative
620
660
  ? dynamicModel.input.includes("image")
621
661
  : existingModel.input.includes("image") || dynamicModel.input.includes("image");
622
- // Synthetic's discovery is authoritative (`dynamicModelsAuthoritative`) and
623
- // its per-model `reasoning_parameters.efforts` vocabulary is the route's
624
- // whole truth: when the wire advertises only the `none` off-state the
625
- // mapper emits `reasoning: false`, and OR-ing the bundled reference's
626
- // stale `reasoning: true` back would re-arm an effort dial the route
627
- // doesn't expose. Other providers keep the OR so a bundled reasoning flag
628
- // survives a discovery row that simply omits the capability.
629
- const dynamicReasoningAuthoritative =
630
- existingModel.provider === "synthetic" && dynamicModel.provider === "synthetic";
631
- const reasoning = dynamicReasoningAuthoritative
632
- ? dynamicModel.reasoning
633
- : existingModel.reasoning || dynamicModel.reasoning;
662
+ // Providers with authoritative reasoning metadata must not regain a
663
+ // stale bundled dial after explicitly reporting no reasoning. Others
664
+ // retain the additive fallback for discovery that omits capabilities.
665
+ const reasoning = authority.reasoning ? dynamicModel.reasoning : existingModel.reasoning || dynamicModel.reasoning;
634
666
  const longContextCost = dynamicModel.cost.longContext ?? existingModel.cost.longContext;
635
667
  const timeBasedCost = dynamicModel.cost.timeBased ?? existingModel.cost.timeBased;
636
668
  const existingHeaders = existingModel.resolveHeaders ?? existingModel.headers;
@@ -651,17 +683,26 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
651
683
  // the effort dial once the id is pinned to Responses (#12901).
652
684
  const compat =
653
685
  dynamicModel.compatConfig ?? (dynamicModel.api === existingModel.api ? existingModel.compatConfig : undefined);
686
+ // A live row that trusts only its own thinking (e.g. CoralBricks' advertised
687
+ // ladder) must not inherit the fallback's dial. Cached rows lose an
688
+ // explicit `thinking: undefined` to JSON, so the spread alone would.
689
+ const liveCompat = dynamicModel.compatConfig;
690
+ const trustsOwnThinking =
691
+ liveCompat !== undefined &&
692
+ "trustExplicitThinkingOnly" in liveCompat &&
693
+ liveCompat.trustExplicitThinkingOnly === true;
654
694
  return buildModel({
655
695
  ...existingModel,
656
696
  ...dynamicModel,
657
697
  name: preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
658
698
  reasoning,
699
+ ...(trustsOwnThinking ? { thinking: dynamicModel.thinking } : {}),
659
700
  input: supportsImage ? ["text", "image"] : ["text"],
660
701
  cost: {
661
- input: preferDiscoveryCost(dynamicModel.cost.input, existingModel.cost.input),
662
- output: preferDiscoveryCost(dynamicModel.cost.output, existingModel.cost.output),
663
- cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead),
664
- cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite),
702
+ input: preferDiscoveryCost(dynamicModel.cost.input, existingModel.cost.input, authority.cost),
703
+ output: preferDiscoveryCost(dynamicModel.cost.output, existingModel.cost.output, authority.cost),
704
+ cacheRead: preferDiscoveryCost(dynamicModel.cost.cacheRead, existingModel.cost.cacheRead, authority.cost),
705
+ cacheWrite: preferDiscoveryCost(dynamicModel.cost.cacheWrite, existingModel.cost.cacheWrite, authority.cost),
665
706
  ...(longContextCost ? { longContext: longContextCost } : {}),
666
707
  ...(timeBasedCost ? { timeBased: timeBasedCost } : {}),
667
708
  },
@@ -678,8 +719,12 @@ function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamic
678
719
  } as ModelSpec<TApi>);
679
720
  }
680
721
 
681
- function preferDiscoveryCost(discoveryCost: number, fallbackCost: number): number {
682
- if (Number.isFinite(discoveryCost) && discoveryCost > 0) {
722
+ /**
723
+ * A zero discovery price usually means "not reported", so the fallback wins
724
+ * unless the provider declared its live prices authoritative (`zeroIsPrice`).
725
+ */
726
+ function preferDiscoveryCost(discoveryCost: number, fallbackCost: number, zeroIsPrice: boolean): number {
727
+ if (Number.isFinite(discoveryCost) && (discoveryCost > 0 || (zeroIsPrice && discoveryCost === 0))) {
683
728
  return discoveryCost;
684
729
  }
685
730
  return fallbackCost;
@@ -708,19 +753,40 @@ function preferDiscoveryLimit(discoveryLimit: number | null, fallbackLimit: numb
708
753
  return discoveryLimit;
709
754
  }
710
755
 
756
+ /**
757
+ * Materializes a remote source's rows. A row `buildModel` rejects (an id newer
758
+ * than this build's rule tree that lands on an equal-rank overlap) is skipped
759
+ * with a warning instead of discarding the source's other rows.
760
+ *
761
+ * @throws the first build error when rows were present but none built, so the
762
+ * caller treats the source as failed (cache/static fallback) rather than as an
763
+ * intentionally empty catalog that an authoritative provider would prune to.
764
+ */
711
765
  function buildDiscoveredModelSpecs<TApi extends Api>(value: unknown): DiscoveredModelSet<TApi> {
712
766
  const models: Model<TApi>[] = [];
713
767
  const explicitKindModels = new Set<Model<TApi>>();
714
768
  if (!Array.isArray(value)) {
715
769
  return { models, explicitKindModels };
716
770
  }
771
+ const failures: unknown[] = [];
717
772
  for (const item of value) {
718
- if (isModelLike(item)) {
719
- const model = buildModel(item as ModelSpec<TApi>);
720
- models.push(model);
721
- if (item.kind !== undefined) explicitKindModels.add(model);
773
+ if (!isModelLike(item)) continue;
774
+ let model: Model<TApi>;
775
+ try {
776
+ model = buildModel(item as ModelSpec<TApi>);
777
+ } catch (error) {
778
+ failures.push(error);
779
+ logger.warn("model discovery skipped a model that failed to build", {
780
+ provider: item.provider,
781
+ model: item.id,
782
+ error: error instanceof Error ? error.message : String(error),
783
+ });
784
+ continue;
722
785
  }
786
+ models.push(model);
787
+ if (item.kind !== undefined) explicitKindModels.add(model);
723
788
  }
789
+ if (models.length === 0 && failures.length > 0) throw failures[0];
724
790
  return { models, explicitKindModels };
725
791
  }
726
792
 
@@ -27,6 +27,9 @@ function claudeTokenizer(identity: ModelIdentity): ModelTokenizer | undefined {
27
27
  if (identity.family === "sonnet" || identity.family === "fable" || identity.family === "mythos") {
28
28
  return revisionAtLeast(identity.revision, "5") ? "claude-v5-sonnet" : "claude-v3";
29
29
  }
30
+ // Haiku 5.5 moved to the post-4.7 tokenizer (~30% more tokens than Haiku
31
+ // 4.5). No Haiku-measured vocabulary is embedded; Sonnet 5's is the nearest.
32
+ if (identity.family === "haiku" && revisionAtLeast(identity.revision, "5.5")) return "claude-v5-sonnet";
30
33
  return "claude-v3";
31
34
  }
32
35