@bitkyc08/opencodex 2.58.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.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
+ }
@@ -2,6 +2,7 @@
2
2
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
3
3
  import { generatePKCE } from "./pkce";
4
4
  import type { OAuthController, OAuthCredentials } from "./types";
5
+ import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBytes } from "../lib/bounded-body";
5
6
 
6
7
  export const ORCAROUTER_DEFAULT_API_BASE_URL = "https://api.orcarouter.ai";
7
8
  export const ORCAROUTER_DEFAULT_AUTH_BASE_URL = "https://www.orcarouter.ai";
@@ -147,6 +148,7 @@ export class OrcaRouterOAuthFlow extends OAuthCallbackFlow {
147
148
 
148
149
  async exchangeToken(code: string, _state: string, _redirectUri: string): Promise<OAuthCredentials> {
149
150
  if (!this.#verifier) throw new Error("OrcaRouter PKCE verifier was not initialized");
151
+ const signal = requestSignal(this.ctrl.signal);
150
152
  let response: Response;
151
153
  try {
152
154
  response = await fetch(new URL("/api/v1/auth/keys", this.#authBaseUrl), {
@@ -158,7 +160,7 @@ export class OrcaRouterOAuthFlow extends OAuthCallbackFlow {
158
160
  code_challenge_method: "S256",
159
161
  }),
160
162
  redirect: "error",
161
- signal: requestSignal(this.ctrl.signal),
163
+ signal,
162
164
  });
163
165
  } catch (error) {
164
166
  if (this.ctrl.signal?.aborted) {
@@ -171,9 +173,20 @@ export class OrcaRouterOAuthFlow extends OAuthCallbackFlow {
171
173
  // never turn a code, verifier, or accidentally returned key into console output.
172
174
  throw new Error(`OrcaRouter key exchange failed with HTTP ${response.status}`);
173
175
  }
176
+ const { bytes, oversized } = await readBoundedResponseBytes(response, {
177
+ maxBytes: BOUNDED_BODY_MAX_BYTES,
178
+ signal,
179
+ }).catch(() => {
180
+ if (signal.aborted) throw signal.reason;
181
+ // Preserve the existing non-reflective error for response-body failures.
182
+ throw new Error("OrcaRouter key exchange returned invalid JSON");
183
+ });
184
+ if (oversized) {
185
+ throw new Error(`OrcaRouter key exchange response exceeded the ${BOUNDED_BODY_MAX_BYTES}-byte limit`);
186
+ }
174
187
  let payload: unknown;
175
188
  try {
176
- payload = await response.json();
189
+ payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
177
190
  } catch {
178
191
  throw new Error("OrcaRouter key exchange returned invalid JSON");
179
192
  }
@@ -437,6 +437,14 @@ function backupLegacyOnce(): void {
437
437
  try {
438
438
  copyFileSync(path, backup);
439
439
  try { chmodSync(backup, 0o600); } catch { /* best-effort */ }
440
+ try {
441
+ // Register only the copy we just created. An unowned home still needs downgrade recovery.
442
+ if (!recordOwnedConfigPath(getConfigDir(), backup)) {
443
+ console.warn("[oauth] Recovery backup created, but uninstall ownership registration failed.");
444
+ }
445
+ } catch {
446
+ console.warn("[oauth] Recovery backup created, but uninstall ownership registration failed.");
447
+ }
440
448
  } catch { /* best-effort */ }
441
449
  }
442
450
 
@@ -36,6 +36,15 @@ export const CODEX_CAPACITY_MAX_QUOTA_AGE_MS = 30 * 60_000;
36
36
  export type CodexCapacityQuota = {
37
37
  fiveHourPercent?: number;
38
38
  fiveHourResetAt?: number;
39
+ /**
40
+ * Local observation time for the burst-window percentage.
41
+ *
42
+ * Carried because the terminal-short-window rule needs it whenever the reading has no reset
43
+ * instant, and the dashboard's account-switch warning evaluates that rule against this DTO.
44
+ * Dropping it here made the warning return "no opinion" for a snapshot routing was already
45
+ * refusing on (#5045).
46
+ */
47
+ shortObservedAt?: number;
39
48
  weeklyPercent?: number;
40
49
  weeklyResetAt?: number;
41
50
  monthlyPercent?: number;
@@ -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 = "";
@@ -195,33 +195,54 @@ const DEFAULT_DEPS: DevinProviderMergeStartupDeps = {
195
195
  * the snapshot is taken strictly before the save, and a backup failure throws
196
196
  * rather than writing without a rollback point.
197
197
  *
198
- * The auth half is deliberately detached. `startServer` is synchronous — an
199
- * `await` in the boot window would suspend the composition root — and
198
+ * Both destination slots are inspected before either account-bound file is
199
+ * changed. The auth write itself is deliberately detached. `startServer` is
200
+ * synchronous — an `await` in the boot window would suspend the composition root — and
200
201
  * `mutateStore` is async-only, so the rekey is fired after its snapshot and
201
- * its outcome is logged when it lands. That is safe here: the credential is
202
- * valid under either slot name while the `devin-cli` alias exists, a conflict
203
- * refuses by design, and a failed rekey simply retries on the next boot.
202
+ * its outcome is logged when it lands. A late concurrent conflict refuses by
203
+ * design, and a failed rekey simply retries on the next boot.
204
204
  */
205
205
  export function runDevinProviderMergeStartupMigration(
206
206
  config: OcxConfig,
207
207
  deps: DevinProviderMergeStartupDeps = DEFAULT_DEPS,
208
208
  ): OcxConfig {
209
209
  const projection = deps.project(config);
210
- // Warnings are emitted even on a no-op: the collision case IS the warning.
210
+ const hasSourceConfig = config.providers?.[FROM_ID] !== undefined;
211
+ const hasSourceAuth = deps.hasAuthSlot(FROM_ID);
212
+ const hasDestinationAuth = deps.hasAuthSlot(TO_ID);
213
+
214
+ // A configured provider and its credentials are one account-bound unit. Do
215
+ // not move either half if the config projection refused, or if the target
216
+ // credential slot could belong to another account.
217
+ if (hasSourceConfig && (!projection.changed || hasDestinationAuth)) {
218
+ // Projection warnings still matter on a no-op: a config collision is the warning.
219
+ for (const warning of projection.warnings) console.warn(`[devin-provider-merge] ${warning}`);
220
+ if (projection.changed && hasDestinationAuth) {
221
+ console.warn(
222
+ `[devin-provider-merge] auth.json already has a "${TO_ID}" credential slot; `
223
+ + `provider "${FROM_ID}" and both credential slots were left untouched. Remove the `
224
+ + "unused destination credential manually, then restart.",
225
+ );
226
+ }
227
+ return config;
228
+ }
229
+
211
230
  for (const warning of projection.warnings) console.warn(`[devin-provider-merge] ${warning}`);
231
+
212
232
  let result = config;
213
233
  if (projection.changed) {
234
+ // Snapshot both account-bound files before changing either one.
235
+ if (hasSourceAuth) deps.backupAuth();
214
236
  deps.backupConfig();
215
237
  deps.save(projection.config);
216
238
  result = projection.config;
217
239
  }
218
240
 
219
- // The auth rekey runs even when the config half refused or had nothing to
220
- // do: a `devin-cli` credential slot is orphaned state regardless of whether
221
- // a provider row still points at it, and the conflict check inside the
222
- // rekey is the same refuse-on-occupied rule the config half applies.
223
- if (!deps.hasAuthSlot(FROM_ID)) return result;
224
- deps.backupAuth();
241
+ // With no legacy config row, a `devin-cli` credential slot is orphaned and
242
+ // can still be rekeyed under the helper's refuse-on-occupied rule. A refused
243
+ // config migration returned above so its account-bound slot stays put.
244
+ if (!hasSourceAuth) return result;
245
+ if (!projection.changed) deps.backupAuth();
225
246
  void deps.rekey(FROM_ID, TO_ID).then(outcome => {
226
247
  if (outcome === "conflict") {
227
248
  console.warn(
@@ -1,3 +1,5 @@
1
+ import { getProviderRegistryEntry } from "./registry";
2
+
1
3
  export type ProviderAccessGroup =
2
4
  | "recurring-or-keyless"
3
5
  | "recurring-uncapped"
@@ -18,7 +20,7 @@ export const FREE_PROVIDER_ACCESS_GROUPS = {
18
20
  ],
19
21
  "recurring-credit": ["bytez", "nous-research"],
20
22
  "signup-credit": [
21
- "agentrouter", "ai21", "baichuan", "baseten", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
23
+ "agentrouter", "ai21", "baichuan", "baseten", "crusoe", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
22
24
  "hyperbolic", "longcat", "monsterapi", "nebius", "novita", "nscale", "nvidia", "predibase", "publicai", "qoder", "qoder-cn",
23
25
  "scaleway", "sensenova", "stepfun", "together", "vertex",
24
26
  ],
@@ -72,6 +74,21 @@ const openAi = (baseUrl: string, dashboardUrl: string, extra: Partial<Connectabl
72
74
  ...extra,
73
75
  });
74
76
 
77
+ function registryLiveKeyProvider(id: string): ConnectableOverride {
78
+ const entry = getProviderRegistryEntry(id);
79
+ if (!entry || entry.authKind !== "key" || !entry.dashboardUrl || entry.liveModels !== true) {
80
+ throw new TypeError(`Free directory registry projection requires a live key provider with a dashboard: ${id}`);
81
+ }
82
+ return {
83
+ baseUrl: entry.baseUrl,
84
+ dashboardUrl: entry.dashboardUrl,
85
+ adapter: entry.adapter,
86
+ authKind: "key",
87
+ discovery: "live",
88
+ liveModels: true,
89
+ };
90
+ }
91
+
75
92
  // API roots are limited to documented or primary-source integrations. Consumer-web/session
76
93
  // providers remain reference-only: this directory never asks users to paste cookies or bypass WAFs.
77
94
  const CONNECTABLE: Record<string, ConnectableOverride> = {
@@ -127,6 +144,7 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
127
144
  // Verified end-to-end 2026-07-30: /v1/models returns the OpenAI-shaped live catalog (13 models),
128
145
  // and a chat completion against moonshotai/Kimi-K3 returned a standard chat.completion payload.
129
146
  baseten: openAi("https://inference.baseten.co/v1", "https://app.baseten.co/settings/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.baseten.co/inference/model-apis/overview", modelsUrl: "https://inference.baseten.co/v1/models", lastVerified: "2026-07-30" }),
147
+ crusoe: { ...registryLiveKeyProvider("crusoe"), supportLevel: "supported", verification: "official", documentationUrl: "https://docs.crusoecloud.com/quickstart/getting-started-with-serverless-inference", modelsUrl: "https://api.inference.crusoecloud.com/v1/models", lastVerified: "2026-09-11" },
130
148
  deepinfra: openAi("https://api.deepinfra.com/v1/openai", "https://deepinfra.com/dash/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://deepinfra.com/docs/openai_api" }),
131
149
  deepseek: openAi("https://api.deepseek.com", "https://platform.deepseek.com/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://api-docs.deepseek.com/api/list-models" }),
132
150
  doubao: openAi("https://ark.cn-beijing.volces.com/api/v3", "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey", { verification: "official" }),
@@ -181,7 +199,7 @@ const LABELS: Record<string, string> = {
181
199
  "t3-web": "T3 Web", uncloseai: "UncloseAI", ainative: "AI Native", baidu: "Baidu Qianfan",
182
200
  glm: "Z.AI GLM", "glm-cn": "BigModel GLM (CN)", "kilo-gateway": "Kilo Gateway", "opencode-zen": "OpenCode Zen",
183
201
  sealion: "SEA-LION", bytez: "Bytez", "nous-research": "Nous Research", agentrouter: "AgentRouter",
184
- ai21: "AI21", baichuan: "Baichuan", deepinfra: "DeepInfra", deepseek: "DeepSeek", doubao: "Doubao",
202
+ ai21: "AI21", baichuan: "Baichuan", crusoe: "Crusoe", deepinfra: "DeepInfra", deepseek: "DeepSeek", doubao: "Doubao",
185
203
  "freemodel-dev": "FreeModel.dev", sambanova: "SambaNova Cloud", nebius: "Nebius Token Factory",
186
204
  novita: "Novita", nscale: "Nscale", nvidia: "NVIDIA NIM",
187
205
  publicai: "PublicAI", qoder: "Qoder", "qoder-cn": "Qoder CN", sensenova: "SenseNova", stepfun: "StepFun", vertex: "Google Vertex AI",