@bitkyc08/opencodex 2.59.0 → 2.60.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 (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -1,6 +1,5 @@
1
1
  import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
2
2
  import { initializeProviderModelSelection } from "../providers/initial-model-selection";
3
- import { parseCallbackInput } from "./callback-server";
4
3
  import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
5
4
  import { ConfigMutationLockError, loadConfig, mutatePersistedConfig, saveConfig } from "../config";
6
5
  import { resolveProviderApiKey } from "../providers/key-store";
@@ -51,8 +50,9 @@ import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../provider
51
50
  import { resolveProviderTransport } from "../providers/xai-transport";
52
51
  import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
53
52
  import { logOAuthEvent } from "./log";
54
- import { captureConfigGeneration, sweepExpiredOnWrite, type GenerationContext } from "../lib/state-store-sweeper";
55
- import { retainedUtf8Bytes } from "../lib/admission";
53
+ import { captureConfigGeneration, sweepExpiredOnWrite } from "../lib/state-store-sweeper";
54
+ import { clearManualCodeSlot, ensureManualCodeSlot, kiroLoginSettling, loginAbort, loginState, waitForManualLoginCode } from "./login-flow-state";
55
+ export { reconcileOAuthFlowState, submitManualLoginCode } from "./login-flow-state";
56
56
  import { randomUUID } from "node:crypto";
57
57
  export {
58
58
  CODEX_HEALTH_AUTH_FAILED_NOTE,
@@ -1710,110 +1710,6 @@ export async function runLogin(
1710
1710
  * localhost), the GUI can POST the final redirect URL or authorization code via
1711
1711
  * submitManualLoginCode(), which feeds OAuthController.onManualCodeInput.
1712
1712
  */
1713
- const loginState = new Map<string, { error?: string; done: boolean }>();
1714
- const loginAbort = new Map<string, AbortController>();
1715
- const kiroLoginSettling = new Set<string>();
1716
-
1717
- /** Pending paste for a login in progress: either a waiter or a stashed early submission. */
1718
- interface ManualCodeSlot {
1719
- pendingInput?: string;
1720
- resolve?: (value: string) => void;
1721
- /** Registered by the callback flow so submits can validate state synchronously. */
1722
- expectedState?: string;
1723
- }
1724
- const loginManual = new Map<string, ManualCodeSlot>();
1725
- const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
1726
- let lastOAuthFlowReconciledGeneration = 0;
1727
-
1728
- export function reconcileOAuthFlowState(context: GenerationContext): number {
1729
- if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
1730
- let removed = 0;
1731
- for (const [provider, state] of loginState) {
1732
- if (context.providerNames.has(provider) || !state.done || loginAbort.has(provider)) continue;
1733
- if (loginState.delete(provider)) removed += 1;
1734
- if (loginManual.delete(provider)) removed += 1;
1735
- if (loginAbort.delete(provider)) removed += 1;
1736
- }
1737
- lastOAuthFlowReconciledGeneration = context.generation;
1738
- return removed;
1739
- }
1740
-
1741
- function clearManualCodeSlot(provider: string): void {
1742
- loginManual.delete(provider);
1743
- }
1744
-
1745
- function ensureManualCodeSlot(provider: string): ManualCodeSlot {
1746
- let slot = loginManual.get(provider);
1747
- if (!slot) {
1748
- slot = {};
1749
- loginManual.set(provider, slot);
1750
- }
1751
- return slot;
1752
- }
1753
-
1754
- /** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
1755
- function waitForManualLoginCode(provider: string, signal: AbortSignal, expectedState?: string): Promise<string> {
1756
- if (signal.aborted) {
1757
- return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
1758
- }
1759
- const slot = ensureManualCodeSlot(provider);
1760
- if (expectedState !== undefined) slot.expectedState = expectedState;
1761
- if (slot.pendingInput !== undefined) {
1762
- const value = slot.pendingInput;
1763
- slot.pendingInput = undefined;
1764
- return Promise.resolve(value);
1765
- }
1766
- return new Promise<string>((resolve, reject) => {
1767
- const onAbort = () => {
1768
- if (slot.resolve === resolve) slot.resolve = undefined;
1769
- reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
1770
- };
1771
- signal.addEventListener("abort", onAbort, { once: true });
1772
- slot.resolve = (value: string) => {
1773
- signal.removeEventListener("abort", onAbort);
1774
- if (slot.resolve === resolve) slot.resolve = undefined;
1775
- resolve(value);
1776
- };
1777
- });
1778
- }
1779
-
1780
- /**
1781
- * Feed a pasted redirect URL or authorization code into an in-progress GUI login.
1782
- * Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
1783
- * here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
1784
- */
1785
- export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
1786
- const trimmed = input.trim();
1787
- if (!trimmed) return { ok: false, error: "empty code" };
1788
- if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) return { ok: false, error: "code too large" };
1789
- const st = loginState.get(provider);
1790
- if (!st || st.done) return { ok: false, error: "no login in progress" };
1791
- const slot = ensureManualCodeSlot(provider);
1792
- // Synchronous validation (validated request/ack): reject un-parseable input and
1793
- // authorization responses (url/query kind) whose state is missing or mismatched
1794
- // once the flow has registered its expected state. Raw codes stay in-session-PKCE
1795
- // protected. Early posts (flow not yet waiting, no expectedState) are stashed and
1796
- // re-validated by the callback loop.
1797
- const parsed = parseCallbackInput(trimmed);
1798
- // Command Code's manual fallback accepts a pasted JSON callback payload
1799
- // (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
1800
- // shared gate so the provider-specific parser can validate it.
1801
- const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
1802
- if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
1803
- if (parsed.kind !== "raw" && slot.expectedState !== undefined) {
1804
- if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
1805
- if (parsed.state !== slot.expectedState) return { ok: false, error: "state mismatch — paste the redirect URL from THIS login attempt" };
1806
- }
1807
- if (slot.resolve) {
1808
- const resolve = slot.resolve;
1809
- slot.resolve = undefined;
1810
- resolve(trimmed);
1811
- } else {
1812
- // Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
1813
- slot.pendingInput = trimmed;
1814
- }
1815
- return { ok: true };
1816
- }
1817
1713
 
