@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
@@ -11,31 +11,39 @@
11
11
  * attribution) keys on the logical `id`.
12
12
  *
13
13
  * Families come from two sources:
14
- * - Hand tables (`VARIANT_COLLAPSE_TABLES`) for providers whose routing needs
15
- * curation (Antigravity tier triplets, single-member renames, recycled ids).
14
+ * - Reviewed `variant-family` nodes in `compat/rules/taxonomy/_collapse.kdl`
15
+ * (compiled into `rules.json`) for providers whose routing needs curation
16
+ * (Antigravity tier triplets, single-member renames, recycled ids). A
17
+ * family whose ids carry `{rev}` is a template, instantiated for every
18
+ * revision the input advertises (`gemini-{rev}-flash` → `gemini-3.8-flash`).
16
19
  * - `deriveThinkingPairFamilies`: the global automatic rule — any live
17
20
  * `X` + `X-thinking` pair (trailing or infix token) collapses into `X`,
18
21
  * routing thinking-enabled requests to `X-thinking`. Gated on identical
19
22
  * pricing and same api: price-divergent twins are distinct SKUs and stay
20
23
  * separate so billing attribution never lies.
21
24
  *
22
- * Family invariants (hold for hand-written and derived tables):
25
+ * Family invariants (hold for compiled, reviewed and derived tables):
23
26
  * - One axis per family. A second id axis (e.g. Cursor's `-fast` service
24
27
  * tier) becomes a sibling family, never a second routing dimension.
25
28
  * - The collapsed spec inherits non-tier fields from the first present
26
29
  * member; members must be cost-homogeneous.
27
30
  *
28
- * `collapseEffortVariants` is pure, deterministic, and idempotent:
31
+ * `collapseVariants` is pure, deterministic, and idempotent:
29
32
  * `collapse(collapse(x))` equals `collapse(x)`, and mixed raw+collapsed input
30
33
  * (stale cache rows, previous-snapshot fallbacks) dedupes to the collapsed
31
34
  * entry. That makes it safe at every source — discovery, the catalog
32
35
  * generator, and the model-manager merge point.
33
36
  */
34
- import { buildCompat, buildModel } from "./build";
35
- import { Effort, THINKING_EFFORTS } from "./effort";
36
- import { stripThinkingVariantToken } from "./identity/family";
37
- import { resolveModelThinking } from "./model-thinking";
38
- import type { Api, Model, ModelSpec, Provider, ThinkingConfig } from "./types";
37
+ import { buildModel } from "../build";
38
+ import { Effort, THINKING_EFFORTS } from "../effort";
39
+ import type { Api, Model, ModelSpec, Provider, ThinkingConfig } from "../types";
40
+ import { resolveModelPolicy } from "./resolve";
41
+ import { parseRevision, parseRevisionConstraint, type RevisionTerm, revisionSatisfies } from "./revision";
42
+ import { collapseVariantId, collapseVocabulary, stripThinkingVariantSuffix } from "./taxonomy";
43
+ import { type CompiledVariantFamily, REVISION_PLACEHOLDER } from "./types";
44
+
45
+ const VARIANT_ROUTING_KEYS: readonly (Effort | "off")[] = ["off", ...THINKING_EFFORTS];
46
+ const DEFAULT_PAIR_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
39
47
 
40
48
  /**
41
49
  * Structural bound for collapse inputs: both raw `ModelSpec`s and built
@@ -81,8 +89,14 @@ export interface EffortVariantFamily {
81
89
  * efforts fall back to `requestModelId ?? id`.
82
90
  */
83
91
  routing: Readonly<Partial<Record<Effort | "off", string>>>;
84
- /** Explicit capability surface for the collapsed spec — no inference. */
85
- thinking: Readonly<Omit<ThinkingConfig, "effortRouting" | "suppressWhenOff">>;
92
+ /**
93
+ * Explicit capability surface for the collapsed spec — no inference. Omit
94
+ * for single-wire-id renames on providers where effort is encoded in the
95
+ * upstream id itself (Devin): with one member and no routing there is no
96
+ * controllable surface, and the collapsed spec must carry no thinking
97
+ * rather than an effort ladder whose every tier resolves to one wire id.
98
+ */
99
+ thinking?: Readonly<Omit<ThinkingConfig, "effortRouting" | "suppressWhenOff">>;
86
100
  /** Thinking-off requests must explicitly suppress thinking on the wire. */
87
101
  suppressWhenOff?: boolean;
88
102
  /**
@@ -96,37 +110,41 @@ export interface EffortVariantFamily {
96
110
  extraAliases?: readonly string[];
97
111
  }
98
112
 
99
- export interface VariantCollapseTable {
100
- families: readonly EffortVariantFamily[];
113
+ /**
114
+ * A reviewed family whose ids carry {@link REVISION_PLACEHOLDER}: one
115
+ * `gemini-{rev}-flash` entry stands for every Flash generation the provider
116
+ * serves with the same sibling layout. Concrete families with the same
117
+ * instantiated id take precedence.
118
+ */
119
+ export interface VariantFamilyTemplate {
120
+ /** Family with placeholders still in every id and the display name. */
121
+ family: EffortVariantFamily;
122
+ /** Revisions the template applies to; unbounded when absent. */
123
+ revision?: readonly RevisionTerm[];
124
+ /**
125
+ * Matches the logical id, any member, or any extra alias (case-insensitive);
126
+ * one alternative per id, each capturing the revision.
127
+ */
128
+ pattern: RegExp;
101
129
  }
102
130
 
