@bitkyc08/opencodex 2.76.0 → 2.77.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 (189) hide show
  1. package/gui/dist/assets/App-COdEeqSj.css +1 -0
  2. package/gui/dist/assets/App-DEpCJEP_.js +54 -0
  3. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
  4. package/gui/dist/assets/index-Cd7ittMv.css +1 -0
  5. package/gui/dist/assets/index-DwJKFJm5.js +94 -0
  6. package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
  7. package/gui/dist/index.html +4 -3
  8. package/package.json +1 -1
  9. package/src/adapters/antigravity-refusal.ts +28 -0
  10. package/src/adapters/cursor/protobuf-request.ts +6 -0
  11. package/src/adapters/cursor/request-builder.ts +3 -0
  12. package/src/adapters/cursor/structured-output.ts +13 -0
  13. package/src/adapters/cursor/types.ts +2 -0
  14. package/src/adapters/ollama-native.ts +29 -2
  15. package/src/adapters/openai-responses/passthrough.ts +133 -103
  16. package/src/adapters/openai-responses/request-strips.ts +10 -4
  17. package/src/adapters/zed.ts +26 -4
  18. package/src/bridge/response-json.ts +14 -14
  19. package/src/bridge/sse.ts +15 -5
  20. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  21. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  22. package/src/claude/agents-inject.ts +4 -43
  23. package/src/claude/gateway-cache.ts +16 -13
  24. package/src/claude/inbound-model-options.ts +9 -2
  25. package/src/claude/intercept/cli-catalog.ts +27 -16
  26. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  27. package/src/claude/intercept/picker-budget.ts +41 -0
  28. package/src/claude/outbound.ts +2 -0
  29. package/src/claude/subagent-force-status.ts +53 -0
  30. package/src/claude/subagent-model.ts +93 -0
  31. package/src/cli/account-api.ts +3 -0
  32. package/src/cli/account.ts +3 -1
  33. package/src/cli/agent.ts +10 -0
  34. package/src/cli/capabilities.ts +9 -0
  35. package/src/cli/chatgpt-command.ts +10 -7
  36. package/src/cli/claude.ts +22 -6
  37. package/src/cli/codex-shim-readiness.ts +89 -0
  38. package/src/cli/connect.ts +19 -0
  39. package/src/cli/dispatch.ts +3 -3
  40. package/src/cli/export-command.ts +20 -9
  41. package/src/cli/gui.ts +8 -3
  42. package/src/cli/help-catalog.ts +84 -0
  43. package/src/cli/help-models-context.ts +17 -0
  44. package/src/cli/help-navigation.ts +50 -0
  45. package/src/cli/help-recovery.ts +66 -0
  46. package/src/cli/help.ts +52 -8
  47. package/src/cli/models-runtime.ts +2 -1
  48. package/src/cli/opencode.ts +54 -10
  49. package/src/cli/provider.ts +3 -29
  50. package/src/cli/registry.ts +29 -5
  51. package/src/cli/root.ts +30 -9
  52. package/src/cli/runtime-api.ts +1 -1
  53. package/src/cli/tray-proxy.ts +3 -2
  54. package/src/clients/config-export/contracts.ts +100 -0
  55. package/src/clients/config-export/droid.ts +54 -2
  56. package/src/clients/config-export/kilo.ts +12 -2
  57. package/src/clients/config-export/model-metadata.ts +10 -4
  58. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  59. package/src/clients/config-export.ts +46 -66
  60. package/src/clients/omo-role-models.ts +17 -3
  61. package/src/codex/account-credit-use.ts +4 -2
  62. package/src/codex/account-runtime-state.ts +21 -0
  63. package/src/codex/account-store.ts +11 -47
  64. package/src/codex/account-usability.ts +11 -8
  65. package/src/codex/auth-api/account-list.ts +73 -63
  66. package/src/codex/auth-api/main-account-probe.ts +25 -12
  67. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  68. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  69. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  70. package/src/codex/auth-context.ts +30 -11
  71. package/src/codex/catalog/aggregation.ts +3 -0
  72. package/src/codex/catalog/model-hints.ts +2 -1
  73. package/src/codex/catalog/model-visibility.ts +6 -3
  74. package/src/codex/catalog/parsing.ts +2 -0
  75. package/src/codex/catalog/provider-models.ts +13 -4
  76. package/src/codex/catalog/retained-sync.ts +2 -1
  77. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  78. package/src/codex/convergence.ts +2 -1
  79. package/src/codex/main-account.ts +144 -11
  80. package/src/codex/quota-refresh-outcome.ts +17 -2
  81. package/src/codex/quota-types.ts +33 -1
  82. package/src/codex/quota.ts +56 -15
  83. package/src/codex/routing/cooldown-math.ts +10 -4
  84. package/src/codex/routing/selection.ts +11 -4
  85. package/src/codex/routing.ts +9 -9
  86. package/src/codex/shim-path-resolution.ts +133 -0
  87. package/src/codex/shim.ts +39 -39
  88. package/src/codex/subagent-model-fallback.ts +2 -1
  89. package/src/combos/failover.ts +82 -1
  90. package/src/combos/jev.ts +13 -3
  91. package/src/config/diagnostics.ts +4 -0
  92. package/src/config/load-degrade.ts +4 -0
  93. package/src/config/provider-validation.ts +3 -0
  94. package/src/config/schema/compaction-triggers.ts +8 -0
  95. package/src/config/schema/config-schema.ts +11 -0
  96. package/src/config/schema/leaf-validators.ts +5 -1
  97. package/src/config/subagent-models.ts +12 -0
  98. package/src/generated/compatibility-version.json +245 -153
  99. package/src/grok/session-identity.ts +33 -0
  100. package/src/integrations/catalog-refresh.ts +1 -1
  101. package/src/integrations/mutation-plan.ts +4 -2
  102. package/src/integrations/state.ts +68 -6
  103. package/src/integrations/writer.ts +18 -4
  104. package/src/lib/gui-pair-capability.ts +17 -0
  105. package/src/lib/provider-client-headers.ts +62 -0
  106. package/src/oauth/anthropic-continuity.ts +65 -0
  107. package/src/oauth/anthropic-identity.ts +75 -0
  108. package/src/oauth/anthropic-routing.ts +3 -3
  109. package/src/oauth/anthropic.ts +7 -1
  110. package/src/oauth/generic-account-failover.ts +21 -0
  111. package/src/oauth/health.ts +32 -5
  112. package/src/oauth/index.ts +36 -27
  113. package/src/oauth/local-token-detect.ts +7 -0
  114. package/src/oauth/refresh-policy.ts +2 -0
  115. package/src/oauth/store.ts +55 -6
  116. package/src/oauth/types.ts +9 -0
  117. package/src/protocols/encoders/messages.ts +4 -3
  118. package/src/providers/antigravity-effort-families.ts +75 -0
  119. package/src/providers/antigravity-models.ts +54 -16
  120. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  121. package/src/providers/derive.ts +2 -0
  122. package/src/providers/model-rename-fields.ts +3 -0
  123. package/src/providers/new-model-policy.ts +48 -10
  124. package/src/providers/registry/entries-extended.ts +3 -0
  125. package/src/providers/registry/model-ids.ts +2 -0
  126. package/src/providers/registry/model-seeds.ts +5 -0
  127. package/src/providers/registry/types.ts +4 -0
  128. package/src/responses/compaction.ts +19 -0
  129. package/src/router.ts +6 -0
  130. package/src/server/auth-cors.ts +9 -0
  131. package/src/server/chat-completions.ts +10 -0
  132. package/src/server/claude-messages.ts +18 -11
  133. package/src/server/droid-reasoning-default.ts +50 -0
  134. package/src/server/github-copilot-responses-repair.ts +3 -6
  135. package/src/server/grok-responses-control-frame.ts +2 -2
  136. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  137. package/src/server/gui-session.ts +33 -11
  138. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  139. package/src/server/index/serve-options.ts +2 -1
  140. package/src/server/management/agent-settings-routes.ts +6 -129
  141. package/src/server/management/config-routes.ts +1 -1
  142. package/src/server/management/integration-routes.ts +64 -6
  143. package/src/server/management/link-routes.ts +20 -13
  144. package/src/server/management/model-row-export-metadata.ts +150 -0
  145. package/src/server/management/model-rows.ts +148 -9
  146. package/src/server/management/route-registry.ts +4 -3
  147. package/src/server/management/shared.ts +19 -5
  148. package/src/server/management/subagent-model-routes.ts +214 -0
  149. package/src/server/relay.ts +14 -44
  150. package/src/server/request-log-generation-window.ts +38 -0
  151. package/src/server/request-log.ts +13 -0
  152. package/src/server/responses/adapter-dispatch.ts +100 -1
  153. package/src/server/responses/codex-auth-error.ts +10 -2
  154. package/src/server/responses/combo-stream-preflight.ts +29 -3
  155. package/src/server/responses/compaction-routing.ts +10 -1
  156. package/src/server/responses/core-auth.ts +57 -26
  157. package/src/server/responses/core-codex-account.ts +6 -3
  158. package/src/server/responses/core-combo-failure.ts +5 -1
  159. package/src/server/responses/core-combo-native.ts +7 -1
  160. package/src/server/responses/core-combo.ts +18 -1
  161. package/src/server/responses/core-normalize.ts +7 -0
  162. package/src/server/responses/core-options.ts +5 -0
  163. package/src/server/responses/fetch-helpers.ts +9 -0
  164. package/src/server/responses/passthrough-delivery.ts +1 -1
  165. package/src/server/responses/policy-fallback.ts +1 -1
  166. package/src/server/responses/request-prepare.ts +5 -0
  167. package/src/server/responses/request-spend.ts +30 -30
  168. package/src/server/responses-custom-tool-repair.ts +2 -2
  169. package/src/server/responses-hosted-image-display.ts +70 -9
  170. package/src/server/responses-terminal-repair.ts +15 -2
  171. package/src/server/sse-frame-buffer.ts +38 -36
  172. package/src/server/sse-payload-rewrite.ts +78 -49
  173. package/src/server/ws-bridge.ts +1 -11
  174. package/src/service/windows-taskxml.ts +1 -2
  175. package/src/service-manager-probe.ts +36 -2
  176. package/src/types/config.ts +4 -0
  177. package/src/types/provider.ts +18 -0
  178. package/src/update/npm-invocation.mjs +35 -3
  179. package/src/update/proxy-liveness-probe.mjs +38 -8
  180. package/src/usage/expected-prices.ts +9 -0
  181. package/src/usage/log.ts +7 -0
  182. package/src/usage/summary.ts +82 -1
  183. package/src/web-search/loop.ts +11 -8
  184. package/src/web-search/run-turn-loop.ts +10 -7
  185. package/gui/dist/assets/App-CddorHdA.js +0 -51
  186. package/gui/dist/assets/App-DMudtISW.css +0 -1
  187. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  188. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  189. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -1,5 +1,7 @@
