@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.
- package/CHANGELOG.md +1204 -0
- package/LICENSE +22 -0
- package/README.md +43 -0
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/types/build.d.ts +19 -0
- package/dist/types/compat/anthropic.d.ts +18 -0
- package/dist/types/compat/apply.d.ts +7 -0
- package/dist/types/compat/bedrock.d.ts +3 -0
- package/dist/types/compat/devin.d.ts +9 -0
- package/dist/types/compat/openai.d.ts +28 -0
- package/dist/types/discovery/antigravity.d.ts +70 -0
- package/dist/types/discovery/codex.d.ts +36 -0
- package/dist/types/discovery/cursor-proto.d.ts +5368 -0
- package/dist/types/discovery/cursor.d.ts +23 -0
- package/dist/types/discovery/devin-proto.d.ts +1549 -0
- package/dist/types/discovery/devin.d.ts +24 -0
- package/dist/types/discovery/gemini-cli.d.ts +33 -0
- package/dist/types/discovery/gemini.d.ts +25 -0
- package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
- package/dist/types/discovery/index.d.ts +7 -0
- package/dist/types/discovery/openai-compatible.d.ts +89 -0
- package/dist/types/discovery/protobuf.d.ts +102 -0
- package/dist/types/effort.d.ts +10 -0
- package/dist/types/fireworks-model-id.d.ts +23 -0
- package/dist/types/hosts.d.ts +169 -0
- package/dist/types/identity/bundled.d.ts +3 -0
- package/dist/types/identity/classify.d.ts +69 -0
- package/dist/types/identity/dialect.d.ts +3 -0
- package/dist/types/identity/family.d.ts +239 -0
- package/dist/types/identity/id.d.ts +12 -0
- package/dist/types/identity/index.d.ts +8 -0
- package/dist/types/identity/markers.d.ts +4 -0
- package/dist/types/identity/priority.d.ts +1 -0
- package/dist/types/identity/reference.d.ts +29 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/model-cache.d.ts +23 -0
- package/dist/types/model-manager.d.ts +77 -0
- package/dist/types/model-thinking.d.ts +70 -0
- package/dist/types/model-tokenizer.d.ts +8 -0
- package/dist/types/models.d.ts +14 -0
- package/dist/types/openai-pricing.d.ts +17 -0
- package/dist/types/provider-models/bundled-references.d.ts +13 -0
- package/dist/types/provider-models/cache-provider-id.d.ts +11 -0
- package/dist/types/provider-models/descriptor-types.d.ts +76 -0
- package/dist/types/provider-models/descriptors.d.ts +505 -0
- package/dist/types/provider-models/google.d.ts +29 -0
- package/dist/types/provider-models/index.d.ts +7 -0
- package/dist/types/provider-models/ollama.d.ts +22 -0
- package/dist/types/provider-models/openai-compat.d.ts +646 -0
- package/dist/types/provider-models/special.d.ts +53 -0
- package/dist/types/types.d.ts +876 -0
- package/dist/types/utils.d.ts +20 -0
- package/dist/types/variant-collapse.d.ts +137 -0
- package/dist/types/wire/alibaba-token-plan.d.ts +22 -0
- package/dist/types/wire/codex.d.ts +66 -0
- package/dist/types/wire/coreweave.d.ts +12 -0
- package/dist/types/wire/gemini-headers.d.ts +62 -0
- package/dist/types/wire/github-copilot.d.ts +47 -0
- package/dist/types/wire/image-fetchers.d.ts +76 -0
- package/package.json +102 -0
- package/src/build.ts +103 -0
- package/src/compat/anthropic.ts +179 -0
- package/src/compat/apply.ts +15 -0
- package/src/compat/bedrock.ts +153 -0
- package/src/compat/devin.ts +12 -0
- package/src/compat/openai.ts +864 -0
- package/src/discovery/antigravity.ts +257 -0
- package/src/discovery/codex.ts +456 -0
- package/src/discovery/cursor-proto.ts +8329 -0
- package/src/discovery/cursor.ts +409 -0
- package/src/discovery/devin-proto.ts +2037 -0
- package/src/discovery/devin.ts +152 -0
- package/src/discovery/gemini-cli.ts +198 -0
- package/src/discovery/gemini.ts +260 -0
- package/src/discovery/gitlab-duo-workflow.ts +859 -0
- package/src/discovery/index.ts +7 -0
- package/src/discovery/openai-compatible.ts +266 -0
- package/src/discovery/protobuf.ts +1073 -0
- package/src/effort.ts +18 -0
- package/src/fireworks-model-id.ts +50 -0
- package/src/hosts.ts +178 -0
- package/src/identity/bundled.ts +28 -0
- package/src/identity/classify.ts +242 -0
- package/src/identity/dialect.ts +42 -0
- package/src/identity/family.ts +477 -0
- package/src/identity/id.ts +81 -0
- package/src/identity/index.ts +8 -0
- package/src/identity/markers.ts +49 -0
- package/src/identity/priority.ts +59 -0
- package/src/identity/reference.ts +167 -0
- package/src/index.ts +19 -0
- package/src/model-cache.ts +346 -0
- package/src/model-manager.ts +683 -0
- package/src/model-thinking.ts +925 -0
- package/src/model-tokenizer.ts +75 -0
- package/src/models.json +304657 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +108 -0
- package/src/openai-pricing.ts +29 -0
- package/src/provider-models/bundled-references.ts +87 -0
- package/src/provider-models/cache-provider-id.ts +97 -0
- package/src/provider-models/descriptor-types.ts +85 -0
- package/src/provider-models/descriptors.ts +585 -0
- package/src/provider-models/google.ts +117 -0
- package/src/provider-models/index.ts +7 -0
- package/src/provider-models/ollama.ts +204 -0
- package/src/provider-models/openai-compat.ts +6405 -0
- package/src/provider-models/special.ts +213 -0
- package/src/types.ts +1032 -0
- package/src/utils.ts +69 -0
- package/src/variant-collapse.ts +1555 -0
- package/src/wire/alibaba-token-plan.ts +54 -0
- package/src/wire/codex.ts +117 -0
- package/src/wire/coreweave.ts +51 -0
- package/src/wire/gemini-headers.ts +135 -0
- package/src/wire/github-copilot.ts +147 -0
- 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
|
+
}
|