1818
1714
  export interface OAuthAccountSummary {
1819
1715
  id: string;
@@ -0,0 +1,127 @@
1
+ import { parseCallbackInput } from "./callback-server";
2
+ import { retainedUtf8Bytes } from "../lib/admission";
3
+ import type { GenerationContext } from "../lib/state-store-sweeper";
4
+
5
+ /**
6
+ * In-flight login flow state and the manual paste slot that feeds it.
7
+ *
8
+ * Split out of `index.ts` when that file crossed the repository file-size ratchet at 2009
9
+ * lines. The cut follows a seam rather than a line count: everything here is bookkeeping for
10
+ * a login that has started and not yet settled, and none of it reads or writes a stored
11
+ * credential. `index.ts` re-exports the two public names, so existing importers are unaffected.
12
+ */
13
+ export const loginState = new Map<string, { error?: string; done: boolean }>();
14
+ export const loginAbort = new Map<string, AbortController>();
15
+ export const kiroLoginSettling = new Set<string>();
16
+
17
+ /** Pending paste for a login in progress: either a waiter or a stashed early submission. */
18
+ export interface ManualCodeSlot {
19
+ pendingInput?: string;
20
+ resolve?: (value: string) => void;
21
+ /** Registered by the callback flow so submits can validate state synchronously. */
22
+ expectedState?: string;
23
+ }
24
+ const loginManual = new Map<string, ManualCodeSlot>();
25
+ const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
26
+ let lastOAuthFlowReconciledGeneration = 0;
27
+
28
+ export function reconcileOAuthFlowState(context: GenerationContext): number {
29
+ if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
30
+ let removed = 0;
31
+ for (const [provider, state] of loginState) {
32
+ if (context.providerNames.has(provider) || !state.done || loginAbort.has(provider)) continue;
33
+ if (loginState.delete(provider)) removed += 1;
34
+ if (loginManual.delete(provider)) removed += 1;
35
+ if (loginAbort.delete(provider)) removed += 1;
36
+ }
37
+ lastOAuthFlowReconciledGeneration = context.generation;
38
+ return removed;
39
+ }
40
+
41
+ export function clearManualCodeSlot(provider: string): void {
42
+ loginManual.delete(provider);
43
+ }
44
+
45
+ export function ensureManualCodeSlot(provider: string): ManualCodeSlot {
46
+ let slot = loginManual.get(provider);
47
+ if (!slot) {
48
+ slot = {};
49
+ loginManual.set(provider, slot);
50
+ }
51
+ return slot;
52
+ }
53
+
54
+ /** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
55
+ export function waitForManualLoginCode(provider: string, signal: AbortSignal, expectedState?: string): Promise<string> {
56
+ if (signal.aborted) {
57
+ return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
58
+ }
59
+ const slot = ensureManualCodeSlot(provider);
60
+ if (expectedState !== undefined) slot.expectedState = expectedState;
61
+ if (slot.pendingInput !== undefined) {
62
+ const value = slot.pendingInput;
63
+ slot.pendingInput = undefined;
64
+ return Promise.resolve(value);
65
+ }
66
+ return new Promise<string>((resolve, reject) => {
67
+ const onAbort = () => {
68
+ if (slot.resolve === resolve) slot.resolve = undefined;
69
+ reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
70
+ };
71
+ signal.addEventListener("abort", onAbort, { once: true });
72
+ slot.resolve = (value: string) => {
73
+ signal.removeEventListener("abort", onAbort);
74
+ if (slot.resolve === resolve) slot.resolve = undefined;
75
+ resolve(value);
76
+ };
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Feed a pasted redirect URL or authorization code into an in-progress GUI login.
82
+ * Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
83
+ * here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
84
+ */
85
+ export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
86
+ const trimmed = input.trim();
87
+ if (!trimmed) return { ok: false, error: "empty code" };
88
+ if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) return { ok: false, error: "code too large" };
89
+ const st = loginState.get(provider);
90
+ if (!st || st.done) return { ok: false, error: "no login in progress" };
91
+ const slot = ensureManualCodeSlot(provider);
92
+ // Synchronous validation (validated request/ack): reject un-parseable input and
93
+ // authorization responses (url/query kind) whose state is missing or mismatched
94
+ // once the flow has registered its expected state. Raw codes stay in-session-PKCE
95
+ // protected — but a raw paste carrying an explicit #state suffix is state-bearing
96
+ // and checked too. Early posts (flow not yet waiting, no expectedState) are
97
+ // stashed and re-validated by the callback loop.
98
+ const parsed = parseCallbackInput(trimmed);
99
+ // Command Code's manual fallback accepts a pasted JSON callback payload
100
+ // (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
101
+ // shared gate so the provider-specific parser can validate it.
102
+ const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
103
+ if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
104
+ // A raw paste carrying an explicit code#state suffix is state-bearing too: it
105
+ // must match the expected state rather than bypass validation.
106
+ const stateBearing = parsed.kind !== "raw" || parsed.state !== undefined;
107
+ if (stateBearing && slot.expectedState !== undefined) {
108
+ if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
109
+ if (parsed.state !== slot.expectedState) {
110
+ return {
111
+ ok: false,
112
+ error: parsed.kind === "raw"
113
+ ? "state mismatch — paste the bare code, or the correct code#state from THIS login attempt"
114
+ : "state mismatch — paste the redirect URL from THIS login attempt",
115
+ };
116
+ }
117
+ }
118
+ if (slot.resolve) {
119
+ const resolve = slot.resolve;
120
+ slot.resolve = undefined;
121
+ resolve(trimmed);
122
+ } else {
123
+ // Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
124
+ slot.pendingInput = trimmed;
125
+ }
126
+ return { ok: true };
127
+ }
@@ -1,5 +1,6 @@
1
1
  import type { CodexAccountMode, OcxProviderConfig } from "../types";
