@bitkyc08/opencodex 2.15.1-preview.20260814 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/gui/dist/assets/{index-1U3HI8uT.js → index-CZwbOse7.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/base.ts +2 -0
  5. package/src/adapters/cursor/effort-map.ts +4 -5
  6. package/src/adapters/cursor/request-builder.ts +1 -1
  7. package/src/adapters/google-antigravity-replay.ts +9 -1
  8. package/src/adapters/kiro-thinking.ts +8 -0
  9. package/src/adapters/kiro.ts +45 -42
  10. package/src/adapters/openai-chat.ts +5 -2
  11. package/src/adapters/openai-responses.ts +5 -1
  12. package/src/cli/dispatch.ts +6 -3
  13. package/src/cli/index.ts +1 -0
  14. package/src/generated/compatibility-version.json +56 -32
  15. package/src/generated/model-metadata.ts +1 -1
  16. package/src/integrations/config-io.ts +119 -1
  17. package/src/integrations/omp-yaml-source.ts +6 -1
  18. package/src/integrations/serialize.ts +80 -1
  19. package/src/integrations/state.ts +37 -6
  20. package/src/integrations/writer.ts +11 -3
  21. package/src/lab/automation/orchestrator.ts +19 -0
  22. package/src/lib/lab-activation.ts +161 -0
  23. package/src/lib/lab-passive-linker-registration.ts +26 -0
  24. package/src/lib/optional-shutdown-hooks.ts +57 -0
  25. package/src/lib/translator-budget.ts +34 -0
  26. package/src/oauth/index.ts +3 -0
  27. package/src/providers/antigravity-models.ts +156 -36
  28. package/src/providers/model-rename-migration.ts +54 -1
  29. package/src/providers/registry.ts +1 -1
  30. package/src/routing/compatibility/assemble.ts +21 -107
  31. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  32. package/src/routing/compatibility/provider-slot.ts +56 -0
  33. package/src/server/index.ts +8 -17
  34. package/src/server/lifecycle.ts +5 -3
  35. package/src/server/management/routing-profile-routes.ts +9 -1
  36. package/src/server/management-api.ts +37 -6
  37. package/src/server/passive-route-linker.ts +66 -0
  38. package/src/server/responses/core.ts +20 -20
  39. package/src/types.ts +10 -0
  40. package/src/usage/expected-prices.ts +13 -0
@@ -71,6 +71,40 @@ export interface TranslatorBudget {
71
71
 
72
72
  const retainedEventOwnership = new WeakMap<object, { budget: TranslatorBudget; bytes: number }>();
73
73
 
74
+ /**
75
+ * Charge one event appended to an incrementally materialized adapter-event batch.
76
+ * The newest event owns the closing array bracket; moving that byte from the old
77
+ * tail keeps in-order release accounting equal to the still-retained JSON array.
78
+ */
79
+ export function retainTranslatedEvent<T extends object>(
80
+ event: T,
81
+ budget: TranslatorBudget,
82
+ previousTail?: object,
83
+ ): void {
84
+ if (retainedEventOwnership.has(event)) {
85
+ throw new Error("translated event is already retained");
86
+ }
87
+ if (previousTail === event) {
88
+ throw new Error("incremental translated event tail must be a distinct object");
89
+ }
90
+ const previousOwnership = previousTail === undefined
91
+ ? undefined
92
+ : retainedEventOwnership.get(previousTail);
93
+ if (
94
+ previousTail !== undefined
95
+ && (!previousOwnership || previousOwnership.budget !== budget || previousOwnership.bytes < 2)
96
+ ) {
97
+ throw new Error("incremental translated event tail is not retained by this budget");
98
+ }
99
+
100
+ const serializedBytes = Buffer.byteLength(JSON.stringify(event));
101
+ budget.chargeRetained(serializedBytes + (previousTail === undefined ? 2 : 1), {
102
+ kind: "retained_collectors",
103
+ });
104
+ if (previousOwnership) previousOwnership.bytes -= 1;
105
+ retainedEventOwnership.set(event, { budget, bytes: serializedBytes + 2 });
106
+ }
107
+
74
108
  /**
75
109
  * Charge a materialized adapter-event batch and attach its lease to the events themselves.
76
110
  * A copied event array (for example terminal-guard collection) preserves the event objects, so
@@ -879,6 +879,9 @@ function isLegacyCommandCodeStaticCatalog(provider: OcxProviderConfig): boolean
879
879
  }
880
880
 
881
881
  function isLegacyAntigravityStaticCatalog(provider: OcxProviderConfig): boolean {
882
+ // A fingerprint of the shape version 1 actually shipped, NOT of the current registry.
883
+ // These literals must stay frozen as the model list moves on: matching them is how we
884
+ // know the row is the untouched v1 seed rather than a user's own selection.
882
885
  return provider.liveModels === false
883
886
  && provider.adapter === "google"
884
887
  && provider.baseUrl === "https://daily-cloudcode-pa.googleapis.com"
@@ -6,14 +6,43 @@ import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./mode
6
6
  // CLI resolves labels against. The ids below separate CCA wire ids, collapsed picker entries,
7
7
  // and hidden compatibility aliases for saved selections. The CCA envelope's `model` field must
8
8
  // receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
9
- // picker exposes collapsed base models only when CCA returns every known tier; otherwise each
10
- // returned wire id remains visible so an unavailable tier cannot be selected.
9
+ // picker exposes collapsed known base models only when CCA returns every known tier; unknown
10
+ // returned wire ids remain visible so they stay directly routable.
11
11
 
12
12
  // ── Wire IDs (what CCA :fetchAvailableModels returns) ──
13
+
14
+ /** Current Antigravity Flash generation. */
15
+ const GEMINI_FLASH_CURRENT = "gemini-3.7-flash";
16
+
17
+ /**
18
+ * Retired Flash ids → the reasoning tier they used to encode.
19
+ *
20
+ * Google pulls the previous Flash model from CCA almost immediately when the next
21
+ * one ships, so a saved selection cannot keep pointing at it. A flat alias would
22
+ * strand the user's tier: `resolveAntigravityEffortWireModel` rule 1 treats any
23
+ * alias as "the suffix IS the effort" and deliberately sends no thinkingConfig, so
24
+ * `gemini-3.6-flash-high` would silently become an untiered 3.7 call. Carrying the
25
+ * level here preserves what the user actually chose.
26
+ *
27
+ * 3.7 exposes tiers through `thinkingLevel` on ONE wire id rather than through
28
+ * suffixed wire ids, which is why the mapping is id → level and not id → id.
29
+ */
30
+ const RETIRED_FLASH_TIERS: Record<string, string> = {
31
+ // 3.6 generation.
32
+ "gemini-3.6-flash": "medium", // bare base carried a medium default
33
+ "gemini-3.6-flash-low": "low",
34
+ "gemini-3.6-flash-medium": "medium",
35
+ "gemini-3.6-flash-high": "high",
36
+ // 3.5 generation — these already pointed at 3.6 wire ids, which are now dead too.
37
+ "gemini-3.5-flash-extra-low": "low",
38
+ "gemini-3.5-flash-low": "medium",
39
+ "gemini-3.5-flash-mid": "medium",
40
+ "gemini-3.5-flash-high": "high",
41
+ "gemini-3-flash-agent": "high",
42
+ };
43
+
13
44
  const ANTIGRAVITY_WIRE_MODELS = [
14
- "gemini-3.6-flash-low",
15
- "gemini-3.6-flash-medium",
16
- "gemini-3.6-flash-high",
45
+ "gemini-3.7-flash",
17
46
  "gemini-3.1-pro-low",
18
47
  "gemini-pro-agent",
19
48
  "gemini-3.1-flash-image",
@@ -23,9 +52,6 @@ const ANTIGRAVITY_WIRE_MODELS = [
23
52
  ];
24
53
 
25
54
  const ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID: Record<string, string> = {
26
- "gemini-3.6-flash-low": "gemini-3.6-flash",
27
- "gemini-3.6-flash-medium": "gemini-3.6-flash",
28
- "gemini-3.6-flash-high": "gemini-3.6-flash",
29
55
  "gemini-3.1-pro-low": "gemini-3.1-pro",
30
56
  "gemini-pro-agent": "gemini-3.1-pro",
31
57
  };
@@ -37,11 +63,46 @@ const ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL: Record<string, string[]> = Object.en
37
63
  return out;
38
64
  }, {});
39
65
 
66
+ const ANTIGRAVITY_DISCOVERY_EFFORTS = ["low", "medium", "high"] as const;
67
+
68
+ function pickerModelIdForDiscoveredWireId(
69
+ wireId: string,
70
+ available: ReadonlyMap<string, Record<string, unknown>>,
71
+ ): string {
72
+ const explicitPickerId = Object.hasOwn(ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID, wireId)
73
+ ? ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId]
74
+ : undefined;
75
+ if (explicitPickerId) {
76
+ const requiredWireIds = Object.hasOwn(ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL, explicitPickerId)
77
+ ? ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL[explicitPickerId] ?? []
78
+ : [];
79
+ if (requiredWireIds.every(id => available.has(id))) return explicitPickerId;
80
+ }
81
+
82
+ // CCA uses a single `-tiered` row for models whose effort levels ride on the
83
+ // request's thinkingLevel field. Keep this generic so new tiered models do not
84
+ // require another provider-specific ID mapping.
85
+ if (wireId.endsWith("-tiered")) {
86
+ const baseId = wireId.slice(0, -"-tiered".length);
87
+ if (isKnownAntigravityPickerModelId(baseId)) return baseId;
88
+ }
89
+
90
+ const effortMatch = /^(.*)-(low|medium|high)$/.exec(wireId);
91
+ if (effortMatch) {
92
+ const baseId = effortMatch[1]!;
93
+ if (isKnownAntigravityPickerModelId(baseId)
94
+ && ANTIGRAVITY_DISCOVERY_EFFORTS.every(effort => available.has(`${baseId}-${effort}`))) {
95
+ return baseId;
96
+ }
97
+ }
98
+ return wireId;
99
+ }
100
+
40
101
  // ── Effort ladders per collapsed base model ──
