@linxiraos/pi-catalog 1.1.7 → 1.1.8

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 (287) hide show
  1. package/CHANGELOG.md +8 -8
  2. package/README.md +9 -8
  3. package/dist/types/build.d.ts +13 -8
  4. package/dist/types/cline-pass-model-id.d.ts +4 -0
  5. package/dist/types/compat/anthropic.d.ts +10 -4
  6. package/dist/types/compat/auth-ids.d.ts +4 -0
  7. package/dist/types/compat/auth.d.ts +7 -0
  8. package/dist/types/compat/axes.d.ts +50 -0
  9. package/dist/types/compat/behavior.d.ts +69 -0
  10. package/dist/types/compat/cascade.d.ts +37 -0
  11. package/dist/types/{variant-collapse.d.ts → compat/collapse.d.ts} +60 -35
  12. package/dist/types/compat/openai.d.ts +7 -26
  13. package/dist/types/compat/resolve.d.ts +15 -0
  14. package/dist/types/compat/revision.d.ts +41 -0
  15. package/dist/types/compat/taxonomy.d.ts +93 -0
  16. package/dist/types/compat/types.d.ts +565 -0
  17. package/dist/types/discovery/antigravity.d.ts +1 -1
  18. package/dist/types/discovery/cursor-proto.d.ts +6 -6
  19. package/dist/types/discovery/cursor.d.ts +5 -3
  20. package/dist/types/discovery/devin-proto.d.ts +363 -0
  21. package/dist/types/discovery/gemini-cli.d.ts +1 -1
  22. package/dist/types/identity/id.d.ts +1 -0
  23. package/dist/types/identity/index.d.ts +3 -3
  24. package/dist/types/identity/reference.d.ts +0 -10
  25. package/dist/types/index.d.ts +1 -1
  26. package/dist/types/model-thinking.d.ts +6 -24
  27. package/dist/types/provider-models/cline-pass.d.ts +22 -0
  28. package/dist/types/provider-models/descriptors.d.ts +20 -1
  29. package/dist/types/provider-models/models-dev-policies.d.ts +2 -1
  30. package/dist/types/provider-models/openai-compat.d.ts +59 -46
  31. package/dist/types/provider-models/special.d.ts +23 -2
  32. package/dist/types/types.d.ts +177 -13
  33. package/dist/types/wire/cline-pass.d.ts +21 -0
  34. package/dist/types/wire/devin-proto.d.ts +14 -0
  35. package/dist/types/wire/devin.d.ts +36 -0
  36. package/dist/types/wire/github-copilot.d.ts +31 -12
  37. package/package.json +6 -4
  38. package/src/build.ts +165 -48
  39. package/src/cline-pass-model-id.ts +11 -0
  40. package/src/compat/anthropic.ts +15 -136
  41. package/src/compat/auth-ids.ts +157 -0
  42. package/src/compat/auth.ts +60 -0
  43. package/src/compat/axes.ts +290 -0
  44. package/src/compat/behavior.ts +207 -0
  45. package/src/compat/cascade.ts +230 -0
  46. package/src/{variant-collapse.ts → compat/collapse.ts} +381 -773
  47. package/src/compat/openai.ts +17 -848
  48. package/src/compat/resolve.ts +1229 -0
  49. package/src/compat/revision.ts +154 -0
  50. package/src/compat/rules/README.md +315 -0
  51. package/src/compat/rules/auth/_order.kdl +13 -0
  52. package/src/compat/rules/auth/abliteration.kdl +9 -0
  53. package/src/compat/rules/auth/aiand.kdl +9 -0
  54. package/src/compat/rules/auth/aimlapi.kdl +3 -0
  55. package/src/compat/rules/auth/alibaba-coding-plan.kdl +5 -0
  56. package/src/compat/rules/auth/alibaba-token-plan.kdl +11 -0
  57. package/src/compat/rules/auth/amazon-bedrock.kdl +5 -0
  58. package/src/compat/rules/auth/anthropic.kdl +53 -0
  59. package/src/compat/rules/auth/azure.kdl +3 -0
  60. package/src/compat/rules/auth/baseten.kdl +9 -0
  61. package/src/compat/rules/auth/bedrock-mantle.kdl +5 -0
  62. package/src/compat/rules/auth/cerebras.kdl +9 -0
  63. package/src/compat/rules/auth/cline-pass.kdl +12 -0
  64. package/src/compat/rules/auth/cloudflare-ai-gateway.kdl +4 -0
  65. package/src/compat/rules/auth/coreweave.kdl +9 -0
  66. package/src/compat/rules/auth/cursor.kdl +5 -0
  67. package/src/compat/rules/auth/deepinfra.kdl +11 -0
  68. package/src/compat/rules/auth/deepseek.kdl +10 -0
  69. package/src/compat/rules/auth/devin.kdl +28 -0
  70. package/src/compat/rules/auth/exa.kdl +9 -0
  71. package/src/compat/rules/auth/firepass.kdl +14 -0
  72. package/src/compat/rules/auth/fireworks.kdl +16 -0
  73. package/src/compat/rules/auth/github-copilot.kdl +7 -0
  74. package/src/compat/rules/auth/gitlab-duo-agent.kdl +23 -0
  75. package/src/compat/rules/auth/gitlab-duo.kdl +24 -0
  76. package/src/compat/rules/auth/gmi-cloud.kdl +9 -0
  77. package/src/compat/rules/auth/google-antigravity.kdl +28 -0
  78. package/src/compat/rules/auth/google-gemini-cli.kdl +26 -0
  79. package/src/compat/rules/auth/google-vertex.kdl +5 -0
  80. package/src/compat/rules/auth/google.kdl +3 -0
  81. package/src/compat/rules/auth/groq.kdl +3 -0
  82. package/src/compat/rules/auth/huggingface.kdl +9 -0
  83. package/src/compat/rules/auth/kagi.kdl +9 -0
  84. package/src/compat/rules/auth/kilo.kdl +4 -0
  85. package/src/compat/rules/auth/kimi-code.kdl +18 -0
  86. package/src/compat/rules/auth/litellm.kdl +8 -0
  87. package/src/compat/rules/auth/llama.cpp.kdl +10 -0
  88. package/src/compat/rules/auth/lm-studio.kdl +7 -0
  89. package/src/compat/rules/auth/meta.kdl +9 -0
  90. package/src/compat/rules/auth/minimax-code-cn.kdl +9 -0
  91. package/src/compat/rules/auth/minimax-code.kdl +9 -0
  92. package/src/compat/rules/auth/minimax.kdl +3 -0
  93. package/src/compat/rules/auth/mistral.kdl +3 -0
  94. package/src/compat/rules/auth/moonshot.kdl +9 -0
  95. package/src/compat/rules/auth/nanogpt.kdl +9 -0
  96. package/src/compat/rules/auth/novita.kdl +12 -0
  97. package/src/compat/rules/auth/nvidia.kdl +11 -0
  98. package/src/compat/rules/auth/ollama-cloud.kdl +8 -0
  99. package/src/compat/rules/auth/ollama.kdl +9 -0
  100. package/src/compat/rules/auth/openai-codex-device.kdl +20 -0
  101. package/src/compat/rules/auth/openai-codex.kdl +33 -0
  102. package/src/compat/rules/auth/openai.kdl +3 -0
  103. package/src/compat/rules/auth/opencode-go.kdl +10 -0
  104. package/src/compat/rules/auth/opencode-zen.kdl +9 -0
  105. package/src/compat/rules/auth/openrouter.kdl +32 -0
  106. package/src/compat/rules/auth/parallel.kdl +9 -0
  107. package/src/compat/rules/auth/perplexity.kdl +7 -0
  108. package/src/compat/rules/auth/qianfan.kdl +9 -0
  109. package/src/compat/rules/auth/qwen-portal.kdl +9 -0
  110. package/src/compat/rules/auth/sakana.kdl +9 -0
  111. package/src/compat/rules/auth/siliconflow-cn.kdl +9 -0
  112. package/src/compat/rules/auth/siliconflow.kdl +9 -0
  113. package/src/compat/rules/auth/synthetic.kdl +9 -0
  114. package/src/compat/rules/auth/tavily.kdl +9 -0
  115. package/src/compat/rules/auth/together.kdl +14 -0
  116. package/src/compat/rules/auth/umans.kdl +9 -0
  117. package/src/compat/rules/auth/venice.kdl +9 -0
  118. package/src/compat/rules/auth/vercel-ai-gateway.kdl +8 -0
  119. package/src/compat/rules/auth/vllm.kdl +9 -0
  120. package/src/compat/rules/auth/wafer-serverless.kdl +11 -0
  121. package/src/compat/rules/auth/xai-oauth.kdl +27 -0
  122. package/src/compat/rules/auth/xai.kdl +9 -0
  123. package/src/compat/rules/auth/xiaomi-token-plan-ams.kdl +9 -0
  124. package/src/compat/rules/auth/xiaomi-token-plan-cn.kdl +9 -0
  125. package/src/compat/rules/auth/xiaomi-token-plan-sgp.kdl +9 -0
  126. package/src/compat/rules/auth/xiaomi.kdl +6 -0
  127. package/src/compat/rules/auth/yolo-auto.kdl +11 -0
  128. package/src/compat/rules/auth/zai-coding-plan.kdl +29 -0
  129. package/src/compat/rules/auth/zai.kdl +9 -0
  130. package/src/compat/rules/auth/zenmux.kdl +9 -0
  131. package/src/compat/rules/auth/zhipu-coding-plan.kdl +9 -0
  132. package/src/compat/rules/classes/amazon.kdl +21 -0
  133. package/src/compat/rules/classes/anthropic.kdl +285 -0
  134. package/src/compat/rules/classes/baidu.kdl +9 -0
  135. package/src/compat/rules/classes/bytedance.kdl +9 -0
  136. package/src/compat/rules/classes/cohere.kdl +7 -0
  137. package/src/compat/rules/classes/deepseek.kdl +80 -0
  138. package/src/compat/rules/classes/gemini.kdl +107 -0
  139. package/src/compat/rules/classes/gemma.kdl +14 -0
  140. package/src/compat/rules/classes/glm.kdl +115 -0
  141. package/src/compat/rules/classes/gpt-oss.kdl +16 -0
  142. package/src/compat/rules/classes/kimi.kdl +113 -0
  143. package/src/compat/rules/classes/meta.kdl +15 -0
  144. package/src/compat/rules/classes/mimo.kdl +32 -0
  145. package/src/compat/rules/classes/minimax.kdl +79 -0
  146. package/src/compat/rules/classes/mistral.kdl +19 -0
  147. package/src/compat/rules/classes/openai.kdl +42 -0
  148. package/src/compat/rules/classes/qwen.kdl +72 -0
  149. package/src/compat/rules/classes/stepfun.kdl +10 -0
  150. package/src/compat/rules/classes/xai.kdl +45 -0
  151. package/src/compat/rules/providers/abliteration.kdl +8 -0
  152. package/src/compat/rules/providers/aiand.kdl +19 -0
  153. package/src/compat/rules/providers/aimlapi.kdl +24 -0
  154. package/src/compat/rules/providers/alibaba-coding-plan.kdl +20 -0
  155. package/src/compat/rules/providers/alibaba-token-plan.kdl +35 -0
  156. package/src/compat/rules/providers/amazon-bedrock.kdl +36 -0
  157. package/src/compat/rules/providers/anthropic.kdl +24 -0
  158. package/src/compat/rules/providers/azure.kdl +19 -0
  159. package/src/compat/rules/providers/baseten.kdl +19 -0
  160. package/src/compat/rules/providers/bedrock-mantle.kdl +15 -0
  161. package/src/compat/rules/providers/cerebras.kdl +19 -0
  162. package/src/compat/rules/providers/cline-pass.kdl +8 -0
  163. package/src/compat/rules/providers/cloudflare-ai-gateway.kdl +26 -0
  164. package/src/compat/rules/providers/coreweave.kdl +16 -0
  165. package/src/compat/rules/providers/cursor.kdl +52 -0
  166. package/src/compat/rules/providers/deepseek.kdl +21 -0
  167. package/src/compat/rules/providers/firepass.kdl +15 -0
  168. package/src/compat/rules/providers/fireworks.kdl +43 -0
  169. package/src/compat/rules/providers/github-copilot.kdl +62 -0
  170. package/src/compat/rules/providers/gitlab-duo.kdl +24 -0
  171. package/src/compat/rules/providers/gmi-cloud.kdl +8 -0
  172. package/src/compat/rules/providers/google-antigravity.kdl +59 -0
  173. package/src/compat/rules/providers/google-gemini-cli.kdl +12 -0
  174. package/src/compat/rules/providers/google-vertex.kdl +25 -0
  175. package/src/compat/rules/providers/google.kdl +17 -0
  176. package/src/compat/rules/providers/groq.kdl +8 -0
  177. package/src/compat/rules/providers/huggingface.kdl +24 -0
  178. package/src/compat/rules/providers/kilo.kdl +51 -0
  179. package/src/compat/rules/providers/kimi-code.kdl +37 -0
  180. package/src/compat/rules/providers/llama.cpp.kdl +6 -0
  181. package/src/compat/rules/providers/lm-studio.kdl +6 -0
  182. package/src/compat/rules/providers/meta.kdl +10 -0
  183. package/src/compat/rules/providers/minimax-code-cn.kdl +13 -0
  184. package/src/compat/rules/providers/minimax-code.kdl +13 -0
  185. package/src/compat/rules/providers/minimax.kdl +6 -0
  186. package/src/compat/rules/providers/mistral.kdl +8 -0
  187. package/src/compat/rules/providers/moonshot.kdl +36 -0
  188. package/src/compat/rules/providers/nanogpt.kdl +100 -0
  189. package/src/compat/rules/providers/novita.kdl +34 -0
  190. package/src/compat/rules/providers/nvidia.kdl +27 -0
  191. package/src/compat/rules/providers/ollama-cloud.kdl +20 -0
  192. package/src/compat/rules/providers/ollama.kdl +12 -0
  193. package/src/compat/rules/providers/openai-codex.kdl +121 -0
  194. package/src/compat/rules/providers/openai.kdl +82 -0
  195. package/src/compat/rules/providers/opencode-go.kdl +97 -0
  196. package/src/compat/rules/providers/opencode-zen.kdl +103 -0
  197. package/src/compat/rules/providers/openrouter.kdl +66 -0
  198. package/src/compat/rules/providers/qianfan.kdl +8 -0
  199. package/src/compat/rules/providers/sakana.kdl +10 -0
  200. package/src/compat/rules/providers/synthetic.kdl +16 -0
  201. package/src/compat/rules/providers/together.kdl +22 -0
  202. package/src/compat/rules/providers/umans.kdl +28 -0
  203. package/src/compat/rules/providers/venice.kdl +46 -0
  204. package/src/compat/rules/providers/vercel-ai-gateway.kdl +73 -0
  205. package/src/compat/rules/providers/vllm.kdl +8 -0
  206. package/src/compat/rules/providers/wafer-serverless.kdl +35 -0
  207. package/src/compat/rules/providers/xai-oauth.kdl +48 -0
  208. package/src/compat/rules/providers/xai.kdl +50 -0
  209. package/src/compat/rules/providers/xiaomi-token-plan-ams.kdl +9 -0
  210. package/src/compat/rules/providers/xiaomi-token-plan-cn.kdl +9 -0
  211. package/src/compat/rules/providers/xiaomi-token-plan-sgp.kdl +9 -0
  212. package/src/compat/rules/providers/xiaomi.kdl +22 -0
  213. package/src/compat/rules/providers/yolo-auto.kdl +16 -0
  214. package/src/compat/rules/providers/zai.kdl +36 -0
  215. package/src/compat/rules/providers/zenmux.kdl +108 -0
  216. package/src/compat/rules/providers/zhipu-coding-plan.kdl +49 -0
  217. package/src/compat/rules/runtime/behavior.kdl +254 -0
  218. package/src/compat/rules/taxonomy/_collapse.kdl +747 -0
  219. package/src/compat/rules/taxonomy/_discovery.kdl +60 -0
  220. package/src/compat/rules/taxonomy/ai21.kdl +6 -0
  221. package/src/compat/rules/taxonomy/amazon.kdl +8 -0
  222. package/src/compat/rules/taxonomy/anthropic.kdl +17 -0
  223. package/src/compat/rules/taxonomy/baidu.kdl +5 -0
  224. package/src/compat/rules/taxonomy/bytedance.kdl +5 -0
  225. package/src/compat/rules/taxonomy/cohere.kdl +6 -0
  226. package/src/compat/rules/taxonomy/deepseek.kdl +18 -0
  227. package/src/compat/rules/taxonomy/gemini.kdl +9 -0
  228. package/src/compat/rules/taxonomy/gemma.kdl +3 -0
  229. package/src/compat/rules/taxonomy/glm.kdl +17 -0
  230. package/src/compat/rules/taxonomy/gpt-oss.kdl +11 -0
  231. package/src/compat/rules/taxonomy/kimi.kdl +20 -0
  232. package/src/compat/rules/taxonomy/meta.kdl +10 -0
  233. package/src/compat/rules/taxonomy/mimo.kdl +7 -0
  234. package/src/compat/rules/taxonomy/minimax.kdl +9 -0
  235. package/src/compat/rules/taxonomy/mistral.kdl +7 -0
  236. package/src/compat/rules/taxonomy/openai.kdl +43 -0
  237. package/src/compat/rules/taxonomy/qwen.kdl +33 -0
  238. package/src/compat/rules/taxonomy/stepfun.kdl +5 -0
  239. package/src/compat/rules/taxonomy/unknown.kdl +3 -0
  240. package/src/compat/rules/taxonomy/xai.kdl +12 -0
  241. package/src/compat/rules.d.json.ts +5 -0
  242. package/src/compat/rules.json +16933 -0
  243. package/src/compat/taxonomy.ts +452 -0
  244. package/src/compat/types.ts +586 -0
  245. package/src/discovery/antigravity.ts +5 -6
  246. package/src/discovery/codex.ts +12 -7
  247. package/src/discovery/cursor-proto.ts +7 -7
  248. package/src/discovery/cursor.ts +47 -58
  249. package/src/discovery/devin-proto.ts +613 -0
  250. package/src/discovery/devin.ts +358 -52
  251. package/src/discovery/gemini-cli.ts +14 -11
  252. package/src/discovery/gemini.ts +2 -0
  253. package/src/identity/dialect.ts +2 -2
  254. package/src/identity/id.ts +12 -0
  255. package/src/identity/index.ts +3 -3
  256. package/src/identity/reference.ts +30 -4
  257. package/src/index.ts +1 -1
  258. package/src/model-manager.ts +4 -4
  259. package/src/model-thinking.ts +6 -810
  260. package/src/model-tokenizer.ts +37 -44
  261. package/src/models.json +86187 -23440
  262. package/src/models.ts +6 -1
  263. package/src/provider-models/bundled-references.ts +3 -1
  264. package/src/provider-models/cline-pass.ts +226 -0
  265. package/src/provider-models/descriptors.ts +18 -0
  266. package/src/provider-models/google.ts +14 -5
  267. package/src/provider-models/models-dev-policies.ts +4 -29
  268. package/src/provider-models/ollama.ts +15 -2
  269. package/src/provider-models/openai-compat.ts +794 -520
  270. package/src/provider-models/special.ts +186 -2
  271. package/src/types.ts +197 -11
  272. package/src/wire/cline-pass.ts +35 -0
  273. package/src/wire/devin-proto.ts +30 -0
  274. package/src/wire/devin.ts +72 -0
  275. package/src/wire/github-copilot.ts +49 -7
  276. package/dist/types/compat/bedrock.d.ts +0 -3
  277. package/dist/types/compat/devin.d.ts +0 -9
  278. package/dist/types/identity/classify.d.ts +0 -69
  279. package/dist/types/identity/family.d.ts +0 -243
  280. package/dist/types/identity/markers.d.ts +0 -4
  281. package/dist/types/openai-pricing.d.ts +0 -17
  282. package/src/compat/bedrock.ts +0 -153
  283. package/src/compat/devin.ts +0 -12
  284. package/src/identity/classify.ts +0 -242
  285. package/src/identity/family.ts +0 -496
  286. package/src/identity/markers.ts +0 -49
  287. package/src/openai-pricing.ts +0 -29
