@linxiraos/pi-catalog 1.2.1 → 1.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +4 -2
  3. package/THIRD-PARTY-NOTICES.txt +55 -5
  4. package/dist/types/build.d.ts +3 -2
  5. package/dist/types/compat/auth-ids.d.ts +2 -2
  6. package/dist/types/compat/axes.d.ts +1 -1
  7. package/dist/types/compat/behavior.d.ts +5 -0
  8. package/dist/types/compat/catalog-endpoint.d.ts +8 -0
  9. package/dist/types/compat/image-tokenization.d.ts +9 -1
  10. package/dist/types/compat/judgment-images.d.ts +25 -0
  11. package/dist/types/compat/provider-ids.d.ts +1 -1
  12. package/dist/types/compat/request-compression.d.ts +23 -0
  13. package/dist/types/compat/request-size.d.ts +12 -3
  14. package/dist/types/compat/thinking-replay.d.ts +9 -0
  15. package/dist/types/compat/types.d.ts +4 -0
  16. package/dist/types/discovery/antigravity.d.ts +7 -0
  17. package/dist/types/model-cache.d.ts +7 -0
  18. package/dist/types/model-manager.d.ts +6 -0
  19. package/dist/types/model-thinking.d.ts +11 -1
  20. package/dist/types/models.d.ts +16 -1
  21. package/dist/types/pricing.d.ts +37 -1
  22. package/dist/types/provider-models/openai-compat.d.ts +18 -46
  23. package/dist/types/provider-models/special.d.ts +6 -1
  24. package/dist/types/types.d.ts +63 -12
  25. package/dist/types/wire/codex.d.ts +2 -0
  26. package/package.json +5 -5
  27. package/src/build.d.ts +3 -2
  28. package/src/build.ts +159 -80
  29. package/src/compat/auth-ids.ts +2 -0
  30. package/src/compat/axes.d.ts +1 -1
  31. package/src/compat/axes.ts +129 -4
  32. package/src/compat/behavior.d.ts +5 -0
  33. package/src/compat/behavior.ts +9 -1
  34. package/src/compat/cascade.ts +4 -1
  35. package/src/compat/catalog-endpoint.ts +43 -0
  36. package/src/compat/image-tokenization.ts +28 -10
  37. package/src/compat/judgment-images.ts +83 -0
  38. package/src/compat/provider-ids.d.ts +1 -1
  39. package/src/compat/provider-ids.ts +1 -0
  40. package/src/compat/request-compression.ts +43 -0
  41. package/src/compat/request-size.ts +20 -5
  42. package/src/compat/resolve.ts +90 -53
  43. package/src/compat/rules/README.md +32 -3
  44. package/src/compat/rules/auth/_order.kdl +1 -1
  45. package/src/compat/rules/auth/google-antigravity.kdl +1 -1
  46. package/src/compat/rules/auth/sambanova.kdl +9 -0
  47. package/src/compat/rules/auth/stencil.kdl +1 -1
  48. package/src/compat/rules/classes/anthropic.kdl +42 -2
  49. package/src/compat/rules/classes/deepseek.kdl +4 -0
  50. package/src/compat/rules/classes/gemini.kdl +46 -2
  51. package/src/compat/rules/classes/meta.kdl +7 -0
  52. package/src/compat/rules/classes/minimax.kdl +11 -1
  53. package/src/compat/rules/classes/openai.kdl +34 -3
  54. package/src/compat/rules/classes/xai.kdl +26 -0
  55. package/src/compat/rules/providers/amazon-bedrock.kdl +1 -0
  56. package/src/compat/rules/providers/anthropic.kdl +23 -0
  57. package/src/compat/rules/providers/azure.kdl +2 -0
  58. package/src/compat/rules/providers/cerebras.kdl +3 -0
  59. package/src/compat/rules/providers/cline-pass.kdl +16 -0
  60. package/src/compat/rules/providers/commandcode.kdl +10 -0
  61. package/src/compat/rules/providers/cursor.kdl +50 -1
  62. package/src/compat/rules/providers/deepinfra.kdl +3 -1
  63. package/src/compat/rules/providers/devin.kdl +117 -6
  64. package/src/compat/rules/providers/factory-droid.kdl +6 -0
  65. package/src/compat/rules/providers/firepass.kdl +15 -0
  66. package/src/compat/rules/providers/fireworks.kdl +26 -0
  67. package/src/compat/rules/providers/github-copilot.kdl +24 -0
  68. package/src/compat/rules/providers/google-antigravity.kdl +37 -0
  69. package/src/compat/rules/providers/google-gemini-cli.kdl +1 -0
  70. package/src/compat/rules/providers/google-vertex.kdl +5 -0
  71. package/src/compat/rules/providers/google.kdl +1 -0
  72. package/src/compat/rules/providers/image-tokenization.kdl +2 -1
  73. package/src/compat/rules/providers/local.kdl +8 -1
  74. package/src/compat/rules/providers/meta.kdl +16 -0
  75. package/src/compat/rules/providers/moonshot.kdl +2 -0
  76. package/src/compat/rules/providers/muse-code.kdl +49 -0
  77. package/src/compat/rules/providers/native-tools.kdl +3 -0
  78. package/src/compat/rules/providers/ollama-cloud.kdl +10 -0
  79. package/src/compat/rules/providers/openai-codex.kdl +29 -5
  80. package/src/compat/rules/providers/openai.kdl +1 -0
  81. package/src/compat/rules/providers/opencode-go.kdl +35 -0
  82. package/src/compat/rules/providers/opencode-zen.kdl +4 -0
  83. package/src/compat/rules/providers/openrouter.kdl +27 -1
  84. package/src/compat/rules/providers/sambanova.kdl +60 -0
  85. package/src/compat/rules/providers/tool-call-names.kdl +37 -0
  86. package/src/compat/rules/providers/typesafe.kdl +22 -1
  87. package/src/compat/rules/providers/umans.kdl +7 -0
  88. package/src/compat/rules/providers/venice.kdl +7 -0
  89. package/src/compat/rules/providers/web.kdl +7 -0
  90. package/src/compat/rules/providers/xai-oauth.kdl +16 -6
  91. package/src/compat/rules/providers/xai.kdl +3 -0
  92. package/src/compat/rules/providers/zai.kdl +5 -0
  93. package/src/compat/rules/runtime/behavior.kdl +129 -8
  94. package/src/compat/rules/taxonomy/_collapse.kdl +11 -0
  95. package/src/compat/rules/taxonomy/gemma.kdl +1 -0
  96. package/src/compat/rules/taxonomy/meta.kdl +7 -0
  97. package/src/compat/rules/taxonomy/openai.kdl +2 -0
  98. package/src/compat/rules.json +1 -1
  99. package/src/compat/thinking-replay.ts +33 -0
  100. package/src/compat/types.d.ts +4 -0
  101. package/src/compat/types.ts +4 -0
  102. package/src/discovery/antigravity.d.ts +7 -0
  103. package/src/discovery/antigravity.ts +13 -0
  104. package/src/discovery/codex.ts +0 -4
  105. package/src/discovery/cursor.ts +28 -20
  106. package/src/discovery/devin.ts +7 -8
  107. package/src/model-cache.d.ts +7 -0
  108. package/src/model-cache.ts +8 -0
  109. package/src/model-manager.d.ts +6 -0
  110. package/src/model-manager.ts +41 -21
  111. package/src/model-thinking.d.ts +80 -0
  112. package/src/model-thinking.ts +13 -1
  113. package/src/model-tokenizer.ts +7 -1
  114. package/src/models.d.ts +16 -1
  115. package/src/models.json +1 -1
  116. package/src/models.ts +26 -12
  117. package/src/pricing.d.ts +37 -1
  118. package/src/pricing.ts +123 -12
  119. package/src/provider-models/bundled-references.ts +5 -2
  120. package/src/provider-models/cache-provider-id.ts +1 -1
  121. package/src/provider-models/descriptors.ts +2 -0
  122. package/src/provider-models/google.ts +7 -1
  123. package/src/provider-models/openai-compat.d.ts +18 -46
  124. package/src/provider-models/openai-compat.ts +188 -104
  125. package/src/provider-models/special.d.ts +6 -1
  126. package/src/provider-models/special.ts +15 -9
  127. package/src/types.d.ts +63 -12
  128. package/src/types.ts +71 -9
  129. package/src/wire/codex.d.ts +2 -0
  130. package/src/wire/codex.ts +17 -0