2
2
  import { cloneFastWire } from "./fastwire";
3
+ import { resolveModelPolicy } from "./resolved-model-policy";
3
4
  import {
4
5
  PROVIDER_REGISTRY,
5
6
  registryEntryForProviderDestination,
@@ -220,6 +221,14 @@ export function applyDirectReasoningEffortContracts(
220
221
  */
221
222
  export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderConfig {
222
223
  const liveModels = registryEntrySupportsLiveModelDiscovery(entry) ? entry.liveModels : false;
224
+ const staticPolicy = resolveModelPolicy({
225
+ providerName: entry.id,
226
+ modelId: entry.defaultModel ?? entry.models?.[0] ?? "__provider_seed__",
227
+ provider: { adapter: entry.adapter, baseUrl: entry.baseUrl, authMode: entry.authKind },
228
+ registryEntry: entry,
229
+ transportMatchedRegistry: true,
230
+ effectiveAuth: { authMode: entry.authKind },
231
+ }).provider;
223
232
  return {
224
233
  adapter: entry.adapter,
225
234
  baseUrl: entry.baseUrl,
@@ -239,17 +248,17 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
239
248
  ...(entry.models ? { models: [...entry.models] } : {}),
240
249
  ...(liveModels !== undefined ? { liveModels } : {}),
241
250
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
242
- ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
243
- ...(entry.modelDisplayNames ? { modelDisplayNames: { ...entry.modelDisplayNames } } : {}),
244
- ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
245
- ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: { ...entry.modelMaxInputTokens } } : {}),
251
+ ...(staticPolicy.modelContextWindows ? { modelContextWindows: { ...staticPolicy.modelContextWindows } } : {}),
252
+ ...(staticPolicy.modelDisplayNames ? { modelDisplayNames: { ...staticPolicy.modelDisplayNames } } : {}),
253
+ ...(staticPolicy.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(staticPolicy.modelInputModalities) } : {}),
254
+ ...(staticPolicy.modelMaxInputTokens ? { modelMaxInputTokens: { ...staticPolicy.modelMaxInputTokens } } : {}),
246
255
  ...(entry.defaultMaxOutputTokens !== undefined ? { defaultMaxOutputTokens: entry.defaultMaxOutputTokens } : {}),
247
- ...(entry.modelMaxOutputTokens ? { modelMaxOutputTokens: { ...entry.modelMaxOutputTokens } } : {}),
256
+ ...(staticPolicy.modelMaxOutputTokens ? { modelMaxOutputTokens: { ...staticPolicy.modelMaxOutputTokens } } : {}),
248
257
  ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
249
- ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
250
- ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
251
- ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
252
- ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
258
+ ...(staticPolicy.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(staticPolicy.modelReasoningEfforts) } : {}),
259
+ ...(staticPolicy.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...staticPolicy.modelDefaultReasoningEfforts } } : {}),
260
+ ...(staticPolicy.reasoningEffortMap ? { reasoningEffortMap: { ...staticPolicy.reasoningEffortMap } } : {}),
261
+ ...(staticPolicy.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(staticPolicy.modelReasoningEffortMap) } : {}),
253
262
  ...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}),
