@linxiraos/pi-catalog 1.1.26 → 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 (128) hide show
  1. package/CHANGELOG.md +4 -6
  2. package/dist/types/compat/auth-ids.d.ts +2 -2
  3. package/dist/types/compat/collapse.d.ts +7 -0
  4. package/dist/types/compat/image-tokenization.d.ts +52 -0
  5. package/dist/types/compat/judgment-questions.d.ts +7 -0
  6. package/dist/types/compat/prompt-cache-lookback.d.ts +7 -0
  7. package/dist/types/compat/provider-ids.d.ts +1 -1
  8. package/dist/types/compat/reasoning-carry.d.ts +21 -0
  9. package/dist/types/compat/request-size.d.ts +7 -0
  10. package/dist/types/compat/taxonomy.d.ts +0 -10
  11. package/dist/types/compat/types.d.ts +4 -0
  12. package/dist/types/discovery/antigravity.d.ts +13 -3
  13. package/dist/types/hosts.d.ts +4 -0
  14. package/dist/types/model-cache.d.ts +6 -0
  15. package/dist/types/model-manager.d.ts +16 -0
  16. package/dist/types/provider-models/account-access.d.ts +11 -0
  17. package/dist/types/provider-models/google.d.ts +23 -1
  18. package/dist/types/provider-models/openai-compat.d.ts +19 -1
  19. package/dist/types/provider-models/special.d.ts +8 -0
  20. package/dist/types/types.d.ts +134 -23
  21. package/dist/types/wire/cloudflare-ai-gateway.d.ts +0 -10
  22. package/dist/types/wire/cloudflare.d.ts +10 -0
  23. package/dist/types/wire/factory-droid.d.ts +1 -1
  24. package/dist/types/wire/gemini-headers.d.ts +6 -3
  25. package/package.json +4 -4
  26. package/src/build.ts +39 -35
  27. package/src/compat/auth-ids.ts +6 -0
  28. package/src/compat/axes.ts +43 -5
  29. package/src/compat/cascade.ts +6 -1
  30. package/src/compat/collapse.d.ts +7 -0
  31. package/src/compat/collapse.ts +11 -0
  32. package/src/compat/image-tokenization.ts +166 -0
  33. package/src/compat/judgment-questions.ts +12 -0
  34. package/src/compat/prompt-cache-lookback.ts +23 -0
  35. package/src/compat/provider-ids.d.ts +1 -1
  36. package/src/compat/provider-ids.ts +3 -0
  37. package/src/compat/reasoning-carry.ts +64 -0
  38. package/src/compat/request-size.ts +25 -0
  39. package/src/compat/resolve.ts +36 -25
  40. package/src/compat/rules/README.md +13 -3
  41. package/src/compat/rules/auth/_order.kdl +2 -2
  42. package/src/compat/rules/auth/cloudflare-workers-ai.kdl +9 -0
  43. package/src/compat/rules/auth/coralbricks.kdl +13 -0
  44. package/src/compat/rules/auth/minimax-code-cn.kdl +1 -1
  45. package/src/compat/rules/auth/minimax-code.kdl +1 -1
  46. package/src/compat/rules/auth/openai-codex.kdl +1 -1
  47. package/src/compat/rules/auth/snowflake.kdl +11 -0
  48. package/src/compat/rules/auth/stencil.kdl +1 -1
  49. package/src/compat/rules/classes/anthropic.kdl +188 -7
  50. package/src/compat/rules/classes/deepseek.kdl +11 -3
  51. package/src/compat/rules/classes/gemini.kdl +12 -0
  52. package/src/compat/rules/classes/glm.kdl +17 -8
  53. package/src/compat/rules/classes/kimi.kdl +15 -0
  54. package/src/compat/rules/classes/meta.kdl +4 -4
  55. package/src/compat/rules/classes/minimax.kdl +31 -1
  56. package/src/compat/rules/classes/openai.kdl +114 -4
  57. package/src/compat/rules/classes/qwen.kdl +2 -1
  58. package/src/compat/rules/providers/abliteration.kdl +7 -0
  59. package/src/compat/rules/providers/amazon-bedrock.kdl +0 -7
  60. package/src/compat/rules/providers/anthropic.kdl +13 -0
  61. package/src/compat/rules/providers/apple.kdl +1 -0
  62. package/src/compat/rules/providers/cloudflare-workers-ai.kdl +34 -0
  63. package/src/compat/rules/providers/coralbricks.kdl +62 -0
  64. package/src/compat/rules/providers/cursor.kdl +28 -8
  65. package/src/compat/rules/providers/devin.kdl +2 -2
  66. package/src/compat/rules/providers/factory-droid.kdl +77 -13
  67. package/src/compat/rules/providers/github-copilot.kdl +29 -0
  68. package/src/compat/rules/providers/google-antigravity.kdl +8 -1
  69. package/src/compat/rules/providers/groq.kdl +21 -0
  70. package/src/compat/rules/providers/image-tokenization.kdl +42 -0
  71. package/src/compat/rules/providers/judgment-questions.kdl +53 -0
  72. package/src/compat/rules/providers/litellm.kdl +4 -1
  73. package/src/compat/rules/providers/meta.kdl +3 -3
  74. package/src/compat/rules/providers/minimax-code-cn.kdl +10 -5
  75. package/src/compat/rules/providers/minimax-code.kdl +10 -5
  76. package/src/compat/rules/providers/mistral.kdl +25 -1
  77. package/src/compat/rules/providers/moonshot.kdl +11 -0
  78. package/src/compat/rules/providers/muse-code.kdl +7 -0
  79. package/src/compat/rules/providers/ollama.kdl +3 -0
  80. package/src/compat/rules/providers/openai-codex.kdl +18 -13
  81. package/src/compat/rules/providers/openai.kdl +22 -0
  82. package/src/compat/rules/providers/opencode-go.kdl +11 -0
  83. package/src/compat/rules/providers/opencode-zen.kdl +20 -0
  84. package/src/compat/rules/providers/openrouter.kdl +6 -0
  85. package/src/compat/rules/providers/snowflake.kdl +135 -0
  86. package/src/compat/rules/providers/tool-result-images.kdl +16 -0
  87. package/src/compat/rules/providers/zhipu-coding-plan.kdl +10 -0
  88. package/src/compat/rules/runtime/behavior.kdl +8 -5
  89. package/src/compat/rules/taxonomy/_collapse.kdl +52 -2
  90. package/src/compat/rules/taxonomy/deepseek.kdl +4 -0
  91. package/src/compat/rules/taxonomy/kimi.kdl +5 -0
  92. package/src/compat/rules.json +1 -1
  93. package/src/compat/taxonomy.d.ts +0 -10
  94. package/src/compat/taxonomy.ts +3 -3
  95. package/src/compat/types.d.ts +4 -0
  96. package/src/compat/types.ts +4 -0
  97. package/src/discovery/antigravity.d.ts +13 -3
  98. package/src/discovery/antigravity.ts +33 -17
  99. package/src/discovery/cursor.ts +28 -4
  100. package/src/discovery/devin.ts +20 -2
  101. package/src/hosts.d.ts +4 -0
  102. package/src/hosts.ts +2 -0
  103. package/src/model-cache.d.ts +6 -0
  104. package/src/model-cache.ts +123 -47
  105. package/src/model-manager.d.ts +16 -0
  106. package/src/model-manager.ts +107 -41
  107. package/src/model-tokenizer.ts +3 -0
  108. package/src/models.json +1 -1
  109. package/src/provider-models/account-access.d.ts +11 -0
  110. package/src/provider-models/account-access.ts +30 -0
  111. package/src/provider-models/cache-provider-id.ts +13 -6
  112. package/src/provider-models/cline-pass.ts +33 -0
  113. package/src/provider-models/descriptors.ts +4 -0
  114. package/src/provider-models/google.d.ts +23 -1
  115. package/src/provider-models/google.ts +72 -14
  116. package/src/provider-models/openai-compat.d.ts +19 -1
  117. package/src/provider-models/openai-compat.ts +281 -42
  118. package/src/provider-models/special.d.ts +8 -0
  119. package/src/provider-models/special.ts +26 -22
  120. package/src/types.d.ts +134 -23
  121. package/src/types.ts +183 -20
  122. package/src/wire/cloudflare-ai-gateway.d.ts +0 -10
  123. package/src/wire/cloudflare-ai-gateway.ts +0 -36
  124. package/src/wire/cloudflare.ts +40 -0
  125. package/src/wire/factory-droid.d.ts +1 -1
  126. package/src/wire/factory-droid.ts +1 -1
  127. package/src/wire/gemini-headers.d.ts +6 -3
  128. package/src/wire/gemini-headers.ts +39 -21