@@ -39,8 +39,17 @@ import {
39
39
  type OpenAICompat,
40
40
  type Provider,
41
41
  type ThinkingConfig,
42
+ type TokenCost,
42
43
  } from "../types";
43
- import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
44
+ import {
45
+ discoveryFetch,
46
+ isAnthropicOAuthToken,
47
+ isRecord,
48
+ toBoolean,
49
+ toNumber,
50
+ toPositiveNumber,
51
+ toPositiveNumberOrNull,
52
+ } from "../utils";
44
53
  import { ALIBABA_TOKEN_PLAN_BASE_URL, parseAlibabaTokenPlanCredential } from "../wire/alibaba-token-plan";
45
54
  import { normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
46
55
  import { CLINEPASS_API_BASE_URL, clinePassClientHeaders } from "../wire/cline-pass";
@@ -550,6 +559,40 @@ function mapWithBundledReference<TApi extends Api>(
550
559
  };
551
560
  }
552
561
 
562
+ /**
563
+ * Rates from an OpenRouter-shaped `pricing` object (USD per token, as decimal
564
+ * strings). Missing, zero, or negative legs (`-1` marks router-priced rows)
565
+ * map to 0, which the model manager fills from its reference sources.
566
+ */
567
+ function perTokenPricingCost(pricing: unknown): TokenCost {
568
+ const rates = isRecord(pricing) ? pricing : {};
569
+ return {
570
+ input: toPositiveNumber(rates.prompt, 0) * 1_000_000,
571
+ output: toPositiveNumber(rates.completion, 0) * 1_000_000,
572
+ cacheRead: toPositiveNumber(rates.input_cache_read, 0) * 1_000_000,
573
+ cacheWrite: toPositiveNumber(rates.input_cache_write, 0) * 1_000_000,
574
+ };
575
+ }
576
+
577
+ /**
578
+ * Rates from NanoGPT's detailed roster: prompt and completion are USD per
579
+ * million tokens, the cache legs per thousand. Legs the row does not price
580
+ * keep `fallback`.
581
+ */
582
+ function nanoGptPricingCost(pricing: unknown, fallback: TokenCost): TokenCost {
583
+ if (!isRecord(pricing) || pricing.unit !== "per_million_tokens") return fallback;
584
+ const perThousand = (value: unknown, fallbackRate: number): number => {
585
+ const rate = toPositiveNumber(value, 0);
586
+ return rate > 0 ? rate * 1_000 : fallbackRate;
587
+ };
588
+ return {
589
+ input: toPositiveNumber(pricing.prompt, fallback.input),
590
+ output: toPositiveNumber(pricing.completion, fallback.output),
591
+ cacheRead: perThousand(pricing.cacheReadInputPer1kTokens, fallback.cacheRead),
592
+ cacheWrite: perThousand(pricing.cacheWriteInputPer1kTokens, fallback.cacheWrite),
593
+ };
594
+ }
595
+
553
596
  function normalizeAnthropicBaseUrl(baseUrl: string | undefined, fallback: string): string {
554
597
  const value = baseUrl?.trim();
555
598
  if (!value) {
@@ -1265,6 +1308,87 @@ export function cerebrasModelManagerOptions(
1265
1308
  });
1266
1309
  }
