@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
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Typed accessors over the compiled runtime-behavior vocabulary
3
+ * (`rules/runtime/behavior.kdl`): provider/model heuristics that run before
4
+ * or outside exact bundled-model lookup — responses routing, API routing,
5
+ * quota tiers, plan requirements, model limits, roster exclusions, hosted
6
+ * defaults, and pricing peers.
7
+ */
8
+ import { globMatch } from "./cascade";
9
+ import rules from "./rules.json";
10
+ import type { CompiledMatchList } from "./types";
11
+
12
+ const behavior = rules.behavior;
13
+
14
+ function matchesList(match: CompiledMatchList, model: string, modelLower: string): boolean {
15
+ if (match.exact?.some(candidate => candidate === model)) return true;
16
+ if (match.prefix?.some(prefix => model.startsWith(prefix))) return true;
17
+ if (match.substring?.some(sub => model.includes(sub))) return true;
18
+ if (match.token !== undefined) {
19
+ const parts = modelLower.split(/[^a-z0-9]+/);
20
+ if (match.token.some(token => parts.includes(token))) return true;
21
+ }
22
+ if (match.glob?.some(pattern => globMatch(pattern, modelLower))) return true;
23
+ return false;
24
+ }
25
+
26
+ /**
27
+ * Conservative heuristic for a normalized, lowercase discovered model id with
28
+ * no exact bundled record: whether it likely rides the OpenAI Responses API.
29
+ */
30
+ export function isLikelyOpenAIResponsesId(model: string): boolean {
31
+ const rule = behavior.openaiResponsesHeuristic;
32
+ if (!rule) return false;
33
+ if (rule.excludePrefixes.some(prefix => model.startsWith(prefix))) return false;
34
+ if (rule.excludeSubstrings.some(sub => model.includes(sub))) return false;
35
+ return rule.includePrefixes.some(prefix => model.startsWith(prefix));
36
+ }
37
+
38
+ /**
39
+ * Additional catalog-declared operations for a discovered provider/model pair
40
+ * (e.g. `generate_image`), augmenting provider discovery metadata.
41
+ */
42
+ export function modelOperationOverrides(provider: string, model: string): readonly string[] {
43
+ const lower = model.toLowerCase();
44
+ const out: string[] = [];
45
+ for (const rule of behavior.modelOperations) {
46
+ if (rule.provider !== provider || !matchesList(rule.models, lower, lower)) continue;
47
+ for (const operation of rule.operations) {
48
+ if (!out.includes(operation)) out.push(operation);
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * Splits a Cursor effort-suffixed OpenAI sibling id into its base id and
56
+ * declared effort tier. The family gate requires the declared marker
57
+ * (`gpt-`) followed immediately by an ASCII digit; matching stays
58
+ * case-sensitive to preserve Cursor wire-id behavior.
59
+ */
60
+ export function cursorEffortSuffix(model: string): { base: string; tier: string } | undefined {
61
+ const rule = behavior.cursorEffort;
62
+ if (!rule) return undefined;
63
+ for (const tier of rule.tiers) {
64
+ if (!model.endsWith(tier)) continue;
65
+ const prefix = model.slice(0, model.length - tier.length);
66
+ if (!prefix.endsWith("-")) continue;
67
+ const base = prefix.slice(0, -1);
68
+ let family = false;
69
+ let index = base.indexOf(rule.familyMarker);
70
+ while (index !== -1) {
71
+ const next = base.charCodeAt(index + rule.familyMarker.length);
72
+ if (next >= 48 && next <= 57) {
73
+ family = true;
74
+ break;
75
+ }
76
+ index = base.indexOf(rule.familyMarker, index + 1);
77
+ }
78
+ if (!family) return undefined;
79
+ return { base, tier };
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ /** Fixed Cursor `requestedModel` parameters declared for an exact wire model. */
85
+ export function cursorModelParameters(model: string): readonly { id: string; value: string }[] {
86
+ return behavior.cursorParameters.filter(parameter => parameter.model === model);
87
+ }
88
+
89
+ /**
90
+ * The catalog-declared quota scope or display tier for a provider model id.
91
+ * Exact authored memberships win; provider-authored substring fallbacks
92
+ * preserve quota semantics for newly discovered ids.
93
+ */
94
+ export function quotaTierFor(provider: string, model: string): string | undefined {
95
+ const rule = behavior.quotaTiers.find(candidate => candidate.provider === provider);
96
+ if (!rule) return undefined;
97
+ for (const tier of rule.tiers) {
98
+ if (tier.models.includes(model)) return tier.label;
99
+ }
100
+ for (const fallback of rule.fallbacks) {
101
+ if (model.includes(fallback.substring)) return fallback.label;
102
+ }
103
+ return undefined;
104
+ }
105
+
106
+ /** Whether a provider has catalog-authored model quota scopes. */
107
+ export function hasQuotaTierPolicy(provider: string): boolean {
108
+ return behavior.quotaTiers.some(rule => rule.provider === provider);
109
+ }
110
+
111
+ /** The provider-default wire model for a model-less hosted operation. */
112
+ export function hostedDefaultModel(provider: string): string | undefined {
113
+ return behavior.hostedDefaults.find(entry => entry.provider === provider)?.model;
114
+ }
115
+
116
+ /** One resolved API route for a provider model id. */
117
+ export interface ApiRouteMatch {
118
+ /** Transport API the id rides. */
119
+ api: string;
120
+ /** Wire id after prefix stripping, when the route declares `strip-prefix`. */
121
+ requestModelId?: string;
122
+ }
123
+
124
+ /**
125
+ * Resolves the declared API route for a provider model id. Routes match in
126
+ * declaration order; a `strip-prefix` route with a prefix matcher strips the
127
+ * matched prefix off the wire id. Falls back to the node's `default` API.
128
+ */
129
+ export function apiRouteFor(provider: string, model: string): ApiRouteMatch | undefined {
130
+ const table = behavior.apiRoutes.find(candidate => candidate.provider === provider);
131
+ if (!table) return undefined;
132
+ const lower = model.toLowerCase();
133
+ for (const route of table.routes) {
134
+ if (!matchesList(route.match, model, lower)) continue;
135
+ const out: ApiRouteMatch = { api: route.api };
136
+ if (route.stripPrefix) {
137
+ const prefix = route.match.prefix?.find(candidate => model.startsWith(candidate));
138
+ if (prefix) out.requestModelId = model.slice(prefix.length);
139
+ }
140
+ return out;
141
+ }
142
+ return table.default !== undefined ? { api: table.default } : undefined;
143
+ }
144
+
145
+ /** Exact model ids named by a provider's api-routes rules (cache-migration drop lists). */
146
+ export function apiRouteExactModelIds(provider: string): string[] {
147
+ const table = behavior.apiRoutes.find(candidate => candidate.provider === provider);
148
+ if (!table) return [];
149
+ const ids: string[] = [];
150
+ for (const route of table.routes) {
151
+ for (const id of route.match.exact ?? []) {
152
+ if (!ids.includes(id)) ids.push(id);
153
+ }
154
+ }
155
+ return ids;
156
+ }
157
+
158
+ /** Declared context-window / max-token pins for a provider model id. */
159
+ export function modelLimitsFor(provider: string, model: string): { context?: number; maxTokens?: number } | undefined {
160
+ for (const table of behavior.modelLimits) {
161
+ if (table.provider !== provider) continue;
162
+ const limit = table.limits.find(entry => entry.model === model);
163
+ if (limit) {
164
+ const out: { context?: number; maxTokens?: number } = {};
165
+ if (limit.context !== undefined) out.context = limit.context;
166
+ if (limit.maxTokens !== undefined) out.maxTokens = limit.maxTokens;
167
+ return out;
168
+ }
169
+ }
170
+ return undefined;
171
+ }
172
+
173
+ /** Whether a provider roster entry is a declared non-chat/unsupported SKU. */
174
+ export function isExcludedModel(provider: string, model: string): boolean {
175
+ const lower = model.toLowerCase();
176
+ return behavior.excludeModels.some(rule => rule.provider === provider && matchesList(rule.match, lower, lower));
177
+ }
178
+
179
+ /**
180
+ * Whether a provider is retired: its entire roster is excluded from the
181
+ * generated bundle and must never be resurrected from the previous snapshot.
182
+ */
183
+ export function isRetiredProvider(provider: string): boolean {
184
+ return behavior.retiredProviders.includes(provider);
185
+ }
186
+
187
+ /** The declared subscription tier required to use a provider model id, if any. */
188
+ export function planRequirementFor(provider: string, model: string): string | undefined {
189
+ const rule = behavior.planRequirements.find(candidate => candidate.provider === provider);
190
+ if (!rule) return undefined;
191
+ const lower = model.toLowerCase();
192
+ for (const tier of rule.tiers) {
193
+ if (matchesList(tier.match, model, lower)) return tier.tier;
194
+ }
195
+ return undefined;
196
+ }
197
+
198
+ /** Cross-provider pricing-peer resolution for one provider model id. */
199
+ export function pricingPeerFor(
200
+ provider: string,
201
+ model: string,
202
+ ): { peers: readonly string[]; peerId: string } | undefined {
203
+ const rule = behavior.pricingPeers.find(candidate => candidate.provider === provider);
204
+ if (!rule) return undefined;
205
+ const alias = rule.aliases.find(candidate => candidate.model === model);
206
+ return { peers: rule.peers, peerId: alias?.peerId ?? model };
207
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Runtime compat cascade: resolves per-axis wire/thinking/catalog assignments
3
+ * for one structured model target from the compiled rule tree.
4
+ *
5
+ * Faithful port of the o2 reference resolver (`cascade.rs`): rules are
6
+ * conjunctions over `(class, provider, family, revision, models)`; per axis
7
+ * the matching rule with the greatest `(model-selector exactness,
8
+ * constrained-dimension count, priority)` tuple wins, and an equal-tuple
9
+ * same-axis contest throws {@link AmbiguousOverlapError}. Declaration and
10
+ * file order are never semantic.
11
+ */
12
+ import { parseRevision, type Revision, type RevisionTerm, revisionSatisfies } from "./revision";
13
+ import rules from "./rules.json";
14
+ import type { CompiledCascade, CompiledRule, CompiledSelector, ResolvedAxes, ResolveTarget } from "./types";
15
+
16
+ /**
17
+ * Two equal-rank rules contest one axis for a target. CI-time for bundled
18
+ * targets (the parity sweep exercises every catalog row); fix with an
19
+ * explicit `priority=` in the KDL, never in code.
20
+ */
21
+ export class AmbiguousOverlapError extends Error {
22
+ constructor(
23
+ readonly provider: string,
24
+ readonly model: string,
25
+ readonly axis: string,
26
+ readonly first: string,
27
+ readonly second: string,
28
+ ) {
29
+ super(
30
+ `ambiguous overlap for \`${provider}/${model}\` on axis \`${axis}\`: rules \`${first}\` and \`${second}\` tie; add an explicit priority`,
31
+ );
32
+ this.name = "AmbiguousOverlapError";
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Anchored `*`-wildcard match; both sides must be pre-lowercased. `*` spans
38
+ * any substring; non-wildcard text stays anchored in order.
39
+ */
40
+ export function globMatch(pattern: string, value: string): boolean {
41
+ const segments = pattern.split("*");
42
+ if (segments.length === 1) return value === pattern;
43
+ const head = segments[0];
44
+ if (!value.startsWith(head)) return false;
45
+ let remainder = value.slice(head.length);
46
+ for (let i = 1; i < segments.length - 1; i++) {
47
+ const segment = segments[i];
48
+ if (!segment) continue;
49
+ const found = remainder.indexOf(segment);
50
+ if (found === -1) return false;
51
+ remainder = remainder.slice(found + segment.length);
52
+ }
53
+ const last = segments[segments.length - 1];
54
+ return last === "" || remainder.endsWith(last);
55
+ }
56
+
57
+ interface IndexedRule {
58
+ compiled: CompiledRule;
59
+ revision?: RevisionTerm[];
60
+ priority: number;
61
+ dimensions: number;
62
+ hasExactEffortsRule: boolean;
63
+ }
64
+
65
+ let ruleIndex: IndexedRule[] | undefined;
66
+
67
+ function buildRuleIndex(cascade: CompiledCascade): IndexedRule[] {
68
+ return cascade.rules.map(compiled => {
69
+ const revision = compiled.revision?.map(term => {
70
+ const parsed = parseRevision(term.revision);
71
+ if (!parsed) throw new Error(`invalid compiled revision term in ${compiled.source}`);
72
+ return { op: term.op, revision: parsed } satisfies RevisionTerm;
73
+ });
74
+ const dimensions =
75
+ Number(compiled.class !== undefined) +
76
+ Number(compiled.providers !== undefined) +
77
+ Number(compiled.family !== undefined) +
78
+ Number(compiled.revision !== undefined) +
79
+ Number(compiled.models !== undefined);
80
+ return {
81
+ compiled,
82
+ revision,
83
+ priority: compiled.priority ?? 0,
84
+ dimensions,
85
+ hasExactEffortsRule: compiled.thinking !== undefined && "efforts" in compiled.thinking,
86
+ };
87
+ });
88
+ }
89
+
90
+ function getRuleIndex(): IndexedRule[] {
91
+ ruleIndex ??= buildRuleIndex(rules.cascade);
92
+ return ruleIndex;
93
+ }
94
+
95
+ function selectorMatches(selector: CompiledSelector, model: string, modelLower: string): boolean {
96
+ switch (selector.kind) {
97
+ case "exact":
98
+ return selector.value === model;
99
+ case "glob":
100
+ return globMatch(selector.value, modelLower);
101
+ case "token": {
102
+ for (const part of modelLower.split(/[^a-z0-9]+/)) {
103
+ if (part === selector.value) return true;
104
+ }
105
+ return false;
106
+ }
107
+ }
108
+ }
109
+
110
+ /** `(exactness, dimensions, priority)` when the rule matches, else undefined. */
111
+ function rankRule(
112
+ rule: IndexedRule,
113
+ target: ResolveTarget,
114
+ revision: Revision | undefined,
115
+ modelLower: string,
116
+ ): readonly [number, number, number] | undefined {
117
+ const { compiled } = rule;
118
+ if (compiled.class !== undefined && compiled.class !== target.class) return undefined;
119
+ if (compiled.providers !== undefined && !compiled.providers.includes(target.provider)) return undefined;
120
+ if (compiled.family !== undefined && compiled.family !== target.family) return undefined;
121
+ if (rule.revision !== undefined && (!revision || !revisionSatisfies(revision, rule.revision))) return undefined;
122
+ let exactness = 0;
123
+ if (compiled.models !== undefined) {
124
+ let best = -1;
125
+ for (const selector of compiled.models) {
126
+ if (!selectorMatches(selector, target.model, modelLower)) continue;
127
+ const value = selector.kind === "exact" ? 2 : 1;
128
+ if (value > best) best = value;
129
+ }
130
+ if (best < 0) return undefined;
131
+ exactness = best;
132
+ }
133
+ return [exactness, rule.dimensions, rule.priority];
134
+ }
135
+
136
+ type WinnerTable = Record<string, { rank: readonly [number, number, number]; rule: IndexedRule }>;
137
+
138
+ function rankCompare(a: readonly [number, number, number], b: readonly [number, number, number]): number {
139
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
140
+ }
141
+
142
+ function contest(
143
+ winners: WinnerTable,
144
+ axes: Record<string, unknown> | undefined,
145
+ rank: readonly [number, number, number],
146
+ rule: IndexedRule,
147
+ target: ResolveTarget,
148
+ ): void {
149
+ if (!axes) return;
150
+ for (const axis in axes) {
151
+ const held = winners[axis];
152
+ if (held) {
153
+ const order = rankCompare(held.rank, rank);
154
+ if (order === 0) {
155
+ throw new AmbiguousOverlapError(
156
+ target.provider,
157
+ target.model,
158
+ axis,
159
+ held.rule.compiled.source,
160
+ rule.compiled.source,
161
+ );
162
+ }
163
+ if (order > 0) continue;
164
+ }
165
+ winners[axis] = { rank, rule };
166
+ }
167
+ }
168
+
169
+ function collect(winners: WinnerTable, pick: (rule: CompiledRule) => Record<string, unknown> | undefined) {
170
+ const out: Record<string, unknown> = {};
171
+ for (const axis in winners) {
172
+ out[axis] = pick(winners[axis].rule.compiled)?.[axis];
173
+ }
174
+ return out;
175
+ }
176
+
177
+ /**
178
+ * Resolves wire, thinking, and catalog assignments for one structured target.
179
+ *
180
+ * Thinking axes are gated on `target.reasoning`, except that an exact model
181
+ * selector declaring `thinking-efforts` upgrades the target (a reviewed
182
+ * correction to stale capability metadata). Family and revision selectors
183
+ * never match targets missing that identity rank. Unmatched targets resolve
184
+ * to empty maps.
185
+ *
186
+ * @throws AmbiguousOverlapError when two equal-rank rules contest one axis.
187
+ */
188
+ export function resolveCascade(target: ResolveTarget): ResolvedAxes {
189
+ return resolveOverIndex(getRuleIndex(), target);
190
+ }
191
+
192
+ /**
193
+ * Resolves a target against an arbitrary compiled cascade (test seam and
194
+ * scratch evaluations); `resolveCascade` delegates here with the bundled
195
+ * rule index.
196
+ */
197
+ export function resolveCascadeRules(cascade: CompiledCascade, target: ResolveTarget): ResolvedAxes {
198
+ return resolveOverIndex(buildRuleIndex(cascade), target);
199
+ }
200
+
201
+ function resolveOverIndex(index: IndexedRule[], target: ResolveTarget): ResolvedAxes {
202
+ const modelLower = target.model.toLowerCase();
203
+ const revision = target.revision === undefined ? undefined : parseRevision(target.revision);
204
+ const wire: WinnerTable = {};
205
+ const thinking: WinnerTable = {};
206
+ const catalog: WinnerTable = {};
207
+ let reasoning = target.reasoning;
208
+ if (!reasoning) {
209
+ for (const rule of index) {
210
+ if (!rule.hasExactEffortsRule) continue;
211
+ const rank = rankRule(rule, target, revision, modelLower);
212
+ if (rank && rank[0] === 2) {
213
+ reasoning = true;
214
+ break;
215
+ }
216
+ }
217
+ }
218
+ for (const rule of index) {
219
+ const rank = rankRule(rule, target, revision, modelLower);
220
+ if (!rank) continue;
221
+ contest(wire, rule.compiled.wire, rank, rule, target);
222
+ contest(catalog, rule.compiled.catalog, rank, rule, target);
223
+ if (reasoning) contest(thinking, rule.compiled.thinking, rank, rule, target);
224
+ }
225
+ return {
226
+ wire: collect(wire, rule => rule.wire),
227
+ thinking: collect(thinking, rule => rule.thinking),
228
+ catalog: collect(catalog, rule => rule.catalog),
229
+ };
230
+ }