@linxiraos/pi-catalog 1.1.7 → 1.1.8
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 +8 -8
- package/README.md +9 -8
- package/dist/types/build.d.ts +13 -8
- package/dist/types/cline-pass-model-id.d.ts +4 -0
- package/dist/types/compat/anthropic.d.ts +10 -4
- package/dist/types/compat/auth-ids.d.ts +4 -0
- package/dist/types/compat/auth.d.ts +7 -0
- package/dist/types/compat/axes.d.ts +50 -0
- package/dist/types/compat/behavior.d.ts +69 -0
- package/dist/types/compat/cascade.d.ts +37 -0
- package/dist/types/{variant-collapse.d.ts → compat/collapse.d.ts} +60 -35
- package/dist/types/compat/openai.d.ts +7 -26
- package/dist/types/compat/resolve.d.ts +15 -0
- package/dist/types/compat/revision.d.ts +41 -0
- package/dist/types/compat/taxonomy.d.ts +93 -0
- package/dist/types/compat/types.d.ts +565 -0
- package/dist/types/discovery/antigravity.d.ts +1 -1
- package/dist/types/discovery/cursor-proto.d.ts +6 -6
- package/dist/types/discovery/cursor.d.ts +5 -3
- package/dist/types/discovery/devin-proto.d.ts +363 -0
- package/dist/types/discovery/gemini-cli.d.ts +1 -1
- package/dist/types/identity/id.d.ts +1 -0
- package/dist/types/identity/index.d.ts +3 -3
- package/dist/types/identity/reference.d.ts +0 -10
- package/dist/types/index.d.ts +1 -1
- package/dist/types/model-thinking.d.ts +6 -24
- package/dist/types/provider-models/cline-pass.d.ts +22 -0
- package/dist/types/provider-models/descriptors.d.ts +20 -1
- package/dist/types/provider-models/models-dev-policies.d.ts +2 -1
- package/dist/types/provider-models/openai-compat.d.ts +59 -46
- package/dist/types/provider-models/special.d.ts +23 -2
- package/dist/types/types.d.ts +177 -13
- package/dist/types/wire/cline-pass.d.ts +21 -0
- package/dist/types/wire/devin-proto.d.ts +14 -0
- package/dist/types/wire/devin.d.ts +36 -0
- package/dist/types/wire/github-copilot.d.ts +31 -12
- package/package.json +6 -4
- package/src/build.ts +165 -48
- package/src/cline-pass-model-id.ts +11 -0
- package/src/compat/anthropic.ts +15 -136
- package/src/compat/auth-ids.ts +157 -0
- package/src/compat/auth.ts +60 -0
- package/src/compat/axes.ts +290 -0
- package/src/compat/behavior.ts +207 -0
- package/src/compat/cascade.ts +230 -0
- package/src/{variant-collapse.ts → compat/collapse.ts} +381 -773
- package/src/compat/openai.ts +17 -848
- package/src/compat/resolve.ts +1229 -0
- package/src/compat/revision.ts +154 -0
- package/src/compat/rules/README.md +315 -0
- package/src/compat/rules/auth/_order.kdl +13 -0
- package/src/compat/rules/auth/abliteration.kdl +9 -0
- package/src/compat/rules/auth/aiand.kdl +9 -0
- package/src/compat/rules/auth/aimlapi.kdl +3 -0
- package/src/compat/rules/auth/alibaba-coding-plan.kdl +5 -0
- package/src/compat/rules/auth/alibaba-token-plan.kdl +11 -0
- package/src/compat/rules/auth/amazon-bedrock.kdl +5 -0
- package/src/compat/rules/auth/anthropic.kdl +53 -0
- package/src/compat/rules/auth/azure.kdl +3 -0
- package/src/compat/rules/auth/baseten.kdl +9 -0
- package/src/compat/rules/auth/bedrock-mantle.kdl +5 -0
- package/src/compat/rules/auth/cerebras.kdl +9 -0
- package/src/compat/rules/auth/cline-pass.kdl +12 -0
- package/src/compat/rules/auth/cloudflare-ai-gateway.kdl +4 -0
- package/src/compat/rules/auth/coreweave.kdl +9 -0
- package/src/compat/rules/auth/cursor.kdl +5 -0
- package/src/compat/rules/auth/deepinfra.kdl +11 -0
- package/src/compat/rules/auth/deepseek.kdl +10 -0
- package/src/compat/rules/auth/devin.kdl +28 -0
- package/src/compat/rules/auth/exa.kdl +9 -0
- package/src/compat/rules/auth/firepass.kdl +14 -0
- package/src/compat/rules/auth/fireworks.kdl +16 -0
- package/src/compat/rules/auth/github-copilot.kdl +7 -0
- package/src/compat/rules/auth/gitlab-duo-agent.kdl +23 -0
- package/src/compat/rules/auth/gitlab-duo.kdl +24 -0
- package/src/compat/rules/auth/gmi-cloud.kdl +9 -0
- package/src/compat/rules/auth/google-antigravity.kdl +28 -0
- package/src/compat/rules/auth/google-gemini-cli.kdl +26 -0
- package/src/compat/rules/auth/google-vertex.kdl +5 -0
- package/src/compat/rules/auth/google.kdl +3 -0
- package/src/compat/rules/auth/groq.kdl +3 -0
- package/src/compat/rules/auth/huggingface.kdl +9 -0
- package/src/compat/rules/auth/kagi.kdl +9 -0
- package/src/compat/rules/auth/kilo.kdl +4 -0
- package/src/compat/rules/auth/kimi-code.kdl +18 -0
- package/src/compat/rules/auth/litellm.kdl +8 -0
- package/src/compat/rules/auth/llama.cpp.kdl +10 -0
- package/src/compat/rules/auth/lm-studio.kdl +7 -0
- package/src/compat/rules/auth/meta.kdl +9 -0
- package/src/compat/rules/auth/minimax-code-cn.kdl +9 -0
- package/src/compat/rules/auth/minimax-code.kdl +9 -0
- package/src/compat/rules/auth/minimax.kdl +3 -0
- package/src/compat/rules/auth/mistral.kdl +3 -0
- package/src/compat/rules/auth/moonshot.kdl +9 -0
- package/src/compat/rules/auth/nanogpt.kdl +9 -0
- package/src/compat/rules/auth/novita.kdl +12 -0
- package/src/compat/rules/auth/nvidia.kdl +11 -0
- package/src/compat/rules/auth/ollama-cloud.kdl +8 -0
- package/src/compat/rules/auth/ollama.kdl +9 -0
- package/src/compat/rules/auth/openai-codex-device.kdl +20 -0
- package/src/compat/rules/auth/openai-codex.kdl +33 -0
- package/src/compat/rules/auth/openai.kdl +3 -0
- package/src/compat/rules/auth/opencode-go.kdl +10 -0
- package/src/compat/rules/auth/opencode-zen.kdl +9 -0
- package/src/compat/rules/auth/openrouter.kdl +32 -0
- package/src/compat/rules/auth/parallel.kdl +9 -0
- package/src/compat/rules/auth/perplexity.kdl +7 -0
- package/src/compat/rules/auth/qianfan.kdl +9 -0
- package/src/compat/rules/auth/qwen-portal.kdl +9 -0
- package/src/compat/rules/auth/sakana.kdl +9 -0
- package/src/compat/rules/auth/siliconflow-cn.kdl +9 -0
- package/src/compat/rules/auth/siliconflow.kdl +9 -0
- package/src/compat/rules/auth/synthetic.kdl +9 -0
- package/src/compat/rules/auth/tavily.kdl +9 -0
- package/src/compat/rules/auth/together.kdl +14 -0
- package/src/compat/rules/auth/umans.kdl +9 -0
- package/src/compat/rules/auth/venice.kdl +9 -0
- package/src/compat/rules/auth/vercel-ai-gateway.kdl +8 -0
- package/src/compat/rules/auth/vllm.kdl +9 -0
- package/src/compat/rules/auth/wafer-serverless.kdl +11 -0
- package/src/compat/rules/auth/xai-oauth.kdl +27 -0
- package/src/compat/rules/auth/xai.kdl +9 -0
- package/src/compat/rules/auth/xiaomi-token-plan-ams.kdl +9 -0
- package/src/compat/rules/auth/xiaomi-token-plan-cn.kdl +9 -0
- package/src/compat/rules/auth/xiaomi-token-plan-sgp.kdl +9 -0
- package/src/compat/rules/auth/xiaomi.kdl +6 -0
- package/src/compat/rules/auth/yolo-auto.kdl +11 -0
- package/src/compat/rules/auth/zai-coding-plan.kdl +29 -0
- package/src/compat/rules/auth/zai.kdl +9 -0
- package/src/compat/rules/auth/zenmux.kdl +9 -0
- package/src/compat/rules/auth/zhipu-coding-plan.kdl +9 -0
- package/src/compat/rules/classes/amazon.kdl +21 -0
- package/src/compat/rules/classes/anthropic.kdl +285 -0
- package/src/compat/rules/classes/baidu.kdl +9 -0
- package/src/compat/rules/classes/bytedance.kdl +9 -0
- package/src/compat/rules/classes/cohere.kdl +7 -0
- package/src/compat/rules/classes/deepseek.kdl +80 -0
- package/src/compat/rules/classes/gemini.kdl +107 -0
- package/src/compat/rules/classes/gemma.kdl +14 -0
- package/src/compat/rules/classes/glm.kdl +115 -0
- package/src/compat/rules/classes/gpt-oss.kdl +16 -0
- package/src/compat/rules/classes/kimi.kdl +113 -0
- package/src/compat/rules/classes/meta.kdl +15 -0
- package/src/compat/rules/classes/mimo.kdl +32 -0
- package/src/compat/rules/classes/minimax.kdl +79 -0
- package/src/compat/rules/classes/mistral.kdl +19 -0
- package/src/compat/rules/classes/openai.kdl +42 -0
- package/src/compat/rules/classes/qwen.kdl +72 -0
- package/src/compat/rules/classes/stepfun.kdl +10 -0
- package/src/compat/rules/classes/xai.kdl +45 -0
- package/src/compat/rules/providers/abliteration.kdl +8 -0
- package/src/compat/rules/providers/aiand.kdl +19 -0
- package/src/compat/rules/providers/aimlapi.kdl +24 -0
- package/src/compat/rules/providers/alibaba-coding-plan.kdl +20 -0
- package/src/compat/rules/providers/alibaba-token-plan.kdl +35 -0
- package/src/compat/rules/providers/amazon-bedrock.kdl +36 -0
- package/src/compat/rules/providers/anthropic.kdl +24 -0
- package/src/compat/rules/providers/azure.kdl +19 -0
- package/src/compat/rules/providers/baseten.kdl +19 -0
- package/src/compat/rules/providers/bedrock-mantle.kdl +15 -0
- package/src/compat/rules/providers/cerebras.kdl +19 -0
- package/src/compat/rules/providers/cline-pass.kdl +8 -0
- package/src/compat/rules/providers/cloudflare-ai-gateway.kdl +26 -0
- package/src/compat/rules/providers/coreweave.kdl +16 -0
- package/src/compat/rules/providers/cursor.kdl +52 -0
- package/src/compat/rules/providers/deepseek.kdl +21 -0
- package/src/compat/rules/providers/firepass.kdl +15 -0
- package/src/compat/rules/providers/fireworks.kdl +43 -0
- package/src/compat/rules/providers/github-copilot.kdl +62 -0
- package/src/compat/rules/providers/gitlab-duo.kdl +24 -0
- package/src/compat/rules/providers/gmi-cloud.kdl +8 -0
- package/src/compat/rules/providers/google-antigravity.kdl +59 -0
- package/src/compat/rules/providers/google-gemini-cli.kdl +12 -0
- package/src/compat/rules/providers/google-vertex.kdl +25 -0
- package/src/compat/rules/providers/google.kdl +17 -0
- package/src/compat/rules/providers/groq.kdl +8 -0
- package/src/compat/rules/providers/huggingface.kdl +24 -0
- package/src/compat/rules/providers/kilo.kdl +51 -0
- package/src/compat/rules/providers/kimi-code.kdl +37 -0
- package/src/compat/rules/providers/llama.cpp.kdl +6 -0
- package/src/compat/rules/providers/lm-studio.kdl +6 -0
- package/src/compat/rules/providers/meta.kdl +10 -0
- package/src/compat/rules/providers/minimax-code-cn.kdl +13 -0
- package/src/compat/rules/providers/minimax-code.kdl +13 -0
- package/src/compat/rules/providers/minimax.kdl +6 -0
- package/src/compat/rules/providers/mistral.kdl +8 -0
- package/src/compat/rules/providers/moonshot.kdl +36 -0
- package/src/compat/rules/providers/nanogpt.kdl +100 -0
- package/src/compat/rules/providers/novita.kdl +34 -0
- package/src/compat/rules/providers/nvidia.kdl +27 -0
- package/src/compat/rules/providers/ollama-cloud.kdl +20 -0
- package/src/compat/rules/providers/ollama.kdl +12 -0
- package/src/compat/rules/providers/openai-codex.kdl +121 -0
- package/src/compat/rules/providers/openai.kdl +82 -0
- package/src/compat/rules/providers/opencode-go.kdl +97 -0
- package/src/compat/rules/providers/opencode-zen.kdl +103 -0
- package/src/compat/rules/providers/openrouter.kdl +66 -0
- package/src/compat/rules/providers/qianfan.kdl +8 -0
- package/src/compat/rules/providers/sakana.kdl +10 -0
- package/src/compat/rules/providers/synthetic.kdl +16 -0
- package/src/compat/rules/providers/together.kdl +22 -0
- package/src/compat/rules/providers/umans.kdl +28 -0
- package/src/compat/rules/providers/venice.kdl +46 -0
- package/src/compat/rules/providers/vercel-ai-gateway.kdl +73 -0
- package/src/compat/rules/providers/vllm.kdl +8 -0
- package/src/compat/rules/providers/wafer-serverless.kdl +35 -0
- package/src/compat/rules/providers/xai-oauth.kdl +48 -0
- package/src/compat/rules/providers/xai.kdl +50 -0
- package/src/compat/rules/providers/xiaomi-token-plan-ams.kdl +9 -0
- package/src/compat/rules/providers/xiaomi-token-plan-cn.kdl +9 -0
- package/src/compat/rules/providers/xiaomi-token-plan-sgp.kdl +9 -0
- package/src/compat/rules/providers/xiaomi.kdl +22 -0
- package/src/compat/rules/providers/yolo-auto.kdl +16 -0
- package/src/compat/rules/providers/zai.kdl +36 -0
- package/src/compat/rules/providers/zenmux.kdl +108 -0
- package/src/compat/rules/providers/zhipu-coding-plan.kdl +49 -0
- package/src/compat/rules/runtime/behavior.kdl +254 -0
- package/src/compat/rules/taxonomy/_collapse.kdl +747 -0
- package/src/compat/rules/taxonomy/_discovery.kdl +60 -0
- package/src/compat/rules/taxonomy/ai21.kdl +6 -0
- package/src/compat/rules/taxonomy/amazon.kdl +8 -0
- package/src/compat/rules/taxonomy/anthropic.kdl +17 -0
- package/src/compat/rules/taxonomy/baidu.kdl +5 -0
- package/src/compat/rules/taxonomy/bytedance.kdl +5 -0
- package/src/compat/rules/taxonomy/cohere.kdl +6 -0
- package/src/compat/rules/taxonomy/deepseek.kdl +18 -0
- package/src/compat/rules/taxonomy/gemini.kdl +9 -0
- package/src/compat/rules/taxonomy/gemma.kdl +3 -0
- package/src/compat/rules/taxonomy/glm.kdl +17 -0
- package/src/compat/rules/taxonomy/gpt-oss.kdl +11 -0
- package/src/compat/rules/taxonomy/kimi.kdl +20 -0
- package/src/compat/rules/taxonomy/meta.kdl +10 -0
- package/src/compat/rules/taxonomy/mimo.kdl +7 -0
- package/src/compat/rules/taxonomy/minimax.kdl +9 -0
- package/src/compat/rules/taxonomy/mistral.kdl +7 -0
- package/src/compat/rules/taxonomy/openai.kdl +43 -0
- package/src/compat/rules/taxonomy/qwen.kdl +33 -0
- package/src/compat/rules/taxonomy/stepfun.kdl +5 -0
- package/src/compat/rules/taxonomy/unknown.kdl +3 -0
- package/src/compat/rules/taxonomy/xai.kdl +12 -0
- package/src/compat/rules.d.json.ts +5 -0
- package/src/compat/rules.json +16933 -0
- package/src/compat/taxonomy.ts +452 -0
- package/src/compat/types.ts +586 -0
- package/src/discovery/antigravity.ts +5 -6
- package/src/discovery/codex.ts +12 -7
- package/src/discovery/cursor-proto.ts +7 -7
- package/src/discovery/cursor.ts +47 -58
- package/src/discovery/devin-proto.ts +613 -0
- package/src/discovery/devin.ts +358 -52
- package/src/discovery/gemini-cli.ts +14 -11
- package/src/discovery/gemini.ts +2 -0
- package/src/identity/dialect.ts +2 -2
- package/src/identity/id.ts +12 -0
- package/src/identity/index.ts +3 -3
- package/src/identity/reference.ts +30 -4
- package/src/index.ts +1 -1
- package/src/model-manager.ts +4 -4
- package/src/model-thinking.ts +6 -810
- package/src/model-tokenizer.ts +37 -44
- package/src/models.json +86187 -23440
- package/src/models.ts +6 -1
- package/src/provider-models/bundled-references.ts +3 -1
- package/src/provider-models/cline-pass.ts +226 -0
- package/src/provider-models/descriptors.ts +18 -0
- package/src/provider-models/google.ts +14 -5
- package/src/provider-models/models-dev-policies.ts +4 -29
- package/src/provider-models/ollama.ts +15 -2
- package/src/provider-models/openai-compat.ts +794 -520
- package/src/provider-models/special.ts +186 -2
- package/src/types.ts +197 -11
- package/src/wire/cline-pass.ts +35 -0
- package/src/wire/devin-proto.ts +30 -0
- package/src/wire/devin.ts +72 -0
- package/src/wire/github-copilot.ts +49 -7
- package/dist/types/compat/bedrock.d.ts +0 -3
- package/dist/types/compat/devin.d.ts +0 -9
- package/dist/types/identity/classify.d.ts +0 -69
- package/dist/types/identity/family.d.ts +0 -243
- package/dist/types/identity/markers.d.ts +0 -4
- package/dist/types/openai-pricing.d.ts +0 -17
- package/src/compat/bedrock.ts +0 -153
- package/src/compat/devin.ts +0 -12
- package/src/identity/classify.ts +0 -242
- package/src/identity/family.ts +0 -496
- package/src/identity/markers.ts +0 -49
- package/src/openai-pricing.ts +0 -29
|
@@ -11,31 +11,39 @@
|
|
|
11
11
|
* attribution) keys on the logical `id`.
|
|
12
12
|
*
|
|
13
13
|
* Families come from two sources:
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
14
|
+
* - Reviewed `variant-family` nodes in `compat/rules/taxonomy/_collapse.kdl`
|
|
15
|
+
* (compiled into `rules.json`) for providers whose routing needs curation
|
|
16
|
+
* (Antigravity tier triplets, single-member renames, recycled ids). A
|
|
17
|
+
* family whose ids carry `{rev}` is a template, instantiated for every
|
|
18
|
+
* revision the input advertises (`gemini-{rev}-flash` → `gemini-3.8-flash`).
|
|
16
19
|
* - `deriveThinkingPairFamilies`: the global automatic rule — any live
|
|
17
20
|
* `X` + `X-thinking` pair (trailing or infix token) collapses into `X`,
|
|
18
21
|
* routing thinking-enabled requests to `X-thinking`. Gated on identical
|
|
19
22
|
* pricing and same api: price-divergent twins are distinct SKUs and stay
|
|
20
23
|
* separate so billing attribution never lies.
|
|
21
24
|
*
|
|
22
|
-
* Family invariants (hold for
|
|
25
|
+
* Family invariants (hold for compiled, reviewed and derived tables):
|
|
23
26
|
* - One axis per family. A second id axis (e.g. Cursor's `-fast` service
|
|
24
27
|
* tier) becomes a sibling family, never a second routing dimension.
|
|
25
28
|
* - The collapsed spec inherits non-tier fields from the first present
|
|
26
29
|
* member; members must be cost-homogeneous.
|
|
27
30
|
*
|
|
28
|
-
* `
|
|
31
|
+
* `collapseVariants` is pure, deterministic, and idempotent:
|
|
29
32
|
* `collapse(collapse(x))` equals `collapse(x)`, and mixed raw+collapsed input
|
|
30
33
|
* (stale cache rows, previous-snapshot fallbacks) dedupes to the collapsed
|
|
31
34
|
* entry. That makes it safe at every source — discovery, the catalog
|
|
32
35
|
* generator, and the model-manager merge point.
|
|
33
36
|
*/
|
|
34
|
-
import {
|
|
35
|
-
import { Effort, THINKING_EFFORTS } from "
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import
|
|
37
|
+
import { buildModel } from "../build";
|
|
38
|
+
import { Effort, THINKING_EFFORTS } from "../effort";
|
|
39
|
+
import type { Api, Model, ModelSpec, Provider, ThinkingConfig } from "../types";
|
|
40
|
+
import { resolveModelPolicy } from "./resolve";
|
|
41
|
+
import { parseRevision, parseRevisionConstraint, type RevisionTerm, revisionSatisfies } from "./revision";
|
|
42
|
+
import { collapseVariantId, collapseVocabulary, stripThinkingVariantSuffix } from "./taxonomy";
|
|
43
|
+
import { type CompiledVariantFamily, REVISION_PLACEHOLDER } from "./types";
|
|
44
|
+
|
|
45
|
+
const VARIANT_ROUTING_KEYS: readonly (Effort | "off")[] = ["off", ...THINKING_EFFORTS];
|
|
46
|
+
const DEFAULT_PAIR_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
39
47
|
|
|
40
48
|
/**
|
|
41
49
|
* Structural bound for collapse inputs: both raw `ModelSpec`s and built
|
|
@@ -81,8 +89,14 @@ export interface EffortVariantFamily {
|
|
|
81
89
|
* efforts fall back to `requestModelId ?? id`.
|
|
82
90
|
*/
|
|
83
91
|
routing: Readonly<Partial<Record<Effort | "off", string>>>;
|
|
84
|
-
/**
|
|
85
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Explicit capability surface for the collapsed spec — no inference. Omit
|
|
94
|
+
* for single-wire-id renames on providers where effort is encoded in the
|
|
95
|
+
* upstream id itself (Devin): with one member and no routing there is no
|
|
96
|
+
* controllable surface, and the collapsed spec must carry no thinking
|
|
97
|
+
* rather than an effort ladder whose every tier resolves to one wire id.
|
|
98
|
+
*/
|
|
99
|
+
thinking?: Readonly<Omit<ThinkingConfig, "effortRouting" | "suppressWhenOff">>;
|
|
86
100
|
/** Thinking-off requests must explicitly suppress thinking on the wire. */
|
|
87
101
|
suppressWhenOff?: boolean;
|
|
88
102
|
/**
|
|
@@ -96,37 +110,41 @@ export interface EffortVariantFamily {
|
|
|
96
110
|
extraAliases?: readonly string[];
|
|
97
111
|
}
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
/**
|
|
114
|
+
* A reviewed family whose ids carry {@link REVISION_PLACEHOLDER}: one
|
|
115
|
+
* `gemini-{rev}-flash` entry stands for every Flash generation the provider
|
|
116
|
+
* serves with the same sibling layout. Concrete families with the same
|
|
117
|
+
* instantiated id take precedence.
|
|
118
|
+
*/
|
|
119
|
+
export interface VariantFamilyTemplate {
|
|
120
|
+
/** Family with placeholders still in every id and the display name. */
|
|
121
|
+
family: EffortVariantFamily;
|
|
122
|
+
/** Revisions the template applies to; unbounded when absent. */
|
|
123
|
+
revision?: readonly RevisionTerm[];
|
|
124
|
+
/**
|
|
125
|
+
* Matches the logical id, any member, or any extra alias (case-insensitive);
|
|
126
|
+
* one alternative per id, each capturing the revision.
|
|
127
|
+
*/
|
|
128
|
+
pattern: RegExp;
|
|
101
129
|
}
|
|
102
130
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Thinking-off routes to the non-thinking backing id, where omitting
|
|
117
|
-
// thinkingConfig is already correct — no suppressWhenOff.
|
|
118
|
-
thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
|
119
|
-
preserveAbsentEffortRoutes: true,
|
|
120
|
-
};
|
|
131
|
+
export interface VariantCollapseTable {
|
|
132
|
+
families: readonly EffortVariantFamily[];
|
|
133
|
+
/** Revision-templated families, instantiated against live ids. */
|
|
134
|
+
templates?: readonly VariantFamilyTemplate[];
|
|
135
|
+
/**
|
|
136
|
+
* Provider-scoped selector aliases: short native-CLI names and dotted
|
|
137
|
+
* upstream spellings → logical model id. Unlike family members and
|
|
138
|
+
* `extraAliases` these are deliberately invisible to the bare-id lookup
|
|
139
|
+
* ({@link resolveBareVariantSelector}) and to the reverse index — a generic
|
|
140
|
+
* label like `gpt` or `opus` only means something once a provider is
|
|
141
|
+
* named, and must never hijack an unqualified selector or re-key config.
|
|
142
|
+
*/
|
|
143
|
+
providerAliases?: Readonly<Record<string, string>>;
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
type TierRoutes = Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", string>>;
|
|
124
147
|
|
|
125
|
-
/** Devin families with a `-max` sibling: five wire tiers, `low` floor. */
|
|
126
|
-
const DEVIN_FIVE_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
|
|
127
|
-
/** Pre-5.6 Devin GPT families top out at `-xhigh`: four wire tiers, `low` floor. */
|
|
128
|
-
const DEVIN_FOUR_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
|
|
129
|
-
|
|
130
148
|
/**
|
|
131
149
|
* Build one effort-tier family from a tier→wire-id map: routing keeps only the
|
|
132
150
|
* listed efforts, `members` dedupes the targets in tier order, and thinking is
|
|
@@ -187,617 +205,140 @@ function tierFamily(
|
|
|
187
205
|
};
|
|
188
206
|
}
|
|
189
207
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
tierFamily(
|
|
198
|
-
base,
|
|
199
|
-
name,
|
|
200
|
-
{
|
|
201
|
-
off: `${base}-none`,
|
|
202
|
-
low: `${base}-low`,
|
|
203
|
-
medium: `${base}-medium`,
|
|
204
|
-
high: `${base}-high`,
|
|
205
|
-
xhigh: `${base}-xhigh`,
|
|
206
|
-
max: `${base}-max`,
|
|
207
|
-
},
|
|
208
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
209
|
-
),
|
|
210
|
-
tierFamily(
|
|
211
|
-
`${base}-fast`,
|
|
212
|
-
`${name} Fast`,
|
|
213
|
-
{
|
|
214
|
-
off: `${base}-none-priority`,
|
|
215
|
-
low: `${base}-low-priority`,
|
|
216
|
-
medium: `${base}-medium-priority`,
|
|
217
|
-
high: `${base}-high-priority`,
|
|
218
|
-
xhigh: `${base}-xhigh-priority`,
|
|
219
|
-
max: `${base}-max-priority`,
|
|
220
|
-
},
|
|
221
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
222
|
-
),
|
|
223
|
-
];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const GEMINI_3_FLASH_FAMILY_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
227
|
-
const GEMINI_3_PRO_FAMILY_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High];
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Antigravity Cloud Code Assist sends an explicit `thinkingBudget` per tier
|
|
231
|
-
* (verified against captured `daily-cloudcode-pa` requests). Flash uses round
|
|
232
|
-
* budgets; Pro offsets every budget by +1. Minimal mirrors Low (the Antigravity
|
|
233
|
-
* UI exposes Low/Medium/High only) so the effort stays selectable.
|
|
234
|
-
*/
|
|
235
|
-
const GEMINI_3_FLASH_FAMILY_BUDGETS: Readonly<Partial<Record<Effort, number>>> = {
|
|
236
|
-
[Effort.Minimal]: 1000,
|
|
237
|
-
[Effort.Low]: 1000,
|
|
238
|
-
[Effort.Medium]: 4000,
|
|
239
|
-
[Effort.High]: 10000,
|
|
240
|
-
};
|
|
241
|
-
const GEMINI_3_PRO_FAMILY_BUDGETS: Readonly<Partial<Record<Effort, number>>> = {
|
|
242
|
-
[Effort.Low]: 1001,
|
|
243
|
-
[Effort.High]: 10001,
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Cloud Code Assist's legacy Gemini 3.5 Flash and 3.1 Pro families use
|
|
248
|
-
* different thinking transports: `google-antigravity` (daily-cloudcode-pa)
|
|
249
|
-
* sends captured `thinkingBudget` values, while `google-gemini-cli`
|
|
250
|
-
* (cloudcode-pa) follows the official Gemini CLI and uses `thinkingLevel`.
|
|
251
|
-
* Gemini 3.6 exposes one wire id per level and uses `thinkingLevel` on both
|
|
252
|
-
* endpoints.
|
|
253
|
-
*/
|
|
254
|
-
function geminiFlashFamily(mode: "budget" | "google-level"): EffortVariantFamily {
|
|
255
|
-
const budget = mode === "budget";
|
|
256
|
-
return {
|
|
257
|
-
id: "gemini-3.5-flash",
|
|
258
|
-
name: "Gemini 3.5 Flash",
|
|
259
|
-
members: ["gemini-3.5-flash-extra-low", "gemini-3.5-flash-low", "gemini-3-flash-agent"],
|
|
260
|
-
routing: budget
|
|
261
|
-
? {
|
|
262
|
-
off: "gemini-3.5-flash-extra-low",
|
|
263
|
-
[Effort.Minimal]: "gemini-3.5-flash-extra-low",
|
|
264
|
-
[Effort.Low]: "gemini-3.5-flash-extra-low",
|
|
265
|
-
[Effort.Medium]: "gemini-3.5-flash-low",
|
|
266
|
-
[Effort.High]: "gemini-3-flash-agent",
|
|
267
|
-
}
|
|
268
|
-
: {
|
|
269
|
-
off: "gemini-3.5-flash-extra-low",
|
|
270
|
-
[Effort.Minimal]: "gemini-3-flash-agent",
|
|
271
|
-
[Effort.Low]: "gemini-3.5-flash-extra-low",
|
|
272
|
-
[Effort.Medium]: "gemini-3.5-flash-extra-low",
|
|
273
|
-
[Effort.High]: "gemini-3.5-flash-low",
|
|
274
|
-
},
|
|
275
|
-
thinking: budget
|
|
276
|
-
? { mode: "budget", efforts: GEMINI_3_FLASH_FAMILY_EFFORTS, effortBudgets: GEMINI_3_FLASH_FAMILY_BUDGETS }
|
|
277
|
-
: { mode: "google-level", efforts: GEMINI_3_FLASH_FAMILY_EFFORTS },
|
|
278
|
-
suppressWhenOff: true,
|
|
279
|
-
// Retired bare id; the alias only fires when no live model holds it
|
|
280
|
-
// (exact match wins in every resolver).
|
|
281
|
-
extraAliases: ["gemini-3-flash"],
|
|
208
|
+
/** One reviewed family from the compiled collapse vocabulary → runtime shape. */
|
|
209
|
+
function compiledFamily(compiled: CompiledVariantFamily): EffortVariantFamily {
|
|
210
|
+
const family: EffortVariantFamily = {
|
|
211
|
+
id: compiled.id,
|
|
212
|
+
name: compiled.name,
|
|
213
|
+
members: compiled.members,
|
|
214
|
+
routing: compiled.routing,
|
|
282
215
|
};
|
|
216
|
+
if (compiled.defaultMember !== undefined) family.defaultMember = compiled.defaultMember;
|
|
217
|
+
if (compiled.retiredMembers?.length) family.retiredMembers = compiled.retiredMembers;
|
|
218
|
+
if (!compiled.noThinking && compiled.mode !== undefined) {
|
|
219
|
+
family.thinking = {
|
|
220
|
+
mode: compiled.mode,
|
|
221
|
+
efforts: compiled.efforts ?? [],
|
|
222
|
+
...(compiled.effortBudgets !== undefined && {
|
|
223
|
+
effortBudgets: compiled.effortBudgets,
|
|
224
|
+
}),
|
|
225
|
+
...(compiled.defaultLevel !== undefined && { defaultLevel: compiled.defaultLevel }),
|
|
226
|
+
...(compiled.requiresEffort === true && { requiresEffort: true }),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (compiled.suppressWhenOff) family.suppressWhenOff = true;
|
|
230
|
+
if (compiled.preserveAbsentEffortRoutes) family.preserveAbsentEffortRoutes = true;
|
|
231
|
+
if (compiled.extraAliases?.length) family.extraAliases = compiled.extraAliases;
|
|
232
|
+
return family;
|
|
283
233
|
}
|
|
284
234
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
[Effort.High]: `${id}-high`,
|
|
300
|
-
},
|
|
301
|
-
thinking: {
|
|
302
|
-
mode: "google-level",
|
|
303
|
-
efforts: GEMINI_3_FLASH_FAMILY_EFFORTS,
|
|
304
|
-
requiresEffort: true,
|
|
305
|
-
},
|
|
235
|
+
const REVISION_CAPTURE = String.raw`(\d+(?:\.\d+){0,2})`;
|
|
236
|
+
|
|
237
|
+
/** One compiled `{rev}` family → runtime template with its id matcher. */
|
|
238
|
+
function compiledTemplate(compiled: CompiledVariantFamily): VariantFamilyTemplate {
|
|
239
|
+
const family = compiledFamily(compiled);
|
|
240
|
+
const alternatives = [family.id, ...family.members, ...(family.extraAliases ?? [])].map(id =>
|
|
241
|
+
id
|
|
242
|
+
.split(REVISION_PLACEHOLDER)
|
|
243
|
+
.map(part => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
244
|
+
.join(REVISION_CAPTURE),
|
|
245
|
+
);
|
|
246
|
+
const template: VariantFamilyTemplate = {
|
|
247
|
+
family,
|
|
248
|
+
pattern: new RegExp(`^(?:${alternatives.join("|")})$`, "i"),
|
|
306
249
|
};
|
|
250
|
+
if (compiled.revision !== undefined) {
|
|
251
|
+
const terms = parseRevisionConstraint(compiled.revision);
|
|
252
|
+
if (terms !== undefined) template.revision = terms;
|
|
253
|
+
}
|
|
254
|
+
return template;
|
|
307
255
|
}
|
|
308
256
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
[Effort.High]: "gemini-pro-agent",
|
|
329
|
-
},
|
|
330
|
-
thinking: budget
|
|
331
|
-
? { mode: "budget", efforts: GEMINI_3_PRO_FAMILY_EFFORTS, effortBudgets: GEMINI_3_PRO_FAMILY_BUDGETS }
|
|
332
|
-
: { mode: "google-level", efforts: GEMINI_3_PRO_FAMILY_EFFORTS },
|
|
333
|
-
suppressWhenOff: true,
|
|
257
|
+
/** Substitute the placeholder throughout one templated family. */
|
|
258
|
+
function instantiateTemplate(template: VariantFamilyTemplate, rev: string): EffortVariantFamily | undefined {
|
|
259
|
+
if (template.revision !== undefined) {
|
|
260
|
+
const parsed = parseRevision(rev);
|
|
261
|
+
if (parsed === undefined || !revisionSatisfies(parsed, template.revision)) return undefined;
|
|
262
|
+
}
|
|
263
|
+
const fill = (id: string): string => id.replaceAll(REVISION_PLACEHOLDER, rev);
|
|
264
|
+
const source = template.family;
|
|
265
|
+
const routing: Partial<Record<Effort | "off", string>> = {};
|
|
266
|
+
for (const effort of VARIANT_ROUTING_KEYS) {
|
|
267
|
+
const target = source.routing[effort];
|
|
268
|
+
if (target !== undefined) routing[effort] = fill(target);
|
|
269
|
+
}
|
|
270
|
+
const family: EffortVariantFamily = {
|
|
271
|
+
...source,
|
|
272
|
+
id: fill(source.id),
|
|
273
|
+
name: fill(source.name),
|
|
274
|
+
members: source.members.map(fill),
|
|
275
|
+
routing,
|
|
334
276
|
};
|
|
277
|
+
if (source.defaultMember !== undefined) family.defaultMember = fill(source.defaultMember);
|
|
278
|
+
if (source.retiredMembers !== undefined) family.retiredMembers = source.retiredMembers.map(fill);
|
|
279
|
+
if (source.extraAliases !== undefined) family.extraAliases = source.extraAliases.map(fill);
|
|
280
|
+
return family;
|
|
335
281
|
}
|
|
336
282
|
|
|
337
|
-
/** CCA families shared verbatim by both providers (transport-agnostic). */
|
|
338
|
-
const SHARED_CCA_FAMILIES: readonly EffortVariantFamily[] = [
|
|
339
|
-
{
|
|
340
|
-
// Legacy static family — covers stale snapshots and caches. Stale ids are
|
|
341
|
-
// unverified against the budget-mode CCA contract; keep them on level.
|
|
342
|
-
id: "gemini-3-pro",
|
|
343
|
-
name: "Gemini 3 Pro",
|
|
344
|
-
members: ["gemini-3-pro-low", "gemini-3-pro-high"],
|
|
345
|
-
routing: {
|
|
346
|
-
off: "gemini-3-pro-low",
|
|
347
|
-
[Effort.Low]: "gemini-3-pro-low",
|
|
348
|
-
[Effort.High]: "gemini-3-pro-high",
|
|
349
|
-
},
|
|
350
|
-
thinking: { mode: "google-level", efforts: GEMINI_3_PRO_FAMILY_EFFORTS },
|
|
351
|
-
suppressWhenOff: true,
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
// Rename-only collapse: every effort and off fall back to the wire id.
|
|
355
|
-
id: "gpt-oss-120b",
|
|
356
|
-
name: "GPT-OSS 120B",
|
|
357
|
-
members: ["gpt-oss-120b-medium"],
|
|
358
|
-
routing: {},
|
|
359
|
-
thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
|
360
|
-
},
|
|
361
|
-
// Antigravity Cloud Code Assist exposes Claude 4.6 asymmetrically: only the
|
|
362
|
-
// bare `claude-sonnet-4-6` wire id (no `-thinking` twin) and only the
|
|
363
|
-
// `claude-opus-4-6-thinking` wire id (no bare twin). Per-effort thinking is
|
|
364
|
-
// carried in the request body via `thinkingBudget`, so both ids accept on/off
|
|
365
|
-
// requests. Listing both candidates in `members` (priority order) keeps the
|
|
366
|
-
// collapse correct if the backend mix ever rebalances; `retiredMembers`
|
|
367
|
-
// re-points stale collapsed snapshots (bundled catalog rows, cache rows
|
|
368
|
-
// written by prior generations) away from the dead wire id via
|
|
369
|
-
// `reconcileRetiredRouting`.
|
|
370
|
-
{
|
|
371
|
-
id: "claude-sonnet-4-6",
|
|
372
|
-
name: "Claude Sonnet 4.6",
|
|
373
|
-
members: ["claude-sonnet-4-6", "claude-sonnet-4-6-thinking"],
|
|
374
|
-
retiredMembers: ["claude-sonnet-4-6-thinking"],
|
|
375
|
-
routing: {},
|
|
376
|
-
thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
{
|
|
380
|
-
id: "claude-opus-4-6",
|
|
381
|
-
name: "Claude Opus 4.6",
|
|
382
|
-
members: ["claude-opus-4-6-thinking", "claude-opus-4-6"],
|
|
383
|
-
retiredMembers: ["claude-opus-4-6"],
|
|
384
|
-
routing: {},
|
|
385
|
-
thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] },
|
|
386
|
-
},
|
|
387
|
-
thinkingPair("claude-sonnet-4-5", "Claude Sonnet 4.5"),
|
|
388
|
-
thinkingPair("claude-opus-4-5", "Claude Opus 4.5"),
|
|
389
|
-
thinkingPair("gemini-2.5-flash", "Gemini 2.5 Flash"),
|
|
390
|
-
];
|
|
391
|
-
|
|
392
|
-
/** `google-antigravity` Gemini families, using each generation's native transport. */
|
|
393
|
-
export const ANTIGRAVITY_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
|
|
394
|
-
families: [
|
|
395
|
-
GEMINI_36_FLASH_FAMILY,
|
|
396
|
-
GEMINI_37_FLASH_FAMILY,
|
|
397
|
-
geminiFlashFamily("budget"),
|
|
398
|
-
geminiProFamily("budget"),
|
|
399
|
-
...SHARED_CCA_FAMILIES,
|
|
400
|
-
],
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
/** `google-gemini-cli` Gemini families on the official CLI's level transport. */
|
|
404
|
-
export const GEMINI_CLI_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
|
|
405
|
-
families: [
|
|
406
|
-
GEMINI_36_FLASH_FAMILY,
|
|
407
|
-
GEMINI_37_FLASH_FAMILY,
|
|
408
|
-
geminiFlashFamily("google-level"),
|
|
409
|
-
geminiProFamily("google-level"),
|
|
410
|
-
...SHARED_CCA_FAMILIES,
|
|
411
|
-
],
|
|
412
|
-
};
|
|
413
|
-
export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
|
|
414
|
-
families: [
|
|
415
|
-
tierFamily(
|
|
416
|
-
"claude-opus-5",
|
|
417
|
-
"Claude Opus 5",
|
|
418
|
-
{
|
|
419
|
-
low: "claude-opus-5-low",
|
|
420
|
-
medium: "claude-opus-5-medium",
|
|
421
|
-
high: "claude-opus-5-high",
|
|
422
|
-
xhigh: "claude-opus-5-xhigh",
|
|
423
|
-
max: "claude-opus-5-max",
|
|
424
|
-
},
|
|
425
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
426
|
-
),
|
|
427
|
-
tierFamily(
|
|
428
|
-
"claude-opus-5-fast",
|
|
429
|
-
"Claude Opus 5 Fast",
|
|
430
|
-
{
|
|
431
|
-
low: "claude-opus-5-low-fast",
|
|
432
|
-
medium: "claude-opus-5-medium-fast",
|
|
433
|
-
high: "claude-opus-5-high-fast",
|
|
434
|
-
xhigh: "claude-opus-5-xhigh-fast",
|
|
435
|
-
max: "claude-opus-5-max-fast",
|
|
436
|
-
},
|
|
437
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
438
|
-
),
|
|
439
|
-
tierFamily(
|
|
440
|
-
"claude-fable-5",
|
|
441
|
-
"Claude Fable 5",
|
|
442
|
-
{
|
|
443
|
-
low: "claude-5-fable-low",
|
|
444
|
-
medium: "claude-5-fable-medium",
|
|
445
|
-
high: "claude-5-fable-high",
|
|
446
|
-
xhigh: "claude-5-fable-xhigh",
|
|
447
|
-
max: "claude-5-fable-max",
|
|
448
|
-
},
|
|
449
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
450
|
-
),
|
|
451
|
-
tierFamily(
|
|
452
|
-
"claude-sonnet-5",
|
|
453
|
-
"Claude Sonnet 5",
|
|
454
|
-
{
|
|
455
|
-
low: "claude-sonnet-5-low",
|
|
456
|
-
medium: "claude-sonnet-5-medium",
|
|
457
|
-
high: "claude-sonnet-5-high",
|
|
458
|
-
xhigh: "claude-sonnet-5-xhigh",
|
|
459
|
-
max: "claude-sonnet-5-max",
|
|
460
|
-
},
|
|
461
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
462
|
-
),
|
|
463
|
-
tierFamily(
|
|
464
|
-
"claude-opus-4-7",
|
|
465
|
-
"Claude Opus 4.7",
|
|
466
|
-
{
|
|
467
|
-
low: "claude-opus-4-7-low",
|
|
468
|
-
medium: "claude-opus-4-7-medium",
|
|
469
|
-
high: "claude-opus-4-7-high",
|
|
470
|
-
xhigh: "claude-opus-4-7-xhigh",
|
|
471
|
-
max: "claude-opus-4-7-max",
|
|
472
|
-
},
|
|
473
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
474
|
-
),
|
|
475
|
-
tierFamily(
|
|
476
|
-
"claude-opus-4-7-fast",
|
|
477
|
-
"Claude Opus 4.7 Fast",
|
|
478
|
-
{
|
|
479
|
-
low: "claude-opus-4-7-low-fast",
|
|
480
|
-
medium: "claude-opus-4-7-medium-fast",
|
|
481
|
-
high: "claude-opus-4-7-high-fast",
|
|
482
|
-
xhigh: "claude-opus-4-7-xhigh-fast",
|
|
483
|
-
max: "claude-opus-4-7-max-fast",
|
|
484
|
-
},
|
|
485
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
486
|
-
),
|
|
487
|
-
tierFamily(
|
|
488
|
-
"claude-opus-4-8",
|
|
489
|
-
"Claude Opus 4.8",
|
|
490
|
-
{
|
|
491
|
-
low: "claude-opus-4-8-low",
|
|
492
|
-
medium: "claude-opus-4-8-medium",
|
|
493
|
-
high: "claude-opus-4-8-high",
|
|
494
|
-
xhigh: "claude-opus-4-8-xhigh",
|
|
495
|
-
max: "claude-opus-4-8-max",
|
|
496
|
-
},
|
|
497
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
498
|
-
),
|
|
499
|
-
tierFamily(
|
|
500
|
-
"claude-opus-4-8-fast",
|
|
501
|
-
"Claude Opus 4.8 Fast",
|
|
502
|
-
{
|
|
503
|
-
low: "claude-opus-4-8-low-fast",
|
|
504
|
-
medium: "claude-opus-4-8-medium-fast",
|
|
505
|
-
high: "claude-opus-4-8-high-fast",
|
|
506
|
-
xhigh: "claude-opus-4-8-xhigh-fast",
|
|
507
|
-
max: "claude-opus-4-8-max-fast",
|
|
508
|
-
},
|
|
509
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
510
|
-
),
|
|
511
|
-
tierFamily(
|
|
512
|
-
"gpt-5-2",
|
|
513
|
-
"GPT-5.2",
|
|
514
|
-
{
|
|
515
|
-
off: "MODEL_GPT_5_2_NONE",
|
|
516
|
-
low: "MODEL_GPT_5_2_LOW",
|
|
517
|
-
medium: "MODEL_GPT_5_2_MEDIUM",
|
|
518
|
-
high: "MODEL_GPT_5_2_HIGH",
|
|
519
|
-
xhigh: "MODEL_GPT_5_2_XHIGH",
|
|
520
|
-
},
|
|
521
|
-
DEVIN_FOUR_TIER_EFFORTS,
|
|
522
|
-
),
|
|
523
|
-
tierFamily(
|
|
524
|
-
"gpt-5-3-codex",
|
|
525
|
-
"GPT-5.3 Codex",
|
|
526
|
-
{
|
|
527
|
-
low: "gpt-5-3-codex-low",
|
|
528
|
-
medium: "gpt-5-3-codex-medium",
|
|
529
|
-
high: "gpt-5-3-codex-high",
|
|
530
|
-
xhigh: "gpt-5-3-codex-xhigh",
|
|
531
|
-
},
|
|
532
|
-
DEVIN_FOUR_TIER_EFFORTS,
|
|
533
|
-
),
|
|
534
|
-
tierFamily(
|
|
535
|
-
"gpt-5-3-codex-fast",
|
|
536
|
-
"GPT-5.3 Codex Fast",
|
|
537
|
-
{
|
|
538
|
-
low: "gpt-5-3-codex-low-priority",
|
|
539
|
-
medium: "gpt-5-3-codex-medium-priority",
|
|
540
|
-
high: "gpt-5-3-codex-high-priority",
|
|
541
|
-
xhigh: "gpt-5-3-codex-xhigh-priority",
|
|
542
|
-
},
|
|
543
|
-
DEVIN_FOUR_TIER_EFFORTS,
|
|
544
|
-
),
|
|
545
|
-
tierFamily(
|
|
546
|
-
"gpt-5-4",
|
|
547
|
-
"GPT-5.4",
|
|
548
|
-
{
|
|
549
|
-
off: "gpt-5-4-none",
|
|
550
|
-
low: "gpt-5-4-low",
|
|
551
|
-
medium: "gpt-5-4-medium",
|
|
552
|
-
high: "gpt-5-4-high",
|
|
553
|
-
xhigh: "gpt-5-4-xhigh",
|
|
554
|
-
},
|
|
555
|
-
DEVIN_FOUR_TIER_EFFORTS,
|
|
556
|
-
),
|
|
557
|
-
tierFamily(
|
|
558
|
-
"gpt-5-4-fast",
|
|
559
|
-
"GPT-5.4 Fast",
|
|
560
|
-
{
|
|
561
|
-
off: "gpt-5-4-none-priority",
|
|
562
|
-
low: "gpt-5-4-low-priority",
|
|
563
|
-
medium: "gpt-5-4-medium-priority",
|
|
564
|
-
high: "gpt-5-4-high-priority",
|
|
565
|
-
xhigh: "gpt-5-4-xhigh-priority",
|
|
566
|
-
},
|
|
567
|
-
DEVIN_FOUR_TIER_EFFORTS,
|
|
568
|
-
),
|
|
569
|
-
tierFamily(
|
|
570
|
-
"gpt-5-4-mini",
|
|
571
|
-
"GPT-5.4 Mini",
|
|
572
|
-
{
|
|
573
|
-
low: "gpt-5-4-mini-low",
|
|
574
|
-
medium: "gpt-5-4-mini-medium",
|
|
575
|
-
high: "gpt-5-4-mini-high",
|
|
576
|
-
xhigh: "gpt-5-4-mini-xhigh",
|
|
577
|
-
},
|
|
578
|
-
DEVIN_FOUR_TIER_EFFORTS,
|
|
579
|
-
),
|
|
580
|
-
tierFamily(
|
|
581
|
-
"gpt-5-5",
|
|
582
|
-
"GPT-5.5",
|
|
583
|
-
{
|
|
584
|
-
off: "gpt-5-5-none",
|
|
585
|
-
low: "gpt-5-5-low",
|
|
586
|
-
medium: "gpt-5-5-medium",
|
|
587
|
-
high: "gpt-5-5-high",
|
|
588
|
-
xhigh: "gpt-5-5-xhigh",
|
|
589
|
-
},
|
|
590
|
-
DEVIN_FOUR_TIER_EFFORTS,
|
|
591
|
-
),
|
|
592
|
-
tierFamily(
|
|
593
|
-
"gpt-5-5-fast",
|
|
594
|
-
"GPT-5.5 Fast",
|
|
595
|
-
{
|
|
596
|
-
off: "gpt-5-5-none-priority",
|
|
597
|
-
low: "gpt-5-5-low-priority",
|
|
598
|
-
medium: "gpt-5-5-medium-priority",
|
|
599
|
-
high: "gpt-5-5-high-priority",
|
|
600
|
-
xhigh: "gpt-5-5-xhigh-priority",
|
|
601
|
-
},
|
|
602
|
-
DEVIN_FOUR_TIER_EFFORTS,
|
|
603
|
-
),
|
|
604
|
-
...devinGpt56Families("luna", "GPT-5.6 Luna"),
|
|
605
|
-
...devinGpt56Families("sol", "GPT-5.6 Sol"),
|
|
606
|
-
...devinGpt56Families("terra", "GPT-5.6 Terra"),
|
|
607
|
-
tierFamily(
|
|
608
|
-
"kimi-k3",
|
|
609
|
-
"Kimi K3",
|
|
610
|
-
{
|
|
611
|
-
low: "kimi-k3-low",
|
|
612
|
-
high: "kimi-k3-high",
|
|
613
|
-
max: "kimi-k3-max",
|
|
614
|
-
},
|
|
615
|
-
[Effort.Low, Effort.High, Effort.Max],
|
|
616
|
-
),
|
|
617
|
-
tierFamily(
|
|
618
|
-
"swe-1-7",
|
|
619
|
-
"SWE-1.7",
|
|
620
|
-
{
|
|
621
|
-
medium: "swe-1-7-medium",
|
|
622
|
-
max: "swe-1-7",
|
|
623
|
-
},
|
|
624
|
-
[Effort.Medium, Effort.Max],
|
|
625
|
-
),
|
|
626
|
-
tierFamily(
|
|
627
|
-
"grok-4-5",
|
|
628
|
-
"Grok 4.5",
|
|
629
|
-
{
|
|
630
|
-
low: "grok-4-5-low",
|
|
631
|
-
medium: "grok-4-5-medium",
|
|
632
|
-
high: "grok-4-5-high",
|
|
633
|
-
},
|
|
634
|
-
[Effort.Low, Effort.Medium, Effort.High],
|
|
635
|
-
),
|
|
636
|
-
tierFamily(
|
|
637
|
-
"inkling",
|
|
638
|
-
"Inkling",
|
|
639
|
-
{
|
|
640
|
-
off: "inkling-none",
|
|
641
|
-
low: "inkling-low",
|
|
642
|
-
medium: "inkling-medium",
|
|
643
|
-
high: "inkling-high",
|
|
644
|
-
xhigh: "inkling-xhigh",
|
|
645
|
-
max: "inkling-max",
|
|
646
|
-
},
|
|
647
|
-
DEVIN_FIVE_TIER_EFFORTS,
|
|
648
|
-
),
|
|
649
|
-
tierFamily(
|
|
650
|
-
"gemini-3-1-pro",
|
|
651
|
-
"Gemini 3.1 Pro",
|
|
652
|
-
{
|
|
653
|
-
low: "gemini-3-1-pro-low",
|
|
654
|
-
high: "gemini-3-1-pro-high",
|
|
655
|
-
},
|
|
656
|
-
[Effort.Low, Effort.High],
|
|
657
|
-
),
|
|
658
|
-
tierFamily(
|
|
659
|
-
"gemini-3-5-flash",
|
|
660
|
-
"Gemini 3.5 Flash",
|
|
661
|
-
{
|
|
662
|
-
minimal: "gemini-3-5-flash-minimal",
|
|
663
|
-
low: "gemini-3-5-flash-low",
|
|
664
|
-
medium: "gemini-3-5-flash-medium",
|
|
665
|
-
high: "gemini-3-5-flash-high",
|
|
666
|
-
},
|
|
667
|
-
[Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
|
|
668
|
-
),
|
|
669
|
-
tierFamily(
|
|
670
|
-
"gemini-3-6-flash",
|
|
671
|
-
"Gemini 3.6 Flash",
|
|
672
|
-
{
|
|
673
|
-
minimal: "gemini-3-6-flash-minimal",
|
|
674
|
-
low: "gemini-3-6-flash-low",
|
|
675
|
-
medium: "gemini-3-6-flash-medium",
|
|
676
|
-
high: "gemini-3-6-flash-high",
|
|
677
|
-
},
|
|
678
|
-
[Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
|
|
679
|
-
),
|
|
680
|
-
tierFamily(
|
|
681
|
-
"gemini-3-flash",
|
|
682
|
-
"Gemini 3 Flash",
|
|
683
|
-
{
|
|
684
|
-
minimal: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL",
|
|
685
|
-
low: "MODEL_GOOGLE_GEMINI_3_0_FLASH_LOW",
|
|
686
|
-
medium: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM",
|
|
687
|
-
high: "MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH",
|
|
688
|
-
},
|
|
689
|
-
[Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
|
|
690
|
-
),
|
|
691
|
-
// GLM-5.2 200K — only the base wire UID `glm-5-2` is free on Devin's
|
|
692
|
-
// Coding Plan (verified via streamDevin: `glm-5-2-none` and `glm-5-2-max`
|
|
693
|
-
// both return "weekly usage quota exhausted" while `glm-5-2` streams
|
|
694
|
-
// successfully). Route every effort to `glm-5-2` so the collapsed entry
|
|
695
|
-
// is always free; include the paid 200K variants as members so they are
|
|
696
|
-
// hidden from the model list. The 1M-context variants stay as separate
|
|
697
|
-
// paid entries (collapsed below).
|
|
698
|
-
{
|
|
699
|
-
id: "glm-5-2",
|
|
700
|
-
name: "GLM-5.2",
|
|
701
|
-
members: ["glm-5-2", "glm-5-2-none", "glm-5-2-max"],
|
|
702
|
-
routing: {
|
|
703
|
-
[Effort.High]: "glm-5-2",
|
|
704
|
-
[Effort.XHigh]: "glm-5-2",
|
|
705
|
-
},
|
|
706
|
-
thinking: {
|
|
707
|
-
mode: "effort",
|
|
708
|
-
efforts: [Effort.High, Effort.XHigh],
|
|
709
|
-
requiresEffort: true,
|
|
710
|
-
},
|
|
711
|
-
},
|
|
712
|
-
// GLM-5.2 1M — paid variants that consume weekly quota. Collapse the
|
|
713
|
-
// three 1M-context variants into one entry with proper effort routing.
|
|
714
|
-
tierFamily(
|
|
715
|
-
"glm-5-2-1m",
|
|
716
|
-
"GLM-5.2 1M",
|
|
717
|
-
{
|
|
718
|
-
off: "glm-5-2-none-1m",
|
|
719
|
-
high: "glm-5-2-1m",
|
|
720
|
-
xhigh: "glm-5-2-max-1m",
|
|
721
|
-
},
|
|
722
|
-
[Effort.High, Effort.XHigh],
|
|
723
|
-
),
|
|
724
|
-
],
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
/** Cursor's Grok tier tokens equal the effort id, so routing is a direct map. */
|
|
728
|
-
const CURSOR_GROK_45_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
|
|
729
|
-
const CURSOR_GROK_46_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
|
|
730
|
-
/** Cursor GPT-5.6 (Luna/Sol/Terra) serves the full five-tier `low..max` scale. */
|
|
731
|
-
const CURSOR_GPT_56_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
|
|
732
|
-
|
|
733
283
|
/**
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* service-tier lane (`-low-fast`, …). Each lane collapses into its own logical
|
|
737
|
-
* model — the `-fast` axis is a sibling family, never a second routing
|
|
738
|
-
* dimension — with effort routing onto the live sibling wire ids. The mandatory
|
|
739
|
-
* `-medium` default is the tier Cursor's Start plan serves at fixed settings;
|
|
740
|
-
* the `-low` floor is refused there (issue #9478).
|
|
284
|
+
* The family a template yields for `id` (logical id, member, or alias), or
|
|
285
|
+
* undefined when no template matches or the revision is out of range.
|
|
741
286
|
*/
|
|
742
|
-
function
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
efforts,
|
|
754
|
-
`cursor-grok-${version}-${Effort.Medium}${suffix}`,
|
|
755
|
-
);
|
|
756
|
-
};
|
|
757
|
-
return [build(false), build(true)];
|
|
287
|
+
function templateFamilyFor(templates: readonly VariantFamilyTemplate[], id: string): EffortVariantFamily | undefined {
|
|
288
|
+
for (const template of templates) {
|
|
289
|
+
const match = template.pattern.exec(id);
|
|
290
|
+
if (match === null) continue;
|
|
291
|
+
let rev: string | undefined;
|
|
292
|
+
for (let group = 1; group < match.length && rev === undefined; group++) rev = match[group];
|
|
293
|
+
if (rev === undefined) continue;
|
|
294
|
+
const family = instantiateTemplate(template, rev);
|
|
295
|
+
if (family !== undefined) return family;
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
758
298
|
}
|
|
759
299
|
|
|
760
300
|
/**
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
* service-tier lane (`gpt-5.6-luna-high-fast`, …). Same shape as Devin's
|
|
764
|
-
* `devinGpt56Families`, but cursor keys the version with a dot, marks the
|
|
765
|
-
* thinking-off tier `-none`, and names the fast lane with `-fast` (Devin uses
|
|
766
|
-
* `-priority`). Each lane collapses into its own logical model — `-fast` is a
|
|
767
|
-
* sibling SKU, never a second routing dimension — while the 1M / Max Mode SKU
|
|
768
|
-
* stays a separate row (handled by discovery's context-window resolution).
|
|
301
|
+
* Concrete families for every revision present in `ids`, one per
|
|
302
|
+
* instantiated id, skipping ids a concrete family in `table` already owns.
|
|
769
303
|
*/
|
|
770
|
-
function
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
max: `${base}-max${suffix}`,
|
|
784
|
-
},
|
|
785
|
-
CURSOR_GPT_56_EFFORTS,
|
|
786
|
-
);
|
|
787
|
-
};
|
|
788
|
-
return [build(false), build(true)];
|
|
304
|
+
function instantiateTemplates(table: VariantCollapseTable, ids: Iterable<string>): EffortVariantFamily[] {
|
|
305
|
+
const templates = table.templates;
|
|
306
|
+
if (templates === undefined || templates.length === 0) return [];
|
|
307
|
+
const seen = new Set<string>();
|
|
308
|
+
for (const family of table.families) seen.add(family.id);
|
|
309
|
+
const out: EffortVariantFamily[] = [];
|
|
310
|
+
for (const id of ids) {
|
|
311
|
+
const family = templateFamilyFor(templates, id);
|
|
312
|
+
if (family === undefined || seen.has(family.id)) continue;
|
|
313
|
+
seen.add(family.id);
|
|
314
|
+
out.push(family);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
789
317
|
}
|
|
790
318
|
|
|
791
|
-
/**
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
};
|
|
319
|
+
/** Provider id → reviewed collapse table, built once from the compiled vocabulary. */
|
|
320
|
+
function buildCompiledTables(): Readonly<Record<string, VariantCollapseTable>> {
|
|
321
|
+
const { variantFamilies, providerAliases } = collapseVocabulary();
|
|
322
|
+
const tables: Record<
|
|
323
|
+
string,
|
|
324
|
+
{ families: EffortVariantFamily[]; templates?: VariantFamilyTemplate[]; providerAliases?: Record<string, string> }
|
|
325
|
+
> = {};
|
|
326
|
+
for (const compiled of variantFamilies) {
|
|
327
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: compiled stratum walker assigns during traversal
|
|
328
|
+
const table = (tables[compiled.provider] ??= { families: [] });
|
|
329
|
+
if (compiled.id.includes(REVISION_PLACEHOLDER)) {
|
|
330
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: compiled stratum walker assigns during traversal
|
|
331
|
+
(table.templates ??= []).push(compiledTemplate(compiled));
|
|
332
|
+
} else {
|
|
333
|
+
table.families.push(compiledFamily(compiled));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const [provider, aliases] of Object.entries(providerAliases)) {
|
|
337
|
+
tables[provider] ??= { families: [] };
|
|
338
|
+
tables[provider].providerAliases = aliases;
|
|
339
|
+
}
|
|
340
|
+
return tables;
|
|
341
|
+
}
|
|
801
342
|
|
|
802
343
|
type CursorTierToken = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
803
344
|
|
|
@@ -940,13 +481,16 @@ function deriveCursorEffortFamilies<TSpec extends VariantSpecLike>(specs: readon
|
|
|
940
481
|
return families;
|
|
941
482
|
}
|
|
942
483
|
|
|
943
|
-
/** Provider id →
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
484
|
+
/** Provider id → reviewed collapse table. The CCA providers diverge on thinking transport. */
|
|
485
|
+
const REVIEWED_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTable>> = buildCompiledTables();
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Return the reviewed collapse table compiled for `provider`, when declared.
|
|
489
|
+
* Discovery uses this accessor to preserve injectable table seams.
|
|
490
|
+
*/
|
|
491
|
+
export function reviewedCollapseTable(provider: string): VariantCollapseTable | undefined {
|
|
492
|
+
return REVIEWED_COLLAPSE_TABLES[provider] ?? REVIEWED_COLLAPSE_TABLES[provider.toLowerCase()];
|
|
493
|
+
}
|
|
950
494
|
|
|
951
495
|
/**
|
|
952
496
|
* The global automatic rule: derive an `X` + `X-thinking` family for every
|
|
@@ -955,7 +499,7 @@ export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTab
|
|
|
955
499
|
* - known pricing must match — all-zero cost rows count as unknown
|
|
956
500
|
* (aggregators routinely ship them), but twins that BOTH carry real,
|
|
957
501
|
* differing prices are distinct SKUs and never merge,
|
|
958
|
-
* - ids claimed by the provider's
|
|
502
|
+
* - ids claimed by the provider's reviewed table are skipped (curation wins).
|
|
959
503
|
* The capability surface prefers the thinking member's metadata, then the
|
|
960
504
|
* bare member's, then the canonical deriver (aggregators often ship
|
|
961
505
|
* `reasoning: false` and no thinking config on the twin), then a budget
|
|
@@ -965,6 +509,7 @@ export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTab
|
|
|
965
509
|
export function deriveThinkingPairFamilies<TSpec extends VariantSpecLike>(
|
|
966
510
|
specs: readonly TSpec[],
|
|
967
511
|
table?: VariantCollapseTable,
|
|
512
|
+
provider?: string,
|
|
968
513
|
): EffortVariantFamily[] {
|
|
969
514
|
const byId = new Map<string, TSpec>();
|
|
970
515
|
for (const spec of specs) {
|
|
@@ -973,20 +518,19 @@ export function deriveThinkingPairFamilies<TSpec extends VariantSpecLike>(
|
|
|
973
518
|
const claimed = table ? getAliasIndex(table) : undefined;
|
|
974
519
|
const families: EffortVariantFamily[] = [];
|
|
975
520
|
for (const spec of specs) {
|
|
976
|
-
const
|
|
521
|
+
const collapsed = provider === undefined ? undefined : collapseVariantId(provider, spec.id);
|
|
522
|
+
const baseId = collapsed?.thinkingVariant ? collapsed.logicalId : stripThinkingVariantSuffix(spec.id);
|
|
977
523
|
if (baseId === undefined || baseId === spec.id) continue;
|
|
978
524
|
const base = byId.get(baseId);
|
|
979
525
|
if (!base) continue;
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
claimed.
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
526
|
+
if (
|
|
527
|
+
claimed &&
|
|
528
|
+
(claimed.resolve(spec.id) !== undefined ||
|
|
529
|
+
claimed.resolve(baseId) !== undefined ||
|
|
530
|
+
claimed.isFamily(spec.id) ||
|
|
531
|
+
claimed.isFamily(baseId))
|
|
532
|
+
) {
|
|
533
|
+
continue;
|
|
990
534
|
}
|
|
991
535
|
if (spec.api !== base.api) continue;
|
|
992
536
|
const specPriced = spec.cost.input !== 0 || spec.cost.output !== 0;
|
|
@@ -1017,14 +561,6 @@ export function deriveThinkingPairFamilies<TSpec extends VariantSpecLike>(
|
|
|
1017
561
|
return families;
|
|
1018
562
|
}
|
|
1019
563
|
|
|
1020
|
-
const DEFAULT_PAIR_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
1021
|
-
|
|
1022
|
-
/**
|
|
1023
|
-
* Surface fallback chain: thinking member → bare member → canonical deriver →
|
|
1024
|
-
* budget default. `requiresEffort` is dropped from every source: the COLLAPSED
|
|
1025
|
-
* pair can disable thinking (off routes to the bare backing id), even though
|
|
1026
|
-
* the thinking member alone cannot.
|
|
1027
|
-
*/
|
|
1028
564
|
function derivePairThinkingSurface(
|
|
1029
565
|
thinkingSpec: VariantSpecLike,
|
|
1030
566
|
baseSpec: VariantSpecLike,
|
|
@@ -1034,10 +570,12 @@ function derivePairThinkingSurface(
|
|
|
1034
570
|
const { effortRouting: _routing, suppressWhenOff: _suppress, requiresEffort: _required, ...surface } = baked;
|
|
1035
571
|
return surface;
|
|
1036
572
|
}
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
573
|
+
const { compat: _compat, ...policySpec } = thinkingSpec;
|
|
574
|
+
const derived = resolveModelPolicy({
|
|
575
|
+
...policySpec,
|
|
576
|
+
reasoning: true,
|
|
577
|
+
thinking: undefined,
|
|
578
|
+
}).thinking;
|
|
1041
579
|
if (derived && derived.efforts.length > 0) {
|
|
1042
580
|
const { effortRouting: _dRouting, suppressWhenOff: _dSuppress, requiresEffort: _dRequired, ...surface } = derived;
|
|
1043
581
|
return surface;
|
|
@@ -1048,25 +586,25 @@ function derivePairThinkingSurface(
|
|
|
1048
586
|
/**
|
|
1049
587
|
* True when `spec` is the output of collapsing rather than a raw upstream
|
|
1050
588
|
* member. `thinking.effortRouting` is written only by collapsing; the
|
|
1051
|
-
* `requestModelId` arm is scoped to the provider's
|
|
589
|
+
* `requestModelId` arm is scoped to the provider's reviewed-table family ids so
|
|
1052
590
|
* unrelated carriers (GitHub Copilot `-1m` context variants) never match.
|
|
1053
591
|
*/
|
|
1054
|
-
export function
|
|
592
|
+
export function isCollapsedVariantSpec(spec: VariantSpecLike): boolean {
|
|
1055
593
|
if (spec.thinking?.effortRouting !== undefined) {
|
|
1056
594
|
return true;
|
|
1057
595
|
}
|
|
1058
596
|
if (spec.requestModelId === undefined) {
|
|
1059
597
|
return false;
|
|
1060
598
|
}
|
|
1061
|
-
const table =
|
|
1062
|
-
return table !== undefined && getAliasIndex(table).
|
|
599
|
+
const table = reviewedCollapseTable(spec.provider);
|
|
600
|
+
return table !== undefined && getAliasIndex(table).isFamily(spec.id);
|
|
1063
601
|
}
|
|
1064
602
|
|
|
1065
603
|
/**
|
|
1066
604
|
* Re-point a stale collapsed spec whose `requestModelId` or routing still
|
|
1067
605
|
* targets a retired wire id. Collapsed snapshots (bundled catalog, cache
|
|
1068
606
|
* rows, previous-generation fallbacks) pass through collapsing untouched, so
|
|
1069
|
-
* a
|
|
607
|
+
* a reviewed-table routing fix would otherwise never reach them. Only retired
|
|
1070
608
|
* targets are rewritten — presence-filtered routing decisions from live
|
|
1071
609
|
* discovery stay authoritative for everything else. Per retired entry the
|
|
1072
610
|
* table's route for that effort wins, then the off/first-live-member wire id,
|
|
@@ -1078,12 +616,13 @@ function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
|
|
|
1078
616
|
family: EffortVariantFamily,
|
|
1079
617
|
retired: ReadonlySet<string>,
|
|
1080
618
|
): TSpec {
|
|
1081
|
-
const
|
|
619
|
+
const currentThinking = spec.thinking;
|
|
620
|
+
const routing = currentThinking?.effortRouting;
|
|
1082
621
|
const requestRetired = spec.requestModelId !== undefined && retired.has(spec.requestModelId);
|
|
1083
622
|
let routingRetired = false;
|
|
1084
623
|
if (routing !== undefined) {
|
|
1085
|
-
for (const key
|
|
1086
|
-
const target = routing[key
|
|
624
|
+
for (const key of VARIANT_ROUTING_KEYS) {
|
|
625
|
+
const target = routing[key];
|
|
1087
626
|
if (target !== undefined && retired.has(target)) {
|
|
1088
627
|
routingRetired = true;
|
|
1089
628
|
break;
|
|
@@ -1096,10 +635,9 @@ function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
|
|
|
1096
635
|
const fallbackWireId =
|
|
1097
636
|
offTarget !== undefined && !retired.has(offTarget) ? offTarget : family.members.find(id => !retired.has(id));
|
|
1098
637
|
const next: TSpec = { ...spec };
|
|
1099
|
-
if (routingRetired && routing !== undefined) {
|
|
638
|
+
if (routingRetired && routing !== undefined && currentThinking !== undefined) {
|
|
1100
639
|
const nextRouting: Partial<Record<Effort | "off", string>> = {};
|
|
1101
|
-
for (const
|
|
1102
|
-
const effortKey = key as Effort | "off";
|
|
640
|
+
for (const effortKey of VARIANT_ROUTING_KEYS) {
|
|
1103
641
|
const target = routing[effortKey];
|
|
1104
642
|
if (target === undefined) continue;
|
|
1105
643
|
if (!retired.has(target)) {
|
|
@@ -1113,7 +651,7 @@ function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
|
|
|
1113
651
|
nextRouting[effortKey] = fallbackWireId;
|
|
1114
652
|
}
|
|
1115
653
|
}
|
|
1116
|
-
next.thinking = { ...
|
|
654
|
+
next.thinking = { ...currentThinking, effortRouting: nextRouting };
|
|
1117
655
|
}
|
|
1118
656
|
if (requestRetired) {
|
|
1119
657
|
if (fallbackWireId !== undefined && fallbackWireId !== spec.id) {
|
|
@@ -1130,7 +668,7 @@ function reconcileRetiredRouting<TSpec extends VariantSpecLike>(
|
|
|
1130
668
|
* prev-generation snapshots freeze a family's transport, budgets, and routing;
|
|
1131
669
|
* discovery emits the canonical id but the exact-id merge never overwrites a
|
|
1132
670
|
* stale `family.id` row (e.g. `gemini-3.1-pro`) nor a recycled `extraAliases`
|
|
1133
|
-
* row (e.g. `gemini-3-flash`). This re-applies the
|
|
671
|
+
* row (e.g. `gemini-3-flash`). This re-applies the reviewed-table family's thinking,
|
|
1134
672
|
* routing, and default wire id while keeping the spec id (load-bearing for exact
|
|
1135
673
|
* selectors and bundled lookups). Returns `spec` by reference when unchanged.
|
|
1136
674
|
*/
|
|
@@ -1142,19 +680,20 @@ function refreshCollapsedThinking<TSpec extends VariantSpecLike>(
|
|
|
1142
680
|
// Scope snapshot self-heal to families carrying a curated per-effort budget
|
|
1143
681
|
// contract (Antigravity gemini-3.x). Their routing targets are all verified
|
|
1144
682
|
// live, so rebuilding routing here is safe; families without `effortBudgets`
|
|
1145
|
-
// (derived `X`/`X-thinking` pairs, claude pairs) keep
|
|
1146
|
-
// snapshot routing untouched.
|
|
1147
|
-
|
|
683
|
+
// (derived `X`/`X-thinking` pairs, claude pairs, surface-less renames) keep
|
|
684
|
+
// their presence-filtered snapshot routing untouched.
|
|
685
|
+
const familyThinking = family.thinking;
|
|
686
|
+
if (!spec.reasoning || familyThinking?.effortBudgets === undefined) return spec;
|
|
1148
687
|
const routing: Partial<Record<Effort | "off", string>> = {};
|
|
1149
688
|
let hasRouting = false;
|
|
1150
|
-
for (const effortKey
|
|
1151
|
-
const target = family.routing[effortKey
|
|
689
|
+
for (const effortKey of VARIANT_ROUTING_KEYS) {
|
|
690
|
+
const target = family.routing[effortKey];
|
|
1152
691
|
if (target !== undefined && !retired?.has(target)) {
|
|
1153
|
-
routing[effortKey
|
|
692
|
+
routing[effortKey] = target;
|
|
1154
693
|
hasRouting = true;
|
|
1155
694
|
}
|
|
1156
695
|
}
|
|
1157
|
-
const thinking: ThinkingConfig = { ...
|
|
696
|
+
const thinking: ThinkingConfig = { ...familyThinking };
|
|
1158
697
|
if (hasRouting) thinking.effortRouting = routing;
|
|
1159
698
|
if (family.suppressWhenOff) thinking.suppressWhenOff = true;
|
|
1160
699
|
const offTarget = family.routing.off;
|
|
@@ -1191,8 +730,8 @@ function reconcileDefaultMember<TSpec extends VariantSpecLike>(
|
|
|
1191
730
|
if (target === undefined || spec.requestModelId === target) return spec;
|
|
1192
731
|
const routing = spec.thinking?.effortRouting;
|
|
1193
732
|
if (routing === undefined) return spec;
|
|
1194
|
-
for (const key
|
|
1195
|
-
if (routing[key
|
|
733
|
+
for (const key of VARIANT_ROUTING_KEYS) {
|
|
734
|
+
if (routing[key] === target) return { ...spec, requestModelId: target };
|
|
1196
735
|
}
|
|
1197
736
|
return spec;
|
|
1198
737
|
}
|
|
@@ -1202,7 +741,7 @@ function reconcileDefaultMember<TSpec extends VariantSpecLike>(
|
|
|
1202
741
|
* through verbatim (by reference), order preserved; the collapsed spec
|
|
1203
742
|
* replaces the first occurrence of its family.
|
|
1204
743
|
*/
|
|
1205
|
-
|
|
744
|
+
function collapseWithTable<TSpec extends VariantSpecLike>(
|
|
1206
745
|
specs: readonly TSpec[],
|
|
1207
746
|
table: VariantCollapseTable,
|
|
1208
747
|
): TSpec[] {
|
|
@@ -1216,7 +755,9 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1216
755
|
/** spec ids that belong to a touched family (members + logical id). */
|
|
1217
756
|
const familyIdBySpecId = new Map<string, string>();
|
|
1218
757
|
|
|
1219
|
-
|
|
758
|
+
const instantiated = instantiateTemplates(table, byId.keys());
|
|
759
|
+
const families = instantiated.length === 0 ? table.families : [...table.families, ...instantiated];
|
|
760
|
+
for (const family of families) {
|
|
1220
761
|
const retired =
|
|
1221
762
|
family.retiredMembers !== undefined && family.retiredMembers.length > 0
|
|
1222
763
|
? new Set(family.retiredMembers)
|
|
@@ -1233,7 +774,7 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1233
774
|
if (rawPresent.length === 0) {
|
|
1234
775
|
// Inert (no members) or already collapsed (pass-through). A stale
|
|
1235
776
|
// family.id-keyed snapshot is refreshed in place from the current
|
|
1236
|
-
//
|
|
777
|
+
// reviewed-table family (transport/budgets/routing); retired targets drop.
|
|
1237
778
|
// Recycled extraAliases rows are healed in a later pass.
|
|
1238
779
|
const refreshed =
|
|
1239
780
|
existing !== undefined && existingCollapsed
|
|
@@ -1249,31 +790,36 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1249
790
|
for (const id of rawPresent) familyIdBySpecId.set(id, family.id);
|
|
1250
791
|
if (existing) familyIdBySpecId.set(family.id, family.id);
|
|
1251
792
|
|
|
1252
|
-
if (existingCollapsed) {
|
|
793
|
+
if (existingCollapsed && reconciled !== undefined) {
|
|
1253
794
|
// Mixed input: the collapsed entry wins; stale raw members are deduped
|
|
1254
795
|
// away. Retired targets are re-pointed first, then the default wire id
|
|
1255
796
|
// prefers the family's declared member when live and otherwise falls
|
|
1256
797
|
// back to the first member the account actually advertised.
|
|
1257
|
-
replacement.set(family.id, reconcileDefaultMember(reconciled
|
|
798
|
+
replacement.set(family.id, reconcileDefaultMember(reconciled, family, new Set(rawPresent)));
|
|
1258
799
|
continue;
|
|
1259
800
|
}
|
|
1260
801
|
|
|
1261
|
-
const memberSpecs =
|
|
802
|
+
const memberSpecs: TSpec[] = [];
|
|
803
|
+
for (const id of rawPresent) {
|
|
804
|
+
const member = byId.get(id);
|
|
805
|
+
if (member !== undefined) memberSpecs.push(member);
|
|
806
|
+
}
|
|
807
|
+
const firstMember = memberSpecs[0];
|
|
808
|
+
if (firstMember === undefined) continue;
|
|
1262
809
|
const presentSet = new Set(rawPresent);
|
|
1263
810
|
const routing: Partial<Record<Effort | "off", string>> = {};
|
|
1264
811
|
let hasRouting = false;
|
|
1265
812
|
let hasEffortRoute = false;
|
|
1266
813
|
let usedAbsentEffortRoute = false;
|
|
1267
|
-
for (const
|
|
1268
|
-
const target = family.routing[
|
|
1269
|
-
const effort = effortKey as Effort | "off";
|
|
814
|
+
for (const effort of VARIANT_ROUTING_KEYS) {
|
|
815
|
+
const target = family.routing[effort];
|
|
1270
816
|
const targetPresent = target !== undefined && presentSet.has(target);
|
|
1271
817
|
const preserveAbsentEffort =
|
|
1272
818
|
target !== undefined && effort !== "off" && family.preserveAbsentEffortRoutes === true;
|
|
1273
819
|
if (target !== undefined && (targetPresent || preserveAbsentEffort) && !retired?.has(target)) {
|
|
1274
820
|
routing[effort] = target;
|
|
1275
821
|
hasRouting = true;
|
|
1276
|
-
if (
|
|
822
|
+
if (effort !== "off") hasEffortRoute = true;
|
|
1277
823
|
if (!targetPresent && effort !== "off") usedAbsentEffortRoute = true;
|
|
1278
824
|
}
|
|
1279
825
|
}
|
|
@@ -1281,16 +827,19 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1281
827
|
// A family that routes efforts to a live thinking backing id reasons
|
|
1282
828
|
// even when upstream metadata forgot to mark the members.
|
|
1283
829
|
const reasoning = memberSpecs.some(spec => spec.reasoning) || hasEffortRoute;
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
if (
|
|
830
|
+
const familyThinking = family.thinking;
|
|
831
|
+
const thinking: ThinkingConfig | undefined = familyThinking ? { ...familyThinking } : undefined;
|
|
832
|
+
if (thinking !== undefined) {
|
|
833
|
+
if (hasRouting) thinking.effortRouting = routing;
|
|
834
|
+
if (family.suppressWhenOff) thinking.suppressWhenOff = true;
|
|
835
|
+
}
|
|
1287
836
|
|
|
1288
837
|
const input: ("text" | "image")[] = [];
|
|
1289
838
|
if (memberSpecs.some(spec => spec.input.includes("text"))) input.push("text");
|
|
1290
839
|
if (memberSpecs.some(spec => spec.input.includes("image"))) input.push("image");
|
|
1291
840
|
|
|
1292
841
|
const collapsed: TSpec = {
|
|
1293
|
-
...
|
|
842
|
+
...firstMember,
|
|
1294
843
|
id: family.id,
|
|
1295
844
|
name: family.name,
|
|
1296
845
|
reasoning,
|
|
@@ -1309,16 +858,20 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1309
858
|
? family.defaultMember
|
|
1310
859
|
: undefined;
|
|
1311
860
|
const defaultWireId = preferredDefault ?? rawPresent.find(id => !retired?.has(id)) ?? rawPresent[0];
|
|
861
|
+
if (defaultWireId === undefined) continue;
|
|
1312
862
|
if (defaultWireId === family.id) {
|
|
1313
863
|
if (usedAbsentEffortRoute) {
|
|
1314
|
-
collapsed.requestModelId = defaultWireId
|
|
864
|
+
collapsed.requestModelId = defaultWireId;
|
|
1315
865
|
} else {
|
|
1316
866
|
delete collapsed.requestModelId;
|
|
1317
867
|
}
|
|
1318
868
|
} else {
|
|
1319
|
-
collapsed.requestModelId = defaultWireId
|
|
869
|
+
collapsed.requestModelId = defaultWireId;
|
|
1320
870
|
}
|
|
1321
|
-
|
|
871
|
+
// A surface-less family (single wire id, uid-encoded effort) keeps
|
|
872
|
+
// `reasoning` but carries no thinking: every tier would resolve to the
|
|
873
|
+
// same upstream id, so there is nothing to select.
|
|
874
|
+
if (reasoning && thinking !== undefined) {
|
|
1322
875
|
collapsed.thinking = thinking;
|
|
1323
876
|
} else {
|
|
1324
877
|
delete collapsed.thinking;
|
|
@@ -1359,7 +912,8 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1359
912
|
}
|
|
1360
913
|
if (emitted.has(familyId)) continue;
|
|
1361
914
|
emitted.add(familyId);
|
|
1362
|
-
|
|
915
|
+
const familySpec = replacement.get(familyId);
|
|
916
|
+
if (familySpec !== undefined) out.push(familySpec);
|
|
1363
917
|
}
|
|
1364
918
|
return out;
|
|
1365
919
|
}
|
|
@@ -1413,13 +967,17 @@ function resolveCollapsedModelReference(
|
|
|
1413
967
|
}
|
|
1414
968
|
|
|
1415
969
|
/**
|
|
1416
|
-
* Collapse a full mixed-provider list: per provider, the
|
|
970
|
+
* Collapse a full mixed-provider list: per provider, the compiled reviewed table, Cursor's
|
|
1417
971
|
* conservative live effort-sibling rule, and the automatic `X`/`X-thinking`
|
|
1418
972
|
* pair rule. Used by the catalog generator; the runtime equivalent lives at
|
|
1419
973
|
* the model-manager merge point. Output is regrouped by provider — callers
|
|
1420
974
|
* re-sort.
|
|
1421
975
|
*/
|
|
1422
|
-
export function
|
|
976
|
+
export function collapseVariants<TSpec extends VariantSpecLike>(
|
|
977
|
+
specs: readonly TSpec[],
|
|
978
|
+
opts?: { table?: VariantCollapseTable },
|
|
979
|
+
): TSpec[] {
|
|
980
|
+
if (opts?.table !== undefined) return collapseWithTable(specs, opts.table);
|
|
1423
981
|
const byProvider = new Map<string, TSpec[]>();
|
|
1424
982
|
for (const spec of specs) {
|
|
1425
983
|
const slice = byProvider.get(spec.provider);
|
|
@@ -1431,17 +989,17 @@ export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecL
|
|
|
1431
989
|
}
|
|
1432
990
|
const out: TSpec[] = [];
|
|
1433
991
|
for (const [provider, slice] of byProvider) {
|
|
1434
|
-
const table =
|
|
1435
|
-
let result = table ?
|
|
992
|
+
const table = reviewedCollapseTable(provider);
|
|
993
|
+
let result = table ? collapseWithTable(slice, table) : slice;
|
|
1436
994
|
if (provider === "cursor") {
|
|
1437
995
|
const cursorDerived = deriveCursorEffortFamilies(result);
|
|
1438
996
|
if (cursorDerived.length > 0) {
|
|
1439
|
-
result =
|
|
997
|
+
result = collapseWithTable(result, { families: cursorDerived });
|
|
1440
998
|
}
|
|
1441
999
|
}
|
|
1442
|
-
const derived = deriveThinkingPairFamilies(result, table);
|
|
1000
|
+
const derived = deriveThinkingPairFamilies(result, table, provider);
|
|
1443
1001
|
if (derived.length > 0) {
|
|
1444
|
-
result =
|
|
1002
|
+
result = collapseWithTable(result, { families: derived });
|
|
1445
1003
|
}
|
|
1446
1004
|
registerCollapsedVariantAliases(provider, result);
|
|
1447
1005
|
out.push(...result);
|
|
@@ -1457,56 +1015,105 @@ export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecL
|
|
|
1457
1015
|
* logical specs so thinking wire defaults stay resolved. Untouched entries
|
|
1458
1016
|
* pass through by reference.
|
|
1459
1017
|
*/
|
|
1460
|
-
export function
|
|
1461
|
-
const collapsed =
|
|
1018
|
+
export function collapseBuiltVariants<TApi extends Api>(models: readonly Model<TApi>[]): Model<TApi>[] {
|
|
1019
|
+
const collapsed = collapseVariants(models);
|
|
1462
1020
|
const inputRefs = new Set<Model<TApi>>(models);
|
|
1463
|
-
return collapsed.map(model =>
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1021
|
+
return collapsed.map(model => (inputRefs.has(model) ? model : buildModel(projectModelSpec(model))));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function projectModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi> {
|
|
1025
|
+
const {
|
|
1026
|
+
compat: _resolvedCompat,
|
|
1027
|
+
compatConfig,
|
|
1028
|
+
identity: _identity,
|
|
1029
|
+
requiresGlyphTokenization: _requiresGlyphTokenization,
|
|
1030
|
+
supportsComputerUseConfig: _supportsComputerUseConfig,
|
|
1031
|
+
...spec
|
|
1032
|
+
} = model;
|
|
1033
|
+
return { ...spec, compat: compatConfig };
|
|
1467
1034
|
}
|
|
1468
1035
|
|
|
1469
|
-
|
|
1036
|
+
/**
|
|
1037
|
+
* Alias index over a collapse table (or the live families observed at
|
|
1038
|
+
* runtime). Templated families are materialized on first lookup of any id
|
|
1039
|
+
* they match, so a revision discovery has never advertised still resolves.
|
|
1040
|
+
*/
|
|
1041
|
+
class VariantAliasIndex {
|
|
1470
1042
|
/** lowercased retired id → replacement model id. */
|
|
1471
|
-
forward
|
|
1043
|
+
readonly #forward = new Map<string, string>();
|
|
1044
|
+
/**
|
|
1045
|
+
* lowercased provider-scoped alias → replacement model id. Kept apart from
|
|
1046
|
+
* `#forward` so bare-id and reverse lookups never see it.
|
|
1047
|
+
*/
|
|
1048
|
+
readonly providerScoped = new Map<string, string>();
|
|
1472
1049
|
/** replacement model id → retired ids that resolve to it. */
|
|
1473
|
-
reverse
|
|
1050
|
+
readonly #reverse = new Map<string, string[]>();
|
|
1474
1051
|
/** Collapsed logical ids declared by the table or observed at runtime. */
|
|
1475
|
-
familyIds
|
|
1476
|
-
|
|
1052
|
+
readonly #familyIds = new Set<string>();
|
|
1053
|
+
readonly #templates: readonly VariantFamilyTemplate[];
|
|
1477
1054
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1055
|
+
constructor(templates: readonly VariantFamilyTemplate[] = []) {
|
|
1056
|
+
this.#templates = templates;
|
|
1057
|
+
}
|
|
1480
1058
|
|
|
1481
|
-
|
|
1059
|
+
/** Register `from` → `to`; false when `from` is already claimed. */
|
|
1060
|
+
add(from: string, to: string): boolean {
|
|
1061
|
+
if (from === to || this.#forward.has(from.toLowerCase())) return false;
|
|
1062
|
+
this.#forward.set(from.toLowerCase(), to);
|
|
1063
|
+
const sources = this.#reverse.get(to);
|
|
1064
|
+
if (sources) {
|
|
1065
|
+
sources.push(from);
|
|
1066
|
+
} else {
|
|
1067
|
+
this.#reverse.set(to, [from]);
|
|
1068
|
+
}
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1482
1071
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1072
|
+
/** Register a family: its logical id plus every member and extra alias. */
|
|
1073
|
+
addFamily(family: EffortVariantFamily): void {
|
|
1074
|
+
this.#familyIds.add(family.id);
|
|
1075
|
+
for (const member of family.members) this.add(member, family.id);
|
|
1076
|
+
for (const alias of family.extraAliases ?? []) this.add(alias, family.id);
|
|
1077
|
+
}
|
|
1486
1078
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
reverse: new Map<string, string[]>(),
|
|
1491
|
-
familyIds: new Set<string>(),
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1079
|
+
markFamily(id: string): void {
|
|
1080
|
+
this.#familyIds.add(id);
|
|
1081
|
+
}
|
|
1494
1082
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1083
|
+
/** Replacement model id for a retired/member id (case-insensitive). */
|
|
1084
|
+
resolve(id: string): string | undefined {
|
|
1085
|
+
const lower = id.toLowerCase();
|
|
1086
|
+
this.#learn(lower);
|
|
1087
|
+
return this.#forward.get(lower);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Whether `id` is a collapsed logical id of this index. */
|
|
1091
|
+
isFamily(id: string): boolean {
|
|
1092
|
+
this.#learn(id);
|
|
1093
|
+
return this.#familyIds.has(id);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** Retired ids that resolve to logical `id`. */
|
|
1097
|
+
sourcesOf(id: string): readonly string[] | undefined {
|
|
1098
|
+
this.#learn(id);
|
|
1099
|
+
return this.#reverse.get(id);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** Materialize the template family that owns `id`, once. */
|
|
1103
|
+
#learn(id: string): void {
|
|
1104
|
+
if (this.#templates.length === 0) return;
|
|
1105
|
+
const family = templateFamilyFor(this.#templates, id);
|
|
1106
|
+
if (family === undefined || this.#familyIds.has(family.id)) return;
|
|
1107
|
+
this.addFamily(family);
|
|
1503
1108
|
}
|
|
1504
|
-
return true;
|
|
1505
1109
|
}
|
|
1506
1110
|
|
|
1111
|
+
const dynamicAliasIndexes = new Map<string, VariantAliasIndex>();
|
|
1112
|
+
const kAliasIndex = Symbol("compat-collapse.aliasIndex");
|
|
1113
|
+
|
|
1507
1114
|
/**
|
|
1508
1115
|
* Persist aliases embedded in collapsed routing so generated catalog rows and
|
|
1509
|
-
* newly discovered families expose the same selector migrations as
|
|
1116
|
+
* newly discovered families expose the same selector migrations as reviewed tables.
|
|
1510
1117
|
*/
|
|
1511
1118
|
function registerCollapsedVariantAliases(provider: Provider, specs: readonly VariantSpecLike[]): void {
|
|
1512
1119
|
const providerId = provider.toLowerCase();
|
|
@@ -1518,51 +1125,52 @@ function registerCollapsedVariantAliases(provider: Provider, specs: readonly Var
|
|
|
1518
1125
|
for (const effort of VARIANT_ROUTING_KEYS) {
|
|
1519
1126
|
const source = routing[effort];
|
|
1520
1127
|
if (!source || source === spec.id) continue;
|
|
1521
|
-
index ??=
|
|
1522
|
-
registered =
|
|
1128
|
+
index ??= new VariantAliasIndex();
|
|
1129
|
+
registered = index.add(source, spec.id) || registered;
|
|
1523
1130
|
}
|
|
1524
1131
|
if (spec.requestModelId && spec.requestModelId !== spec.id) {
|
|
1525
|
-
index ??=
|
|
1526
|
-
registered =
|
|
1132
|
+
index ??= new VariantAliasIndex();
|
|
1133
|
+
registered = index.add(spec.requestModelId, spec.id) || registered;
|
|
1527
1134
|
}
|
|
1528
|
-
if (registered) index?.
|
|
1135
|
+
if (registered) index?.markFamily(spec.id);
|
|
1529
1136
|
}
|
|
1530
1137
|
if (index) dynamicAliasIndexes.set(providerId, index);
|
|
1531
1138
|
}
|
|
1532
1139
|
|
|
1533
1140
|
function resolveRegisteredVariantAlias(provider: Provider, normalizedModelId: string): string | undefined {
|
|
1534
1141
|
const providerId = provider.toLowerCase();
|
|
1535
|
-
const table =
|
|
1142
|
+
const table = reviewedCollapseTable(provider) ?? reviewedCollapseTable(providerId);
|
|
1536
1143
|
return (
|
|
1537
|
-
(table ? getAliasIndex(table).
|
|
1538
|
-
dynamicAliasIndexes.get(providerId)?.
|
|
1144
|
+
(table ? getAliasIndex(table).resolve(normalizedModelId) : undefined) ??
|
|
1145
|
+
dynamicAliasIndexes.get(providerId)?.resolve(normalizedModelId)
|
|
1539
1146
|
);
|
|
1540
1147
|
}
|
|
1541
1148
|
|
|
1542
1149
|
function getAliasIndex(table: VariantCollapseTable): VariantAliasIndex {
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const
|
|
1547
|
-
for (const
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
for (const alias of family.extraAliases ?? []) addVariantAlias(index, alias, family.id);
|
|
1150
|
+
const cached: unknown = Reflect.get(table, kAliasIndex);
|
|
1151
|
+
if (cached instanceof VariantAliasIndex) return cached;
|
|
1152
|
+
const index = new VariantAliasIndex(table.templates);
|
|
1153
|
+
for (const family of table.families) index.addFamily(family);
|
|
1154
|
+
for (const alias in table.providerAliases) {
|
|
1155
|
+
const target = table.providerAliases[alias];
|
|
1156
|
+
if (target !== undefined && alias !== target) index.providerScoped.set(alias.toLowerCase(), target);
|
|
1551
1157
|
}
|
|
1552
|
-
|
|
1158
|
+
Reflect.set(table, kAliasIndex, index);
|
|
1553
1159
|
return index;
|
|
1554
1160
|
}
|
|
1555
1161
|
|
|
1556
1162
|
/**
|
|
1557
|
-
* Resolve a retired effort-tier variant id
|
|
1558
|
-
* its replacement model id for `provider
|
|
1559
|
-
*
|
|
1560
|
-
*
|
|
1561
|
-
* Callers must try an exact model lookup first — a live model always wins over
|
|
1562
|
-
* an alias.
|
|
1163
|
+
* Resolve a retired effort-tier variant id, registered live alias, or
|
|
1164
|
+
* provider-scoped native alias to its replacement model id for `provider`.
|
|
1165
|
+
* Returns `undefined` when the id is unknown. Callers must try an exact model
|
|
1166
|
+
* lookup first because a live model always wins over an alias.
|
|
1563
1167
|
*/
|
|
1564
|
-
export function
|
|
1565
|
-
|
|
1168
|
+
export function resolveVariantSelector(provider: Provider, modelId: string): string | undefined {
|
|
1169
|
+
const normalized = modelId.trim().toLowerCase();
|
|
1170
|
+
const registered = resolveRegisteredVariantAlias(provider, normalized);
|
|
1171
|
+
if (registered !== undefined) return registered;
|
|
1172
|
+
const table = reviewedCollapseTable(provider);
|
|
1173
|
+
return table ? getAliasIndex(table).providerScoped.get(normalized) : undefined;
|
|
1566
1174
|
}
|
|
1567
1175
|
|
|
1568
1176
|
/** Bare-id alias hit: replacement id plus the providers declaring it. */
|
|
@@ -1578,10 +1186,10 @@ export interface BareVariantAliasHit {
|
|
|
1578
1186
|
* on unrelated providers too (e.g. a retired Cursor tier id must not resolve
|
|
1579
1187
|
* to `openai/gpt-5.4`).
|
|
1580
1188
|
*/
|
|
1581
|
-
export function
|
|
1189
|
+
export function resolveBareVariantSelector(modelId: string): BareVariantAliasHit | undefined {
|
|
1582
1190
|
const normalized = modelId.trim().toLowerCase();
|
|
1583
1191
|
const providerIds = new Set<string>();
|
|
1584
|
-
for (const provider in
|
|
1192
|
+
for (const provider in REVIEWED_COLLAPSE_TABLES) providerIds.add(provider);
|
|
1585
1193
|
for (const provider of dynamicAliasIndexes.keys()) providerIds.add(provider);
|
|
1586
1194
|
for (const provider of providerIds) {
|
|
1587
1195
|
const hit = resolveRegisteredVariantAlias(provider, normalized);
|
|
@@ -1599,15 +1207,15 @@ export function resolveBareVariantAlias(modelId: string): BareVariantAliasHit |
|
|
|
1599
1207
|
|
|
1600
1208
|
/**
|
|
1601
1209
|
* Reverse alias lookup: the retired ids that resolve to `modelId` for
|
|
1602
|
-
* `provider` via
|
|
1210
|
+
* `provider` via reviewed-table or registered live aliases. Used to re-key config
|
|
1603
1211
|
* keyed by raw member ids (models.yml `modelOverrides`, suppressed selectors)
|
|
1604
1212
|
* onto the collapsed model.
|
|
1605
1213
|
*/
|
|
1606
1214
|
export function getVariantAliasSources(provider: Provider, modelId: string): readonly string[] {
|
|
1607
1215
|
const providerId = provider.toLowerCase();
|
|
1608
|
-
const table =
|
|
1609
|
-
const staticSources = table ? getAliasIndex(table).
|
|
1610
|
-
const dynamicSources = dynamicAliasIndexes.get(providerId)?.
|
|
1216
|
+
const table = reviewedCollapseTable(provider) ?? reviewedCollapseTable(providerId);
|
|
1217
|
+
const staticSources = table ? getAliasIndex(table).sourcesOf(modelId) : undefined;
|
|
1218
|
+
const dynamicSources = dynamicAliasIndexes.get(providerId)?.sourcesOf(modelId);
|
|
1611
1219
|
if (!staticSources) return dynamicSources ?? [];
|
|
1612
1220
|
if (!dynamicSources) return staticSources;
|
|
1613
1221
|
return [...new Set([...staticSources, ...dynamicSources])];
|