254
263
  ...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
255
264
  ...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
@@ -447,13 +456,16 @@ function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistry
447
456
  * was skipped and the reasoning ladder was advertised without summary support — exactly the
448
457
  * inconsistency that makes Codex drop the inbound reasoning object.
449
458
  *
450
- * Deliberately narrow: reasoning-summary and effort metadata only, via
459
+ * Deliberately narrow: reasoning-summary, effort, and replay-compatibility metadata only, via
451
460
  * `registryEntryForProviderDestination`, which matches fixed key destinations and refuses
452
461
  * templated or overridable base URLs. A custom row keeps its own identity for everything else.
453
462
  */
454
463
  function enrichReasoningMetadataByDestination(prov: OcxProviderConfig): void {
455
464
  const destination = registryEntryForProviderDestination(prov);
456
465
  applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries);
466
+ if (prov.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined) {
467
+ prov.dropResponsesReasoningItems = destination.dropResponsesReasoningItems;
468
+ }
457
469
  if (destination?.modelReasoningEfforts) {
458
470
  prov.modelReasoningEfforts = fillRecordOfArrays(destination.modelReasoningEfforts, prov.modelReasoningEfforts);
459
471
  }
@@ -491,6 +503,14 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
491
503
  modelReasoningEffortMap: prov.modelReasoningEffortMap,
492
504
  };
493
505
  const seed = providerConfigSeed(entry);
506
+ const resolvedStatic = resolveModelPolicy({
507
+ providerName: name,
508
+ modelId: prov.defaultModel ?? entry.defaultModel ?? "__provider_enrich__",
509
+ provider: prov,
510
+ registryEntry: entry,
511
+ transportMatchedRegistry: true,
512
+ ...(prov.authMode ? { effectiveAuth: { authMode: prov.authMode } } : {}),
513
+ }).provider;
494
514
  repairStaticModelCatalogProvider(name, prov);
495
515
  if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
496
516
  if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
@@ -504,10 +524,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
504
524
  if (!prov.modelContextWindows && seed.modelContextWindows) prov.modelContextWindows = { ...seed.modelContextWindows };
505
525
  // Per-model fill, not all-or-nothing: an operator who renamed ONE model must still receive
506
526
  // labels for the rest, and an existing install must pick up newly seeded rows on enrich.
507
- if (seed.modelDisplayNames) {
508
- prov.modelDisplayNames = { ...seed.modelDisplayNames, ...(prov.modelDisplayNames ?? {}) };
509
- }
510
- if (seed.modelInputModalities) prov.modelInputModalities = fillRecordOfArrays(seed.modelInputModalities, prov.modelInputModalities);
527
+ if (resolvedStatic.modelDisplayNames) prov.modelDisplayNames = { ...resolvedStatic.modelDisplayNames };
528
+ if (resolvedStatic.modelInputModalities) prov.modelInputModalities = cloneRecordOfArrays(resolvedStatic.modelInputModalities);
511
529
  if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
