@linxiraos/pi-catalog 1.0.0

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