41
102
  // Gemini models: effort → wire model suffix (official agy UI pattern).
42
103
  // Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
43
104
  export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
44
- "gemini-3.6-flash": ["low", "medium", "high"],
105
+ "gemini-3.7-flash": ["low", "medium", "high"],
45
106
  "gemini-3.1-pro": ["low", "high"],
46
107
  "claude-sonnet-4-6": ["low", "medium", "high", "max"],
47
108
  "claude-opus-4-6-thinking": ["low", "medium", "high", "max"],
@@ -49,11 +110,6 @@ export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
49
110
 
50
111
  // ── Effort → wire model map for Gemini base models ──
51
112
  const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
52
- "gemini-3.6-flash": {
53
- low: "gemini-3.6-flash-low",
54
- medium: "gemini-3.6-flash-medium",
55
- high: "gemini-3.6-flash-high",
56
- },
57
113
  "gemini-3.1-pro": {
58
114
  low: "gemini-3.1-pro-low",
59
115
  high: "gemini-pro-agent",
@@ -62,11 +118,20 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
62
118
 
63
119
  // ── Default effort per Gemini base model ──
64
120
  const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
65
- "gemini-3.6-flash": "medium",
66
121
  "gemini-3.1-pro": "high",
67
122
  };
68
123
 
69
- const ANTIGRAVITY_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high"]);
124
+ /**
125
+ * Gemini base models whose efforts ride on `thinkingLevel` against a single wire id
126
+ * instead of suffixed wire ids, with the level applied when the caller names none.
127
+ */
128
+ const ANTIGRAVITY_THINKING_LEVEL_MODELS: Record<string, string> = {
129
+ "gemini-3.7-flash": "medium",
130
+ };
131
+
132
+ // `minimal` is deliberately absent: Google documents it as unsupported for the current
133
+ // Flash generation, where it is an error rather than a quieter tier.
134
+ const ANTIGRAVITY_THINKING_LEVELS = new Set(["low", "medium", "high"]);
70
135
 
