@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,925 @@
1
+ /**
2
+ * Thinking metadata: build-time derivation and runtime field-read helpers.
3
+ *
4
+ * Derivation (`resolveModelThinking`) runs exactly once per model — from
5
+ * `buildModel` for dynamic specs and from the catalog generator for bundled
6
+ * entries. Everything below the "runtime helpers" divider reads baked fields
7
+ * only: no id parsing, no host matching, no compat detection per request.
8
+ */
9
+ import { Effort, THINKING_EFFORTS } from "./effort";
10
+ import { modelMatchesHost } from "./hosts";
11
+ import {
12
+ type AnthropicModel,
13
+ bareModelId,
14
+ type GeminiModel,
15
+ isAnthropicAdaptiveGenAtLeast,
16
+ type OpenAIModel,
17
+ type ParsedModel,
18
+ parseAnthropicModel,
19
+ parseKnownModel,
20
+ parseOpenAIModel,
21
+ semverEqual,
22
+ semverGte,
23
+ } from "./identity/classify";
24
+ import {
25
+ findThinkingVariantToken,
26
+ isDeepseekModelIdOrName,
27
+ isDeepseekV4FlashModelId,
28
+ isGlm52ReasoningEffortModelId,
29
+ isGlm53ReasoningEffortModelId,
30
+ isGrokXHighEffortCapable,
31
+ isKimiK3ModelId,
32
+ isMimoModelIdOrName,
33
+ isMinimaxM2FamilyModelId,
34
+ isMinimaxM3FamilyModelId,
35
+ isOpenAIGptOssModelId,
36
+ isQwenModelId,
37
+ supportsAdaptiveThinkingDisplay,
38
+ } from "./identity/family";
39
+ import type {
40
+ Api,
41
+ CompatOf,
42
+ Model,
43
+ ModelSpec,
44
+ ResolvedDevinCompat,
45
+ ResolvedOpenAICompat,
46
+ ResolvedOpenAIResponsesCompat,
47
+ ThinkingConfig,
48
+ } from "./types";
49
+
50
+ /**
51
+ * Runtime helpers read baked metadata only, so they accept both pre-build
52
+ * specs and built models.
53
+ */
54
+ type ApiModel<TApi extends Api = Api> = ModelSpec<TApi> | Model<TApi>;
55
+
56
+ const DEFAULT_REASONING_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
57
+ const DEFAULT_REASONING_EFFORTS_WITH_XHIGH: readonly Effort[] = [
58
+ Effort.Minimal,
59
+ Effort.Low,
60
+ Effort.Medium,
61
+ Effort.High,
62
+ Effort.XHigh,
63
+ ];
64
+ const GEMINI_3_PRO_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High];
65
+ const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
66
+ const GPT_5_2_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
67
+ const GPT_5_1_CODEX_MINI_EFFORTS: readonly Effort[] = [Effort.Medium, Effort.High];
68
+ const LOW_MEDIUM_HIGH_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
69
+ /** Wire-exact `low`/`high`/`max` scale used by Kimi K3 and DeepSeek V4 (Flash and Pro, direct API and aggregators). */
70
+ const LOW_HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High, Effort.Max];
71
+ /** Wire-exact two-tier scale (`high`/`max`): GLM-5.2 on Z.ai/Umans/Ollama Cloud/Baseten, Sakana Fugu, older DeepSeek reasoners (V3.x/R1). */
72
+ const HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.High, Effort.Max];
73
+ /** OpenRouter's DeepSeek route accepts only `high`. */
74
+ const HIGH_ONLY_REASONING_EFFORTS: readonly Effort[] = [Effort.High];
75
+ /**
76
+ * Qwen 3.8+ open-weight chat template: prompt-steered `reasoning_effort`
77
+ * kwarg with exactly three wire tiers (template default is `xhigh`).
78
+ */
79
+ const QWEN38_TEMPLATE_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.XHigh];
80
+ /**
81
+ * Five wire tiers with a `low` floor: GPT-5.6+, Anthropic adaptive models
82
+ * with the real xhigh tier (Opus 4.7+, Sonnet 5+, Fable/Mythos 5), and the
83
+ * Fire Pass Kimi router (distinct xhigh and max budgets).
84
+ */
85
+ const FIVE_TIER_EFFORTS_LOW_TO_MAX: readonly Effort[] = [
86
+ Effort.Low,
87
+ Effort.Medium,
88
+ Effort.High,
89
+ Effort.XHigh,
90
+ Effort.Max,
91
+ ];
92
+ /** Legacy adaptive scale (Opus/Sonnet 4.6, every Bedrock adaptive model): four wire tiers, no xhigh. */
93
+ const FOUR_TIER_EFFORTS_LOW_TO_MAX: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.Max];
94
+ /** GLM-5.2 resellers that pass the default lower tiers verbatim and expose the genuine `max` top tier. */
95
+ const DEFAULT_REASONING_EFFORTS_WITH_MAX: readonly Effort[] = [
96
+ Effort.Minimal,
97
+ Effort.Low,
98
+ Effort.Medium,
99
+ Effort.High,
100
+ Effort.Max,
101
+ ];
102
+ /** Local Ollama wire vocabulary (`low`/`medium`/`high`/`max`; `none` is thinking-off). */
103
+ const OLLAMA_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.Max];
104
+ type EffortMap = Partial<Record<Effort, string>>;
105
+
106
+ const GROQ_QWEN3_32B_REASONING_EFFORT_MAP: Readonly<EffortMap> = {
107
+ [Effort.Minimal]: "default",
108
+ [Effort.Low]: "default",
109
+ [Effort.Medium]: "default",
110
+ [Effort.High]: "default",
111
+ [Effort.XHigh]: "default",
112
+ };
113
+ const FIREWORKS_REASONING_EFFORT_MAP: Readonly<EffortMap> = {
114
+ [Effort.Minimal]: "none",
115
+ };
116
+ const MIMO_REASONING_EFFORT_MAP: Readonly<EffortMap> = {
117
+ [Effort.Minimal]: "low",
118
+ [Effort.XHigh]: "high",
119
+ };
120
+
121
+ const MINIMAX_ANTHROPIC_ADAPTIVE_EFFORT_MAP: Readonly<EffortMap> = {
122
+ [Effort.Low]: "adaptive",
123
+ [Effort.Medium]: "adaptive",
124
+ [Effort.High]: "adaptive",
125
+ };
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Build-time derivation (buildModel + catalog generator only)
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /**
132
+ * Resolve the canonical thinking metadata for a spec. Called exactly once per
133
+ * model by `buildModel`, after compat resolution.
134
+ *
135
+ * - Non-reasoning models never carry thinking.
136
+ * - Models that reason natively but reject the wire effort param
137
+ * (`compat.supportsReasoningEffort: false` on openai-responses*) carry no
138
+ * thinking either: `reasoning: true, thinking: undefined` IS the encoding
139
+ * for "thinks, but exposes no control surface".
140
+ * - Explicit spec thinking (generator-baked or user-authored) owns the
141
+ * capability surface (`mode`, `efforts`, `defaultLevel`); the wire facts
142
+ * (`effortMap`, `supportsDisplay`) are backfilled from identity when not
143
+ * explicitly set, so configs never need to know provider wire tier tables.
144
+ * - Sparse specs go through full inference.
145
+ */
146
+ export function resolveModelThinking<TApi extends Api>(
147
+ spec: ModelSpec<TApi>,
148
+ compat: CompatOf<TApi>,
149
+ ): ThinkingConfig | undefined {
150
+ if (!spec.reasoning) return undefined;
151
+ if (omitsWireReasoningEffort(spec.api, compat)) return undefined;
152
+ if (spec.thinking && Array.isArray(spec.thinking.efforts) && spec.thinking.efforts.length > 0) {
153
+ return fillThinkingWireDefaults(spec, compat, spec.thinking);
154
+ }
155
+ // Cascade selects effort only by routing to a sibling model id, so a Devin
156
+ // model with no explicit routed thinking has no controllable surface —
157
+ // never fabricate an effort ladder from identity.
158
+ if ((compat as ResolvedDevinCompat | undefined)?.trustExplicitThinkingOnly === true) return undefined;
159
+ // Empty/malformed explicit metadata is treated as absent — infer instead.
160
+ return deriveThinking(spec, compat);
161
+ }
162
+
163
+ /**
164
+ * Backfill identity-derived wire facts onto explicit thinking metadata.
165
+ * Explicit `effortMap` / `supportsDisplay` (including `false`) win, except
166
+ * when the model-defined effort ladder disagrees with the cached surface:
167
+ * then both the ladder AND the wire map are re-derived from identity, so
168
+ * stale cached metadata from before a wire-truth change (e.g. the retired
169
+ * shifted five-tier maps) cannot survive normalization.
170
+ */
171
+ function fillThinkingWireDefaults<TApi extends Api>(
172
+ spec: ModelSpec<TApi>,
173
+ compat: CompatOf<TApi>,
174
+ thinking: ThinkingConfig,
175
+ ): ThinkingConfig {
176
+ const parsed = parseKnownModel(spec.id);
177
+ const normalizedEfforts = getModelDefinedEfforts(spec, compat) ?? thinking.efforts;
178
+ const effortsChanged = !sameEffortList(normalizedEfforts, thinking.efforts);
179
+ const effortMap =
180
+ thinking.effortMap === undefined || effortsChanged
181
+ ? inferEffortMap(spec, compat, thinking.mode, normalizedEfforts)
182
+ : undefined;
183
+ const shouldReplaceEffortMap = thinking.effortMap === undefined ? effortMap !== undefined : effortsChanged;
184
+ const needsDisplay =
185
+ thinking.supportsDisplay === undefined &&
186
+ (spec.api === "anthropic-messages" || spec.api === "bedrock-converse-stream") &&
187
+ supportsAdaptiveThinkingDisplay(spec.id);
188
+ const needsRequiresEffort =
189
+ thinking.requiresEffort === undefined &&
190
+ (impliesMandatoryReasoning(parsed, spec.id) ||
191
+ isQwenTemplateReasoningEffortCompat(compat) ||
192
+ isOpenCodeGatewayOxAlphaModel(spec));
193
+ const needsDefaultLevel =
194
+ thinking.defaultLevel === undefined && (isKimiK3ModelId(spec.id) || isGlm53ReasoningEffortModelId(spec.id));
195
+ if (!effortsChanged && !shouldReplaceEffortMap && !needsDisplay && !needsRequiresEffort && !needsDefaultLevel) {
196
+ return thinking;
197
+ }
198
+ const filled: ThinkingConfig = { ...thinking };
199
+ if (effortsChanged) {
200
+ filled.efforts = normalizedEfforts;
201
+ }
202
+ if (shouldReplaceEffortMap) {
203
+ if (effortMap === undefined) {
204
+ delete filled.effortMap;
205
+ } else {
206
+ filled.effortMap = effortMap;
207
+ }
208
+ }
209
+ if (needsDisplay) {
210
+ filled.supportsDisplay = true;
211
+ }
212
+ if (needsDefaultLevel) {
213
+ filled.defaultLevel = Effort.Max;
214
+ }
215
+ if (needsRequiresEffort) {
216
+ filled.requiresEffort = true;
217
+ }
218
+ return filled;
219
+ }
220
+
221
+ /** Derive thinking from identity + resolved compat, ignoring any baked value. Generator-side entry. */
222
+ export function deriveThinking<TApi extends Api>(spec: ModelSpec<TApi>, compat: CompatOf<TApi>): ThinkingConfig {
223
+ const parsed = parseKnownModel(spec.id);
224
+ const efforts = inferSupportedEfforts(parsed, spec, compat);
225
+ if (efforts.length === 0) {
226
+ throw new Error(`Model ${spec.provider}/${spec.id} resolved to an empty thinking range`);
227
+ }
228
+ const config: ThinkingConfig = {
229
+ mode: inferThinkingControlMode(spec, parsed),
230
+ efforts,
231
+ };
232
+ if (isKimiK3ModelId(spec.id) || isGlm53ReasoningEffortModelId(spec.id)) {
233
+ config.defaultLevel = Effort.Max;
234
+ }
235
+ const effortMap = inferEffortMap(spec, compat, config.mode, config.efforts);
236
+ if (effortMap !== undefined) {
237
+ config.effortMap = effortMap;
238
+ }
239
+ if (
240
+ (spec.api === "anthropic-messages" || spec.api === "bedrock-converse-stream") &&
241
+ supportsAdaptiveThinkingDisplay(spec.id)
242
+ ) {
243
+ config.supportsDisplay = true;
244
+ }
245
+ if (
246
+ impliesMandatoryReasoning(parsed, spec.id) ||
247
+ isQwenTemplateReasoningEffortCompat(compat) ||
248
+ isOpenCodeGatewayOxAlphaModel(spec)
249
+ ) {
250
+ config.requiresEffort = true;
251
+ }
252
+ return config;
253
+ }
254
+
255
+ /**
256
+ * True when the model reasons natively but rejects the wire `reasoning.effort`
257
+ * param. Scoped to openai-responses* because that's the only API surface where
258
+ * `compat.supportsReasoningEffort: false` means "omit the field entirely"
259
+ * (xAI Grok off the `isGrokReasoningEffortCapable` allowlist: grok-build,
260
+ * grok-4.20-0309-reasoning). openai-completions keeps its thinking config even
261
+ * without effort support — binary thinking formats (zai/qwen) drive reasoning
262
+ * through other request fields.
263
+ */
264
+ function omitsWireReasoningEffort(api: Api, compat: CompatOf<Api>): boolean {
265
+ if (api !== "openai-responses" && api !== "openai-codex-responses" && api !== "azure-openai-responses") {
266
+ return false;
267
+ }
268
+ return (compat as ResolvedOpenAIResponsesCompat | undefined)?.supportsReasoningEffort === false;
269
+ }
270
+
271
+ function inferEffortMap<TApi extends Api>(
272
+ spec: ModelSpec<TApi>,
273
+ compat: CompatOf<TApi>,
274
+ mode: ThinkingConfig["mode"],
275
+ efforts: readonly Effort[],
276
+ ): EffortMap | undefined {
277
+ const detected = inferDetectedEffortMap(spec, compat, mode);
278
+ const configured = readCompatEffortMap(compat);
279
+ const merged =
280
+ detected === undefined ? configured : configured === undefined ? detected : { ...detected, ...configured };
281
+ return merged === undefined ? undefined : filterEffortMapToSupportedEfforts(merged, efforts);
282
+ }
283
+
284
+ function filterEffortMapToSupportedEfforts(map: EffortMap, efforts: readonly Effort[]): EffortMap | undefined {
285
+ let filtered: EffortMap | undefined;
286
+ for (const effort of efforts) {
287
+ const mapped = map[effort];
288
+ if (mapped === undefined) continue;
289
+ if (filtered === undefined) filtered = {};
290
+ filtered[effort] = mapped;
291
+ }
292
+ return filtered;
293
+ }
294
+
295
+ function sameEffortList(left: readonly Effort[], right: readonly Effort[]): boolean {
296
+ if (left.length !== right.length) return false;
297
+ for (let index = 0; index < left.length; index++) {
298
+ if (left[index] !== right[index]) return false;
299
+ }
300
+ return true;
301
+ }
302
+
303
+ function isOpenAICompatReasoningApi(api: Api): boolean {
304
+ return api === "openai-completions" || api === "openrouter";
305
+ }
306
+
307
+ /**
308
+ * GPT-5.6+ addressed through a wire `reasoning.effort`/`reasoning_effort`
309
+ * field, where the five-tier `low..max` wire scale applies. Devin
310
+ * (`devin-agent`) selects effort by routing to per-tier sibling model ids
311
+ * instead and must stay unmapped.
312
+ */
313
+ function isGpt56PlusWireEffortModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
314
+ switch (spec.api) {
315
+ case "openai-responses":
316
+ case "openai-codex-responses":
317
+ case "azure-openai-responses":
318
+ case "openai-completions":
319
+ case "openrouter":
320
+ break;
321
+ default:
322
+ return false;
323
+ }
324
+ const parsed = parseOpenAIModel(bareModelId(spec.id));
325
+ return parsed !== null && semverGte(parsed.version, "5.6");
326
+ }
327
+
328
+ function getModelDefinedEfforts<TApi extends Api>(
329
+ spec: ModelSpec<TApi>,
330
+ compat: CompatOf<TApi>,
331
+ ): readonly Effort[] | undefined {
332
+ if (isGlm53ReasoningEffortModelId(spec.id)) {
333
+ // GLM-5.3+ exposes a uniform wire-exact low/high/max ladder on every
334
+ // host — unlike GLM-5.2, whose reasoning_effort dialect is
335
+ // host-specific. Thinking can no longer be disabled (handled by
336
+ // impliesMandatoryReasoning), and the default effort is `max`.
337
+ return LOW_HIGH_MAX_REASONING_EFFORTS;
338
+ }
339
+ if (isGlm52ReasoningEffortModelId(spec.id)) {
340
+ // GLM-5.2's reasoning_effort dialect is host-specific (verified against
341
+ // live endpoints):
342
+ // - Z.ai/Zhipu ("zai" dialect) expose only high/max ("none" is the
343
+ // thinking-off state, not a user tier).
344
+ // - Umans, Ollama Cloud, and Baseten serve the same two-tier
345
+ // high/max scale on their GLM-5.2 routes.
346
+ // - OpenRouter rejects `max` — `xhigh` IS its top tier.
347
+ // - Other openai-compat hosts (Fireworks, resellers) pass the
348
+ // default lower tiers through verbatim and expose the genuine
349
+ // `max` above `high` (host quirks like Fireworks' minimal→none
350
+ // stay in the host maps).
351
+ if (isOpenRouterThinkingFormat(compat)) {
352
+ return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
353
+ }
354
+ if (
355
+ isZaiThinkingFormat(compat) ||
356
+ isAnthropicMessagesGlm52ReasoningEffortModel(spec) ||
357
+ isOllamaCloudGlm52ReasoningEffortModel(spec) ||
358
+ spec.provider === "baseten"
359
+ ) {
360
+ return HIGH_MAX_REASONING_EFFORTS;
361
+ }
362
+ if (isOpenAICompatReasoningApi(spec.api)) {
363
+ return DEFAULT_REASONING_EFFORTS_WITH_MAX;
364
+ }
365
+ }
366
+ if (isKimiK3ModelId(spec.id)) {
367
+ return LOW_HIGH_MAX_REASONING_EFFORTS;
368
+ }
369
+ if (isOpenCodeGatewayOxAlphaModel(spec)) {
370
+ return LOW_HIGH_MAX_REASONING_EFFORTS;
371
+ }
372
+ if (isSakanaFuguReasoningModel(spec)) {
373
+ return HIGH_MAX_REASONING_EFFORTS;
374
+ }
375
+ if (isGpt56PlusWireEffortModel(spec)) {
376
+ // Normalize stale baked/discovered `low..xhigh` surfaces to the
377
+ // wire-exact five-tier `low..max` ladder.
378
+ return FIVE_TIER_EFFORTS_LOW_TO_MAX;
379
+ }
380
+ const anthropicAdaptive = getAnthropicAdaptiveEfforts(spec);
381
+ if (anthropicAdaptive !== undefined) {
382
+ return anthropicAdaptive;
383
+ }
384
+ // Fire Pass's Kimi router accepts low..max with distinct xhigh and max
385
+ // budgets; user minimal has no wire tier there.
386
+ if (spec.provider === "firepass") {
387
+ return FIVE_TIER_EFFORTS_LOW_TO_MAX;
388
+ }
389
+ // Local Ollama's effort vocabulary is low/medium/high/max regardless of
390
+ // model. Custom OpenAI-compatible providers pointed at an Ollama port
391
+ // under a different provider id must set `compat.reasoningEffortMap`
392
+ // themselves.
393
+ if (spec.provider === "ollama") {
394
+ return OLLAMA_REASONING_EFFORTS;
395
+ }
396
+ // Qwen 3.8+ served through a local llama.cpp-style backend: the chat
397
+ // template's prompt-steered `reasoning_effort` kwarg accepts exactly
398
+ // low/medium/xhigh (and thinking cannot be turned off — the official 3.8
399
+ // template raises on `enable_thinking: false`, hence requiresEffort).
400
+ if (isOpenAICompatReasoningApi(spec.api) && isQwenTemplateReasoningEffortCompat(compat)) {
401
+ return QWEN38_TEMPLATE_REASONING_EFFORTS;
402
+ }
403
+ if (
404
+ (isOpenAICompatReasoningApi(spec.api) ||
405
+ spec.api === "openai-responses" ||
406
+ (spec.api === "ollama-chat" && spec.provider === "ollama-cloud")) &&
407
+ isDeepseekReasoningModel(spec)
408
+ ) {
409
+ // The DeepSeek V4 effort ladder is a model property, not a transport one:
410
+ // `opencode-go/deepseek-v4-flash` is pinned to `openai-responses` (the Go
411
+ // gateway serves it only at /responses), yet carries the same wire-exact
412
+ // low/high/max scale — so the Responses transport is admitted here too.
413
+ // DeepSeek V4 (Flash and Pro) accepts the wire-exact low/high/max ladder
414
+ // on every first-party/aggregator host — the direct API, aggregators, and
415
+ // Ollama Cloud alike (medium/xhigh fold into high, max is a real wire
416
+ // tier). See https://api-docs.deepseek.com/api/create-chat-completion.
417
+ // OpenRouter's non-Flash V4 route exposes only high, except the dated
418
+ // `deepseek-v4-pro-0813` SKU: its /models metadata advertises (and the
419
+ // route accepts) the full low/high/max ladder like every other host.
420
+ // The older reasoners (V3.x, R1, deepseek-reasoner) top out at high/max.
421
+ if (isDeepseekV4FlashModelId(spec.id)) {
422
+ return LOW_HIGH_MAX_REASONING_EFFORTS;
423
+ }
424
+ if (bareModelId(spec.id).toLowerCase().includes("deepseek-v4")) {
425
+ if (!isOpenRouterThinkingFormat(compat)) {
426
+ return LOW_HIGH_MAX_REASONING_EFFORTS;
427
+ }
428
+ return bareModelId(spec.id).toLowerCase() === "deepseek-v4-pro-0813"
429
+ ? LOW_HIGH_MAX_REASONING_EFFORTS
430
+ : HIGH_ONLY_REASONING_EFFORTS;
431
+ }
432
+ return isOpenRouterThinkingFormat(compat) ? HIGH_ONLY_REASONING_EFFORTS : HIGH_MAX_REASONING_EFFORTS;
433
+ }
434
+ if (spec.provider === "baseten" && isOpenAIGptOssModelId(spec.id)) {
435
+ // Baseten's gpt-oss router mirrors its GLM route: high/max only.
436
+ return HIGH_MAX_REASONING_EFFORTS;
437
+ }
438
+ // First-party Grok: `grok-4.6*` and `grok-4.20-multi-agent*` advertise
439
+ // `xhigh`. Other effort-capable SKUs stay on `minimal/low/medium/high`.
440
+ if (modelMatchesHost({ provider: spec.provider, baseUrl: spec.baseUrl ?? "" }, "xai")) {
441
+ return isGrokXHighEffortCapable(spec.id) ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS;
442
+ }
443
+ return isOpenAICompatReasoningApi(spec.api) &&
444
+ (isMinimaxM2FamilyModelId(spec.id) ||
445
+ isOpenAIGptOssModelId(spec.id) ||
446
+ isOpenAICompatMimoReasoningEffortModel(spec, compat))
447
+ ? LOW_MEDIUM_HIGH_REASONING_EFFORTS
448
+ : undefined;
449
+ }
450
+
451
+ /**
452
+ * Wire-exact effort ladders for Anthropic adaptive models (4.6+). Model-defined
453
+ * so stale cached surfaces normalize on every build: Messages-API models with
454
+ * the real xhigh tier (4.7+) expose the full five-tier `low..max` scale;
455
+ * Opus/Sonnet 4.6 and every Bedrock adaptive model stay on the four-tier
456
+ * `low/medium/high/max` scale.
457
+ */
458
+ function getAnthropicAdaptiveEfforts<TApi extends Api>(spec: ModelSpec<TApi>): readonly Effort[] | undefined {
459
+ const parsed = parseAnthropicModel(bareModelId(spec.id));
460
+ if (!parsed || !isAnthropicAdaptiveGenAtLeast(parsed, "4.6")) return undefined;
461
+ if (spec.api === "anthropic-messages" || spec.api === "bedrock-converse-stream") {
462
+ return anthropicModelHasRealXHighEffort(spec, parsed)
463
+ ? FIVE_TIER_EFFORTS_LOW_TO_MAX
464
+ : FOUR_TIER_EFFORTS_LOW_TO_MAX;
465
+ }
466
+ if (isOpenRouterAnthropicAdaptiveReasoningModel(parsed, spec)) {
467
+ return isAnthropicAdaptiveGenAtLeast(parsed, "4.7") ? FIVE_TIER_EFFORTS_LOW_TO_MAX : FOUR_TIER_EFFORTS_LOW_TO_MAX;
468
+ }
469
+ return undefined;
470
+ }
471
+
472
+ function isOllamaCloudGlm52ReasoningEffortModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
473
+ return spec.api === "ollama-chat" && spec.provider === "ollama-cloud" && isGlm52ReasoningEffortModelId(spec.id);
474
+ }
475
+
476
+ function isAnthropicMessagesGlm52ReasoningEffortModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
477
+ return (
478
+ spec.api === "anthropic-messages" &&
479
+ (spec.provider === "umans" || spec.provider === "zai") &&
480
+ isGlm52ReasoningEffortModelId(spec.id)
481
+ );
482
+ }
483
+
484
+ function isMinimaxReasoningModelOnAnthropicEndpoint<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
485
+ return spec.api === "anthropic-messages" && (isMinimaxM2FamilyModelId(spec.id) || isMinimaxM3FamilyModelId(spec.id));
486
+ }
487
+
488
+ function isOpenAICompatMimoReasoningEffortModel<TApi extends Api>(
489
+ spec: ModelSpec<TApi>,
490
+ compat: CompatOf<TApi>,
491
+ ): boolean {
492
+ if (!isOpenAICompatReasoningApi(spec.api)) return false;
493
+ if (!isMimoModelIdOrName(spec.id) && !isMimoModelIdOrName(spec.name ?? "")) return false;
494
+ const resolved = compat as ResolvedOpenAICompat | undefined;
495
+ return (
496
+ (resolved?.thinkingFormat === "openai" || resolved?.thinkingFormat === "openrouter") &&
497
+ resolved.supportsReasoningEffort
498
+ );
499
+ }
500
+
501
+ function readCompatEffortMap(compat: CompatOf<Api>): EffortMap | undefined {
502
+ if (compat === undefined || !("reasoningEffortMap" in compat)) {
503
+ return undefined;
504
+ }
505
+ const map = compat.reasoningEffortMap;
506
+ return map && Object.keys(map).length > 0 ? map : undefined;
507
+ }
508
+
509
+ function isOpenRouterThinkingFormat(compat: CompatOf<Api>): boolean {
510
+ return compat !== undefined && "thinkingFormat" in compat && compat.thinkingFormat === "openrouter";
511
+ }
512
+
513
+ function isZaiThinkingFormat(compat: CompatOf<Api>): boolean {
514
+ return compat !== undefined && "thinkingFormat" in compat && compat.thinkingFormat === "zai";
515
+ }
516
+ /** Resolved-compat gate for the Qwen 3.8+ local template `reasoning_effort` dialect. */
517
+ function isQwenTemplateReasoningEffortCompat(compat: CompatOf<Api>): boolean {
518
+ return (
519
+ compat !== undefined && "qwenTemplateReasoningEffort" in compat && compat.qwenTemplateReasoningEffort === true
520
+ );
521
+ }
522
+
523
+ function inferDetectedEffortMap<TApi extends Api>(
524
+ spec: ModelSpec<TApi>,
525
+ compat: CompatOf<TApi>,
526
+ mode: ThinkingConfig["mode"],
527
+ ): EffortMap | undefined {
528
+ if (mode === "anthropic-adaptive") {
529
+ if (isMinimaxReasoningModelOnAnthropicEndpoint(spec)) {
530
+ return MINIMAX_ANTHROPIC_ADAPTIVE_EFFORT_MAP;
531
+ }
532
+ // Adaptive effort ladders are wire-exact (see
533
+ // getAnthropicAdaptiveEfforts) — no mapping needed.
534
+ return undefined;
535
+ }
536
+ if (!isOpenAICompatReasoningApi(spec.api)) {
537
+ return undefined;
538
+ }
539
+ if (spec.provider === "groq" && spec.id === "qwen/qwen3-32b") {
540
+ return GROQ_QWEN3_32B_REASONING_EFFORT_MAP;
541
+ }
542
+ if (isOpenAICompatMimoReasoningEffortModel(spec, compat)) {
543
+ return MIMO_REASONING_EFFORT_MAP;
544
+ }
545
+ // Host quirk: Fireworks rejects `minimal` (maps to `none`) on ladders
546
+ // that genuinely include it. Filtered to supported efforts later.
547
+ if (modelMatchesHost(spec, "fireworks")) {
548
+ return FIREWORKS_REASONING_EFFORT_MAP;
549
+ }
550
+ return undefined;
551
+ }
552
+
553
+ function isSakanaFuguReasoningModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
554
+ return spec.provider === "sakana" && /^fugu(?:$|-)/i.test(spec.id);
555
+ }
556
+
557
+ /**
558
+ * "Ox Alpha" stealth models on the OpenCode gateways (`opencode-go` /
559
+ * `opencode-zen`) reason through the wire-exact `low`/`high`/`max` ladder with
560
+ * mandatory thinking: the gateway rejects `minimal`/`medium`/`xhigh`
561
+ * (`[1210] ... please use low, high, or max`), the same dialect it already
562
+ * serves for GLM-5.3 and Kimi K3. Other hosts proxying an `ox-alpha` SKU
563
+ * (Kilo, NanoGPT, Venice, OpenRouter) expose their own vocabularies and are
564
+ * left untouched. See issue #9349.
565
+ */
566
+ function isOpenCodeGatewayOxAlphaModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
567
+ return (
568
+ (spec.provider === "opencode-go" || spec.provider === "opencode-zen") &&
569
+ /(?:^|\/)ox-alpha(?:-|$)/i.test(bareModelId(spec.id))
570
+ );
571
+ }
572
+
573
+ function isDeepseekReasoningModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
574
+ if (!spec.reasoning) return false;
575
+ const lowerId = spec.id.toLowerCase();
576
+ const lowerName = (spec.name ?? "").toLowerCase();
577
+ const isOpenCodeDeepseekAlias =
578
+ spec.provider === "opencode-zen" && (lowerId === "big-pickle" || lowerName === "big pickle");
579
+ return (
580
+ modelMatchesHost(spec, "deepseekFamily") ||
581
+ isDeepseekModelIdOrName(spec.id) ||
582
+ isDeepseekModelIdOrName(spec.name ?? "") ||
583
+ isOpenCodeDeepseekAlias
584
+ );
585
+ }
586
+
587
+ function inferSupportedEfforts<TApi extends Api>(
588
+ parsedModel: ParsedModel,
589
+ spec: ModelSpec<TApi>,
590
+ compat: CompatOf<TApi>,
591
+ ): readonly Effort[] {
592
+ const modelDefinedEfforts = getModelDefinedEfforts(spec, compat);
593
+ if (modelDefinedEfforts !== undefined) {
594
+ return modelDefinedEfforts;
595
+ }
596
+ switch (parsedModel.family) {
597
+ case "openai":
598
+ return inferOpenAISupportedEfforts(parsedModel);
599
+ case "gemini":
600
+ return inferGeminiSupportedEfforts(parsedModel);
601
+ case "anthropic":
602
+ return inferAnthropicSupportedEfforts(parsedModel, spec, compat);
603
+ case "unknown":
604
+ return inferFallbackEfforts(spec, compat);
605
+ }
606
+ }
607
+
608
+ function inferOpenAISupportedEfforts(model: OpenAIModel): readonly Effort[] {
609
+ if (model.variant === "codex-mini" && semverEqual(model.version, "5.1")) {
610
+ return GPT_5_1_CODEX_MINI_EFFORTS;
611
+ }
612
+ // 5.6+ exposes the wire-exact five-tier ladder low..max.
613
+ if (semverGte(model.version, "5.6")) {
614
+ return FIVE_TIER_EFFORTS_LOW_TO_MAX;
615
+ }
616
+ if (semverGte(model.version, "5.2")) {
617
+ return GPT_5_2_PLUS_EFFORTS;
618
+ }
619
+ return DEFAULT_REASONING_EFFORTS;
620
+ }
621
+
622
+ function inferGeminiSupportedEfforts(model: GeminiModel): readonly Effort[] {
623
+ if (!semverGte(model.version, "3.0")) {
624
+ return DEFAULT_REASONING_EFFORTS;
625
+ }
626
+ return model.kind === "pro" ? GEMINI_3_PRO_EFFORTS : GEMINI_3_FLASH_EFFORTS;
627
+ }
628
+
629
+ const OPENAI_O_SERIES_RE = /^o[134](?:$|[-:.])/i;
630
+
631
+ /**
632
+ * Reasoning-only upstreams reject disabled or omitted thinking ("Reasoning is
633
+ * mandatory for this endpoint and cannot be disabled") — the floor is the
634
+ * lowest effort, never off:
635
+ * - Gemini 3.x exposes levels only; Gemini 2.5 Pro floors thinkingBudget at
636
+ * 128 and rejects 0 (2.5 Flash/Flash-Lite keep the off switch).
637
+ * - OpenAI o-series and MiniMax M2 are reasoning-first architectures.
638
+ * - Thinking-variant SKUs (`*-thinking`, `*-reasoner`, `*-reasoning`) ARE the
639
+ * thinking checkpoint; live bare twins pair-collapse away
640
+ * (variant-collapse) and the collapsed entry owns off — this floor protects
641
+ * the orphans.
642
+ */
643
+ function impliesMandatoryReasoning(parsed: ParsedModel, modelId: string): boolean {
644
+ if (parsed.family === "gemini") {
645
+ if (semverGte(parsed.version, "3.0")) return true;
646
+ if (parsed.kind === "pro" && semverGte(parsed.version, "2.5")) return true;
647
+ }
648
+ if (isKimiK3ModelId(modelId)) return true;
649
+ // GLM-5.3+ no longer supports disabling thinking — thinking.type must
650
+ // always be "enabled". Floor thinking-off requests to the lowest effort.
651
+ if (isGlm53ReasoningEffortModelId(modelId)) return true;
652
+ if (isMinimaxM2FamilyModelId(modelId)) return true;
653
+ if (OPENAI_O_SERIES_RE.test(bareModelId(modelId))) return true;
654
+ return findThinkingVariantToken(modelId) !== undefined;
655
+ }
656
+
657
+ function inferAnthropicSupportedEfforts<TApi extends Api>(
658
+ parsedModel: AnthropicModel,
659
+ spec: ModelSpec<TApi>,
660
+ compat: CompatOf<TApi>,
661
+ ): readonly Effort[] {
662
+ // Ladders for adaptive-generation models (Opus 4.6+, Sonnet 5+,
663
+ // Fable/Mythos) are model-defined and already resolved by
664
+ // getAnthropicAdaptiveEfforts. Every other 4.6+ model on the Messages
665
+ // API (Sonnet/Haiku 4.6) still runs adaptive mode with the three-tier
666
+ // low/medium/high wire scale — no minimal, no max.
667
+ if (spec.api === "anthropic-messages" && semverGte(parsedModel.version, "4.6")) {
668
+ return LOW_MEDIUM_HIGH_REASONING_EFFORTS;
669
+ }
670
+ // Non-adaptive 4.6 models on Bedrock stay budget-mode, where minimal is
671
+ // a legitimate synthetic budget tier.
672
+ if (spec.api === "bedrock-converse-stream" && semverGte(parsedModel.version, "4.6")) {
673
+ return DEFAULT_REASONING_EFFORTS;
674
+ }
675
+ return inferFallbackEfforts(spec, compat);
676
+ }
677
+
678
+ function inferFallbackEfforts<TApi extends Api>(spec: ModelSpec<TApi>, compat: CompatOf<TApi>): readonly Effort[] {
679
+ const modelDefinedEfforts = getModelDefinedEfforts(spec, compat);
680
+ if (modelDefinedEfforts !== undefined) return modelDefinedEfforts;
681
+ if (isMinimaxReasoningModelOnAnthropicEndpoint(spec)) {
682
+ return LOW_MEDIUM_HIGH_REASONING_EFFORTS;
683
+ }
684
+ if (spec.api === "anthropic-messages") {
685
+ return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
686
+ }
687
+ if (spec.name.includes("deepseek-v4")) {
688
+ return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
689
+ }
690
+ if (spec.api === "bedrock-converse-stream") {
691
+ return DEFAULT_REASONING_EFFORTS;
692
+ }
693
+ if (isOpenAICompatReasoningApi(spec.api)) {
694
+ const resolved = compat as ResolvedOpenAICompat;
695
+ if (
696
+ resolved.thinkingFormat === "openai" &&
697
+ modelMatchesHost({ provider: spec.provider, baseUrl: spec.baseUrl ?? "" }, "venice") &&
698
+ isQwenModelId(spec.id)
699
+ ) {
700
+ return DEFAULT_REASONING_EFFORTS;
701
+ }
702
+ if (resolved.thinkingFormat === "openai" && resolved.supportsReasoningEffort) {
703
+ return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
704
+ }
705
+ return DEFAULT_REASONING_EFFORTS;
706
+ }
707
+ // OpenAI Responses APIs encode discrete effort levels, including xhigh.
708
+ if (
709
+ spec.api === "openai-responses" ||
710
+ spec.api === "openai-codex-responses" ||
711
+ spec.api === "azure-openai-responses"
712
+ ) {
713
+ return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
714
+ }
715
+ return DEFAULT_REASONING_EFFORTS;
716
+ }
717
+
718
+ function inferThinkingControlMode<TApi extends Api>(
719
+ spec: ModelSpec<TApi>,
720
+ parsedModel: ParsedModel,
721
+ ): ThinkingConfig["mode"] {
722
+ switch (spec.api) {
723
+ case "google-generative-ai":
724
+ case "google-gemini-cli":
725
+ case "google-vertex":
726
+ return parsedModel.family === "gemini" &&
727
+ semverGte(parsedModel.version, "3.0") &&
728
+ parsedModel.version.major === 3
729
+ ? "google-level"
730
+ : "budget";
731
+
732
+ case "anthropic-messages":
733
+ if (isMinimaxReasoningModelOnAnthropicEndpoint(spec)) {
734
+ return "anthropic-adaptive";
735
+ }
736
+ if (isAnthropicMessagesGlm52ReasoningEffortModel(spec)) {
737
+ return "anthropic-budget-effort";
738
+ }
739
+ if (parsedModel.family === "anthropic") {
740
+ if (semverGte(parsedModel.version, "4.6")) {
741
+ return "anthropic-adaptive";
742
+ }
743
+ // Opus 4.5 supports `output_config.effort` (sent alongside
744
+ // `thinking.budget_tokens`); Sonnet 4.5 and Haiku 4.5 reject the
745
+ // field with HTTP 400 "This model does not support the effort
746
+ // parameter." (#3497).
747
+ if (parsedModel.kind === "opus" && semverGte(parsedModel.version, "4.5")) {
748
+ return "anthropic-budget-effort";
749
+ }
750
+ }
751
+ return "budget";
752
+
753
+ case "bedrock-converse-stream":
754
+ if (parsedModel.family === "anthropic") {
755
+ if (isAnthropicAdaptiveGenAtLeast(parsedModel, "4.6")) {
756
+ return "anthropic-adaptive";
757
+ }
758
+ // Opus 4.5 on Bedrock metadata mirrors the direct-Anthropic
759
+ // shape; the Bedrock provider still emits plain budget thinking
760
+ // on the wire for the budget-effort mode.
761
+ if (parsedModel.kind === "opus" && semverGte(parsedModel.version, "4.5")) {
762
+ return "anthropic-budget-effort";
763
+ }
764
+ }
765
+ return "budget";
766
+
767
+ default:
768
+ return "effort";
769
+ }
770
+ }
771
+
772
+ function isOpenRouterAnthropicAdaptiveReasoningModel<TApi extends Api>(
773
+ parsedModel: AnthropicModel,
774
+ spec: ModelSpec<TApi>,
775
+ ): boolean {
776
+ if (!isOpenAICompatReasoningApi(spec.api)) return false;
777
+ if (!modelMatchesHost(spec, "openrouter")) return false;
778
+ return isAnthropicAdaptiveGenAtLeast(parsedModel, "4.6");
779
+ }
780
+
781
+ /**
782
+ * Opus 4.7+, Sonnet 5+, and Fable/Mythos 5+ on the Messages API expose the full five-tier
783
+ * adaptive scale (low/medium/high/xhigh/max). Bedrock Converse stays on the
784
+ * four-tier scale regardless of model version.
785
+ */
786
+ function anthropicModelHasRealXHighEffort<TApi extends Api>(spec: ModelSpec<TApi>, parsedModel: ParsedModel): boolean {
787
+ if (spec.api !== "anthropic-messages") return false;
788
+ if (parsedModel.family !== "anthropic") return false;
789
+ return isAnthropicAdaptiveGenAtLeast(parsedModel, "4.7");
790
+ }
791
+
792
+ // ---------------------------------------------------------------------------
793
+ // Runtime helpers (field reads only — safe per request)
794
+ // ---------------------------------------------------------------------------
795
+
796
+ /**
797
+ * Returns the supported thinking efforts declared on the model metadata.
798
+ * Empty for non-reasoning models and for reasoning models without a
799
+ * controllable effort surface (`thinking: undefined`).
800
+ */
801
+ export function getSupportedEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly Effort[] {
802
+ if (!model.reasoning) {
803
+ return [];
804
+ }
805
+ return model.thinking?.efforts ?? [];
806
+ }
807
+
808
+ /**
809
+ * Clamps a requested thinking level against explicit model metadata.
810
+ *
811
+ * Non-reasoning models always resolve to `undefined`.
812
+ */
813
+ export function clampThinkingLevelForModel<TApi extends Api>(
814
+ model: ApiModel<TApi> | undefined,
815
+ requested: Effort | undefined,
816
+ ): Effort | undefined {
817
+ if (!model) {
818
+ return requested;
819
+ }
820
+ if (!model.reasoning || requested === undefined) {
821
+ return undefined;
822
+ }
823
+
824
+ const levels = getSupportedEfforts(model);
825
+ if (levels.includes(requested)) {
826
+ return requested;
827
+ }
828
+
829
+ const requestedIndex = THINKING_EFFORTS.indexOf(requested);
830
+ if (requestedIndex === -1) {
831
+ return undefined;
832
+ }
833
+
834
+ let clamped: Effort | undefined;
835
+ for (const effort of levels) {
836
+ if (THINKING_EFFORTS.indexOf(effort) > requestedIndex) {
837
+ break;
838
+ }
839
+ clamped = effort;
840
+ }
841
+
842
+ return clamped ?? levels[0];
843
+ }
844
+
845
+ export function requireSupportedEffort<TApi extends Api>(model: ApiModel<TApi>, effort: Effort): Effort {
846
+ if (!model.reasoning) {
847
+ throw new Error(`Model ${model.provider}/${model.id} does not support thinking`);
848
+ }
849
+ const levels = getSupportedEfforts(model);
850
+ if (!levels.includes(effort)) {
851
+ throw new Error(
852
+ `Thinking effort ${effort} is not supported by ${model.provider}/${model.id}. Supported efforts: ${levels.join(", ")}`,
853
+ );
854
+ }
855
+ return effort;
856
+ }
857
+
858
+ /** Maps a normalized thinking effort to Google's `thinkingLevel` enum values.
859
+ * When a collapsed family routes `minimal` onto the same wire id as `low`
860
+ * (Antigravity Gemini 3.6/3.7 Flash), emit `LOW` — Cloud Code Assist rejects
861
+ * `MINIMAL` on those `-low` SKUs.
862
+ */
863
+ export function mapEffortToGoogleThinkingLevel<TApi extends Api>(
864
+ effort: Effort,
865
+ model?: ApiModel<TApi>,
866
+ ): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" {
867
+ if (effort === Effort.Minimal) {
868
+ const routing = model?.thinking?.effortRouting;
869
+ if (routing?.[Effort.Minimal] && routing[Effort.Minimal] === routing[Effort.Low]) {
870
+ return "LOW";
871
+ }
872
+ return "MINIMAL";
873
+ }
874
+ switch (effort) {
875
+ case Effort.Low:
876
+ return "LOW";
877
+ case Effort.Medium:
878
+ return "MEDIUM";
879
+ case Effort.High:
880
+ case Effort.XHigh:
881
+ case Effort.Max:
882
+ return "HIGH";
883
+ }
884
+ }
885
+
886
+ /**
887
+ * Maps a normalized thinking effort to Anthropic adaptive effort values via
888
+ * the model's baked `thinking.effortMap` (identity for unmapped efforts).
889
+ */
890
+ export function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(
891
+ model: ApiModel<TApi>,
892
+ effort: Effort,
893
+ ): "low" | "medium" | "high" | "xhigh" | "max" | "adaptive" {
894
+ const supported = requireSupportedEffort(model, effort);
895
+ return (model.thinking?.effortMap?.[supported] ?? supported) as
896
+ | "low"
897
+ | "medium"
898
+ | "high"
899
+ | "xhigh"
900
+ | "max"
901
+ | "adaptive";
902
+ }
903
+
904
+ /**
905
+ * Resolves the upstream wire model id for a request at the given effort
906
+ * (`undefined` = thinking off). Collapsed effort-tier variants route through
907
+ * `thinking.effortRouting`; everything else falls back to
908
+ * `requestModelId ?? id`.
909
+ */
910
+ export function resolveWireModelId<TApi extends Api>(model: ApiModel<TApi>, effort: Effort | undefined): string {
911
+ return model.thinking?.effortRouting?.[effort ?? "off"] ?? model.requestModelId ?? model.id;
912
+ }
913
+
914
+ /**
915
+ * Lowest supported effort in canonical order — the clamp target for
916
+ * thinking-off requests on `thinking.requiresEffort` models.
917
+ */
918
+ export function minimumSupportedEffort<TApi extends Api>(model: ApiModel<TApi>): Effort | undefined {
919
+ const efforts = model.thinking?.efforts;
920
+ if (!efforts || efforts.length === 0) return undefined;
921
+ for (const effort of THINKING_EFFORTS) {
922
+ if (efforts.includes(effort)) return effort;
923
+ }
924
+ return efforts[0];
925
+ }