103
- /** `X` + `X-thinking` hand family: off routes to the bare id, efforts to `-thinking`. */
104
- function thinkingPair(baseId: string, name: string): EffortVariantFamily {
105
- return {
106
- id: baseId,
107
- name,
108
- members: [baseId, `${baseId}-thinking`],
109
- routing: {
110
- off: baseId,
111
- [Effort.Minimal]: `${baseId}-thinking`,
112
- [Effort.Low]: `${baseId}-thinking`,
113
- [Effort.Medium]: `${baseId}-thinking`,
114
- [Effort.High]: `${baseId}-thinking`,
115
- },
116
- // Thinking-off routes to the non-thinking backing id, where omitting
117
- // thinkingConfig is already correct — no suppressWhenOff.
118
- thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
119
- preserveAbsentEffortRoutes: true,
120
- };
131
+ export interface VariantCollapseTable {
132
+ families: readonly EffortVariantFamily[];
133
+ /** Revision-templated families, instantiated against live ids. */
134
+ templates?: readonly VariantFamilyTemplate[];
135
+ /**
136
+ * Provider-scoped selector aliases: short native-CLI names and dotted
137
+ * upstream spellings → logical model id. Unlike family members and
138
+ * `extraAliases` these are deliberately invisible to the bare-id lookup
139
+ * ({@link resolveBareVariantSelector}) and to the reverse index — a generic
140
+ * label like `gpt` or `opus` only means something once a provider is
141
+ * named, and must never hijack an unqualified selector or re-key config.
142
+ */
143
+ providerAliases?: Readonly<Record<string, string>>;
121
144
  }
122
145
 
123
146
  type TierRoutes = Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", string>>;
124
147
 
125
- /** Devin families with a `-max` sibling: five wire tiers, `low` floor. */
126
- const DEVIN_FIVE_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
127
- /** Pre-5.6 Devin GPT families top out at `-xhigh`: four wire tiers, `low` floor. */
128
- const DEVIN_FOUR_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
129
-
130
148
  /**
131
149
  * Build one effort-tier family from a tier→wire-id map: routing keeps only the
132
150
  * listed efforts, `members` dedupes the targets in tier order, and thinking is
@@ -187,617 +205,140 @@ function tierFamily(
187
205
  };
188
206
  }
189
207
 
190
- /**
191
- * GPT-5.6 (Luna/Sol/Terra) serves per-tier siblings for the full five-tier
192
- * `low..max` wire scale in both standard and fast lanes.
193
- */
194
- function devinGpt56Families(variant: "luna" | "sol" | "terra", name: string): readonly EffortVariantFamily[] {
195
- const base = `gpt-5-6-${variant}`;
196
- return [
197
- tierFamily(
198
- base,
199
- name,
200
- {
201
- off: `${base}-none`,
202
- low: `${base}-low`,
203
- medium: `${base}-medium`,
204
- high: `${base}-high`,
205
- xhigh: `${base}-xhigh`,
206
- max: `${base}-max`,
207
- },
208
- DEVIN_FIVE_TIER_EFFORTS,
209
- ),
210
- tierFamily(
211
- `${base}-fast`,
212
- `${name} Fast`,
213
- {
214
- off: `${base}-none-priority`,
215
- low: `${base}-low-priority`,
216
- medium: `${base}-medium-priority`,
217
- high: `${base}-high-priority`,
218
- xhigh: `${base}-xhigh-priority`,
219
- max: `${base}-max-priority`,
220
- },
221
- DEVIN_FIVE_TIER_EFFORTS,
222
- ),
223
- ];
224
- }
225
-
226
- const GEMINI_3_FLASH_FAMILY_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
227
- const GEMINI_3_PRO_FAMILY_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High];
228
-
229
- /**
230
- * Antigravity Cloud Code Assist sends an explicit `thinkingBudget` per tier
231
- * (verified against captured `daily-cloudcode-pa` requests). Flash uses round
232
- * budgets; Pro offsets every budget by +1. Minimal mirrors Low (the Antigravity
233
- * UI exposes Low/Medium/High only) so the effort stays selectable.
234
- */
235
- const GEMINI_3_FLASH_FAMILY_BUDGETS: Readonly<Partial<Record<Effort, number>>> = {
236
- [Effort.Minimal]: 1000,
237
- [Effort.Low]: 1000,
238
- [Effort.Medium]: 4000,
239
- [Effort.High]: 10000,
240
- };
241
- const GEMINI_3_PRO_FAMILY_BUDGETS: Readonly<Partial<Record<Effort, number>>> = {
242
- [Effort.Low]: 1001,
243
- [Effort.High]: 10001,
244
- };
245
-
246
- /**
247
- * Cloud Code Assist's legacy Gemini 3.5 Flash and 3.1 Pro families use
248
- * different thinking transports: `google-antigravity` (daily-cloudcode-pa)
249
- * sends captured `thinkingBudget` values, while `google-gemini-cli`
250
- * (cloudcode-pa) follows the official Gemini CLI and uses `thinkingLevel`.
251
- * Gemini 3.6 exposes one wire id per level and uses `thinkingLevel` on both
252
- * endpoints.
253
- */
254
- function geminiFlashFamily(mode: "budget" | "google-level"): EffortVariantFamily {
255
- const budget = mode === "budget";
256
- return {
257
- id: "gemini-3.5-flash",
258
- name: "Gemini 3.5 Flash",
259
- members: ["gemini-3.5-flash-extra-low", "gemini-3.5-flash-low", "gemini-3-flash-agent"],
260
- routing: budget
261
- ? {
262
- off: "gemini-3.5-flash-extra-low",
263
- [Effort.Minimal]: "gemini-3.5-flash-extra-low",
264
- [Effort.Low]: "gemini-3.5-flash-extra-low",
265
- [Effort.Medium]: "gemini-3.5-flash-low",
266
- [Effort.High]: "gemini-3-flash-agent",
267
- }
268
- : {
269
- off: "gemini-3.5-flash-extra-low",
270
- [Effort.Minimal]: "gemini-3-flash-agent",
271
- [Effort.Low]: "gemini-3.5-flash-extra-low",
272
- [Effort.Medium]: "gemini-3.5-flash-extra-low",
273
- [Effort.High]: "gemini-3.5-flash-low",
274
- },
275
- thinking: budget
276
- ? { mode: "budget", efforts: GEMINI_3_FLASH_FAMILY_EFFORTS, effortBudgets: GEMINI_3_FLASH_FAMILY_BUDGETS }
277
- : { mode: "google-level", efforts: GEMINI_3_FLASH_FAMILY_EFFORTS },
278
- suppressWhenOff: true,
279
- // Retired bare id; the alias only fires when no live model holds it
280
- // (exact match wins in every resolver).
281
- extraAliases: ["gemini-3-flash"],
208
+ /** One reviewed family from the compiled collapse vocabulary → runtime shape. */
209
+ function compiledFamily(compiled: CompiledVariantFamily): EffortVariantFamily {
210
+ const family: EffortVariantFamily = {
211
+ id: compiled.id,
212
+ name: compiled.name,
213
+ members: compiled.members,
214
+ routing: compiled.routing,
282
215
  };
216
+ if (compiled.defaultMember !== undefined) family.defaultMember = compiled.defaultMember;
217
+ if (compiled.retiredMembers?.length) family.retiredMembers = compiled.retiredMembers;
218
+ if (!compiled.noThinking && compiled.mode !== undefined) {
219
+ family.thinking = {
220
+ mode: compiled.mode,
221
+ efforts: compiled.efforts ?? [],
222
+ ...(compiled.effortBudgets !== undefined && {
223
+ effortBudgets: compiled.effortBudgets,
224
+ }),
225
+ ...(compiled.defaultLevel !== undefined && { defaultLevel: compiled.defaultLevel }),
226
+ ...(compiled.requiresEffort === true && { requiresEffort: true }),
227
+ };
228
+ }
229
+ if (compiled.suppressWhenOff) family.suppressWhenOff = true;
230
+ if (compiled.preserveAbsentEffortRoutes) family.preserveAbsentEffortRoutes = true;
231
+ if (compiled.extraAliases?.length) family.extraAliases = compiled.extraAliases;
232
+ return family;
283
233
  }
284
234
 
285
- /**
286
- * Gemini 3.6+ Flash exposes one mandatory-reasoning wire id per thinking
287
- * level. Some generations retain additional discovery-only aliases.
288
- */
289
- function geminiLevelFlashFamily(version: "3.6" | "3.7", ...additionalMembers: string[]): EffortVariantFamily {
290
- const id = `gemini-${version}-flash`;
291
- return {
292
- id,
293
- name: `Gemini ${version} Flash`,
294
- members: [`${id}-low`, `${id}-medium`, `${id}-high`, ...additionalMembers],
295
- routing: {
296
- [Effort.Minimal]: `${id}-low`,
297
- [Effort.Low]: `${id}-low`,
298
- [Effort.Medium]: `${id}-medium`,
299
- [Effort.High]: `${id}-high`,
300
- },
301
- thinking: {
302
- mode: "google-level",
303
- efforts: GEMINI_3_FLASH_FAMILY_EFFORTS,
304
- requiresEffort: true,
305
- },
235
+ const REVISION_CAPTURE = String.raw`(\d+(?:\.\d+){0,2})`;
236
+
237
+ /** One compiled `{rev}` family → runtime template with its id matcher. */
238
+ function compiledTemplate(compiled: CompiledVariantFamily): VariantFamilyTemplate {
239
+ const family = compiledFamily(compiled);
240
+ const alternatives = [family.id, ...family.members, ...(family.extraAliases ?? [])].map(id =>
241
+ id
242
+ .split(REVISION_PLACEHOLDER)
243
+ .map(part => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
244
+ .join(REVISION_CAPTURE),
245
+ );
246
+ const template: VariantFamilyTemplate = {
247
+ family,
248
+ pattern: new RegExp(`^(?:${alternatives.join("|")})$`, "i"),
306
249
  };
250
+ if (compiled.revision !== undefined) {
251
+ const terms = parseRevisionConstraint(compiled.revision);
252
+ if (terms !== undefined) template.revision = terms;
253
+ }
254
+ return template;
307
255
  }
308
256
 
309
- const GEMINI_36_FLASH_FAMILY = geminiLevelFlashFamily("3.6", "gemini-3.6-flash-tiered");
310
- const GEMINI_37_FLASH_FAMILY = geminiLevelFlashFamily("3.7", "gemini-3.7-flash-tiered");
311
-
312
- function geminiProFamily(mode: "budget" | "google-level"): EffortVariantFamily {
313
- const budget = mode === "budget";
314
- return {
315
- id: "gemini-3.1-pro",
316
- name: "Gemini 3.1 Pro",
317
- // High routes to `gemini-pro-agent` — the upstream `gemini-3.1-pro-high`
318
- // deployment returns INVALID_ARGUMENT on every streamGenerateContent
319
- // request (both CCA endpoints) while discovery still lists it;
320
- // `gemini-pro-agent` is the same model ("Gemini 3.1 Pro (High)", same
321
- // thinking budget/caps) and accepts the identical request body.
322
- // `gemini-3.1-pro-high` stays a member so the dead raw id is consumed.
323
- members: ["gemini-3.1-pro-low", "gemini-pro-agent", "gemini-3.1-pro-high"],
324
- retiredMembers: ["gemini-3.1-pro-high"],
325
- routing: {
326
- off: "gemini-3.1-pro-low",
327
- [Effort.Low]: "gemini-3.1-pro-low",
328
- [Effort.High]: "gemini-pro-agent",
329
- },
330
- thinking: budget
331
- ? { mode: "budget", efforts: GEMINI_3_PRO_FAMILY_EFFORTS, effortBudgets: GEMINI_3_PRO_FAMILY_BUDGETS }
332
- : { mode: "google-level", efforts: GEMINI_3_PRO_FAMILY_EFFORTS },
333
- suppressWhenOff: true,
257
+ /** Substitute the placeholder throughout one templated family. */
258
+ function instantiateTemplate(template: VariantFamilyTemplate, rev: string): EffortVariantFamily | undefined {
259
+ if (template.revision !== undefined) {
260
+ const parsed = parseRevision(rev);
261
+ if (parsed === undefined || !revisionSatisfies(parsed, template.revision)) return undefined;
262
+ }
263
+ const fill = (id: string): string => id.replaceAll(REVISION_PLACEHOLDER, rev);
264
+ const source = template.family;
265
+ const routing: Partial<Record<Effort | "off", string>> = {};
266
+ for (const effort of VARIANT_ROUTING_KEYS) {
267
+ const target = source.routing[effort];
268
+ if (target !== undefined) routing[effort] = fill(target);
269
+ }
270
+ const family: EffortVariantFamily = {
271
+ ...source,
272
+ id: fill(source.id),
273
+ name: fill(source.name),
274
+ members: source.members.map(fill),
275
+ routing,
334
276
  };
277
+ if (source.defaultMember !== undefined) family.defaultMember = fill(source.defaultMember);
278
+ if (source.retiredMembers !== undefined) family.retiredMembers = source.retiredMembers.map(fill);
279
+ if (source.extraAliases !== undefined) family.extraAliases = source.extraAliases.map(fill);
280
+ return family;
335
281
  }
336
282
 
337
- /** CCA families shared verbatim by both providers (transport-agnostic). */
338
- const SHARED_CCA_FAMILIES: readonly EffortVariantFamily[] = [
339
- {
340
- // Legacy static family — covers stale snapshots and caches. Stale ids are
341
- // unverified against the budget-mode CCA contract; keep them on level.
342
- id: "gemini-3-pro",
343
- name: "Gemini 3 Pro",
344
- members: ["gemini-3-pro-low", "gemini-3-pro-high"],
345
- routing: {
346
- off: "gemini-3-pro-low",
347
- [Effort.Low]: "gemini-3-pro-low",
348
- [Effort.High]: "gemini-3-pro-high",
349
- },
350
- thinking: { mode: "google-level", efforts: GEMINI_3_PRO_FAMILY_EFFORTS },
351
- suppressWhenOff: true,
352
- },
353
- {
354
- // Rename-only collapse: every effort and off fall back to the wire id.
355
- id: "gpt-oss-120b",
356
- name: "GPT-OSS 120B",
357
- members: ["gpt-oss-120b-medium"],
358
- routing: {},
359
- thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
360
- },
361
- // Antigravity Cloud Code Assist exposes Claude 4.6 asymmetrically: only the
362
- // bare `claude-sonnet-4-6` wire id (no `-thinking` twin) and only the
363
- // `claude-opus-4-6-thinking` wire id (no bare twin). Per-effort thinking is
364
- // carried in the request body via `thinkingBudget`, so both ids accept on/off
365
- // requests. Listing both candidates in `members` (priority order) keeps the
366
- // collapse correct if the backend mix ever rebalances; `retiredMembers`
367
- // re-points stale collapsed snapshots (bundled catalog rows, cache rows
368
- // written by prior generations) away from the dead wire id via
369
- // `reconcileRetiredRouting`.
370
- {
371
- id: "claude-sonnet-4-6",
372
- name: "Claude Sonnet 4.6",
373
- members: ["claude-sonnet-4-6", "claude-sonnet-4-6-thinking"],
374
- retiredMembers: ["claude-sonnet-4-6-thinking"],
375
- routing: {},
376
- thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
377
- },
378
-
379
- {
380
- id: "claude-opus-4-6",
381
- name: "Claude Opus 4.6",
382
- members: ["claude-opus-4-6-thinking", "claude-opus-4-6"],
383
- retiredMembers: ["claude-opus-4-6"],
384
- routing: {},
385
- thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
386
- },
387
- thinkingPair("claude-sonnet-4-5", "Claude Sonnet 4.5"),
388
- thinkingPair("claude-opus-4-5", "Claude Opus 4.5"),
389
- thinkingPair("gemini-2.5-flash", "Gemini 2.5 Flash"),
390
- ];
391
-
392
- /** `google-antigravity` Gemini families, using each generation's native transport. */
393
- export const ANTIGRAVITY_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
394
- families: [
395
- GEMINI_36_FLASH_FAMILY,
396
- GEMINI_37_FLASH_FAMILY,
397
- geminiFlashFamily("budget"),
398
- geminiProFamily("budget"),
399
- ...SHARED_CCA_FAMILIES,
400
- ],
401
- };
402
-
403
- /** `google-gemini-cli` Gemini families on the official CLI's level transport. */
404
- export const GEMINI_CLI_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
405
- families: [
406
- GEMINI_36_FLASH_FAMILY,
407
- GEMINI_37_FLASH_FAMILY,
408
- geminiFlashFamily("google-level"),
409
- geminiProFamily("google-level"),
410
- ...SHARED_CCA_FAMILIES,
411
- ],
412
- };
413
- export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
414
- families: [
415
- tierFamily(
416
- "claude-opus-5",
417
- "Claude Opus 5",
418
- {
419
- low: "claude-opus-5-low",
420
- medium: "claude-opus-5-medium",
421
- high: "claude-opus-5-high",
422
- xhigh: "claude-opus-5-xhigh",
423
- max: "claude-opus-5-max",
424
- },
425
- DEVIN_FIVE_TIER_EFFORTS,
426
- ),
427
- tierFamily(
428
- "claude-opus-5-fast",
429
- "Claude Opus 5 Fast",
430
- {
431
- low: "claude-opus-5-low-fast",
432
- medium: "claude-opus-5-medium-fast",
433
- high: "claude-opus-5-high-fast",
434
- xhigh: "claude-opus-5-xhigh-fast",
435
- max: "claude-opus-5-max-fast",
436
- },
437
- DEVIN_FIVE_TIER_EFFORTS,
438
- ),
439
- tierFamily(
440
- "claude-fable-5",
441
- "Claude Fable 5",
442
- {
443
- low: "claude-5-fable-low",
444
- medium: "claude-5-fable-medium",
445
- high: "claude-5-fable-high",
446
- xhigh: "claude-5-fable-xhigh",
447
- max: "claude-5-fable-max",
448
- },
449
- DEVIN_FIVE_TIER_EFFORTS,
450
- ),
451
- tierFamily(
452
- "claude-sonnet-5",
453
- "Claude Sonnet 5",
454
- {
455
- low: "claude-sonnet-5-low",
456
- medium: "claude-sonnet-5-medium",
457
- high: "claude-sonnet-5-high",
458
- xhigh: "claude-sonnet-5-xhigh",
459
- max: "claude-sonnet-5-max",
460
- },
461
- DEVIN_FIVE_TIER_EFFORTS,
462
- ),
463
- tierFamily(
464
- "claude-opus-4-7",
465
- "Claude Opus 4.7",
466
- {
467
- low: "claude-opus-4-7-low",
468
- medium: "claude-opus-4-7-medium",
469
- high: "claude-opus-4-7-high",
470
- xhigh: "claude-opus-4-7-xhigh",
471
- max: "claude-opus-4-7-max",
472
- },
473
- DEVIN_FIVE_TIER_EFFORTS,
474
- ),
475
- tierFamily(
476
- "claude-opus-4-7-fast",
477
- "Claude Opus 4.7 Fast",
478
- {
479
- low: "claude-opus-4-7-low-fast",
480
- medium: "claude-opus-4-7-medium-fast",
481
- high: "claude-opus-4-7-high-fast",
482
- xhigh: "claude-opus-4-7-xhigh-fast",
483
- max: "claude-opus-4-7-max-fast",
484
- },
485
- DEVIN_FIVE_TIER_EFFORTS,
486
- ),
487
- tierFamily(
488
- "claude-opus-4-8",
489
- "Claude Opus 4.8",
490
- {
491
- low: "claude-opus-4-8-low",
492
- medium: "claude-opus-4-8-medium",
493
- high: "claude-opus-4-8-high",
494
- xhigh: "claude-opus-4-8-xhigh",
495
- max: "claude-opus-4-8-max",
496
- },
497
- DEVIN_FIVE_TIER_EFFORTS,
498
- ),
499
- tierFamily(
500
- "claude-opus-4-8-fast",
501
- "Claude Opus 4.8 Fast",
502
- {
503
- low: "claude-opus-4-8-low-fast",
504
- medium: "claude-opus-4-8-medium-fast",
505
- high: "claude-opus-4-8-high-fast",
506
- xhigh: "claude-opus-4-8-xhigh-fast",
507
- max: "claude-opus-4-8-max-fast",
508
- },
509
- DEVIN_FIVE_TIER_EFFORTS,
510
- ),
511
- tierFamily(
512
- "gpt-5-2",
513
- "GPT-5.2",
514
- {
515
- off: "MODEL_GPT_5_2_NONE",
516
- low: "MODEL_GPT_5_2_LOW",
517
- medium: "MODEL_GPT_5_2_MEDIUM",
518
- high: "MODEL_GPT_5_2_HIGH",
519
- xhigh: "MODEL_GPT_5_2_XHIGH",
520
- },
521
- DEVIN_FOUR_TIER_EFFORTS,
522
- ),
523
- tierFamily(
524
- "gpt-5-3-codex",
525
- "GPT-5.3 Codex",
526
- {
527
- low: "gpt-5-3-codex-low",
528
- medium: "gpt-5-3-codex-medium",
529
- high: "gpt-5-3-codex-high",
530
- xhigh: "gpt-5-3-codex-xhigh",
531
- },
532
- DEVIN_FOUR_TIER_EFFORTS,
533
- ),
534
- tierFamily(
535
- "gpt-5-3-codex-fast",
536
- "GPT-5.3 Codex Fast",
537
- {
538
- low: "gpt-5-3-codex-low-priority",
539
- medium: "gpt-5-3-codex-medium-priority",
540
- high: "gpt-5-3-codex-high-priority",
541
- xhigh: "gpt-5-3-codex-xhigh-priority",
542
- },
543
- DEVIN_FOUR_TIER_EFFORTS,
544
- ),
545
- tierFamily(
546
- "gpt-5-4",
547
- "GPT-5.4",
548
- {
549
- off: "gpt-5-4-none",
550
- low: "gpt-5-4-low",
551
- medium: "gpt-5-4-medium",
552
- high: "gpt-5-4-high",
553
- xhigh: "gpt-5-4-xhigh",
554
- },
555
- DEVIN_FOUR_TIER_EFFORTS,
556
- ),
557
- tierFamily(
558
- "gpt-5-4-fast",
559
- "GPT-5.4 Fast",
560
- {
561
- off: "gpt-5-4-none-priority",
562
- low: "gpt-5-4-low-priority",
563
- medium: "gpt-5-4-medium-priority",
564
- high: "gpt-5-4-high-priority",
565
- xhigh: "gpt-5-4-xhigh-priority",
566
- },
567
- DEVIN_FOUR_TIER_EFFORTS,
568
- ),
569
- tierFamily(
570
- "gpt-5-4-mini",
571
- "GPT-5.4 Mini",
572
- {
573
- low: "gpt-5-4-mini-low",
574
- medium: "gpt-5-4-mini-medium",
575
- high: "gpt-5-4-mini-high",
576
- xhigh: "gpt-5-4-mini-xhigh",
577
- },
578
- DEVIN_FOUR_TIER_EFFORTS,
579
- ),
580
- tierFamily(
581
- "gpt-5-5",
582
- "GPT-5.5",
583
- {
584
- off: "gpt-5-5-none",
585
- low: "gpt-5-5-low",
586
- medium: "gpt-5-5-medium",
587
- high: "gpt-5-5-high",
588
- xhigh: "gpt-5-5-xhigh",
589
- },
590
- DEVIN_FOUR_TIER_EFFORTS,
591
- ),
592
- tierFamily(
593
- "gpt-5-5-fast",
594
- "GPT-5.5 Fast",
595
- {
596
- off: "gpt-5-5-none-priority",
597
- low: "gpt-5-5-low-priority",
598
- medium: "gpt-5-5-medium-priority",
599
- high: "gpt-5-5-high-priority",
600
- xhigh: "gpt-5-5-xhigh-priority",
601
- },
602
- DEVIN_FOUR_TIER_EFFORTS,
603
- ),
604
- ...devinGpt56Families("luna", "GPT-5.6 Luna"),
605
- ...devinGpt56Families("sol", "GPT-5.6 Sol"),
606
- ...devinGpt56Families("terra", "GPT-5.6 Terra"),
607
- tierFamily(
608
- "kimi-k3",
609
- "Kimi K3",
610
- {
611
- low: "kimi-k3-low",
612
- high: "kimi-k3-high",
613
- max: "kimi-k3-max",
614
- },
615
- [Effort.Low, Effort.High, Effort.Max],
616
- ),
617
- tierFamily(
618
- "swe-1-7",
619
- "SWE-1.7",
620
- {
621
- medium: "swe-1-7-medium",
622
- max: "swe-1-7",
623
- },
624
- [Effort.Medium, Effort.Max],
625
- ),
626
- tierFamily(
627
- "grok-4-5",
628
- "Grok 4.5",
629
- {
630
- low: "grok-4-5-low",
631
- medium: "grok-4-5-medium",
632
- high: "grok-4-5-high",
633
- },
634
- [Effort.Low, Effort.Medium, Effort.High],
635
- ),
636
- tierFamily(
637
- "inkling",
638
- "Inkling",
639
- {
640
- off: "inkling-none",
641
- low: "inkling-low",
642
- medium: "inkling-medium",
643
- high: "inkling-high",
644
- xhigh: "inkling-xhigh",
645
- max: "inkling-max",
646
- },
647
- DEVIN_FIVE_TIER_EFFORTS,
648
- ),
649
- tierFamily(
650
- "gemini-3-1-pro",
651
- "Gemini 3.1 Pro",
652
- {
653
- low: "gemini-3-1-pro-low",
654
- high: "gemini-3-1-pro-high",
655
- },
656
- [Effort.Low, Effort.High],
657
- ),
658
- tierFamily(
659
- "gemini-3-5-flash",
660
- "Gemini 3.5 Flash",
661
- {
662
- minimal: "gemini-3-5-flash-minimal",
663
- low: "gemini-3-5-flash-low",
664
- medium: "gemini-3-5-flash-medium",
665
- high: "gemini-3-5-flash-high",
666
- },
667
- [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
668
- ),
669
- tierFamily(
670
- "gemini-3-6-flash",
671
- "Gemini 3.6 Flash",
672
- {
673
- minimal: "gemini-3-6-flash-minimal",
674
- low: "gemini-3-6-flash-low",
675
- medium: "gemini-3-6-flash-medium",
676
- high: "gemini-3-6-flash-high",
677
- },
678
- [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
679
- ),
680
- tierFamily(
681
- "gemini-3-flash",
682
- "Gemini 3 Flash",
683
- {
684
- minimal: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL",
685
- low: "MODEL_GOOGLE_GEMINI_3_0_FLASH_LOW",
686
- medium: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM",
687
- high: "MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH",
688
- },
689
- [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
690
- ),
691
- // GLM-5.2 200K — only the base wire UID `glm-5-2` is free on Devin's
692
- // Coding Plan (verified via streamDevin: `glm-5-2-none` and `glm-5-2-max`
693
- // both return "weekly usage quota exhausted" while `glm-5-2` streams
694
- // successfully). Route every effort to `glm-5-2` so the collapsed entry
695
- // is always free; include the paid 200K variants as members so they are
696
- // hidden from the model list. The 1M-context variants stay as separate
697
- // paid entries (collapsed below).
698
- {
699
- id: "glm-5-2",
700
- name: "GLM-5.2",
701
- members: ["glm-5-2", "glm-5-2-none", "glm-5-2-max"],
702
- routing: {
703
- [Effort.High]: "glm-5-2",
704
- [Effort.XHigh]: "glm-5-2",
705
- },
706
- thinking: {
707
- mode: "effort",
708
- efforts: [Effort.High, Effort.XHigh],
709
- requiresEffort: true,
710
- },
711
- },
712
- // GLM-5.2 1M — paid variants that consume weekly quota. Collapse the
713
- // three 1M-context variants into one entry with proper effort routing.
714
- tierFamily(
715
- "glm-5-2-1m",
716
- "GLM-5.2 1M",
717
- {
718
- off: "glm-5-2-none-1m",
719
- high: "glm-5-2-1m",
720
- xhigh: "glm-5-2-max-1m",
721
- },
722
- [Effort.High, Effort.XHigh],
723
- ),
724
- ],
725
- };
726
-
727
- /** Cursor's Grok tier tokens equal the effort id, so routing is a direct map. */
728
- const CURSOR_GROK_45_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
729
- const CURSOR_GROK_46_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
730
- /** Cursor GPT-5.6 (Luna/Sol/Terra) serves the full five-tier `low..max` scale. */
731
- const CURSOR_GPT_56_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
732
-
733
283
  /**
734
- * Cursor serves Grok 4.5/4.6 as per-effort sibling ids
735
- * (`cursor-grok-4.6-low|-medium|-high|-xhigh`) alongside a parallel `-fast`
736
- * service-tier lane (`-low-fast`, …). Each lane collapses into its own logical
737
- * model — the `-fast` axis is a sibling family, never a second routing
738
- * dimension — with effort routing onto the live sibling wire ids. The mandatory
739
- * `-medium` default is the tier Cursor's Start plan serves at fixed settings;
740
- * the `-low` floor is refused there (issue #9478).
284
+ * The family a template yields for `id` (logical id, member, or alias), or
285
+ * undefined when no template matches or the revision is out of range.
741
286
  */
742
- function cursorGrokFamilies(version: "4.5" | "4.6", efforts: readonly Effort[]): readonly EffortVariantFamily[] {
743
- const build = (fast: boolean): EffortVariantFamily => {
744
- const suffix = fast ? "-fast" : "";
745
- const routes: TierRoutes = {};
746
- for (const effort of efforts) {
747
- routes[effort] = `cursor-grok-${version}-${effort}${suffix}`;
748
- }
749
- return tierFamily(
750
- `cursor-grok-${version}${suffix}`,
751
- `Grok ${version}${fast ? " Fast" : ""}`,
752
- routes,
753
- efforts,
754
- `cursor-grok-${version}-${Effort.Medium}${suffix}`,
755
- );
756
- };
757
- return [build(false), build(true)];
287
+ function templateFamilyFor(templates: readonly VariantFamilyTemplate[], id: string): EffortVariantFamily | undefined {
288
+ for (const template of templates) {
289
+ const match = template.pattern.exec(id);
290
+ if (match === null) continue;
291
+ let rev: string | undefined;
292
+ for (let group = 1; group < match.length && rev === undefined; group++) rev = match[group];
293
+ if (rev === undefined) continue;
294
+ const family = instantiateTemplate(template, rev);
295
+ if (family !== undefined) return family;
296
+ }
297
+ return undefined;
758
298
  }
759
299
 
760
300
  /**
761
- * Cursor serves GPT-5.6 (Luna/Sol/Terra) as per-tier sibling ids
762
- * (`gpt-5.6-luna-none|-low|-medium|-high|-xhigh|-max`) with a parallel `-fast`
763
- * service-tier lane (`gpt-5.6-luna-high-fast`, …). Same shape as Devin's
764
- * `devinGpt56Families`, but cursor keys the version with a dot, marks the
765
- * thinking-off tier `-none`, and names the fast lane with `-fast` (Devin uses
766
- * `-priority`). Each lane collapses into its own logical model — `-fast` is a
767
- * sibling SKU, never a second routing dimension — while the 1M / Max Mode SKU
768
- * stays a separate row (handled by discovery's context-window resolution).
301
+ * Concrete families for every revision present in `ids`, one per
302
+ * instantiated id, skipping ids a concrete family in `table` already owns.
769
303
  */
770
- function cursorGpt56Families(variant: "luna" | "sol" | "terra", name: string): readonly EffortVariantFamily[] {
771
- const build = (fast: boolean): EffortVariantFamily => {
772
- const suffix = fast ? "-fast" : "";
773
- const base = `gpt-5.6-${variant}`;
774
- return tierFamily(
775
- `${base}${suffix}`,
776
- `${name}${fast ? " Fast" : ""}`,
777
- {
778
- off: `${base}-none${suffix}`,
779
- low: `${base}-low${suffix}`,
780
- medium: `${base}-medium${suffix}`,
781
- high: `${base}-high${suffix}`,
782
- xhigh: `${base}-xhigh${suffix}`,
783
- max: `${base}-max${suffix}`,
784
- },
785
- CURSOR_GPT_56_EFFORTS,
786
- );
787
- };
788
- return [build(false), build(true)];
304
+ function instantiateTemplates(table: VariantCollapseTable, ids: Iterable<string>): EffortVariantFamily[] {
305
+ const templates = table.templates;
306
+ if (templates === undefined || templates.length === 0) return [];
307
+ const seen = new Set<string>();
308
+ for (const family of table.families) seen.add(family.id);
309
+ const out: EffortVariantFamily[] = [];
310
+ for (const id of ids) {
311
+ const family = templateFamilyFor(templates, id);
312
+ if (family === undefined || seen.has(family.id)) continue;
313
+ seen.add(family.id);
314
+ out.push(family);
315
+ }
316
+ return out;
789
317
  }
790
318
 
791
- /** `cursor` per-effort sibling families collapsed per service-tier lane: Grok 4.5/4.6 plus GPT-5.6 Luna/Sol/Terra. */
792
- export const CURSOR_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
793
- families: [
794
- ...cursorGrokFamilies("4.5", CURSOR_GROK_45_EFFORTS),
795
- ...cursorGrokFamilies("4.6", CURSOR_GROK_46_EFFORTS),
796
- ...cursorGpt56Families("luna", "GPT-5.6 Luna"),
797
- ...cursorGpt56Families("sol", "GPT-5.6 Sol"),
798
- ...cursorGpt56Families("terra", "GPT-5.6 Terra"),
799
- ],
800
- };
319
+ /** Provider id → reviewed collapse table, built once from the compiled vocabulary. */
320
+ function buildCompiledTables(): Readonly<Record<string, VariantCollapseTable>> {
321
+ const { variantFamilies, providerAliases } = collapseVocabulary();
322
+ const tables: Record<
323
+ string,
324
+ { families: EffortVariantFamily[]; templates?: VariantFamilyTemplate[]; providerAliases?: Record<string, string> }
325
+ > = {};
326
+ for (const compiled of variantFamilies) {
327
+ // biome-ignore lint/suspicious/noAssignInExpressions: compiled stratum walker assigns during traversal
328
+ const table = (tables[compiled.provider] ??= { families: [] });
329
+ if (compiled.id.includes(REVISION_PLACEHOLDER)) {
330
+ // biome-ignore lint/suspicious/noAssignInExpressions: compiled stratum walker assigns during traversal
331
+ (table.templates ??= []).push(compiledTemplate(compiled));
332
+ } else {
333
+ table.families.push(compiledFamily(compiled));
334
+ }
335
+ }
336
+ for (const [provider, aliases] of Object.entries(providerAliases)) {
337
+ tables[provider] ??= { families: [] };
338
+ tables[provider].providerAliases = aliases;
339
+ }
340
+ return tables;
341
+ }
801
342
 
802
343
  type CursorTierToken = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
803
344
 
@@ -940,13 +481,16 @@ function deriveCursorEffortFamilies<TSpec extends VariantSpecLike>(specs: readon
940
481
  return families;
941
482
  }
942
483
 
943
- /** Provider id → hand collapse table. The CCA providers diverge on thinking transport. */
944
- export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTable>> = {
945
- "google-antigravity": ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
946
- "google-gemini-cli": GEMINI_CLI_VARIANT_COLLAPSE_TABLE,
947
- devin: DEVIN_VARIANT_COLLAPSE_TABLE,
948
- cursor: CURSOR_VARIANT_COLLAPSE_TABLE,
949
- };
484
+ /** Provider id → reviewed collapse table. The CCA providers diverge on thinking transport. */
485
+ const REVIEWED_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTable>> = buildCompiledTables();
486
+
487
+ /**
488
+ * Return the reviewed collapse table compiled for `provider`, when declared.
489
+ * Discovery uses this accessor to preserve injectable table seams.
490
+ */
491
+ export function reviewedCollapseTable(provider: string): VariantCollapseTable | undefined {
492
+ return REVIEWED_COLLAPSE_TABLES[provider] ?? REVIEWED_COLLAPSE_TABLES[provider.toLowerCase()];
493
+ }
950
494
 
951
495
  /**
952
496
  * The global automatic rule: derive an `X` + `X-thinking` family for every
@@ -955,7 +499,7 @@ export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTab
955
499
  * - known pricing must match — all-zero cost rows count as unknown
956
500
  * (aggregators routinely ship them), but twins that BOTH carry real,
957
501
  * differing prices are distinct SKUs and never merge,
958
- * - ids claimed by the provider's hand `table` are skipped (curation wins).
502
+ * - ids claimed by the provider's reviewed table are skipped (curation wins).
959
503
  * The capability surface prefers the thinking member's metadata, then the
960
504
  * bare member's, then the canonical deriver (aggregators often ship
961
505
  * `reasoning: false` and no thinking config on the twin), then a budget
@@ -965,6 +509,7 @@ export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTab
965
509
  export function deriveThinkingPairFamilies<TSpec extends VariantSpecLike>(
966
510
  specs: readonly TSpec[],
967
511
  table?: VariantCollapseTable,
512
+ provider?: string,
968
513
  ): EffortVariantFamily[] {
969
514
  const byId = new Map<string, TSpec>();
970
515
  for (const spec of specs) {
@@ -973,20 +518,19 @@ export function deriveThinkingPairFamilies<TSpec extends VariantSpecLike>(
973
518
  const claimed = table ? getAliasIndex(table) : undefined;
974
519
  const families: EffortVariantFamily[] = [];
975
520
  for (const spec of specs) {
976
- const baseId = stripThinkingVariantToken(spec.id);
521
+ const collapsed = provider === undefined ? undefined : collapseVariantId(provider, spec.id);
522
+ const baseId = collapsed?.thinkingVariant ? collapsed.logicalId : stripThinkingVariantSuffix(spec.id);
977
523
  if (baseId === undefined || baseId === spec.id) continue;
978
524
  const base = byId.get(baseId);
979
525
  if (!base) continue;
980
- if (claimed) {
981
- const forward = claimed.forward;
982
- if (
983
- forward.has(spec.id.toLowerCase()) ||
984
- forward.has(baseId.toLowerCase()) ||
985
- claimed.familyIds.has(spec.id) ||
986
- claimed.familyIds.has(baseId)
987
- ) {
988
- continue;
989
- }
526
+ if (
527
+ claimed &&
528
+ (claimed.resolve(spec.id) !== undefined ||
529
+ claimed.resolve(baseId) !== undefined ||
530
+ claimed.isFamily(spec.id) ||
531
+ claimed.isFamily(baseId))
532
+ ) {
533
+ continue;
990
534
  }
991
535
  if (spec.api !== base.api) continue;
992
536
  const specPriced = spec.cost.input !== 0 || spec.cost.output !== 0;
@@ -1017,14 +561,6 @@ export function deriveThinkingPairFamilies<TSpec extends VariantSpecLike>(
1017
561
  return families;
1018
562
  }
1019
563
 
1020
- const DEFAULT_PAIR_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
1021
-
1022
- /**
1023
- * Surface fallback chain: thinking member → bare member → canonical deriver →
1024
- * budget default. `requiresEffort` is dropped from every source: the COLLAPSED
1025
- * pair can disable thinking (off routes to the bare backing id), even though
1026
- * the thinking member alone cannot.
1027
- */
1028
564
  function derivePairThinkingSurface(
1029
565
  thinkingSpec: VariantSpecLike,
1030
566
  baseSpec: VariantSpecLike,
@@ -1034,10 +570,12 @@ function derivePairThinkingSurface(
1034
570
  const { effortRouting: _routing, suppressWhenOff: _suppress, requiresEffort: _required, ...surface } = baked;
1035
571
  return surface;
1036
572
  }
1037
- const derived = resolveModelThinking(
1038
- { ...(thinkingSpec as unknown as ModelSpec<Api>), reasoning: true, thinking: undefined },
1039
- buildCompat(thinkingSpec as unknown as ModelSpec<Api>),
1040
- );
573
+ const { compat: _compat, ...policySpec } = thinkingSpec;
574
+ const derived = resolveModelPolicy({
575
+ ...policySpec,
576
+ reasoning: true,
577
+ thinking: undefined,
578
+ }).thinking;
1041
579
  if (derived && derived.efforts.length > 0) {
1042
580
  const { effortRouting: _dRouting, suppressWhenOff: _dSuppress, requiresEffort: _dRequired, ...surface } = derived;
1043
581
  return surface;
@@ -1048,25 +586,25 @@ function derivePairThinkingSurface(
1048
586
  /**
1049
587
  * True when `spec` is the output of collapsing rather than a raw upstream
1050
588
  * member. `thinking.effortRouting` is written only by collapsing; the
1051
- * `requestModelId` arm is scoped to the provider's hand-table family ids so
589
+ * `requestModelId` arm is scoped to the provider's reviewed-table family ids so
1052
590
  * unrelated carriers (GitHub Copilot `-1m` context variants) never match.
1053
591
  */
1054
- export function isVariantCollapsedSpec(spec: VariantSpecLike): boolean {
592
+ export function isCollapsedVariantSpec(spec: VariantSpecLike): boolean {
1055
593
  if (spec.thinking?.effortRouting !== undefined) {
1056
594
  return true;
1057
595
  }
1058
596
  if (spec.requestModelId === undefined) {
1059
597
  return false;
1060
598
  }
1061
- const table = VARIANT_COLLAPSE_TABLES[spec.provider];
1062
- return table !== undefined && getAliasIndex(table).familyIds.has(spec.id);
599
+ const table = reviewedCollapseTable(spec.provider);
600
+ return table !== undefined && getAliasIndex(table).isFamily(spec.id);
1063
601
  }
1064
602
 
1065
603
  /**
1066
604
  * Re-point a stale collapsed spec whose `requestModelId` or routing still
1067
605
  * targets a retired wire id. Collapsed snapshots (bundled catalog, cache
1068
606
  * rows, previous-generation fallbacks) pass through collapsing untouched, so
1069
- * a hand-table routing fix would otherwise never reach them. Only retired
607
+ * a reviewed-table routing fix would otherwise never reach them. Only retired
1070
608
  * targets are rewritten — presence-filtered routing decisions from live
1071
609
  * discovery stay authoritative for everything else. Per retired entry the
1072
610
  * table's route for that effort wins, then the off/first-live-member wire id,
@@ -1078,12 +616,13 @@ function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
1078
616
  family: EffortVariantFamily,
1079
617
  retired: ReadonlySet<string>,
1080
618
  ): TSpec {
1081
- const routing = spec.thinking?.effortRouting;
619
+ const currentThinking = spec.thinking;
620
+ const routing = currentThinking?.effortRouting;
1082
621
  const requestRetired = spec.requestModelId !== undefined && retired.has(spec.requestModelId);
1083
622
  let routingRetired = false;
1084
623
  if (routing !== undefined) {
1085
- for (const key in routing) {
1086
- const target = routing[key as Effort | "off"];
624
+ for (const key of VARIANT_ROUTING_KEYS) {
625
+ const target = routing[key];
1087
626
  if (target !== undefined && retired.has(target)) {
1088
627
  routingRetired = true;
1089
628
  break;
@@ -1096,10 +635,9 @@ function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
1096
635
  const fallbackWireId =
1097
636
  offTarget !== undefined && !retired.has(offTarget) ? offTarget : family.members.find(id => !retired.has(id));
1098
637
  const next: TSpec = { ...spec };
1099
- if (routingRetired && routing !== undefined) {
638
+ if (routingRetired && routing !== undefined && currentThinking !== undefined) {
1100
639
  const nextRouting: Partial<Record<Effort | "off", string>> = {};
1101
- for (const key in routing) {
1102
- const effortKey = key as Effort | "off";
640
+ for (const effortKey of VARIANT_ROUTING_KEYS) {
1103
641
  const target = routing[effortKey];
1104
642
  if (target === undefined) continue;
1105
643
  if (!retired.has(target)) {
@@ -1113,7 +651,7 @@ function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
1113
651
  nextRouting[effortKey] = fallbackWireId;
1114
652
  }
1115
653
  }
1116
- next.thinking = { ...(spec.thinking as ThinkingConfig), effortRouting: nextRouting };
654
+ next.thinking = { ...currentThinking, effortRouting: nextRouting };
1117
655
  }
1118
656
  if (requestRetired) {
1119
657
  if (fallbackWireId !== undefined && fallbackWireId !== spec.id) {
@@ -1130,7 +668,7 @@ function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
1130
668
  * prev-generation snapshots freeze a family's transport, budgets, and routing;
1131
669
  * discovery emits the canonical id but the exact-id merge never overwrites a
1132
670
  * stale `family.id` row (e.g. `gemini-3.1-pro`) nor a recycled `extraAliases`
1133
- * row (e.g. `gemini-3-flash`). This re-applies the hand-table family's thinking,
671
+ * row (e.g. `gemini-3-flash`). This re-applies the reviewed-table family's thinking,
1134
672
  * routing, and default wire id while keeping the spec id (load-bearing for exact
1135
673
  * selectors and bundled lookups). Returns `spec` by reference when unchanged.
1136
674
  */
@@ -1142,19 +680,20 @@ function refreshCollapsedThinking<TSpec extends VariantSpecLike>(
1142
680
  // Scope snapshot self-heal to families carrying a curated per-effort budget
1143
681
  // contract (Antigravity gemini-3.x). Their routing targets are all verified
1144
682
  // live, so rebuilding routing here is safe; families without `effortBudgets`
1145
- // (derived `X`/`X-thinking` pairs, claude pairs) keep their presence-filtered
1146
- // snapshot routing untouched.
1147
- if (!spec.reasoning || family.thinking.effortBudgets === undefined) return spec;
683
+ // (derived `X`/`X-thinking` pairs, claude pairs, surface-less renames) keep
684
+ // their presence-filtered snapshot routing untouched.
685
+ const familyThinking = family.thinking;
686
+ if (!spec.reasoning || familyThinking?.effortBudgets === undefined) return spec;
1148
687
  const routing: Partial<Record<Effort | "off", string>> = {};
1149
688
  let hasRouting = false;
1150
- for (const effortKey in family.routing) {
1151
- const target = family.routing[effortKey as Effort | "off"];
689
+ for (const effortKey of VARIANT_ROUTING_KEYS) {
690
+ const target = family.routing[effortKey];
1152
691
  if (target !== undefined && !retired?.has(target)) {
1153
- routing[effortKey as Effort | "off"] = target;
692
+ routing[effortKey] = target;
1154
693
  hasRouting = true;
1155
694
  }
1156
695
  }
1157
- const thinking: ThinkingConfig = { ...family.thinking };
696
+ const thinking: ThinkingConfig = { ...familyThinking };
1158
697
  if (hasRouting) thinking.effortRouting = routing;
1159
698
  if (family.suppressWhenOff) thinking.suppressWhenOff = true;
1160
699
  const offTarget = family.routing.off;
@@ -1191,8 +730,8 @@ function reconcileDefaultMember<TSpec extends VariantSpecLike>(
1191
730
  if (target === undefined || spec.requestModelId === target) return spec;
1192
731
  const routing = spec.thinking?.effortRouting;
1193
732
  if (routing === undefined) return spec;
1194
- for (const key in routing) {
1195
- if (routing[key as Effort | "off"] === target) return { ...spec, requestModelId: target };
733
+ for (const key of VARIANT_ROUTING_KEYS) {
734
+ if (routing[key] === target) return { ...spec, requestModelId: target };
1196
735
  }
1197
736
  return spec;
1198
737
  }
@@ -1202,7 +741,7 @@ function reconcileDefaultMember<TSpec extends VariantSpecLike>(
1202
741
  * through verbatim (by reference), order preserved; the collapsed spec
1203
742
  * replaces the first occurrence of its family.
1204
743
  */
1205
- export function collapseEffortVariants<TSpec extends VariantSpecLike>(
744
+ function collapseWithTable<TSpec extends VariantSpecLike>(
1206
745
  specs: readonly TSpec[],
1207
746
  table: VariantCollapseTable,
1208
747
  ): TSpec[] {
@@ -1216,7 +755,9 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
1216
755
  /** spec ids that belong to a touched family (members + logical id). */
1217
756
  const familyIdBySpecId = new Map<string, string>();
1218
757
 
1219
- for (const family of table.families) {
758
+ const instantiated = instantiateTemplates(table, byId.keys());
759
+ const families = instantiated.length === 0 ? table.families : [...table.families, ...instantiated];
760
+ for (const family of families) {
1220
761
  const retired =
1221
762
  family.retiredMembers !== undefined && family.retiredMembers.length > 0
1222
763
  ? new Set(family.retiredMembers)
@@ -1233,7 +774,7 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
1233
774
  if (rawPresent.length === 0) {
1234
775
  // Inert (no members) or already collapsed (pass-through). A stale
1235
776
  // family.id-keyed snapshot is refreshed in place from the current
1236
- // hand-table family (transport/budgets/routing); retired targets drop.
777
+ // reviewed-table family (transport/budgets/routing); retired targets drop.
1237
778
  // Recycled extraAliases rows are healed in a later pass.
1238
779
  const refreshed =
1239
780
  existing !== undefined && existingCollapsed
@@ -1249,31 +790,36 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
1249
790
  for (const id of rawPresent) familyIdBySpecId.set(id, family.id);
1250
791
  if (existing) familyIdBySpecId.set(family.id, family.id);
1251
792
 
1252
- if (existingCollapsed) {
793
+ if (existingCollapsed && reconciled !== undefined) {
1253
794
  // Mixed input: the collapsed entry wins; stale raw members are deduped
1254
795
  // away. Retired targets are re-pointed first, then the default wire id
1255
796
  // prefers the family's declared member when live and otherwise falls
1256
797
  // back to the first member the account actually advertised.
1257
- replacement.set(family.id, reconcileDefaultMember(reconciled as TSpec, family, new Set(rawPresent)));
798
+ replacement.set(family.id, reconcileDefaultMember(reconciled, family, new Set(rawPresent)));
1258
799
  continue;
1259
800
  }
1260
801
 
1261
- const memberSpecs = rawPresent.map(id => byId.get(id) as TSpec);
802
+ const memberSpecs: TSpec[] = [];
803
+ for (const id of rawPresent) {
804
+ const member = byId.get(id);
805
+ if (member !== undefined) memberSpecs.push(member);
806
+ }
807
+ const firstMember = memberSpecs[0];
808
+ if (firstMember === undefined) continue;
1262
809
  const presentSet = new Set(rawPresent);
1263
810
  const routing: Partial<Record<Effort | "off", string>> = {};
1264
811
  let hasRouting = false;
1265
812
  let hasEffortRoute = false;
1266
813
  let usedAbsentEffortRoute = false;
1267
- for (const effortKey in family.routing) {
1268
- const target = family.routing[effortKey as Effort | "off"];
1269
- const effort = effortKey as Effort | "off";
814
+ for (const effort of VARIANT_ROUTING_KEYS) {
815
+ const target = family.routing[effort];
1270
816
  const targetPresent = target !== undefined && presentSet.has(target);
1271
817
  const preserveAbsentEffort =
1272
818
  target !== undefined && effort !== "off" && family.preserveAbsentEffortRoutes === true;
1273
819
  if (target !== undefined && (targetPresent || preserveAbsentEffort) && !retired?.has(target)) {
1274
820
  routing[effort] = target;
1275
821
  hasRouting = true;
1276
- if (effortKey !== "off") hasEffortRoute = true;
822
+ if (effort !== "off") hasEffortRoute = true;
1277
823
  if (!targetPresent && effort !== "off") usedAbsentEffortRoute = true;
1278
824
  }
1279
825
  }
@@ -1281,16 +827,19 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
1281
827
  // A family that routes efforts to a live thinking backing id reasons
1282
828
  // even when upstream metadata forgot to mark the members.
1283
829
  const reasoning = memberSpecs.some(spec => spec.reasoning) || hasEffortRoute;
1284
- const thinking: ThinkingConfig = { ...family.thinking };
1285
- if (hasRouting) thinking.effortRouting = routing;
1286
- if (family.suppressWhenOff) thinking.suppressWhenOff = true;
830
+ const familyThinking = family.thinking;
831
+ const thinking: ThinkingConfig | undefined = familyThinking ? { ...familyThinking } : undefined;
832
+ if (thinking !== undefined) {
833
+ if (hasRouting) thinking.effortRouting = routing;
834
+ if (family.suppressWhenOff) thinking.suppressWhenOff = true;
835
+ }
1287
836
 
1288
837
  const input: ("text" | "image")[] = [];
1289
838
  if (memberSpecs.some(spec => spec.input.includes("text"))) input.push("text");
1290
839
  if (memberSpecs.some(spec => spec.input.includes("image"))) input.push("image");
1291
840
 
1292
841
  const collapsed: TSpec = {
1293
- ...(memberSpecs[0] as TSpec),
842
+ ...firstMember,
1294
843
  id: family.id,
1295
844
  name: family.name,
1296
845
  reasoning,
@@ -1309,16 +858,20 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
1309
858
  ? family.defaultMember
1310
859
  : undefined;
1311
860
  const defaultWireId = preferredDefault ?? rawPresent.find(id => !retired?.has(id)) ?? rawPresent[0];
861
+ if (defaultWireId === undefined) continue;
1312
862
  if (defaultWireId === family.id) {
1313
863
  if (usedAbsentEffortRoute) {
1314
- collapsed.requestModelId = defaultWireId as string;
864
+ collapsed.requestModelId = defaultWireId;
1315
865
  } else {
1316
866
  delete collapsed.requestModelId;
1317
867
  }
1318
868
  } else {
1319
- collapsed.requestModelId = defaultWireId as string;
869
+ collapsed.requestModelId = defaultWireId;
1320
870
  }
1321
- if (reasoning) {
871
+ // A surface-less family (single wire id, uid-encoded effort) keeps
872
+ // `reasoning` but carries no thinking: every tier would resolve to the
873
+ // same upstream id, so there is nothing to select.
874
+ if (reasoning && thinking !== undefined) {
1322
875
  collapsed.thinking = thinking;
1323
876
  } else {
1324
877
  delete collapsed.thinking;
@@ -1359,7 +912,8 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
1359
912
  }
1360
913
  if (emitted.has(familyId)) continue;
1361
914
  emitted.add(familyId);
1362
- out.push(replacement.get(familyId) as TSpec);
915
+ const familySpec = replacement.get(familyId);
916
+ if (familySpec !== undefined) out.push(familySpec);
1363
917
  }
1364
918
  return out;
1365
919
  }
@@ -1413,13 +967,17 @@ function resolveCollapsedModelReference(
1413
967
  }
1414
968
 
1415
969
  /**
1416
- * Collapse a full mixed-provider list: per provider, the hand table, Cursor's
970
+ * Collapse a full mixed-provider list: per provider, the compiled reviewed table, Cursor's
1417
971
  * conservative live effort-sibling rule, and the automatic `X`/`X-thinking`
1418
972
  * pair rule. Used by the catalog generator; the runtime equivalent lives at
1419
973
  * the model-manager merge point. Output is regrouped by provider — callers
1420
974
  * re-sort.
1421
975
  */
1422
- export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecLike>(specs: readonly TSpec[]): TSpec[] {
976
+ export function collapseVariants<TSpec extends VariantSpecLike>(
977
+ specs: readonly TSpec[],
978
+ opts?: { table?: VariantCollapseTable },
979
+ ): TSpec[] {
980
+ if (opts?.table !== undefined) return collapseWithTable(specs, opts.table);
1423
981
  const byProvider = new Map<string, TSpec[]>();
1424
982
  for (const spec of specs) {
1425
983
  const slice = byProvider.get(spec.provider);
@@ -1431,17 +989,17 @@ export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecL
1431
989
  }
1432
990
  const out: TSpec[] = [];
1433
991
  for (const [provider, slice] of byProvider) {
1434
- const table = VARIANT_COLLAPSE_TABLES[provider];
1435
- let result = table ? collapseEffortVariants(slice, table) : slice;
992
+ const table = reviewedCollapseTable(provider);
993
+ let result = table ? collapseWithTable(slice, table) : slice;
1436
994
  if (provider === "cursor") {
1437
995
  const cursorDerived = deriveCursorEffortFamilies(result);
1438
996
  if (cursorDerived.length > 0) {
1439
- result = collapseEffortVariants(result, { families: cursorDerived });
997
+ result = collapseWithTable(result, { families: cursorDerived });
1440
998
  }
1441
999
  }
1442
- const derived = deriveThinkingPairFamilies(result, table);
1000
+ const derived = deriveThinkingPairFamilies(result, table, provider);
1443
1001
  if (derived.length > 0) {
1444
- result = collapseEffortVariants(result, { families: derived });
1002
+ result = collapseWithTable(result, { families: derived });
1445
1003
  }
1446
1004
  registerCollapsedVariantAliases(provider, result);
1447
1005
  out.push(...result);
@@ -1457,56 +1015,105 @@ export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecL
1457
1015
  * logical specs so thinking wire defaults stay resolved. Untouched entries
1458
1016
  * pass through by reference.
1459
1017
  */
1460
- export function collapseBuiltModelVariants<TApi extends Api>(models: readonly Model<TApi>[]): Model<TApi>[] {
1461
- const collapsed = collapseEffortVariantsAcrossProviders(models);
1018
+ export function collapseBuiltVariants<TApi extends Api>(models: readonly Model<TApi>[]): Model<TApi>[] {
1019
+ const collapsed = collapseVariants(models);
1462
1020
  const inputRefs = new Set<Model<TApi>>(models);
1463
- return collapsed.map(model =>
1464
- // Rebuild from a projected spec (sparse compatConfig) instead of resolved compat.
1465
- inputRefs.has(model) ? model : buildModel({ ...model, compat: model.compatConfig } as unknown as ModelSpec<TApi>),
1466
- );
1021
+ return collapsed.map(model => (inputRefs.has(model) ? model : buildModel(projectModelSpec(model))));
1022
+ }
1023
+
1024
+ function projectModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
1025
+ const {
1026
+ compat: _resolvedCompat,
1027
+ compatConfig,
1028
+ identity: _identity,
1029
+ requiresGlyphTokenization: _requiresGlyphTokenization,
1030
+ supportsComputerUseConfig: _supportsComputerUseConfig,
1031
+ ...spec
1032
+ } = model;
1033
+ return { ...spec, compat: compatConfig };
1467
1034
  }
1468
1035
 
1469
- interface VariantAliasIndex {
1036
+ /**
1037
+ * Alias index over a collapse table (or the live families observed at
1038
+ * runtime). Templated families are materialized on first lookup of any id
1039
+ * they match, so a revision discovery has never advertised still resolves.
1040
+ */
1041
+ class VariantAliasIndex {
1470
1042
  /** lowercased retired id → replacement model id. */
1471
- forward: Map<string, string>;
1043
+ readonly #forward = new Map<string, string>();
1044
+ /**
1045
+ * lowercased provider-scoped alias → replacement model id. Kept apart from
1046
+ * `#forward` so bare-id and reverse lookups never see it.
1047
+ */
1048
+ readonly providerScoped = new Map<string, string>();
1472
1049
  /** replacement model id → retired ids that resolve to it. */
1473
- reverse: Map<string, string[]>;
1050
+ readonly #reverse = new Map<string, string[]>();
1474
1051
  /** Collapsed logical ids declared by the table or observed at runtime. */
1475
- familyIds: Set<string>;
1476
- }
1052
+ readonly #familyIds = new Set<string>();
1053
+ readonly #templates: readonly VariantFamilyTemplate[];
1477
1054
 
1478
- const dynamicAliasIndexes = new Map<string, VariantAliasIndex>();
1479
- const VARIANT_ROUTING_KEYS: readonly (Effort | "off")[] = ["off", ...THINKING_EFFORTS];
1055
+ constructor(templates: readonly VariantFamilyTemplate[] = []) {
1056
+ this.#templates = templates;
1057
+ }
1480
1058
 
1481
- const kAliasIndex = Symbol("variant-collapse.aliasIndex");
1059
+ /** Register `from` → `to`; false when `from` is already claimed. */
1060
+ add(from: string, to: string): boolean {
1061
+ if (from === to || this.#forward.has(from.toLowerCase())) return false;
1062
+ this.#forward.set(from.toLowerCase(), to);
1063
+ const sources = this.#reverse.get(to);
1064
+ if (sources) {
1065
+ sources.push(from);
1066
+ } else {
1067
+ this.#reverse.set(to, [from]);
1068
+ }
1069
+ return true;
1070
+ }
1482
1071
 
1483
- interface TableWithAliasIndex extends VariantCollapseTable {
1484
- [kAliasIndex]?: VariantAliasIndex;
1485
- }
1072
+ /** Register a family: its logical id plus every member and extra alias. */
1073
+ addFamily(family: EffortVariantFamily): void {
1074
+ this.#familyIds.add(family.id);
1075
+ for (const member of family.members) this.add(member, family.id);
1076
+ for (const alias of family.extraAliases ?? []) this.add(alias, family.id);
1077
+ }
1486
1078
 
1487
- function createAliasIndex(): VariantAliasIndex {
1488
- return {
1489
- forward: new Map<string, string>(),
1490
- reverse: new Map<string, string[]>(),
1491
- familyIds: new Set<string>(),
1492
- };
1493
- }
1079
+ markFamily(id: string): void {
1080
+ this.#familyIds.add(id);
1081
+ }
1494
1082
 
1495
- function addVariantAlias(index: VariantAliasIndex, from: string, to: string): boolean {
1496
- if (from === to || index.forward.has(from.toLowerCase())) return false;
1497
- index.forward.set(from.toLowerCase(), to);
1498
- const sources = index.reverse.get(to);
1499
- if (sources) {
1500
- sources.push(from);
1501
- } else {
1502
- index.reverse.set(to, [from]);
1083
+ /** Replacement model id for a retired/member id (case-insensitive). */
1084
+ resolve(id: string): string | undefined {
1085
+ const lower = id.toLowerCase();
1086
+ this.#learn(lower);
1087
+ return this.#forward.get(lower);
1088
+ }
1089
+
1090
+ /** Whether `id` is a collapsed logical id of this index. */
1091
+ isFamily(id: string): boolean {
1092
+ this.#learn(id);
1093
+ return this.#familyIds.has(id);
1094
+ }
1095
+
1096
+ /** Retired ids that resolve to logical `id`. */
1097
+ sourcesOf(id: string): readonly string[] | undefined {
1098
+ this.#learn(id);
1099
+ return this.#reverse.get(id);
1100
+ }
1101
+
1102
+ /** Materialize the template family that owns `id`, once. */
1103
+ #learn(id: string): void {
1104
+ if (this.#templates.length === 0) return;
1105
+ const family = templateFamilyFor(this.#templates, id);
1106
+ if (family === undefined || this.#familyIds.has(family.id)) return;
1107
+ this.addFamily(family);
1503
1108
  }
1504
- return true;
1505
1109
  }
1506
1110
 
1111
+ const dynamicAliasIndexes = new Map<string, VariantAliasIndex>();
1112
+ const kAliasIndex = Symbol("compat-collapse.aliasIndex");
1113
+
1507
1114
  /**
1508
1115
  * Persist aliases embedded in collapsed routing so generated catalog rows and
1509
- * newly discovered families expose the same selector migrations as hand tables.
1116
+ * newly discovered families expose the same selector migrations as reviewed tables.
1510
1117
  */
1511
1118
  function registerCollapsedVariantAliases(provider: Provider, specs: readonly VariantSpecLike[]): void {
1512
1119
  const providerId = provider.toLowerCase();
@@ -1518,51 +1125,52 @@ function registerCollapsedVariantAliases(provider: Provider, specs: readonly Var
1518
1125
  for (const effort of VARIANT_ROUTING_KEYS) {
1519
1126
  const source = routing[effort];
1520
1127
  if (!source || source === spec.id) continue;
1521
- index ??= createAliasIndex();
1522
- registered = addVariantAlias(index, source, spec.id) || registered;
1128
+ index ??= new VariantAliasIndex();
1129
+ registered = index.add(source, spec.id) || registered;
1523
1130
  }
1524
1131
  if (spec.requestModelId && spec.requestModelId !== spec.id) {
1525
- index ??= createAliasIndex();
1526
- registered = addVariantAlias(index, spec.requestModelId, spec.id) || registered;
1132
+ index ??= new VariantAliasIndex();
1133
+ registered = index.add(spec.requestModelId, spec.id) || registered;
1527
1134
  }
1528
- if (registered) index?.familyIds.add(spec.id);
1135
+ if (registered) index?.markFamily(spec.id);
1529
1136
  }
1530
1137
  if (index) dynamicAliasIndexes.set(providerId, index);
1531
1138
  }
1532
1139
 
1533
1140
  function resolveRegisteredVariantAlias(provider: Provider, normalizedModelId: string): string | undefined {
1534
1141
  const providerId = provider.toLowerCase();
1535
- const table = VARIANT_COLLAPSE_TABLES[provider] ?? VARIANT_COLLAPSE_TABLES[providerId];
1142
+ const table = reviewedCollapseTable(provider) ?? reviewedCollapseTable(providerId);
1536
1143
  return (
1537
- (table ? getAliasIndex(table).forward.get(normalizedModelId) : undefined) ??
1538
- dynamicAliasIndexes.get(providerId)?.forward.get(normalizedModelId)
1144
+ (table ? getAliasIndex(table).resolve(normalizedModelId) : undefined) ??
1145
+ dynamicAliasIndexes.get(providerId)?.resolve(normalizedModelId)
1539
1146
  );
1540
1147
  }
1541
1148
 
1542
1149
  function getAliasIndex(table: VariantCollapseTable): VariantAliasIndex {
1543
- const tagged = table as TableWithAliasIndex;
1544
- const cached = tagged[kAliasIndex];
1545
- if (cached) return cached;
1546
- const index = createAliasIndex();
1547
- for (const family of table.families) {
1548
- index.familyIds.add(family.id);
1549
- for (const member of family.members) addVariantAlias(index, member, family.id);
1550
- for (const alias of family.extraAliases ?? []) addVariantAlias(index, alias, family.id);
1150
+ const cached: unknown = Reflect.get(table, kAliasIndex);
1151
+ if (cached instanceof VariantAliasIndex) return cached;
1152
+ const index = new VariantAliasIndex(table.templates);
1153
+ for (const family of table.families) index.addFamily(family);
1154
+ for (const alias in table.providerAliases) {
1155
+ const target = table.providerAliases[alias];
1156
+ if (target !== undefined && alias !== target) index.providerScoped.set(alias.toLowerCase(), target);
1551
1157
  }
1552
- tagged[kAliasIndex] = index;
1158
+ Reflect.set(table, kAliasIndex, index);
1553
1159
  return index;
1554
1160
  }
1555
1161
 
1556
1162
  /**
1557
- * Resolve a retired effort-tier variant id (collapsed member, recycled id) to
1558
- * its replacement model id for `provider` via hand-table or registered live
1559
- * aliases. Returns `undefined` when the id is not a known alias; derived
1560
- * `X-thinking` members also resolve through `stripThinkingVariantToken`.
1561
- * Callers must try an exact model lookup first — a live model always wins over
1562
- * an alias.
1163
+ * Resolve a retired effort-tier variant id, registered live alias, or
1164
+ * provider-scoped native alias to its replacement model id for `provider`.
1165
+ * Returns `undefined` when the id is unknown. Callers must try an exact model
1166
+ * lookup first because a live model always wins over an alias.
1563
1167
  */
1564
- export function resolveVariantAlias(provider: Provider, modelId: string): string | undefined {
1565
- return resolveRegisteredVariantAlias(provider, modelId.trim().toLowerCase());
1168
+ export function resolveVariantSelector(provider: Provider, modelId: string): string | undefined {
1169
+ const normalized = modelId.trim().toLowerCase();
1170
+ const registered = resolveRegisteredVariantAlias(provider, normalized);
1171
+ if (registered !== undefined) return registered;
1172
+ const table = reviewedCollapseTable(provider);
1173
+ return table ? getAliasIndex(table).providerScoped.get(normalized) : undefined;
1566
1174
  }
1567
1175
 
1568
1176
  /** Bare-id alias hit: replacement id plus the providers declaring it. */
@@ -1578,10 +1186,10 @@ export interface BareVariantAliasHit {
1578
1186
  * on unrelated providers too (e.g. a retired Cursor tier id must not resolve
1579
1187
  * to `openai/gpt-5.4`).
1580
1188
  */
1581
- export function resolveBareVariantAlias(modelId: string): BareVariantAliasHit | undefined {
1189
+ export function resolveBareVariantSelector(modelId: string): BareVariantAliasHit | undefined {
1582
1190
  const normalized = modelId.trim().toLowerCase();
1583
1191
  const providerIds = new Set<string>();
1584
- for (const provider in VARIANT_COLLAPSE_TABLES) providerIds.add(provider);
1192
+ for (const provider in REVIEWED_COLLAPSE_TABLES) providerIds.add(provider);
1585
1193
  for (const provider of dynamicAliasIndexes.keys()) providerIds.add(provider);
1586
1194
  for (const provider of providerIds) {
1587
1195
  const hit = resolveRegisteredVariantAlias(provider, normalized);
@@ -1599,15 +1207,15 @@ export function resolveBareVariantAlias(modelId: string): BareVariantAliasHit |
1599
1207
 
1600
1208
  /**
1601
1209
  * Reverse alias lookup: the retired ids that resolve to `modelId` for
1602
- * `provider` via hand-table or registered live aliases. Used to re-key config
1210
+ * `provider` via reviewed-table or registered live aliases. Used to re-key config
1603
1211
  * keyed by raw member ids (models.yml `modelOverrides`, suppressed selectors)
1604
1212
  * onto the collapsed model.
1605
1213
  */
1606
1214
  export function getVariantAliasSources(provider: Provider, modelId: string): readonly string[] {
1607
1215
  const providerId = provider.toLowerCase();
1608
- const table = VARIANT_COLLAPSE_TABLES[provider] ?? VARIANT_COLLAPSE_TABLES[providerId];
1609
- const staticSources = table ? getAliasIndex(table).reverse.get(modelId) : undefined;
1610
- const dynamicSources = dynamicAliasIndexes.get(providerId)?.reverse.get(modelId);
1216
+ const table = reviewedCollapseTable(provider) ?? reviewedCollapseTable(providerId);
1217
+ const staticSources = table ? getAliasIndex(table).sourcesOf(modelId) : undefined;
1218
+ const dynamicSources = dynamicAliasIndexes.get(providerId)?.sourcesOf(modelId);
1611
1219
  if (!staticSources) return dynamicSources ?? [];
1612
1220
  if (!dynamicSources) return staticSources;
1613
1221
  return [...new Set([...staticSources, ...dynamicSources])];