1267
1310
 
1311
+ // ---------------------------------------------------------------------------
1312
+ // 3.5 SambaNova
1313
+ // ---------------------------------------------------------------------------
1314
+
1315
+ /** SambaNova discovery inputs: `apiKey` enables the live roster; `baseUrl` defaults to SambaNova Cloud. */
1316
+ export interface SambaNovaModelManagerConfig {
1317
+ apiKey?: string;
1318
+ baseUrl?: string;
1319
+ fetch?: FetchImpl;
1320
+ }
1321
+
1322
+ /**
1323
+ * One SambaNova per-token USD rate (a decimal string) as a per-million rate.
1324
+ * `absent` is the value for a missing field; a present field that is not a
1325
+ * nonnegative number yields `undefined`.
1326
+ */
1327
+ function sambanovaRate(value: unknown, absent: number | undefined): number | undefined {
1328
+ if (value === undefined || value === null) return absent;
1329
+ const parsed = toNumber(value);
1330
+ return parsed !== undefined && parsed >= 0 ? Math.round(parsed * 1e12) / 1e6 : undefined;
1331
+ }
1332
+
1333
+ /**
1334
+ * Map SambaNova's `pricing` block. Prompt and completion rates are required;
1335
+ * cache rates default to 0 when absent. An unreadable card stays unpriced
1336
+ * (`unknown`) instead of reading as free, and an all-zero card is a declared
1337
+ * free tier.
1338
+ */
1339
+ function sambanovaPricing(
1340
+ pricing: unknown,
1341
+ fallback: TokenCost,
1342
+ ): Pick<ModelSpec<"openai-completions">, "cost" | "pricingStatus"> {
1343
+ const rates = isRecord(pricing) ? pricing : {};
1344
+ const input = sambanovaRate(rates.prompt, undefined);
1345
+ const output = sambanovaRate(rates.completion, undefined);
1346
+ const cacheRead = sambanovaRate(rates.input_cache_read, 0);
1347
+ const cacheWrite = sambanovaRate(rates.input_cache_write, 0);
1348
+ if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined) {
1349
+ return { cost: fallback, pricingStatus: "unknown" };
1350
+ }
1351
+ const cost = { input, output, cacheRead, cacheWrite };
1352
+ return input + output + cacheRead + cacheWrite === 0 ? { cost, pricingStatus: "free" } : { cost };
1353
+ }
1354
+
1355
+ /**
1356
+ * SambaNova Cloud's model manager. `/v1/models` requires a key, so a keyless
1357
+ * manager serves only the bundled seed; with a key the live roster replaces
1358
+ * it. Rows carry pricing and limits but no capability metadata: reasoning,
1359
+ * thinking wire format, and image input come from `providers/sambanova.kdl`.
1360
+ */
1361
+ export function sambanovaModelManagerOptions(
1362
+ config?: SambaNovaModelManagerConfig,
1363
+ ): ModelManagerOptions<"openai-completions"> {
1364
+ const apiKey = config?.apiKey;
1365
+ const baseUrl = config?.baseUrl ?? "https://api.sambanova.ai/v1";
1366
+ return {
1367
+ providerId: "sambanova",
1368
+ dynamicModelsAuthoritative: true,
1369
+ // The live `pricing` block is the tariff: an explicit `0` must not revert
1370
+ // to the bundled seed's rate.
1371
+ dynamicCostAuthoritative: true,
1372
+ ...(apiKey && {
1373
+ fetchDynamicModels: () =>
1374
+ fetchOpenAICompatibleModels({
1375
+ api: "openai-completions",
1376
+ provider: "sambanova",
1377
+ baseUrl,
1378
+ apiKey,
1379
+ filterModel: (_entry, model) => !isExcludedModel("sambanova", model.id),
1380
+ mapModel: (entry, defaults) => ({
1381
+ ...defaults,
1382
+ ...sambanovaPricing(entry.pricing, defaults.cost),
1383
+ contextWindow: toPositiveNumber(entry.context_length, defaults.contextWindow),
1384
+ maxTokens: toPositiveNumber(entry.max_completion_tokens, defaults.maxTokens),
1385
+ }),
1386
+ fetch: config?.fetch,
1387
+ }),
1388
+ }),
1389
+ };
1390
+ }
1391
+
1268
1392
  // ---------------------------------------------------------------------------