1
1
  import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./model-discovery-limits";
2
- import { isModelCacheGenerationCurrent } from "../codex/model-cache";
2
+ import { captureModelCacheGeneration, isModelCacheGenerationCurrent } from "../codex/model-cache";
3
+ import { getConfigDir } from "../config/paths";
4
+ import { readAntigravityWireSnapshot, writeAntigravityWireSnapshot, type AntigravitySuffixMap } from "./antigravity-wire-snapshot";
3
5
 
4
6
  // Google Antigravity (Cloud Code Assist) bundled model list.
5
7
  //
@@ -7,8 +9,8 @@ import { isModelCacheGenerationCurrent } from "../codex/model-cache";
7
9
  // CLI resolves labels against. The ids below separate CCA wire ids, collapsed picker entries,
8
10
  // and hidden compatibility aliases for saved selections. The CCA envelope's `model` field must
9
11
  // receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
10
- // picker exposes collapsed known base models only when CCA returns every known tier; unknown
11
- // returned wire ids remain visible so they stay directly routable.
12
+ // picker collapses complete low/medium/high families discovered by CCA, including new versions.
13
+ // Partial families remain wire-addressable rather than advertising an invented effort ladder.
12
14
 
13
15
  // ── Wire IDs (what CCA :fetchAvailableModels returns) ──