@@ -1071,8 +1071,8 @@ export interface OpenAIModelManagerConfig {
1071
1071
  fetch?: FetchImpl;
1072
1072
  }
1073
1073
 
1074
- export function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses"> {
1075
- return createOpenAICompatibleModelManagerOptions({
1074
+ export function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<Api> {
1075
+ const options = createOpenAICompatibleModelManagerOptions({
1076
1076
  api: "openai-responses",
1077
1077
  providerId: "openai",
1078
1078
  defaultBaseUrl: OPENAI_API_BASE_URL,
@@ -1081,6 +1081,30 @@ export function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): Mo
1081
1081
  filterModel: (_entry, model, references) => isLikelyOpenAIResponsesModelId(model.id, references),
1082
1082
  mapModel: mapWithBundledReference,
1083
1083
  });
1084
+ const fetchDiscovered = options.fetchDynamicModels;
1085
+ if (!fetchDiscovered) return options;
1086
+ return {
1087
+ ...options,
1088
+ fetchDynamicModels: async () => appendUnbundledSeedModels("openai", await fetchDiscovered()),
1089
+ };
1090
+ }
1091
+
1092
+ /**
1093
+ * Append the authored seed rows that neither discovery nor the bundled catalog
1094
+ * carries yet: a seed added since the last models.json regeneration (the
1095
+ * Decisions judge row) reaches runtime through a successful discovery. Seeds
1096
+ * already bundled keep resolving through the bundled slice, and the static
1097
+ * catalog (and with it the cache fingerprint) stays the bundled one.
1098
+ */
1099
+ function appendUnbundledSeedModels(
1100
+ providerId: GeneratedProvider,
1101
+ discovered: readonly ModelSpec<Api>[] | null,
1102
+ ): readonly ModelSpec<Api>[] | null {
1103
+ if (!discovered) return null;
1104
+ const known = new Set(getBundledModels(providerId).map(model => model.id));
1105
+ for (const model of discovered) known.add(model.id);
1106
+ const seeds = seedModels(providerId).filter(seed => !known.has(seed.id));
1107
+ return seeds.length === 0 ? discovered : [...discovered, ...seeds.map(seed => ({ ...seed }))];
1084
1108
  }
1085
1109
 
1086
1110
  /**
@@ -1618,10 +1642,171 @@ export function deepinfraModelManagerOptions(
1618
1642
  return {
1619
1643
  providerId: "deepinfra",
1620
1644
  dynamicModelsAuthoritative: true,
1645
+ // `vision`/`vlm` tags are the whole truth for modality on this host.
1646
+ dynamicInputAuthoritative: true,
1621
1647
  fetchDynamicModels: () => fetchDeepinfraModels({ baseUrl, apiKey, fetch: config?.fetch, references }),
1622
1648
  };
1623
1649
  }
1624
1650
 
1651
+ // ---------------------------------------------------------------------------
1652
+ // CoralBricks
1653
+ // ---------------------------------------------------------------------------
1654
+
1655
+ export const CORALBRICKS_BASE_URL = "https://inference.coralbricks.ai/v1";
1656
+
1657
+ /** CoralBricks OpenAI-compatible discovery configuration. */
1658
+ export interface CoralbricksModelManagerConfig {
1659
+ apiKey?: string;
1660
+ baseUrl?: string;
1661
+ fetch?: FetchImpl;
1662
+ }
1663
+
1664
+ /**
1665
+ * The CoralBricks-specific fields of a `GET /v1/models` row: per-million USD
1666
+ * pricing and the capability flags Coral documents as authoritative
1667
+ * (https://www.coralbricks.ai/docs.md).
1668
+ */
1669
+ interface CoralbricksModelRecord extends OpenAICompatibleModelRecord {
1670
+ context_length?: unknown;
1671
+ pricing?: unknown;
1672
+ supports_chat?: unknown;
1673
+ supports_image_input?: unknown;
1674
+ supports_tools?: unknown;
1675
+ supports_reasoning?: unknown;
1676
+ reasoning?: unknown;
1677
+ }
1678
+
1679
+ /**
1680
+ * Read one live per-million price; a missing or negative field falls back to
1681
+ * the bundled reference. An explicit `0` is a real price: the manager keeps it
1682
+ * because `coralbricksModelManagerOptions` declares `dynamicCostAuthoritative`.
1683
+ */
1684
+ function coralRate(value: unknown, fallback: number): number {
1685
+ const parsed = toNumber(value);
1686
+ return parsed !== undefined && parsed >= 0 ? parsed : fallback;
1687
+ }
1688
+
1689
+ /**
1690
+ * Map one CoralBricks catalog row to a chat model spec; non-chat rows
1691
+ * (`supports_chat: false`) are dropped. Pricing arrives in Coral's own
1692
+ * per-million field names; `cached_input_per_m` is $0 on every model and a
1693
+ * missing field falls back to the bundled reference.
1694
+ * Live reasoning capabilities and controls override the bundled fallback.
1695
+ * The endpoint publishes no output cap, so `maxTokens` keeps its reference
1696
+ * value rather than being invented from the context window.
1697
+ */
1698
+ function mapCoralbricksModel(
1699
+ entry: CoralbricksModelRecord,
1700
+ defaults: ModelSpec<"openai-completions">,
1701
+ reference: ModelSpec<"openai-completions"> | undefined,
1702
+ ): ModelSpec<"openai-completions"> | null {
1703
+ if (entry.supports_chat === false) {
1704
+ return null;
1705
+ }
1706
+ const pricing = isRecord(entry.pricing) ? entry.pricing : {};
1707
+ // A bundled reference may lend metadata, but its runner kind is not
1708
+ // evidence the chat roster advertised it.
1709
+ const { kind: _inheritedKind, ...chatReference } = reference ?? {};
1710
+ const hasReasoningFlag = typeof entry.supports_reasoning === "boolean";
1711
+ const reasoning = hasReasoningFlag
1712
+ ? entry.supports_reasoning === true
1713
+ : (reference?.reasoning ?? defaults.reasoning);
1714
+ const controls = isRecord(entry.reasoning) ? entry.reasoning : undefined;
1715
+ const wireEfforts = controls?.supported_efforts;
1716
+ let thinking = reasoning ? reference?.thinking : undefined;
1717
+ let compat = reference?.compat;
1718
+ if (reasoning && Array.isArray(wireEfforts)) {
1719
+ const efforts = THINKING_EFFORTS.filter(effort => wireEfforts.includes(effort));
1720
+ // `none` is the server's off default, not an Effort (nor `minimal`).
1721
+ const defaultLevel = efforts.find(effort => effort === controls?.default_effort);
1722
+ thinking =
1723
+ efforts.length > 0
1724
+ ? {
1725
+ mode: "effort",
1726
+ efforts,
1727
+ ...(defaultLevel !== undefined && { defaultLevel }),
1728
+ ...(typeof controls?.mandatory === "boolean" && { requiresEffort: controls.mandatory }),
1729
+ }
1730
+ : undefined;
1731
+ }
1732
+ if (hasReasoningFlag || Array.isArray(wireEfforts)) {
1733
+ // An explicit empty/unknown vocabulary must not regrow a guessed dial
1734
+ // from identity or KDL. Missing legacy metadata still uses the reference.
1735
+ compat = { ...compat, trustExplicitThinkingOnly: true };
1736
+ }
1737
+ if (reasoning && controls) {
1738
+ if (controls.mandatory === true || controls.disable === null) {
1739
+ compat = { ...compat, reasoningDisableMode: "lowest-effort" };
1740
+ } else if (isRecord(controls.disable) && controls.disable.reasoning_effort === "none") {
1741
+ compat = { ...compat, reasoningDisableMode: "none-effort" };
1742
+ }
1743
+ }
1744
+ const input: ("text" | "image")[] =
1745
+ entry.supports_image_input === true
1746
+ ? ["text", "image"]
1747
+ : entry.supports_image_input === false
1748
+ ? ["text"]
1749
+ : (reference?.input ?? defaults.input);
1750
+ return {
1751
+ ...defaults,
1752
+ ...chatReference,
1753
+ id: defaults.id,
1754
+ name: reference?.name ?? defaults.name,
1755
+ api: defaults.api,
1756
+ provider: defaults.provider,
1757
+ baseUrl: defaults.baseUrl,
1758
+ reasoning,
1759
+ thinking,
1760
+ compat,
1761
+ input,
1762
+ ...(typeof entry.supports_tools === "boolean" ? { supportsTools: entry.supports_tools } : {}),
1763
+ cost: {
1764
+ input: coralRate(pricing.input_per_m, reference?.cost.input ?? 0),
1765
+ output: coralRate(pricing.output_per_m, reference?.cost.output ?? 0),
1766
+ cacheRead: coralRate(pricing.cached_input_per_m, reference?.cost.cacheRead ?? 0),
1767
+ cacheWrite: coralRate(pricing.cache_write_per_m, reference?.cost.cacheWrite ?? 0),
1768
+ },
1769
+ contextWindow: toPositiveNumber(entry.context_length, reference?.contextWindow ?? null),
1770
+ maxTokens: reference?.maxTokens ?? null,
1771
+ };
1772
+ }
1773
+
1774
+ /**
1775
+ * Builds CoralBricks' model-discovery manager. `/v1/models` is key-protected
1776
+ * (401 without a bearer key), so a keyless config serves only the bundled
1777
+ * reviewed seed rows; with a key, live rows are authoritative over the bundle.
1778
+ */
1779
+ export function coralbricksModelManagerOptions(
1780
+ config?: CoralbricksModelManagerConfig,
1781
+ ): ModelManagerOptions<"openai-completions"> {
1782
+ const apiKey = config?.apiKey;
1783
+ const baseUrl = config?.baseUrl ?? CORALBRICKS_BASE_URL;
1784
+ const references = createBundledReferenceMap<"openai-completions">("coralbricks");
1785
+ return {
1786
+ providerId: "coralbricks",
1787
+ dynamicModelsAuthoritative: true,
1788
+ dynamicReasoningAuthoritative: true,
1789
+ // `supports_image_input` is the row's whole truth for modality (Coral
1790
+ // answers unsupported content with `400 unsupported_content_type`).
1791
+ dynamicInputAuthoritative: true,
1792
+ // Coral's `pricing` block is the deployment tariff; an explicit live `0`
1793
+ // (a free model or no cache-write charge) must not revert to the bundle.
1794
+ dynamicCostAuthoritative: true,
1795
+ ...(apiKey && {
1796
+ fetchDynamicModels: () =>
1797
+ fetchOpenAICompatibleModels({
1798
+ api: "openai-completions",
1799
+ provider: "coralbricks",
1800
+ baseUrl,
1801
+ apiKey,
1802
+ fetch: config?.fetch,
1803
+ mapModel: (entry, defaults) =>
1804
+ mapCoralbricksModel(entry as CoralbricksModelRecord, defaults, references.get(defaults.id)),
1805
+ }),
1806
+ }),
1807
+ };
1808
+ }
1809
+
1625
1810
  // ---------------------------------------------------------------------------
1626
1811
  // 6. xAI
1627
1812
  // ---------------------------------------------------------------------------
@@ -2180,21 +2365,24 @@ export function clampKimiK27CodeMaxTokens(modelId: string, candidate: number | n
2180
2365
  }
2181
2366
 
2182
2367
  /**
2183
- * Fireworks Fast variants we surface. Each inherits the base model's
2184
- * limits/modalities/thinking and overrides only the cost with the Standard-column
2185
- * Fast prices from the Serverless pricing table; `cacheWrite` stays 0 (Fireworks
2186
- * bills no cache-write). Derived from the bundled base entries so metadata stays
2187
- * in lockstep, and the runtime auto-falls back to the base id on a failed fast
2188
- * request. See https://docs.fireworks.ai/serverless/pricing.
2368
+ * Fireworks Fast variants we surface: the Fast table on
2369
+ * https://docs.fireworks.ai/serverless/serverless-modes, minus the US-only
2370
+ * router (it needs the `us.api.fireworks.ai` host). That table, not a request
2371
+ * outcome, decides membership: add or drop a row when Fireworks changes it.
2372
+ * Each inherits the base model's limits/modalities/thinking and overrides
2373
+ * only the cost with the Fast prices from the Serverless pricing table;
2374
+ * `cacheWrite` stays 0 (Fireworks bills no cache-write). Derived from the
2375
+ * bundled base entries so metadata stays in lockstep, and the runtime
2376
+ * auto-falls back to the base id on a failed fast request.
2377
+ * See https://docs.fireworks.ai/serverless/pricing.
2189
2378
  */
2190
2379
  const FIREWORKS_FAST_VARIANT_SPECS: ReadonlyArray<{
2191
2380
  base: string;
2192
2381
  name: string;
2193
2382
  cost: { input: number; output: number; cacheRead: number };
2194
2383
  }> = [
2195
- { base: "kimi-k2.7-code", name: "Kimi K2.7 Code Fast", cost: { input: 1.9, output: 8, cacheRead: 0.38 } },
2196
- { base: "kimi-k2.6", name: "Kimi K2.6 Fast", cost: { input: 2, output: 8, cacheRead: 0.3 } },
2197
- { base: "glm-5.1", name: "GLM-5.1 Fast", cost: { input: 2.8, output: 8.8, cacheRead: 0.52 } },
2384
+ { base: "kimi-k3", name: "Kimi K3 Fast", cost: { input: 4.5, output: 22.5, cacheRead: 0.45 } },
2385
+ { base: "glm-5.3", name: "GLM-5.3 Fast", cost: { input: 2.1, output: 6.6, cacheRead: 0.39 } },
2198
2386
  { base: "glm-5.2", name: "GLM-5.2 Fast", cost: { input: 2.1, output: 6.6, cacheRead: 0.21 } },
2199
2387
  ];
2200
2388
 
@@ -2299,6 +2487,7 @@ function mapFireworksControlPlaneModel(
2299
2487
  publicModelId: string,
2300
2488
  reference: ModelSpec<"openai-completions"> | undefined,
2301
2489
  baseUrl: string,
2490
+ cost: ModelSpec<"openai-completions">["cost"] | undefined,
2302
2491
  ): ModelSpec<"openai-completions"> {
2303
2492
  const name = toModelName(record.displayName, reference?.name ?? publicModelId);
2304
2493
  const supportsImage = toBoolean(record.supportsImageInput) === true;
@@ -2332,6 +2521,7 @@ function mapFireworksControlPlaneModel(
2332
2521
  provider: "fireworks",
2333
2522
  baseUrl,
2334
2523
  name,
2524
+ cost: cost ?? base.cost,
2335
2525
  // The control plane exposes capability flags but no reasoning bit. Every
2336
2526
  // serverless chat LLM Fireworks ships reasons, and `buildModel` derives
2337
2527
  // the Fireworks effort map from the id at build time — so default
@@ -2355,6 +2545,7 @@ async function fetchFireworksServerlessModels(options: {
2355
2545
  baseUrl: string;
2356
2546
  apiKey: string;
2357
2547
  resolveReference: (publicModelId: string) => ModelSpec<"openai-completions"> | undefined;
2548
+ resolveCost: (publicModelId: string) => ModelSpec<"openai-completions">["cost"] | undefined;
2358
2549
  fetch?: FetchImpl;
2359
2550
  }): Promise<ModelSpec<"openai-completions">[] | null> {
2360
2551
  const listUrl = toFireworksControlPlaneModelsUrl(options.baseUrl, FIREWORKS_CONTROL_PLANE_ACCOUNT);
@@ -2401,6 +2592,7 @@ async function fetchFireworksServerlessModels(options: {
2401
2592
  publicModelId,
2402
2593
  options.resolveReference(publicModelId),
2403
2594
  options.baseUrl,
2595
+ options.resolveCost(publicModelId),
2404
2596
  ),
2405
2597
  );
2406
2598
  }
@@ -2449,6 +2641,39 @@ async function loadModelsDevReferences<TApi extends Api>(fetchImpl?: FetchImpl):
2449
2641
  return new Map<string, ModelSpec<TApi>>();
2450
2642
  }
2451
2643
  }
2644
+
2645
+ /**
2646
+ * Fireworks' own models.dev rows, consulted only for pricing during dynamic
2647
+ * discovery. A bare-id reference comes from whichever host carries the id with
2648
+ * the largest window, so its price is often another host's, and Fireworks-only
2649
+ * models have none. models.dev keys these rows by wire id
2650
+ * (`accounts/fireworks/models/glm-5p3`), so they are re-keyed to public ids.
2651
+ * Rows that publish no price are skipped so the reference price stays; a
2652
+ * published zero is Fireworks' price and wins. Absent from
2653
+ * `MODELS_DEV_PROVIDER_DESCRIPTORS`: the control plane alone decides which
2654
+ * models exist.
2655
+ */
2656
+ const FIREWORKS_MODELS_DEV_DESCRIPTORS: readonly ModelsDevProviderDescriptor[] = [
2657
+ openAiCompletionsDescriptor("fireworks-ai", "fireworks", "https://api.fireworks.ai/inference/v1", {
2658
+ // `mapModelsDevToModels` maps an absent price to zeros, so tell them apart on the raw row.
2659
+ filterModel: (_id, raw) => typeof raw.cost?.input === "number" || typeof raw.cost?.output === "number",
2660
+ }),
2661
+ ];
2662
+
2663
+ async function loadFireworksModelsDevCosts(
2664
+ fetchImpl?: FetchImpl,
2665
+ ): Promise<Map<string, ModelSpec<"openai-completions">["cost"]>> {
2666
+ const costs = new Map<string, ModelSpec<"openai-completions">["cost"]>();
2667
+ try {
2668
+ const payload = await fetchWellKnownModels(fetchImpl);
2669
+ for (const model of mapModelsDevToModels(payload as Record<string, unknown>, FIREWORKS_MODELS_DEV_DESCRIPTORS)) {
2670
+ costs.set(toFireworksPublicModelId(model.id), model.cost);
2671
+ }
2672
+ } catch {
2673
+ // Optional enrichment: without it, discovered rows keep their reference price.
2674
+ }
2675
+ return costs;
2676
+ }
2452
2677
  export function fireworksModelManagerOptions(
2453
2678
  config?: FireworksModelManagerConfig,
2454
2679
  ): ModelManagerOptions<"openai-completions"> {
@@ -2461,12 +2686,17 @@ export function fireworksModelManagerOptions(
2461
2686
  providerId: "fireworks",
2462
2687
  ...(apiKey && {
2463
2688
  fetchDynamicModels: async () => {
2464
- const modelsDevReferences = await loadModelsDevReferences<"openai-completions">(config?.fetch);
2689
+ // Both loaders share one in-flight models.dev request.
2690
+ const [modelsDevReferences, fireworksCosts] = await Promise.all([
2691
+ loadModelsDevReferences<"openai-completions">(config?.fetch),
2692
+ loadFireworksModelsDevCosts(config?.fetch),
2693
+ ]);
2465
2694
  return fetchFireworksServerlessModels({
2466
2695
  baseUrl,
2467
2696
  apiKey,
2468
2697
  resolveReference: publicModelId =>
2469
2698
  modelsDevReferences.get(publicModelId) ?? bundledReferences(publicModelId),
2699
+ resolveCost: publicModelId => fireworksCosts.get(publicModelId),
2470
2700
  fetch: config?.fetch,
2471
2701
  });
2472
2702
  },
@@ -3004,7 +3234,7 @@ function openCodeBaseUrlForApi(api: Api, basePath: string): string {
3004
3234
  // rules (`runtime/behavior.kdl`; #887, #1617, #8957).
3005
3235
  // Runtime-discovered rows cached before model-identity corrections retain
3006
3236
  // stale capability metadata until the authoritative catalog TTL expires.
3007
- const OPENCODE_CACHE_MIGRATION_MODEL_IDS = ["glm-5.3-flash"] as const;
3237
+ const OPENCODE_CACHE_MIGRATION_MODEL_IDS = ["glm-5.3-flash", "longcat-2.5-preview-free", "space-bunny-free"] as const;
3008
3238
  const OPENCODE_ZEN_CACHE_MIGRATION_MODEL_IDS = ["gemini-3.7-flash", "gemini-3.8-flash"] as const;
3009
3239
 
3010
3240
  // Billing-variant suffixes the OpenCode gateways append to a base model id
@@ -3093,8 +3323,8 @@ function openCodeModelManagerOptions(
3093
3323
  },
3094
3324
  },
3095
3325
  ...(apiKey && {
3096
- fetchDynamicModels: () =>
3097
- fetchOpenAICompatibleModels<Api>({
3326
+ fetchDynamicModels: async () => {
3327
+ const discovered = await fetchOpenAICompatibleModels<Api>({
3098
3328
  api: "openai-completions",
3099
3329
  provider: providerId,
3100
3330
  baseUrl: discoveryBaseUrl,
@@ -3152,7 +3382,12 @@ function openCodeModelManagerOptions(
3152
3382
  };
3153
3383
  },
3154
3384
  fetch: config?.fetch,
3155
- }),
3385
+ });
3386
+ if (!discovered) return null;
3387
+ const seeds = seedModels(providerId).map(seed => ({ ...seed, baseUrl: basePath }));
3388
+ const seedIds = new Set(seeds.map(seed => seed.id));
3389
+ return [...discovered.filter(model => !seedIds.has(model.id)), ...seeds];
3390
+ },
3156
3391
  }),
3157
3392
  };
3158
3393
  }
@@ -3245,8 +3480,9 @@ export interface OpenRouterModelManagerConfig {
3245
3480
  * OpenRouter's Decisions API lives at `/api/alpha`, a sibling of the `/api/v1`
3246
3481
  * chat root; derive it so a custom gateway base URL keeps both aligned.
3247
3482
  */
3248
- function openrouterDecisionsBaseUrl(chatBaseUrl: string): string {
3249
- return chatBaseUrl.endsWith("/v1") ? `${chatBaseUrl.slice(0, -"/v1".length)}/alpha` : `${chatBaseUrl}/alpha`;
3483
+ export function openrouterDecisionsBaseUrl(chatBaseUrl: string): string {
3484
+ const base = chatBaseUrl.replace(/\/+$/g, "");
3485
+ return base.endsWith("/v1") ? `${base.slice(0, -"/v1".length)}/alpha` : `${base}/alpha`;
3250
3486
  }
3251
3487
 
3252
3488
  function mapOpenRouterThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {
@@ -4219,6 +4455,7 @@ export function syntheticModelManagerOptions(
4219
4455
  return {
4220
4456
  providerId: "synthetic",
4221
4457
  dynamicModelsAuthoritative: true,
4458
+ dynamicReasoningAuthoritative: true,
4222
4459
  ...(apiKey && {
4223
4460
  fetchDynamicModels: () =>
4224
4461
  fetchOpenAICompatibleModels({
@@ -4477,8 +4714,8 @@ const META_MUSE_MODEL_BY_ID: Partial<Record<string, ModelSpec<"openai-responses"
4477
4714
  * text-only model with no limits. Only ids that classify into the
4478
4715
  * `muse-spark` family with a revision qualify. The template's explicit
4479
4716
  * `thinking` is dropped: only reviewed seed rows may advertise `max` (1.3
4480
- * standard), so an unknown revision takes the provider's five-tier ladder
4481
- * from `providers/meta.kdl` at build time.
4717
+ * standard and contributor), so an unknown revision takes the provider's
4718
+ * five-tier ladder from `providers/meta.kdl` at build time.
4482
4719
  */
4483
4720
  function museSparkLineageSpec(id: string): ModelSpec<"openai-responses"> | undefined {
4484
4721
  const identity = classifyModel("meta", id, { lenient: true });
@@ -6142,7 +6379,7 @@ function parseCopilotTokenPriceTier(value: unknown): CopilotTokenPriceTier | und
6142
6379
  return undefined;
6143
6380
  }
6144
6381
  return {
6145
- contextMax: toNumber(value.context_max),
6382
+ contextMax: toNumber(value.max_prompt_tokens) ?? toNumber(value.context_max),
6146
6383
  inputPrice: toNumber(value.input_price),
6147
6384
  outputPrice: toNumber(value.output_price),
6148
6385
  cachePrice: toNumber(value.cache_price),
@@ -6258,6 +6495,9 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
6258
6495
  return {
6259
6496
  providerId: "github-copilot",
6260
6497
  cacheProviderId: resolveModelCacheProviderId("github-copilot", { apiKey: rawApiKey, baseUrl }),
6498
+ // Copilot discovery pre-applies the correct image fallback for omitted
6499
+ // `supports.vision`; the live row's modality is authoritative.
6500
+ dynamicInputAuthoritative: true,
6261
6501
  dropCachedModelIdsOnStaticMismatch: COPILOT_CACHE_INVALIDATED_MODEL_IDS,
6262
6502
  // COPILOT_API_HEADERS are compile-time wire identity constants, not
6263
6503
  // credentials. The cache omits all request headers for
@@ -6328,11 +6568,21 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
6328
6568
  ? ["text"]
6329
6569
  : (reference?.input ?? defaults.input);
6330
6570
  // With COPILOT_API_HEADERS the served window is the long-context
6331
- // ceiling; the default tier ends at token_prices.default.context_max
6332
- // prompt tokens. Cap the base entry to the default tier — the long
6333
- // tier is the opt-in `-1m` sibling below.
6571
+ // ceiling; the default tier reports its prompt boundary in
6572
+ // token_prices.default.max_prompt_tokens (or legacy context_max).
6573
+ // Cap the base entry to the default tier — the long tier is
6574
+ // the opt-in `-1m` sibling below. On tiered legacy rows the
6575
+ // model-wide max_prompt_tokens may be tighter than the billed
6576
+ // default ceiling (#13912), so use the smaller bound.
6334
6577
  const tokenPrices = extractCopilotTokenPrices(entry);
6335
- const defaultContextMax = tokenPrices.defaultTier?.contextMax;
6578
+ const billedDefaultMax = tokenPrices.defaultTier?.contextMax;
6579
+ const tieredPromptBudget =
6580
+ (tokenPrices.longContext?.contextMax ?? 0) > 0 ? (copilotLimits.maxPromptTokens ?? 0) : 0;
6581
+ const defaultContextMax =
6582
+ tieredPromptBudget > 0 &&
6583
+ (billedDefaultMax === undefined || billedDefaultMax <= 0 || tieredPromptBudget < billedDefaultMax)
6584
+ ? tieredPromptBudget
6585
+ : billedDefaultMax;
6336
6586
  const defaultTierWindow =
6337
6587
  defaultContextMax !== undefined &&
6338
6588
  defaultContextMax > 0 &&
@@ -7077,23 +7327,12 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
7077
7327
  allowsSyntheticReasoningContentForToolCalls: false,
7078
7328
  },
7079
7329
  }),
7080
- // --- MiniMax Coding Plan ---
7081
- openAiCompletionsDescriptor("minimax-coding-plan", "minimax-code", "https://api.minimax.io/v1", {
7082
- compat: {
7083
- supportsStore: false,
7084
- supportsDeveloperRole: false,
7085
- supportsReasoningEffort: false,
7086
- reasoningContentField: "reasoning_content",
7087
- },
7088
- }),
7089
- openAiCompletionsDescriptor("minimax-cn-coding-plan", "minimax-code-cn", "https://api.minimaxi.com/v1", {
7090
- compat: {
7091
- supportsStore: false,
7092
- supportsDeveloperRole: false,
7093
- supportsReasoningEffort: false,
7094
- reasoningContentField: "reasoning_content",
7095
- },
7096
- }),
7330
+ // --- MiniMax Token Plan ---
7331
+ // MiniMax documents its Anthropic-compatible API as the recommended
7332
+ // protocol: signed thinking blocks, `output_config.effort` depth control,
7333
+ // and prompt-cache usage reporting.
7334
+ anthropicMessagesDescriptor("minimax-coding-plan", "minimax-code", "https://api.minimax.io/anthropic"),
7335
+ anthropicMessagesDescriptor("minimax-cn-coding-plan", "minimax-code-cn", "https://api.minimaxi.com/anthropic"),
7097
7336
  // --- Alibaba Coding Plan ---
7098
7337
  openAiCompletionsDescriptor(
7099
7338
  "alibaba-coding-plan",
@@ -24,6 +24,12 @@ export interface OpenAICodexModelManagerConfig {
24
24
  * keeps the previous/bundled catalog instead.
25
25
  */
26
26
  resolveAccounts?: () => Promise<readonly OpenAICodexAccount[] | null>;
27
+ /**
28
+ * Codex backend base URL (e.g. a Codex-compatible gateway from `models.yml`).
29
+ * Defaults to the official ChatGPT backend. Also scopes the discovery cache,
30
+ * so a gateway roster never serves the official endpoint and vice versa.
31
+ */
32
+ baseUrl?: string;
27
33
  clientVersion?: string;
28
34
  fetch?: FetchImpl;
29
35
  }
@@ -59,6 +65,8 @@ export declare function devinModelManagerOptions(config?: DevinModelManagerConfi
59
65
  export declare function factoryDroidModelManagerOptions(config?: FactoryDroidModelDiscoveryOptions): ModelManagerOptions<"factory-droid-agent">;
60
66
  export declare function localModelManagerOptions(): ModelManagerOptions<"local-inference">;
61
67
  export declare function webModelManagerOptions(): ModelManagerOptions<"web-search">;
68
+ /** Cloudflare Workers AI judges: the authored Clef rows; the account id resolves per request. */
69
+ export declare function cloudflareWorkersAiModelManagerOptions(): ModelManagerOptions<"workers-ai-systemone">;
62
70
  /** Credentials and endpoint overrides for the TypeSafe catalog manager. */
63
71
  export interface TypeSafeModelManagerConfig {
64
72
  apiKey?: string;
@@ -1,7 +1,7 @@
1
1
  import { logger, once } from "@linxiraos/pi-utils";
2
2
  import { buildModel } from "../build";
3
3
  import { apiRouteFor } from "../compat/behavior";
4
- import { seedModels } from "../compat/providers";
4
+ import { providerEntry, seedModels } from "../compat/providers";
5
5
  import { type CodexModelDiscoveryResult, fetchCodexModels } from "../discovery/codex";
6
6
  import type { DevinModelDiscoveryOptions } from "../discovery/devin";
7
7
  import {
@@ -15,6 +15,7 @@ import type { ModelManagerOptions } from "../model-manager";
15
15
  import { getBundledModel } from "../models";
16
16
  import type { Api, FetchImpl, Model, ModelSpec } from "../types";
17
17
  import { DEVIN_DEFAULT_BASE_URL } from "../wire/devin";
18
+ import { unionAccountCatalogs } from "./account-access";
18
19
  import { toModelSpec } from "./bundled-references";
19
20
  import { resolveModelCacheProviderId } from "./cache-provider-id";
20
21
 
@@ -45,6 +46,12 @@ export interface OpenAICodexModelManagerConfig {
45
46
  * keeps the previous/bundled catalog instead.
46
47
  */
47
48
  resolveAccounts?: () => Promise<readonly OpenAICodexAccount[] | null>;
49
+ /**
50
+ * Codex backend base URL (e.g. a Codex-compatible gateway from `models.yml`).
51
+ * Defaults to the official ChatGPT backend. Also scopes the discovery cache,
52
+ * so a gateway roster never serves the official endpoint and vice versa.
53
+ */
54
+ baseUrl?: string;
48
55
  clientVersion?: string;
49
56
  fetch?: FetchImpl;
50
57
  }
@@ -52,11 +59,11 @@ export interface OpenAICodexModelManagerConfig {
52
59
  export function openaiCodexModelManagerOptions(
53
60
  config: OpenAICodexModelManagerConfig = {},
54
61
  ): ModelManagerOptions<"openai-codex-responses"> {
55
- const { resolveAccounts, clientVersion, fetch } = config;
62
+ const { resolveAccounts, baseUrl, clientVersion, fetch } = config;
56
63
  return {
57
64
  providerId: "openai-codex",
58
- cacheProviderId: resolveModelCacheProviderId("openai-codex"),
59
- dynamicModelsAuthoritative: true,
65
+ cacheProviderId: resolveModelCacheProviderId("openai-codex", { baseUrl }),
66
+ dynamicModelsAuthoritative: providerEntry("openai-codex")?.dynamicModelsAuthoritative === true,
60
67
  ...(resolveAccounts
61
68
  ? {
62
69
  fetchDynamicModels: async () => {
@@ -68,6 +75,7 @@ export function openaiCodexModelManagerOptions(
68
75
  result: await fetchCodexModels({
69
76
  accessToken: account.accessToken,
70
77
  accountId: account.accountId,
78
+ baseUrl,
71
79
  clientVersion,
72
80
  fetchFn: fetch,
73
81
  }),
@@ -81,9 +89,8 @@ export function openaiCodexModelManagerOptions(
81
89
  }
82
90
 
83
91
  /**
84
- * Merge complete per-account Codex catalogs into one authoritative list,
85
- * deduped by model id. The first account to expose an id supplies its spec;
86
- * account access is merged from every account whose catalog lists that id.
92
+ * Merge complete per-account Codex catalogs into one authoritative list via
93
+ * {@link unionAccountCatalogs}.
87
94
  *
88
95
  * Returns `null` when any account's fetch failed transiently, so a partial list
89
96
  * cannot replace the previous or bundled authoritative catalog. An account
@@ -95,8 +102,7 @@ export function openaiCodexModelManagerOptions(
95
102
  function unionCodexModels(
96
103
  results: readonly { accountId: string | undefined; result: CodexModelDiscoveryResult | null }[],
97
104
  ): ModelSpec<"openai-codex-responses">[] | null {
98
- const byId = new Map<string, ModelSpec<"openai-codex-responses">>();
99
- let catalogs = 0;
105
+ const catalogs: ModelSpec<"openai-codex-responses">[][] = [];
100
106
  for (const { accountId, result } of results) {
101
107
  if (!result) return null;
102
108
  if (result.rejectedStatus !== undefined) {
@@ -106,20 +112,9 @@ function unionCodexModels(
106
112
  });
107
113
  continue;
108
114
  }
109
- catalogs++;
110
- for (const model of result.models) {
111
- const existing = byId.get(model.id);
112
- if (!existing) {
113
- byId.set(model.id, model);
114
- } else if (model.accountAccess) {
115
- byId.set(model.id, {
116
- ...existing,
117
- accountAccess: { ...existing.accountAccess, ...model.accountAccess },
118
- });
119
- }
120
- }
115
+ catalogs.push(result.models);
121
116
  }
122
- return catalogs > 0 ? [...byId.values()] : null;
117
+ return catalogs.length > 0 ? unionAccountCatalogs(catalogs) : null;
123
118
  }
124
119
 
125
120
  // ---------------------------------------------------------------------------
@@ -410,6 +405,15 @@ export function webModelManagerOptions(): ModelManagerOptions<"web-search"> {
410
405
  };
411
406
  }
412
407
 
408
+ /** Cloudflare Workers AI judges: the authored Clef rows; the account id resolves per request. */
409
+ export function cloudflareWorkersAiModelManagerOptions(): ModelManagerOptions<"workers-ai-systemone"> {
410
+ return {
411
+ providerId: "cloudflare-workers-ai",
412
+ cacheProviderId: resolveModelCacheProviderId("cloudflare-workers-ai"),
413
+ staticModels: seedModels<"workers-ai-systemone">("cloudflare-workers-ai"),
414
+ };
415
+ }
416
+
413
417
  /** Credentials and endpoint overrides for the TypeSafe catalog manager. */
414
418
  export interface TypeSafeModelManagerConfig {
415
419
  apiKey?: string;