512
530
  if (!prov.modelMaxOutputTokens && seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens };
513
531
  if ((!prov.reasoningEfforts || hasLegacyClinePassReasoningEfforts(name, prov)) && seed.reasoningEfforts) {
@@ -518,9 +536,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
518
536
  // provider. That split the two planes apart — routing merges these maps per key
519
537
  // (mergeRecordFill in src/router.ts), so the wire honored the effort while /v1/models and
520
538
  // every client export showed no effort control at all.
521
- if (seed.modelReasoningEfforts) {
522
- prov.modelReasoningEfforts = fillRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts);
523
- }
539
+ if (resolvedStatic.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(resolvedStatic.modelReasoningEfforts);
524
540
  if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
525
541
  if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
526
542
  if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap);
@@ -563,6 +579,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
563
579
  prov.supportsResponsesCustomTools = entry.supportsResponsesCustomTools;
564
580
  }
565
581
  if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
582
+ if (prov.dropResponsesReasoningItems === undefined && entry.dropResponsesReasoningItems !== undefined) prov.dropResponsesReasoningItems = entry.dropResponsesReasoningItems;
566
583
  applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
567
584
  applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
568
585
  applyVerbosityDefaults(prov, entry);
@@ -12,14 +12,12 @@
12
12
  * key: the `devin-cli` registry entry is gone, so the PROVIDER_REGISTRY lookup
13
13
  * that gated the rewrite here could never fire again.
14
14
  *
15
- * The adapter repair must keep working for CUSTOM-NAMED rows. The ACP adapter
16
- * was removed, so `"devin-cli"` is not a constructible adapter id and
17
- * `createRegisteredAdapter` would throw `Unknown adapter: devin-cli`. The
18
- * canonical `devin` row gets its adapter pinned from the registry by
19
- * `routedProviderConfig`, but a row named e.g. `"devin-acp"` has nothing
20
- * pinning it and would fail every request. So every row naming the retired
21
- * adapter is rewritten to `devin`, whatever the row is called, and each
22
- * rewrite is reported.
15
+ * Custom-named rows fail closed. The retired ACP transport used the installed
16
+ * CLI's identity and ignored both configured and caller credentials, so
17
+ * silently changing one of those rows into a cloud provider could send a
18
+ * credential with different provenance to Cognition. The warning tells the
19
+ * operator to reconfigure it explicitly; retaining the unknown adapter id
20
+ * prevents requests from reaching any upstream in the meantime.
23
21
  *
24
22
  * A row carrying the retired ACP identity URL is repointed at the api-server
25
23
  * in the same pass. That URL was never a destination — it existed only so
@@ -55,10 +53,16 @@ export function projectDevinCliAuthMode(config: OcxConfig): DevinCliAuthModeProj
55
53
  const warnings: string[] = [];
56
54
  let changed = false;
57
55
 
58
- // Every row, not only the registry id: a custom-named row had no registry pin,
59
- // so after the ACP removal it is the one that cannot construct an adapter.
60
56
  for (const [name, row] of Object.entries(config.providers ?? {})) {
61
57
  if (!row || row.adapter !== RETIRED_ACP_ADAPTER) continue;
58
+ if (name !== RETIRED_ACP_ADAPTER) {
59
+ warnings.push(
60
+ `left custom provider "${name}" unchanged because adapter ${RETIRED_ACP_ADAPTER} was removed: `
61
+ + "the row was not migrated; switch its adapter to devin and configure Devin authentication "
62
+ + "explicitly before using this provider.",
63
+ );
64
+ continue;
65
+ }
62
66
  row.adapter = "devin";
63
67
  changed = true;
64
68
  let detail = "";
@@ -11,6 +11,8 @@
11
11
  import { commitProviderApiKeySelection } from "./api-key-selection";
12
12
  import type { ProviderApiKeySelection } from "../types/provider";
13
13
  import { routedProviderConfig } from "../router";
14
+ import { getProviderRegistryEntry } from "./registry";
15
+ import { normalizedBaseUrl } from "./quota/vendor-probes-key";
14
16
  import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, TransientRetryPolicy } from "../types";
15
17
  import { OPENCODE_GO_SESSION_HEADER } from "./opencode-go-transport";