1269
1393
  // 4. Hugging Face
1270
1394
  // ---------------------------------------------------------------------------
@@ -2296,74 +2420,6 @@ export function zhipuCodingPlanModelManagerOptions(
2296
2420
  // 7.5 Fireworks
2297
2421
  // ---------------------------------------------------------------------------
2298
2422
 
2299
- /**
2300
- * Fireworks-published cap for the Kimi K2 family. Fireworks' `/v1/models`
2301
- * envelope generically reports `max_completion_tokens: 65536` for every Kimi
2302
- * deployment, but Kimi K2 (instruct / thinking / turbo) on Fireworks is
2303
- * documented to ship long reasoning traces that should be bounded — capping
2304
- * at 32,768 prevents handing callers a budget the router cannot honor.
2305
- * See https://github.com/can1357/oh-my-pi/issues/1849.
2306
- */
2307
- export const FIREWORKS_KIMI_MAX_TOKENS = 32_768;
2308
-
2309
- /**
2310
- * Fireworks' output ceiling for Kimi K2.7-Code specifically. Its `/v1/models`
2311
- * generic `max_completion_tokens` is 65,536 and Fireworks serves it in full —
2312
- * verified with a single completion emitting 58,971 output tokens and
2313
- * `max_tokens: 200000` accepted without error. Unlike the older K2.5/K2.6
2314
- * family (see {@link FIREWORKS_KIMI_MAX_TOKENS}), K2.7-Code is not clamped to
2315
- * 32,768; that ceiling only truncated it.
2316
- */
2317
- export const FIREWORKS_KIMI_K27_CODE_MAX_TOKENS = 65_536;
2318
-
2319
- /**
2320
- * Returns true for the Kimi K2.5 / K2.6 family served by Fireworks-backed
2321
- * providers (`fireworks` direct, `firepass` router) that share the 32,768
2322
- * `maxTokens` ceiling. Matches both the public catalog id (`kimi-k2.5`,
2323
- * `kimi-k2.6`, `kimi-k2.6-turbo`) and the canonical Fireworks wire id
2324
- * (`accounts/fireworks/{models,routers}/kimi-k2…`).
2325
- *
2326
- * K2.7-Code (incl. `-fast` / `-highspeed`) is deliberately excluded: unlike the
2327
- * earlier K2 family it serves its full context on Fireworks — verified with a
2328
- * single completion emitting 58,971 output tokens and `max_tokens: 200000`
2329
- * accepted without error — so the 32,768 cap would only truncate it. It inherits
2330
- * Fireworks' reported `max_completion_tokens` (65,536) instead.
2331
- */
2332
- export function isFireworksKimiK2ModelId(modelId: string): boolean {
2333
- const identity = classifyModel("fireworks", modelId, { lenient: true });
2334
- if (identity.class !== "kimi" || identity.family === "k2.7-code") return false;
2335
- return identity.family?.startsWith("k2") === true;
2336
- }
2337
-
2338
- /**
2339
- * Clamp the Kimi K2 family's `maxTokens` to {@link FIREWORKS_KIMI_MAX_TOKENS}
2340
- * on Fireworks-backed providers, leaving every other model untouched.
2341
- */
2342
- export function clampFireworksKimiMaxTokens(modelId: string, candidate: number): number;
2343
- export function clampFireworksKimiMaxTokens(modelId: string, candidate: number | null): number | null;
2344
- export function clampFireworksKimiMaxTokens(modelId: string, candidate: number | null): number | null {
2345
- if (candidate === null) return null;
2346
- return isFireworksKimiK2ModelId(modelId) ? Math.min(candidate, FIREWORKS_KIMI_MAX_TOKENS) : candidate;
2347
- }
2348
-
2349
- /**
2350
- * Kimi K2.7 Code's documented recommended output budget. Some provider
2351
- * discovery rows report the context-sized `max_completion_tokens` instead.
2352
- */
2353
- export const KIMI_K27_CODE_RECOMMENDED_MAX_TOKENS = 32_768;
2354
-
2355
- export function isKimiK27CodeModelId(modelId: string): boolean {
2356
- const identity = classifyModel("fireworks", modelId, { lenient: true });
2357
- return identity.class === "kimi" && identity.family === "k2.7-code";
2358
- }
2359
-
2360
- export function clampKimiK27CodeMaxTokens(modelId: string, candidate: number): number;
2361
- export function clampKimiK27CodeMaxTokens(modelId: string, candidate: number | null): number | null;
2362
- export function clampKimiK27CodeMaxTokens(modelId: string, candidate: number | null): number | null {
2363
- if (candidate === null) return null;
2364
- return isKimiK27CodeModelId(modelId) ? Math.min(candidate, KIMI_K27_CODE_RECOMMENDED_MAX_TOKENS) : candidate;
2365
- }
2366
-
2367
2423
  /**
2368
2424
  * Fireworks Fast variants we surface: the Fast table on
2369
2425
  * https://docs.fireworks.ai/serverless/serverless-modes, minus the US-only
@@ -2493,15 +2549,9 @@ function mapFireworksControlPlaneModel(
2493
2549
  const supportsImage = toBoolean(record.supportsImageInput) === true;
2494
2550
  const supportsTools = toBoolean(record.supportsTools);
2495
2551
  const contextWindow = toPositiveNumber(record.contextLength, reference?.contextWindow ?? null);
2496
- // The control plane reports no max-output budget. Default K2.7-Code to its
2497
- // verified 65,536 ceiling, the older K2.5/K2.6 family to the clamped 32,768,
2498
- // everyone else to the discovery fallback, then clamp.
2499
- const fallbackMaxTokens = isKimiK27CodeModelId(publicModelId)
2500
- ? FIREWORKS_KIMI_K27_CODE_MAX_TOKENS
2501
- : isFireworksKimiK2ModelId(publicModelId)
2502
- ? FIREWORKS_KIMI_MAX_TOKENS
2503
- : null;
2504
- const maxTokens = clampFireworksKimiMaxTokens(publicModelId, reference?.maxTokens ?? fallbackMaxTokens);
2552
+ // The control plane reports no max-output budget; Fireworks' per-model
2553
+ // output caps and fallbacks are rule-owned (providers/fireworks.kdl).
2554
+ const maxTokens = reference?.maxTokens ?? null;
2505
2555
  const base: ModelSpec<"openai-completions"> = reference ?? {
2506
2556
  id: publicModelId,
2507
2557
  name,
@@ -2941,8 +2991,9 @@ function buildClinePassFreeModel(id: string, liveCatalog?: ClinePassLiveCatalog)
2941
2991
  input: upstream?.input ?? live?.input ?? fallback.input,
2942
2992
  contextWindow: upstream?.contextWindow ?? live?.contextWindow ?? fallback.contextWindow,
2943
2993
  maxTokens: upstream?.maxTokens ?? live?.maxTokens ?? fallback.maxTokens,
2944
- // A non-reasoning reference must not carry the Cline effort ladder.
2945
- ...(reasoning ? {} : { thinking: undefined }),
2994
+ // No reasoning-effort control means no effort ladder: the cline-pass
2995
+ // rules trust only explicit thinking, so a copied ladder would surface.
2996
+ thinking: undefined,
2946
2997
  compat: { supportsReasoningEffort: false, wireModelIdMode: "raw" },
2947
2998
  };
2948
2999
  }
@@ -3215,12 +3266,18 @@ export interface OpenCodeModelManagerConfig {
3215
3266
  fetch?: FetchImpl;
3216
3267
  }
3217
3268
 
3269
+ const OPENCODE_ZEN_BASE_PATH = "https://opencode.ai/zen";
3270
+ const OPENCODE_ZEN_GOOGLE_BASE_URL = "https://opencode.ai/inference/google/v1beta";
3271
+
3218
3272
  function normalizeOpenCodeBasePath(baseUrl: string | undefined, fallbackBasePath: string): string {
3219
3273
  const value = normalizeAnthropicBaseUrl(baseUrl, fallbackBasePath);
3220
3274
  return value.endsWith("/v1") ? value.slice(0, -3) : value;
3221
3275
  }
3222
3276
 
3223
3277
  function openCodeBaseUrlForApi(api: Api, basePath: string): string {
3278
+ if (api === "google-generative-ai" && basePath === OPENCODE_ZEN_BASE_PATH) {
3279
+ return OPENCODE_ZEN_GOOGLE_BASE_URL;
3280
+ }
3224
3281
  return api === "anthropic-messages" ? basePath : `${basePath}/v1`;
3225
3282
  }
3226
3283
 
@@ -3234,7 +3291,12 @@ function openCodeBaseUrlForApi(api: Api, basePath: string): string {
3234
3291
  // rules (`runtime/behavior.kdl`; #887, #1617, #8957).
3235
3292
  // Runtime-discovered rows cached before model-identity corrections retain
3236
3293
  // stale capability metadata until the authoritative catalog TTL expires.
3237
- const OPENCODE_CACHE_MIGRATION_MODEL_IDS = ["glm-5.3-flash", "longcat-2.5-preview-free", "space-bunny-free"] as const;
3294
+ const OPENCODE_CACHE_MIGRATION_MODEL_IDS = [
3295
+ "deepseek-flash",
3296
+ "glm-5.3-flash",
3297
+ "longcat-2.5-preview-free",
3298
+ "space-bunny-free",
3299
+ ] as const;
3238
3300
  const OPENCODE_ZEN_CACHE_MIGRATION_MODEL_IDS = ["gemini-3.7-flash", "gemini-3.8-flash"] as const;
3239
3301
 
3240
3302
  // Billing-variant suffixes the OpenCode gateways append to a base model id
@@ -3269,7 +3331,10 @@ function openCodeModelManagerOptions(
3269
3331
  const apiOverrides = (id: string): Api | undefined => {
3270
3332
  // Route pins live in the `api-routes` behavior rules.
3271
3333
  const route = apiRouteFor(providerId, id)?.api;
3272
- return route === "openai-completions" || route === "openai-responses" || route === "anthropic-messages"
3334
+ return route === "openai-completions" ||
3335
+ route === "openai-responses" ||
3336
+ route === "anthropic-messages" ||
3337
+ route === "google-generative-ai"
3273
3338
  ? route
3274
3339
  : undefined;
3275
3340
  };
@@ -3893,6 +3958,7 @@ export function kiloModelManagerOptions(config?: KiloModelManagerConfig): ModelM
3893
3958
  provider: "kilo",
3894
3959
  baseUrl,
3895
3960
  apiKey,
3961
+ mapModel: (entry, defaults) => ({ ...defaults, cost: perTokenPricingCost(entry.pricing) }),
3896
3962
  fetch: config?.fetch,
3897
3963
  }),
3898
3964
  };
@@ -4570,11 +4636,7 @@ export function veniceModelManagerOptions(
4570
4636
  config,
4571
4637
  mapModel: (entry, defaults, reference) => {
4572
4638
  const model = mapWithBundledReference(entry, defaults, reference);
4573
- return {
4574
- ...model,
4575
- maxTokens: clampKimiK27CodeMaxTokens(defaults.id, model.maxTokens),
4576
- compat: { ...model.compat, supportsUsageInStreaming: false },
4577
- };
4639
+ return { ...model, compat: { ...model.compat, supportsUsageInStreaming: false } };
4578
4640
  },
4579
4641
  });
4580
4642
  }
@@ -4727,7 +4789,9 @@ function museSparkLineageSpec(id: string): ModelSpec<"openai-responses"> | undef
4727
4789
  let templateRevision: readonly [number, number, number] | undefined;
4728
4790
  for (const model of seedModels<"openai-responses">("meta")) {
4729
4791
  if ((billingVariantPlain(model.id) !== undefined) !== contributor) continue;
4730
- const candidate = parseRevision(classifyModel("meta", model.id, { lenient: true }).revision ?? "");
4792
+ const seedIdentity = classifyModel("meta", model.id, { lenient: true });
4793
+ if (seedIdentity.family !== "muse-spark") continue;
4794
+ const candidate = parseRevision(seedIdentity.revision ?? "");
4731
4795
  if (candidate === undefined) continue;
4732
4796
  if (templateRevision === undefined || compareRevision(candidate, templateRevision) > 0) {
4733
4797
  template = model;
@@ -6282,10 +6346,29 @@ export function nanoGptModelManagerOptions(
6282
6346
  provider: "nanogpt",
6283
6347
  baseUrl,
6284
6348
  apiKey,
6349
+ // Only the detailed roster carries per-model pricing, names, and limits.
6350
+ query: { detailed: "true" },
6285
6351
  mapModel: (entry, defaults) => {
6286
6352
  const reference = resolveReference(defaults.id);
6287
- const mapped = mapWithBundledReference(entry, defaults, reference);
6288
- return { ...mapped, api: "openai-completions", provider: "nanogpt" };
6353
+ // The detailed roster names its output cap `max_output_tokens`.
6354
+ const maxOutputTokens = entry.max_output_tokens ?? entry.max_completion_tokens;
6355
+ const mapped = mapWithBundledReference(
6356
+ { ...entry, max_completion_tokens: maxOutputTokens },
6357
+ reference
6358
+ ? defaults
6359
+ : {
6360
+ ...defaults,
6361
+ contextWindow: toPositiveNumberOrNull(entry.context_length),
6362
+ maxTokens: toPositiveNumberOrNull(maxOutputTokens),
6363
+ },
6364
+ reference,
6365
+ );
6366
+ return {
6367
+ ...mapped,
6368
+ api: "openai-completions",
6369
+ provider: "nanogpt",
6370
+ cost: { ...mapped.cost, ...nanoGptPricingCost(entry.pricing, mapped.cost) },
6371
+ };
6289
6372
  },
6290
6373
  filterModel: (_entry, model) => {
6291
6374
  const match = NANO_GPT_THINKING_SUFFIX_RE.exec(model.id);
@@ -6969,13 +7052,10 @@ function createOpenCodeApiResolution(
6969
7052
  rules: ApiResolutionRule[];
6970
7053
  } {
6971
7054
  const completionsBaseUrl = `${basePath}/v1`;
6972
- // Per-API base URLs on the OpenCode-style endpoint:
6973
- // - openai-completions / openai-responses / google-generative-ai → /v1
6974
- // - anthropic-messages → bare basePath (the Anthropic client appends /v1/messages)
6975
- const baseUrlForApi = (api: Api): string => (api === "anthropic-messages" ? basePath : completionsBaseUrl);
7055
+ const baseUrlForApi = (api: Api): string => openCodeBaseUrlForApi(api, basePath);
6976
7056
  // Per-id route pins live in the `api-routes` behavior rules.
6977
7057
  const overrideRules: ApiResolutionRule[] = (
6978
- ["openai-completions", "openai-responses", "anthropic-messages"] as const
7058
+ ["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"] as const
6979
7059
  ).map(api => ({
6980
7060
  matches: modelId => apiRouteFor(providerId, modelId)?.api === api,
6981
7061
  resolved: { api, baseUrl: baseUrlForApi(api) },
@@ -7004,8 +7084,17 @@ function createOpenCodeApiResolution(
7004
7084
 
7005
7085
  // Resolver rules for the models.dev descriptor path; the per-id pins live in
7006
7086
  // the `api-routes` behavior rules, which also drive the live-discovery mapper.
7007
- const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen", "opencode-zen");
7008
- const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go", "opencode-go");
7087
+ const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution(OPENCODE_ZEN_BASE_PATH, "opencode-zen");
7088
+ const OPENCODE_GO_BASE_PATH = "https://opencode.ai/inference/go";
7089
+ const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution(OPENCODE_GO_BASE_PATH, "opencode-go");
7090
+
7091
+ /**
7092
+ * OpenCode Go endpoint for one request API. The model generator re-bases
7093
+ * previous-snapshot rows with it so retained rows follow endpoint moves.
7094
+ */
7095
+ export function openCodeGoBaseUrlForApi(api: Api): string {
7096
+ return openCodeBaseUrlForApi(api, OPENCODE_GO_BASE_PATH);
7097
+ }
7009
7098
 
7010
7099
  const COPILOT_BASE_URL = "https://api.githubcopilot.com";
7011
7100
 
@@ -7383,7 +7472,7 @@ const OPENCODE_MODELS_DEV_DESCRIPTORS: readonly ModelsDevProviderDescriptor[] =
7383
7472
  OPENCODE_ZEN_API_RESOLUTION.defaultResolution,
7384
7473
  ),
