@oh-my-pi/pi-catalog 18.1.2 → 18.1.4

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 CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.4] - 2026-09-02
6
+
7
+ ### Changed
8
+
9
+ - Enabled Cursor tool schema projection for supported models
10
+
11
+ ### Fixed
12
+
13
+ - Antigravity and Gemini CLI now collapse every Gemini Flash generation from 3.6 on (`gemini-3.8-flash-low/-medium/-high` and the `-tiered` alias, and future revisions) into one routed `gemini-<rev>-flash` entry via a revision-templated `variant-family`, instead of surfacing raw per-level ids until a per-revision rule lands.
14
+
15
+ ## [18.1.3] - 2026-09-02
16
+
17
+ ### Added
18
+
19
+ - Added support for Claude Fable 5.1
20
+
21
+ ### Changed
22
+
23
+ - Updated pricing and context limits for various Claude models
24
+
25
+ ### Fixed
26
+
27
+ - Claude Sonnet 5 no longer advertises unsupported mid-conversation system messages.
28
+ - Custom GLM 5.2 models on `alibaba-coding-plan` (and other blanket-GLM hosts) no longer crash startup with `AmbiguousOverlapError` ([#10553](https://github.com/can1357/oh-my-pi/issues/10553)).
29
+ - Gemini 3.7 Flash no longer offers the `minimal` thinking effort on direct google-level hosts (`google`, `google-vertex`, `opencode-zen`), which reject `thinkingLevel: MINIMAL` with a 400; budget and reasoning-effort resellers keep the tier ([#10543](https://github.com/can1357/oh-my-pi/issues/10543)).
30
+ - Fixed Alibaba Token Plan discovery for `qwen3.8-flash` to include its context limits, reasoning support, and image input.
31
+ - Z.AI GLM-5.3-Flash now uses the native API instead of failing through the unsupported Anthropic-compatible route ([#10539](https://github.com/can1357/oh-my-pi/issues/10539)).
32
+
5
33
  ## [18.1.2] - 2026-09-01
6
34
 
7
35
  ### Added
@@ -1,5 +1,6 @@
1
1
  import { Effort } from "../effort.js";
2
2
  import type { Api, Model, ModelSpec, Provider, ThinkingConfig } from "../types.js";
3
+ import { type RevisionTerm } from "./revision.js";
3
4
  /**
4
5
  * Structural bound for collapse inputs: both raw `ModelSpec`s and built
5
6
  * `Model`s qualify. (`Model.compat` is the resolved record, not the sparse
@@ -65,8 +66,27 @@ export interface EffortVariantFamily {
65
66
  /** Retired/recycled selector ids that alias to this family without being members. */
66
67
  extraAliases?: readonly string[];
67
68
  }
69
+ /**
70
+ * A reviewed family whose ids carry {@link REVISION_PLACEHOLDER}: one
71
+ * `gemini-{rev}-flash` entry stands for every Flash generation the provider
72
+ * serves with the same sibling layout. Concrete families with the same
73
+ * instantiated id take precedence.
74
+ */
75
+ export interface VariantFamilyTemplate {
76
+ /** Family with placeholders still in every id and the display name. */
77
+ family: EffortVariantFamily;
78
+ /** Revisions the template applies to; unbounded when absent. */
79
+ revision?: readonly RevisionTerm[];
80
+ /**
81
+ * Matches the logical id, any member, or any extra alias (case-insensitive);
82
+ * one alternative per id, each capturing the revision.
83
+ */
84
+ pattern: RegExp;
85
+ }
68
86
  export interface VariantCollapseTable {
69
87
  families: readonly EffortVariantFamily[];
88
+ /** Revision-templated families, instantiated against live ids. */
89
+ templates?: readonly VariantFamilyTemplate[];
70
90
  /**
71
91
  * Provider-scoped selector aliases: short native-CLI names and dotted
72
92
  * upstream spellings → logical model id. Unlike family members and
@@ -80,6 +80,13 @@ export interface CompiledEffortFamily {
80
80
  }
81
81
  /** Per-effort routing tier keys (`"off"` = thinking disabled). */
82
82
  export type VariantTier = Effort | "off";
83
+ /**
84
+ * Revision placeholder in a templated `variant-family` (`gemini-{rev}-flash`).
85
+ * A family whose id carries it is instantiated once per live revision that
86
+ * discovery advertises, so new generations of a lineage collapse without a
87
+ * new reviewed entry.
88
+ */
89
+ export declare const REVISION_PLACEHOLDER = "{rev}";
83
90
  /**
84
91
  * One reviewed provider-scoped variant family: a logical model whose provider
85
92
  * serves per-effort/thinking sibling wire ids, with explicit routing, ladder,
@@ -89,6 +96,11 @@ export interface CompiledVariantFamily {
89
96
  provider: string;
90
97
  id: string;
91
98
  name: string;
99
+ /**
100
+ * Revision constraint (`">=3.6"`) gating instantiation of a
101
+ * {@link REVISION_PLACEHOLDER} template; absent on concrete families.
102
+ */
103
+ revision?: string;
92
104
  /** Member wire ids in priority order. */
93
105
  members: string[];
94
106
  /** Preferred default wire id when live. */
@@ -502,7 +502,7 @@ export declare const CATALOG_PROVIDERS: readonly [{
502
502
  readonly id: "zai";
503
503
  readonly defaultModel: "glm-5.3";
504
504
  readonly envVars: readonly ["ZAI_API_KEY"];
505
- readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"anthropic-messages", unknown>;
505
+ readonly createModelManagerOptions: (config: ModelManagerConfig) => import("../index.js").ModelManagerOptions<"anthropic-messages" | "openai-completions", unknown>;
506
506
  readonly catalogDiscovery: {
507
507
  readonly label: "zAI";
508
508
  };
@@ -673,6 +673,11 @@ export interface ModelsDevProviderDescriptor {
673
673
  }
674
674
  /** Generic mapper that converts models.dev data using provider descriptors. */
675
675
  export declare function mapModelsDevToModels(data: Record<string, unknown>, descriptors: readonly ModelsDevProviderDescriptor[]): ModelSpec<Api>[];
676
+ /** Resolves the transport and endpoint for one Z.AI model catalog entry. */
677
+ export declare function resolveZaiApi(modelId: string): {
678
+ api: "anthropic-messages" | "openai-completions";
679
+ baseUrl: string;
680
+ };
676
681
  /** All provider descriptors for models.dev data mapping in generate-models.ts. */
677
682
  export declare const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescriptor[];
678
683
  /** Providers whose bundled catalog can receive additive models.dev updates at runtime. */
@@ -70,4 +70,5 @@ export declare const DEVIN_STATIC_MODELS: readonly ModelSpec<"devin-agent">[];
70
70
  export declare function devinModelManagerOptions(config?: DevinModelManagerConfig): ModelManagerOptions<"devin-agent">;
71
71
  export interface ZaiModelManagerConfig {
72
72
  }
73
- export declare function zaiModelManagerOptions(_config?: ZaiModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
73
+ /** Creates model-manager options for Z.AI's mixed native and Anthropic transports. */
74
+ export declare function zaiModelManagerOptions(_config?: ZaiModelManagerConfig): ModelManagerOptions<"anthropic-messages" | "openai-completions">;
@@ -879,6 +879,8 @@ export interface Model<TApi extends Api = Api> {
879
879
  * inferring it from the transport API.
880
880
  */
881
881
  requiresGlyphTokenization?: boolean;
882
+ /** Whether this model requires Cursor's tool-schema combiner projection. */
883
+ requiresCursorToolSchemaProjection?: boolean;
882
884
  /**
883
885
  * Model id to send on the wire when it differs from `id`. Used by catalog
884
886
  * variants that present one upstream model under several local entries —
@@ -1033,7 +1035,7 @@ export interface Model<TApi extends Api = Api> {
1033
1035
  * vocabulary of `buildModel`. Identical to `Model` except `compat` carries the
1034
1036
  * sparse override shape and nothing is resolved yet.
1035
1037
  */
1036
- export interface ModelSpec<TApi extends Api = Api> extends Omit<Model<TApi>, "compat" | "identity" | "compatConfig" | "requiresGlyphTokenization" | "supportsComputerUseConfig"> {
1038
+ export interface ModelSpec<TApi extends Api = Api> extends Omit<Model<TApi>, "compat" | "identity" | "compatConfig" | "requiresGlyphTokenization" | "requiresCursorToolSchemaProjection" | "supportsComputerUseConfig"> {
1037
1039
  /** Sparse compatibility overrides; resolved into `Model.compat` by `buildModel`. */
1038
1040
  compat?: CompatConfigOf<TApi>;
1039
1041
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "18.1.2",
4
+ "version": "18.1.4",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,12 +35,12 @@
35
35
  "gen:proto": "bun scripts/generate-protocols.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/omptype": "18.1.2",
39
- "@oh-my-pi/pi-utils": "18.1.2"
38
+ "@oh-my-pi/omptype": "18.1.4",
39
+ "@oh-my-pi/pi-utils": "18.1.4"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@bgotink/kdl": "0.4.0",
43
- "@oh-my-pi/pi-ai": "18.1.2",
43
+ "@oh-my-pi/pi-ai": "18.1.4",
44
44
  "@types/bun": "^1.3.14"
45
45
  },
46
46
  "engines": {
package/src/build.ts CHANGED
@@ -32,7 +32,8 @@ function isInputModalities(value: unknown): value is ("text" | "image")[] {
32
32
  * Applies resolved catalog-data axes onto the model: reviewed metadata
33
33
  * corrections (`cost-patch`, `limits-patch`, `long-context-cost`,
34
34
  * `context-window-floor`) overwrite upstream values; selection metadata
35
- * (`priority`, `apply-patch-tool-type`, `service-tier-cost`) is rule-owned;
35
+ * (`priority`, `apply-patch-tool-type`, `service-tier-cost`,
36
+ * `requires-cursor-tool-schema-projection`) is rule-owned;
36
37
  * `context-promotion-target` fills only when the spec left it unset.
37
38
  */
38
39
  function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog: Record<string, unknown>): void {
@@ -51,6 +52,12 @@ function applyCatalogAssignments<TApi extends Api>(model: Model<TApi>, catalog:
51
52
  if (applyPatchToolType === "freeform" || applyPatchToolType === "function") {
52
53
  model.applyPatchToolType = applyPatchToolType;
53
54
  }
55
+ const requiresCursorToolSchemaProjection = catalog.requiresCursorToolSchemaProjection;
56
+ if (requiresCursorToolSchemaProjection === true) {
57
+ model.requiresCursorToolSchemaProjection = true;
58
+ } else {
59
+ delete model.requiresCursorToolSchemaProjection;
60
+ }
54
61
  const contextPromotionTarget = catalog.contextPromotionTarget;
55
62
  if (typeof contextPromotionTarget === "string" && model.contextPromotionTarget === undefined) {
56
63
  model.contextPromotionTarget = contextPromotionTarget;
@@ -262,6 +262,11 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
262
262
  "limits-patch": { key: "limitsPatch", set: "catalog", shape: "object" },
263
263
  "long-context-cost": { key: "longContext", set: "catalog", shape: "object" },
264
264
  "long-usage-limit-fallback": { key: "longUsageLimitFallback", set: "catalog", shape: "scalar" },
265
+ "requires-cursor-tool-schema-projection": {
266
+ key: "requiresCursorToolSchemaProjection",
267
+ set: "catalog",
268
+ shape: "scalar",
269
+ },
265
270
  priority: { key: "priority", set: "catalog", shape: "scalar" },
266
271
  "service-tier-cost": { key: "serviceTierCost", set: "catalog", shape: "object" },
267
272
  };
@@ -13,7 +13,9 @@
13
13
  * Families come from two sources:
14
14
  * - Reviewed `variant-family` nodes in `compat/rules/taxonomy/_collapse.kdl`
15
15
  * (compiled into `rules.json`) for providers whose routing needs curation
16
- * (Antigravity tier triplets, single-member renames, recycled ids).
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`).
17
19
  * - `deriveThinkingPairFamilies`: the global automatic rule — any live
18
20
  * `X` + `X-thinking` pair (trailing or infix token) collapses into `X`,
19
21
  * routing thinking-enabled requests to `X-thinking`. Gated on identical
@@ -36,8 +38,9 @@ import { buildModel } from "../build";
36
38
  import { Effort, THINKING_EFFORTS } from "../effort";
37
39
  import type { Api, Model, ModelSpec, Provider, ThinkingConfig } from "../types";
38
40
  import { resolveModelPolicy } from "./resolve";
41
+ import { parseRevision, parseRevisionConstraint, type RevisionTerm, revisionSatisfies } from "./revision";
39
42
  import { collapseVariantId, collapseVocabulary, stripThinkingVariantSuffix } from "./taxonomy";
40
- import type { CompiledVariantFamily } from "./types";
43
+ import { type CompiledVariantFamily, REVISION_PLACEHOLDER } from "./types";
41
44
 
42
45
  const VARIANT_ROUTING_KEYS: readonly (Effort | "off")[] = ["off", ...THINKING_EFFORTS];
43
46
  const DEFAULT_PAIR_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
@@ -107,8 +110,28 @@ export interface EffortVariantFamily {
107
110
  extraAliases?: readonly string[];
108
111
  }
109
112
 
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;
129
+ }
130
+
110
131
  export interface VariantCollapseTable {
111
132
  families: readonly EffortVariantFamily[];
133
+ /** Revision-templated families, instantiated against live ids. */
134
+ templates?: readonly VariantFamilyTemplate[];
112
135
  /**
113
136
  * Provider-scoped selector aliases: short native-CLI names and dotted
114
137
  * upstream spellings → logical model id. Unlike family members and
@@ -209,13 +232,104 @@ function compiledFamily(compiled: CompiledVariantFamily): EffortVariantFamily {
209
232
  return family;
210
233
  }
211
234
 
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"),
249
+ };
250
+ if (compiled.revision !== undefined) {
251
+ const terms = parseRevisionConstraint(compiled.revision);
252
+ if (terms !== undefined) template.revision = terms;
253
+ }
254
+ return template;
255
+ }
256
+
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,
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;
281
+ }
282
+
283
+ /**
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.
286
+ */
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;
298
+ }
299
+
300
+ /**
301
+ * Concrete families for every revision present in `ids`, one per
302
+ * instantiated id, skipping ids a concrete family in `table` already owns.
303
+ */
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;
317
+ }
318
+
212
319
  /** Provider id → reviewed collapse table, built once from the compiled vocabulary. */
213
320
  function buildCompiledTables(): Readonly<Record<string, VariantCollapseTable>> {
214
321
  const { variantFamilies, providerAliases } = collapseVocabulary();
215
- const tables: Record<string, { families: EffortVariantFamily[]; providerAliases?: Record<string, string> }> = {};
322
+ const tables: Record<
323
+ string,
324
+ { families: EffortVariantFamily[]; templates?: VariantFamilyTemplate[]; providerAliases?: Record<string, string> }
325
+ > = {};
216
326
  for (const compiled of variantFamilies) {
217
- tables[compiled.provider] ??= { families: [] };
218
- tables[compiled.provider].families.push(compiledFamily(compiled));
327
+ const table = (tables[compiled.provider] ??= { families: [] });
328
+ if (compiled.id.includes(REVISION_PLACEHOLDER)) {
329
+ (table.templates ??= []).push(compiledTemplate(compiled));
330
+ } else {
331
+ table.families.push(compiledFamily(compiled));
332
+ }
219
333
  }
220
334
  for (const [provider, aliases] of Object.entries(providerAliases)) {
221
335
  tables[provider] ??= { families: [] };
@@ -407,16 +521,14 @@ export function deriveThinkingPairFamilies<TSpec extends VariantSpecLike>(
407
521
  if (baseId === undefined || baseId === spec.id) continue;
408
522
  const base = byId.get(baseId);
409
523
  if (!base) continue;
410
- if (claimed) {
411
- const forward = claimed.forward;
412
- if (
413
- forward.has(spec.id.toLowerCase()) ||
414
- forward.has(baseId.toLowerCase()) ||
415
- claimed.familyIds.has(spec.id) ||
416
- claimed.familyIds.has(baseId)
417
- ) {
418
- continue;
419
- }
524
+ if (
525
+ claimed &&
526
+ (claimed.resolve(spec.id) !== undefined ||
527
+ claimed.resolve(baseId) !== undefined ||
528
+ claimed.isFamily(spec.id) ||
529
+ claimed.isFamily(baseId))
530
+ ) {
531
+ continue;
420
532
  }
421
533
  if (spec.api !== base.api) continue;
422
534
  const specPriced = spec.cost.input !== 0 || spec.cost.output !== 0;
@@ -483,7 +595,7 @@ export function isCollapsedVariantSpec(spec: VariantSpecLike): boolean {
483
595
  return false;
484
596
  }
485
597
  const table = reviewedCollapseTable(spec.provider);
486
- return table !== undefined && getAliasIndex(table).familyIds.has(spec.id);
598
+ return table !== undefined && getAliasIndex(table).isFamily(spec.id);
487
599
  }
488
600
 
489
601
  /**
@@ -641,7 +753,9 @@ function collapseWithTable<TSpec extends VariantSpecLike>(
641
753
  /** spec ids that belong to a touched family (members + logical id). */
642
754
  const familyIdBySpecId = new Map<string, string>();
643
755
 
644
- for (const family of table.families) {
756
+ const instantiated = instantiateTemplates(table, byId.keys());
757
+ const families = instantiated.length === 0 ? table.families : [...table.families, ...instantiated];
758
+ for (const family of families) {
645
759
  const retired =
646
760
  family.retiredMembers !== undefined && family.retiredMembers.length > 0
647
761
  ? new Set(family.retiredMembers)
@@ -917,39 +1031,84 @@ function projectModelSpec<TApi extends Api>(model: Model<TApi>): ModelSpec<TApi>
917
1031
  return { ...spec, compat: compatConfig };
918
1032
  }
919
1033
 
1034
+ /**
1035
+ * Alias index over a collapse table (or the live families observed at
1036
+ * runtime). Templated families are materialized on first lookup of any id
1037
+ * they match, so a revision discovery has never advertised still resolves.
1038
+ */
920
1039
  class VariantAliasIndex {
921
1040
  /** lowercased retired id → replacement model id. */
922
- readonly forward = new Map<string, string>();
1041
+ readonly #forward = new Map<string, string>();
923
1042
  /**
924
1043
  * lowercased provider-scoped alias → replacement model id. Kept apart from
925
- * `forward` so bare-id and reverse lookups never see it.
1044
+ * `#forward` so bare-id and reverse lookups never see it.
926
1045
  */
927
1046
  readonly providerScoped = new Map<string, string>();
928
1047
  /** replacement model id → retired ids that resolve to it. */
929
- readonly reverse = new Map<string, string[]>();
1048
+ readonly #reverse = new Map<string, string[]>();
930
1049
  /** Collapsed logical ids declared by the table or observed at runtime. */
931
- readonly familyIds = new Set<string>();
932
- }
1050
+ readonly #familyIds = new Set<string>();
1051
+ readonly #templates: readonly VariantFamilyTemplate[];
933
1052
 
934
- const dynamicAliasIndexes = new Map<string, VariantAliasIndex>();
935
- const kAliasIndex = Symbol("compat-collapse.aliasIndex");
1053
+ constructor(templates: readonly VariantFamilyTemplate[] = []) {
1054
+ this.#templates = templates;
1055
+ }
936
1056
 
937
- function createAliasIndex(): VariantAliasIndex {
938
- return new VariantAliasIndex();
939
- }
1057
+ /** Register `from` → `to`; false when `from` is already claimed. */
1058
+ add(from: string, to: string): boolean {
1059
+ if (from === to || this.#forward.has(from.toLowerCase())) return false;
1060
+ this.#forward.set(from.toLowerCase(), to);
1061
+ const sources = this.#reverse.get(to);
1062
+ if (sources) {
1063
+ sources.push(from);
1064
+ } else {
1065
+ this.#reverse.set(to, [from]);
1066
+ }
1067
+ return true;
1068
+ }
940
1069
 
941
- function addVariantAlias(index: VariantAliasIndex, from: string, to: string): boolean {
942
- if (from === to || index.forward.has(from.toLowerCase())) return false;
943
- index.forward.set(from.toLowerCase(), to);
944
- const sources = index.reverse.get(to);
945
- if (sources) {
946
- sources.push(from);
947
- } else {
948
- index.reverse.set(to, [from]);
1070
+ /** Register a family: its logical id plus every member and extra alias. */
1071
+ addFamily(family: EffortVariantFamily): void {
1072
+ this.#familyIds.add(family.id);
1073
+ for (const member of family.members) this.add(member, family.id);
1074
+ for (const alias of family.extraAliases ?? []) this.add(alias, family.id);
1075
+ }
1076
+
1077
+ markFamily(id: string): void {
1078
+ this.#familyIds.add(id);
1079
+ }
1080
+
1081
+ /** Replacement model id for a retired/member id (case-insensitive). */
1082
+ resolve(id: string): string | undefined {
1083
+ const lower = id.toLowerCase();
1084
+ this.#learn(lower);
1085
+ return this.#forward.get(lower);
1086
+ }
1087
+
1088
+ /** Whether `id` is a collapsed logical id of this index. */
1089
+ isFamily(id: string): boolean {
1090
+ this.#learn(id);
1091
+ return this.#familyIds.has(id);
1092
+ }
1093
+
1094
+ /** Retired ids that resolve to logical `id`. */
1095
+ sourcesOf(id: string): readonly string[] | undefined {
1096
+ this.#learn(id);
1097
+ return this.#reverse.get(id);
1098
+ }
1099
+
1100
+ /** Materialize the template family that owns `id`, once. */
1101
+ #learn(id: string): void {
1102
+ if (this.#templates.length === 0) return;
1103
+ const family = templateFamilyFor(this.#templates, id);
1104
+ if (family === undefined || this.#familyIds.has(family.id)) return;
1105
+ this.addFamily(family);
949
1106
  }
950
- return true;
951
1107
  }
952
1108
 
1109
+ const dynamicAliasIndexes = new Map<string, VariantAliasIndex>();
1110
+ const kAliasIndex = Symbol("compat-collapse.aliasIndex");
1111
+
953
1112
  /**
954
1113
  * Persist aliases embedded in collapsed routing so generated catalog rows and
955
1114
  * newly discovered families expose the same selector migrations as reviewed tables.
@@ -964,14 +1123,14 @@ function registerCollapsedVariantAliases(provider: Provider, specs: readonly Var
964
1123
  for (const effort of VARIANT_ROUTING_KEYS) {
965
1124
  const source = routing[effort];
966
1125
  if (!source || source === spec.id) continue;
967
- index ??= createAliasIndex();
968
- registered = addVariantAlias(index, source, spec.id) || registered;
1126
+ index ??= new VariantAliasIndex();
1127
+ registered = index.add(source, spec.id) || registered;
969
1128
  }
970
1129
  if (spec.requestModelId && spec.requestModelId !== spec.id) {
971
- index ??= createAliasIndex();
972
- registered = addVariantAlias(index, spec.requestModelId, spec.id) || registered;
1130
+ index ??= new VariantAliasIndex();
1131
+ registered = index.add(spec.requestModelId, spec.id) || registered;
973
1132
  }
974
- if (registered) index?.familyIds.add(spec.id);
1133
+ if (registered) index?.markFamily(spec.id);
975
1134
  }
976
1135
  if (index) dynamicAliasIndexes.set(providerId, index);
977
1136
  }
@@ -980,20 +1139,16 @@ function resolveRegisteredVariantAlias(provider: Provider, normalizedModelId: st
980
1139
  const providerId = provider.toLowerCase();
981
1140
  const table = reviewedCollapseTable(provider) ?? reviewedCollapseTable(providerId);
982
1141
  return (
983
- (table ? getAliasIndex(table).forward.get(normalizedModelId) : undefined) ??
984
- dynamicAliasIndexes.get(providerId)?.forward.get(normalizedModelId)
1142
+ (table ? getAliasIndex(table).resolve(normalizedModelId) : undefined) ??
1143
+ dynamicAliasIndexes.get(providerId)?.resolve(normalizedModelId)
985
1144
  );
986
1145
  }
987
1146
 
988
1147
  function getAliasIndex(table: VariantCollapseTable): VariantAliasIndex {
989
1148
  const cached: unknown = Reflect.get(table, kAliasIndex);
990
1149
  if (cached instanceof VariantAliasIndex) return cached;
991
- const index = createAliasIndex();
992
- for (const family of table.families) {
993
- index.familyIds.add(family.id);
994
- for (const member of family.members) addVariantAlias(index, member, family.id);
995
- for (const alias of family.extraAliases ?? []) addVariantAlias(index, alias, family.id);
996
- }
1150
+ const index = new VariantAliasIndex(table.templates);
1151
+ for (const family of table.families) index.addFamily(family);
997
1152
  for (const alias in table.providerAliases) {
998
1153
  const target = table.providerAliases[alias];
999
1154
  if (target !== undefined && alias !== target) index.providerScoped.set(alias.toLowerCase(), target);
@@ -1057,8 +1212,8 @@ export function resolveBareVariantSelector(modelId: string): BareVariantAliasHit
1057
1212
  export function getVariantAliasSources(provider: Provider, modelId: string): readonly string[] {
1058
1213
  const providerId = provider.toLowerCase();
1059
1214
  const table = reviewedCollapseTable(provider) ?? reviewedCollapseTable(providerId);
1060
- const staticSources = table ? getAliasIndex(table).reverse.get(modelId) : undefined;
1061
- const dynamicSources = dynamicAliasIndexes.get(providerId)?.reverse.get(modelId);
1215
+ const staticSources = table ? getAliasIndex(table).sourcesOf(modelId) : undefined;
1216
+ const dynamicSources = dynamicAliasIndexes.get(providerId)?.sourcesOf(modelId);
1062
1217
  if (!staticSources) return dynamicSources ?? [];
1063
1218
  if (!dynamicSources) return staticSources;
1064
1219
  return [...new Set([...staticSources, ...dynamicSources])];
@@ -846,7 +846,7 @@ function resolveAnthropicPolicy(
846
846
  allowAnthropicHeaderOverrides: false,
847
847
  supportsEagerToolInputStreaming: official,
848
848
  supportsLongCacheRetention: official,
849
- supportsMidConversationSystem: official && facts.anthropicAdaptiveGenAtLeast("4.8"),
849
+ supportsMidConversationSystem: official && !facts.family("sonnet") && facts.anthropicAdaptiveGenAtLeast("4.8"),
850
850
  supportsTurnScopedSystem: false,
851
851
  supportsMidConversationToolChanges: false,
852
852
  supportsPerMessageEffort: false,
@@ -116,13 +116,14 @@ collapse {
116
116
  effort-suffix "-minimal" tier="minimal"
117
117
  effort-suffix "-max" tier="max" except-bare-prefix="qwen"
118
118
  effort-lane-suffix "-fast" "cursor" bare-prefix="cursor-grok"
119
- effort-family "google-antigravity" "gemini-3.7-flash" "gemini-3.7-flash-tiered"
119
+ effort-family "cursor" "gpt-5.6-luna"
120
120
  routing-variant-suffix "-wm" "openai-codex" "openai-codex-device"
121
121
  variant-family "devin" "claude-opus-5" name="Claude Opus 5" { /* reviewed routing */ }
122
+ variant-family "google-antigravity" "gemini-{rev}-flash" name="Gemini {rev} Flash" revision=">=3.6" { /* per-revision template */ }
122
123
  provider-alias "devin" "opus" "claude-opus-5"
123
124
  }
124
125
  ```
125
- `variant-family` declares one reviewed provider-scoped collapsed family: positional provider and logical id, `name=` display name, and a body of `members "a" "b" …` (wire ids in priority order), `route "<tier>" "<wire-id>"` per effort tier (`off` included), and optional `mode`, `efforts`, `default-level`, `default-member`, `retired-members`, `effort-budget "<tier>" <n>`, `requires-effort`, `suppress-when-off`, `no-thinking`, `preserve-absent-effort-routes`, and `extra-aliases`. `provider-alias` maps one provider-scoped selector spelling onto a logical id without making it a family member.
126
+ `variant-family` declares one reviewed provider-scoped collapsed family: positional provider and logical id, `name=` display name, and a body of `members "a" "b" …` (wire ids in priority order), `route "<tier>" "<wire-id>"` per effort tier (`off` included), and optional `mode`, `efforts`, `default-level`, `default-member`, `retired-members`, `effort-budget "<tier>" <n>`, `requires-effort`, `suppress-when-off`, `no-thinking`, `preserve-absent-effort-routes`, and `extra-aliases`. A `{rev}` placeholder in the logical id makes the node a **template**: it is instantiated once per revision found in live ids (`gemini-{rev}-flash` matches `gemini-3.8-flash-low` → family `gemini-3.8-flash`), every wire id in the body and the `name=` carry the same placeholder, and an optional `revision=` constraint (`">=3.6"`) bounds the generations it applies to. A concrete family with the same instantiated id wins over the template. `provider-alias` maps one provider-scoped selector spelling onto a logical id without making it a family member.
126
127
 
127
128
  `thinking-suffix` accepts one non-empty suffix and no properties. `pair-token` declares bounded (possibly infix) tokens naming the thinking sibling of a live bare twin (`sonar-reasoning-pro` beside `sonar-pro`); it drives thinking-pair derivation only — never identity suffix collapse — and negated `no-`/`non-` forms never match. `effort-suffix` additionally requires `tier` with one of the effort values above, and may have `except-bare-prefix`. `routing-variant-suffix` takes one non-empty suffix followed by one or more provider IDs: a wire identifier carrying the suffix on one of those providers is a **routing variant** of its plain identifier — discovery derives base-model metadata from the plain bundled SKU while keeping the suffixed wire identifier for requests; routing variants never participate in effort collapse. `effort-lane-suffix` takes one non-empty lane suffix followed by one or more provider IDs, plus an optional `bare-prefix` gate: on a declared provider, an identifier ending in the lane suffix collapses the effort suffix wedged before the lane token while keeping the lane on the logical id. `effort-family` takes a provider, the canonical logical id, and zero or more exact aliases that fold onto it.
128
129
 
@@ -66,17 +66,32 @@ class "gemini" {
66
66
  }
67
67
  }
68
68
  }
69
+ // Gemini 3.7 Flash is the one Flash revision whose native thinkingLevel does
70
+ // not accept `minimal` (400 THINKING_LEVEL_MINIMAL). Only the direct
71
+ // google-level transports emit `thinkingLevel` on the wire, so the tier is
72
+ // dropped there; budget (anthropic-messages) and reasoning_effort resellers
73
+ // never send the rejected value and keep the four-tier ladder. The collapsed
74
+ // google-antigravity / google-gemini-cli variants remap minimal -> LOW and
75
+ // carry their own explicit ladder, so they are unaffected.
76
+ // https://ai.google.dev/gemini-api/docs/generate-content/thinking#thinking-levels
77
+ on "google" "google-vertex" "opencode-zen" {
78
+ family "flash" {
79
+ revision ">=3.7 <3.8" {
80
+ thinking-efforts "low" "medium" "high"
81
+ }
82
+ }
83
+ }
69
84
 
70
- // Gemini 3 tool calls require the sentinel signature on unsigned replay.
71
85
  revision ">=3" {
72
86
  // Replaces the Google multimodal function-response baseline.
73
87
  multimodal-function-response #true
74
- requires-skip-thought-signature #true
75
88
  // Replaces the Gemini 3+ mandatory-reasoning fallback.
76
89
  thinking-requires-effort #true
77
90
  }
91
+ // Only public-Gemini-compatible transports accept the bypass sentinel for unsigned calls.
78
92
  on "google" "opencode-zen" {
79
93
  revision ">=3" {
94
+ requires-skip-thought-signature #true
80
95
  supports-function-part-id #true
81
96
  }
82
97
  }
@@ -10,7 +10,10 @@ class "glm" {
10
10
  // GLM-5.2 exposes the default lower tiers plus a genuine max on generic
11
11
  // OpenAI-compatible hosts; the zai-dialect and Anthropic-route hosts
12
12
  // (Z.ai, Zhipu, Umans, Ollama Cloud, Baseten) serve only high/max.
13
- revision ">=5.2 <5.3" {
13
+ // priority breaks the intentional overlap with provider-level blanket GLM
14
+ // ladders authored for the pre-5.2 census (e.g. alibaba-coding-plan.kdl),
15
+ // so the 5.2 lineage ladder wins on generic hosts that lack a 5.2 residue.
16
+ revision ">=5.2 <5.3" priority=10 {
14
17
  thinking-efforts "minimal" "low" "medium" "high" "max"
15
18
  }
16
19
  on "zai" "zhipu-coding-plan" {