@rigelbuild/omp-catalog 18.0.3

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 (117) hide show
  1. package/CHANGELOG.md +1204 -0
  2. package/LICENSE +22 -0
  3. package/README.md +43 -0
  4. package/THIRD-PARTY-NOTICES.txt +22909 -0
  5. package/dist/types/build.d.ts +19 -0
  6. package/dist/types/compat/anthropic.d.ts +18 -0
  7. package/dist/types/compat/apply.d.ts +7 -0
  8. package/dist/types/compat/bedrock.d.ts +3 -0
  9. package/dist/types/compat/devin.d.ts +9 -0
  10. package/dist/types/compat/openai.d.ts +28 -0
  11. package/dist/types/discovery/antigravity.d.ts +70 -0
  12. package/dist/types/discovery/codex.d.ts +36 -0
  13. package/dist/types/discovery/cursor-proto.d.ts +5368 -0
  14. package/dist/types/discovery/cursor.d.ts +23 -0
  15. package/dist/types/discovery/devin-proto.d.ts +1549 -0
  16. package/dist/types/discovery/devin.d.ts +24 -0
  17. package/dist/types/discovery/gemini-cli.d.ts +33 -0
  18. package/dist/types/discovery/gemini.d.ts +25 -0
  19. package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
  20. package/dist/types/discovery/index.d.ts +7 -0
  21. package/dist/types/discovery/openai-compatible.d.ts +89 -0
  22. package/dist/types/discovery/protobuf.d.ts +102 -0
  23. package/dist/types/effort.d.ts +10 -0
  24. package/dist/types/fireworks-model-id.d.ts +23 -0
  25. package/dist/types/hosts.d.ts +169 -0
  26. package/dist/types/identity/bundled.d.ts +3 -0
  27. package/dist/types/identity/classify.d.ts +69 -0
  28. package/dist/types/identity/dialect.d.ts +3 -0
  29. package/dist/types/identity/family.d.ts +239 -0
  30. package/dist/types/identity/id.d.ts +12 -0
  31. package/dist/types/identity/index.d.ts +8 -0
  32. package/dist/types/identity/markers.d.ts +4 -0
  33. package/dist/types/identity/priority.d.ts +1 -0
  34. package/dist/types/identity/reference.d.ts +29 -0
  35. package/dist/types/index.d.ts +19 -0
  36. package/dist/types/model-cache.d.ts +23 -0
  37. package/dist/types/model-manager.d.ts +77 -0
  38. package/dist/types/model-thinking.d.ts +70 -0
  39. package/dist/types/model-tokenizer.d.ts +8 -0
  40. package/dist/types/models.d.ts +14 -0
  41. package/dist/types/openai-pricing.d.ts +17 -0
  42. package/dist/types/provider-models/bundled-references.d.ts +13 -0
  43. package/dist/types/provider-models/cache-provider-id.d.ts +11 -0
  44. package/dist/types/provider-models/descriptor-types.d.ts +76 -0
  45. package/dist/types/provider-models/descriptors.d.ts +505 -0
  46. package/dist/types/provider-models/google.d.ts +29 -0
  47. package/dist/types/provider-models/index.d.ts +7 -0
  48. package/dist/types/provider-models/ollama.d.ts +22 -0
  49. package/dist/types/provider-models/openai-compat.d.ts +646 -0
  50. package/dist/types/provider-models/special.d.ts +53 -0
  51. package/dist/types/types.d.ts +876 -0
  52. package/dist/types/utils.d.ts +20 -0
  53. package/dist/types/variant-collapse.d.ts +137 -0
  54. package/dist/types/wire/alibaba-token-plan.d.ts +22 -0
  55. package/dist/types/wire/codex.d.ts +66 -0
  56. package/dist/types/wire/coreweave.d.ts +12 -0
  57. package/dist/types/wire/gemini-headers.d.ts +62 -0
  58. package/dist/types/wire/github-copilot.d.ts +47 -0
  59. package/dist/types/wire/image-fetchers.d.ts +76 -0
  60. package/package.json +102 -0
  61. package/src/build.ts +103 -0
  62. package/src/compat/anthropic.ts +179 -0
  63. package/src/compat/apply.ts +15 -0
  64. package/src/compat/bedrock.ts +153 -0
  65. package/src/compat/devin.ts +12 -0
  66. package/src/compat/openai.ts +864 -0
  67. package/src/discovery/antigravity.ts +257 -0
  68. package/src/discovery/codex.ts +456 -0
  69. package/src/discovery/cursor-proto.ts +8329 -0
  70. package/src/discovery/cursor.ts +409 -0
  71. package/src/discovery/devin-proto.ts +2037 -0
  72. package/src/discovery/devin.ts +152 -0
  73. package/src/discovery/gemini-cli.ts +198 -0
  74. package/src/discovery/gemini.ts +260 -0
  75. package/src/discovery/gitlab-duo-workflow.ts +859 -0
  76. package/src/discovery/index.ts +7 -0
  77. package/src/discovery/openai-compatible.ts +266 -0
  78. package/src/discovery/protobuf.ts +1073 -0
  79. package/src/effort.ts +18 -0
  80. package/src/fireworks-model-id.ts +50 -0
  81. package/src/hosts.ts +178 -0
  82. package/src/identity/bundled.ts +28 -0
  83. package/src/identity/classify.ts +242 -0
  84. package/src/identity/dialect.ts +42 -0
  85. package/src/identity/family.ts +477 -0
  86. package/src/identity/id.ts +81 -0
  87. package/src/identity/index.ts +8 -0
  88. package/src/identity/markers.ts +49 -0
  89. package/src/identity/priority.ts +59 -0
  90. package/src/identity/reference.ts +167 -0
  91. package/src/index.ts +19 -0
  92. package/src/model-cache.ts +346 -0
  93. package/src/model-manager.ts +683 -0
  94. package/src/model-thinking.ts +925 -0
  95. package/src/model-tokenizer.ts +75 -0
  96. package/src/models.json +304657 -0
  97. package/src/models.json.d.ts +9 -0
  98. package/src/models.ts +108 -0
  99. package/src/openai-pricing.ts +29 -0
  100. package/src/provider-models/bundled-references.ts +87 -0
  101. package/src/provider-models/cache-provider-id.ts +97 -0
  102. package/src/provider-models/descriptor-types.ts +85 -0
  103. package/src/provider-models/descriptors.ts +585 -0
  104. package/src/provider-models/google.ts +117 -0
  105. package/src/provider-models/index.ts +7 -0
  106. package/src/provider-models/ollama.ts +204 -0
  107. package/src/provider-models/openai-compat.ts +6405 -0
  108. package/src/provider-models/special.ts +213 -0
  109. package/src/types.ts +1032 -0
  110. package/src/utils.ts +69 -0
  111. package/src/variant-collapse.ts +1555 -0
  112. package/src/wire/alibaba-token-plan.ts +54 -0
  113. package/src/wire/codex.ts +117 -0
  114. package/src/wire/coreweave.ts +51 -0
  115. package/src/wire/gemini-headers.ts +135 -0
  116. package/src/wire/github-copilot.ts +147 -0
  117. package/src/wire/image-fetchers.ts +159 -0
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Model-family id predicates: the shared vocabulary for "is this id a member
3
+ * of family X" checks that gate wire-level behavior across hosts (a Kimi or
4
+ * DeepSeek model keeps its quirks no matter which OpenAI-compatible proxy
5
+ * serves it). Looser per-feature heuristics (e.g. stream-markup healing)
6
+ * deliberately keep their own patterns — only provably-shared matchers live
7
+ * here.
8
+ */
9
+
10
+ import {
11
+ bareModelId,
12
+ isAnthropicAdaptiveGenAtLeast,
13
+ isFableOrMythos,
14
+ parseAnthropicModel,
15
+ parseGlmModel,
16
+ parseKnownModel,
17
+ parseOpenAIModel,
18
+ semverGte,
19
+ } from "./classify";
20
+
21
+ /** Bounded process-lifetime cache memo helper. */
22
+ function memo<T>(fn: (modelId: string) => T): (modelId: string) => T {
23
+ const cache = new Map<string, T>();
24
+ return (modelId: string) => {
25
+ if (cache.has(modelId)) {
26
+ return cache.get(modelId) as T;
27
+ }
28
+ const result = fn(modelId);
29
+ cache.set(modelId, result);
30
+ return result;
31
+ };
32
+ }
33
+
34
+ /** Kimi family ids in any namespace form (`moonshotai/kimi-*`, `kimi-k2.6`, `vendor/kimi.x`). */
35
+ export const isKimiModelId = memo((modelId: string): boolean => {
36
+ return modelId.includes("moonshotai/kimi") || /(^|\/)kimi[-.]/i.test(modelId);
37
+ });
38
+
39
+ /** Kimi K2.6 specifically, including router ids that spell the version `k2p6`. */
40
+ export const isKimiK26ModelId = memo((modelId: string): boolean => {
41
+ return /(^|\/)kimi-k2(?:\.6|p6)(?:[-:]|$)/i.test(modelId);
42
+ });
43
+
44
+ /**
45
+ * Kimi K3 in any namespace form (`kimi-k3`, `kimi-k3.1`, `kimi-k3-turbo`,
46
+ * `moonshotai/kimi-k3`). K3 always reasons and drives thinking via OpenAI-style
47
+ * `reasoning_effort: "max"`, not the K2.x binary `thinking: { type }` block —
48
+ * see the moonshot discovery mapper and `buildOpenAICompat`.
49
+ */
50
+ export const isKimiK3ModelId = memo((modelId: string): boolean => {
51
+ return /(^|\/)kimi-k3(?:\.\d+)?(?:[-.:_]|$)/i.test(modelId);
52
+ });
53
+
54
+ /**
55
+ * Claude ids in any namespace form: bare (`claude-*`), path-namespaced
56
+ * (`anthropic/claude.x`), or dot-prefixed (`us.anthropic.claude-…`,
57
+ * `global.anthropic.claude-…`, `au.anthropic.claude-…` — Bedrock cross-region
58
+ * inference profiles). Necessary because {@link parseAnthropicModel} only
59
+ * classifies kinds enumerated in its regex, so any dotted profile whose kind
60
+ * (e.g. `haiku`) is not enumerated would otherwise slip past this fallback.
61
+ */
62
+ export const isClaudeModelId = memo((modelId: string): boolean => {
63
+ return /(^|[/.])claude[-.]/i.test(modelId);
64
+ });
65
+
66
+ /** `anthropic/`-namespaced ids (aggregator catalogs like OpenRouter). */
67
+ export const isAnthropicNamespacedModelId = memo((modelId: string): boolean => {
68
+ return /(^|\/)anthropic\//i.test(modelId);
69
+ });
70
+
71
+ /** Qwen family ids (substring match — Qwen SKUs have no stable prefix shape). */
72
+ export const isQwenModelId = memo((modelId: string): boolean => {
73
+ return modelId.toLowerCase().includes("qwen");
74
+ });
75
+
76
+ /**
77
+ * Open-weight Qwen 3.8+ releases (`qwen3.8-27b`, `qwen3.8-2.4t-a95b`, GGUF
78
+ * names like `Qwen3.8-27B-UD-Q6_K_XL`) whose chat template steers thinking
79
+ * depth through a `reasoning_effort` template kwarg (`low`/`medium`/`xhigh`,
80
+ * template default `xhigh`; thinking itself cannot be disabled). Compared
81
+ * component-wise so `qwen3.10` sorts after `qwen3.8`. API-only `-max` SKUs are
82
+ * excluded — Dashscope drives them through OpenAI-style `reasoning_effort`
83
+ * with curated compat. The trailing guard rejects parameter-count lookalikes
84
+ * (`qwen-3.8b`) without breaking `qwen3.8-27b`.
85
+ */
86
+ export const isQwen38PlusTemplateEffortModelId = memo((modelId: string): boolean => {
87
+ const match = /qwen[-_ ]?(\d+)\.(\d+)(?![\dbB])/i.exec(modelId);
88
+ if (!match) return false;
89
+ const major = Number.parseInt(match[1], 10);
90
+ const minor = Number.parseInt(match[2], 10);
91
+ if (major < 3 || (major === 3 && minor < 8)) return false;
92
+ return !/^-max(?:$|[-.:])/i.test(modelId.slice(match.index + match[0].length));
93
+ });
94
+
95
+ /** Gemma open-weights family (`gemma-3-27b-it`, `google/gemma-4-E2B-it`, `gemma2-9b`). */
96
+ export const isGemmaModelId = memo((modelId: string): boolean => {
97
+ return /(^|\/)gemma[-.]?\d/i.test(modelId);
98
+ });
99
+
100
+ /** DeepSeek family by id or display name (proxies often rename the id but keep the name). */
101
+ export const isDeepseekModelIdOrName = memo((value: string): boolean => {
102
+ return value.toLowerCase().includes("deepseek");
103
+ });
104
+
105
+ /**
106
+ * DeepSeek V4 Flash SKU in any host/namespace form (`deepseek-v4-flash`, dated
107
+ * `deepseek-v4-flash-0731`, `deepseek-ai/DeepSeek-V4-Flash`). Both V4 SKUs
108
+ * (Flash and Pro) accept the `low` reasoning_effort tier; this predicate keeps
109
+ * Flash distinguishable from Pro where a host quirk splits them (e.g.
110
+ * OpenRouter exposes `low` on Flash but only `high` on non-Flash V4).
111
+ * See https://api-docs.deepseek.com/api/create-chat-completion.
112
+ */
113
+ export const isDeepseekV4FlashModelId = memo((modelId: string): boolean => {
114
+ return bareModelId(modelId).toLowerCase().includes("deepseek-v4-flash");
115
+ });
116
+
117
+ /** Xiaomi MiMo family by id or display name. */
118
+ export const isMimoModelIdOrName = memo((value: string): boolean => {
119
+ return value.toLowerCase().includes("mimo");
120
+ });
121
+
122
+ /** StepFun Step 3.7 Flash SKU in any namespace form (`kilo/stepfun/step-3.7-flash:free`). */
123
+ export const isStep37FlashModelId = memo((modelId: string): boolean => {
124
+ return modelId.toLowerCase().includes("step-3.7-flash");
125
+ });
126
+
127
+ /** Gemini family ids in any namespace form (`gemini-*`, `google/gemini-*`, `openrouter/google/gemini-…`). */
128
+ export const isGeminiModelId = memo((modelId: string): boolean => {
129
+ return /(^|\/)gemini[-.]?/i.test(modelId);
130
+ });
131
+
132
+ /** Grok family ids across namespace and delimiter forms (`grok-*`, `cursor-grok-*`, `xai/grok-*`). */
133
+ export const isGrokModelId = memo((modelId: string): boolean => {
134
+ return /(?:^|[./_-])grok(?:[-.]|$)/i.test(modelId);
135
+ });
136
+
137
+ const GROK_EFFORT_CAPABLE_PREFIXES = [
138
+ "grok-3-mini",
139
+ "grok-4.20-multi-agent",
140
+ "grok-4.3",
141
+ "grok-4.5",
142
+ "grok-4.6",
143
+ ] as const;
144
+
145
+ /**
146
+ * Grok SKUs that expose the wire `reasoning.effort` dial. Other Grok reasoners
147
+ * (e.g. `grok-build`, `grok-4.20-0309-reasoning`) think natively but reject the
148
+ * param, so callers must omit reasoning effort for them. `grok-4.6` accepts
149
+ * `low`/`medium`/`high`/`xhigh` and 400s on `max`.
150
+ */
151
+ export const isGrokReasoningEffortCapable = memo((modelId: string): boolean => {
152
+ const bare = bareModelId(modelId).trim().toLowerCase();
153
+ if (!bare) return false;
154
+ return GROK_EFFORT_CAPABLE_PREFIXES.some(prefix => bare.startsWith(prefix));
155
+ });
156
+
157
+ /**
158
+ * `grok-4.20-multi-agent*` uses `reasoning.effort` to pick agent count
159
+ * (`xhigh` is the 16-agent mode). Other first-party Grok effort SKUs stay on
160
+ * `low|medium|high` unless {@link isGrokXHighEffortCapable} (currently
161
+ * `grok-4.6*` plus multi-agent).
162
+ * https://docs.x.ai/developers/model-capabilities/text/reasoning
163
+ */
164
+ export const isGrokMultiAgentModelId = memo((modelId: string): boolean => {
165
+ return bareModelId(modelId).trim().toLowerCase().startsWith("grok-4.20-multi-agent");
166
+ });
167
+
168
+ /**
169
+ * First-party Grok SKUs whose Responses wire accepts `reasoning.effort: "xhigh"`.
170
+ * `grok-4.6*` documents xhigh as a reasoning depth; multi-agent uses it as
171
+ * 16-agent mode. `grok-4.5` / `grok-4.3` / `grok-3-mini` do not.
172
+ */
173
+ export const isGrokXHighEffortCapable = memo((modelId: string): boolean => {
174
+ if (isGrokMultiAgentModelId(modelId)) return true;
175
+ return bareModelId(modelId).trim().toLowerCase().startsWith("grok-4.6");
176
+ });
177
+
178
+ /**
179
+ * MiniMax M2-generation family (M2, M2.1, M2.5, M2.7, including `-highspeed`/
180
+ * `-lightning`/`-her`/`-turbo` variants, dotless aliases like `minimax-m21`,
181
+ * and short `minimax/m2-…` ids on aggregator hosts). Underlying model accepts
182
+ * only `low|medium|high` for `reasoning_effort` and 400s on `minimal`,
183
+ * `xhigh`, or `none` — so hosts whose default effort map otherwise lowers
184
+ * `minimal` to `none` (Fireworks) or expects the full 5-tier scale must
185
+ * clamp instead. Excludes M1, M3, MiniMax-Text-01, music, hailuo, voice ids.
186
+ */
187
+ export const isMinimaxM2FamilyModelId = memo((modelId: string): boolean => {
188
+ const lower = modelId.toLowerCase();
189
+ if (!lower.includes("minimax")) return false;
190
+ // Boundary-delimited `m2` token followed by zero or more digits (dotless
191
+ // variants like `m21`/`m25`/`m27`) and an optional dotted minor version.
192
+ return /(?:^|[/.-])m2\d*(?:[.-]\d+)?(?:[-.:_]|$)/i.test(lower);
193
+ });
194
+
195
+ /** MiniMax M3 family ids in bundled/default and aggregator namespace forms. */
196
+ export const isMinimaxM3FamilyModelId = memo((modelId: string): boolean => {
197
+ const lower = modelId.toLowerCase();
198
+ if (!lower.includes("minimax")) return false;
199
+ return /(?:^|[/._-])(?:minimax[/._-])?m3(?:[-.:_]|$)/i.test(lower);
200
+ });
201
+
202
+ /**
203
+ * OpenAI gpt-oss family (`gpt-oss-20b`, `gpt-oss-120b`, `gpt-oss:120b`,
204
+ * `vendor/gpt-oss-…`). The Harmony reasoning format only accepts
205
+ * `low|medium|high` for `reasoning_effort` and rejects `minimal`, `xhigh`,
206
+ * and `none`.
207
+ */
208
+ export const isOpenAIGptOssModelId = memo((modelId: string): boolean => {
209
+ return /(^|\/)gpt-oss[-:]/i.test(modelId);
210
+ });
211
+
212
+ /**
213
+ * Meta Muse Spark ids (`muse-spark-1.1`, `muse-spark-1.2`,
214
+ * `muse-spark-1.2-contributor`, `meta/muse-spark-1.2`). The Responses
215
+ * `reasoning.effort` wire accepts `none` (thinking-off) plus
216
+ * `minimal`/`low`/`medium`/`high`/`xhigh`.
217
+ */
218
+ export const isMuseSparkModelId = memo((modelId: string): boolean => {
219
+ return /(^|\/)muse-spark(?:[-.]|$)/i.test(modelId);
220
+ });
221
+
222
+ /** OpenAI model ids (gpt-*, chatgpt-*, o1/o3/o4 SKUs, codex-*, or openai/*). */
223
+ export const isOpenAIModelId = memo((modelId: string): boolean => {
224
+ return (
225
+ /(^|\/)(?:gpt|chatgpt|codex)[-.]/i.test(modelId) ||
226
+ /(^|\/)o[134](?:[-.]|$)/i.test(modelId) ||
227
+ modelId.toLowerCase().includes("openai/")
228
+ );
229
+ });
230
+
231
+ /** OpenAI models at or above the gpt-5.4 wire generation, keyed off the parsed version. */
232
+ const isOpenAIWireGen54Plus = memo((modelId: string): boolean => {
233
+ const parsed = parseOpenAIModel(bareModelId(modelId));
234
+ if (!parsed) return false;
235
+ return semverGte(parsed.version, "5.4");
236
+ });
237
+
238
+ /**
239
+ * OpenAI Codex models that honor `reasoning.context: "all_turns"` (full
240
+ * cross-turn reasoning replay). The `reasoning.context` field itself exists for
241
+ * the whole gpt-5/o-series family, but the `all_turns` value is only accepted
242
+ * from gpt-5.4 onward; earlier ids (`gpt-5.1-codex`, `gpt-5.3-codex`, and
243
+ * `gpt-5.3-codex-spark`) reject it with
244
+ * `Unsupported value: 'all_turns' is not supported with this model`. Version
245
+ * floor (not an allowlist) so 5.6/6.x inherit support automatically. Callers
246
+ * fall back to omitting `context`, letting the server default to `current_turn`.
247
+ */
248
+ export const supportsAllTurnsReasoningContext = isOpenAIWireGen54Plus;
249
+
250
+ /**
251
+ * OpenAI Codex models that accept `reasoning.summary`. Shares the gpt-5.4 wire
252
+ * floor with {@link supportsAllTurnsReasoningContext}: earlier Codex ids
253
+ * (`gpt-5.1-codex`, `gpt-5.3-codex`, `gpt-5.3-codex-spark`) reject the field
254
+ * with `Unsupported parameter: 'reasoning.summary' is not supported with this
255
+ * model`. Callers omit `summary` for unsupported ids, letting the server skip
256
+ * the human-readable summary stream.
257
+ */
258
+ export const supportsCodexReasoningSummary = isOpenAIWireGen54Plus;
259
+
260
+ /** OpenAI proprietary reasoning families keyed off the parsed gpt version (gpt-5+). */
261
+ const isOpenAIWireGen5Plus = memo((modelId: string): boolean => {
262
+ const parsed = parseOpenAIModel(bareModelId(modelId));
263
+ if (!parsed) return false;
264
+ return semverGte(parsed.version, "5");
265
+ });
266
+
267
+ /** o-series reasoning ids (`o1`, `o1-pro`, `o3`, `o3-mini`, `o4-mini`, `openai/o3`, …). */
268
+ const O_SERIES_REASONING_RE = /(^|\/)o[134](?:[-.]|$)/i;
269
+
270
+ /**
271
+ * OpenAI proprietary models whose serving path rejects explicit sampling
272
+ * parameters (`temperature`, `top_p`, `top_k`, …) with
273
+ * `400 Unsupported parameter: 'temperature' is not supported with this model`.
274
+ * Covers the o-series and the entire gpt-5+ generation — base, `mini`, `nano`,
275
+ * `codex*`, the `luna`/`sol`/`terra` SKUs, and the `-chat-latest` variants,
276
+ * since even the non-reasoning gpt-5 chat models reject sampling params (see
277
+ * litellm#13781). Holds regardless of which OpenAI-serving host proxies the
278
+ * model (official, Azure, GitHub Copilot). Version floor (not an allowlist) so
279
+ * 6.x inherits automatically. Issue #5606.
280
+ */
281
+ export const isOpenAISamplingRestrictedModelId = memo((modelId: string): boolean => {
282
+ const bare = bareModelId(modelId);
283
+ return isOpenAIWireGen5Plus(modelId) || O_SERIES_REASONING_RE.test(bare);
284
+ });
285
+
286
+ /**
287
+ * Reasoning-capable GLM coding SKUs: glm-4.5 and up on the base / `-air` /
288
+ * `-turbo` lines. Excludes the vision (`…v`) shape, the non-reasoning
289
+ * `-flash`/`-flashx`/`-preview` variants, and pre-4.5 ids. Matching the family
290
+ * keeps newly-bumped integers (`glm-5.3`, `glm-6`, …) covered without a per-id
291
+ * allowlist.
292
+ */
293
+ export const isReasoningGlmModelId = memo((modelId: string): boolean => {
294
+ const glm = parseGlmModel(bareModelId(modelId));
295
+ if (!glm || glm.vision) {
296
+ return false;
297
+ }
298
+ if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
299
+ return false;
300
+ }
301
+ return semverGte(glm.version, "4.5");
302
+ });
303
+
304
+ /** GLM-5.2+ coding SKUs accept `reasoning_effort` in addition to binary thinking. */
305
+ export const isGlm52ReasoningEffortModelId = memo((modelId: string): boolean => {
306
+ const glm = parseGlmModel(bareModelId(modelId));
307
+ if (!glm || glm.vision) {
308
+ return false;
309
+ }
310
+ if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
311
+ return false;
312
+ }
313
+ return semverGte(glm.version, "5.2");
314
+ });
315
+
316
+ /**
317
+ * GLM-5.3+ coding SKUs. Unlike GLM-5.2 (whose reasoning_effort dialect is
318
+ * host-specific), GLM-5.3+ exposes a uniform wire-exact `low`/`high`/`max`
319
+ * ladder on every host, and thinking can no longer be disabled —
320
+ * `thinking.type` must always be `enabled`. Matching the family keeps future
321
+ * bumps (`glm-5.4`, `glm-6`, …) covered while excluding the vision (`…v`)
322
+ * shape and the non-reasoning `-flash`/`-flashx`/`-preview` variants.
323
+ */
324
+ export const isGlm53ReasoningEffortModelId = memo((modelId: string): boolean => {
325
+ const glm = parseGlmModel(bareModelId(modelId));
326
+ if (!glm || glm.vision) {
327
+ return false;
328
+ }
329
+ if (glm.variant !== "base" && glm.variant !== "air" && glm.variant !== "turbo") {
330
+ return false;
331
+ }
332
+ return semverGte(glm.version, "5.3");
333
+ });
334
+
335
+ /** GLM vision SKUs — the `v` that attaches to the version (`glm-4v`, `glm-4.5v`). */
336
+ export const isGlmVisionModelId = memo((modelId: string): boolean => {
337
+ return parseGlmModel(bareModelId(modelId))?.vision === true;
338
+ });
339
+
340
+ /**
341
+ * Coarse vendor-lineage token for "are two models the same family?" checks
342
+ * (e.g. picking a cross-family reviewer). All Claude point releases share a token,
343
+ * Claude and GPT differ; namespace prefixes and aggregator mirrors fold onto the
344
+ * lineage via {@link parseKnownModel}'s `bareModelId` normalization. Opaque and
345
+ * comparison-only — not a stable key to persist, since the vocabulary tracks new
346
+ * releases. Returns `""` for ids it cannot classify; callers fall back to the provider.
347
+ *
348
+ * Vendor-only by design: a model's kind/variant (opus vs sonnet, codex vs base) is
349
+ * collapsed onto the single vendor token; use {@link parseKnownModel} for finer breakdowns.
350
+ */
351
+ export const modelFamilyToken = memo((modelId: string): string => {
352
+ const parsed = parseKnownModel(modelId);
353
+ if (parsed.family !== "unknown") return parsed.family;
354
+ if (isClaudeModelId(modelId) || isAnthropicNamespacedModelId(modelId)) return "anthropic";
355
+ if (isGeminiModelId(modelId)) return "gemini";
356
+ if (isGrokModelId(modelId)) return "grok";
357
+ if (isDeepseekModelIdOrName(modelId)) return "deepseek";
358
+ if (isOpenAIModelId(modelId)) return "openai";
359
+ if (isKimiModelId(modelId)) return "kimi";
360
+ if (isQwenModelId(modelId)) return "qwen";
361
+ if (isMinimaxM2FamilyModelId(modelId) || isMinimaxM3FamilyModelId(modelId)) return "minimax";
362
+ if (isOpenAIGptOssModelId(modelId)) return "gpt-oss";
363
+ if (isMimoModelIdOrName(modelId)) return "mimo";
364
+ if (isGemmaModelId(modelId)) return "gemma";
365
+ if (parseGlmModel(bareModelId(modelId))) return "glm";
366
+ return "";
367
+ });
368
+
369
+ /**
370
+ * True for Claude generations that support extended thinking: Sonnet/Opus 3.7+,
371
+ * every 4.x/5+ Opus/Sonnet, and the Fable/Mythos generation. Pre-thinking
372
+ * models (Claude 3.5 and older) are excluded so no thinking effort dial is
373
+ * fabricated for a model that rejects thinking parameters. Classifier-based, so
374
+ * dotted and dashed version forms both match; ids the classifier does not parse
375
+ * (e.g. Haiku, bare dated ids) return false.
376
+ */
377
+ export const anthropicModelSupportsThinking = memo((modelId: string): boolean => {
378
+ const parsed = parseAnthropicModel(bareModelId(modelId));
379
+ return parsed !== null && semverGte(parsed.version, "3.7");
380
+ });
381
+
382
+ /**
383
+ * Adaptive thinking `display` is supported starting with Claude Opus 4.7+,
384
+ * Sonnet 5+, and the Claude Fable/Mythos 5 generation. Older adaptive-thinking
385
+ * models (Opus 4.6, Sonnet 4.6) reject the field. Classifier-based, so dotted
386
+ * and dashed version forms both match while bare dated ids
387
+ * (`claude-opus-4-20250514` = Opus 4.0) stay excluded.
388
+ */
389
+ export const supportsAdaptiveThinkingDisplay = memo((modelId: string): boolean => {
390
+ const parsed = parseAnthropicModel(bareModelId(modelId));
391
+ return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
392
+ });
393
+
394
+ /**
395
+ * Returns true for Anthropic models with Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+
396
+ * API restrictions:
397
+ * - Sampling parameters (temperature/top_p/top_k) return 400 error
398
+ * - Thinking content is omitted by default (needs display: "summarized")
399
+ */
400
+ export const hasOpus47ApiRestrictions = memo((modelId: string): boolean => {
401
+ const parsed = parseAnthropicModel(bareModelId(modelId));
402
+ return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.7");
403
+ });
404
+
405
+ /**
406
+ * Mid-conversation `role: "system"` messages (system instructions appended at
407
+ * non-first positions in the `messages` array) are supported starting with
408
+ * Claude Opus 4.8+, Sonnet 5+, and the Claude Fable/Mythos 5 generation.
409
+ * Earlier Claude models reject the role.
410
+ * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
411
+ */
412
+ export const supportsMidConversationSystemMessages = memo((modelId: string): boolean => {
413
+ const parsed = parseAnthropicModel(bareModelId(modelId));
414
+ return parsed !== null && isAnthropicAdaptiveGenAtLeast(parsed, "4.8");
415
+ });
416
+
417
+ /**
418
+ * Models that reliably follow the hashline line-anchored edit dialect
419
+ * (`[path#TAG]` headers plus 1-indexed anchors). Kimi, MiMo, DeepSeek V4
420
+ * Flash, and Step 3.7 Flash miscount anchors or drop the tag header often
421
+ * enough that hosts fall back to the sloppy edit format for
422
+ * them.
423
+ */
424
+ export const supportsHashlineEdits = memo((modelId: string): boolean => {
425
+ return !(
426
+ isKimiModelId(modelId) ||
427
+ isMimoModelIdOrName(modelId) ||
428
+ isDeepseekV4FlashModelId(modelId) ||
429
+ isStep37FlashModelId(modelId)
430
+ );
431
+ });
432
+
433
+ export const isAnthropicFableOrMythosModel = memo((modelId: string): boolean => {
434
+ const parsed = parseAnthropicModel(bareModelId(modelId));
435
+ return parsed !== null && isFableOrMythos(parsed.kind);
436
+ });
437
+
438
+ /** Thinking-variant token location inside a model id. */
439
+ export interface ThinkingVariantToken {
440
+ index: number;
441
+ length: number;
442
+ }
443
+
444
+ const THINKING_VARIANT_TOKEN_RE = /-(?:thinking|reasoner|reasoning)(?=$|[^a-z0-9])/gi;
445
+
446
+ /**
447
+ * Locates the first thinking-variant token (`-thinking`, `-reasoner`,
448
+ * `-reasoning`; trailing or infix) in a model id. The token ends at the id
449
+ * end or any non-alphanumeric boundary, and negated forms (`non-thinking`,
450
+ * `no-thinking`) never match — those name the NON-thinking SKU.
451
+ */
452
+ export function findThinkingVariantToken(modelId: string): ThinkingVariantToken | undefined {
453
+ THINKING_VARIANT_TOKEN_RE.lastIndex = 0;
454
+ let match = THINKING_VARIANT_TOKEN_RE.exec(modelId);
455
+ while (match !== null) {
456
+ const preceding = /([a-z0-9]+)$/i.exec(modelId.slice(0, match.index))?.[1]?.toLowerCase();
457
+ if (preceding !== "non" && preceding !== "no") {
458
+ return { index: match.index, length: match[0].length };
459
+ }
460
+ match = THINKING_VARIANT_TOKEN_RE.exec(modelId);
461
+ }
462
+ return undefined;
463
+ }
464
+
465
+ /**
466
+ * Removes the located thinking-variant token: `kimi-k2-thinking` → `kimi-k2`,
467
+ * `mimo-v2-flash-thinking-original` → `mimo-v2-flash-original`,
468
+ * `grok-4.1-fast-reasoning` → `grok-4.1-fast`. Returns `undefined` when no
469
+ * token exists or nothing would remain. Callers MUST verify the result names
470
+ * a live model.
471
+ */
472
+ export const stripThinkingVariantToken = memo((modelId: string): string | undefined => {
473
+ const token = findThinkingVariantToken(modelId);
474
+ if (!token) return undefined;
475
+ const stripped = modelId.slice(0, token.index) + modelId.slice(token.index + token.length);
476
+ return stripped.length > 0 ? stripped : undefined;
477
+ });
@@ -0,0 +1,81 @@
1
+ const LEADING_BRACKETED_AFFIX_PATTERN = /^(?:\s*(?:\[|【)[^\]】]+(?:\]|】)\s*)+/u;
2
+ const TRAILING_BRACKETED_AFFIX_PATTERN = /(?:\s*(?:\[|【)[^\]】]+(?:\]|】)\s*)+$/u;
3
+ const MODEL_ID_SEGMENT_PATTERN = /[a-z0-9.:-]+/g;
4
+ const MODEL_FAMILY_PREFIX_PATTERN =
5
+ /^(claude|gemini|gpt|grok|glm|qwen|deepseek|kimi|mimo|doubao|ernie|gpt-oss|gemma|minimax|step|command|jamba|llama|o[1345])/i;
6
+
7
+ function normalizeModelIdWhitespace(value: string): string {
8
+ return value.trim().replace(/\s+/g, " ");
9
+ }
10
+
11
+ /** Ordering for model-like segments: longest first, ties broken lexicographically. */
12
+ function compareSegmentPreference(left: string, right: string): number {
13
+ return left.length !== right.length ? right.length - left.length : left.localeCompare(right);
14
+ }
15
+
16
+ export function getModelLikeIdSegments(modelId: string): string[] {
17
+ const matches = normalizeModelIdWhitespace(modelId).toLowerCase().match(MODEL_ID_SEGMENT_PATTERN);
18
+ if (!matches) return [];
19
+ const segments = new Set<string>();
20
+ for (const segment of matches) {
21
+ if (MODEL_FAMILY_PREFIX_PATTERN.test(segment) && /\d/.test(segment)) segments.add(segment);
22
+ }
23
+ return [...segments].sort(compareSegmentPreference);
24
+ }
25
+
26
+ export function getLongestModelLikeIdSegment(modelId: string): string | undefined {
27
+ const matches = normalizeModelIdWhitespace(modelId).toLowerCase().match(MODEL_ID_SEGMENT_PATTERN);
28
+ if (!matches) return undefined;
29
+ let best: string | undefined;
30
+ for (const segment of matches) {
31
+ if (
32
+ MODEL_FAMILY_PREFIX_PATTERN.test(segment) &&
33
+ /\d/.test(segment) &&
34
+ (best === undefined || compareSegmentPreference(segment, best) < 0)
35
+ ) {
36
+ best = segment;
37
+ }
38
+ }
39
+ return best;
40
+ }
41
+
42
+ function hasBracketAffixMarker(value: string): boolean {
43
+ for (let index = 0; index < value.length; index++) {
44
+ const code = value.charCodeAt(index);
45
+ if (code === 91 || code === 93 || code === 0x3010 || code === 0x3011) {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+
52
+ /**
53
+ * Strip reseller / wrapper tags that are injected as bracketed affixes around an
54
+ * upstream model id, e.g.
55
+ * "[Kiro] claude-opus-4-8" -> "claude-opus-4-8"
56
+ * "[gcli转] gemini-3.1-pro-preview [假流]" -> "gemini-3.1-pro-preview"
57
+ *
58
+ * Candidates are returned most-stripped first: both ends, then leading-only, then trailing-only.
59
+ */
60
+ export function getBracketStrippedModelIdCandidates(modelId: string): string[] {
61
+ if (!hasBracketAffixMarker(modelId)) return [];
62
+ const normalized = normalizeModelIdWhitespace(modelId);
63
+ if (!normalized) return [];
64
+
65
+ const strippedLeading = normalized.replace(LEADING_BRACKETED_AFFIX_PATTERN, "");
66
+ const withoutLeading = normalizeModelIdWhitespace(strippedLeading);
67
+ const withoutTrailing = normalizeModelIdWhitespace(normalized.replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""));
68
+ const withoutBoth = normalizeModelIdWhitespace(strippedLeading.replace(TRAILING_BRACKETED_AFFIX_PATTERN, ""));
69
+
70
+ const candidates = new Set<string>();
71
+ for (const candidate of [withoutBoth, withoutLeading, withoutTrailing]) {
72
+ if (candidate && candidate !== normalized) {
73
+ candidates.add(candidate);
74
+ }
75
+ }
76
+ return [...candidates];
77
+ }
78
+
79
+ export function stripBracketedModelIdAffixes(modelId: string): string | undefined {
80
+ return getBracketStrippedModelIdCandidates(modelId)[0];
81
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./bundled";
2
+ export * from "./classify";
3
+ export * from "./dialect";
4
+ export * from "./family";
5
+ export * from "./id";
6
+ export * from "./markers";
7
+ export * from "./priority";
8
+ export * from "./reference";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Trailing-marker vocabulary shared by canonical-id resolution and
3
+ * proxy-reference lookup. A "marker" is a routing/quantization/effort suffix
4
+ * a reseller or aggregator appends to an upstream model id
5
+ * (`-thinking`, `:nitro`, `-fp8`, …) that does not change model identity.
6
+ */
7
+ const TRAILING_MARKERS = [
8
+ "thinking",
9
+ "customtools",
10
+ "high",
11
+ "low",
12
+ "medium",
13
+ "minimal",
14
+ "xhigh",
15
+ "free",
16
+ "cloud",
17
+ "exacto",
18
+ "nitro",
19
+ "original",
20
+ "optimized",
21
+ "nvfp4",
22
+ "fp8",
23
+ "fp4",
24
+ "bf16",
25
+ "int8",
26
+ "int4",
27
+ ] as const;
28
+
29
+ /**
30
+ * Markers treated as identity-preserving ONLY when recovering bundled metadata
31
+ * for a proxied model id, never during canonical-id coalescing: Perplexity's
32
+ * `sonar-pro-search` is a distinct model from `sonar-pro`, so canonical
33
+ * resolution must not strip `search`, while a proxy id like
34
+ * `claude-opus-4-6-search` should still inherit the upstream pricing/limits.
35
+ */
36
+ const REFERENCE_ONLY_TRAILING_MARKERS = ["search"] as const;
37
+
38
+ function buildTrailingMarkerPattern(markers: readonly string[]): RegExp {
39
+ return new RegExp(`[-:](?:${markers.join("|")})$`, "i");
40
+ }
41
+
42
+ /** Marker pattern used by canonical-id resolution (`search` excluded). */
43
+ export const CANONICAL_TRAILING_MARKER_PATTERN = buildTrailingMarkerPattern(TRAILING_MARKERS);
44
+
45
+ /** Marker pattern used by proxy-reference lookup (`search` included). */
46
+ export const REFERENCE_TRAILING_MARKER_PATTERN = buildTrailingMarkerPattern([
47
+ ...TRAILING_MARKERS,
48
+ ...REFERENCE_ONLY_TRAILING_MARKERS,
49
+ ]);
@@ -0,0 +1,59 @@
1
+ const DEFAULT_MODEL_PROVIDER_ORDER = [
2
+ // First-party / native account providers. Prefer these over relays when the
3
+ // same upstream model is available in more than one place.
4
+ "openai-codex",
5
+ "anthropic",
6
+ "openai",
7
+ "google-gemini-cli",
8
+ "google",
9
+ "google-vertex",
10
+ "kimi-code",
11
+ "moonshot",
12
+ "qwen-portal",
13
+ "alibaba-token-plan",
14
+ "zai",
15
+ "xai-oauth",
16
+ "xai",
17
+ "mistral",
18
+ "deepseek",
19
+ "groq",
20
+
21
+ // High-quality aggregators / hosted inference providers.
22
+ "fireworks",
23
+ "cerebras",
24
+ "baseten",
25
+ "openrouter",
26
+ "aimlapi",
27
+ "together",
28
+
29
+ // Generic gateways and editor/proxy providers. These are useful when picked
30
+ // explicitly, but should not win ambiguous automatic role selection.
31
+ "alibaba-coding-plan",
32
+ "umans",
33
+ "google-antigravity",
34
+ "opencode-zen",
35
+ "gitlab-duo",
36
+ "opencode-go",
37
+ "kilo",
38
+ "vercel-ai-gateway",
39
+ "cloudflare-ai-gateway",
40
+ "nanogpt",
41
+ "github-copilot",
42
+ ] as const;
43
+
44
+ function addProviderRank(rank: Map<string, number>, provider: string): void {
45
+ const normalized = provider.trim().toLowerCase();
46
+ if (!normalized || rank.has(normalized)) return;
47
+ rank.set(normalized, rank.size);
48
+ }
49
+
50
+ export function buildModelProviderPriorityRank(configuredProviderOrder?: readonly string[]): Map<string, number> {
51
+ const rank = new Map<string, number>();
52
+ for (const provider of configuredProviderOrder ?? []) {
53
+ addProviderRank(rank, provider);
54
+ }
55
+ for (const provider of DEFAULT_MODEL_PROVIDER_ORDER) {
56
+ addProviderRank(rank, provider);
57
+ }
58
+ return rank;
59
+ }