@@ -1,6 +1,18 @@
1
1
  import { USER_AGENT } from "@linxiraos/pi-utils";
2
2
  import * as logger from "@linxiraos/pi-utils/logger";
3
+ import { toClinePassPublicModelId } from "../cline-pass-model-id";
4
+ import {
5
+ apiRouteExactModelIds,
6
+ apiRouteFor,
7
+ isExcludedModel,
8
+ isLikelyOpenAIResponsesId,
9
+ modelLimitsFor,
10
+ pricingPeerFor,
11
+ } from "../compat/behavior";
3
12
  import { xaiResponsesReasoningEffortMap } from "../compat/openai";
13
+ import { resolveModelPolicy } from "../compat/resolve";
14
+ import { compareRevision, parseRevision } from "../compat/revision";
15
+ import { billingVariantPlain, classifyModel, discoveryVocabulary } from "../compat/taxonomy";
4
16
  import {
5
17
  DEFAULT_OPENAI_COMPATIBLE_DISCOVERY_TIMEOUT_MS,
6
18
  fetchOpenAICompatibleModels,
@@ -10,47 +22,46 @@ import {
10
22
  import { Effort, THINKING_EFFORTS } from "../effort";
11
23
  import { FIREWORKS_FAST_SUFFIX, toFireworksPublicModelId } from "../fireworks-model-id";
12
24
  import { getBundledModelReferenceIndex } from "../identity/bundled";
13
- import {
14
- anthropicModelSupportsThinking,
15
- isGlm52ReasoningEffortModelId,
16
- isGlmVisionModelId,
17
- isGrokReasoningEffortCapable,
18
- isKimiK3ModelId,
19
- isKimiModelId,
20
- isMuseSparkModelId,
21
- isQwen38PlusTemplateEffortModelId,
22
- isReasoningGlmModelId,
23
- } from "../identity/family";
24
25
  import { resolveModelReference } from "../identity/reference";
25
26
  import type { ModelManagerOptions, ModelsDevFallback } from "../model-manager";
26
27
  import { type GeneratedProvider, getBundledModels } from "../models";
27
- import { OPENAI_GPT_56_CYBER_STANDARD_COST, OPENAI_GPT_56_SOL_STANDARD_COST } from "../openai-pricing";
28
- import type {
29
- Api,
30
- FetchImpl,
31
- LongContextTokenCost,
32
- Model,
33
- ModelSpec,
34
- OpenAICompat,
35
- Provider,
36
- ThinkingConfig,
37
- } from "../types";
28
+ import type { Api, FetchImpl, Model, ModelSpec, OpenAICompat, Provider, ThinkingConfig, TokenCost } from "../types";
38
29
  import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
39
30
  import { ALIBABA_TOKEN_PLAN_BASE_URL, parseAlibabaTokenPlanCredential } from "../wire/alibaba-token-plan";
31
+ import { CLINEPASS_API_BASE_URL, clinePassClientHeaders } from "../wire/cline-pass";
40
32
  import { CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL } from "../wire/cloudflare-ai-gateway";
41
33
  import { coreWeaveProjectHeaders } from "../wire/coreweave";
42
34
  import {
43
35
  COPILOT_API_HEADERS,
36
+ COPILOT_DISCOVERY_HEADERS,
44
37
  discoverGitHubCopilotApiEndpoint,
45
38
  getGitHubCopilotBaseUrl,
46
39
  isPersonalGitHubCopilotBaseUrl,
40
+ mergeCopilotApiHeaders,
47
41
  parseGitHubCopilotApiKey,
48
42
  } from "../wire/github-copilot";
49
43
  import { createBundledReferenceMap, createReferenceResolver, toModelSpec } from "./bundled-references";
50
44
  import { getDefaultModelDiscoveryBaseUrl, resolveModelCacheProviderId } from "./cache-provider-id";
45
+ import { getClinePassModelMetadata } from "./cline-pass";
51
46
  import type { ModelManagerConfig } from "./descriptor-types";
52
47
  import { filterModelsDevCatalogRows } from "./models-dev-policies";
53
48
 
49
+ function revisionAtLeast(revision: string | undefined, floor: string): boolean {
50
+ if (revision === undefined) return false;
51
+ const parsedRevision = parseRevision(revision);
52
+ const parsedFloor = parseRevision(floor);
53
+ return (
54
+ parsedRevision !== undefined && parsedFloor !== undefined && compareRevision(parsedRevision, parsedFloor) >= 0
55
+ );
56
+ }
57
+
58
+ function isGlmReasoningIdentity(provider: string, modelId: string, floor: string): boolean {
59
+ const identity = classifyModel(provider, modelId, { lenient: true });
60
+ if (identity.class !== "glm" || !revisionAtLeast(identity.revision, floor)) return false;
61
+ if (identity.family === "flash") return revisionAtLeast(identity.revision, "5.3");
62
+ return identity.family === undefined || identity.family === "air" || identity.family === "turbo";
63
+ }
64
+
54
65
  const MODELS_DEV_URL = "https://catalog.stencil.so/models.json.zstd";
55
66
 
56
67
  /** Little-endian magic number opening every zstd frame (RFC 8878). */
@@ -83,6 +94,7 @@ export interface ModelsDevModel {
83
94
  name?: string;
84
95
  tool_call?: boolean;
85
96
  reasoning?: boolean;
97
+ reasoning_options?: Array<{ type?: string; values?: string[]; min?: number; max?: number }>;
86
98
  limit?: {
87
99
  context?: number;
88
100
  output?: number;
@@ -430,7 +442,6 @@ async function fetchOllamaNativeModels(
430
442
  provider: "ollama",
431
443
  baseUrl,
432
444
  reasoning: metadata.reasoning ?? false,
433
- thinking: metadata.thinking,
434
445
  input: metadata.input ?? ["text"],
435
446
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
436
447
  contextWindow: metadata.contextWindow,
@@ -458,7 +469,6 @@ interface OllamaResolvedMetadata {
458
469
  maxTokens: number;
459
470
  capabilities?: string[];
460
471
  reasoning?: boolean;
461
- thinking?: ThinkingConfig;
462
472
  input?: ("text" | "image")[];
463
473
  }
464
474
 
@@ -467,7 +477,6 @@ interface OllamaShowMetadata {
467
477
  maxTokens?: number;
468
478
  capabilities?: string[];
469
479
  reasoning?: boolean;
470
- thinking?: ThinkingConfig;
471
480
  input?: ("text" | "image")[];
472
481
  }
473
482
 
@@ -492,13 +501,6 @@ function getOllamaCapabilities(value: unknown): string[] | undefined {
492
501
  return value.filter((item): item is string => typeof item === "string");
493
502
  }
494
503
 
495
- function getOllamaThinkingConfig(capabilities: string[] | undefined): ThinkingConfig | undefined {
496
- if (!capabilities?.includes("thinking")) {
497
- return undefined;
498
- }
499
- return { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] };
500
- }
501
-
502
504
  /**
503
505
  * Query Ollama's `/api/show` endpoint for a single model and pull native
504
506
  * context and capability metadata from the response. Returns `undefined` when
@@ -526,7 +528,6 @@ async function fetchOllamaShowMetadata(
526
528
  maxTokens: contextWindow ? OLLAMA_DEFAULT_MAX_TOKENS : undefined,
527
529
  capabilities,
528
530
  reasoning: capabilities ? capabilities.includes("thinking") : undefined,
529
- thinking: getOllamaThinkingConfig(capabilities),
530
531
  input: capabilities
531
532
  ? capabilities.includes("vision")
532
533
  ? (["text", "image"] as Array<"text" | "image">)
@@ -571,17 +572,6 @@ function createOllamaMetadataResolver(
571
572
  };
572
573
  }
573
574
 
574
- const OPENAI_NON_RESPONSES_PREFIXES = [
575
- "text-embedding",
576
- "whisper-",
577
- "tts-",
578
- "omni-moderation",
579
- "omni-transcribe",
580
- "omni-speech",
581
- "gpt-image-",
582
- "gpt-realtime",
583
- ] as const;
584
-
585
575
  function isLikelyOpenAIResponsesModelId(
586
576
  id: string,
587
577
  references?: ReadonlyMap<string, ModelSpec<"openai-responses">>,
@@ -593,35 +583,9 @@ function isLikelyOpenAIResponsesModelId(
593
583
  if (references?.has(trimmed)) {
594
584
  return true;
595
585
  }
596
- const normalized = trimmed.toLowerCase();
597
- if (OPENAI_NON_RESPONSES_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
598
- return false;
599
- }
600
- if (normalized.includes("embedding")) {
601
- return false;
602
- }
603
- return (
604
- normalized.startsWith("gpt-") ||
605
- normalized.startsWith("o1") ||
606
- normalized.startsWith("o3") ||
607
- normalized.startsWith("o4") ||
608
- normalized.startsWith("chatgpt")
609
- );
586
+ return isLikelyOpenAIResponsesId(trimmed);
610
587
  }
611
588
 
612
- const NANO_GPT_NON_TEXT_MODEL_TOKENS = [
613
- "embedding",
614
- "image",
615
- "vision",
616
- "audio",
617
- "speech",
618
- "transcribe",
619
- "moderation",
620
- "realtime",
621
- "whisper",
622
- "tts",
623
- ] as const;
624
-
625
589
  /** Regex matching NanoGPT `:thinking` suffixed model IDs (with or without a level). */
626
590
  const NANO_GPT_THINKING_SUFFIX_RE = /:thinking(:[^:]+)?$/;
627
591
 
@@ -633,7 +597,7 @@ function isLikelyNanoGptTextModelId(id: string): boolean {
633
597
  if (NANO_GPT_THINKING_SUFFIX_RE.test(normalized)) {
634
598
  return false;
635
599
  }
636
- return !NANO_GPT_NON_TEXT_MODEL_TOKENS.some(token => normalized.includes(token));
600
+ return !isExcludedModel("nanogpt", normalized);
637
601
  }
638
602
 
639
603
  type SimpleProviderDiscoveryHeaders = Record<string, string> | (() => Record<string, string> | undefined);
@@ -929,30 +893,23 @@ export function umansModelManagerOptions(config?: UmansModelManagerConfig): Mode
929
893
  // ---------------------------------------------------------------------------
930
894
 
931
895
  const OPENAI_API_BASE_URL = "https://api.openai.com/v1";
932
- /** GPT-5.6 rates applied when a first-party request exceeds 272K input tokens. */
933
- export const OPENAI_GPT_56_LONG_CONTEXT_COSTS = {
934
- luna: {
935
- inputThreshold: 272_000,
936
- input: 0.4,
937
- output: 1.8,
938
- cacheRead: 0.04,
939
- cacheWrite: 0.5,
940
- },
941
- sol: {
942
- inputThreshold: 272_000,
943
- input: 10,
944
- output: 45,
945
- cacheRead: 1,
946
- cacheWrite: 12.5,
947
- },
948
- terra: {
949
- inputThreshold: 272_000,
950
- input: 4,
951
- output: 18,
952
- cacheRead: 0.4,
953
- cacheWrite: 5,
954
- },
955
- } as const satisfies Readonly<Record<"luna" | "sol" | "terra", LongContextTokenCost>>;
896
+ // Curated seed pricing for approval-gated aliases stencil.so does not list
897
+ // yet; upstream catalog rows outrank these once discovery serves them.
898
+ /** Standard GPT-5.6 Sol rates used by the Daybreak Blue aliases. */
899
+ const OPENAI_GPT_56_SOL_STANDARD_COST = {
900
+ input: 5,
901
+ output: 30,
902
+ cacheRead: 0.5,
903
+ cacheWrite: 6.25,
904
+ } as const satisfies TokenCost;
905
+
906
+ /** Standard GPT-5.6 Cyber rates used by the Daybreak Red aliases. */
907
+ const OPENAI_GPT_56_CYBER_STANDARD_COST = {
908
+ input: 12.5,
909
+ output: 75,
910
+ cacheRead: 1.25,
911
+ cacheWrite: 15.625,
912
+ } as const satisfies TokenCost;
956
913
 
957
914
  export interface OpenAIModelManagerConfig {
958
915
  apiKey?: string;
@@ -986,10 +943,9 @@ export const OPENAI_DAYBREAK_CURATED_FALLBACK_MODELS: readonly ModelSpec<"openai
986
943
  baseUrl: OPENAI_API_BASE_URL,
987
944
  reasoning: true,
988
945
  input: ["text", "image"],
989
- cost: {
990
- ...OPENAI_GPT_56_SOL_STANDARD_COST,
991
- longContext: OPENAI_GPT_56_LONG_CONTEXT_COSTS.sol,
992
- },
946
+ // The >272K long-context tier is rule-owned (`providers/openai.kdl`
947
+ // long-context-cost) and baked at build time.
948
+ cost: OPENAI_GPT_56_SOL_STANDARD_COST,
993
949
  contextWindow: 1_050_000,
994
950
  maxTokens: 128_000,
995
951
  },
@@ -1019,32 +975,21 @@ export const OPENAI_DAYBREAK_CURATED_FALLBACK_MODELS: readonly ModelSpec<"openai
1019
975
  },
1020
976
  ];
1021
977
 
1022
- /** First-party gpt-5.6 SKUs that accept `reasoning: { mode: "pro" }` on the Responses APIs. */
1023
- const OPENAI_PRO_REASONING_BASE_IDS: Record<string, true> = {
1024
- "gpt-5.6-luna": true,
1025
- "gpt-5.6-sol": true,
1026
- "gpt-5.6-terra": true,
1027
- };
1028
- /**
1029
- * Providers whose generated pro aliases this pass owns. `openai-codex` stays in
1030
- * the sweep so stale aliases from earlier snapshots are dropped on regen, but
1031
- * projection is `openai`-only — subscription (Codex) auth does not offer pro
1032
- * reasoning.
1033
- */
1034
- const OPENAI_PRO_REASONING_SWEEP_PROVIDERS: Record<string, true> = { openai: true, "openai-codex": true };
1035
-
1036
978
  /**
1037
979
  * A row this generator pass owns: one of the derived `gpt-5.6-*-pro` alias ids
1038
980
  * on a swept provider that carries the generated `reasoningMode` marker.
1039
981
  * A real upstream model occupying the same id has no `reasoningMode` and is
1040
982
  * never touched.
983
+ * The reviewed base-id and sweep tables live in the compiled discovery
984
+ * vocabulary (`pro-reasoning-alias` / `pro-reasoning-sweep`).
1041
985
  */
1042
986
  function isGeneratedOpenAIProReasoningAlias(model: ModelSpec<Api>): boolean {
987
+ const { proReasoningAliases, proReasoningSweep } = discoveryVocabulary();
1043
988
  return (
1044
- OPENAI_PRO_REASONING_SWEEP_PROVIDERS[model.provider] === true &&
989
+ proReasoningSweep.includes(model.provider) &&
1045
990
  model.reasoningMode !== undefined &&
1046
991
  model.id.endsWith("-pro") &&
1047
- OPENAI_PRO_REASONING_BASE_IDS[model.id.slice(0, -"-pro".length)] === true
992
+ Object.values(proReasoningAliases).some(bases => bases.includes(model.id.slice(0, -"-pro".length)))
1048
993
  );
1049
994
  }
1050
995
 
@@ -1064,8 +1009,8 @@ export function projectOpenAIProReasoningAliases(models: readonly ModelSpec<Api>
1064
1009
  const ids = new Set(kept.map(model => `${model.provider}/${model.id}`));
1065
1010
  const out = [...kept];
1066
1011
  for (const model of kept) {
1067
- if (model.provider !== "openai") continue;
1068
- if (!OPENAI_PRO_REASONING_BASE_IDS[model.id]) continue;
1012
+ const bases = discoveryVocabulary().proReasoningAliases[model.provider];
1013
+ if (!bases?.includes(model.id)) continue;
1069
1014
  const aliasId = `${model.id}-pro`;
1070
1015
  const aliasKey = `${model.provider}/${aliasId}`;
1071
1016
  if (ids.has(aliasKey)) continue;
@@ -1197,18 +1142,6 @@ export function groqModelManagerOptions(config?: GroqModelManagerConfig): ModelM
1197
1142
  // 3. Cerebras
1198
1143
  // ---------------------------------------------------------------------------
1199
1144
 
1200
- const CEREBRAS_IMAGE_INPUT_MODEL_IDS = new Set(["gemma-4-31b"]);
1201
-
1202
- function applyCerebrasDiscoveryOverrides(model: ModelSpec<"openai-completions">): ModelSpec<"openai-completions"> {
1203
- if (!CEREBRAS_IMAGE_INPUT_MODEL_IDS.has(model.id)) {
1204
- return model;
1205
- }
1206
- return {
1207
- ...model,
1208
- input: ["text", "image"],
1209
- };
1210
- }
1211
-
1212
1145
  export interface CerebrasModelManagerConfig {
1213
1146
  apiKey?: string;
1214
1147
  baseUrl?: string;
@@ -1224,8 +1157,7 @@ export function cerebrasModelManagerOptions(
1224
1157
  defaultBaseUrl: "https://api.cerebras.ai/v1",
1225
1158
  config,
1226
1159
  requireApiKey: true,
1227
- mapModel: (entry, defaults, reference) =>
1228
- applyCerebrasDiscoveryOverrides(mapWithBundledReference(entry, defaults, reference)),
1160
+ mapModel: (entry, defaults, reference) => mapWithBundledReference(entry, defaults, reference),
1229
1161
  });
1230
1162
  }
1231
1163
 
@@ -1534,64 +1466,31 @@ export interface XaiModelManagerConfig {
1534
1466
  fetch?: FetchImpl;
1535
1467
  }
1536
1468
 
1537
- // xAI bills the whole request at 2x the base rates once the prompt reaches
1538
- // 200K tokens, for every current Grok SKU that carries the two-tier card
1539
- // (grok-4.3/4.5/4.6, every grok-4.20 variant, grok-build-0.1). Source:
1540
- // docs.x.ai/developers/models. Computing the tier from the base keeps it in
1541
- // lockstep with stencil.so price updates instead of a hand-maintained table.
1542
- const XAI_LONG_CONTEXT_THRESHOLD = 200_000;
1543
- const XAI_LONG_CONTEXT_MULTIPLIER = 2;
1544
-
1545
1469
  // SuperGrok surfaces a few models under IDs that differ from their public
1546
1470
  // `xai` catalog equivalent, so the exact-ID price fallback misses them. Map
1547
1471
  // the OAuth ID to the paid ID it mirrors.
1548
- const XAI_OAUTH_PRICE_ALIASES: Record<string, string> = {
1549
- "grok-4.20-multi-agent-0309": "grok-4.20-multi-agent-beta-latest",
1550
- };
1551
-
1552
- function xaiHasLongContextTier(modelId: string): boolean {
1553
- return (
1554
- modelId === "grok-4.3" ||
1555
- modelId === "grok-4.5" ||
1556
- modelId === "grok-4.6" ||
1557
- modelId === "grok-build-0.1" ||
1558
- modelId.startsWith("grok-4.20")
1559
- );
1560
- }
1561
-
1472
+ // The alias map lives in the `pricing-peer` behavior rule.
1562
1473
  function hasTokenPrice(cost: ModelSpec["cost"]): boolean {
1563
1474
  return cost.input !== 0 || cost.output !== 0 || cost.cacheRead !== 0 || cost.cacheWrite !== 0;
1564
1475
  }
1565
1476
 
1566
- function withXaiLongContextTier(model: ModelSpec): ModelSpec {
1567
- if (!xaiHasLongContextTier(model.id) || !hasTokenPrice(model.cost)) return model;
1568
- const longContext: LongContextTokenCost = {
1569
- inputThreshold: XAI_LONG_CONTEXT_THRESHOLD,
1570
- inputThresholdInclusive: true,
1571
- input: model.cost.input * XAI_LONG_CONTEXT_MULTIPLIER,
1572
- output: model.cost.output * XAI_LONG_CONTEXT_MULTIPLIER,
1573
- cacheRead: model.cost.cacheRead * XAI_LONG_CONTEXT_MULTIPLIER,
1574
- cacheWrite: model.cost.cacheWrite * XAI_LONG_CONTEXT_MULTIPLIER,
1575
- };
1576
- return { ...model, cost: { ...model.cost, longContext } };
1577
- }
1578
-
1579
1477
  /**
1580
- * Applies xAI's long-context rate card and mirrors exact public-model prices
1581
- * onto matching SuperGrok catalog rows.
1478
+ * Mirrors exact public-model prices onto matching SuperGrok catalog rows.
1479
+ * The >200K long-context tier itself is rule-owned (`classes/xai.kdl`
1480
+ * `long-context-cost` multiplier axis) and derives at build time.
1582
1481
  */
1583
1482
  export function applyXaiCatalogPricing(models: readonly ModelSpec[]): ModelSpec[] {
1584
- const pricedModels = models.map(model => (model.provider === "xai" ? withXaiLongContextTier(model) : model));
1585
1483
  const publicCosts = new Map(
1586
- pricedModels
1484
+ models
1587
1485
  .filter(model => model.provider === "xai" && hasTokenPrice(model.cost))
1588
1486
  .map(model => [model.id, model.cost]),
1589
1487
  );
1590
1488
 
1591
- return pricedModels.map(model => {
1489
+ return models.map(model => {
1592
1490
  if (model.provider !== "xai-oauth" || hasTokenPrice(model.cost)) return model;
1593
- const alias = XAI_OAUTH_PRICE_ALIASES[model.id];
1594
- const publicCost = publicCosts.get(model.id) ?? (alias ? publicCosts.get(alias) : undefined);
1491
+ const peer = pricingPeerFor("xai-oauth", model.id);
1492
+ const publicCost =
1493
+ publicCosts.get(model.id) ?? (peer && peer.peerId !== model.id ? publicCosts.get(peer.peerId) : undefined);
1595
1494
  return publicCost ? { ...model, cost: { ...publicCost } } : model;
1596
1495
  });
1597
1496
  }
@@ -1630,9 +1529,8 @@ interface XAICuratedModel {
1630
1529
  /**
1631
1530
  * Whether xAI accepts the `reasoning.effort` wire param for this model.
1632
1531
  * Default true. When false: the picker hides the effort dial (via
1633
- * getSupportedEfforts in model-thinking.ts) AND the wire omits the param —
1634
- * both derive from `isGrokReasoningEffortCapable` (identity/family.ts), the
1635
- * single allowlist shared by this curated layer and the compat builder.
1532
+ * getSupportedEfforts in model-thinking.ts) AND the wire omits the param.
1533
+ * Both read the compat engine's resolved reasoning-effort axes.
1636
1534
  */
1637
1535
  supportsReasoningEffort?: boolean;
1638
1536
  /**
@@ -1702,15 +1600,13 @@ export const XAI_OAUTH_CURATED_MODELS: readonly XAICuratedModel[] = [
1702
1600
  // xAI /v1/models returns chat, image, voice, and STT entries. Tool surfaces
1703
1601
  // route through dedicated tools (generate_image, tts) with their own model
1704
1602
  // strings; the chat picker MUST exclude these prefixes or selecting them 400s.
1705
- const XAI_NON_CHAT_PREFIXES = ["grok-imagine-", "grok-stt-", "grok-voice-"] as const;
1706
-
1707
1603
  function withXaiOAuthCompatDefaults(model: ModelSpec<"openai-responses">): ModelSpec<"openai-responses"> {
1708
1604
  const compat = {
1709
- ...(model.compat ?? {}),
1605
+ ...model.compat,
1710
1606
  includeEncryptedReasoning: model.compat?.includeEncryptedReasoning ?? true,
1711
1607
  filterReasoningHistory: model.compat?.filterReasoningHistory ?? false,
1712
1608
  supportsImageDetailOriginal: model.compat?.supportsImageDetailOriginal ?? false,
1713
- omitReasoningEffort: model.compat?.omitReasoningEffort ?? !isGrokReasoningEffortCapable(model.id),
1609
+ omitReasoningEffort: model.compat?.omitReasoningEffort ?? resolveModelPolicy(model).compat.omitReasoningEffort,
1714
1610
  };
1715
1611
  return { ...model, compat };
1716
1612
  }
@@ -1724,8 +1620,8 @@ function withXaiOAuthCompatDefaults(model: ModelSpec<"openai-responses">): Model
1724
1620
  * Bake first-party xAI Responses effort-dial metadata onto a catalog spec.
1725
1621
  *
1726
1622
  * models.dev marks many Grok SKUs as reasoners and the thinking rebake would
1727
- * otherwise emit a default `minimal/low/medium/high` dial. api.x.ai only
1728
- * accepts `reasoning.effort` for {@link isGrokReasoningEffortCapable} ids —
1623
+ * otherwise emit a default `minimal/low/medium/high` dial. api.x.ai
1624
+ * accepts `reasoning.effort` only for ids selected by the compat rule —
1729
1625
  * off-allowlist reasoners (`grok-code-fast-1`, `grok-build-0.1`,
1730
1626
  * `grok-4.20-0309-reasoning`, …) 400 if the param is sent. SuperGrok
1731
1627
  * (`xai-oauth`) already curates this via {@link mergeCuratedIntoModel}; paid
@@ -1735,9 +1631,10 @@ function withXaiOAuthCompatDefaults(model: ModelSpec<"openai-responses">): Model
1735
1631
  * Explicit `compat.supportsReasoningEffort` / `omitReasoningEffort` win.
1736
1632
  */
1737
1633
  export function applyXaiResponsesThinkingPolicy(model: ModelSpec<"openai-responses">): ModelSpec<"openai-responses"> {
1738
- const effortCapable = model.compat?.supportsReasoningEffort ?? isGrokReasoningEffortCapable(model.id);
1634
+ const effortCapable =
1635
+ model.compat?.supportsReasoningEffort ?? resolveModelPolicy(model).compat.supportsReasoningEffort;
1739
1636
  const compat = {
1740
- ...(model.compat ?? {}),
1637
+ ...model.compat,
1741
1638
  supportsReasoningEffort: effortCapable,
1742
1639
  omitReasoningEffort: model.compat?.omitReasoningEffort ?? !effortCapable,
1743
1640
  };
@@ -1767,14 +1664,16 @@ export function applyXaiResponsesThinkingPolicy(model: ModelSpec<"openai-respons
1767
1664
  // param, so a map on those specs is dead weight.
1768
1665
  // The effort-dial pair (`supportsReasoningEffort`/`omitReasoningEffort`) is
1769
1666
  // authoritative: a stale flag on `base` (previous snapshot or dynamic fetch)
1770
- // must not outlive an allowlist change in identity/family.ts.
1667
+ // must not outlive a compat-rule change.
1771
1668
  function mergeCuratedIntoModel(
1772
1669
  base: ModelSpec<"openai-responses">,
1773
1670
  curated: XAICuratedModel,
1774
1671
  ): ModelSpec<"openai-responses"> {
1775
- const effortCapable = curated.supportsReasoningEffort ?? isGrokReasoningEffortCapable(curated.id);
1672
+ const effortCapable =
1673
+ curated.supportsReasoningEffort ??
1674
+ resolveModelPolicy({ ...base, id: curated.id, provider: "xai-oauth" }).compat.supportsReasoningEffort;
1776
1675
  const compat = {
1777
- ...(base.compat ?? {}),
1676
+ ...base.compat,
1778
1677
  includeEncryptedReasoning: base.compat?.includeEncryptedReasoning ?? true,
1779
1678
  filterReasoningHistory: false,
1780
1679
  supportsImageDetailOriginal: base.compat?.supportsImageDetailOriginal ?? false,
@@ -1820,7 +1719,7 @@ function mergeCuratedIntoModel(
1820
1719
  * in original order.
1821
1720
  */
1822
1721
  function applyXAIOAuthCuration(dynamic: readonly ModelSpec<"openai-responses">[]): ModelSpec<"openai-responses">[] {
1823
- const filtered = dynamic.filter(e => !XAI_NON_CHAT_PREFIXES.some(p => e.id.startsWith(p)));
1722
+ const filtered = dynamic.filter(e => !isExcludedModel("xai-oauth", e.id));
1824
1723
 
1825
1724
  const byId = new Map<string, ModelSpec<"openai-responses">>(filtered.map(e => [e.id, e]));
1826
1725
  for (const curated of XAI_OAUTH_CURATED_MODELS) {
@@ -1935,18 +1834,10 @@ export function xaiOAuthModelManagerOptions(
1935
1834
  // 6.4 AIML API
1936
1835
  // ---------------------------------------------------------------------------
1937
1836
 
1938
- const AIML_API_NON_CHAT_MODEL_ID_PATTERN =
1939
- /(?:^|[/:._-])(?:audio|embed|embedding|embeddings|i2i|i2v|image|speech|t2i|t2v|tts|video)(?:$|[/:._-])/i;
1940
-
1941
- const AIML_API_NON_CHAT_MODEL_ID_SUBSTRINGS = ["dall-e", "dalle", "flux", "imagen", "sora", "veo", "whisper"] as const;
1942
-
1943
1837
  export function isLikelyAimlApiChatModelId(id: string): boolean {
1944
1838
  const normalized = id.trim().toLowerCase();
1945
1839
  if (!normalized) return false;
1946
- return (
1947
- !AIML_API_NON_CHAT_MODEL_ID_PATTERN.test(normalized) &&
1948
- !AIML_API_NON_CHAT_MODEL_ID_SUBSTRINGS.some(token => normalized.includes(token))
1949
- );
1840
+ return !isExcludedModel("aimlapi", normalized);
1950
1841
  }
1951
1842
 
1952
1843
  export interface AimlApiModelManagerConfig {
@@ -2002,35 +1893,12 @@ export interface SiliconFlowModelManagerConfig {
2002
1893
  * completions — and carries no per-model type field, so non-chat entries are
2003
1894
  * dropped by id to keep the picker usable.
2004
1895
  */
2005
- const SILICONFLOW_NON_CHAT_MODEL_TOKENS = [
2006
- "embedding",
2007
- "reranker",
2008
- "bge-",
2009
- "bce-",
2010
- "stable-diffusion",
2011
- "image",
2012
- "flux",
2013
- "kolors",
2014
- "sensevoice",
2015
- "cosyvoice",
2016
- "fish-speech",
2017
- "indextts",
2018
- "sovits",
2019
- "whisper",
2020
- "hunyuanvideo",
2021
- "wan2",
2022
- "ltx-video",
2023
- "speech",
2024
- "moderator",
2025
- "tts",
2026
- ] as const;
2027
-
2028
1896
  export function isLikelySiliconFlowChatModelId(id: string): boolean {
2029
1897
  const normalized = id.trim().toLowerCase();
2030
1898
  if (!normalized) {
2031
1899
  return false;
2032
1900
  }
2033
- return !SILICONFLOW_NON_CHAT_MODEL_TOKENS.some(token => normalized.includes(token));
1901
+ return !isExcludedModel("siliconflow", normalized);
2034
1902
  }
2035
1903
 
2036
1904
  /**
@@ -2176,10 +2044,14 @@ export function zhipuCodingPlanModelManagerOptions(
2176
2044
  _context: OpenAICompatibleModelMapperContext<"openai-completions">,
2177
2045
  ): ModelSpec<"openai-completions"> => {
2178
2046
  const id = defaults.id;
2047
+ const identity = classifyModel("zhipu-coding-plan", id, { lenient: true });
2179
2048
  return {
2180
2049
  ...defaults,
2181
- reasoning: isReasoningGlmModelId(id) || id.includes("thinking"),
2182
- input: isGlmVisionModelId(id) ? (["text", "image"] as const) : ["text"],
2050
+ reasoning:
2051
+ isGlmReasoningIdentity("zhipu-coding-plan", id, "4.5") || identity.thinkingVariant === true,
2052
+ // Vision-capable families are rule-owned (`providers/zhipu-coding-plan.kdl`
2053
+ // input-modalities) and corrected at build time.
2054
+ input: ["text"],
2183
2055
  compat: {
2184
2056
  thinkingFormat: "zai",
2185
2057
  reasoningContentField: "reasoning_content",
@@ -2231,10 +2103,9 @@ export const FIREWORKS_KIMI_K27_CODE_MAX_TOKENS = 65_536;
2231
2103
  * Fireworks' reported `max_completion_tokens` (65,536) instead.
2232
2104
  */
2233
2105
  export function isFireworksKimiK2ModelId(modelId: string): boolean {
2234
- const trimmed = modelId.toLowerCase();
2235
- if (/kimi[-._]?k2(?:[._-]?|p)7[-._]?code/.test(trimmed)) return false;
2236
- if (trimmed.startsWith("kimi-k2")) return true;
2237
- return /\/kimi-k2(?:p\d+)?(?:[._-]|$)/.test(trimmed);
2106
+ const identity = classifyModel("fireworks", modelId, { lenient: true });
2107
+ if (identity.class !== "kimi" || identity.family === "k2.7-code") return false;
2108
+ return identity.family?.startsWith("k2") === true;
2238
2109
  }
2239
2110
 
2240
2111
  /**
@@ -2255,7 +2126,8 @@ export function clampFireworksKimiMaxTokens(modelId: string, candidate: number |
2255
2126
  export const KIMI_K27_CODE_RECOMMENDED_MAX_TOKENS = 32_768;
2256
2127
 
2257
2128
  export function isKimiK27CodeModelId(modelId: string): boolean {
2258
- return /(?:^|\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i.test(modelId);
2129
+ const identity = classifyModel("fireworks", modelId, { lenient: true });
2130
+ return identity.class === "kimi" && identity.family === "k2.7-code";
2259
2131
  }
2260
2132
 
2261
2133
  export function clampKimiK27CodeMaxTokens(modelId: string, candidate: number): number;
@@ -2319,7 +2191,7 @@ export function stripFireworksDeepSeekThinkingToggle(
2319
2191
  model: ModelSpec<"openai-completions">,
2320
2192
  publicModelId: string,
2321
2193
  ): ModelSpec<"openai-completions"> {
2322
- if (!publicModelId.startsWith("deepseek-v4")) return model;
2194
+ if (!isDeepseekV4Generation("fireworks", publicModelId)) return model;
2323
2195
  const compat = model.compat;
2324
2196
  if (!compat?.extraBody || !("thinking" in compat.extraBody)) return model;
2325
2197
 
@@ -2582,6 +2454,304 @@ export function firepassModelManagerOptions(
2582
2454
  };
2583
2455
  }
2584
2456
 
2457
+ const CLINEPASS_MODELS_URL = `${CLINEPASS_API_BASE_URL}/ai/cline/recommended-models`;
2458
+ const CLINEPASS_FALLBACK_CONTEXT_WINDOW = 128_000;
2459
+ const CLINEPASS_FALLBACK_MAX_TOKENS = 8_192;
2460
+
2461
+ function buildClinePassFallbackModel(id: string): ModelSpec<"openai-completions"> {
2462
+ return {
2463
+ id,
2464
+ name: id,
2465
+ api: "openai-completions",
2466
+ provider: "cline-pass",
2467
+ baseUrl: CLINEPASS_API_BASE_URL,
2468
+ reasoning: true,
2469
+ input: ["text"],
2470
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2471
+ contextWindow: CLINEPASS_FALLBACK_CONTEXT_WINDOW,
2472
+ maxTokens: CLINEPASS_FALLBACK_MAX_TOKENS,
2473
+ compat: { supportsReasoningEffort: false },
2474
+ };
2475
+ }
2476
+
2477
+ /**
2478
+ * Live reference catalog mirroring Cline's own client enrichment: the official
2479
+ * CLI fills roster metadata from OpenRouter's public `/models` catalog (their
2480
+ * gateway routes through it), keyed by full id and by slug. Consulted only for
2481
+ * ids the bundled reference has no real upstream entry for: the bundle stays
2482
+ * authoritative for known ids, and unknown ids ride fresh live data instead of
2483
+ * the conservative constants until the next regen.
2484
+ */
2485
+ interface ClinePassLiveCatalogEntry {
2486
+ name?: string;
2487
+ contextWindow?: number;
2488
+ maxTokens?: number;
2489
+ cost?: ModelSpec<"openai-completions">["cost"];
2490
+ reasoning?: boolean;
2491
+ input?: ("text" | "image")[];
2492
+ }
2493
+
2494
+ interface ClinePassLiveCatalog {
2495
+ byId: ReadonlyMap<string, ClinePassLiveCatalogEntry>;
2496
+ bySlug: ReadonlyMap<string, ClinePassLiveCatalogEntry>;
2497
+ }
2498
+
2499
+ const CLINE_PASS_LIVE_CATALOG_URL = "https://openrouter.ai/api/v1/models";
2500
+
2501
+ /** Enrichment tier, never authoritative: any failure degrades to the bundled path. */
2502
+ async function fetchClinePassLiveCatalog(fetchImpl: FetchImpl): Promise<ClinePassLiveCatalog | undefined> {
2503
+ try {
2504
+ const response = await withCatalogDiscoveryTimeout(5_000, signal =>
2505
+ fetchImpl(CLINE_PASS_LIVE_CATALOG_URL, { signal }),
2506
+ );
2507
+ if (!response.ok) return undefined;
2508
+ const payload: unknown = await response.json();
2509
+ if (!isRecord(payload) || !Array.isArray(payload.data)) return undefined;
2510
+ const byId = new Map<string, ClinePassLiveCatalogEntry>();
2511
+ const bySlug = new Map<string, ClinePassLiveCatalogEntry>();
2512
+ for (const raw of payload.data) {
2513
+ if (!isRecord(raw) || typeof raw.id !== "string" || !raw.id) continue;
2514
+ const pricing = isRecord(raw.pricing) ? raw.pricing : undefined;
2515
+ const topProvider = isRecord(raw.top_provider) ? raw.top_provider : undefined;
2516
+ const params = Array.isArray(raw.supported_parameters) ? raw.supported_parameters : undefined;
2517
+ const modality = String(isRecord(raw.architecture) ? (raw.architecture.modality ?? "") : "");
2518
+ const contextWindow = toPositiveNumber(raw.context_length, 0);
2519
+ const maxTokens = toPositiveNumber(topProvider?.max_completion_tokens, 0);
2520
+ const entry: ClinePassLiveCatalogEntry = {
2521
+ ...(typeof raw.name === "string" && raw.name ? { name: raw.name.replace(/^[^:]+:\s*/, "") } : {}),
2522
+ ...(contextWindow > 0 ? { contextWindow } : {}),
2523
+ ...(maxTokens > 0 ? { maxTokens } : {}),
2524
+ ...(pricing
2525
+ ? {
2526
+ cost: {
2527
+ input: parseFloat(String(pricing.prompt ?? "0")) * 1_000_000,
2528
+ output: parseFloat(String(pricing.completion ?? "0")) * 1_000_000,
2529
+ cacheRead: parseFloat(String(pricing.input_cache_read ?? "0")) * 1_000_000,
2530
+ cacheWrite: parseFloat(String(pricing.input_cache_write ?? "0")) * 1_000_000,
2531
+ },
2532
+ }
2533
+ : {}),
2534
+ ...(params ? { reasoning: params.includes("reasoning") } : {}),
2535
+ ...(modality
2536
+ ? { input: modality.includes("image") ? (["text", "image"] as const) : (["text"] as const) }
2537
+ : {}),
2538
+ };
2539
+ byId.set(raw.id, entry);
2540
+ // Cline's `buildModelsNameMap` algorithm: roster ids are lab-less slugs
2541
+ // ("glm-5.2"), so the catalog is also keyed by its last id segment.
2542
+ const slug = raw.id.split("/").at(-1);
2543
+ if (slug) bySlug.set(slug, entry);
2544
+ }
2545
+ return byId.size > 0 ? { byId, bySlug } : undefined;
2546
+ } catch {
2547
+ return undefined;
2548
+ }
2549
+ }
2550
+
2551
+ /** Zero legs mean "unpriced upstream", not "free" — pass models bill quota regardless. */
2552
+ function hasClinePassBillableCost(cost: ModelSpec<"openai-completions">["cost"] | undefined): boolean {
2553
+ return cost !== undefined && (cost.input > 0 || cost.output > 0);
2554
+ }
2555
+
2556
+ function buildCuratedClinePassModel(
2557
+ id: string,
2558
+ tier: "subscription" | "free",
2559
+ ): ModelSpec<"openai-completions"> | undefined {
2560
+ const metadata = getClinePassModelMetadata(id);
2561
+ if (!metadata || metadata.tier !== tier) return undefined;
2562
+ return {
2563
+ ...buildClinePassFallbackModel(id),
2564
+ name: metadata.name,
2565
+ contextWindow: metadata.contextWindow,
2566
+ maxTokens: metadata.maxTokens,
2567
+ input: metadata.input,
2568
+ cost: metadata.cost,
2569
+ reasoning: metadata.reasoning,
2570
+ thinking: metadata.thinking,
2571
+ compat: {
2572
+ ...(metadata.thinking === undefined ? { supportsReasoningEffort: false } : {}),
2573
+ ...(tier === "free" ? { wireModelIdMode: "raw" as const } : {}),
2574
+ },
2575
+ };
2576
+ }
2577
+
2578
+ /**
2579
+ * Subscription models bill against plan quota, not per token — but the catalog still
2580
+ * carries the upstream list price so cost display reads as API-equivalent spend. Same
2581
+ * policy as the codex/antigravity generator fixups and the zai PAYG descriptor swap
2582
+ * (issue #5598: an all-$0 roster surfaces every model as "free" in the picker, which
2583
+ * was treated as a bug there). The price comes from the bundled upstream reference,
2584
+ * so it works offline and survives roster refreshes (`preferDiscoveryCost` never lets
2585
+ * a zero overwrite it). Ids with no upstream reference keep the honest zero until
2586
+ * models.dev prices them.
2587
+ */
2588
+ function buildClinePassSubscriptionModel(
2589
+ id: string,
2590
+ references: ReadonlyMap<string, ModelSpec<"openai-completions">>,
2591
+ liveCatalog?: ClinePassLiveCatalog,
2592
+ ): ModelSpec<"openai-completions"> {
2593
+ const curated = buildCuratedClinePassModel(id, "subscription");
2594
+ if (curated) return curated;
2595
+ const base = references.get(id) ?? buildClinePassFallbackModel(id);
2596
+ const reference = resolveModelReference(id, getBundledModelReferenceIndex());
2597
+ // A reference pointing back at the ClinePass bundle means the last regen
2598
+ // knew nothing about the id and encoded fallback constants — treat as none.
2599
+ const upstream = reference && reference.provider !== "cline-pass" ? reference : undefined;
2600
+ // Same degeneracy on the bundle side: a bundled entry whose limits are the
2601
+ // fallback pair was written by a regen that had no upstream data for the id.
2602
+ const baseLimitsAreFallback =
2603
+ base.contextWindow === CLINEPASS_FALLBACK_CONTEXT_WINDOW && base.maxTokens === CLINEPASS_FALLBACK_MAX_TOKENS;
2604
+ if (upstream) {
2605
+ // Known to the bundle with real limits: overlay the list price only.
2606
+ // Otherwise full enrichment, like the free-tier path.
2607
+ return references.has(id) && !baseLimitsAreFallback
2608
+ ? { ...base, cost: upstream.cost }
2609
+ : {
2610
+ ...base,
2611
+ name: references.has(id) ? base.name : upstream.name,
2612
+ reasoning: upstream.reasoning,
2613
+ input: upstream.input,
2614
+ cost: upstream.cost,
2615
+ contextWindow: upstream.contextWindow,
2616
+ maxTokens: upstream.maxTokens,
2617
+ ...(upstream.reasoning ? {} : { thinking: undefined }),
2618
+ };
2619
+ }
2620
+ const live = liveCatalog?.bySlug.get(id);
2621
+ if (!live) return base;
2622
+ return {
2623
+ ...base,
2624
+ ...(live.name ? { name: live.name } : {}),
2625
+ ...(live.contextWindow ? { contextWindow: live.contextWindow } : {}),
2626
+ ...(live.maxTokens ? { maxTokens: live.maxTokens } : {}),
2627
+ ...(hasClinePassBillableCost(live.cost) ? { cost: live.cost } : {}),
2628
+ ...(live.reasoning !== undefined ? { reasoning: live.reasoning } : {}),
2629
+ ...(live.input ? { input: live.input } : {}),
2630
+ ...(live.reasoning === false ? { thinking: undefined } : {}),
2631
+ };
2632
+ }
2633
+
2634
+ /**
2635
+ * Free-tier entries arrive as full OpenRouter-style ids (`deepseek/deepseek-v4-flash`,
2636
+ * `poolside/laguna-s-2.1:free`) and ride usage billing at $0 outside the subscription
2637
+ * quota — their cost is genuinely zero, unlike the subscription roster's list-price
2638
+ * display. Limits and modalities resolve through the bundled upstream reference (its
2639
+ * candidate expansion treats provider prefixes and trailing `:free`-style markers as
2640
+ * identity-preserving), so known models self-enrich offline. Compat and thinking are
2641
+ * deliberately NOT inherited: those describe the reference's native host, while the
2642
+ * Cline gateway dialect is resolved per provider/family. The raw wire tag is
2643
+ * bucket-derived — the gateway already namespaces these ids, so the cline-pass prefix
2644
+ * transform must never apply to them.
2645
+ */
2646
+ function buildClinePassFreeModel(id: string, liveCatalog?: ClinePassLiveCatalog): ModelSpec<"openai-completions"> {
2647
+ const curated = buildCuratedClinePassModel(id, "free");
2648
+ if (curated) return curated;
2649
+ const reference = resolveModelReference(id, getBundledModelReferenceIndex());
2650
+ const upstream = reference && reference.provider !== "cline-pass" ? reference : undefined;
2651
+ // Free ids are OpenRouter-native, so the live catalog matches by full id
2652
+ // before falling back to the slug — the official client's own lookup order.
2653
+ const live = liveCatalog?.byId.get(id) ?? liveCatalog?.bySlug.get(id.split("/").at(-1) ?? id);
2654
+ // References can carry their own free markers (e.g. OpenRouter-style
2655
+ // "Laguna S 2.1 (free)"); strip before applying our single tier suffix.
2656
+ const baseName = (upstream?.name ?? live?.name ?? id.split("/").at(-1) ?? id)
2657
+ .replace(/\s*\(free\)\s*$/i, "")
2658
+ .replace(/:free$/i, "")
2659
+ .trim();
2660
+ const fallback = buildClinePassFallbackModel(id);
2661
+ const reasoning = upstream?.reasoning ?? live?.reasoning ?? fallback.reasoning;
2662
+ return {
2663
+ ...fallback,
2664
+ name: `${baseName || id} (free)`,
2665
+ reasoning,
2666
+ input: upstream?.input ?? live?.input ?? fallback.input,
2667
+ contextWindow: upstream?.contextWindow ?? live?.contextWindow ?? fallback.contextWindow,
2668
+ maxTokens: upstream?.maxTokens ?? live?.maxTokens ?? fallback.maxTokens,
2669
+ // A non-reasoning reference must not carry the Cline effort ladder.
2670
+ ...(reasoning ? {} : { thinking: undefined }),
2671
+ compat: { supportsReasoningEffort: false, wireModelIdMode: "raw" },
2672
+ };
2673
+ }
2674
+
2675
+ async function fetchClinePassModels(
2676
+ fetchImpl: FetchImpl,
2677
+ references: ReadonlyMap<string, ModelSpec<"openai-completions">>,
2678
+ ): Promise<ModelSpec<"openai-completions">[]> {
2679
+ // The roster is authoritative for ids; the live reference catalog is a pure
2680
+ // enrichment tier fetched alongside and tolerated away (offline → bundle).
2681
+ const [response, liveCatalog] = await Promise.all([
2682
+ withCatalogDiscoveryTimeout(5_000, signal =>
2683
+ fetchImpl(CLINEPASS_MODELS_URL, { signal, headers: clinePassClientHeaders() }),
2684
+ ),
2685
+ fetchClinePassLiveCatalog(fetchImpl),
2686
+ ]);
2687
+ if (!response.ok) {
2688
+ throw new Error(`Failed to fetch ClinePass models: HTTP ${response.status}`);
2689
+ }
2690
+ const payload: unknown = await response.json();
2691
+ if (!isRecord(payload) || !Array.isArray(payload.clinePass)) {
2692
+ throw new Error("ClinePass model catalog response is missing clinePass");
2693
+ }
2694
+
2695
+ const models = new Map<string, ModelSpec<"openai-completions">>();
2696
+ for (const entry of payload.clinePass) {
2697
+ if (!isRecord(entry) || typeof entry.id !== "string") {
2698
+ continue;
2699
+ }
2700
+ const wireId = entry.id.trim();
2701
+ if (!wireId.startsWith("cline-pass/")) {
2702
+ continue;
2703
+ }
2704
+ const id = toClinePassPublicModelId(wireId).trim();
2705
+ if (!id) {
2706
+ continue;
2707
+ }
2708
+ models.set(id, buildClinePassSubscriptionModel(id, references, liveCatalog));
2709
+ }
2710
+ if (models.size === 0) {
2711
+ throw new Error("ClinePass model catalog contains no valid model IDs");
2712
+ }
2713
+
2714
+ // Free tier rides the same key and gateway at $0 outside the subscription
2715
+ // quota. Optional overlay: entries validate individually, a cline-pass/-shaped
2716
+ // entry belongs to the pass bucket, pass ids win any collision, and pass-first
2717
+ // insertion order keeps the registry's first-available fallback on a
2718
+ // subscription model.
2719
+ const freeEntries = Array.isArray(payload.free) ? payload.free : [];
2720
+ for (const entry of freeEntries) {
2721
+ if (!isRecord(entry) || typeof entry.id !== "string") {
2722
+ continue;
2723
+ }
2724
+ const id = entry.id.trim();
2725
+ if (!id || id.startsWith("cline-pass/") || models.has(id)) {
2726
+ continue;
2727
+ }
2728
+ models.set(id, buildClinePassFreeModel(id, liveCatalog));
2729
+ }
2730
+ return [...models.values()];
2731
+ }
2732
+
2733
+ /**
2734
+ * Cline's OpenAI-compatible `/models` route is absent, but its public
2735
+ * recommended-models endpoint is the authoritative ClinePass roster: the
2736
+ * `clinePass` bucket lists subscription models and the optional `free` bucket
2737
+ * adds $0 usage-billing models selectable with the same key. Metadata resolves
2738
+ * in three tiers: the current Cline-published metadata snapshot first, bundled
2739
+ * upstream references and OpenRouter enrichment for future ids second, then
2740
+ * conservative constants. Newly added ids remain usable before the next regen;
2741
+ * known ids retain exact Cline limits, plan pricing, and reasoning controls.
2742
+ */
2743
+ export function clinePassModelManagerOptions(config?: ModelManagerConfig): ModelManagerOptions<"openai-completions"> {
2744
+ const references = new Map(
2745
+ (getBundledModels("cline-pass") as Model<"openai-completions">[]).map(model => [model.id, toModelSpec(model)]),
2746
+ );
2747
+ const fetchImpl = discoveryFetch(config?.fetch);
2748
+ return {
2749
+ providerId: "cline-pass",
2750
+ dynamicModelsAuthoritative: true,
2751
+ fetchDynamicModels: () => fetchClinePassModels(fetchImpl, references),
2752
+ };
2753
+ }
2754
+
2585
2755
  // ---------------------------------------------------------------------------
2586
2756
  // 7.7 Wafer Serverless
2587
2757
  // ---------------------------------------------------------------------------
@@ -2629,6 +2799,8 @@ export function resolveWaferServerlessThinkingFormat(
2629
2799
  modelId: string,
2630
2800
  upstreamProvider: unknown,
2631
2801
  ): WaferThinkingFormat | undefined {
2802
+ // residue: parses Wafer's upstream-provider metadata FIELD (vendor
2803
+ // spellings), not a model id; the id fallback below is classify-driven.
2632
2804
  const upstream = typeof upstreamProvider === "string" ? upstreamProvider.trim().toLowerCase() : "";
2633
2805
  if (upstream) {
2634
2806
  if (
@@ -2650,7 +2822,8 @@ export function resolveWaferServerlessThinkingFormat(
2650
2822
  // Older Wafer snapshots (and some endpoint responses) do not carry the
2651
2823
  // upstream-provider hint. Only GLM/Kimi need a sparse override: qwen and
2652
2824
  // deepseek IDs are resolved safely by `buildOpenAICompat` from the model id.
2653
- return isReasoningGlmModelId(modelId.toLowerCase()) || isKimiModelId(modelId) ? "zai" : undefined;
2825
+ const identity = classifyModel("wafer-serverless", modelId, { lenient: true });
2826
+ return isGlmReasoningIdentity("wafer-serverless", modelId, "4.5") || identity.class === "kimi" ? "zai" : undefined;
2654
2827
  }
2655
2828
 
2656
2829
  function mapWaferModel(
@@ -2782,56 +2955,12 @@ function openCodeBaseUrlForApi(api: Api, basePath: string): string {
2782
2955
  // /v1/models discovery mapper in openCodeModelManagerOptions — the gateways
2783
2956
  // list ids that models.dev omits entirely (muse-spark-1.2[-contributor] on
2784
2957
  // opencode-go, #8957), so discovery cannot rely on bundled references alone.
2785
- //
2786
- // OpenCode Zen: models.dev declares minimax-m3-free (and forward-compat
2787
- // minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but the Zen gateway
2788
- // only serves them at https://opencode.ai/zen/v1/chat/completions (verified
2789
- // against the live /v1/models response — minimax-m3-free is listed there, and
2790
- // the gateway has no /v1/messages route for it). Without this override the
2791
- // resolver POSTs anthropic-shaped requests to /v1/messages and the UI surfaces
2792
- // raw <invoke>/<|minimax|>/<tool_call> markup (#1617).
2793
- const OPENCODE_ZEN_API_ID_OVERRIDES: Readonly<Record<string, Api>> = {
2794
- "minimax-m3": "openai-completions",
2795
- "minimax-m3-free": "openai-completions",
2796
- };
2797
- // OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus
2798
- // (and now also minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but
2799
- // the OpenCode Go gateway only serves them at
2800
- // `https://opencode.ai/zen/go/v1/chat/completions` (verified against
2801
- // https://opencode.ai/zen/go/v1/models and the upstream endpoint table at
2802
- // https://opencode.ai/docs/go/#endpoints — minimax-m2.5 works the same way
2803
- // and lacks an `npm` field on models.dev so it already falls through to the
2804
- // openai-completions default). Without this override the resolver would POST
2805
- // anthropic-style requests to /v1/messages and the gateway would return its
2806
- // `Page Not Found` HTML (issue #887 for the qwen/m2.7 entries; minimax-m3
2807
- // and minimax-m3-free added under #1617 for the same root cause).
2808
- //
2809
- // deepseek-v4-flash is the inverse case: it falls through to
2810
- // openai-completions by default, but the Go gateway's
2811
- // /zen/go/v1/chat/completions route does not work for this model while
2812
- // /zen/go/v1/responses does (user-verified against the live gateway,
2813
- // 2026-08-08; Flash only — deepseek-v4-pro serves fine on chat completions).
2814
- //
2815
- // muse-spark-1.2 / muse-spark-1.2-contributor are the same inverse case, but
2816
- // worse: models.dev does not list them under opencode-go at all, so they have
2817
- // no bundled reference and only exist via live gateway discovery. Without the
2818
- // discovery-side pin they default to openai-completions even though the
2819
- // gateway only serves them at /zen/go/v1/responses (@ai-sdk/openai per
2820
- // https://opencode.ai/docs/go/#endpoints). The completions parser then closes
2821
- // the stream with no finish_reason on every tool-call turn (#8957).
2822
- const OPENCODE_GO_API_ID_OVERRIDES: Readonly<Record<string, Api>> = {
2823
- "deepseek-v4-flash": "openai-responses",
2824
- "muse-spark-1.2": "openai-responses",
2825
- "muse-spark-1.2-contributor": "openai-responses",
2826
- "minimax-m2.7": "openai-completions",
2827
- "minimax-m3": "openai-completions",
2828
- "minimax-m3-free": "openai-completions",
2829
- "qwen3.5-plus": "openai-completions",
2830
- "qwen3.6-plus": "openai-completions",
2831
- };
2958
+ // The per-id gateway-verified route pins live in the `api-routes` behavior
2959
+ // rules (`runtime/behavior.kdl`; #887, #1617, #8957).
2832
2960
  // Runtime-discovered rows cached before model-identity corrections retain
2833
2961
  // stale capability metadata until the authoritative catalog TTL expires.
2834
2962
  const OPENCODE_CACHE_MIGRATION_MODEL_IDS = ["glm-5.3-flash"] as const;
2963
+ const OPENCODE_ZEN_CACHE_MIGRATION_MODEL_IDS = ["gemini-3.7-flash"] as const;
2835
2964
 
2836
2965
  // Billing-variant suffixes the OpenCode gateways append to a base model id
2837
2966
  // without changing its transport (`deepseek-v4-flash-free`,
@@ -2862,7 +2991,13 @@ function openCodeModelManagerOptions(
2862
2991
  const siblingReferences = createBundledReferenceMap<Api>(
2863
2992
  providerId === "opencode-go" ? "opencode-zen" : "opencode-go",
2864
2993
  );
2865
- const apiOverrides = providerId === "opencode-go" ? OPENCODE_GO_API_ID_OVERRIDES : OPENCODE_ZEN_API_ID_OVERRIDES;
2994
+ const apiOverrides = (id: string): Api | undefined => {
2995
+ // Route pins live in the `api-routes` behavior rules.
2996
+ const route = apiRouteFor(providerId, id)?.api;
2997
+ return route === "openai-completions" || route === "openai-responses" || route === "anthropic-messages"
2998
+ ? route
2999
+ : undefined;
3000
+ };
2866
3001
  // Routes a discovered id with no same-provider metadata. models.dev lags
2867
3002
  // the gateway (muse-spark-1.2[-contributor] shipped gateway-first, #8957),
2868
3003
  // so borrow the openai-responses route from the sibling gateway or the
@@ -2881,8 +3016,8 @@ function openCodeModelManagerOptions(
2881
3016
  const resolveApi = (id: string, defaultApi: Api): Api => {
2882
3017
  const base = openCodeBaseModelId(id);
2883
3018
  return (
2884
- apiOverrides[id] ??
2885
- (base ? apiOverrides[base] : undefined) ??
3019
+ apiOverrides(id) ??
3020
+ (base ? apiOverrides(base) : undefined) ??
2886
3021
  references.get(id)?.api ??
2887
3022
  fallbackApi(id, base) ??
2888
3023
  defaultApi
@@ -2894,8 +3029,12 @@ function openCodeModelManagerOptions(
2894
3029
  dynamicModelsAuthoritative: true,
2895
3030
  // Per-id route pins and capability migrations are cache identity:
2896
3031
  // without this, rows cached before a correction keep the stale route or
2897
- // thinking surface until TTL expiry (#8957, #9960).
2898
- dropCachedModelIdsOnStaticMismatch: [...Object.keys(apiOverrides), ...OPENCODE_CACHE_MIGRATION_MODEL_IDS],
3032
+ // thinking surface until TTL expiry (#8957, #9960, #10543).
3033
+ dropCachedModelIdsOnStaticMismatch: [
3034
+ ...apiRouteExactModelIds(providerId),
3035
+ ...OPENCODE_CACHE_MIGRATION_MODEL_IDS,
3036
+ ...(providerId === "opencode-zen" ? OPENCODE_ZEN_CACHE_MIGRATION_MODEL_IDS : []),
3037
+ ],
2899
3038
  modelsDev: {
2900
3039
  fetch: () => fetchRevalidatedWellKnownModelsWithTimeout(config?.fetch),
2901
3040
  map: payload => {
@@ -2922,12 +3061,13 @@ function openCodeModelManagerOptions(
2922
3061
  // stencil fallback; the fallback never selects a transport.
2923
3062
  const api = resolveApi(defaults.id, defaults.api);
2924
3063
  const baseUrl = openCodeBaseUrlForApi(api, basePath);
2925
- if (isMuseSparkModelId(defaults.id)) {
2926
- // Gateway lists these as bare ids with no capability
2927
- // metadata and no local bundled row, so the generic
2928
- // defaults would hide the effort dial
2929
- // (`reasoning: false`). Keep the pinned/fallback route
2930
- // and restore the documented thinking surface.
3064
+ const lineage = museSparkLineageSpec(defaults.id);
3065
+ if (lineage) {
3066
+ // Gateway lists Muse Spark as bare ids with no capability
3067
+ // metadata and often no local bundled row, so the generic
3068
+ // defaults would hide the effort dial (`reasoning: false`).
3069
+ // Keep the pinned/fallback route and restore the lineage's
3070
+ // thinking surface.
2931
3071
  return {
2932
3072
  ...(reference ?? defaults),
2933
3073
  id: defaults.id,
@@ -2936,10 +3076,16 @@ function openCodeModelManagerOptions(
2936
3076
  provider: providerId,
2937
3077
  baseUrl,
2938
3078
  reasoning: true,
2939
- input: reference?.input ?? ["text", "image"],
2940
- thinking: reference?.thinking ?? META_MUSE_SPARK_THINKING,
2941
- contextWindow: toPositiveNumber(entry.context_length, reference?.contextWindow ?? 1_048_576),
2942
- maxTokens: toPositiveNumber(entry.max_completion_tokens, reference?.maxTokens ?? 131_072),
3079
+ input: reference?.input ?? lineage.input,
3080
+ thinking: reference?.thinking ?? lineage.thinking,
3081
+ contextWindow: toPositiveNumber(
3082
+ entry.context_length,
3083
+ reference?.contextWindow ?? lineage.contextWindow,
3084
+ ),
3085
+ maxTokens: toPositiveNumber(
3086
+ entry.max_completion_tokens,
3087
+ reference?.maxTokens ?? lineage.maxTokens,
3088
+ ),
2943
3089
  };
2944
3090
  }
2945
3091
  if (!reference) {
@@ -3015,7 +3161,9 @@ export function ollamaModelManagerOptions(config?: OllamaModelManagerConfig): Mo
3015
3161
  model.contextWindow = metadata.contextWindow;
3016
3162
  if (metadata.reasoning !== undefined) {
3017
3163
  model.reasoning = metadata.reasoning;
3018
- model.thinking = metadata.thinking;
3164
+ // The effort ladder is rule-owned (`providers/ollama.kdl`);
3165
+ // derived at build time from the reasoning flag.
3166
+ model.thinking = undefined;
3019
3167
  }
3020
3168
  if (metadata.input) {
3021
3169
  model.input = metadata.input;
@@ -3117,7 +3265,7 @@ export function openrouterModelManagerOptions(
3117
3265
  ? topProvider.max_completion_tokens
3118
3266
  : baseModel.maxTokens,
3119
3267
  ...(!supportsToolChoice && {
3120
- compat: { ...(baseModel.compat ?? {}), supportsToolChoice: false },
3268
+ compat: { ...baseModel.compat, supportsToolChoice: false },
3121
3269
  }),
3122
3270
  };
3123
3271
  },
@@ -3158,7 +3306,7 @@ function isZenMuxAnthropicModel(entry: OpenAICompatibleModelRecord, modelId: str
3158
3306
  if (typeof entry.owned_by === "string" && entry.owned_by.toLowerCase() === "anthropic") {
3159
3307
  return true;
3160
3308
  }
3161
- return modelId.toLowerCase().startsWith("anthropic/");
3309
+ return classifyModel("zenmux", modelId, { lenient: true }).class === "anthropic";
3162
3310
  }
3163
3311
 
3164
3312
  function getZenMuxPricingValue(pricings: Record<string, unknown> | undefined, key: string): number {
@@ -3352,6 +3500,20 @@ export const ALIBABA_TOKEN_PLAN_STATIC_MODELS: readonly ModelSpec<"openai-comple
3352
3500
  },
3353
3501
  compat: ALIBABA_TOKEN_PLAN_QWEN_EFFORT_COMPAT,
3354
3502
  },
3503
+ {
3504
+ id: "qwen3.8-flash",
3505
+ name: "Qwen3.8 Flash",
3506
+ api: "openai-completions",
3507
+ provider: "alibaba-token-plan",
3508
+ baseUrl: ALIBABA_TOKEN_PLAN_BASE_URL,
3509
+ reasoning: true,
3510
+ input: ["text", "image"],
3511
+ cost: ALIBABA_TOKEN_PLAN_COST,
3512
+ contextWindow: 1_000_000,
3513
+ maxTokens: 131_072,
3514
+ thinking: ALIBABA_TOKEN_PLAN_REASONING,
3515
+ compat: ALIBABA_TOKEN_PLAN_COMPAT,
3516
+ },
3355
3517
  {
3356
3518
  id: "qwen3.7-max",
3357
3519
  name: "Qwen3.7 Max",
@@ -3430,82 +3592,9 @@ export const ALIBABA_TOKEN_PLAN_STATIC_MODELS: readonly ModelSpec<"openai-comple
3430
3592
  },
3431
3593
  ];
3432
3594
 
3433
- /**
3434
- * Metadata for Alibaba Token Plan models that are dynamically discovered but not
3435
- * in the static catalog. Context window and max tokens are sourced from
3436
- * official model documentation and provider catalogs. Unknown future models
3437
- * remain available with null limits instead of assigning unsafe guessed limits.
3438
- */
3439
- export interface AlibabaTokenPlanModelLimits {
3440
- contextWindow: number;
3441
- maxTokens: number;
3442
- }
3443
-
3444
- export const ALIBABA_TOKEN_PLAN_DISCOVERED_MODEL_LIMITS: Readonly<Record<string, AlibabaTokenPlanModelLimits>> = {
3445
- "qwen3.6-plus": {
3446
- contextWindow: 1_000_000,
3447
- maxTokens: 65_536,
3448
- },
3449
- "qwen3.8-max": {
3450
- contextWindow: 1_000_000,
3451
- maxTokens: 131_072,
3452
- },
3453
- "deepseek-v4-flash": {
3454
- contextWindow: 1_000_000,
3455
- maxTokens: 384_000,
3456
- },
3457
- "deepseek-v4-flash-0731": {
3458
- contextWindow: 1_000_000,
3459
- maxTokens: 384_000,
3460
- },
3461
- "deepseek-v4-pro-0813": {
3462
- contextWindow: 1_000_000,
3463
- maxTokens: 384_000,
3464
- },
3465
- "deepseek-v3.2": {
3466
- contextWindow: 131_072,
3467
- maxTokens: 65_536,
3468
- },
3469
- "glm-5.1": {
3470
- contextWindow: 202_752,
3471
- maxTokens: 128_000,
3472
- },
3473
- "glm-5": {
3474
- contextWindow: 202_752,
3475
- maxTokens: 16_384,
3476
- },
3477
- "kimi-k2.7-code": {
3478
- contextWindow: 262_144,
3479
- maxTokens: 262_144,
3480
- },
3481
- "kimi-k2.6": {
3482
- contextWindow: 262_144,
3483
- maxTokens: 262_144,
3484
- },
3485
- "kimi-k2.5": {
3486
- contextWindow: 262_144,
3487
- maxTokens: 98_304,
3488
- },
3489
- "minimax-m2.5": {
3490
- contextWindow: 196_608,
3491
- maxTokens: 32_768,
3492
- },
3493
- };
3494
-
3495
- const ALIBABA_TOKEN_PLAN_NON_CHAT_MODEL_PREFIXES = [
3496
- "fun-asr",
3497
- "happyhorse-",
3498
- "qwen-audio-",
3499
- "qwen-image-",
3500
- "text-embedding-",
3501
- "wan2.7-",
3502
- ] as const;
3503
-
3504
3595
  function isAlibabaTokenPlanChatModelId(id: string): boolean {
3505
3596
  const normalized = id.trim().toLowerCase();
3506
- return (
3507
- normalized.length > 0 && !ALIBABA_TOKEN_PLAN_NON_CHAT_MODEL_PREFIXES.some(prefix => normalized.startsWith(prefix))
3508
- );
3597
+ return normalized.length > 0 && !isExcludedModel("alibaba-token-plan", normalized);
3509
3598
  }
3510
3599
 
3511
3600
  export interface AlibabaTokenPlanModelManagerConfig {
@@ -3547,16 +3636,18 @@ export function alibabaTokenPlanModelManagerOptions(
3547
3636
  };
3548
3637
  }
3549
3638
  const normalizedId = defaults.id.trim().toLowerCase();
3550
- const limits = ALIBABA_TOKEN_PLAN_DISCOVERED_MODEL_LIMITS[normalizedId];
3639
+ // Discovery carries no limit fields; the `model-limits` behavior
3640
+ // rules pin values from official model documentation.
3641
+ const limits = modelLimitsFor("alibaba-token-plan", normalizedId);
3551
3642
  const enriched = limits
3552
3643
  ? {
3553
3644
  ...defaults,
3554
- contextWindow: limits.contextWindow,
3555
- maxTokens: limits.maxTokens,
3645
+ contextWindow: limits.context ?? defaults.contextWindow,
3646
+ maxTokens: limits.maxTokens ?? defaults.maxTokens,
3556
3647
  }
3557
3648
  : defaults;
3558
3649
 
3559
- if (normalizedId.startsWith("deepseek-v4")) {
3650
+ if (isDeepseekV4Generation("alibaba-token-plan", normalizedId)) {
3560
3651
  return {
3561
3652
  ...enriched,
3562
3653
  reasoning: true,
@@ -3685,7 +3776,12 @@ function kimiSupportsReasoning(entry: OpenAICompatibleModelRecord, modelId: stri
3685
3776
  case "no":
3686
3777
  return false;
3687
3778
  default:
3688
- return entry.supports_reasoning === true || modelId.includes("thinking");
3779
+ // Without a reasoning bit, the classified thinking-variant fact names
3780
+ // the reasoning sibling.
3781
+ return (
3782
+ entry.supports_reasoning === true ||
3783
+ classifyModel("kimi-code", modelId, { lenient: true }).thinkingVariant === true
3784
+ );
3689
3785
  }
3690
3786
  }
3691
3787
 
@@ -3701,15 +3797,13 @@ function mapKimiApiFormat(protocol: unknown): OpenAICompat["kimiApiFormat"] {
3701
3797
  * per-family caps instead of a blanket constant. Values match models.dev's
3702
3798
  * `kimi-for-coding` and `moonshotai` kimi-k3 entries. See #6711.
3703
3799
  */
3704
- export const KIMI_CODE_K3_MAX_TOKENS = 131_072;
3705
- export const KIMI_CODE_FOR_CODING_MAX_TOKENS = 32_768;
3706
-
3707
3800
  /** Fallback output cap for Kimi Code families without a documented ceiling (legacy K2 discovery rows). */
3708
3801
  export const KIMI_CODE_DEFAULT_MAX_TOKENS = 32_000;
3709
3802
 
3710
3803
  /**
3711
- * Resolve a Kimi Code model's output ceiling from its id: `k3` / `k3-256k` ->
3712
- * 131072, `kimi-for-coding[-highspeed]` -> 32768, everything else -> `fallback`.
3804
+ * Resolve a Kimi Code model's output ceiling; the per-family caps are
3805
+ * rule-owned (`runtime/behavior.kdl` model-limits), everything else ->
3806
+ * `fallback`.
3713
3807
  */
3714
3808
  export function kimiCodeMaxTokens(modelId: string, fallback?: number): number;
3715
3809
  export function kimiCodeMaxTokens(modelId: string, fallback: number | null): number | null;
@@ -3717,10 +3811,7 @@ export function kimiCodeMaxTokens(
3717
3811
  modelId: string,
3718
3812
  fallback: number | null = KIMI_CODE_DEFAULT_MAX_TOKENS,
3719
3813
  ): number | null {
3720
- const id = modelId.toLowerCase();
3721
- if (id.startsWith("k3")) return KIMI_CODE_K3_MAX_TOKENS;
3722
- if (id.startsWith("kimi-for-coding")) return KIMI_CODE_FOR_CODING_MAX_TOKENS;
3723
- return fallback;
3814
+ return modelLimitsFor("kimi-code", modelId.trim().toLowerCase())?.maxTokens ?? fallback;
3724
3815
  }
3725
3816
 
3726
3817
  export function kimiCodeModelManagerOptions(
@@ -3753,7 +3844,9 @@ export function kimiCodeModelManagerOptions(
3753
3844
  ...defaults,
3754
3845
  name: typeof entry.display_name === "string" ? entry.display_name : defaults.name,
3755
3846
  reasoning,
3756
- input: entry.supports_image_in === true || id.includes("k2.5") ? ["text", "image"] : ["text"],
3847
+ // K2.5's misreported vision support is rule-owned
3848
+ // (`providers/kimi-code.kdl` input-modalities).
3849
+ input: entry.supports_image_in === true ? ["text", "image"] : ["text"],
3757
3850
  contextWindow: typeof entry.context_length === "number" ? entry.context_length : 262144,
3758
3851
  maxTokens: kimiCodeMaxTokens(id),
3759
3852
  thinking,
@@ -3838,7 +3931,7 @@ export async function fetchLmStudioNativeModelMetadata(
3838
3931
  try {
3839
3932
  const response = await fetchImpl(`${nativeBaseUrl}/api/v0/models`, {
3840
3933
  method: "GET",
3841
- headers: { Accept: "application/json", ...(options?.headers ?? {}) },
3934
+ headers: { Accept: "application/json", ...options?.headers },
3842
3935
  signal,
3843
3936
  });
3844
3937
  if (!response.ok) {
@@ -4189,9 +4282,10 @@ export function basetenModelManagerOptions(
4189
4282
  // model. Only models with a verified Baseten reasoning policy are enabled
4190
4283
  // here; an unknown model may use a different reasoning wire shape or effort
4191
4284
  // vocabulary, which OMP must not guess.
4285
+ const identity = classifyModel("baseten", defaults.id, { lenient: true });
4192
4286
  const isSupportedBasetenReasoningModel =
4193
- isKimiK3ModelId(defaults.id) ||
4194
- isGlm52ReasoningEffortModelId(defaults.id) ||
4287
+ (identity.class === "kimi" && identity.family === "k3") ||
4288
+ isGlmReasoningIdentity("baseten", defaults.id, "5.2") ||
4195
4289
  defaults.id === "openai/gpt-oss-120b" ||
4196
4290
  defaults.id === "deepseek-ai/DeepSeek-V4-Pro";
4197
4291
  const reasoning =
@@ -4266,55 +4360,25 @@ export function coreWeaveModelManagerOptions(
4266
4360
 
4267
4361
  const META_MODEL_API_BASE_URL = "https://api.meta.ai/v1";
4268
4362
  const META_MUSE_SPARK_COST = { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 } as const;
4363
+ // Contributor SKUs (`-contributor`): same model, discounted because prompts
4364
+ // are used for training.
4365
+ const META_MUSE_SPARK_CONTRIBUTOR_COST = { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 } as const;
4269
4366
  const META_MUSE_SPARK_THINKING: ThinkingConfig = {
4270
4367
  mode: "effort",
4271
4368
  efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh],
4272
4369
  };
4273
4370
 
4274
- export const META_MUSE_STATIC_MODELS: readonly ModelSpec<"openai-responses">[] = [
4275
- {
4276
- id: "muse-spark-1.1",
4277
- name: "Muse Spark 1.1",
4278
- api: "openai-responses",
4279
- provider: "meta",
4280
- baseUrl: META_MODEL_API_BASE_URL,
4281
- reasoning: true,
4282
- input: ["text", "image"],
4283
- cost: META_MUSE_SPARK_COST,
4284
- contextWindow: 1_048_576,
4285
- maxTokens: 131_072,
4286
- thinking: META_MUSE_SPARK_THINKING,
4287
- compat: {
4288
- supportsReasoningEffort: true,
4289
- includeEncryptedReasoning: true,
4290
- },
4291
- },
4292
- {
4293
- id: "muse-spark-1.2",
4294
- name: "Muse Spark 1.2",
4295
- api: "openai-responses",
4296
- provider: "meta",
4297
- baseUrl: META_MODEL_API_BASE_URL,
4298
- reasoning: true,
4299
- input: ["text", "image"],
4300
- cost: META_MUSE_SPARK_COST,
4301
- contextWindow: 1_048_576,
4302
- maxTokens: 131_072,
4303
- thinking: META_MUSE_SPARK_THINKING,
4304
- compat: {
4305
- supportsReasoningEffort: true,
4306
- includeEncryptedReasoning: true,
4307
- },
4308
- },
4309
- {
4310
- id: "muse-spark-1.2-contributor",
4311
- name: "Muse Spark 1.2 Contributor (Data Used for Training)",
4371
+ function museSparkSpec(revision: string, tier: "standard" | "contributor"): ModelSpec<"openai-responses"> {
4372
+ const contributor = tier === "contributor";
4373
+ return {
4374
+ id: contributor ? `muse-spark-${revision}-contributor` : `muse-spark-${revision}`,
4375
+ name: contributor ? `Muse Spark ${revision} (C)` : `Muse Spark ${revision}`,
4312
4376
  api: "openai-responses",
4313
4377
  provider: "meta",
4314
4378
  baseUrl: META_MODEL_API_BASE_URL,
4315
4379
  reasoning: true,
4316
4380
  input: ["text", "image"],
4317
- cost: { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 },
4381
+ cost: contributor ? META_MUSE_SPARK_CONTRIBUTOR_COST : META_MUSE_SPARK_COST,
4318
4382
  contextWindow: 1_048_576,
4319
4383
  maxTokens: 131_072,
4320
4384
  thinking: META_MUSE_SPARK_THINKING,
@@ -4322,9 +4386,42 @@ export const META_MUSE_STATIC_MODELS: readonly ModelSpec<"openai-responses">[] =
4322
4386
  supportsReasoningEffort: true,
4323
4387
  includeEncryptedReasoning: true,
4324
4388
  },
4325
- },
4389
+ };
4390
+ }
4391
+
4392
+ /**
4393
+ * Muse Spark revisions served by Meta's first-party Responses API. Meta's
4394
+ * `/v1/models` lists bare ids with no capability metadata, so every revision
4395
+ * must be seeded here or discovery yields a text-only, non-reasoning model
4396
+ * with an unknown context window.
4397
+ */
4398
+ export const META_MUSE_STATIC_MODELS: readonly ModelSpec<"openai-responses">[] = [
4399
+ museSparkSpec("1.1", "standard"),
4400
+ museSparkSpec("1.2", "standard"),
4401
+ museSparkSpec("1.2", "contributor"),
4402
+ museSparkSpec("1.3", "standard"),
4403
+ museSparkSpec("1.3", "contributor"),
4326
4404
  ];
4327
4405
 
4406
+ const META_MUSE_MODEL_BY_ID: Partial<Record<string, ModelSpec<"openai-responses">>> = Object.fromEntries(
4407
+ META_MUSE_STATIC_MODELS.map(model => [model.id, model]),
4408
+ );
4409
+
4410
+ /**
4411
+ * Lineage reference for a Muse Spark revision Meta ships before the seed
4412
+ * lists it. Every revision so far has kept the 1M window, the effort ladder,
4413
+ * and per-tier pricing, so a new one inherits them (with its own display
4414
+ * name) instead of surfacing as a text-only model with no limits. Only ids
4415
+ * that classify into the `muse-spark` family with a revision qualify.
4416
+ */
4417
+ function museSparkLineageSpec(id: string): ModelSpec<"openai-responses"> | undefined {
4418
+ const identity = classifyModel("meta", id, { lenient: true });
4419
+ if (identity.family !== "muse-spark" || identity.revision === undefined) return undefined;
4420
+ const [major, minor, patch] = parseRevision(identity.revision) ?? [0, 0, 0];
4421
+ const revision = patch === 0 ? `${major}.${minor}` : identity.revision;
4422
+ return museSparkSpec(revision, billingVariantPlain(id) === undefined ? "standard" : "contributor");
4423
+ }
4424
+
4328
4425
  // ---------------------------------------------------------------------------
4329
4426
  // 15.76 Amazon Bedrock Mantle
4330
4427
  // ---------------------------------------------------------------------------
@@ -4459,7 +4556,15 @@ export function metaModelManagerOptions(config?: MetaModelManagerConfig): ModelM
4459
4556
  defaultBaseUrl: META_MODEL_API_BASE_URL,
4460
4557
  config,
4461
4558
  requireApiKey: true,
4462
- mapModel: mapWithBundledReference,
4559
+ filterModel: (_entry, model) => !isExcludedModel("meta", model.id),
4560
+ // The seed backs ids the bundle has not been regenerated with yet;
4561
+ // the lineage spec backs revisions the seed has not been taught.
4562
+ mapModel: (entry, defaults, reference) =>
4563
+ mapWithBundledReference(
4564
+ entry,
4565
+ defaults,
4566
+ reference ?? META_MUSE_MODEL_BY_ID[defaults.id] ?? museSparkLineageSpec(defaults.id),
4567
+ ),
4463
4568
  }),
4464
4569
  staticModels: META_MUSE_STATIC_MODELS,
4465
4570
  };
@@ -4488,10 +4593,6 @@ export interface MoonshotModelManagerConfig {
4488
4593
  * through OpenAI-style `reasoning_effort` (see `buildOpenAICompat`).
4489
4594
  */
4490
4595
  const MOONSHOT_KIMI_K3_COST = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 } as const;
4491
- const MOONSHOT_KIMI_K3_CONTEXT_WINDOW = 1_048_576;
4492
- const MOONSHOT_KIMI_K3_MAX_TOKENS = 131_072;
4493
- const MOONSHOT_KIMI_K3_THINKING: ThinkingConfig = { mode: "effort", efforts: [Effort.Max], requiresEffort: true };
4494
-
4495
4596
  export function moonshotModelManagerOptions(
4496
4597
  config?: MoonshotModelManagerConfig,
4497
4598
  ): ModelManagerOptions<"openai-completions"> {
@@ -4509,39 +4610,29 @@ export function moonshotModelManagerOptions(
4509
4610
  const id = model.id.toLowerCase();
4510
4611
  // Kimi K3 is discovered but has no bundled/models.dev reference, so the
4511
4612
  // generic dynamic defaults would report it "Free" with no capabilities
4512
- // (#5756). Stamp the official pricing/limits when the endpoint doesn't
4513
- // carry them, and mark it reasoning + vision. K3 always reasons via
4514
- // `reasoning_effort: "max"` and does NOT use the K2.x `thinking` block,
4515
- // so its thinking config is the single-tier `max` scale — the wire path
4516
- // routes it through `reasoning_effort` (see `buildOpenAICompat`).
4517
- if (!reference && isKimiK3ModelId(id)) {
4613
+ // (#5756). Modalities, limits, and the reasoning surface are rule-owned
4614
+ // (`providers/moonshot.kdl`, `runtime/behavior.kdl` model-limits) and
4615
+ // baked at build time; the mapper seeds only the official pricing when
4616
+ // the endpoint reports zero cost, plus the structured reasoning fact.
4617
+ const identity = classifyModel("moonshot", id, { lenient: true });
4618
+ if (!reference && identity.class === "kimi" && identity.family === "k3") {
4518
4619
  const isZeroCost = model.cost.input === 0 && model.cost.output === 0 && model.cost.cacheRead === 0;
4620
+ const limits = modelLimitsFor("moonshot", id) ?? modelLimitsFor("moonshot", "kimi-k3");
4519
4621
  return {
4520
4622
  ...model,
4521
4623
  reasoning: true,
4522
- input: ["text", "image"],
4523
4624
  cost: isZeroCost ? { ...MOONSHOT_KIMI_K3_COST } : model.cost,
4524
- contextWindow: model.contextWindow ?? MOONSHOT_KIMI_K3_CONTEXT_WINDOW,
4525
- maxTokens: model.maxTokens ?? MOONSHOT_KIMI_K3_MAX_TOKENS,
4526
- thinking: model.thinking ?? { ...MOONSHOT_KIMI_K3_THINKING },
4625
+ contextWindow: model.contextWindow ?? limits?.context ?? null,
4626
+ maxTokens: model.maxTokens ?? limits?.maxTokens ?? null,
4527
4627
  };
4528
4628
  }
4529
- // Moonshot's K2.x family (K2.5, K2.6, kimi-k2-thinking, …) is reasoning-capable
4530
- // and vision-capable on the native API. Without these flags the openai-completions
4531
- // path skips the z.ai-format `thinking` block, and Moonshot K2.6 stalls on first
4532
- // turn because its endpoint expects an explicit `thinking: {type}` (#2113). Match
4533
- // the bundled K2.5 metadata for every K2.x id we discover.
4534
- const isKimiK2Reasoning = id.includes("thinking") || /(^|\/)kimi-k2(?:\.\d+)?(?:[-:]|$)/.test(id);
4535
- const isVision = id.includes("vision") || id.includes("vl") || /(^|\/)kimi-k2(?:\.\d+)?(?:[-:]|$)/.test(id);
4629
+ // Moonshot's K2.x generation and its thinking variants reason on the
4630
+ // native API (#2113); the effort ladder and modalities are rule-owned
4631
+ // and derived at build time from this flag.
4632
+ const isKimiK2Generation = identity.class === "kimi" && identity.family?.startsWith("k2") === true;
4536
4633
  return {
4537
4634
  ...model,
4538
- reasoning: isKimiK2Reasoning || model.reasoning,
4539
- input: isVision ? ["text", "image"] : model.input,
4540
- thinking:
4541
- model.thinking ??
4542
- (isKimiK2Reasoning
4543
- ? { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] }
4544
- : undefined),
4635
+ reasoning: identity.thinkingVariant === true || isKimiK2Generation || model.reasoning,
4545
4636
  };
4546
4637
  },
4547
4638
  });
@@ -4820,7 +4911,114 @@ export function aiandModelManagerOptions(config?: AiandModelManagerConfig): Mode
4820
4911
  }
4821
4912
 
4822
4913
  // ---------------------------------------------------------------------------
4823
- // 16.7 Yolo-Auto (yolo-auto.com)
4914
+ // 16.7 Abliteration (abliteration.ai)
4915
+ // ---------------------------------------------------------------------------
4916
+
4917
+ const ABLITERATION_DEFAULT_BASE_URL = "https://api.abliteration.ai/v1";
4918
+ const ABLITERATION_MODEL_COST = { input: 3, output: 3, cacheRead: 0.3, cacheWrite: 0 } as const;
4919
+ const ABLITERATION_LARGE_COST = { input: 5, output: 5, cacheRead: 0.5, cacheWrite: 0 } as const;
4920
+
4921
+ function normalizeAbliterationBaseUrl(baseUrl: string | undefined): string {
4922
+ const value = baseUrl?.trim() || ABLITERATION_DEFAULT_BASE_URL;
4923
+ const normalized = value.replace(/\/+$/, "");
4924
+ return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
4925
+ }
4926
+
4927
+ function createAbliterationStaticModel(
4928
+ id: string,
4929
+ name: string,
4930
+ cost: ModelSpec<"openai-responses">["cost"],
4931
+ contextWindow: number,
4932
+ maxTokens: number,
4933
+ input: ModelSpec<"openai-responses">["input"],
4934
+ ): ModelSpec<"openai-responses"> {
4935
+ return {
4936
+ id,
4937
+ name,
4938
+ api: "openai-responses",
4939
+ provider: "abliteration",
4940
+ baseUrl: ABLITERATION_DEFAULT_BASE_URL,
4941
+ reasoning: true,
4942
+ input: [...input],
4943
+ cost: { ...cost },
4944
+ contextWindow,
4945
+ maxTokens,
4946
+ };
4947
+ }
4948
+
4949
+ /**
4950
+ * Documented abliteration.ai catalog (docs.abliteration.ai/models, 2026-09)
4951
+ * bundled so the provider is usable when generation and first boot have no
4952
+ * live key. The `/v1/models` response is authoritative once discovery runs.
4953
+ * Rows carry only the documented limits and pricing; the per-model reasoning
4954
+ * ladders, alias map and wire quirks come from `rules/classes/glm.kdl` and
4955
+ * `rules/providers/abliteration.kdl` via `buildModel`.
4956
+ */
4957
+ export const ABLITERATION_STATIC_MODELS: readonly ModelSpec<"openai-responses">[] = [
4958
+ createAbliterationStaticModel("abliterated-model", "Abliterated Model", ABLITERATION_MODEL_COST, 262_144, 262_134, [
4959
+ "text",
4960
+ "image",
4961
+ ]),
4962
+ createAbliterationStaticModel(
4963
+ "abliterated-model-large-v2",
4964
+ "Abliterated Model Large V2",
4965
+ ABLITERATION_LARGE_COST,
4966
+ 1_000_000,
4967
+ 999_990,
4968
+ ["text"],
4969
+ ),
4970
+ createAbliterationStaticModel(
4971
+ "abliterated-model-large",
4972
+ "Abliterated Model Large",
4973
+ ABLITERATION_LARGE_COST,
4974
+ 1_000_000,
4975
+ 999_990,
4976
+ ["text"],
4977
+ ),
4978
+ ];
4979
+
4980
+ const ABLITERATION_STATIC_MODEL_BY_ID = new Map(ABLITERATION_STATIC_MODELS.map(model => [model.id, model] as const));
4981
+ const ABLITERATION_STATIC_MODEL_IDS = ABLITERATION_STATIC_MODELS.map(model => model.id);
4982
+
4983
+ export interface AbliterationModelManagerConfig {
4984
+ apiKey?: string;
4985
+ baseUrl?: string;
4986
+ fetch?: FetchImpl;
4987
+ }
4988
+
4989
+ export function abliterationModelManagerOptions(
4990
+ config?: AbliterationModelManagerConfig,
4991
+ ): ModelManagerOptions<"openai-responses"> {
4992
+ const apiKey = config?.apiKey;
4993
+ const baseUrl = normalizeAbliterationBaseUrl(config?.baseUrl);
4994
+ const references = createBundledReferenceMap<"openai-responses">("abliteration");
4995
+ return {
4996
+ providerId: "abliteration",
4997
+ dynamicModelsAuthoritative: true,
4998
+ dropCachedModelIdsOnStaticMismatch: ABLITERATION_STATIC_MODEL_IDS,
4999
+ ...(apiKey && {
5000
+ fetchDynamicModels: () =>
5001
+ fetchOpenAICompatibleModels({
5002
+ api: "openai-responses",
5003
+ provider: "abliteration",
5004
+ baseUrl,
5005
+ apiKey,
5006
+ mapModel: (entry, defaults) => {
5007
+ const reference = references.get(defaults.id) ?? ABLITERATION_STATIC_MODEL_BY_ID.get(defaults.id);
5008
+ const model = mapWithBundledReference(entry, defaults, reference);
5009
+ // Every abliteration.ai model reasons; `/v1/models` carries no
5010
+ // capability metadata, so an unseeded id would otherwise lose
5011
+ // its effort dial.
5012
+ return reference ? model : { ...model, reasoning: true };
5013
+ },
5014
+ fetch: config?.fetch,
5015
+ }),
5016
+ }),
5017
+ };
5018
+ }
5019
+
5020
+ // ---------------------------------------------------------------------------
5021
+ // 16.8 Yolo-Auto (yolo-auto.com)
4824
5022
  // ---------------------------------------------------------------------------
4825
5023
 
4826
5024
  const YOLO_AUTO_BASE_URL = "https://yolo-auto.com/v1";
@@ -5048,7 +5246,7 @@ export function xiaomiModelManagerOptions(
5048
5246
  provider: providerId,
5049
5247
  baseUrl: url,
5050
5248
  apiKey,
5051
- filterModel: (_entry, model) => !model.id.includes("-tts") && !model.id.includes("-asr"),
5249
+ filterModel: (_entry, model) => !isExcludedModel(providerId, model.id),
5052
5250
  mapModel: (entry, defaults) => {
5053
5251
  const reference = references.get(defaults.id);
5054
5252
  const model = mapWithBundledReference(entry, defaults, reference);
@@ -5735,6 +5933,7 @@ export function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelM
5735
5933
  apiKey,
5736
5934
  mapModel: (entry, defaults) => {
5737
5935
  const model = mapWithBundledReference(entry, defaults, references.get(defaults.id));
5936
+ const identity = classifyModel("vllm", model.id, { lenient: true });
5738
5937
  return {
5739
5938
  ...model,
5740
5939
  contextWindow: toPositiveNumber(entry.max_model_len, model.contextWindow),
@@ -5742,7 +5941,8 @@ export function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelM
5742
5941
  // open weights always think (the template cannot disable it), so
5743
5942
  // light up the effort dial; buildModel derives the template
5744
5943
  // ladder from the id + local-backend compat.
5745
- reasoning: model.reasoning || isQwen38PlusTemplateEffortModelId(model.id),
5944
+ reasoning:
5945
+ model.reasoning || (identity.class === "qwen" && revisionAtLeast(identity.revision, "3.8")),
5746
5946
  };
5747
5947
  },
5748
5948
  fetch: config?.fetch,
@@ -5818,13 +6018,6 @@ export interface GithubCopilotModelManagerConfig {
5818
6018
  fetch?: FetchImpl;
5819
6019
  }
5820
6020
 
5821
- const COPILOT_ANTHROPIC_MODEL_PATTERN = /^claude-(haiku|sonnet|opus|fable|mythos)-\d/;
5822
- const isCopilotResponsesModelId = (modelId: string): boolean =>
5823
- modelId === "grok-4.5" ||
5824
- modelId === "grok-4.6" ||
5825
- modelId.startsWith("gpt-5") ||
5826
- modelId.startsWith("oswe") ||
5827
- modelId.startsWith("mai-");
5828
6021
  const COPILOT_CACHE_INVALIDATED_MODEL_IDS = [
5829
6022
  "grok-4.5",
5830
6023
  "grok-4.5-1m",
@@ -5834,12 +6027,8 @@ const COPILOT_CACHE_INVALIDATED_MODEL_IDS = [
5834
6027
  ];
5835
6028
 
5836
6029
  function inferCopilotApi(modelId: string): Api {
5837
- if (COPILOT_ANTHROPIC_MODEL_PATTERN.test(modelId)) {
5838
- return "anthropic-messages";
5839
- }
5840
- if (isCopilotResponsesModelId(modelId)) {
5841
- return "openai-responses";
5842
- }
6030
+ const route = apiRouteFor("github-copilot", modelId);
6031
+ if (route?.api === "anthropic-messages" || route?.api === "openai-responses") return route.api;
5843
6032
  return "openai-completions";
5844
6033
  }
5845
6034
 
@@ -5998,8 +6187,8 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
5998
6187
  providerId: "github-copilot",
5999
6188
  cacheProviderId: resolveModelCacheProviderId("github-copilot", { apiKey: rawApiKey, baseUrl }),
6000
6189
  dropCachedModelIdsOnStaticMismatch: COPILOT_CACHE_INVALIDATED_MODEL_IDS,
6001
- // COPILOT_API_HEADERS are compile-time constants (User-Agent + API
6002
- // version), not credentials. The cache omits all request headers for
6190
+ // COPILOT_API_HEADERS are compile-time wire identity constants, not
6191
+ // credentials. The cache omits all request headers for
6003
6192
  // safety and can only restore them from a bundled static entry — so a
6004
6193
  // Copilot model with no bundled reference (e.g. a freshly served
6005
6194
  // claude-opus-5 and its synthesized -1m sibling) is dropped on offline
@@ -6019,7 +6208,7 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
6019
6208
  provider: "github-copilot",
6020
6209
  baseUrl: requestBaseUrl,
6021
6210
  apiKey,
6022
- headers: COPILOT_API_HEADERS,
6211
+ headers: COPILOT_DISCOVERY_HEADERS,
6023
6212
  mapModel: (
6024
6213
  entry: OpenAICompatibleModelRecord,
6025
6214
  defaults: ModelSpec<Api>,
@@ -6079,6 +6268,10 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
6079
6268
  maxTokens !== null
6080
6269
  ? Math.min(contextWindow, defaultContextMax + maxTokens)
6081
6270
  : contextWindow;
6271
+ const unreferencedIdentity =
6272
+ reference === undefined && api === "anthropic-messages"
6273
+ ? classifyModel("github-copilot", defaults.id, { lenient: true })
6274
+ : undefined;
6082
6275
  const base: ModelSpec<Api> = reference
6083
6276
  ? {
6084
6277
  ...reference,
@@ -6089,10 +6282,7 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
6089
6282
  input,
6090
6283
  contextWindow: defaultTierWindow,
6091
6284
  maxTokens,
6092
- headers: {
6093
- ...COPILOT_API_HEADERS,
6094
- ...(getProviderReferences().get(defaults.id)?.headers ?? {}),
6095
- },
6285
+ headers: mergeCopilotApiHeaders(getProviderReferences().get(defaults.id)?.headers),
6096
6286
  ...(api === "openai-completions"
6097
6287
  ? {
6098
6288
  compat: {
@@ -6111,7 +6301,7 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
6111
6301
  input,
6112
6302
  contextWindow: defaultTierWindow,
6113
6303
  maxTokens,
6114
- headers: { ...COPILOT_API_HEADERS },
6304
+ headers: mergeCopilotApiHeaders(),
6115
6305
  // Copilot's `/models` advertises no reasoning bit, so a
6116
6306
  // thinking-capable Claude with no bundled reference would
6117
6307
  // fall back to `reasoning: false` and lose its effort dial.
@@ -6120,7 +6310,8 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
6120
6310
  // (<= 3.5) over the Messages proxy is not handed a fabricated
6121
6311
  // dial it would reject; a modern reference-less model (e.g.
6122
6312
  // claude-opus-5) is marked so `buildModel` derives the ladder.
6123
- ...(api === "anthropic-messages" && anthropicModelSupportsThinking(defaults.id)
6313
+ ...(unreferencedIdentity?.class === "anthropic" &&
6314
+ revisionAtLeast(unreferencedIdentity.revision, "3.7")
6124
6315
  ? { reasoning: true }
6125
6316
  : {}),
6126
6317
  ...(api === "openai-completions"
@@ -6400,7 +6591,7 @@ function resolveApiByRules(
6400
6591
 
6401
6592
  function createOpenCodeApiResolution(
6402
6593
  basePath: string,
6403
- idOverrides: Readonly<Record<string, Api>> = {},
6594
+ providerId: "opencode-zen" | "opencode-go",
6404
6595
  ): {
6405
6596
  defaultResolution: { api: Api; baseUrl: string };
6406
6597
  rules: ApiResolutionRule[];
@@ -6410,8 +6601,11 @@ function createOpenCodeApiResolution(
6410
6601
  // - openai-completions / openai-responses / google-generative-ai → /v1
6411
6602
  // - anthropic-messages → bare basePath (the Anthropic client appends /v1/messages)
6412
6603
  const baseUrlForApi = (api: Api): string => (api === "anthropic-messages" ? basePath : completionsBaseUrl);
6413
- const overrideRules: ApiResolutionRule[] = Object.entries(idOverrides).map(([id, api]) => ({
6414
- matches: modelId => modelId === id,
6604
+ // Per-id route pins live in the `api-routes` behavior rules.
6605
+ const overrideRules: ApiResolutionRule[] = (
6606
+ ["openai-completions", "openai-responses", "anthropic-messages"] as const
6607
+ ).map(api => ({
6608
+ matches: modelId => apiRouteFor(providerId, modelId)?.api === api,
6415
6609
  resolved: { api, baseUrl: baseUrlForApi(api) },
6416
6610
  }));
6417
6611
  return {
@@ -6437,19 +6631,32 @@ function createOpenCodeApiResolution(
6437
6631
  }
6438
6632
 
6439
6633
  // Resolver rules for the models.dev descriptor path; the per-id pins live in
6440
- // OPENCODE_ZEN_API_ID_OVERRIDES / OPENCODE_GO_API_ID_OVERRIDES (section 8),
6441
- // which also drive the live-discovery mapper.
6442
- const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution(
6443
- "https://opencode.ai/zen",
6444
- OPENCODE_ZEN_API_ID_OVERRIDES,
6445
- );
6446
- const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution(
6447
- "https://opencode.ai/zen/go",
6448
- OPENCODE_GO_API_ID_OVERRIDES,
6449
- );
6634
+ // the `api-routes` behavior rules, which also drive the live-discovery mapper.
6635
+ const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen", "opencode-zen");
6636
+ const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go", "opencode-go");
6450
6637
 
6451
6638
  const COPILOT_BASE_URL = "https://api.githubcopilot.com";
6452
6639
 
6640
+ const ZAI_ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic";
6641
+ // The `zai` catalog provider is the GLM Coding Plan: `/login zai` validates and
6642
+ // stores credentials against the coding-plan endpoint (see auth/zai.kdl), so
6643
+ // the native OpenAI transport must ride the coding-plan base rather than the
6644
+ // general PAYG `/api/paas/v4`, which would bypass plan quota or fail auth.
6645
+ const ZAI_OPENAI_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
6646
+
6647
+ /** Resolves the transport and endpoint for one Z.AI model catalog entry. */
6648
+ export function resolveZaiApi(modelId: string): { api: "anthropic-messages" | "openai-completions"; baseUrl: string } {
6649
+ const api = apiRouteFor("zai", modelId)?.api ?? "anthropic-messages";
6650
+ switch (api) {
6651
+ case "anthropic-messages":
6652
+ return { api, baseUrl: ZAI_ANTHROPIC_BASE_URL };
6653
+ case "openai-completions":
6654
+ return { api, baseUrl: ZAI_OPENAI_BASE_URL };
6655
+ default:
6656
+ throw new Error(`Unsupported Z.AI API route: ${api}`);
6657
+ }
6658
+ }
6659
+
6453
6660
  const COPILOT_DEFAULT_RESOLUTION = {
6454
6661
  api: "openai-completions",
6455
6662
  baseUrl: COPILOT_BASE_URL,
@@ -6457,11 +6664,11 @@ const COPILOT_DEFAULT_RESOLUTION = {
6457
6664
 
6458
6665
  const COPILOT_API_RESOLUTION_RULES: readonly ApiResolutionRule[] = [
6459
6666
  {
6460
- matches: modelId => COPILOT_ANTHROPIC_MODEL_PATTERN.test(modelId),
6667
+ matches: modelId => apiRouteFor("github-copilot", modelId)?.api === "anthropic-messages",
6461
6668
  resolved: { api: "anthropic-messages", baseUrl: COPILOT_BASE_URL },
6462
6669
  },
6463
6670
  {
6464
- matches: isCopilotResponsesModelId,
6671
+ matches: modelId => apiRouteFor("github-copilot", modelId)?.api === "openai-responses",
6465
6672
  resolved: { api: "openai-responses", baseUrl: COPILOT_BASE_URL },
6466
6673
  },
6467
6674
  ];
@@ -6536,11 +6743,8 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK: readonly ModelsDevProviderDescrip
6536
6743
  api: "bedrock-converse-stream",
6537
6744
  baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
6538
6745
  filterModel: (id, m) => {
6539
- if (m.tool_call !== true) return false;
6540
- if (id.startsWith("ai21.jamba")) return false;
6541
- if (id.startsWith("amazon.titan-text-express") || id.startsWith("mistral.mistral-7b-instruct-v0"))
6542
- return false;
6543
- return true;
6746
+ // Undrivable SKUs are rule-owned (`runtime/behavior.kdl` exclude-models).
6747
+ return m.tool_call === true && !isExcludedModel("amazon-bedrock", id);
6544
6748
  },
6545
6749
  transformModel: (model, modelId, m) => {
6546
6750
  const crossRegionId = bedrockCrossRegionId(modelId);
@@ -6555,7 +6759,10 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK: readonly ModelsDevProviderDescrip
6555
6759
  // without these rows the catalog only has commercial geos (`us.`/`eu.`/…)
6556
6760
  // and model resolution rejects the GovCloud id (or misroutes commercial
6557
6761
  // geos onto us-east-1 with GovCloud credentials → 403).
6558
- if (modelId.startsWith("anthropic.claude-")) {
6762
+ // Fan out only bare vendor-namespaced source ids (`anthropic.<model>`);
6763
+ // ids already carrying a geo prefix (`us.`/`eu.`/`global.`/`au.`…)
6764
+ // are this fan-out's own output shape.
6765
+ if (modelId.split(".", 1)[0] === "anthropic") {
6559
6766
  const displayName = toModelName(m.name, modelId);
6560
6767
  return [
6561
6768
  bedrockModel,
@@ -6576,19 +6783,57 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK: readonly ModelsDevProviderDescrip
6576
6783
  },
6577
6784
  ];
6578
6785
 
6786
+ const CLINEPASS_REASONING_BUDGET_RATIOS: Readonly<Record<Effort, number>> = {
6787
+ [Effort.Minimal]: 0.1,
6788
+ [Effort.Low]: 0.2,
6789
+ [Effort.Medium]: 0.5,
6790
+ [Effort.High]: 0.8,
6791
+ [Effort.XHigh]: 0.95,
6792
+ [Effort.Max]: 1,
6793
+ };
6794
+
6795
+ function buildClinePassThinking(raw: ModelsDevModel, model: ModelSpec<Api>): ThinkingConfig | undefined {
6796
+ const effortValues = raw.reasoning_options?.find(option => option.type === "effort")?.values;
6797
+ if (effortValues) {
6798
+ const efforts = THINKING_EFFORTS.filter(effort => effortValues.includes(effort));
6799
+ return efforts.length > 0 ? { mode: "effort", efforts } : undefined;
6800
+ }
6801
+
6802
+ const budget = raw.reasoning_options?.find(option => option.type === "budget_tokens");
6803
+ if (!budget) return undefined;
6804
+ const modelMaxTokens = model.maxTokens ?? CLINEPASS_FALLBACK_MAX_TOKENS;
6805
+ const maximum = toPositiveNumber(budget.max, modelMaxTokens) ?? modelMaxTokens;
6806
+ const minimum = toPositiveNumber(budget.min, 1) ?? 1;
6807
+ const scale = Math.min(modelMaxTokens, maximum);
6808
+ return {
6809
+ mode: "budget",
6810
+ efforts: THINKING_EFFORTS,
6811
+ effortBudgets: Object.fromEntries(
6812
+ THINKING_EFFORTS.map(effort => [
6813
+ effort,
6814
+ Math.min(Math.max(Math.floor(scale * CLINEPASS_REASONING_BUDGET_RATIOS[effort]), minimum), maximum),
6815
+ ]),
6816
+ ),
6817
+ };
6818
+ }
6819
+
6820
+ /**
6821
+ * DeepSeek's V4-generation lineage (`v4`, `v4-flash`, `v4.1-pro`) by
6822
+ * structured family. The native roster serves only V4-era SKUs today; the
6823
+ * `tool_call` gate on the descriptor filters the rest.
6824
+ */
6825
+ function isDeepseekV4Generation(providerId: string, modelId: string): boolean {
6826
+ const identity = classifyModel(providerId, modelId, { lenient: true });
6827
+ if (identity.class !== "deepseek") return false;
6828
+ return identity.family === "v4" || identity.family === "flash" || identity.family === "pro";
6829
+ }
6830
+
6579
6831
  const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor[] = [
6580
6832
  // --- Anthropic ---
6581
6833
  anthropicMessagesDescriptor("anthropic", "anthropic", "https://api.anthropic.com", {
6582
6834
  filterModel: (id, m) => {
6583
6835
  if (m.tool_call !== true) return false;
6584
- if (
6585
- id.startsWith("claude-3-5-haiku") ||
6586
- id.startsWith("claude-3-7-sonnet") ||
6587
- id === "claude-3-opus-20240229" ||
6588
- id === "claude-3-sonnet-20240229"
6589
- )
6590
- return false;
6591
- return true;
6836
+ return !isExcludedModel("anthropic", id);
6592
6837
  },
6593
6838
  }),
6594
6839
  // --- Google ---
@@ -6604,19 +6849,46 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor
6604
6849
  openAiCompletionsDescriptor("groq", "groq", "https://api.groq.com/openai/v1"),
6605
6850
  // --- Cerebras ---
6606
6851
  openAiCompletionsDescriptor("cerebras", "cerebras", "https://api.cerebras.ai/v1"),
6852
+ // --- ClinePass ---
6853
+ // The live roster owns membership; this snapshot pins Cline's exact limits,
6854
+ // pricing, modalities, and per-model reasoning controls for known ids.
6855
+ openAiCompletionsDescriptor("cline-pass", "cline-pass", "https://api.cline.bot/api/v1", {
6856
+ transformModel: (model, modelId, raw) => {
6857
+ const id = toClinePassPublicModelId(modelId);
6858
+ const metadata = getClinePassModelMetadata(id);
6859
+ if (metadata) {
6860
+ return {
6861
+ ...model,
6862
+ id,
6863
+ name: metadata.name,
6864
+ contextWindow: metadata.contextWindow,
6865
+ maxTokens: metadata.maxTokens,
6866
+ input: metadata.input,
6867
+ cost: metadata.cost,
6868
+ reasoning: metadata.reasoning,
6869
+ thinking: metadata.thinking,
6870
+ compat:
6871
+ metadata.thinking === undefined ? { ...model.compat, supportsReasoningEffort: false } : model.compat,
6872
+ };
6873
+ }
6874
+ return {
6875
+ ...model,
6876
+ id,
6877
+ thinking: model.reasoning ? buildClinePassThinking(raw, model) : undefined,
6878
+ };
6879
+ },
6880
+ }),
6607
6881
  // --- Together ---
6608
6882
  openAiCompletionsDescriptor("togetherai", "together", "https://api.together.xyz/v1"),
6609
6883
  // --- CoreWeave Serverless Inference ---
6610
6884
  openAiCompletionsDescriptor("wandb", "coreweave", "https://api.inference.wandb.ai/v1", {
6611
6885
  transformModel: model => {
6612
- if (!model.id.startsWith("openai/gpt-oss-")) {
6886
+ // CoreWeave's roster omits the reasoning bit for gpt-oss; flag it and
6887
+ // let the class rules own the effort ladder.
6888
+ if (classifyModel("coreweave", model.id, { lenient: true }).class !== "gpt-oss") {
6613
6889
  return model;
6614
6890
  }
6615
- return {
6616
- ...model,
6617
- reasoning: true,
6618
- thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High] },
6619
- };
6891
+ return { ...model, reasoning: true };
6620
6892
  },
6621
6893
  }),
6622
6894
  // --- NVIDIA ---
@@ -6631,7 +6903,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor
6631
6903
  openAiCompletionsDescriptor("deepseek", "deepseek", "https://api.deepseek.com", {
6632
6904
  // Only ship the v4 family as built-ins; older deepseek-chat / deepseek-reasoner
6633
6905
  // ids are kept off the catalog until the issue thread asks for them.
6634
- filterModel: (id, m) => m.tool_call === true && id.startsWith("deepseek-v4"),
6906
+ filterModel: (id, m) => m.tool_call === true && isDeepseekV4Generation("deepseek", id),
6635
6907
  compat: {
6636
6908
  // DeepSeek V4 effort remapping is derived in model-thinking metadata; this
6637
6909
  // descriptor keeps only transport-shape compat.
@@ -6662,7 +6934,9 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
6662
6934
  // SKU as "Free" in `/models`. The PAYG key carries the real per-token rates for
6663
6935
  // the identical model ids, so the enumerated token costs line up with the other
6664
6936
  // subscription providers for comparison (issue #5598).
6665
- anthropicMessagesDescriptor("zai", "zai", "https://api.z.ai/api/anthropic"),
6937
+ anthropicMessagesDescriptor("zai", "zai", ZAI_ANTHROPIC_BASE_URL, {
6938
+ resolveApi: modelId => resolveZaiApi(modelId),
6939
+ }),
6666
6940
  // --- Umans AI ---
6667
6941
  // Source the pay-as-you-go catalog: the coding-plan key publishes subscription
6668
6942
  // costs as zero, while `/models/info` omits pricing entirely. The generator
@@ -6862,7 +7136,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDes
6862
7136
  openAiCompletionsDescriptor("zenmux", "zenmux", ZENMUX_OPENAI_BASE_URL, {
6863
7137
  filterModel: filterActiveToolCallModels,
6864
7138
  resolveApi: modelId => {
6865
- if (modelId.startsWith("anthropic/")) {
7139
+ if (apiRouteFor("zenmux", modelId)?.api === "anthropic-messages") {
6866
7140
  return { api: "anthropic-messages" as const, baseUrl: ZENMUX_ANTHROPIC_BASE_URL };
6867
7141
  }
6868
7142
  return { api: "openai-completions" as const, baseUrl: ZENMUX_OPENAI_BASE_URL };