71
136
  function resolveAntigravityThinkingLevel(effort: string): string | undefined {
72
137
  if (effort === "xhigh" || effort === "max" || effort === "ultra") return "high";
@@ -83,16 +148,17 @@ const ANTIGRAVITY_VISIBLE_MODEL_ALIASES: Record<string, string> = {
83
148
  // Wire suffix IDs are identity aliases — they resolve to themselves so saved configs
84
149
  // with explicit suffixes (e.g. gemini-3.6-flash-low) continue to work.
85
150
  const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record<string, string> = {
86
- "gemini-3.6-flash-low": "gemini-3.6-flash-low",
87
- "gemini-3.6-flash-medium": "gemini-3.6-flash-medium",
88
- "gemini-3.6-flash-high": "gemini-3.6-flash-high",
89
151
  "gemini-3.1-pro-low": "gemini-3.1-pro-low",
90
152
  "gemini-pro-agent": "gemini-pro-agent",
91
- "gemini-3.5-flash-extra-low": "gemini-3.6-flash-low",
92
- "gemini-3.5-flash-low": "gemini-3.6-flash-medium",
93
- "gemini-3.5-flash-mid": "gemini-3.6-flash-medium",
94
- "gemini-3.5-flash-high": "gemini-3.6-flash-high",
95
- "gemini-3-flash-agent": "gemini-3.6-flash-high",
153
+ // ── Retired Flash generations ──
154
+ // Google takes the previous Antigravity Flash model offline almost immediately
155
+ // once its successor ships, so 3.6 (and the 3.5 ids that used to land on it)
156
+ // route to 3.7. These stay in the alias map — not only in the tier map below —
157
+ // because `parseAntigravityAvailableModels` uses THIS map to keep a stale CCA
158
+ // payload from republishing a dead wire id as a picker row.
159
+ ...Object.fromEntries(
160
+ Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired, GEMINI_FLASH_CURRENT]),
161
+ ),
96
162
  };