16
18
  import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
@@ -278,6 +280,43 @@ const DEFAULT_RATE_LIMIT_RETRY = {
278
280
  respectRetryAfter: true,
279
281
  } as const satisfies Required<RateLimitRetryPolicy>;
280
282
 
283
+ /**
284
+ * Patient same-target 429 fallback for the OpenCode Go destination
285
+ * (https://opencode.ai/zen/go/v1), which serves subscription traffic such as Muse Spark.
286
+ * Single-key pools cannot fail over, so without this a burst 429 surfaces immediately and
287
+ * the client retry budget aborts the goal. Six 10s-paced attempts absorb a short burst
288
+ * window (effective replays are additionally bounded by the shared per-request send
289
+ * budget); Retry-After is still honored and capped. An explicit `retryOn429` (including
290
+ * `enabled: false`) always overrides this fallback.
291
+ */
292
+ const OPENCODE_GO_RATE_LIMIT_RETRY = {
293
+ enabled: true,
294
+ attempts: 6,
295
+ intervalMs: 10_000,
296
+ maxIntervalMs: 60_000,
297
+ respectRetryAfter: true,
298
+ } as const satisfies Required<RateLimitRetryPolicy>;
299
+
300
+ /** True when the provider row points at the OpenCode Go destination. */
301
+ export function isOpenCodeGoDestination(
302
+ provider: Partial<Pick<OcxProviderConfig, "baseUrl" | "authMode">>,
303
+ ): boolean {
304
+ const raw = typeof provider.baseUrl === "string" ? provider.baseUrl : "";
305
+ if (!raw.trim()) return false;
306
+ // Endpoint identity, not adapter identity: the runtime adapter is already overridden
307
+ // per model by the time the recovery loop runs (muse-spark rides `openai-responses`
308
+ // while the preset declares `openai-chat`), so an adapter-strict lookup misses it.
309
+ // Canonicalize with the shared quota-probe normalizer so host case and explicit
310
+ // default ports compare equal; userinfo, query, and fragment never match
311
+ // (follow-up to the review on #5067).
312
+ const endpoint = normalizedBaseUrl(raw.trim());
313
+ if (!endpoint) return false;
314
+ const entry = getProviderRegistryEntry("opencode-go");
315
+ if (!entry) return false;
316
+ const candidates = [entry.baseUrl, ...(entry.destinationAliases ?? []).map(alias => alias.baseUrl)];
317
+ return candidates.some(url => normalizedBaseUrl(url.trim()) === endpoint);
318
+ }
319
+
281
320
  /**
282
321
  * Default transient-5xx retry used when a provider opts in with a bare
283
322
  * `transientRetryOn5xx: {}`. `attempts` is a TOTAL send budget, not extra retries.
@@ -513,29 +552,38 @@ export function selectProactiveApiKeyTransport(
513
552
  }
514
553
 
515
554
  /**
516
- * Normalize a provider's `retryOn429` policy, or return null when the knob is absent,
517
- * explicitly disabled, or the provider is not key-auth (OAuth/forward credentials must not be
518
- * replayed on the same token, forward passthrough never reaches the recovery loop anyway, and
519
- * local runtimes have no remote key to preserve). The returned policy is fully defaulted so
520
- * callers never re-check fields.
555
+ * Normalize a provider's `retryOn429` policy. An explicit object always wins (including
556
+ * `enabled: false` to opt out). When the knob is absent, the OpenCode Go destination
557
+ * (subscription traffic such as Muse Spark) falls back to a patient same-key policy so a
558
+ * burst 429 waits and replays instead of surfacing to the client and aborting a long
559
+ * session; every other provider without the knob keeps today's fail-fast behavior.
560
+ * OAuth/forward/local credentials are never replayed on the same token. The returned
561
+ * policy is fully defaulted so callers never re-check fields.
521
562
  */
