@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,1555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effort-tier variant collapsing.
|
|
3
|
+
*
|
|
4
|
+
* Some providers expose one logical model as several effort- or
|
|
5
|
+
* thinking-suffixed upstream ids (Antigravity CCA:
|
|
6
|
+
* `gemini-3.5-flash-extra-low`/`-low`, `claude-*`/`claude-*-thinking` pairs;
|
|
7
|
+
* aggregators: `X`/`X-thinking` twins). Collapsing replaces the member specs
|
|
8
|
+
* with one logical spec whose `thinking.effortRouting` records the per-effort
|
|
9
|
+
* upstream wire id; request-time code resolves the outbound id via
|
|
10
|
+
* `resolveWireModelId` and everything local (selection, caching, usage
|
|
11
|
+
* attribution) keys on the logical `id`.
|
|
12
|
+
*
|
|
13
|
+
* Families come from two sources:
|
|
14
|
+
* - Hand tables (`VARIANT_COLLAPSE_TABLES`) for providers whose routing needs
|
|
15
|
+
* curation (Antigravity tier triplets, single-member renames, recycled ids).
|
|
16
|
+
* - `deriveThinkingPairFamilies`: the global automatic rule — any live
|
|
17
|
+
* `X` + `X-thinking` pair (trailing or infix token) collapses into `X`,
|
|
18
|
+
* routing thinking-enabled requests to `X-thinking`. Gated on identical
|
|
19
|
+
* pricing and same api: price-divergent twins are distinct SKUs and stay
|
|
20
|
+
* separate so billing attribution never lies.
|
|
21
|
+
*
|
|
22
|
+
* Family invariants (hold for hand-written and derived tables):
|
|
23
|
+
* - One axis per family. A second id axis (e.g. Cursor's `-fast` service
|
|
24
|
+
* tier) becomes a sibling family, never a second routing dimension.
|
|
25
|
+
* - The collapsed spec inherits non-tier fields from the first present
|
|
26
|
+
* member; members must be cost-homogeneous.
|
|
27
|
+
*
|
|
28
|
+
* `collapseEffortVariants` is pure, deterministic, and idempotent:
|
|
29
|
+
* `collapse(collapse(x))` equals `collapse(x)`, and mixed raw+collapsed input
|
|
30
|
+
* (stale cache rows, previous-snapshot fallbacks) dedupes to the collapsed
|
|
31
|
+
* entry. That makes it safe at every source — discovery, the catalog
|
|
32
|
+
* generator, and the model-manager merge point.
|
|
33
|
+
*/
|
|
34
|
+
import { buildCompat, buildModel } from "./build";
|
|
35
|
+
import { Effort, THINKING_EFFORTS } from "./effort";
|
|
36
|
+
import { stripThinkingVariantToken } from "./identity/family";
|
|
37
|
+
import { resolveModelThinking } from "./model-thinking";
|
|
38
|
+
import type { Api, Model, ModelSpec, Provider, ThinkingConfig } from "./types";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Structural bound for collapse inputs: both raw `ModelSpec`s and built
|
|
42
|
+
* `Model`s qualify. (`Model.compat` is the resolved record, not the sparse
|
|
43
|
+
* config, so the two are not mutually assignable — collapsing never touches
|
|
44
|
+
* `compat`.)
|
|
45
|
+
*/
|
|
46
|
+
export type VariantSpecLike = Omit<ModelSpec<Api>, "compat"> & { compat?: unknown };
|
|
47
|
+
|
|
48
|
+
/** One collapsed family: logical id + member wire ids + per-effort routing. */
|
|
49
|
+
export interface EffortVariantFamily {
|
|
50
|
+
/** Collapsed logical id (may equal a member id — e.g. bare/thinking pairs). */
|
|
51
|
+
id: string;
|
|
52
|
+
/** Final display name, no tier marker. */
|
|
53
|
+
name: string;
|
|
54
|
+
/**
|
|
55
|
+
* Member wire ids in priority order. The first member present in the input
|
|
56
|
+
* becomes the collapsed spec's default wire id (`requestModelId`; omitted
|
|
57
|
+
* when it equals the logical id).
|
|
58
|
+
*/
|
|
59
|
+
members: readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* Wire ids upstream no longer serves (e.g. a deployment killed while
|
|
62
|
+
* discovery still advertises it). Fresh collapsing never routes to them,
|
|
63
|
+
* and stale collapsed snapshots (bundled catalog, cache rows,
|
|
64
|
+
* previous-generation fallbacks) get routing/`requestModelId` entries that
|
|
65
|
+
* target them re-pointed through `routing`. Keep retired ids in `members`
|
|
66
|
+
* so the raw upstream spec is still consumed and aliased.
|
|
67
|
+
*/
|
|
68
|
+
retiredMembers?: readonly string[];
|
|
69
|
+
/**
|
|
70
|
+
* Per-effort upstream wire id; `"off"` applies when thinking is disabled.
|
|
71
|
+
* Entries whose target member is absent from the input are dropped — those
|
|
72
|
+
* efforts fall back to `requestModelId ?? id`.
|
|
73
|
+
*/
|
|
74
|
+
routing: Readonly<Partial<Record<Effort | "off", string>>>;
|
|
75
|
+
/** Explicit capability surface for the collapsed spec — no inference. */
|
|
76
|
+
thinking: Readonly<Omit<ThinkingConfig, "effortRouting" | "suppressWhenOff">>;
|
|
77
|
+
/** Thinking-off requests must explicitly suppress thinking on the wire. */
|
|
78
|
+
suppressWhenOff?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Preserve non-off effort routes even when discovery omits the backing member.
|
|
81
|
+
* Used for Cloud Code Assist `X`/`X-thinking` pairs where upstream accepts
|
|
82
|
+
* the `-thinking` wire id but the model-list endpoint may advertise only the
|
|
83
|
+
* bare id.
|
|
84
|
+
*/
|
|
85
|
+
preserveAbsentEffortRoutes?: boolean;
|
|
86
|
+
/** Retired/recycled selector ids that alias to this family without being members. */
|
|
87
|
+
extraAliases?: readonly string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface VariantCollapseTable {
|
|
91
|
+
families: readonly EffortVariantFamily[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `X` + `X-thinking` hand family: off routes to the bare id, efforts to `-thinking`. */
|
|
95
|
+
function thinkingPair(baseId: string, name: string): EffortVariantFamily {
|
|
96
|
+
return {
|
|
97
|
+
id: baseId,
|
|
98
|
+
name,
|
|
99
|
+
members: [baseId, `${baseId}-thinking`],
|
|
100
|
+
routing: {
|
|
101
|
+
off: baseId,
|
|
102
|
+
[Effort.Minimal]: `${baseId}-thinking`,
|
|
103
|
+
[Effort.Low]: `${baseId}-thinking`,
|
|
104
|
+
[Effort.Medium]: `${baseId}-thinking`,
|
|
105
|
+
[Effort.High]: `${baseId}-thinking`,
|
|
106
|
+
},
|
|
107
|
+
// Thinking-off routes to the non-thinking backing id, where omitting
|
|
108
|
+
// thinkingConfig is already correct — no suppressWhenOff.
|
|
109
|
+
thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
|
110
|
+
preserveAbsentEffortRoutes: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type TierRoutes = Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", string>>;
|
|
115
|
+
|
|
116
|
+
/** Devin families with a `-max` sibling: five wire tiers, `low` floor. */
|
|
117
|
+
const DEVIN_FIVE_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
|
|
118
|
+
/** Pre-5.6 Devin GPT families top out at `-xhigh`: four wire tiers, `low` floor. */
|
|
119
|
+
const DEVIN_FOUR_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build one effort-tier family from a tier→wire-id map: routing keeps only the
|
|
123
|
+
* listed efforts, `members` dedupes the targets in tier order, and thinking is
|
|
124
|
+
* `mode: "effort"` (mandatory when the family has no `off` route). Shared by the
|
|
125
|
+
* Devin and Cursor tables, whose per-effort siblings follow the same shape.
|
|
126
|
+
*/
|
|
127
|
+
function tierFamily(id: string, name: string, routes: TierRoutes, efforts: readonly Effort[]): EffortVariantFamily {
|
|
128
|
+
const routing: Partial<Record<Effort | "off", string>> = {};
|
|
129
|
+
if (routes.off) routing.off = routes.off;
|
|
130
|
+
for (const effort of efforts) {
|
|
131
|
+
switch (effort) {
|
|
132
|
+
case Effort.Minimal:
|
|
133
|
+
if (routes.minimal) routing[effort] = routes.minimal;
|
|
134
|
+
break;
|
|
135
|
+
case Effort.Low:
|
|
136
|
+
if (routes.low) routing[effort] = routes.low;
|
|
137
|
+
break;
|
|
138
|
+
case Effort.Medium:
|
|
139
|
+
if (routes.medium) routing[effort] = routes.medium;
|
|
140
|
+
break;
|
|
141
|
+
case Effort.High:
|
|
142
|
+
if (routes.high) routing[effort] = routes.high;
|
|
143
|
+
break;
|
|
144
|
+
case Effort.XHigh:
|
|
145
|
+
if (routes.xhigh) routing[effort] = routes.xhigh;
|
|
146
|
+
break;
|
|
147
|
+
case Effort.Max:
|
|
148
|
+
if (routes.max) routing[effort] = routes.max;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const members = [
|
|
153
|
+
routes.off,
|
|
154
|
+
routes.minimal,
|
|
155
|
+
routes.low,
|
|
156
|
+
routes.medium,
|
|
157
|
+
routes.high,
|
|
158
|
+
routes.xhigh,
|
|
159
|
+
routes.max,
|
|
160
|
+
].filter((member, index, items): member is string => typeof member === "string" && items.indexOf(member) === index);
|
|
161
|
+
return {
|
|
162
|
+
id,
|
|
163
|
+
name,
|
|
164
|
+
members,
|
|
165
|
+
routing,
|
|
166
|
+
thinking: {
|
|
167
|
+
mode: "effort",
|
|
168
|
+
efforts,
|
|
169
|
+
...(routes.off ? undefined : { requiresEffort: true }),
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* GPT-5.6 (Luna/Sol/Terra) serves per-tier siblings for the full five-tier
|
|
176
|
+
* `low..max` wire scale in both standard and fast lanes.
|
|
177
|
+
*/
|
|
178
|
+
function devinGpt56Families(variant: "luna" | "sol" | "terra", name: string): readonly EffortVariantFamily[] {
|
|
179
|
+
const base = `gpt-5-6-${variant}`;
|
|
180
|
+
return [
|
|
181
|
+
tierFamily(
|
|
182
|
+
base,
|
|
183
|
+
name,
|
|
184
|
+
{
|
|
185
|
+
off: `${base}-none`,
|
|
186
|
+
low: `${base}-low`,
|
|
187
|
+
medium: `${base}-medium`,
|
|
188
|
+
high: `${base}-high`,
|
|
189
|
+
xhigh: `${base}-xhigh`,
|
|
190
|
+
max: `${base}-max`,
|
|
191
|
+
},
|
|
192
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
193
|
+
),
|
|
194
|
+
tierFamily(
|
|
195
|
+
`${base}-fast`,
|
|
196
|
+
`${name} Fast`,
|
|
197
|
+
{
|
|
198
|
+
off: `${base}-none-priority`,
|
|
199
|
+
low: `${base}-low-priority`,
|
|
200
|
+
medium: `${base}-medium-priority`,
|
|
201
|
+
high: `${base}-high-priority`,
|
|
202
|
+
xhigh: `${base}-xhigh-priority`,
|
|
203
|
+
max: `${base}-max-priority`,
|
|
204
|
+
},
|
|
205
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
206
|
+
),
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const GEMINI_3_FLASH_FAMILY_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
211
|
+
const GEMINI_3_PRO_FAMILY_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High];
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Antigravity Cloud Code Assist sends an explicit `thinkingBudget` per tier
|
|
215
|
+
* (verified against captured `daily-cloudcode-pa` requests). Flash uses round
|
|
216
|
+
* budgets; Pro offsets every budget by +1. Minimal mirrors Low (the Antigravity
|
|
217
|
+
* UI exposes Low/Medium/High only) so the effort stays selectable.
|
|
218
|
+
*/
|
|
219
|
+
const GEMINI_3_FLASH_FAMILY_BUDGETS: Readonly<Partial<Record<Effort, number>>> = {
|
|
220
|
+
[Effort.Minimal]: 1000,
|
|
221
|
+
[Effort.Low]: 1000,
|
|
222
|
+
[Effort.Medium]: 4000,
|
|
223
|
+
[Effort.High]: 10000,
|
|
224
|
+
};
|
|
225
|
+
const GEMINI_3_PRO_FAMILY_BUDGETS: Readonly<Partial<Record<Effort, number>>> = {
|
|
226
|
+
[Effort.Low]: 1001,
|
|
227
|
+
[Effort.High]: 10001,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Cloud Code Assist's legacy Gemini 3.5 Flash and 3.1 Pro families use
|
|
232
|
+
* different thinking transports: `google-antigravity` (daily-cloudcode-pa)
|
|
233
|
+
* sends captured `thinkingBudget` values, while `google-gemini-cli`
|
|
234
|
+
* (cloudcode-pa) follows the official Gemini CLI and uses `thinkingLevel`.
|
|
235
|
+
* Gemini 3.6 exposes one wire id per level and uses `thinkingLevel` on both
|
|
236
|
+
* endpoints.
|
|
237
|
+
*/
|
|
238
|
+
function geminiFlashFamily(mode: "budget" | "google-level"): EffortVariantFamily {
|
|
239
|
+
const budget = mode === "budget";
|
|
240
|
+
return {
|
|
241
|
+
id: "gemini-3.5-flash",
|
|
242
|
+
name: "Gemini 3.5 Flash",
|
|
243
|
+
members: ["gemini-3.5-flash-extra-low", "gemini-3.5-flash-low", "gemini-3-flash-agent"],
|
|
244
|
+
routing: budget
|
|
245
|
+
? {
|
|
246
|
+
off: "gemini-3.5-flash-extra-low",
|
|
247
|
+
[Effort.Minimal]: "gemini-3.5-flash-extra-low",
|
|
248
|
+
[Effort.Low]: "gemini-3.5-flash-extra-low",
|
|
249
|
+
[Effort.Medium]: "gemini-3.5-flash-low",
|
|
250
|
+
[Effort.High]: "gemini-3-flash-agent",
|
|
251
|
+
}
|
|
252
|
+
: {
|
|
253
|
+
off: "gemini-3.5-flash-extra-low",
|
|
254
|
+
[Effort.Minimal]: "gemini-3-flash-agent",
|
|
255
|
+
[Effort.Low]: "gemini-3.5-flash-extra-low",
|
|
256
|
+
[Effort.Medium]: "gemini-3.5-flash-extra-low",
|
|
257
|
+
[Effort.High]: "gemini-3.5-flash-low",
|
|
258
|
+
},
|
|
259
|
+
thinking: budget
|
|
260
|
+
? { mode: "budget", efforts: GEMINI_3_FLASH_FAMILY_EFFORTS, effortBudgets: GEMINI_3_FLASH_FAMILY_BUDGETS }
|
|
261
|
+
: { mode: "google-level", efforts: GEMINI_3_FLASH_FAMILY_EFFORTS },
|
|
262
|
+
suppressWhenOff: true,
|
|
263
|
+
// Retired bare id; the alias only fires when no live model holds it
|
|
264
|
+
// (exact match wins in every resolver).
|
|
265
|
+
extraAliases: ["gemini-3-flash"],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Gemini 3.6+ Flash exposes one mandatory-reasoning wire id per thinking
|
|
271
|
+
* level. Some generations retain additional discovery-only aliases.
|
|
272
|
+
*/
|
|
273
|
+
function geminiLevelFlashFamily(version: "3.6" | "3.7", ...additionalMembers: string[]): EffortVariantFamily {
|
|
274
|
+
const id = `gemini-${version}-flash`;
|
|
275
|
+
return {
|
|
276
|
+
id,
|
|
277
|
+
name: `Gemini ${version} Flash`,
|
|
278
|
+
members: [`${id}-low`, `${id}-medium`, `${id}-high`, ...additionalMembers],
|
|
279
|
+
routing: {
|
|
280
|
+
[Effort.Minimal]: `${id}-low`,
|
|
281
|
+
[Effort.Low]: `${id}-low`,
|
|
282
|
+
[Effort.Medium]: `${id}-medium`,
|
|
283
|
+
[Effort.High]: `${id}-high`,
|
|
284
|
+
},
|
|
285
|
+
thinking: {
|
|
286
|
+
mode: "google-level",
|
|
287
|
+
efforts: GEMINI_3_FLASH_FAMILY_EFFORTS,
|
|
288
|
+
requiresEffort: true,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const GEMINI_36_FLASH_FAMILY = geminiLevelFlashFamily("3.6", "gemini-3.6-flash-tiered");
|
|
294
|
+
const GEMINI_37_FLASH_FAMILY = geminiLevelFlashFamily("3.7");
|
|
295
|
+
|
|
296
|
+
function geminiProFamily(mode: "budget" | "google-level"): EffortVariantFamily {
|
|
297
|
+
const budget = mode === "budget";
|
|
298
|
+
return {
|
|
299
|
+
id: "gemini-3.1-pro",
|
|
300
|
+
name: "Gemini 3.1 Pro",
|
|
301
|
+
// High routes to `gemini-pro-agent` — the upstream `gemini-3.1-pro-high`
|
|
302
|
+
// deployment returns INVALID_ARGUMENT on every streamGenerateContent
|
|
303
|
+
// request (both CCA endpoints) while discovery still lists it;
|
|
304
|
+
// `gemini-pro-agent` is the same model ("Gemini 3.1 Pro (High)", same
|
|
305
|
+
// thinking budget/caps) and accepts the identical request body.
|
|
306
|
+
// `gemini-3.1-pro-high` stays a member so the dead raw id is consumed.
|
|
307
|
+
members: ["gemini-3.1-pro-low", "gemini-pro-agent", "gemini-3.1-pro-high"],
|
|
308
|
+
retiredMembers: ["gemini-3.1-pro-high"],
|
|
309
|
+
routing: {
|
|
310
|
+
off: "gemini-3.1-pro-low",
|
|
311
|
+
[Effort.Low]: "gemini-3.1-pro-low",
|
|
312
|
+
[Effort.High]: "gemini-pro-agent",
|
|
313
|
+
},
|
|
314
|
+
thinking: budget
|
|
315
|
+
? { mode: "budget", efforts: GEMINI_3_PRO_FAMILY_EFFORTS, effortBudgets: GEMINI_3_PRO_FAMILY_BUDGETS }
|
|
316
|
+
: { mode: "google-level", efforts: GEMINI_3_PRO_FAMILY_EFFORTS },
|
|
317
|
+
suppressWhenOff: true,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** CCA families shared verbatim by both providers (transport-agnostic). */
|
|
322
|
+
const SHARED_CCA_FAMILIES: readonly EffortVariantFamily[] = [
|
|
323
|
+
{
|
|
324
|
+
// Legacy static family — covers stale snapshots and caches. Stale ids are
|
|
325
|
+
// unverified against the budget-mode CCA contract; keep them on level.
|
|
326
|
+
id: "gemini-3-pro",
|
|
327
|
+
name: "Gemini 3 Pro",
|
|
328
|
+
members: ["gemini-3-pro-low", "gemini-3-pro-high"],
|
|
329
|
+
routing: {
|
|
330
|
+
off: "gemini-3-pro-low",
|
|
331
|
+
[Effort.Low]: "gemini-3-pro-low",
|
|
332
|
+
[Effort.High]: "gemini-3-pro-high",
|
|
333
|
+
},
|
|
334
|
+
thinking: { mode: "google-level", efforts: GEMINI_3_PRO_FAMILY_EFFORTS },
|
|
335
|
+
suppressWhenOff: true,
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
// Rename-only collapse: every effort and off fall back to the wire id.
|
|
339
|
+
id: "gpt-oss-120b",
|
|
340
|
+
name: "GPT-OSS 120B",
|
|
341
|
+
members: ["gpt-oss-120b-medium"],
|
|
342
|
+
routing: {},
|
|
343
|
+
thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
|
344
|
+
},
|
|
345
|
+
// Antigravity Cloud Code Assist exposes Claude 4.6 asymmetrically: only the
|
|
346
|
+
// bare `claude-sonnet-4-6` wire id (no `-thinking` twin) and only the
|
|
347
|
+
// `claude-opus-4-6-thinking` wire id (no bare twin). Per-effort thinking is
|
|
348
|
+
// carried in the request body via `thinkingBudget`, so both ids accept on/off
|
|
349
|
+
// requests. Listing both candidates in `members` (priority order) keeps the
|
|
350
|
+
// collapse correct if the backend mix ever rebalances; `retiredMembers`
|
|
351
|
+
// re-points stale collapsed snapshots (bundled catalog rows, cache rows
|
|
352
|
+
// written by prior generations) away from the dead wire id via
|
|
353
|
+
// `reconcileRetiredRouting`.
|
|
354
|
+
{
|
|
355
|
+
id: "claude-sonnet-4-6",
|
|
356
|
+
name: "Claude Sonnet 4.6",
|
|
357
|
+
members: ["claude-sonnet-4-6", "claude-sonnet-4-6-thinking"],
|
|
358
|
+
retiredMembers: ["claude-sonnet-4-6-thinking"],
|
|
359
|
+
routing: {},
|
|
360
|
+
thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
{
|
|
364
|
+
id: "claude-opus-4-6",
|
|
365
|
+
name: "Claude Opus 4.6",
|
|
366
|
+
members: ["claude-opus-4-6-thinking", "claude-opus-4-6"],
|
|
367
|
+
retiredMembers: ["claude-opus-4-6"],
|
|
368
|
+
routing: {},
|
|
369
|
+
thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
|
370
|
+
},
|
|
371
|
+
thinkingPair("claude-sonnet-4-5", "Claude Sonnet 4.5"),
|
|
372
|
+
thinkingPair("claude-opus-4-5", "Claude Opus 4.5"),
|
|
373
|
+
thinkingPair("gemini-2.5-flash", "Gemini 2.5 Flash"),
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
/** `google-antigravity` Gemini families, using each generation's native transport. */
|
|
377
|
+
export const ANTIGRAVITY_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
|
|
378
|
+
families: [
|
|
379
|
+
GEMINI_36_FLASH_FAMILY,
|
|
380
|
+
GEMINI_37_FLASH_FAMILY,
|
|
381
|
+
geminiFlashFamily("budget"),
|
|
382
|
+
geminiProFamily("budget"),
|
|
383
|
+
...SHARED_CCA_FAMILIES,
|
|
384
|
+
],
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/** `google-gemini-cli` Gemini families on the official CLI's level transport. */
|
|
388
|
+
export const GEMINI_CLI_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
|
|
389
|
+
families: [
|
|
390
|
+
GEMINI_36_FLASH_FAMILY,
|
|
391
|
+
GEMINI_37_FLASH_FAMILY,
|
|
392
|
+
geminiFlashFamily("google-level"),
|
|
393
|
+
geminiProFamily("google-level"),
|
|
394
|
+
...SHARED_CCA_FAMILIES,
|
|
395
|
+
],
|
|
396
|
+
};
|
|
397
|
+
export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
|
|
398
|
+
families: [
|
|
399
|
+
tierFamily(
|
|
400
|
+
"claude-opus-5",
|
|
401
|
+
"Claude Opus 5",
|
|
402
|
+
{
|
|
403
|
+
low: "claude-opus-5-low",
|
|
404
|
+
medium: "claude-opus-5-medium",
|
|
405
|
+
high: "claude-opus-5-high",
|
|
406
|
+
xhigh: "claude-opus-5-xhigh",
|
|
407
|
+
max: "claude-opus-5-max",
|
|
408
|
+
},
|
|
409
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
410
|
+
),
|
|
411
|
+
tierFamily(
|
|
412
|
+
"claude-opus-5-fast",
|
|
413
|
+
"Claude Opus 5 Fast",
|
|
414
|
+
{
|
|
415
|
+
low: "claude-opus-5-low-fast",
|
|
416
|
+
medium: "claude-opus-5-medium-fast",
|
|
417
|
+
high: "claude-opus-5-high-fast",
|
|
418
|
+
xhigh: "claude-opus-5-xhigh-fast",
|
|
419
|
+
max: "claude-opus-5-max-fast",
|
|
420
|
+
},
|
|
421
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
422
|
+
),
|
|
423
|
+
tierFamily(
|
|
424
|
+
"claude-fable-5",
|
|
425
|
+
"Claude Fable 5",
|
|
426
|
+
{
|
|
427
|
+
low: "claude-5-fable-low",
|
|
428
|
+
medium: "claude-5-fable-medium",
|
|
429
|
+
high: "claude-5-fable-high",
|
|
430
|
+
xhigh: "claude-5-fable-xhigh",
|
|
431
|
+
max: "claude-5-fable-max",
|
|
432
|
+
},
|
|
433
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
434
|
+
),
|
|
435
|
+
tierFamily(
|
|
436
|
+
"claude-sonnet-5",
|
|
437
|
+
"Claude Sonnet 5",
|
|
438
|
+
{
|
|
439
|
+
low: "claude-sonnet-5-low",
|
|
440
|
+
medium: "claude-sonnet-5-medium",
|
|
441
|
+
high: "claude-sonnet-5-high",
|
|
442
|
+
xhigh: "claude-sonnet-5-xhigh",
|
|
443
|
+
max: "claude-sonnet-5-max",
|
|
444
|
+
},
|
|
445
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
446
|
+
),
|
|
447
|
+
tierFamily(
|
|
448
|
+
"claude-opus-4-7",
|
|
449
|
+
"Claude Opus 4.7",
|
|
450
|
+
{
|
|
451
|
+
low: "claude-opus-4-7-low",
|
|
452
|
+
medium: "claude-opus-4-7-medium",
|
|
453
|
+
high: "claude-opus-4-7-high",
|
|
454
|
+
xhigh: "claude-opus-4-7-xhigh",
|
|
455
|
+
max: "claude-opus-4-7-max",
|
|
456
|
+
},
|
|
457
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
458
|
+
),
|
|
459
|
+
tierFamily(
|
|
460
|
+
"claude-opus-4-7-fast",
|
|
461
|
+
"Claude Opus 4.7 Fast",
|
|
462
|
+
{
|
|
463
|
+
low: "claude-opus-4-7-low-fast",
|
|
464
|
+
medium: "claude-opus-4-7-medium-fast",
|
|
465
|
+
high: "claude-opus-4-7-high-fast",
|
|
466
|
+
xhigh: "claude-opus-4-7-xhigh-fast",
|
|
467
|
+
max: "claude-opus-4-7-max-fast",
|
|
468
|
+
},
|
|
469
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
470
|
+
),
|
|
471
|
+
tierFamily(
|
|
472
|
+
"claude-opus-4-8",
|
|
473
|
+
"Claude Opus 4.8",
|
|
474
|
+
{
|
|
475
|
+
low: "claude-opus-4-8-low",
|
|
476
|
+
medium: "claude-opus-4-8-medium",
|
|
477
|
+
high: "claude-opus-4-8-high",
|
|
478
|
+
xhigh: "claude-opus-4-8-xhigh",
|
|
479
|
+
max: "claude-opus-4-8-max",
|
|
480
|
+
},
|
|
481
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
482
|
+
),
|
|
483
|
+
tierFamily(
|
|
484
|
+
"claude-opus-4-8-fast",
|
|
485
|
+
"Claude Opus 4.8 Fast",
|
|
486
|
+
{
|
|
487
|
+
low: "claude-opus-4-8-low-fast",
|
|
488
|
+
medium: "claude-opus-4-8-medium-fast",
|
|
489
|
+
high: "claude-opus-4-8-high-fast",
|
|
490
|
+
xhigh: "claude-opus-4-8-xhigh-fast",
|
|
491
|
+
max: "claude-opus-4-8-max-fast",
|
|
492
|
+
},
|
|
493
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
494
|
+
),
|
|
495
|
+
tierFamily(
|
|
496
|
+
"gpt-5-2",
|
|
497
|
+
"GPT-5.2",
|
|
498
|
+
{
|
|
499
|
+
off: "MODEL_GPT_5_2_NONE",
|
|
500
|
+
low: "MODEL_GPT_5_2_LOW",
|
|
501
|
+
medium: "MODEL_GPT_5_2_MEDIUM",
|
|
502
|
+
high: "MODEL_GPT_5_2_HIGH",
|
|
503
|
+
xhigh: "MODEL_GPT_5_2_XHIGH",
|
|
504
|
+
},
|
|
505
|
+
DEVIN_FOUR_TIER_EFFORTS,
|
|
506
|
+
),
|
|
507
|
+
tierFamily(
|
|
508
|
+
"gpt-5-3-codex",
|
|
509
|
+
"GPT-5.3 Codex",
|
|
510
|
+
{
|
|
511
|
+
low: "gpt-5-3-codex-low",
|
|
512
|
+
medium: "gpt-5-3-codex-medium",
|
|
513
|
+
high: "gpt-5-3-codex-high",
|
|
514
|
+
xhigh: "gpt-5-3-codex-xhigh",
|
|
515
|
+
},
|
|
516
|
+
DEVIN_FOUR_TIER_EFFORTS,
|
|
517
|
+
),
|
|
518
|
+
tierFamily(
|
|
519
|
+
"gpt-5-3-codex-fast",
|
|
520
|
+
"GPT-5.3 Codex Fast",
|
|
521
|
+
{
|
|
522
|
+
low: "gpt-5-3-codex-low-priority",
|
|
523
|
+
medium: "gpt-5-3-codex-medium-priority",
|
|
524
|
+
high: "gpt-5-3-codex-high-priority",
|
|
525
|
+
xhigh: "gpt-5-3-codex-xhigh-priority",
|
|
526
|
+
},
|
|
527
|
+
DEVIN_FOUR_TIER_EFFORTS,
|
|
528
|
+
),
|
|
529
|
+
tierFamily(
|
|
530
|
+
"gpt-5-4",
|
|
531
|
+
"GPT-5.4",
|
|
532
|
+
{
|
|
533
|
+
off: "gpt-5-4-none",
|
|
534
|
+
low: "gpt-5-4-low",
|
|
535
|
+
medium: "gpt-5-4-medium",
|
|
536
|
+
high: "gpt-5-4-high",
|
|
537
|
+
xhigh: "gpt-5-4-xhigh",
|
|
538
|
+
},
|
|
539
|
+
DEVIN_FOUR_TIER_EFFORTS,
|
|
540
|
+
),
|
|
541
|
+
tierFamily(
|
|
542
|
+
"gpt-5-4-fast",
|
|
543
|
+
"GPT-5.4 Fast",
|
|
544
|
+
{
|
|
545
|
+
off: "gpt-5-4-none-priority",
|
|
546
|
+
low: "gpt-5-4-low-priority",
|
|
547
|
+
medium: "gpt-5-4-medium-priority",
|
|
548
|
+
high: "gpt-5-4-high-priority",
|
|
549
|
+
xhigh: "gpt-5-4-xhigh-priority",
|
|
550
|
+
},
|
|
551
|
+
DEVIN_FOUR_TIER_EFFORTS,
|
|
552
|
+
),
|
|
553
|
+
tierFamily(
|
|
554
|
+
"gpt-5-4-mini",
|
|
555
|
+
"GPT-5.4 Mini",
|
|
556
|
+
{
|
|
557
|
+
low: "gpt-5-4-mini-low",
|
|
558
|
+
medium: "gpt-5-4-mini-medium",
|
|
559
|
+
high: "gpt-5-4-mini-high",
|
|
560
|
+
xhigh: "gpt-5-4-mini-xhigh",
|
|
561
|
+
},
|
|
562
|
+
DEVIN_FOUR_TIER_EFFORTS,
|
|
563
|
+
),
|
|
564
|
+
tierFamily(
|
|
565
|
+
"gpt-5-5",
|
|
566
|
+
"GPT-5.5",
|
|
567
|
+
{
|
|
568
|
+
off: "gpt-5-5-none",
|
|
569
|
+
low: "gpt-5-5-low",
|
|
570
|
+
medium: "gpt-5-5-medium",
|
|
571
|
+
high: "gpt-5-5-high",
|
|
572
|
+
xhigh: "gpt-5-5-xhigh",
|
|
573
|
+
},
|
|
574
|
+
DEVIN_FOUR_TIER_EFFORTS,
|
|
575
|
+
),
|
|
576
|
+
tierFamily(
|
|
577
|
+
"gpt-5-5-fast",
|
|
578
|
+
"GPT-5.5 Fast",
|
|
579
|
+
{
|
|
580
|
+
off: "gpt-5-5-none-priority",
|
|
581
|
+
low: "gpt-5-5-low-priority",
|
|
582
|
+
medium: "gpt-5-5-medium-priority",
|
|
583
|
+
high: "gpt-5-5-high-priority",
|
|
584
|
+
xhigh: "gpt-5-5-xhigh-priority",
|
|
585
|
+
},
|
|
586
|
+
DEVIN_FOUR_TIER_EFFORTS,
|
|
587
|
+
),
|
|
588
|
+
...devinGpt56Families("luna", "GPT-5.6 Luna"),
|
|
589
|
+
...devinGpt56Families("sol", "GPT-5.6 Sol"),
|
|
590
|
+
...devinGpt56Families("terra", "GPT-5.6 Terra"),
|
|
591
|
+
tierFamily(
|
|
592
|
+
"kimi-k3",
|
|
593
|
+
"Kimi K3",
|
|
594
|
+
{
|
|
595
|
+
low: "kimi-k3-low",
|
|
596
|
+
high: "kimi-k3-high",
|
|
597
|
+
max: "kimi-k3-max",
|
|
598
|
+
},
|
|
599
|
+
[Effort.Low, Effort.High, Effort.Max],
|
|
600
|
+
),
|
|
601
|
+
tierFamily(
|
|
602
|
+
"swe-1-7",
|
|
603
|
+
"SWE-1.7",
|
|
604
|
+
{
|
|
605
|
+
medium: "swe-1-7-medium",
|
|
606
|
+
max: "swe-1-7",
|
|
607
|
+
},
|
|
608
|
+
[Effort.Medium, Effort.Max],
|
|
609
|
+
),
|
|
610
|
+
tierFamily(
|
|
611
|
+
"grok-4-5",
|
|
612
|
+
"Grok 4.5",
|
|
613
|
+
{
|
|
614
|
+
low: "grok-4-5-low",
|
|
615
|
+
medium: "grok-4-5-medium",
|
|
616
|
+
high: "grok-4-5-high",
|
|
617
|
+
},
|
|
618
|
+
[Effort.Low, Effort.Medium, Effort.High],
|
|
619
|
+
),
|
|
620
|
+
tierFamily(
|
|
621
|
+
"inkling",
|
|
622
|
+
"Inkling",
|
|
623
|
+
{
|
|
624
|
+
off: "inkling-none",
|
|
625
|
+
low: "inkling-low",
|
|
626
|
+
medium: "inkling-medium",
|
|
627
|
+
high: "inkling-high",
|
|
628
|
+
xhigh: "inkling-xhigh",
|
|
629
|
+
max: "inkling-max",
|
|
630
|
+
},
|
|
631
|
+
DEVIN_FIVE_TIER_EFFORTS,
|
|
632
|
+
),
|
|
633
|
+
tierFamily(
|
|
634
|
+
"gemini-3-1-pro",
|
|
635
|
+
"Gemini 3.1 Pro",
|
|
636
|
+
{
|
|
637
|
+
low: "gemini-3-1-pro-low",
|
|
638
|
+
high: "gemini-3-1-pro-high",
|
|
639
|
+
},
|
|
640
|
+
[Effort.Low, Effort.High],
|
|
641
|
+
),
|
|
642
|
+
tierFamily(
|
|
643
|
+
"gemini-3-5-flash",
|
|
644
|
+
"Gemini 3.5 Flash",
|
|
645
|
+
{
|
|
646
|
+
minimal: "gemini-3-5-flash-minimal",
|
|
647
|
+
low: "gemini-3-5-flash-low",
|
|
648
|
+
medium: "gemini-3-5-flash-medium",
|
|
649
|
+
high: "gemini-3-5-flash-high",
|
|
650
|
+
},
|
|
651
|
+
[Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
|
|
652
|
+
),
|
|
653
|
+
tierFamily(
|
|
654
|
+
"gemini-3-6-flash",
|
|
655
|
+
"Gemini 3.6 Flash",
|
|
656
|
+
{
|
|
657
|
+
minimal: "gemini-3-6-flash-minimal",
|
|
658
|
+
low: "gemini-3-6-flash-low",
|
|
659
|
+
medium: "gemini-3-6-flash-medium",
|
|
660
|
+
high: "gemini-3-6-flash-high",
|
|
661
|
+
},
|
|
662
|
+
[Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
|
|
663
|
+
),
|
|
664
|
+
tierFamily(
|
|
665
|
+
"gemini-3-flash",
|
|
666
|
+
"Gemini 3 Flash",
|
|
667
|
+
{
|
|
668
|
+
minimal: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL",
|
|
669
|
+
low: "MODEL_GOOGLE_GEMINI_3_0_FLASH_LOW",
|
|
670
|
+
medium: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM",
|
|
671
|
+
high: "MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH",
|
|
672
|
+
},
|
|
673
|
+
[Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
|
|
674
|
+
),
|
|
675
|
+
// GLM-5.2 200K — only the base wire UID `glm-5-2` is free on Devin's
|
|
676
|
+
// Coding Plan (verified via streamDevin: `glm-5-2-none` and `glm-5-2-max`
|
|
677
|
+
// both return "weekly usage quota exhausted" while `glm-5-2` streams
|
|
678
|
+
// successfully). Route every effort to `glm-5-2` so the collapsed entry
|
|
679
|
+
// is always free; include the paid 200K variants as members so they are
|
|
680
|
+
// hidden from the model list. The 1M-context variants stay as separate
|
|
681
|
+
// paid entries (collapsed below).
|
|
682
|
+
{
|
|
683
|
+
id: "glm-5-2",
|
|
684
|
+
name: "GLM-5.2",
|
|
685
|
+
members: ["glm-5-2", "glm-5-2-none", "glm-5-2-max"],
|
|
686
|
+
routing: {
|
|
687
|
+
[Effort.High]: "glm-5-2",
|
|
688
|
+
[Effort.XHigh]: "glm-5-2",
|
|
689
|
+
},
|
|
690
|
+
thinking: {
|
|
691
|
+
mode: "effort",
|
|
692
|
+
efforts: [Effort.High, Effort.XHigh],
|
|
693
|
+
requiresEffort: true,
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
// GLM-5.2 1M — paid variants that consume weekly quota. Collapse the
|
|
697
|
+
// three 1M-context variants into one entry with proper effort routing.
|
|
698
|
+
tierFamily(
|
|
699
|
+
"glm-5-2-1m",
|
|
700
|
+
"GLM-5.2 1M",
|
|
701
|
+
{
|
|
702
|
+
off: "glm-5-2-none-1m",
|
|
703
|
+
high: "glm-5-2-1m",
|
|
704
|
+
xhigh: "glm-5-2-max-1m",
|
|
705
|
+
},
|
|
706
|
+
[Effort.High, Effort.XHigh],
|
|
707
|
+
),
|
|
708
|
+
],
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
/** Cursor's Grok tier tokens equal the effort id, so routing is a direct map. */
|
|
712
|
+
const CURSOR_GROK_45_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
|
|
713
|
+
const CURSOR_GROK_46_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
|
|
714
|
+
/** Cursor GPT-5.6 (Luna/Sol/Terra) serves the full five-tier `low..max` scale. */
|
|
715
|
+
const CURSOR_GPT_56_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Cursor serves Grok 4.5/4.6 as per-effort sibling ids
|
|
719
|
+
* (`cursor-grok-4.6-low|-medium|-high|-xhigh`) alongside a parallel `-fast`
|
|
720
|
+
* service-tier lane (`-low-fast`, …). Each lane collapses into its own logical
|
|
721
|
+
* model — the `-fast` axis is a sibling family, never a second routing
|
|
722
|
+
* dimension — with effort routing onto the live sibling wire ids.
|
|
723
|
+
*/
|
|
724
|
+
function cursorGrokFamilies(version: "4.5" | "4.6", efforts: readonly Effort[]): readonly EffortVariantFamily[] {
|
|
725
|
+
const build = (fast: boolean): EffortVariantFamily => {
|
|
726
|
+
const suffix = fast ? "-fast" : "";
|
|
727
|
+
const routes: TierRoutes = {};
|
|
728
|
+
for (const effort of efforts) {
|
|
729
|
+
routes[effort] = `cursor-grok-${version}-${effort}${suffix}`;
|
|
730
|
+
}
|
|
731
|
+
return tierFamily(`cursor-grok-${version}${suffix}`, `Grok ${version}${fast ? " Fast" : ""}`, routes, efforts);
|
|
732
|
+
};
|
|
733
|
+
return [build(false), build(true)];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Cursor serves GPT-5.6 (Luna/Sol/Terra) as per-tier sibling ids
|
|
738
|
+
* (`gpt-5.6-luna-none|-low|-medium|-high|-xhigh|-max`) with a parallel `-fast`
|
|
739
|
+
* service-tier lane (`gpt-5.6-luna-high-fast`, …). Same shape as Devin's
|
|
740
|
+
* `devinGpt56Families`, but cursor keys the version with a dot, marks the
|
|
741
|
+
* thinking-off tier `-none`, and names the fast lane with `-fast` (Devin uses
|
|
742
|
+
* `-priority`). Each lane collapses into its own logical model — `-fast` is a
|
|
743
|
+
* sibling SKU, never a second routing dimension — while the 1M / Max Mode SKU
|
|
744
|
+
* stays a separate row (handled by discovery's context-window resolution).
|
|
745
|
+
*/
|
|
746
|
+
function cursorGpt56Families(variant: "luna" | "sol" | "terra", name: string): readonly EffortVariantFamily[] {
|
|
747
|
+
const build = (fast: boolean): EffortVariantFamily => {
|
|
748
|
+
const suffix = fast ? "-fast" : "";
|
|
749
|
+
const base = `gpt-5.6-${variant}`;
|
|
750
|
+
return tierFamily(
|
|
751
|
+
`${base}${suffix}`,
|
|
752
|
+
`${name}${fast ? " Fast" : ""}`,
|
|
753
|
+
{
|
|
754
|
+
off: `${base}-none${suffix}`,
|
|
755
|
+
low: `${base}-low${suffix}`,
|
|
756
|
+
medium: `${base}-medium${suffix}`,
|
|
757
|
+
high: `${base}-high${suffix}`,
|
|
758
|
+
xhigh: `${base}-xhigh${suffix}`,
|
|
759
|
+
max: `${base}-max${suffix}`,
|
|
760
|
+
},
|
|
761
|
+
CURSOR_GPT_56_EFFORTS,
|
|
762
|
+
);
|
|
763
|
+
};
|
|
764
|
+
return [build(false), build(true)];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** `cursor` per-effort sibling families collapsed per service-tier lane: Grok 4.5/4.6 plus GPT-5.6 Luna/Sol/Terra. */
|
|
768
|
+
export const CURSOR_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
|
|
769
|
+
families: [
|
|
770
|
+
...cursorGrokFamilies("4.5", CURSOR_GROK_45_EFFORTS),
|
|
771
|
+
...cursorGrokFamilies("4.6", CURSOR_GROK_46_EFFORTS),
|
|
772
|
+
...cursorGpt56Families("luna", "GPT-5.6 Luna"),
|
|
773
|
+
...cursorGpt56Families("sol", "GPT-5.6 Sol"),
|
|
774
|
+
...cursorGpt56Families("terra", "GPT-5.6 Terra"),
|
|
775
|
+
],
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
type CursorTierToken = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
779
|
+
|
|
780
|
+
interface CursorTierMember<TSpec extends VariantSpecLike> {
|
|
781
|
+
baseId: string;
|
|
782
|
+
fast: boolean;
|
|
783
|
+
spec: TSpec;
|
|
784
|
+
tier: CursorTierToken;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const CURSOR_TIER_ID_PATTERN = /^(.+?)-(extra-high|none|minimal|low|medium|high|xhigh|max)(-fast)?$/;
|
|
788
|
+
const CURSOR_TIER_BASE_PATTERN = /-(extra-high|none|minimal|low|medium|high|xhigh|max)$/;
|
|
789
|
+
const CURSOR_THINKING_TOKEN_PATTERN = /(^|-)thinking($|-)/;
|
|
790
|
+
const CURSOR_TIER_BY_TOKEN: Readonly<Record<string, CursorTierToken | undefined>> = {
|
|
791
|
+
none: "none",
|
|
792
|
+
minimal: "minimal",
|
|
793
|
+
low: "low",
|
|
794
|
+
medium: "medium",
|
|
795
|
+
high: "high",
|
|
796
|
+
"extra-high": "xhigh",
|
|
797
|
+
xhigh: "xhigh",
|
|
798
|
+
max: "max",
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
/** Whether an existing logical row already routes every live member in `members`. */
|
|
802
|
+
function collapsedCursorLogicalMatches<TSpec extends VariantSpecLike>(
|
|
803
|
+
spec: TSpec,
|
|
804
|
+
members: readonly CursorTierMember<TSpec>[],
|
|
805
|
+
): boolean {
|
|
806
|
+
const routing = spec.thinking?.effortRouting;
|
|
807
|
+
if (!routing) return false;
|
|
808
|
+
for (const member of members) {
|
|
809
|
+
if (spec.requestModelId === member.spec.id) continue;
|
|
810
|
+
let matched = false;
|
|
811
|
+
for (const effort of VARIANT_ROUTING_KEYS) {
|
|
812
|
+
if (routing[effort] === member.spec.id) {
|
|
813
|
+
matched = true;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (!matched) return false;
|
|
818
|
+
}
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Derive safe Cursor per-effort families from live wire ids. A family is
|
|
824
|
+
* intentionally left expanded when its base is an independent live SKU, any
|
|
825
|
+
* member already has a thinking ladder, member metadata differs, or a tier
|
|
826
|
+
* token is also part of a product name.
|
|
827
|
+
*/
|
|
828
|
+
function deriveCursorEffortFamilies<TSpec extends VariantSpecLike>(specs: readonly TSpec[]): EffortVariantFamily[] {
|
|
829
|
+
const byId = new Map<string, TSpec>();
|
|
830
|
+
const groups = new Map<string, CursorTierMember<TSpec>[]>();
|
|
831
|
+
const candidateBases = new Set<string>();
|
|
832
|
+
|
|
833
|
+
for (const spec of specs) {
|
|
834
|
+
if (!byId.has(spec.id)) byId.set(spec.id, spec);
|
|
835
|
+
const match = CURSOR_TIER_ID_PATTERN.exec(spec.id);
|
|
836
|
+
if (!match) continue;
|
|
837
|
+
const baseId = match[1];
|
|
838
|
+
const tier = CURSOR_TIER_BY_TOKEN[match[2] ?? ""];
|
|
839
|
+
const fast = match[3] !== undefined;
|
|
840
|
+
if (!baseId || !tier) continue;
|
|
841
|
+
const member = { baseId, fast, spec, tier };
|
|
842
|
+
const key = `${baseId}\0${fast ? "fast" : "standard"}`;
|
|
843
|
+
const group = groups.get(key);
|
|
844
|
+
if (group) {
|
|
845
|
+
group.push(member);
|
|
846
|
+
} else {
|
|
847
|
+
groups.set(key, [member]);
|
|
848
|
+
}
|
|
849
|
+
candidateBases.add(baseId);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const unsafeBases = new Set<string>();
|
|
853
|
+
for (const group of groups.values()) {
|
|
854
|
+
const first = group[0];
|
|
855
|
+
if (!first) continue;
|
|
856
|
+
const { baseId } = first;
|
|
857
|
+
const standardGroup = groups.get(`${baseId}\0standard`) ?? [];
|
|
858
|
+
const standardBase = byId.get(baseId);
|
|
859
|
+
const laneBase = byId.get(`${baseId}${first.fast ? "-fast" : ""}`);
|
|
860
|
+
const independentStandardBase =
|
|
861
|
+
standardBase !== undefined && !collapsedCursorLogicalMatches(standardBase, standardGroup);
|
|
862
|
+
const independentLaneBase = laneBase !== undefined && !collapsedCursorLogicalMatches(laneBase, group);
|
|
863
|
+
if (
|
|
864
|
+
independentStandardBase ||
|
|
865
|
+
independentLaneBase ||
|
|
866
|
+
new Set(group.map(member => member.tier)).size !== group.length ||
|
|
867
|
+
CURSOR_TIER_BASE_PATTERN.test(baseId) ||
|
|
868
|
+
CURSOR_THINKING_TOKEN_PATTERN.test(baseId) ||
|
|
869
|
+
candidateBases.has(`${baseId}-thinking`) ||
|
|
870
|
+
byId.has(`${baseId}-thinking`) ||
|
|
871
|
+
byId.has(`${baseId}-thinking-fast`) ||
|
|
872
|
+
byId.has(`${baseId}-fast-thinking`) ||
|
|
873
|
+
group.some(member => member.spec.thinking !== undefined || member.spec.requestModelId !== undefined) ||
|
|
874
|
+
group.some(member => candidateBases.has(`${baseId}-${member.tier}`))
|
|
875
|
+
) {
|
|
876
|
+
unsafeBases.add(baseId);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const families: EffortVariantFamily[] = [];
|
|
881
|
+
for (const group of groups.values()) {
|
|
882
|
+
const first = group[0];
|
|
883
|
+
if (!first || group.length < 2 || unsafeBases.has(first.baseId)) continue;
|
|
884
|
+
if (
|
|
885
|
+
group.some(
|
|
886
|
+
member =>
|
|
887
|
+
member.spec.api !== first.spec.api ||
|
|
888
|
+
member.spec.baseUrl !== first.spec.baseUrl ||
|
|
889
|
+
member.spec.contextWindow !== first.spec.contextWindow ||
|
|
890
|
+
member.spec.maxTokens !== first.spec.maxTokens ||
|
|
891
|
+
member.spec.cursorMaxMode !== first.spec.cursorMaxMode ||
|
|
892
|
+
!Bun.deepEquals(member.spec.cost, first.spec.cost) ||
|
|
893
|
+
!Bun.deepEquals(member.spec.compat, first.spec.compat),
|
|
894
|
+
)
|
|
895
|
+
) {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const routes: TierRoutes = {};
|
|
900
|
+
for (const member of group) {
|
|
901
|
+
if (member.tier === "none") {
|
|
902
|
+
routes.off = member.spec.id;
|
|
903
|
+
} else {
|
|
904
|
+
routes[member.tier] = member.spec.id;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const efforts = THINKING_EFFORTS.filter(effort => routes[effort] !== undefined);
|
|
908
|
+
if (efforts.length === 0) continue;
|
|
909
|
+
const strippedName = first.spec.name
|
|
910
|
+
.replace(/\s+(extra-high|none|minimal|low|medium|high|xhigh|max)(\s+fast)?$/i, "")
|
|
911
|
+
.trim();
|
|
912
|
+
const baseName = strippedName === first.spec.id ? first.baseId : strippedName || first.baseId;
|
|
913
|
+
const suffix = first.fast ? "-fast" : "";
|
|
914
|
+
families.push(tierFamily(`${first.baseId}${suffix}`, `${baseName}${first.fast ? " Fast" : ""}`, routes, efforts));
|
|
915
|
+
}
|
|
916
|
+
return families;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Provider id → hand collapse table. The CCA providers diverge on thinking transport. */
|
|
920
|
+
export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTable>> = {
|
|
921
|
+
"google-antigravity": ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
|
|
922
|
+
"google-gemini-cli": GEMINI_CLI_VARIANT_COLLAPSE_TABLE,
|
|
923
|
+
devin: DEVIN_VARIANT_COLLAPSE_TABLE,
|
|
924
|
+
cursor: CURSOR_VARIANT_COLLAPSE_TABLE,
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* The global automatic rule: derive an `X` + `X-thinking` family for every
|
|
929
|
+
* pair where both ids are live in `specs` (trailing or infix token). Gates:
|
|
930
|
+
* - both members share the same `api`,
|
|
931
|
+
* - known pricing must match — all-zero cost rows count as unknown
|
|
932
|
+
* (aggregators routinely ship them), but twins that BOTH carry real,
|
|
933
|
+
* differing prices are distinct SKUs and never merge,
|
|
934
|
+
* - ids claimed by the provider's hand `table` are skipped (curation wins).
|
|
935
|
+
* The capability surface prefers the thinking member's metadata, then the
|
|
936
|
+
* bare member's, then the canonical deriver (aggregators often ship
|
|
937
|
+
* `reasoning: false` and no thinking config on the twin), then a budget
|
|
938
|
+
* default. `off` routes to the bare id; every supported effort routes to the
|
|
939
|
+
* thinking id.
|
|
940
|
+
*/
|
|
941
|
+
export function deriveThinkingPairFamilies<TSpec extends VariantSpecLike>(
|
|
942
|
+
specs: readonly TSpec[],
|
|
943
|
+
table?: VariantCollapseTable,
|
|
944
|
+
): EffortVariantFamily[] {
|
|
945
|
+
const byId = new Map<string, TSpec>();
|
|
946
|
+
for (const spec of specs) {
|
|
947
|
+
if (!byId.has(spec.id)) byId.set(spec.id, spec);
|
|
948
|
+
}
|
|
949
|
+
const claimed = table ? getAliasIndex(table) : undefined;
|
|
950
|
+
const families: EffortVariantFamily[] = [];
|
|
951
|
+
for (const spec of specs) {
|
|
952
|
+
const baseId = stripThinkingVariantToken(spec.id);
|
|
953
|
+
if (baseId === undefined || baseId === spec.id) continue;
|
|
954
|
+
const base = byId.get(baseId);
|
|
955
|
+
if (!base) continue;
|
|
956
|
+
if (claimed) {
|
|
957
|
+
const forward = claimed.forward;
|
|
958
|
+
if (
|
|
959
|
+
forward.has(spec.id.toLowerCase()) ||
|
|
960
|
+
forward.has(baseId.toLowerCase()) ||
|
|
961
|
+
claimed.familyIds.has(spec.id) ||
|
|
962
|
+
claimed.familyIds.has(baseId)
|
|
963
|
+
) {
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (spec.api !== base.api) continue;
|
|
968
|
+
const specPriced = spec.cost.input !== 0 || spec.cost.output !== 0;
|
|
969
|
+
const basePriced = base.cost.input !== 0 || base.cost.output !== 0;
|
|
970
|
+
if (
|
|
971
|
+
specPriced &&
|
|
972
|
+
basePriced &&
|
|
973
|
+
(spec.cost.input !== base.cost.input ||
|
|
974
|
+
spec.cost.output !== base.cost.output ||
|
|
975
|
+
spec.cost.cacheRead !== base.cost.cacheRead ||
|
|
976
|
+
spec.cost.cacheWrite !== base.cost.cacheWrite)
|
|
977
|
+
) {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const surface = derivePairThinkingSurface(spec, base);
|
|
981
|
+
const routing: Partial<Record<Effort | "off", string>> = { off: base.id };
|
|
982
|
+
for (const effort of surface.efforts) {
|
|
983
|
+
routing[effort] = spec.id;
|
|
984
|
+
}
|
|
985
|
+
families.push({
|
|
986
|
+
id: base.id,
|
|
987
|
+
name: base.name,
|
|
988
|
+
members: [base.id, spec.id],
|
|
989
|
+
routing,
|
|
990
|
+
thinking: surface,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
return families;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const DEFAULT_PAIR_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Surface fallback chain: thinking member → bare member → canonical deriver →
|
|
1000
|
+
* budget default. `requiresEffort` is dropped from every source: the COLLAPSED
|
|
1001
|
+
* pair can disable thinking (off routes to the bare backing id), even though
|
|
1002
|
+
* the thinking member alone cannot.
|
|
1003
|
+
*/
|
|
1004
|
+
function derivePairThinkingSurface(
|
|
1005
|
+
thinkingSpec: VariantSpecLike,
|
|
1006
|
+
baseSpec: VariantSpecLike,
|
|
1007
|
+
): Omit<ThinkingConfig, "effortRouting" | "suppressWhenOff" | "requiresEffort"> {
|
|
1008
|
+
const baked = thinkingSpec.thinking ?? baseSpec.thinking;
|
|
1009
|
+
if (baked && baked.efforts.length > 0) {
|
|
1010
|
+
const { effortRouting: _routing, suppressWhenOff: _suppress, requiresEffort: _required, ...surface } = baked;
|
|
1011
|
+
return surface;
|
|
1012
|
+
}
|
|
1013
|
+
const derived = resolveModelThinking(
|
|
1014
|
+
{ ...(thinkingSpec as unknown as ModelSpec<Api>), reasoning: true, thinking: undefined },
|
|
1015
|
+
buildCompat(thinkingSpec as unknown as ModelSpec<Api>),
|
|
1016
|
+
);
|
|
1017
|
+
if (derived && derived.efforts.length > 0) {
|
|
1018
|
+
const { effortRouting: _dRouting, suppressWhenOff: _dSuppress, requiresEffort: _dRequired, ...surface } = derived;
|
|
1019
|
+
return surface;
|
|
1020
|
+
}
|
|
1021
|
+
return { mode: "budget", efforts: DEFAULT_PAIR_EFFORTS };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* True when `spec` is the output of collapsing rather than a raw upstream
|
|
1026
|
+
* member. `thinking.effortRouting` is written only by collapsing; the
|
|
1027
|
+
* `requestModelId` arm is scoped to the provider's hand-table family ids so
|
|
1028
|
+
* unrelated carriers (GitHub Copilot `-1m` context variants) never match.
|
|
1029
|
+
*/
|
|
1030
|
+
export function isVariantCollapsedSpec(spec: VariantSpecLike): boolean {
|
|
1031
|
+
if (spec.thinking?.effortRouting !== undefined) {
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
if (spec.requestModelId === undefined) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
const table = VARIANT_COLLAPSE_TABLES[spec.provider];
|
|
1038
|
+
return table !== undefined && getAliasIndex(table).familyIds.has(spec.id);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Re-point a stale collapsed spec whose `requestModelId` or routing still
|
|
1043
|
+
* targets a retired wire id. Collapsed snapshots (bundled catalog, cache
|
|
1044
|
+
* rows, previous-generation fallbacks) pass through collapsing untouched, so
|
|
1045
|
+
* a hand-table routing fix would otherwise never reach them. Only retired
|
|
1046
|
+
* targets are rewritten — presence-filtered routing decisions from live
|
|
1047
|
+
* discovery stay authoritative for everything else. Per retired entry the
|
|
1048
|
+
* table's route for that effort wins, then the off/first-live-member wire id,
|
|
1049
|
+
* then the route is dropped (falls back to `requestModelId ?? id`). Returns
|
|
1050
|
+
* `spec` by reference when nothing targets a retired id.
|
|
1051
|
+
*/
|
|
1052
|
+
function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
|
|
1053
|
+
spec: TSpec,
|
|
1054
|
+
family: EffortVariantFamily,
|
|
1055
|
+
retired: ReadonlySet<string>,
|
|
1056
|
+
): TSpec {
|
|
1057
|
+
const routing = spec.thinking?.effortRouting;
|
|
1058
|
+
const requestRetired = spec.requestModelId !== undefined && retired.has(spec.requestModelId);
|
|
1059
|
+
let routingRetired = false;
|
|
1060
|
+
if (routing !== undefined) {
|
|
1061
|
+
for (const key in routing) {
|
|
1062
|
+
const target = routing[key as Effort | "off"];
|
|
1063
|
+
if (target !== undefined && retired.has(target)) {
|
|
1064
|
+
routingRetired = true;
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (!requestRetired && !routingRetired) return spec;
|
|
1070
|
+
|
|
1071
|
+
const offTarget = family.routing.off;
|
|
1072
|
+
const fallbackWireId =
|
|
1073
|
+
offTarget !== undefined && !retired.has(offTarget) ? offTarget : family.members.find(id => !retired.has(id));
|
|
1074
|
+
const next: TSpec = { ...spec };
|
|
1075
|
+
if (routingRetired && routing !== undefined) {
|
|
1076
|
+
const nextRouting: Partial<Record<Effort | "off", string>> = {};
|
|
1077
|
+
for (const key in routing) {
|
|
1078
|
+
const effortKey = key as Effort | "off";
|
|
1079
|
+
const target = routing[effortKey];
|
|
1080
|
+
if (target === undefined) continue;
|
|
1081
|
+
if (!retired.has(target)) {
|
|
1082
|
+
nextRouting[effortKey] = target;
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const tableTarget = family.routing[effortKey];
|
|
1086
|
+
if (tableTarget !== undefined && !retired.has(tableTarget)) {
|
|
1087
|
+
nextRouting[effortKey] = tableTarget;
|
|
1088
|
+
} else if (fallbackWireId !== undefined) {
|
|
1089
|
+
nextRouting[effortKey] = fallbackWireId;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
next.thinking = { ...(spec.thinking as ThinkingConfig), effortRouting: nextRouting };
|
|
1093
|
+
}
|
|
1094
|
+
if (requestRetired) {
|
|
1095
|
+
if (fallbackWireId !== undefined && fallbackWireId !== spec.id) {
|
|
1096
|
+
next.requestModelId = fallbackWireId;
|
|
1097
|
+
} else {
|
|
1098
|
+
delete next.requestModelId;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return next;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Refresh a collapsed snapshot's thinking surface in place. Bundled catalog and
|
|
1106
|
+
* prev-generation snapshots freeze a family's transport, budgets, and routing;
|
|
1107
|
+
* discovery emits the canonical id but the exact-id merge never overwrites a
|
|
1108
|
+
* stale `family.id` row (e.g. `gemini-3.1-pro`) nor a recycled `extraAliases`
|
|
1109
|
+
* row (e.g. `gemini-3-flash`). This re-applies the hand-table family's thinking,
|
|
1110
|
+
* routing, and default wire id while keeping the spec id (load-bearing for exact
|
|
1111
|
+
* selectors and bundled lookups). Returns `spec` by reference when unchanged.
|
|
1112
|
+
*/
|
|
1113
|
+
function refreshCollapsedThinking<TSpec extends VariantSpecLike>(
|
|
1114
|
+
spec: TSpec,
|
|
1115
|
+
family: EffortVariantFamily,
|
|
1116
|
+
retired: ReadonlySet<string> | undefined,
|
|
1117
|
+
): TSpec {
|
|
1118
|
+
// Scope snapshot self-heal to families carrying a curated per-effort budget
|
|
1119
|
+
// contract (Antigravity gemini-3.x). Their routing targets are all verified
|
|
1120
|
+
// live, so rebuilding routing here is safe; families without `effortBudgets`
|
|
1121
|
+
// (derived `X`/`X-thinking` pairs, claude pairs) keep their presence-filtered
|
|
1122
|
+
// snapshot routing untouched.
|
|
1123
|
+
if (!spec.reasoning || family.thinking.effortBudgets === undefined) return spec;
|
|
1124
|
+
const routing: Partial<Record<Effort | "off", string>> = {};
|
|
1125
|
+
let hasRouting = false;
|
|
1126
|
+
for (const effortKey in family.routing) {
|
|
1127
|
+
const target = family.routing[effortKey as Effort | "off"];
|
|
1128
|
+
if (target !== undefined && !retired?.has(target)) {
|
|
1129
|
+
routing[effortKey as Effort | "off"] = target;
|
|
1130
|
+
hasRouting = true;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const thinking: ThinkingConfig = { ...family.thinking };
|
|
1134
|
+
if (hasRouting) thinking.effortRouting = routing;
|
|
1135
|
+
if (family.suppressWhenOff) thinking.suppressWhenOff = true;
|
|
1136
|
+
const offTarget = family.routing.off;
|
|
1137
|
+
const requestModelId =
|
|
1138
|
+
offTarget !== undefined && !retired?.has(offTarget) && offTarget !== spec.id ? offTarget : spec.requestModelId;
|
|
1139
|
+
if (Bun.deepEquals(thinking, spec.thinking) && requestModelId === spec.requestModelId) {
|
|
1140
|
+
return spec;
|
|
1141
|
+
}
|
|
1142
|
+
return { ...spec, thinking, ...(requestModelId !== undefined ? { requestModelId } : {}) };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Collapse every family in `table` found in `specs`. Non-member specs pass
|
|
1147
|
+
* through verbatim (by reference), order preserved; the collapsed spec
|
|
1148
|
+
* replaces the first occurrence of its family.
|
|
1149
|
+
*/
|
|
1150
|
+
export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
1151
|
+
specs: readonly TSpec[],
|
|
1152
|
+
table: VariantCollapseTable,
|
|
1153
|
+
): TSpec[] {
|
|
1154
|
+
const byId = new Map<string, TSpec>();
|
|
1155
|
+
for (const spec of specs) {
|
|
1156
|
+
if (!byId.has(spec.id)) byId.set(spec.id, spec);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/** family id → spec to emit at the family's first occurrence. */
|
|
1160
|
+
const replacement = new Map<string, TSpec>();
|
|
1161
|
+
/** spec ids that belong to a touched family (members + logical id). */
|
|
1162
|
+
const familyIdBySpecId = new Map<string, string>();
|
|
1163
|
+
|
|
1164
|
+
for (const family of table.families) {
|
|
1165
|
+
const retired =
|
|
1166
|
+
family.retiredMembers !== undefined && family.retiredMembers.length > 0
|
|
1167
|
+
? new Set(family.retiredMembers)
|
|
1168
|
+
: undefined;
|
|
1169
|
+
const existing = byId.get(family.id);
|
|
1170
|
+
const existingCollapsed =
|
|
1171
|
+
existing !== undefined &&
|
|
1172
|
+
(existing.requestModelId !== undefined || existing.thinking?.effortRouting !== undefined);
|
|
1173
|
+
const reconciled =
|
|
1174
|
+
existing !== undefined && existingCollapsed && retired !== undefined
|
|
1175
|
+
? reconcileRetiredRouting(existing, family, retired)
|
|
1176
|
+
: existing;
|
|
1177
|
+
const rawPresent = family.members.filter(id => byId.has(id) && !(id === family.id && existingCollapsed));
|
|
1178
|
+
if (rawPresent.length === 0) {
|
|
1179
|
+
// Inert (no members) or already collapsed (pass-through). A stale
|
|
1180
|
+
// family.id-keyed snapshot is refreshed in place from the current
|
|
1181
|
+
// hand-table family (transport/budgets/routing); retired targets drop.
|
|
1182
|
+
// Recycled extraAliases rows are healed in a later pass.
|
|
1183
|
+
const refreshed =
|
|
1184
|
+
existing !== undefined && existingCollapsed
|
|
1185
|
+
? refreshCollapsedThinking(reconciled ?? existing, family, retired)
|
|
1186
|
+
: reconciled;
|
|
1187
|
+
if (refreshed !== undefined && refreshed !== existing) {
|
|
1188
|
+
familyIdBySpecId.set(family.id, family.id);
|
|
1189
|
+
replacement.set(family.id, refreshed);
|
|
1190
|
+
}
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
for (const id of rawPresent) familyIdBySpecId.set(id, family.id);
|
|
1195
|
+
if (existing) familyIdBySpecId.set(family.id, family.id);
|
|
1196
|
+
|
|
1197
|
+
if (existingCollapsed) {
|
|
1198
|
+
// Mixed input: the collapsed entry (live truth) wins; stale raw
|
|
1199
|
+
// members are deduped away. Retired targets are re-pointed first.
|
|
1200
|
+
replacement.set(family.id, reconciled as TSpec);
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const memberSpecs = rawPresent.map(id => byId.get(id) as TSpec);
|
|
1205
|
+
const presentSet = new Set(rawPresent);
|
|
1206
|
+
const routing: Partial<Record<Effort | "off", string>> = {};
|
|
1207
|
+
let hasRouting = false;
|
|
1208
|
+
let hasEffortRoute = false;
|
|
1209
|
+
let usedAbsentEffortRoute = false;
|
|
1210
|
+
for (const effortKey in family.routing) {
|
|
1211
|
+
const target = family.routing[effortKey as Effort | "off"];
|
|
1212
|
+
const effort = effortKey as Effort | "off";
|
|
1213
|
+
const targetPresent = target !== undefined && presentSet.has(target);
|
|
1214
|
+
const preserveAbsentEffort =
|
|
1215
|
+
target !== undefined && effort !== "off" && family.preserveAbsentEffortRoutes === true;
|
|
1216
|
+
if (target !== undefined && (targetPresent || preserveAbsentEffort) && !retired?.has(target)) {
|
|
1217
|
+
routing[effort] = target;
|
|
1218
|
+
hasRouting = true;
|
|
1219
|
+
if (effortKey !== "off") hasEffortRoute = true;
|
|
1220
|
+
if (!targetPresent && effort !== "off") usedAbsentEffortRoute = true;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// A family that routes efforts to a live thinking backing id reasons
|
|
1225
|
+
// even when upstream metadata forgot to mark the members.
|
|
1226
|
+
const reasoning = memberSpecs.some(spec => spec.reasoning) || hasEffortRoute;
|
|
1227
|
+
const thinking: ThinkingConfig = { ...family.thinking };
|
|
1228
|
+
if (hasRouting) thinking.effortRouting = routing;
|
|
1229
|
+
if (family.suppressWhenOff) thinking.suppressWhenOff = true;
|
|
1230
|
+
|
|
1231
|
+
const input: ("text" | "image")[] = [];
|
|
1232
|
+
if (memberSpecs.some(spec => spec.input.includes("text"))) input.push("text");
|
|
1233
|
+
if (memberSpecs.some(spec => spec.input.includes("image"))) input.push("image");
|
|
1234
|
+
|
|
1235
|
+
const collapsed: TSpec = {
|
|
1236
|
+
...(memberSpecs[0] as TSpec),
|
|
1237
|
+
id: family.id,
|
|
1238
|
+
name: family.name,
|
|
1239
|
+
reasoning,
|
|
1240
|
+
input,
|
|
1241
|
+
contextWindow: maxOrNull(memberSpecs.map(spec => spec.contextWindow)),
|
|
1242
|
+
maxTokens: maxOrNull(memberSpecs.map(spec => spec.maxTokens)),
|
|
1243
|
+
};
|
|
1244
|
+
// The default wire id is the highest-priority live member; omit when it
|
|
1245
|
+
// equals the logical id (bare/thinking pairs) — `resolveWireModelId`
|
|
1246
|
+
// falls back. Retired members never become the default.
|
|
1247
|
+
const defaultWireId = rawPresent.find(id => !retired?.has(id)) ?? rawPresent[0];
|
|
1248
|
+
if (defaultWireId === family.id) {
|
|
1249
|
+
if (usedAbsentEffortRoute) {
|
|
1250
|
+
collapsed.requestModelId = defaultWireId as string;
|
|
1251
|
+
} else {
|
|
1252
|
+
delete collapsed.requestModelId;
|
|
1253
|
+
}
|
|
1254
|
+
} else {
|
|
1255
|
+
collapsed.requestModelId = defaultWireId as string;
|
|
1256
|
+
}
|
|
1257
|
+
if (reasoning) {
|
|
1258
|
+
collapsed.thinking = thinking;
|
|
1259
|
+
} else {
|
|
1260
|
+
delete collapsed.thinking;
|
|
1261
|
+
}
|
|
1262
|
+
replacement.set(family.id, collapsed);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Refresh stale alias-keyed snapshots in place (recycled bare ids). Runs even
|
|
1266
|
+
// when the canonical family.id row is also present, since the exact-id merge
|
|
1267
|
+
// keeps the stale alias row alongside the discovered canonical one.
|
|
1268
|
+
for (const family of table.families) {
|
|
1269
|
+
if (family.extraAliases === undefined) continue;
|
|
1270
|
+
const retired =
|
|
1271
|
+
family.retiredMembers !== undefined && family.retiredMembers.length > 0
|
|
1272
|
+
? new Set(family.retiredMembers)
|
|
1273
|
+
: undefined;
|
|
1274
|
+
for (const alias of family.extraAliases) {
|
|
1275
|
+
if (alias === family.id || familyIdBySpecId.has(alias)) continue;
|
|
1276
|
+
const aliasSpec = byId.get(alias);
|
|
1277
|
+
if (aliasSpec === undefined) continue;
|
|
1278
|
+
const refreshed = refreshCollapsedThinking(aliasSpec, family, retired);
|
|
1279
|
+
if (refreshed !== aliasSpec) {
|
|
1280
|
+
familyIdBySpecId.set(alias, alias);
|
|
1281
|
+
replacement.set(alias, refreshed);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (replacement.size === 0) return [...specs];
|
|
1287
|
+
|
|
1288
|
+
const emitted = new Set<string>();
|
|
1289
|
+
const out: TSpec[] = [];
|
|
1290
|
+
for (const spec of specs) {
|
|
1291
|
+
const familyId = familyIdBySpecId.get(spec.id);
|
|
1292
|
+
if (familyId === undefined) {
|
|
1293
|
+
out.push(spec);
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (emitted.has(familyId)) continue;
|
|
1297
|
+
emitted.add(familyId);
|
|
1298
|
+
out.push(replacement.get(familyId) as TSpec);
|
|
1299
|
+
}
|
|
1300
|
+
return out;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Re-key model-to-model configuration after collapse removes a referenced
|
|
1305
|
+
* member id. Qualified targets keep their provider; bare targets remain bare.
|
|
1306
|
+
*/
|
|
1307
|
+
function retargetCollapsedModelReferences<TSpec extends VariantSpecLike>(specs: TSpec[]): void {
|
|
1308
|
+
const liveIdsByProvider = new Map<string, Set<string>>();
|
|
1309
|
+
for (const spec of specs) {
|
|
1310
|
+
const provider = spec.provider.toLowerCase();
|
|
1311
|
+
let liveIds = liveIdsByProvider.get(provider);
|
|
1312
|
+
if (!liveIds) {
|
|
1313
|
+
liveIds = new Set<string>();
|
|
1314
|
+
liveIdsByProvider.set(provider, liveIds);
|
|
1315
|
+
}
|
|
1316
|
+
liveIds.add(spec.id.toLowerCase());
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
for (let index = 0; index < specs.length; index++) {
|
|
1320
|
+
const spec = specs[index];
|
|
1321
|
+
if (!spec) continue;
|
|
1322
|
+
const contextPromotionTarget = resolveCollapsedModelReference(
|
|
1323
|
+
spec.contextPromotionTarget,
|
|
1324
|
+
spec.provider,
|
|
1325
|
+
liveIdsByProvider,
|
|
1326
|
+
);
|
|
1327
|
+
const compactionModel = resolveCollapsedModelReference(spec.compactionModel, spec.provider, liveIdsByProvider);
|
|
1328
|
+
if (contextPromotionTarget === spec.contextPromotionTarget && compactionModel === spec.compactionModel) continue;
|
|
1329
|
+
specs[index] = { ...spec, contextPromotionTarget, compactionModel };
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function resolveCollapsedModelReference(
|
|
1334
|
+
target: string | undefined,
|
|
1335
|
+
currentProvider: Provider,
|
|
1336
|
+
liveIdsByProvider: ReadonlyMap<string, ReadonlySet<string>>,
|
|
1337
|
+
): string | undefined {
|
|
1338
|
+
if (target === undefined) return undefined;
|
|
1339
|
+
const separator = target.indexOf("/");
|
|
1340
|
+
const provider = separator >= 0 ? target.slice(0, separator) : currentProvider;
|
|
1341
|
+
const providerId = provider.toLowerCase();
|
|
1342
|
+
const modelId = separator >= 0 ? target.slice(separator + 1) : target;
|
|
1343
|
+
const normalizedModelId = modelId.trim().toLowerCase();
|
|
1344
|
+
const liveIds = liveIdsByProvider.get(providerId);
|
|
1345
|
+
if (liveIds?.has(normalizedModelId)) return target;
|
|
1346
|
+
const alias = resolveRegisteredVariantAlias(provider, normalizedModelId);
|
|
1347
|
+
if (alias === undefined || !liveIds?.has(alias.toLowerCase())) return target;
|
|
1348
|
+
return separator >= 0 ? `${provider}/${alias}` : alias;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Collapse a full mixed-provider list: per provider, the hand table, Cursor's
|
|
1353
|
+
* conservative live effort-sibling rule, and the automatic `X`/`X-thinking`
|
|
1354
|
+
* pair rule. Used by the catalog generator; the runtime equivalent lives at
|
|
1355
|
+
* the model-manager merge point. Output is regrouped by provider — callers
|
|
1356
|
+
* re-sort.
|
|
1357
|
+
*/
|
|
1358
|
+
export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecLike>(specs: readonly TSpec[]): TSpec[] {
|
|
1359
|
+
const byProvider = new Map<string, TSpec[]>();
|
|
1360
|
+
for (const spec of specs) {
|
|
1361
|
+
const slice = byProvider.get(spec.provider);
|
|
1362
|
+
if (slice) {
|
|
1363
|
+
slice.push(spec);
|
|
1364
|
+
} else {
|
|
1365
|
+
byProvider.set(spec.provider, [spec]);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const out: TSpec[] = [];
|
|
1369
|
+
for (const [provider, slice] of byProvider) {
|
|
1370
|
+
const table = VARIANT_COLLAPSE_TABLES[provider];
|
|
1371
|
+
let result = table ? collapseEffortVariants(slice, table) : slice;
|
|
1372
|
+
if (provider === "cursor") {
|
|
1373
|
+
const cursorDerived = deriveCursorEffortFamilies(result);
|
|
1374
|
+
if (cursorDerived.length > 0) {
|
|
1375
|
+
result = collapseEffortVariants(result, { families: cursorDerived });
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const derived = deriveThinkingPairFamilies(result, table);
|
|
1379
|
+
if (derived.length > 0) {
|
|
1380
|
+
result = collapseEffortVariants(result, { families: derived });
|
|
1381
|
+
}
|
|
1382
|
+
registerCollapsedVariantAliases(provider, result);
|
|
1383
|
+
out.push(...result);
|
|
1384
|
+
}
|
|
1385
|
+
retargetCollapsedModelReferences(out);
|
|
1386
|
+
return out;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Runtime entry point for already-built `Model` lists (the model-manager
|
|
1391
|
+
* merge point, coding-agent registry custom providers): collapses hand
|
|
1392
|
+
* tables plus derived pairs, then re-runs `buildModel` on freshly created
|
|
1393
|
+
* logical specs so thinking wire defaults stay resolved. Untouched entries
|
|
1394
|
+
* pass through by reference.
|
|
1395
|
+
*/
|
|
1396
|
+
export function collapseBuiltModelVariants<TApi extends Api>(models: readonly Model<TApi>[]): Model<TApi>[] {
|
|
1397
|
+
const collapsed = collapseEffortVariantsAcrossProviders(models);
|
|
1398
|
+
const inputRefs = new Set<Model<TApi>>(models);
|
|
1399
|
+
return collapsed.map(model =>
|
|
1400
|
+
// Rebuild from a projected spec (sparse compatConfig) instead of resolved compat.
|
|
1401
|
+
inputRefs.has(model) ? model : buildModel({ ...model, compat: model.compatConfig } as unknown as ModelSpec<TApi>),
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
interface VariantAliasIndex {
|
|
1406
|
+
/** lowercased retired id → replacement model id. */
|
|
1407
|
+
forward: Map<string, string>;
|
|
1408
|
+
/** replacement model id → retired ids that resolve to it. */
|
|
1409
|
+
reverse: Map<string, string[]>;
|
|
1410
|
+
/** Collapsed logical ids declared by the table or observed at runtime. */
|
|
1411
|
+
familyIds: Set<string>;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const dynamicAliasIndexes = new Map<string, VariantAliasIndex>();
|
|
1415
|
+
const VARIANT_ROUTING_KEYS: readonly (Effort | "off")[] = ["off", ...THINKING_EFFORTS];
|
|
1416
|
+
|
|
1417
|
+
const kAliasIndex = Symbol("variant-collapse.aliasIndex");
|
|
1418
|
+
|
|
1419
|
+
interface TableWithAliasIndex extends VariantCollapseTable {
|
|
1420
|
+
[kAliasIndex]?: VariantAliasIndex;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function createAliasIndex(): VariantAliasIndex {
|
|
1424
|
+
return {
|
|
1425
|
+
forward: new Map<string, string>(),
|
|
1426
|
+
reverse: new Map<string, string[]>(),
|
|
1427
|
+
familyIds: new Set<string>(),
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function addVariantAlias(index: VariantAliasIndex, from: string, to: string): boolean {
|
|
1432
|
+
if (from === to || index.forward.has(from.toLowerCase())) return false;
|
|
1433
|
+
index.forward.set(from.toLowerCase(), to);
|
|
1434
|
+
const sources = index.reverse.get(to);
|
|
1435
|
+
if (sources) {
|
|
1436
|
+
sources.push(from);
|
|
1437
|
+
} else {
|
|
1438
|
+
index.reverse.set(to, [from]);
|
|
1439
|
+
}
|
|
1440
|
+
return true;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Persist aliases embedded in collapsed routing so generated catalog rows and
|
|
1445
|
+
* newly discovered families expose the same selector migrations as hand tables.
|
|
1446
|
+
*/
|
|
1447
|
+
function registerCollapsedVariantAliases(provider: Provider, specs: readonly VariantSpecLike[]): void {
|
|
1448
|
+
const providerId = provider.toLowerCase();
|
|
1449
|
+
let index = dynamicAliasIndexes.get(providerId);
|
|
1450
|
+
for (const spec of specs) {
|
|
1451
|
+
const routing = spec.thinking?.effortRouting;
|
|
1452
|
+
if (!routing) continue;
|
|
1453
|
+
let registered = false;
|
|
1454
|
+
for (const effort of VARIANT_ROUTING_KEYS) {
|
|
1455
|
+
const source = routing[effort];
|
|
1456
|
+
if (!source || source === spec.id) continue;
|
|
1457
|
+
index ??= createAliasIndex();
|
|
1458
|
+
registered = addVariantAlias(index, source, spec.id) || registered;
|
|
1459
|
+
}
|
|
1460
|
+
if (spec.requestModelId && spec.requestModelId !== spec.id) {
|
|
1461
|
+
index ??= createAliasIndex();
|
|
1462
|
+
registered = addVariantAlias(index, spec.requestModelId, spec.id) || registered;
|
|
1463
|
+
}
|
|
1464
|
+
if (registered) index?.familyIds.add(spec.id);
|
|
1465
|
+
}
|
|
1466
|
+
if (index) dynamicAliasIndexes.set(providerId, index);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function resolveRegisteredVariantAlias(provider: Provider, normalizedModelId: string): string | undefined {
|
|
1470
|
+
const providerId = provider.toLowerCase();
|
|
1471
|
+
const table = VARIANT_COLLAPSE_TABLES[provider] ?? VARIANT_COLLAPSE_TABLES[providerId];
|
|
1472
|
+
return (
|
|
1473
|
+
(table ? getAliasIndex(table).forward.get(normalizedModelId) : undefined) ??
|
|
1474
|
+
dynamicAliasIndexes.get(providerId)?.forward.get(normalizedModelId)
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function getAliasIndex(table: VariantCollapseTable): VariantAliasIndex {
|
|
1479
|
+
const tagged = table as TableWithAliasIndex;
|
|
1480
|
+
const cached = tagged[kAliasIndex];
|
|
1481
|
+
if (cached) return cached;
|
|
1482
|
+
const index = createAliasIndex();
|
|
1483
|
+
for (const family of table.families) {
|
|
1484
|
+
index.familyIds.add(family.id);
|
|
1485
|
+
for (const member of family.members) addVariantAlias(index, member, family.id);
|
|
1486
|
+
for (const alias of family.extraAliases ?? []) addVariantAlias(index, alias, family.id);
|
|
1487
|
+
}
|
|
1488
|
+
tagged[kAliasIndex] = index;
|
|
1489
|
+
return index;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Resolve a retired effort-tier variant id (collapsed member, recycled id) to
|
|
1494
|
+
* its replacement model id for `provider` via hand-table or registered live
|
|
1495
|
+
* aliases. Returns `undefined` when the id is not a known alias; derived
|
|
1496
|
+
* `X-thinking` members also resolve through `stripThinkingVariantToken`.
|
|
1497
|
+
* Callers must try an exact model lookup first — a live model always wins over
|
|
1498
|
+
* an alias.
|
|
1499
|
+
*/
|
|
1500
|
+
export function resolveVariantAlias(provider: Provider, modelId: string): string | undefined {
|
|
1501
|
+
return resolveRegisteredVariantAlias(provider, modelId.trim().toLowerCase());
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/** Bare-id alias hit: replacement id plus the providers declaring it. */
|
|
1505
|
+
export interface BareVariantAliasHit {
|
|
1506
|
+
id: string;
|
|
1507
|
+
/** Providers declaring the alias — candidates from these win ties. */
|
|
1508
|
+
providers: readonly Provider[];
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Provider-agnostic alias lookup for bare-id selectors. Returns the declaring
|
|
1513
|
+
* providers so callers can prefer their models when the replacement id exists
|
|
1514
|
+
* on unrelated providers too (e.g. a retired Cursor tier id must not resolve
|
|
1515
|
+
* to `openai/gpt-5.4`).
|
|
1516
|
+
*/
|
|
1517
|
+
export function resolveBareVariantAlias(modelId: string): BareVariantAliasHit | undefined {
|
|
1518
|
+
const normalized = modelId.trim().toLowerCase();
|
|
1519
|
+
const providerIds = new Set<string>();
|
|
1520
|
+
for (const provider in VARIANT_COLLAPSE_TABLES) providerIds.add(provider);
|
|
1521
|
+
for (const provider of dynamicAliasIndexes.keys()) providerIds.add(provider);
|
|
1522
|
+
for (const provider of providerIds) {
|
|
1523
|
+
const hit = resolveRegisteredVariantAlias(provider, normalized);
|
|
1524
|
+
if (hit === undefined) continue;
|
|
1525
|
+
const providers: Provider[] = [];
|
|
1526
|
+
for (const candidate of providerIds) {
|
|
1527
|
+
if (resolveRegisteredVariantAlias(candidate, normalized) === hit) {
|
|
1528
|
+
providers.push(candidate);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return { id: hit, providers };
|
|
1532
|
+
}
|
|
1533
|
+
return undefined;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Reverse alias lookup: the retired ids that resolve to `modelId` for
|
|
1538
|
+
* `provider` via hand-table or registered live aliases. Used to re-key config
|
|
1539
|
+
* keyed by raw member ids (models.yml `modelOverrides`, suppressed selectors)
|
|
1540
|
+
* onto the collapsed model.
|
|
1541
|
+
*/
|
|
1542
|
+
export function getVariantAliasSources(provider: Provider, modelId: string): readonly string[] {
|
|
1543
|
+
const providerId = provider.toLowerCase();
|
|
1544
|
+
const table = VARIANT_COLLAPSE_TABLES[provider] ?? VARIANT_COLLAPSE_TABLES[providerId];
|
|
1545
|
+
const staticSources = table ? getAliasIndex(table).reverse.get(modelId) : undefined;
|
|
1546
|
+
const dynamicSources = dynamicAliasIndexes.get(providerId)?.reverse.get(modelId);
|
|
1547
|
+
if (!staticSources) return dynamicSources ?? [];
|
|
1548
|
+
if (!dynamicSources) return staticSources;
|
|
1549
|
+
return [...new Set([...staticSources, ...dynamicSources])];
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function maxOrNull(values: ReadonlyArray<number | null>): number | null {
|
|
1553
|
+
const known = values.filter((v): v is number => v != null);
|
|
1554
|
+
return known.length ? Math.max(...known) : null;
|
|
1555
|
+
}
|