97
163
 
98
164
  export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
@@ -102,7 +168,7 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
102
168
 
103
169
  // Picker-visible: collapsed base models only.
104
170
  export const ANTIGRAVITY_MODELS = [
105
- "gemini-3.6-flash",
171
+ GEMINI_FLASH_CURRENT,
106
172
  "gemini-3.1-pro",
107
173
  "gemini-3.1-flash-image",
108
174
  "claude-sonnet-4-6",
@@ -110,11 +176,13 @@ export const ANTIGRAVITY_MODELS = [
110
176
  "gpt-oss-120b-medium",
111
177
  ];
112
178
 
179
+ function isKnownAntigravityPickerModelId(value: string): boolean {
180
+ return isValidModelDiscoveryModelId(value) && ANTIGRAVITY_MODELS.includes(value);
181
+ }
182
+
113
183
  // Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
114
184
  const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
115
- "gemini-3.6-flash-low": 1_048_576,
116
- "gemini-3.6-flash-medium": 1_048_576,
117
- "gemini-3.6-flash-high": 1_048_576,
185
+ "gemini-3.7-flash": 1_048_576,
118
186
  "gemini-3.1-pro-low": 1_048_576,
119
187
  "gemini-pro-agent": 1_048_576,
120
188
  "gemini-3.1-flash-image": 1_048_576,
@@ -125,7 +193,7 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
125
193
 
126
194
  export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
127
195
  // Collapsed base IDs — explicit entries for the picker.
128
- "gemini-3.6-flash": 1_048_576,
196
+ "gemini-3.7-flash": 1_048_576,
129
197
  "gemini-3.1-pro": 1_048_576,
130
198
  // Wire IDs and aliases via derivation.
131
199
  ...ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS,
@@ -138,7 +206,11 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
138
206
  };
139
207
 
140
208
  export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
141
- "gemini-3.6-flash": ["text", "image"],
209
+ // Google documents 3.7 Flash as also accepting video, audio and PDF, but this proxy
210
+ // carries only text and image parts (`OcxImageContent`, src/types.ts) and the Codex
211
+ // catalog normalizes `input_modalities` against a closed enum. Advertising a modality
212
+ // the wire cannot carry would be a promise we break at request time.
213
+ "gemini-3.7-flash": ["text", "image"],
142
214
  "gemini-3.1-pro": ["text", "image"],
143
215
  "gemini-3.1-flash-image": ["text", "image"],
144
216
  "claude-sonnet-4-6": ["text", "image"],