522
563
  export function rateLimitRetryPolicyFor(
523
- provider: Pick<OcxProviderConfig, "retryOn429" | "authMode">,
564
+ provider: Pick<OcxProviderConfig, "retryOn429" | "authMode"> &
565
+ Partial<Pick<OcxProviderConfig, "baseUrl" | "adapter">>,
524
566
  ): Required<RateLimitRetryPolicy> | null {
525
567
  const policy = provider.retryOn429;
526
- if (!policy || policy.enabled === false) return null;
527
- // Fail closed: only explicit key auth or the documented omitted-default (undefined == key for
528
- // custom API-key providers) may use same-key replays. OAuth/forward are never replayed on the
529
- // same token, local runtimes have no remote key to preserve, and unknown/custom values are
530
- // rejected rather than guessed at.
568
+ if (policy) {
569
+ if (policy.enabled === false) return null;
570
+ // Fail closed: only explicit key auth or the documented omitted-default (undefined == key for
571
+ // custom API-key providers) may use same-key replays. OAuth/forward are never replayed on the
572
+ // same token, local runtimes have no remote key to preserve, and unknown/custom values are
573
+ // rejected rather than guessed at.
574
+ if (provider.authMode !== undefined && provider.authMode !== "key") return null;
575
+ return {
576
+ enabled: policy.enabled ?? DEFAULT_RATE_LIMIT_RETRY.enabled,
577
+ attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
578
+ intervalMs: policy.intervalMs ?? DEFAULT_RATE_LIMIT_RETRY.intervalMs,
579
+ maxIntervalMs: policy.maxIntervalMs ?? DEFAULT_RATE_LIMIT_RETRY.maxIntervalMs,
580
+ respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
581
+ };
582
+ }
583
+ // No explicit knob: patient fallback for the OpenCode Go destination only.
531
584
  if (provider.authMode !== undefined && provider.authMode !== "key") return null;
532
- return {
533
- enabled: policy.enabled ?? DEFAULT_RATE_LIMIT_RETRY.enabled,
534
- attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
535
- intervalMs: policy.intervalMs ?? DEFAULT_RATE_LIMIT_RETRY.intervalMs,
536
- maxIntervalMs: policy.maxIntervalMs ?? DEFAULT_RATE_LIMIT_RETRY.maxIntervalMs,
537
- respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
538
- };
585
+ if (!isOpenCodeGoDestination(provider)) return null;
586
+ return { ...OPENCODE_GO_RATE_LIMIT_RETRY };
539
587
  }
540
588
 