7385
7474
  }),
7386
- openAiCompletionsDescriptor("opencode-go", "opencode-go", "https://opencode.ai/zen/go/v1", {
7475
+ openAiCompletionsDescriptor("opencode-go", "opencode-go", `${OPENCODE_GO_BASE_PATH}/v1`, {
7387
7476
  filterModel: filterActiveToolCallModels,
7388
7477
  resolveApi: (modelId, raw) =>
7389
7478
  resolveApiByRules(
@@ -7464,12 +7553,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDes
7464
7553
  // --- Synthetic ---
7465
7554
  openAiCompletionsDescriptor("synthetic", "synthetic", "https://api.synthetic.new/openai/v1"),
7466
7555
  // --- Venice AI ---
7467
- openAiCompletionsDescriptor("venice", "venice", "https://api.venice.ai/api/v1", {
7468
- transformModel: model => {
7469
- const maxTokens = clampKimiK27CodeMaxTokens(model.id, model.maxTokens);
7470
- return maxTokens === model.maxTokens ? model : { ...model, maxTokens };
7471
- },
7472
- }),
7556
+ openAiCompletionsDescriptor("venice", "venice", "https://api.venice.ai/api/v1"),
7473
7557
  // --- Ollama Cloud ---
7474
7558
  simpleModelsDevDescriptor("ollama-cloud", "ollama-cloud", "ollama-chat", "https://ollama.com"),
7475
7559
  // --- Xiaomi Token Plan ---
@@ -73,7 +73,12 @@ export interface TypeSafeModelManagerConfig {
73
73
  baseUrl?: string;
74
74
  fetch?: FetchImpl;
75
75
  }
76
- /** Discover account-visible judge models while keeping the bundled offline seed. */
76
+ /**
77
+ * Discover account-visible judge models while keeping the bundled offline seed. `GET /v1/models` lists
78
+ * only aliases, yet TypeSafe accepts versioned ids whether or not they are listed, so a successful
79
+ * discovery is extended with the seed rows it omits (the pinned default judge, `jev-1.14.0`) instead of
80
+ * pruning them as `dynamicModelsAuthoritative` otherwise would.
81
+ */
77
82
  export declare function typesafeModelManagerOptions(config?: TypeSafeModelManagerConfig): ModelManagerOptions<"typesafe">;
78
83
  export interface ZaiModelManagerConfig {
79
84
  }
@@ -64,6 +64,7 @@ export function openaiCodexModelManagerOptions(
64
64
  providerId: "openai-codex",
65
65
  cacheProviderId: resolveModelCacheProviderId("openai-codex", { baseUrl }),
66
66
  dynamicModelsAuthoritative: providerEntry("openai-codex")?.dynamicModelsAuthoritative === true,
67
+ keepStaticNames: providerEntry("openai-codex")?.keepBundledNames === true,
67
68
  ...(resolveAccounts
68
69
  ? {
69
70
  fetchDynamicModels: async () => {
@@ -421,25 +422,30 @@ export interface TypeSafeModelManagerConfig {
421
422
  fetch?: FetchImpl;
422
423
  }
423
424
 
424
- /** Discover account-visible judge models while keeping the bundled offline seed. */
425
+ /**
426
+ * Discover account-visible judge models while keeping the bundled offline seed. `GET /v1/models` lists
427
+ * only aliases, yet TypeSafe accepts versioned ids whether or not they are listed, so a successful
428
+ * discovery is extended with the seed rows it omits (the pinned default judge, `jev-1.14.0`) instead of
429
+ * pruning them as `dynamicModelsAuthoritative` otherwise would.
430
+ */
425
431
  export function typesafeModelManagerOptions(config: TypeSafeModelManagerConfig = {}): ModelManagerOptions<"typesafe"> {
426
432
  const { apiKey } = config;
427
433
  const envBaseUrl = Bun.env.TYPESAFE_BASE_URL?.trim();
428
434
  const baseUrl = (config.baseUrl ?? (envBaseUrl || TYPESAFE_DEFAULT_BASE_URL)).replace(/\/+$/, "");
429
- const staticModels = seedModels<"typesafe">("typesafe");
435
+ const staticModels = seedModels<"typesafe">("typesafe").map(model => ({ ...model, baseUrl }));
430
436
  return {
431
437
  providerId: "typesafe",
432
438
  cacheProviderId: resolveModelCacheProviderId("typesafe"),
433
- staticModels: staticModels.map(model => ({ ...model, baseUrl })),
439
+ staticModels,
434
440
  ...(apiKey ? { dynamicModelsAuthoritative: true } : undefined),
435
441
  ...(apiKey
436
442
  ? {
437
- fetchDynamicModels: () =>
438
- fetchTypeSafeModels({
439
- apiKey,
440
- baseUrl,
441
- fetch: config.fetch,
442
- }),
443
+ fetchDynamicModels: async () => {
444
+ const discovered = await fetchTypeSafeModels({ apiKey, baseUrl, fetch: config.fetch });
445
+ if (!discovered) return discovered;
446
+ const listed = new Set(discovered.map(model => model.id));
447
+ return [...discovered, ...staticModels.filter(model => !listed.has(model.id))];
448
+ },
443
449
  }
444
450
  : undefined),
445
451
  };