@@ -189,6 +261,7 @@ export function parseAntigravityAvailableModels(
189
261
  if (!Array.isArray(modelIds)) return null;
190
262
  for (const id of modelIds) {
191
263
  if (!isValidModelDiscoveryModelId(id)
264
+ || !Object.hasOwn(models, id)
192
265
  || !antigravityRecord(models[id])
193
266
  || ids.length >= limit) return null;
194
267
  ids.push(id);
@@ -202,6 +275,19 @@ export function parseAntigravityAvailableModels(
202
275
  if (ids.length >= limit) return null;
203
276
  ids.push("gemini-3.1-flash-image");
204
277
  }
278
+ // Newer CCA responses identify tiered Flash models through this index instead of
279
+ // adding their synthetic wire ids to agentModelSorts.
280
+ const tieredModelIds = antigravityRecord(body.tieredModelIds);
281
+ const flashTieredIds = tieredModelIds?.flash;
282
+ if (Array.isArray(flashTieredIds)) {
283
+ for (const id of flashTieredIds) {
284
+ if (!isValidModelDiscoveryModelId(id)
285
+ || !Object.hasOwn(models, id)
286
+ || !antigravityRecord(models[id])
287
+ || ids.length >= limit) return null;
288
+ ids.push(id);
289
+ }
290
+ }
205
291
 
206
292
  const available = new Map<string, Record<string, unknown>>();
207
293
  for (const wireId of ids) {
@@ -209,17 +295,17 @@ export function parseAntigravityAvailableModels(
209
295
  if (!info || available.has(wireId)) continue;
210
296
  // Legacy compatibility aliases are deliberately routed to newer wire ids for saved
211
297
  // selections. They are not safe as independently discovered picker rows.
212
- if (ANTIGRAVITY_MODEL_ALIASES[wireId] && ANTIGRAVITY_MODEL_ALIASES[wireId] !== wireId) continue;
298
+ const alias = Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, wireId)
299
+ ? ANTIGRAVITY_MODEL_ALIASES[wireId]
300
+ : undefined;
301
+ if (alias && alias !== wireId) continue;
213
302
  available.set(wireId, info);
214
303
  }
215
304
 
216
305
  const out: AntigravityAvailableModel[] = [];
217
306
  const seen = new Set<string>();
218
307
  for (const [wireId, info] of available) {
219
- const pickerId = ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId];
220
- const completePickerSet = pickerId !== undefined
221
- && ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL[pickerId]!.every(id => available.has(id));
222
- const id = completePickerSet ? pickerId! : wireId;
308
+ const id = pickerModelIdForDiscoveredWireId(wireId, available);
223
309
  if (seen.has(id)) continue;
224
310
  seen.add(id);
225
311
  out.push({
@@ -232,7 +318,9 @@ export function parseAntigravityAvailableModels(
232
318
  }
233
319
 
234
320
  export function resolveAntigravityWireModelId(modelId: string): string {
235
- return ANTIGRAVITY_MODEL_ALIASES[modelId] ?? modelId;
321
+ return Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, modelId)
322
+ ? ANTIGRAVITY_MODEL_ALIASES[modelId]
323
+ : modelId;
236
324
  }
237
325
 
238
326
  /**
@@ -244,6 +332,11 @@ export function isAntigravitySuffixModelId(modelId: string): boolean {
244
332
  return !(ANTIGRAVITY_MODELS as string[]).includes(modelId);
245
333
  }
246
334
 
335
+ /** The reasoning tier a retired Flash id used to encode, if it is one. */
336
+ export function retiredAntigravityFlashTier(modelId: string): string | undefined {
337
+ return Object.hasOwn(RETIRED_FLASH_TIERS, modelId) ? RETIRED_FLASH_TIERS[modelId] : undefined;
338
+ }
339
+
247
340
  /**
248
341
  * Resolve a picker-visible base model + optional reasoning effort to the CCA wire model ID.
249
342
  *
@@ -258,11 +351,32 @@ export function resolveAntigravityEffortWireModel(
258
351
  modelId: string,
259
352
  effort?: string,
260
353
  ): { wireModelId: string; thinkingLevel?: string } {
354
+ // Rule 0: retired Flash id — Google has taken the wire id offline, so route to the
355
+ // current generation and carry the tier the retired id encoded. This runs BEFORE the
356
+ // suffix check because those ids are aliases, and rule 1 would drop the tier.
357
+ const retiredTier = retiredAntigravityFlashTier(modelId);
358
+ if (retiredTier) {
359
+ return {
360
+ wireModelId: GEMINI_FLASH_CURRENT,
361
+ thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? retiredTier : retiredTier,
362
+ };
363
+ }
364
+
261
365
  // Rule 1: suffix/compat alias — suffix IS the effort.
262
366
  if (isAntigravitySuffixModelId(modelId)) {
263
367
  return { wireModelId: resolveAntigravityWireModelId(modelId) };
264
368
  }
265
369
 
370
+ // Rule 1b: single-wire-id Gemini model whose tiers ride on thinkingLevel. Without
371
+ // this the model falls to rule 5 and silently loses reasoning control entirely.
372
+ const defaultLevel = ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
373
+ if (defaultLevel) {
374
+ return {
375
+ wireModelId: modelId,
376
+ thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? defaultLevel : defaultLevel,
377
+ };
378
+ }
379
+
266
380
  // Rule 2/3: mapped Gemini base model.
267
381
  const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId];
268
382
  if (effortMap) {
@@ -301,6 +415,12 @@ const ANTIGRAVITY_USAGE_BASE_BY_ID: Record<string, string> = (() => {
301
415
  // If alias is itself a base/wire already mapped, keep that mapping.
302
416
  else if (rev[wire]) rev[alias] = rev[wire]!;
303
417
  }
418
+ // Retired ids keep their OWN identity for usage aggregation, overriding the alias
419
+ // pass above. Routing sends new 3.6 calls to 3.7, but a usage row written months ago
420
+ // records a model the user actually called: relabelling it would move historical spend
421
+ // onto a model that did not exist then, and away from the 3.6 price row that still
422
+ // prices it correctly. Retirement changes what we CALL, not what we RECORD.
423
+ for (const retired of Object.keys(RETIRED_FLASH_TIERS)) rev[retired] = retired;
304
424
  // Visible aliases that only appear in ANTIGRAVITY_VISIBLE_MODEL_ALIASES are already
305
425
  // included via ANTIGRAVITY_MODEL_ALIASES. Identity bases without effort maps remain.
306
426
  return rev;
@@ -26,6 +26,22 @@ export interface ModelRename {
26
26
  to: string;
27
27
  /** Why the vendor retired it, for the startup warning and future readers. */
28
28
  reason: string;
29
+ /**
30
+ * Drop the retired key from `modelReasoningEffortMap` instead of renaming it.
31
+ *
32
+ * Renaming preserves the VALUE, which is right for records whose values describe the
33
+ * model — a context window or an effort ladder survives a rename — and wrong for the
34
+ * one record whose values are themselves wire ids. An effort map saved as
35
+ * `high -> gemini-3.6-flash-high` would keep naming a dead wire id under the new key,
36
+ * and the adapter maps effort BEFORE resolving CCA routing, so that value arrives as
37
+ * an unrecognised effort and silently degrades to the default tier.
38
+ *
39
+ * Only that one record is dropped. Emptying the others would be worse than the bug:
40
+ * catalog enrichment treats an existing `{}` as "already populated" and will not
41
+ * restore the registry's records, so the migrated user would keep routing correctly
42
+ * but lose the reasoning picker entirely.
43
+ */
44
+ dropReasoningEffortMap?: boolean;
29
45
  }
30
46
 
31
47
  /**
@@ -46,6 +62,22 @@ export const MODEL_RENAMES: readonly ModelRename[] = [
46
62
  to: "qwen3.8-max",
47
63
  reason: "Alibaba shipped Qwen3.8-Max as stable and documents the preview endpoint as liable to be taken offline once preview concludes",
48
64
  },
65
+ // Antigravity Flash generations. Google takes the previous Flash model off Cloud Code
66
+ // Assist almost immediately when the next ships, so a saved 3.6 (or older 3.5) id is a
67
+ // dead selection rather than a merely outdated one. Routing already redirects these ids
68
+ // at request time; this migration repairs the saved config so the picker, the allowlist
69
+ // and the capability maps stop naming a model the backend no longer serves.
70
+ ...(["gemini-3.6-flash", "gemini-3.6-flash-low", "gemini-3.6-flash-medium", "gemini-3.6-flash-high",
71
+ "gemini-3.5-flash-extra-low", "gemini-3.5-flash-low", "gemini-3.5-flash-mid", "gemini-3.5-flash-high",
72
+ "gemini-3-flash-agent"] as const).map(from => ({
73
+ provider: "google-antigravity",
74
+ from,
75
+ to: "gemini-3.7-flash",
76
+ reason: "Google retires the previous Antigravity Flash generation from Cloud Code Assist when its successor ships, so the saved id no longer resolves to a live model",
77
+ // The retired Flash tiers were wire ids, so any saved per-model record keyed by one
78
+ // may also hold one as a value. 3.7 expresses tiers as thinkingLevel names instead.
79
+ dropReasoningEffortMap: true,
80
+ })),
49
81
  ];
50
82
 
51
83
  /** Provider fields that key metadata by model id. */
@@ -61,6 +93,12 @@ const MODEL_KEYED_RECORDS = [
61
93
  /** Provider fields that are flat lists of model ids. */
62
94
  const MODEL_ID_LISTS = [
63
95
  "models",
96
+ // A retired id left here is worse than a stale label: `filterCatalogModels` treats
97
+ // `selectedModels` as an exact-match allowlist, so a user who allowlisted only the
98
+ // retired model gets NO replacement row at all — the model silently vanishes from
99
+ // their catalog instead of being renamed. OAuth reconciliation does not cover this
100
+ // field, so the rename has to.
101
+ "selectedModels",
64
102
  "noVisionModels",
65
103
  "noReasoningModels",
66
104
  "noTemperatureModels",
@@ -102,6 +140,19 @@ function renameInRecord(value: unknown, from: string, to: string): Record<string
102
140
  return next;
103
141
  }
104
142
 
143
+ /** Drop the retired key entirely, leaving any entry already saved under the new id. */
144
+ function dropFromRecord(value: unknown, from: string): Record<string, unknown> | null {
145
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
146
+ const record = value as Record<string, unknown>;
147
+ if (!(from in record)) return null;
148
+ const next: Record<string, unknown> = {};
149
+ for (const [key, entry] of Object.entries(record)) {
150
+ if (key === from) continue;
151
+ next[key] = entry;
152
+ }
153
+ return next;
154
+ }
155
+
105
156
  /**
106
157
  * `provider/model` rows in the top-level `disabledModels` list.
107
158
  *
@@ -179,7 +230,9 @@ export function projectModelRenames(
179
230
  touched = true;
180
231
  }
181
232
  for (const field of MODEL_KEYED_RECORDS) {
182
- const next = renameInRecord(row[field], rename.from, rename.to);
233
+ const next = rename.dropReasoningEffortMap && field === "modelReasoningEffortMap"
234
+ ? dropFromRecord(row[field], rename.from)
235
+ : renameInRecord(row[field], rename.from, rename.to);
183
236
  if (!next) continue;
184
237
  row[field] = next;
185
238
  touched = true;
@@ -1451,7 +1451,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1451
1451
  // 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
1452
1452
  // evidence from ai.google.dev does not establish Vertex publisher availability.
1453
1453
  { id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
1454
- { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.6-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
1454
+ { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.7-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
1455
1455
  { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
1456
1456
  { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
1457
1457
  { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
@@ -5,73 +5,22 @@ import type { PolicyCandidateEvidence } from "../evaluator";
5
5
  import { policyCandidateHealthEvidence } from "../health";
6
6
  import type { NormalizedRoutingProfile } from "../profile";
7
7
  import { quotaEvidenceForCandidate } from "../quota";
8
- import {
9
- compatibilitySuiteKey,
10
- loadCompatibilityCatalogSnapshot,
11
- type CompatibilityCatalogSnapshot,
12
- } from "./catalog";
13
- import { findVerdictForSuite, loadCompatibilityEvidenceSnapshot } from "./reader";
14
- import {
15
- resolvePolicyCompatibilitySubjects,
16
- type ResolvedPolicyCompatibilitySubjects,
17
- } from "./subject";
18
- import type { CandidateCompatibilityEvidence } from "./types";
8
+ import { resolveCompatibilityEvidenceProvider, type CoreEvidenceOptions } from "./provider-slot";
19
9
 
20
10
  export type RoutedProviderResolver = (
21
11
  providerName: string,
22
12
  provider: OcxProviderConfig,
23
13
  ) => OcxProviderConfig;
24
14
 
25
- export interface AssemblePolicyEvidenceOptions {
26
- configDir?: string;
27
- routedProviderConfig: RoutedProviderResolver;
28
- resolveSubjects?: typeof resolvePolicyCompatibilitySubjects;
29
- loadEvidenceSnapshot?: typeof loadCompatibilityEvidenceSnapshot;
30
- loadCatalogSnapshot?: typeof loadCompatibilityCatalogSnapshot;
31
- }
32
-
33
- function attachCompatibilityEvidence(
34
- resolved: ResolvedPolicyCompatibilitySubjects | undefined,
35
- snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot>,
36
- catalog: CompatibilityCatalogSnapshot,
37
- profile: NonNullable<NormalizedRoutingProfile["compatibility"]>,
38
- ): CandidateCompatibilityEvidence {
39
- const subjectIds = resolved?.subjectIds ?? {};
40
- const suites: CandidateCompatibilityEvidence["suites"] = [];
41
-
42
- for (const requirement of profile.requiredSuites) {
43
- const subjectId = subjectIds[requirement.evidenceLayer];
44
- if (!subjectId) continue;
45
- const metadata = catalog.get(compatibilitySuiteKey(requirement.evidenceLayer, requirement.suiteId));
46
- if (!metadata) continue;
47
- const row = findVerdictForSuite(
48
- snapshot,
49
- subjectId,
50
- requirement.evidenceLayer,
51
- requirement.suiteId,
52
- metadata.suiteVersion,
53
- metadata.suiteManifestDigest,
54
- );
55
- if (!row) continue;
56
- suites.push({
57
- subjectId,
58
- suiteId: row.suiteId,
59
- evidenceLayer: requirement.evidenceLayer,
60
- suiteVersion: row.suiteVersion,
61
- suiteManifestDigest: row.suiteManifestDigest,
62
- verdict: row.verdict,
63
- asOf: row.asOf,
64
- maxAgeMs: metadata.maxAgeMs,
65
- notes: row.notes,
66
- });
67
- }
68
-
69
- return {
70
- subjectIds: { ...subjectIds },
71
- projectionAvailable: snapshot.projectionAvailable,
72
- suites,
73
- };
74
- }
15
+ /**
16
+ * Options the core assembler needs. Provider-specific test seams (subject resolution,
17
+ * catalog and projection loading) belong to the compatibility provider, not here -- keeping
18
+ * them out is what stops the core assembler from naming Lab-backed contracts.
19
+ *
20
+ * The provider reads its own seams off the same object, so callers may still pass a
21
+ * `LabCompatibilityProviderOptions`; that type extends this one.
22
+ */
23
+ export type AssemblePolicyEvidenceOptions = CoreEvidenceOptions;
75
24
 
76
25
  /**
77
26
  * Assemble production policy candidate evidence including compatibility snapshots.
@@ -89,55 +38,20 @@ export function assemblePolicyCandidateEvidence(
89
38
  const hasCompatibilityRequirements = Boolean(
90
39
  compatibilityPolicy && compatibilityPolicy.requiredSuites.length > 0,
91
40
  );
92
- const resolvedByCandidate = new Map<string, ResolvedPolicyCompatibilitySubjects>();
93
- let catalog: CompatibilityCatalogSnapshot = new Map();
94
- let snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot> = {
95
- projectionAvailable: true,
96
- projectionIncompatible: false,
97
- bySubject: new Map(),
98
- };
99
-
100
- if (hasCompatibilityRequirements && compatibilityPolicy) {
101
- const resolveSubjects = options.resolveSubjects ?? resolvePolicyCompatibilitySubjects;
102
- const loadCatalog = options.loadCatalogSnapshot ?? loadCompatibilityCatalogSnapshot;
103
- const loadEvidence = options.loadEvidenceSnapshot ?? loadCompatibilityEvidenceSnapshot;
104
- catalog = loadCatalog(compatibilityPolicy.requiredSuites);
105
- const subjectIds = new Set<string>();
106
-
107
- for (const candidate of profile.candidates) {
108
- const provider = config.providers[candidate.provider];
109
- if (!provider) continue;
110
- try {
111
- const routed = options.routedProviderConfig(candidate.provider, provider);
112
- const resolved = resolveSubjects(
113
- config,
114
- candidate.provider,
115
- candidate.model,
116
- routed,
117
- options.configDir,
118
- );
119
- resolvedByCandidate.set(`${candidate.provider}/${candidate.model}`, resolved);
120
- for (const subjectId of Object.values(resolved.subjectIds)) {
121
- if (subjectId) subjectIds.add(subjectId);
122
- }
123
- } catch {
124
- // Subject construction failure is handled per required layer as unknown.
125
- }
126
- }
127
-
128
- snapshot = loadEvidence([...subjectIds], options.configDir);
129
- }
41
+ // Compatibility evidence is supplied by an opt-in subsystem. With no provider registered
42
+ // -- every install without compatibility-gated profiles -- the evaluator sees no
43
+ // compatibility evidence and scores on capability, health, quota, and cost exactly as it
44
+ // did before compatibility policy existed.
45
+ const compatibilityProvider = hasCompatibilityRequirements && compatibilityPolicy
46
+ ? resolveCompatibilityEvidenceProvider()
47
+ : null;
48
+ const compatibilityByCandidate = compatibilityProvider && compatibilityPolicy
49
+ ? compatibilityProvider(config, profile, compatibilityPolicy, options)
50
+ : null;
130
51
 
131
52
  return profile.candidates.map(candidate => {
132
53
  const key = `${candidate.provider}/${candidate.model}`;
133
- const compatibility = hasCompatibilityRequirements && compatibilityPolicy
134
- ? attachCompatibilityEvidence(
135
- resolvedByCandidate.get(key),
136
- snapshot,
137
- catalog,
138
- compatibilityPolicy,
139
- )
140
- : undefined;
54
+ const compatibility = compatibilityByCandidate?.get(key);
141
55
 
142
56
  return {
143
57
  provider: candidate.provider,