541
589
  /**
@@ -14,9 +14,12 @@
14
14
  // This migration is deliberately NOT general reconciliation. It rewrites exactly
15
15
  // one thing: an id this file declares retired, on a provider that still carries
16
16
  // the registry's transport, and only when the registry currently seeds the
17
- // replacement. Everything else in the row is left alone.
17
+ // replacement — and it skips a field where the retired entry is registry residue
18
+ // with nothing left to carry forward (see `isRegistryResidue`, issue #5066).
19
+ // Everything else in the row is left alone.
18
20
 
19
21
  import { PROVIDER_REGISTRY } from "./registry";
22
+ import { providerConfigSeed } from "./derive";
20
23
  import type { OcxConfig, OcxProviderConfig } from "../types";
21
24
 
22
25
  export interface ModelRename {
@@ -194,6 +197,54 @@ function registrySeedsTarget(rename: ModelRename): boolean {
194
197
  return !!entry?.models?.includes(rename.to);
195
198
  }
196
199
 
200
+ /** The row the registry contributes to a saved config, or `undefined` when it dropped the id. */
201
+ function registrySeed(provider: string): Record<string, unknown> | undefined {
202
+ const entry = PROVIDER_REGISTRY.find(row => row.id === provider);
203
+ return entry ? (providerConfigSeed(entry) as unknown as Record<string, unknown>) : undefined;
204
+ }
205
+
206
+ /** Whether a saved list or model-keyed record names this id at all. */
207
+ function publishes(container: unknown, id: string): boolean {
208
+ if (Array.isArray(container)) return container.includes(id);
209
+ if (!container || typeof container !== "object") return false;
210
+ return id in (container as Record<string, unknown>);
211
+ }
212
+
213
+ /**
214
+ * Whether this field’s retired entry is registry residue rather than something to repair.
215
+ *
216
+ * Two things have to be true. The registry’s own seed — `providerConfigSeed`, the row both the
217
+ * OAuth preset (`deriveOAuthProviderConfig`) and `enrichProviderFromRegistry` contribute — must
218
+ * still publish the retired id in this field, so the entry arrives in the config on its own. And
219
+ * the saved field must already carry the supported id, so the rename would only DELETE a key,
220
+ * with nothing to carry forward.
221
+ *
222
+ * Under those two conditions the rewrite is worse than a no-op, because it never converges.
223
+ * `startServer` runs this migration and then `reconcileOAuthProviders`, and
224
+ * `applyOAuthPresetCatalog` copies the preset’s record over the saved one whenever the two
225
+ * differ. The deletion is undone inside the same boot, the config is written twice, and the user
226
+ * is told about a rename that never sticks on every single start. Issue #5066 is exactly that:
227
+ * `ANTIGRAVITY_MODEL_CONTEXT_WINDOWS` derives a window for every compatibility alias, so
228
+ * `modelContextWindows` holds a key for all nine retired Flash ids beside the live ones — the
229
+ * nine `[model-rename-migration]` lines the reporter saw at every `ocx start`, none of which
230
+ * required them to have selected any of those nine models.
231
+ *
232
+ * Those registry entries are deliberate and stay: a request that still names `gemini-3.6-flash`
233
+ * routes to 3.7 and needs a context window under the id it asked for. The second condition is
234
+ * what keeps this narrow. A row that saved `{ "gemini-3.6-flash": 1048576 }` and nothing else
235
+ * still gets the value carried onto the supported id, because dropping it there would blank the
236
+ * record instead of moving it (`tests/adapters/google/gemini-37-flash-migration.test.ts`).
237
+ */
238
+ function isRegistryResidue(
239
+ seed: Record<string, unknown> | undefined,
240
+ field: string,
241
+ saved: unknown,
242
+ rename: ModelRename,
243
+ ): boolean {
244
+ if (!seed) return false;
245
+ return publishes(seed[field], rename.from) && publishes(saved, rename.to);
246
+ }
247
+
197
248
  export interface ModelRenameProjection {
198
249
  config: OcxConfig;
199
250
  changed: boolean;
@@ -226,14 +277,17 @@ export function projectModelRenames(
226
277
  // Provider config is a closed interface, so index through one unknown-cast
227
278
  // view rather than casting at each assignment.
228
279
  const row = prov as unknown as Record<string, unknown>;
280
+ const seed = registrySeed(rename.provider);
229
281
  let touched = false;
230
282
  for (const field of MODEL_ID_LISTS) {
283
+ if (isRegistryResidue(seed, field, row[field], rename)) continue;
231
284
  const next = renameInList(row[field], rename.from, rename.to);
232
285
  if (!next) continue;
233
286
  row[field] = next;
234
287
  touched = true;
235
288
  }
236
289
  for (const field of MODEL_KEYED_RECORDS) {
290
+ if (isRegistryResidue(seed, field, row[field], rename)) continue;
237
291
  const next = rename.dropReasoningEffortMap && field === "modelReasoningEffortMap"
238
292
  ? dropFromRecord(row[field], rename.from)
239
293
  : renameInRecord(row[field], rename.from, rename.to);
@@ -1,12 +1,13 @@
1
1
  import { mutatePersistedConfig } from "../config";
2
2
  import { projectModelRenames } from "./model-rename-migration";
3
3
  import { projectStaleContextWindows } from "./stale-context-window-migration";
4
+ import { projectStaleVisionClassifications } from "./stale-vision-classification-migration";
4
5
  import { projectDevinCliAuthMode } from "./devin-cli-authmode-migration";
5
6
  import type { OcxConfig } from "../types";
6
7
 
7
8
  /**
8
- * The startup projection: registry model renames, then the context-window
9
- * repair. Both fix a saved row the registry can no longer reach on its own —
9
+ * The startup projection: registry model renames, then the context-window and
10
+ * vision-classification repairs. All three fix a saved row the registry can no longer reach on its own —
10
11
  * `enrichProviderFromRegistry` backfills a missing field and never rewrites a
11
12
  * present one — so they share this pass rather than adding a second boot step
12
13
  * with its own persistence, adopt, and failure handling.
@@ -14,11 +15,12 @@ import type { OcxConfig } from "../types";
14
15
  export function projectStartupConfigRepairs(config: OcxConfig): ReturnType<typeof projectModelRenames> {
15
16
  const renames = projectModelRenames(config);
16
17
  const windows = projectStaleContextWindows(renames.config);
17
- const devinCli = projectDevinCliAuthMode(windows.config);
18
+ const vision = projectStaleVisionClassifications(windows.config);
19
+ const devinCli = projectDevinCliAuthMode(vision.config);
18
20
  return {
19
21
  config: devinCli.config,
20
- changed: renames.changed || windows.changed || devinCli.changed,
21
- warnings: [...renames.warnings, ...windows.warnings, ...devinCli.warnings],
22
+ changed: renames.changed || windows.changed || vision.changed || devinCli.changed,
23
+ warnings: [...renames.warnings, ...windows.warnings, ...vision.warnings, ...devinCli.warnings],
22
24
  };
23
25
  }
24
26