14
16
 
@@ -121,10 +123,12 @@ function pickerModelIdForDiscoveredWireId(
121
123
  const effortMatch = /^(.*)-(low|medium|high)$/.exec(wireId);
122
124
  if (effortMatch) {
123
125
  const baseId = effortMatch[1]!;
124
- if (isKnownAntigravityPickerModelId(baseId)
126
+ if (isValidModelDiscoveryModelId(baseId)
125
127
  && ANTIGRAVITY_DISCOVERY_EFFORTS.every(effort => available.has(`${baseId}-${effort}`))) {
126
128
  return baseId;
127
129
  }
130
+ // A shared display label must not collapse an incomplete family or rename its wire tier.
131
+ return wireId;
128
132
  }
129
133
 
130
134
  // Display labels are a LAST resort, never a first one. CCA labels a tier row
@@ -200,12 +204,13 @@ function completeDiscoveredEffortWireModelIds(
200
204
  pickerId: string,
201
205
  available: ReadonlyMap<string, Record<string, unknown>>,
202
206
  ): AntigravityEffortWireModelIds | undefined {
203
- const explicitEffortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[pickerId];
207
+ const explicitEffortMap = Object.hasOwn(ANTIGRAVITY_EFFORT_WIRE_MAP, pickerId)
208
+ ? ANTIGRAVITY_EFFORT_WIRE_MAP[pickerId] : undefined;
204
209
  if (explicitEffortMap && Object.values(explicitEffortMap).every(wireId => available.has(wireId))) {
205
210
  return { ...explicitEffortMap };
206
211
  }
207
212
 
208
- if (!isKnownAntigravityPickerModelId(pickerId)) return undefined;
213
+ if (!isValidModelDiscoveryModelId(pickerId)) return undefined;
209
214
  const suffixEffortMap: AntigravityEffortWireModelIds = {};
210
215
  for (const effort of ANTIGRAVITY_DISCOVERY_EFFORTS) {
211
216
  const wireId = `${pickerId}-${effort}`;
@@ -364,7 +369,7 @@ interface DiscoveredWireModelMapping {
364
369
  readonly generation?: { provider: string; cacheGeneration: string };
365
370
  }
366
371
 
367
- const discoveredWireModelsByBaseUrl = new Map<string, DiscoveredWireModelMapping>();
372
+ const discoveredWireModelsByBaseUrl = new Map<string, DiscoveredWireModelMapping | null>();
368
373
 
369
374
  /**
370
375
  * Strip trailing slashes without a backtracking regex.
@@ -387,9 +392,9 @@ function antigravityBaseUrlKey(baseUrl: string | undefined): string | undefined
387
392
  const url = new URL(trimmed);
388
393
  url.hash = "";
389
394
  url.search = "";
390
- return stripTrailingSlashes(url.toString()).toLowerCase();
395
+ return stripTrailingSlashes(url.toString());
391
396
  } catch {
392
- return trimmed.toLowerCase();
397
+ return trimmed;
393
398
  }
394
399
  }
395
400
 
@@ -401,13 +406,23 @@ export function registerAntigravityDiscoveredWireModels(
401
406
  ): void {
402
407
  const key = antigravityBaseUrlKey(baseUrl);
403
408
  if (!key) return;
409
+ if (generation) {
410
+ if (!isModelCacheGenerationCurrent(generation.provider, generation.cacheGeneration)) return;
411
+ const families: Record<string, AntigravitySuffixMap> = Object.create(null);
412
+ for (const model of models) {
413
+ if (ANTIGRAVITY_DISCOVERY_EFFORTS.every(effort => model.effortWireModelIds?.[effort] === `${model.id}-${effort}`)) {
414
+ families[model.id] = { low: `${model.id}-low`, medium: `${model.id}-medium`, high: `${model.id}-high` };
415
+ }
416
+ }
417
+ writeAntigravityWireSnapshot(key, { version: 1, provider: generation.provider, families });
418
+ }
404
419
  const wireModels = new Map<string, string>();
405
420
  const effortModels = new Map<string, AntigravityEffortWireModelIds>();
406
421
  for (const model of models) {
407
422
  wireModels.set(model.id, model.wireModelId);
408
423
  if (model.effortWireModelIds) effortModels.set(model.id, { ...model.effortWireModelIds });
409
424
  }
410
- discoveredWireModelsByBaseUrl.set(key, {
425
+ discoveredWireModelsByBaseUrl.set(`${getConfigDir()}\0${key}`, {
411
426
  models: wireModels,
412
427
  effortModels,
413
428
  ...(generation ? { generation } : {}),
@@ -419,11 +434,21 @@ function discoveredAntigravityMapping(
419
434
  ): DiscoveredWireModelMapping | undefined {
420
435
  const key = antigravityBaseUrlKey(baseUrl);
421
436
  if (!key) return undefined;
422
- const mapping = discoveredWireModelsByBaseUrl.get(key);
437
+ const scopedKey = `${getConfigDir()}\0${key}`;
438
+ if (!discoveredWireModelsByBaseUrl.has(scopedKey)) {
439
+ const saved = readAntigravityWireSnapshot(key);
440
+ discoveredWireModelsByBaseUrl.set(scopedKey, saved ? {
441
+ models: new Map(Object.entries(saved.families).map(([id, map]) => [id, map.medium])),
442
+ effortModels: new Map(Object.entries(saved.families)),
443
+ generation: { provider: saved.provider, cacheGeneration: captureModelCacheGeneration(saved.provider) },
444
+ } : null);
445
+ }
446
+ const mapping = discoveredWireModelsByBaseUrl.get(scopedKey);
423
447
  if (!mapping) return undefined;
424
448
  if (mapping.generation
425
449
  && !isModelCacheGenerationCurrent(mapping.generation.provider, mapping.generation.cacheGeneration)) {
426
- discoveredWireModelsByBaseUrl.delete(key);
450
+ // Keep a tombstone: clearing account/cache authority must not reload old disk evidence.
451
+ discoveredWireModelsByBaseUrl.set(scopedKey, null);
427
452
  return undefined;
428
453
  }
429
454
  return mapping;
@@ -450,7 +475,8 @@ function discoveredAntigravityEffortWireModelId(
450
475
  }
451
476
 
452
477
  const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]
453
- ?? ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
478
+ ?? ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId]
479
+ ?? "medium";
454
480
  if (defaultEffort && isAntigravityDiscoveryEffort(defaultEffort) && effortMap[defaultEffort]) {
455
481
  return effortMap[defaultEffort];
456
482
  }
@@ -573,20 +599,27 @@ export function parseAntigravityAvailableModels(
573
599
  if (seen.has(id)) continue;
574
600
  seen.add(id);
575
601
  const effortWireModelIds = completeDiscoveredEffortWireModelIds(id, available);
602
+ const tierInfo = effortWireModelIds
603
+ ? Object.values(effortWireModelIds).map(wire => available.get(wire)!) : [info];
604
+ const windows = tierInfo.map(tier => antigravityPositiveInteger(tier.maxTokens));
605
+ const contextWindow = windows.every((window): window is number => window !== undefined)
606
+ ? Math.min(...windows) : undefined;
607
+ const supportsImages = tierInfo.every(tier => tier.supportsImages === true) ? true
608
+ : tierInfo.some(tier => tier.supportsImages === false) ? false : undefined;
576
609
  out.push({
577
610
  id,
578
611
  wireModelId: wireId,
579
612
  ...(effortWireModelIds ? { effortWireModelIds } : {}),
580
- ...(antigravityPositiveInteger(info.maxTokens) ? { contextWindow: antigravityPositiveInteger(info.maxTokens) } : {}),
613
+ ...(contextWindow ? { contextWindow } : {}),
581
614
  // Tri-state, deliberately not a ternary: `true` asserts image support,
582
615
  // `false` asserts against it, and ABSENT is unknown. Collapsing absent into
583
616
  // `["text"]` let routing read it as a confident `image: false` (#1796). The
584
617
  // strict catalog still receives its `["text"]` compatibility default
585
618
  // downstream via ensureStrictCatalogFields; only the routing-evidence
586
619
  // channel stays honest about what was never asserted.
587
- ...(info.supportsImages === true
620
+ ...(supportsImages === true
588
621
  ? { inputModalities: ["text", "image"] as string[] }
589
- : info.supportsImages === false
622
+ : supportsImages === false
590
623
  ? { inputModalities: ["text"] as string[] }
591
624
  : {}),
592
625
  });
@@ -737,6 +770,11 @@ const ANTIGRAVITY_USAGE_BASE_BY_ID: Record<string, string> = (() => {
737
770
  // onto a model that did not exist then, and away from the 3.6 price row that still
738
771
  // prices it correctly. Retirement changes what we CALL, not what we RECORD.
739
772
  for (const retired of Object.keys(RETIRED_FLASH_TIERS)) rev[retired] = retired;
773
+ // Usage identity is stable even before discovery; routing keeps its own live evidence.
774
+ for (const base of ["claude-sonnet-5-5", "claude-opus-5-5"]) {
775
+ rev[base] = base;
776
+ for (const effort of ANTIGRAVITY_DISCOVERY_EFFORTS) rev[`${base}-${effort}`] = base;
777
+ }
740
778
  // Visible aliases that only appear in ANTIGRAVITY_VISIBLE_MODEL_ALIASES are already
741
779
  // included via ANTIGRAVITY_MODEL_ALIASES. Identity bases without effort maps remain.
742
780
  return rev;
@@ -0,0 +1,66 @@
1
+ import { createHash } from "node:crypto";
2
+ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { atomicWriteFileNoFollow } from "../config/atomic-write";
5
+ import { getConfigDir } from "../config/paths";
6
+ import { isValidProviderName } from "../config/provider-name";
7
+ import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS, MODEL_DISCOVERY_MAX_RESPONSE_BYTES } from "./model-discovery-limits";
8
+
9
+ export type AntigravitySuffixMap = Record<"low" | "medium" | "high", string>;
10
+ export interface AntigravityWireSnapshot {
11
+ version: 1;
12
+ provider: string;
13
+ families: Record<string, AntigravitySuffixMap>;
14
+ }
15
+ const EFFORTS = ["low", "medium", "high"] as const;
16
+
17
+ function snapshotPath(destination: string): string {
18
+ const key = createHash("sha256").update(destination).digest("hex");
19
+ return join(getConfigDir(), `antigravity-wire-${key}.json`);
20
+ }
21
+
22
+ function validatedSnapshot(value: unknown): AntigravityWireSnapshot | undefined {
23
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
24
+ const row = value as Record<string, unknown>;
25
+ if (row.version !== 1 || typeof row.provider !== "string" || !isValidProviderName(row.provider)
26
+ || !row.families || typeof row.families !== "object" || Array.isArray(row.families)) return undefined;
27
+ const entries = Object.entries(row.families);
28
+ if (entries.length > MODEL_DISCOVERY_MAX_MODELS) return undefined;
29
+ const families: Record<string, AntigravitySuffixMap> = Object.create(null);
30
+ for (const [base, candidate] of entries) {
31
+ if (!isValidModelDiscoveryModelId(base) || !candidate || typeof candidate !== "object" || Array.isArray(candidate)) return undefined;
32
+ const map = candidate as Record<string, unknown>;
33
+ if (!EFFORTS.every(effort => map[effort] === `${base}-${effort}` && isValidModelDiscoveryModelId(map[effort]))) return undefined;
34
+ families[base] = { low: `${base}-low`, medium: `${base}-medium`, high: `${base}-high` };
35
+ }
36
+ return { version: 1, provider: row.provider, families };
37
+ }
38
+
39
+ /** Routing evidence only, never an account's current availability or entitlement. */
40
+ export function readAntigravityWireSnapshot(destination: string): AntigravityWireSnapshot | undefined {
41
+ let fd: number | undefined;
42
+ try {
43
+ fd = openSync(snapshotPath(destination), "r");
44
+ const stat = fstatSync(fd);
45
+ if (!stat.isFile() || stat.size > MODEL_DISCOVERY_MAX_RESPONSE_BYTES) return undefined;
46
+ const bytes = Buffer.alloc(Math.min(stat.size + 1, MODEL_DISCOVERY_MAX_RESPONSE_BYTES + 1));
47
+ let length = 0;
48
+ while (length < bytes.byteLength) {
49
+ const read = readSync(fd, bytes, length, bytes.byteLength - length, null);
50
+ if (read === 0) break;
51
+ length += read;
52
+ }
53
+ if (length > stat.size || length > MODEL_DISCOVERY_MAX_RESPONSE_BYTES) return undefined;
54
+ return validatedSnapshot(JSON.parse(bytes.subarray(0, length).toString("utf8")));
55
+ } catch { return undefined; }
56
+ finally { if (fd !== undefined) closeSync(fd); }
57
+ }
58
+
59
+ /** Throw before publishing new synthetic catalog IDs if their routing evidence cannot be saved. */
60
+ export function writeAntigravityWireSnapshot(destination: string, snapshot: AntigravityWireSnapshot): void {
61
+ const validated = validatedSnapshot(snapshot);
62
+ if (!validated) throw new Error("invalid Antigravity wire snapshot");
63
+ const text = JSON.stringify(validated);
64
+ if (Buffer.byteLength(text) > MODEL_DISCOVERY_MAX_RESPONSE_BYTES) throw new Error("Antigravity wire snapshot exceeds size limit");
65
+ atomicWriteFileNoFollow(snapshotPath(destination), text);
66
+ }
@@ -590,6 +590,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
590
590
  prov.supportsResponsesCustomTools = entry.supportsResponsesCustomTools;
591
591
  }
592
592
  if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
593
+ if (prov.preserveResponsesInputItemIds === undefined && entry.preserveResponsesInputItemIds !== undefined) prov.preserveResponsesInputItemIds = entry.preserveResponsesInputItemIds;
594
+ if (prov.preserveResponsesMessageMetadata === undefined && entry.preserveResponsesMessageMetadata !== undefined) prov.preserveResponsesMessageMetadata = entry.preserveResponsesMessageMetadata;
593
595
  if (prov.dropResponsesReasoningItems === undefined && entry.dropResponsesReasoningItems !== undefined) prov.dropResponsesReasoningItems = entry.dropResponsesReasoningItems;
594
596
  applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
595
597
  applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
@@ -30,6 +30,8 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
30
30
  fastEnabled: "none",
31
31
  modelSupportsServiceTier: "record",
32
32
  preserveResponsesReasoningContent: "none",
33
+ preserveResponsesInputItemIds: "none",
34
+ preserveResponsesMessageMetadata: "none",
33
35
  modelReasoningEffortsAuthoritative: "none",
34
36
  dropResponsesReasoningItems: "none",
35
37
  decodesNativeCompactionBlobs: "none",
@@ -69,6 +71,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
69
71
  autoReviewModel: "none",
70
72
  autoReviewModelOverrides: "record",
71
73
  headers: "none",
74
+ forwardClientHeaders: "none",
72
75
  openRouterRouting: "none",
73
76
  modelOpenRouterRouting: "record",
74
77
  vercelGatewayRouting: "none",
@@ -1,5 +1,9 @@
1
1
  import type { OcxConfig } from "../types";
2
- import { routedSlug } from "./slug-codec";
2
+ import { routedSlug, slugEquals } from "./slug-codec";
3
+ import {
4
+ antigravityEffortFamilyIds, antigravityFamilyDisabled,
5
+ type AntigravityEffortFamilyRow,
6
+ } from "./antigravity-effort-families";
3
7
 
4
8
  export const MODEL_REMOVAL_GRACE_FETCHES = 3;
5
9
  export const MAX_KNOWN_MODELS_PER_PROVIDER = 2_000;
@@ -115,30 +119,63 @@ export function effectiveNewModelPolicy(config: OcxConfig, provider: string): Ne
115
119
  return config.modelDiscovery?.newModelPolicy ?? "on";
116
120
  }
117
121
 
122
+ /** Normalize policy identities only; the original rows and saved wire selections stay intact. */
123
+ function normalizeEffortFamilies(config: OcxConfig, provider: string, rows: AntigravityEffortFamilyRow[]) {
124
+ const aliases = new Map<string, string>();
125
+ const inheritedDisables: string[] = [];
126
+ const bases = new Set(rows.filter(row => antigravityEffortFamilyIds(row)).map(row => row.id));
127
+ for (const row of rows) {
128
+ const ids = antigravityEffortFamilyIds(row);
129
+ if (!ids) continue;
130
+ for (const id of ids) if (!bases.has(id)) aliases.set(id, row.id);
131
+ if (antigravityFamilyDisabled(config, row, rows)
132
+ && !config.disabledModels?.some(slug => slugEquals(slug, provider, row.id))) {
133
+ inheritedDisables.push(routedSlug(provider, row.id));
134
+ }
135
+ }
136
+ const normalize = (id: string) => aliases.get(id) ?? id;
137
+ const normalizeIds = (ids: string[]) => [...new Set(ids.map(normalize))].sort();
138
+ const prior = config.modelDiscovery?.knownModels?.[provider];
139
+ let baseline = prior;
140
+ if (prior && aliases.size) {
141
+ const missing: Record<string, number> = {};
142
+ for (const [id, count] of Object.entries(prior.missing ?? {})) {
143
+ const key = normalize(id);
144
+ // A family cannot disappear sooner than its least-missing known tier.
145
+ missing[key] = Math.min(missing[key] ?? count, count);
146
+ }
147
+ baseline = { ...prior, ids: normalizeIds(prior.ids), removed: normalizeIds(prior.removed),
148
+ ...(Object.keys(missing).length ? { missing } : {}) };
149
+ }
150
+ return { discoveredIds: normalizeIds(rows.map(row => row.id)), baseline, inheritedDisables };
151
+ }
152
+
118
153
  /** Apply authoritative provider rows to a mutable convergence copy; degraded providers are omitted. */
119
154
  export function reconcileSuccessfulModelDiscoveries(options: {
120
155
  config: OcxConfig;
121
- models: Iterable<{ provider: string; id: string; custom?: boolean }>;
156
+ models: Iterable<AntigravityEffortFamilyRow>;
122
157
  authoritativeProviders: Iterable<string>;
123
158
  now: string;
124
159
  /** See {@link NewModelPolicyMode}; defaults to `converge`. */
125
160
  mode?: NewModelPolicyMode;
126
161
  }): boolean {
127
- const byProvider = new Map<string, string[]>();
162
+ const byProvider = new Map<string, AntigravityEffortFamilyRow[]>();
128
163
  for (const model of options.models) {
129
- if (model.custom) continue;
130
- const ids = byProvider.get(model.provider) ?? [];
131
- ids.push(model.id); byProvider.set(model.provider, ids);
164
+ if (model.custom || model.catalogKind === "custom-model-v1") continue;
165
+ const rows = byProvider.get(model.provider) ?? [];
166
+ rows.push(model); byProvider.set(model.provider, rows);
132
167
  }
133
168
  let changed = false;
134
169
  for (const provider of options.authoritativeProviders) {
135
170
  const configured = options.config.providers[provider];
136
171
  if (!configured || configured.liveModels === false) continue;
137
- const discoveredIds = byProvider.get(provider) ?? [];
172
+ const { discoveredIds, baseline, inheritedDisables } = normalizeEffortFamilies(
173
+ options.config, provider, byProvider.get(provider) ?? [],
174
+ );
138
175
  const discovery = options.config.modelDiscovery ??= {};
139
176
  const known = discovery.knownModels ??= {};
140
177
  const result = applyNewModelPolicy({
141
- provider, discoveredIds, baseline: known[provider],
178
+ provider, discoveredIds, baseline,
142
179
  policy: effectiveNewModelPolicy(options.config, provider),
143
180
  hasSelectedModels: (configured.selectedModels?.length ?? 0) > 0,
144
181
  now: options.now,
@@ -151,9 +188,10 @@ export function reconcileSuccessfulModelDiscoveries(options: {
151
188
  // Keep the previous timestamp when nothing else moved, so a steady-state roster does not
152
189
  // make the baseline look dirty on the next comparison either.
153
190
  if (!baselineChanged && priorBaseline) known[provider] = priorBaseline;
154
- if (result.slugsToDisable.length) {
191
+ const slugsToDisable = [...result.slugsToDisable, ...inheritedDisables];
192
+ if (slugsToDisable.length) {
155
193
  const disabled = options.config.disabledModels ??= [];
156
- for (const slug of result.slugsToDisable) {
194
+ for (const slug of slugsToDisable) {
157
195
  if (disabled.includes(slug)) continue;
158
196
  disabled.push(slug);
159
197
  changed = true;
@@ -35,6 +35,7 @@ import {
35
35
  ZAI_GLM_5X_REASONING_EFFORTS,
36
36
  MINIMAX_MODELS,
37
37
  MINIMAX_MODEL_CONTEXT_WINDOWS,
38
+ MINIMAX_MODEL_INPUT_MODALITIES,
38
39
  MINIMAX_M3_REASONING_EFFORTS,
39
40
  MINIMAX_M3_REASONING_EFFORT_MAP,
40
41
  MINIMAX_M31_FLASH_PREVIEW,
@@ -1024,6 +1025,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1024
1025
  id: "minimax", label: "MiniMax — Coding Plan", baseUrl: "https://api.minimax.io/v1", adapter: "openai-chat", authKind: "key",
1025
1026
  dashboardUrl: "https://platform.minimax.io", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
1026
1027
  modelContextWindows: MINIMAX_MODEL_CONTEXT_WINDOWS,
1028
+ modelInputModalities: MINIMAX_MODEL_INPUT_MODALITIES,
1027
1029
  modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS, [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_REASONING_EFFORTS },
1028
1030
  modelDefaultReasoningEfforts: { "MiniMax-M3": "medium", [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
1029
1031
  modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
@@ -1049,6 +1051,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1049
1051
  id: "minimax-cn", label: "MiniMax — Coding Plan (CN)", baseUrl: "https://api.minimaxi.com/v1", adapter: "openai-chat", authKind: "key",
1050
1052
  dashboardUrl: "https://platform.minimaxi.com", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
1051
1053
  modelContextWindows: MINIMAX_MODEL_CONTEXT_WINDOWS,
1054
+ modelInputModalities: MINIMAX_MODEL_INPUT_MODALITIES,
1052
1055
  modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS, [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_REASONING_EFFORTS },
1053
1056
  modelDefaultReasoningEfforts: { "MiniMax-M3": "medium", [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
1054
1057
  modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
@@ -87,6 +87,8 @@ export const REGISTRY_FIELD_MODEL_ID_ROLES = {
87
87
  fastOptIn: NONE,
88
88
  modelServiceTierCapabilityBaseUrlGuard: NONE,
89
89
  preserveResponsesReasoningContent: NONE,
90
+ preserveResponsesInputItemIds: NONE,
91
+ preserveResponsesMessageMetadata: NONE,
90
92
  dropResponsesReasoningItems: NONE,
91
93
  modelSupportsReasoningSummaries: RECORD_KEYS,
92
94
  modelSupportsVerbosity: RECORD_KEYS,
@@ -172,6 +172,11 @@ export const MINIMAX_REASONING_SPLIT_MODELS = MINIMAX_MODELS_BEFORE_M31;
172
172
  export const MINIMAX_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
173
173
  MINIMAX_MODELS.map(id => [id, id === "MiniMax-M3" || id === MINIMAX_M31_FLASH_PREVIEW ? 1_000_000 : 204_800]),
174
174
  );
175
+ /** MiniMax's OpenAI-compatible M3 endpoints accept image_url; keep video off Codex's input enum. */
176
+ export const MINIMAX_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
177
+ "MiniMax-M3": ["text", "image"],
178
+ [MINIMAX_M31_FLASH_PREVIEW]: ["text", "image"],
179
+ };
175
180
  export const MINIMAX_M3_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
176
181
  /** Identity efforts on the wire; no map, so none omits the field instead of disabling thinking. */
177
182
  export const MINIMAX_M31_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
@@ -278,6 +278,10 @@ export interface ProviderRegistryEntry {
278
278
  modelServiceTierCapabilityBaseUrlGuard?: (baseUrl: string) => boolean;
279
279
  /** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
280
280
  preserveResponsesReasoningContent?: boolean;
281
+ /** Registry default for preserving input item IDs; see `OcxProviderConfig.preserveResponsesInputItemIds`. */
282
+ preserveResponsesInputItemIds?: boolean;
283
+ /** Registry default for preserving per-item chat metadata; see `OcxProviderConfig.preserveResponsesMessageMetadata`. */
284
+ preserveResponsesMessageMetadata?: boolean;
281
285
  /** Registry default for dropping replayed reasoning items for Responses upstreams that reject them. */
282
286
  dropResponsesReasoningItems?: boolean;
283
287
  /** Registry defaults for per-model Codex reasoning propagation; explicit user keys win during enrichment. */
@@ -15,6 +15,25 @@
15
15
  * routed models get a short "history was compacted" note instead.
16
16
  */
17
17
 
18
+ import type { TranslatorBudget } from "../lib/translator-budget";
19
+
20
+ const ciphertextLeases = new WeakMap<object, { budget: TranslatorBudget; bytes: number }>();
21
+
22
+ /** Attach an already-charged parser ciphertext lease to its exact terminal event. */
23
+ export function transferCompactionCiphertextLease(event: object, budget: TranslatorBudget, bytes: number): void {
24
+ if (bytes === 0) return;
25
+ if (ciphertextLeases.has(event)) throw new Error("compaction ciphertext lease is already transferred");
26
+ ciphertextLeases.set(event, { budget, bytes });
27
+ }
28
+
29
+ /** Release only this event's transferred ciphertext lease, once and for its owning budget. */
30
+ export function releaseCompactionCiphertextLease(event: object, budget: TranslatorBudget): void {
31
+ const lease = ciphertextLeases.get(event);
32
+ if (!lease || lease.budget !== budget) return;
33
+ ciphertextLeases.delete(event);
34
+ budget.releaseRetained(lease.bytes, { kind: "retained_collectors" });
35
+ }
36
+
18
37
  export const OCX_COMPACTION_PREFIX = "ocx1:";
19
38
 
20
39
  export const OCX_NATIVE_REPLAY_RECOVERY_NOTE =
package/src/router.ts CHANGED
@@ -426,6 +426,12 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
426
426
  ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined
427
427
  ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent }
428
428
  : {}),
429
+ ...(provider.preserveResponsesInputItemIds === undefined && registryEntry.preserveResponsesInputItemIds !== undefined
430
+ ? { preserveResponsesInputItemIds: registryEntry.preserveResponsesInputItemIds }
431
+ : {}),
432
+ ...(provider.preserveResponsesMessageMetadata === undefined && registryEntry.preserveResponsesMessageMetadata !== undefined
433
+ ? { preserveResponsesMessageMetadata: registryEntry.preserveResponsesMessageMetadata }
434
+ : {}),
429
435
  ...(provider.dropResponsesReasoningItems === undefined && registryEntry.dropResponsesReasoningItems !== undefined
430
436
  ? { dropResponsesReasoningItems: registryEntry.dropResponsesReasoningItems }
431
437
  : {}),
@@ -27,6 +27,7 @@ import {
27
27
  positiveIntegerRecordConfigError,
28
28
  providerBaseUrlConfigError,
29
29
  providerHeadersConfigError,
30
+ providerForwardClientHeadersConfigError,
30
31
  reasoningSummaryDeliveryRecordConfigError,
31
32
  upstreamHttpVersionConfigError,
32
33
  } from "../config/provider-validation";
@@ -765,6 +766,9 @@ export function providerManagementConfigError(
765
766
  // validation and then rejected by the seed comparison, so canonical OpenAI could never
766
767
  // set OR clear it — the value was admitted and then refused in the same request.
767
768
  delete canonicalCandidate.annotateEmptyToolOutputs;
769
+ // forwardClientHeaders is an editor-managed request-metadata overlay. It is validated
770
+ // separately below and must not make an otherwise canonical OpenAI provider fail the seed check.
771
+ delete canonicalCandidate.forwardClientHeaders;
768
772
  // Canonical ChatGPT keeps WebSocket as the default, but an operator may
769
773
  // select the existing HTTP/SSE path without changing its auth or endpoint.
770
774
  if (raw.upstreamWebsocket !== undefined) {
@@ -800,6 +804,8 @@ export function providerManagementConfigError(
800
804
  }
801
805
  const headersError = providerHeadersConfigError(typed.headers);
802
806
  if (headersError) return `provider ${name} ${headersError}`;
807
+ const forwardClientHeadersError = providerForwardClientHeadersConfigError(raw.forwardClientHeaders);
808
+ if (forwardClientHeadersError) return `provider ${name} ${forwardClientHeadersError}`;
803
809
  const retryOn429Error = retryOn429PolicyConfigError(raw.retryOn429);
804
810
  if (retryOn429Error) {
805
811
  // The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
@@ -998,6 +1004,8 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
998
1004
  supportsServiceTier: "editor",
999
1005
  modelSupportsServiceTier: "editor",
1000
1006
  preserveResponsesReasoningContent: "editor",
1007
+ preserveResponsesInputItemIds: "editor",
1008
+ preserveResponsesMessageMetadata: "editor",
1001
1009
  dropResponsesReasoningItems: "editor",
1002
1010
  modelReasoningEffortsAuthoritative: "editor",
1003
1011
  decodesNativeCompactionBlobs: "editor",
@@ -1042,6 +1050,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
1042
1050
  modelMaxOutputTokens: "editor",
1043
1051
  modelCosts: "editor",
1044
1052
  headers: "redacted",
1053
+ forwardClientHeaders: "editor",
1045
1054
  openRouterRouting: "editor",
1046
1055
  modelOpenRouterRouting: "editor",
1047
1056
  vercelGatewayRouting: "editor",
@@ -86,6 +86,7 @@ import { parseRequestEffortRowId } from "./effort-row";
86
86
  import { parseSyntheticRowId } from "./fast-row";
87
87
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
88
88
  import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported } from "../codex/loopback-target";
89
+ import { applyDroidReasoningDefault, droidReasoningDefault, DROID_DEFAULT_EFFORT_HEADER } from "./droid-reasoning-default";
89
90
 
90
91
  type Rec = Record<string, unknown>;
91
92
 
@@ -157,6 +158,9 @@ async function handleChatCompletionsWithBudget(
157
158
 
158
159
  const requestedModel = chatBody.model as string;
159
160
  const { fastRow, effortRow } = parseSyntheticRowId(requestedModel, config);
161
+ const droidDefaultEffort = effortRow
162
+ ? undefined
163
+ : droidReasoningDefault(req.headers.get(DROID_DEFAULT_EFFORT_HEADER), chatBody);
160
164
  if (effortRow) chatBody.model = effortRow.baseId;
161
165
  if (fastRow) {
162
166
  chatBody.model = fastRow.baseId;
@@ -198,6 +202,9 @@ async function handleChatCompletionsWithBudget(
198
202
  getOrAllocateRequestSessionLane(req),
199
203
  routedProvider,
200
204
  );
205
+ if (!route.combo && route.routeKind !== "policy") {
206
+ applyDroidReasoningDefault(chatBody, droidDefaultEffort, { provider: route.provider, modelId: route.modelId });
207
+ }
201
208
  logCtx.model = route.modelId;
202
209
  logCtx.providerAdapter = route.provider.adapter;
203
210
  logCtx.requestedModel = requestedModel;
@@ -442,6 +449,9 @@ async function handleChatCompletionsWithBudget(
442
449
  abortSignal: req.signal,
443
450
  // Body is Responses-shaped by now, but the client spoke Chat Completions.
444
451
  inboundWire: "chat",
452
+ ...((settledRoute?.combo || settledRoute?.routeKind === "policy") && droidDefaultEffort
453
+ ? { droidDefaultEffort }
454
+ : {}),
445
455
  // PF-07: the combo sends eligible candidates natively from this envelope.
446
456
  ...(envelope && nativeChatCombos ? {
447
457
  protocolSource: createNativeChatComboSource({