@bitkyc08/opencodex 2.79.0 → 2.81.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 (245) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/cursor/live-transport.ts +5 -3
  20. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  21. package/src/adapters/cursor/native-exec.ts +13 -5
  22. package/src/adapters/cursor/protobuf-request.ts +13 -7
  23. package/src/adapters/cursor/request-builder.ts +23 -10
  24. package/src/adapters/cursor/tool-definitions.ts +9 -6
  25. package/src/adapters/cursor/tool-guidance.ts +29 -8
  26. package/src/adapters/cursor/tool-naming.ts +40 -7
  27. package/src/adapters/cursor/tool-wording.ts +46 -0
  28. package/src/adapters/cursor.ts +3 -0
  29. package/src/adapters/devin/live-models.ts +2 -0
  30. package/src/adapters/devin.ts +25 -11
  31. package/src/adapters/kiro-retry.ts +9 -4
  32. package/src/adapters/openai-chat/messages.ts +7 -4
  33. package/src/adapters/physical-send.ts +8 -5
  34. package/src/adapters/qoder/adapter.ts +41 -6
  35. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  36. package/src/adapters/zed.ts +16 -1
  37. package/src/claude/inbound-cache-stabilize.ts +4 -9
  38. package/src/claude/inbound.ts +10 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-listener.ts +71 -4
  41. package/src/cli/account-anthropic-threshold.ts +14 -5
  42. package/src/cli/account-api.ts +3 -1
  43. package/src/cli/account-extended.ts +13 -9
  44. package/src/cli/account-policy-dto.ts +1 -1
  45. package/src/cli/account-policy.ts +13 -4
  46. package/src/cli/account.ts +3 -3
  47. package/src/cli/capabilities-accounts.ts +2 -2
  48. package/src/cli/capabilities-agents-routing.ts +24 -0
  49. package/src/cli/capabilities-integrations.ts +16 -0
  50. package/src/cli/codex-shim-autorestore.ts +3 -0
  51. package/src/cli/dispatch.ts +10 -1
  52. package/src/cli/help.ts +4 -1
  53. package/src/cli/index.ts +11 -5
  54. package/src/cli/integration-plan-dto.ts +1 -0
  55. package/src/cli/integration-preview.ts +8 -1
  56. package/src/cli/integrations.ts +32 -3
  57. package/src/cli/message-args.ts +34 -0
  58. package/src/cli/message-command.ts +43 -0
  59. package/src/cli/message-runtime.ts +6 -0
  60. package/src/cli/observe.ts +3 -1
  61. package/src/cli/provider.ts +29 -6
  62. package/src/cli/registry.ts +24 -2
  63. package/src/clients/config-export/commandcode.ts +131 -0
  64. package/src/clients/config-export/contracts.ts +12 -0
  65. package/src/clients/config-export.ts +64 -1
  66. package/src/codex/auth-api/account-list.ts +11 -3
  67. package/src/codex/auth-api/main-account-probe.ts +25 -8
  68. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  69. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  70. package/src/codex/auth-api/routes.ts +1 -0
  71. package/src/codex/autostart-health.ts +3 -3
  72. package/src/codex/catalog/auto-review.ts +13 -6
  73. package/src/codex/catalog/build-entries.ts +15 -7
  74. package/src/codex/catalog/control-plane.ts +45 -0
  75. package/src/codex/catalog/effort.ts +11 -1
  76. package/src/codex/catalog/gather-capture.ts +5 -3
  77. package/src/codex/catalog/parsing.ts +2 -1
  78. package/src/codex/catalog/provider-models.ts +15 -9
  79. package/src/codex/control-plane-models.ts +6 -0
  80. package/src/codex/journal.ts +36 -5
  81. package/src/codex/shim-overlay.ts +9 -1
  82. package/src/combos/jev-decision-contract.ts +25 -2
  83. package/src/combos/jev.ts +6 -3
  84. package/src/combos/types.ts +1 -1
  85. package/src/config/diagnostics.ts +27 -16
  86. package/src/config/load-degrade.ts +34 -0
  87. package/src/config/schema/anthropic-account-pool.ts +143 -0
  88. package/src/config/schema/config-schema.ts +7 -1
  89. package/src/config/schema/leaf-validators.ts +4 -0
  90. package/src/config.ts +2 -0
  91. package/src/generated/compatibility-version.json +347 -199
  92. package/src/generated/model-metadata.ts +5 -4
  93. package/src/images/loop.ts +2 -1
  94. package/src/integrations/catalog-refresh.ts +1 -1
  95. package/src/integrations/mutation-plan.ts +7 -0
  96. package/src/integrations/registry.ts +8 -0
  97. package/src/integrations/state.ts +9 -2
  98. package/src/integrations/writer.ts +8 -2
  99. package/src/lib/lab-live-route-production.ts +9 -1
  100. package/src/lib/request-execution-budget.ts +236 -58
  101. package/src/lib/spend-pool-alias-validation.ts +79 -0
  102. package/src/lib/spend-pool-continuity.ts +34 -0
  103. package/src/lib/spend-reservation-ledger.ts +412 -50
  104. package/src/lib/state-store-registrations.ts +20 -3
  105. package/src/lib/upstream-retry.ts +17 -5
  106. package/src/lib/workflow-budget.ts +53 -11
  107. package/src/messaging/budget.ts +45 -0
  108. package/src/messaging/discovery.ts +57 -0
  109. package/src/messaging/envelope.ts +49 -0
  110. package/src/messaging/input.ts +37 -0
  111. package/src/messaging/rpc.ts +196 -0
  112. package/src/messaging/send.ts +58 -0
  113. package/src/messaging/socket.ts +72 -0
  114. package/src/messaging/types.ts +29 -0
  115. package/src/oauth/anthropic-account-refusal.ts +51 -17
  116. package/src/oauth/anthropic-account-threshold.ts +8 -2
  117. package/src/oauth/anthropic-continuity.ts +15 -1
  118. package/src/oauth/anthropic-model-quota.ts +120 -48
  119. package/src/oauth/anthropic-model-routes.ts +11 -3
  120. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  121. package/src/oauth/anthropic-pool-config.ts +30 -0
  122. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  123. package/src/oauth/anthropic-routing.ts +1202 -866
  124. package/src/oauth/anthropic-send-ownership.ts +41 -0
  125. package/src/oauth/anthropic.ts +4 -1
  126. package/src/oauth/generic-account-failover.ts +3 -1
  127. package/src/oauth/health.ts +8 -6
  128. package/src/oauth/index.ts +90 -173
  129. package/src/oauth/kiro.ts +22 -9
  130. package/src/oauth/login-cli.ts +2 -0
  131. package/src/oauth/model-discovery-auth.ts +43 -0
  132. package/src/oauth/pool-kernel.ts +11 -5
  133. package/src/oauth/pool-settings-capability.ts +5 -3
  134. package/src/oauth/store-anthropic-instance.ts +87 -0
  135. package/src/oauth/store.ts +67 -9
  136. package/src/oauth/token-guardian.ts +2 -0
  137. package/src/oauth/types.ts +2 -0
  138. package/src/protocols/plan-snapshot.ts +5 -0
  139. package/src/protocols/settings.ts +20 -5
  140. package/src/providers/anthropic-instance-id.ts +47 -0
  141. package/src/providers/anthropic-instance.ts +48 -0
  142. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  143. package/src/providers/derive.ts +2 -0
  144. package/src/providers/kiro-models.ts +3 -0
  145. package/src/providers/label.ts +1 -1
  146. package/src/providers/model-rename-fields.ts +2 -0
  147. package/src/providers/quota/account-cache.ts +37 -11
  148. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  150. package/src/providers/quota/report-cache.ts +24 -1
  151. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  152. package/src/providers/quota.ts +22 -32
  153. package/src/providers/registry/entries-core.ts +35 -30
  154. package/src/providers/registry/model-ids.ts +1 -0
  155. package/src/providers/registry/model-seeds.ts +7 -4
  156. package/src/providers/registry/types.ts +3 -0
  157. package/src/providers/registry.ts +3 -3
  158. package/src/responses/spill-store.ts +5 -1
  159. package/src/responses/state/metrics.ts +87 -0
  160. package/src/responses/state/snapshot-codec.ts +6 -0
  161. package/src/responses/state/spill-queue.ts +1 -0
  162. package/src/responses/state/unforced-store-false.ts +55 -0
  163. package/src/responses/state.ts +50 -66
  164. package/src/router.ts +28 -4
  165. package/src/routing/compatibility/assemble.ts +1 -1
  166. package/src/routing/quota.ts +7 -3
  167. package/src/server/auth-cors.ts +5 -0
  168. package/src/server/chat-completions.ts +6 -1
  169. package/src/server/chat-native.ts +45 -25
  170. package/src/server/claude-messages.ts +16 -2
  171. package/src/server/gui-static.ts +38 -4
  172. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  173. package/src/server/index.ts +16 -16
  174. package/src/server/inference/context.ts +1 -1
  175. package/src/server/lifecycle.ts +1 -1
  176. package/src/server/management/agent-settings-routes.ts +30 -6
  177. package/src/server/management/anthropic-account-threshold.ts +12 -9
  178. package/src/server/management/anthropic-pool-settings.ts +72 -0
  179. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  180. package/src/server/management/config-routes.ts +27 -7
  181. package/src/server/management/decision-routes.ts +2 -1
  182. package/src/server/management/integration-routes.ts +25 -2
  183. package/src/server/management/oauth-account-routes.ts +79 -98
  184. package/src/server/management/provider-instance-ownership.ts +44 -0
  185. package/src/server/management/provider-routes.ts +21 -28
  186. package/src/server/management/route-registry.ts +3 -1
  187. package/src/server/management/routing-profile-routes.ts +4 -4
  188. package/src/server/management/system-restart.ts +44 -14
  189. package/src/server/management/system-routes.ts +24 -2
  190. package/src/server/management/vision-sidecar-options.ts +9 -6
  191. package/src/server/management/web-search-sidecar-options.ts +51 -2
  192. package/src/server/management-auth.ts +6 -0
  193. package/src/server/messages-native-eligibility.ts +3 -2
  194. package/src/server/messages-native-oauth.ts +86 -41
  195. package/src/server/messages-native-scope.ts +4 -1
  196. package/src/server/messages-native-selector.ts +39 -0
  197. package/src/server/messages-native.ts +84 -25
  198. package/src/server/request-log.ts +5 -0
  199. package/src/server/responses/adapter-continuation.ts +29 -17
  200. package/src/server/responses/adapter-dispatch.ts +47 -17
  201. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  202. package/src/server/responses/combo-requested-effort.ts +14 -0
  203. package/src/server/responses/compact.ts +25 -8
  204. package/src/server/responses/compaction-recovery.ts +1 -1
  205. package/src/server/responses/core-combo-native.ts +4 -1
  206. package/src/server/responses/core-combo.ts +21 -18
  207. package/src/server/responses/core-normalize.ts +1 -0
  208. package/src/server/responses/core-options.ts +3 -1
  209. package/src/server/responses/core-replay.ts +5 -0
  210. package/src/server/responses/passthrough-delivery.ts +7 -3
  211. package/src/server/responses/passthrough-dispatch.ts +21 -8
  212. package/src/server/responses/request-prepare.ts +37 -5
  213. package/src/server/responses/request-send-budget.ts +74 -37
  214. package/src/server/responses/request-spend.ts +325 -33
  215. package/src/server/responses/request-transport.ts +132 -52
  216. package/src/server/responses/run-turn-execution.ts +11 -3
  217. package/src/server/responses/sidecar-execution.ts +96 -14
  218. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  219. package/src/server/workflow-refusal.ts +30 -0
  220. package/src/service/desktop-startup.ts +100 -3
  221. package/src/service/ownership-mutation-lease.d.mts +1 -1
  222. package/src/service/ownership-mutation-lease.mjs +5 -7
  223. package/src/sidecar/anthropic-binding.ts +79 -0
  224. package/src/sidecar/auth.ts +76 -2
  225. package/src/types/anthropic-account-pool.ts +33 -0
  226. package/src/types/config.ts +12 -28
  227. package/src/types/provider.ts +5 -0
  228. package/src/types/tools.ts +7 -0
  229. package/src/types.ts +4 -0
  230. package/src/usage/cost.ts +3 -2
  231. package/src/usage/expected-prices.ts +66 -14
  232. package/src/vision/anthropic-describe.ts +16 -3
  233. package/src/vision/index.ts +3 -0
  234. package/src/vision/plan.ts +19 -6
  235. package/src/web-search/alpha-search.ts +7 -1
  236. package/src/web-search/anthropic-executor.ts +16 -4
  237. package/src/web-search/header-deadline.ts +67 -0
  238. package/src/web-search/index.ts +7 -3
  239. package/src/web-search/loop.ts +32 -10
  240. package/src/web-search/passthrough-bridge.ts +4 -1
  241. package/src/web-search/sidecar-providers.ts +4 -3
  242. package/gui/dist/assets/App-BOUa-ek3.js +0 -54
  243. package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
  244. package/gui/dist/assets/index-DwHNDF4s.js +0 -94
  245. package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
@@ -20,12 +20,46 @@ const MIME_TYPES: Record<string, string> = {
20
20
  */
21
21
  const HASHED_ASSET_PATTERN = /-[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9]+$/;
22
22
 
23
- export function findGuiDist(): string | null {
23
+ /** `productName` in desktop/src-tauri/tauri.conf.json; Linux packages install resources under `lib/<productName>`. */
24
+ export const DESKTOP_PRODUCT_NAME = "OpenCodex";
25
+
26
+ /**
27
+ * Where a compiled `ocx` looks for `gui/dist`, given the directory holding the executable.
28
+ *
29
+ * Beside the binary covers a release archive and the Windows desktop install. The desktop shell
30
+ * ships `ocx` as a sidecar and its resources where Tauri's `resource_dir` puts them: `../Resources`
31
+ * inside a macOS app bundle, `../lib/<productName>` for a Linux package (`/usr/bin`, `/usr/local/bin`,
32
+ * an AppImage's `usr/bin`). The shell passes that directory as OPENCODEX_GUI_DIST only to the sidecar
33
+ * it starts itself; `ocx ensure` from the Codex shim and the login service start the same binary
34
+ * without the variable, so they find the dashboard here.
35
+ */
36
+ export function standaloneGuiDistCandidates(executableDir: string, platform: NodeJS.Platform = process.platform): string[] {
37
+ const candidates = [join(executableDir, "gui", "dist")];
38
+ if (platform === "darwin") candidates.push(join(executableDir, "..", "Resources", "gui", "dist"));
39
+ if (platform === "linux") candidates.push(join(executableDir, "..", "lib", DESKTOP_PRODUCT_NAME, "gui", "dist"));
40
+ return candidates;
41
+ }
42
+
43
+ /** Overrides for tests; production callers pass nothing and get this process's own layout. */
44
+ export interface GuiDistLookup {
45
+ /** Directory holding a compiled `ocx`, or null when running from source. */
46
+ standaloneDir?: string | null;
47
+ platform?: NodeJS.Platform;
48
+ /** Directory of this module; a source checkout's `gui/dist` is resolved from it. */
49
+ moduleDir?: string;
50
+ }
51
+
52
+ /** Lookup only: checks for `index.html` and never creates, copies or builds anything. */
53
+ export function findGuiDist(lookup: GuiDistLookup = {}): string | null {
54
+ const standaloneDir = lookup.standaloneDir === undefined
55
+ ? (isStandaloneBinary() ? standaloneRoot() : null)
56
+ : lookup.standaloneDir;
57
+ const moduleDir = lookup.moduleDir ?? import.meta.dir;
24
58
  const candidates = [
25
59
  process.env.OPENCODEX_GUI_DIST,
26
- ...(isStandaloneBinary() ? [join(standaloneRoot(), "gui", "dist")] : []),
27
- join(import.meta.dir, "..", "..", "gui", "dist"),
28
- join(import.meta.dir, "..", "..", "..", "gui", "dist"),
60
+ ...(standaloneDir === null ? [] : standaloneGuiDistCandidates(standaloneDir, lookup.platform)),
61
+ join(moduleDir, "..", "..", "gui", "dist"),
62
+ join(moduleDir, "..", "..", "..", "gui", "dist"),
29
63
  ].filter((candidate): candidate is string => Boolean(candidate));
30
64
  for (const c of candidates) {
31
65
  if (existsSync(join(c, "index.html"))) return c;
@@ -6,6 +6,7 @@ import {
6
6
  import {
7
7
  configureSharedSpendLedger,
8
8
  spendPolicyFromConfig,
9
+ waitForSharedSpendReporterDrain,
9
10
  } from "../../lib/spend-reservation-ledger";
10
11
  import { warnIfSyncedStateDirectory } from "../../lib/synced-state-location";
11
12
 
@@ -26,7 +27,7 @@ export function waitForFailedStartRollback(error: unknown): Promise<void> {
26
27
  }
27
28
 
28
29
  export interface SpendLedgerServerLifecycle {
29
- configure(spend: OcxSpendConfig | undefined): void;
30
+ configure(spend: OcxSpendConfig | undefined, poolAliases?: unknown, providerIds?: readonly string[]): void;
30
31
  track<T extends { stop(closeActiveConnections?: boolean): void | Promise<void> }>(server: T): T;
31
32
  release(): void;
32
33
  releaseAfterFailedStart(): Promise<void>;
@@ -49,13 +50,18 @@ export function acquireSpendLedgerServerLifecycle(configDir: string): SpendLedge
49
50
  owner.release();
50
51
  };
51
52
  return {
52
- configure(spend): void {
53
- configureSharedSpendLedger(spendPolicyFromConfig(spend));
53
+ configure(spend, poolAliases, providerIds): void {
54
+ configureSharedSpendLedger(spendPolicyFromConfig(spend, poolAliases, providerIds));
54
55
  },
55
56
  track<T extends { stop(closeActiveConnections?: boolean): void | Promise<void> }>(server: T): T {
56
57
  // Capture the raw stop before startServer replaces the public method with full teardown.
57
58
  const stop = server.stop.bind(server);
58
- failedStartStops.push(() => stop(true));
59
+ const drainedStop = async (closeActiveConnections?: boolean): Promise<void> => {
60
+ await stop(closeActiveConnections);
61
+ await waitForSharedSpendReporterDrain();
62
+ };
63
+ Object.defineProperty(server, "stop", { configurable: true, value: drainedStop });
64
+ failedStartStops.push(() => drainedStop(true));
59
65
  return server;
60
66
  },
61
67
  release,
@@ -302,7 +302,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
302
302
  configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
303
303
  enforceAppOwnedMemoryBudget();
304
304
  // Observe-only mode still journals physical sends, so every server owns before configuring.
305
- spendLedgerLifecycle.configure(config.spend);
305
+ spendLedgerLifecycle.configure(config.spend, config.spendPoolAliases, Object.keys(config.providers));
306
306
  // After ownership: a second server on the same home is refused above, so the process running
307
307
  // this line is the only one appending to usage.jsonl and the only one that may compact it.
308
308
  setUsageLedgerRetention(config.usageLedgerMaxBytes);
@@ -463,21 +463,19 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
463
463
  return serveGuiFile(rawPath) !== null;
464
464
  }
465
465
 
466
- // Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
467
- // the server_is_overloaded code so clients can back off, but always return a JSON envelope.
468
- // These two run BEFORE the auth/origin checks, so they need the receiving listener's policy
469
- // explicitly (#1102). Reaching for the shared `config` here would attach public-policy CORS
470
- // headers to a 503 on the loopback listener — no model runs and no credential is spent, but
471
- // it is the one error path that would answer a rebinding origin with its own origin echoed
472
- // back.
466
+ // Codex maps 503 + server_is_overloaded to ServerOverloaded ("model at capacity"), retried
467
+ // only with retry advice that older clients ignore (#6642); server_restarting is a
468
+ // retryable UnexpectedStatus in every version and never reads as model capacity.
469
+ // Keep JSON (empty / non-JSON 503 means "Unknown error", #452) and Retry-After.
470
+ // Drain and busy run BEFORE auth/origin checks: use the receiving listener's policy
471
+ // (#1102), because shared public `config` could echo a rebinding origin on loopback.
473
472
  function drainingResponse(req: Request, policy: RequestPolicyView): Response {
474
- const response = formatErrorResponse(503, "server_error", "Service shutting down");
475
- const headers = new Headers(response.headers);
476
- for (const [name, value] of Object.entries(corsHeaders(req, policy))) {
477
- headers.set(name, value);
478
- }
479
- headers.set("Retry-After", "5");
480
- return new Response(response.body, { status: 503, headers });
473
+ return withCors(new Response(JSON.stringify({
474
+ error: { type: "server_error", code: "server_restarting", message: "OpenCodex is restarting; retry this request." },
475
+ }), {
476
+ status: 503,
477
+ headers: { "Content-Type": "application/json", "Retry-After": "5" },
478
+ }), req, policy);
481
479
  }
482
480
 
483
481
  function serverBusyResponse(req: Request, resource: string, policy: RequestPolicyView): Response {
@@ -507,7 +505,9 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
507
505
  const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
508
506
  if (!lease) return serverBusyResponse(req, "active turns", policy);
509
507
  // Root, lane, and the refusal that follows from them, all live in ./workflow-refusal.
510
- const workflow = admitHttpWorkflowTurn(req.headers);
508
+ let workflow: ReturnType<typeof admitHttpWorkflowTurn>;
509
+ try { workflow = admitHttpWorkflowTurn(req.headers); }
510
+ catch (error) { lease.release(); throw error; }
511
511
  if (workflow && !workflow.admitted) {
512
512
  lease.release();
513
513
  // withCors, because without Access-Control-Allow-Origin the exposed refusal header is
@@ -34,7 +34,7 @@ export function createInferenceSendBudget(
34
34
  export function expandInferenceOAuthSendBudget(budget: TransientSendBudget | undefined, accounts: number): void {
35
35
  if (!budget) return;
36
36
  const policy = ingressPolicies.get(budget);
37
- if (!policy || budget.used !== 0) return;
37
+ if (!policy || budget.used !== 0 || ("physicalStarted" in budget && Number(budget.physicalStarted) > 0)) return;
38
38
  ingressPolicies.delete(budget);
39
39
  if (accounts < 2 || !Number.isSafeInteger(accounts)) return;
40
40
  const sends = Math.min(accounts, GENERIC_OAUTH_MAX_ACCOUNTS_PER_REQUEST) * TRANSIENT_RETRY_MAX_ATTEMPTS;
@@ -479,7 +479,7 @@ export async function drainAndShutdown(
479
479
  beginBackgroundShellShutdown();
480
480
  try {
481
481
  while (admittedTurns.size > 0 && Date.now() < deadline) {
482
- await Bun.sleep(100);
482
+ await Bun.sleep(Math.min(100, Math.max(0, deadline - Date.now())));
483
483
  }
484
484
  if (admittedTurns.size > 0) {
485
485
  console.warn(`⚠️ Aborting ${admittedTurns.size} in-flight turn(s) after ${timeoutMs}ms deadline`);
@@ -8,6 +8,8 @@ import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCach
8
8
  import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
9
9
  import { MULTI_AGENT_SURFACE_ADVISORY_VERSION, multiAgentSurfaceAdvisory, resolveMultiAgentMode } from "../../config/multi-agent-surface";
10
10
  import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
11
+ import { anthropicSidecarPatchError } from "../../config/schema/anthropic-account-pool";
12
+ import { isAnthropicInstanceId, type AnthropicInstanceId } from "../../providers/anthropic-instance-id";
11
13
  import {
12
14
  adoptPersistedClaudeCode,
13
15
  DEFAULT_SUBAGENT_MODELS,
@@ -71,9 +73,13 @@ import {
71
73
  webSearchCandidateRows,
72
74
  webSearchModelIsRejected,
73
75
  webSearchModelRejection,
76
+ sidecarAnthropicPoolOptions,
77
+ sidecarSettingsAfterPatch,
78
+ sidecarOptionsAuth,
74
79
  type WebSearchBackend,
75
80
  } from "./web-search-sidecar-options";
76
81
  import { drainAndShutdown } from "../lifecycle";
82
+ import { inheritedAnthropicInstance } from "../../sidecar/auth";
77
83
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
78
84
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
79
85
  import type { PersistedUsageAttempt } from "../../usage/log";
@@ -1368,6 +1374,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1368
1374
  );
1369
1375
  const webSearchOverride = config.claudeCode?.webSearchSidecar;
1370
1376
  const visionOverride = config.claudeCode?.visionSidecar;
1377
+ const helperParent = config.claudeCode?.model?.includes("/") ? config.claudeCode.model.split("/")[0] : undefined;
1378
+ const helperVision = { ...config.visionSidecar, ...visionOverride };
1379
+ // Same pool the runtime picks (vision/plan.ts preferredVisionBackend): explicit, else inherited
1380
+ // from the Claude model's instance, so a usable inherited Pool 2 reports the Anthropic backend.
1381
+ const helperVisionInstance = helperVision.anthropicInstance ?? inheritedAnthropicInstance(config, helperParent);
1382
+ const helperVisionBackend = helperVision.backend ?? (sidecarOptionsAuth(config, helperVisionInstance).isAnthropicAuth ? "anthropic" : "openai");
1371
1383
  const { firstPartyDesired, readFirstPartyProxyStatus } = await import("../../claude/first-party-settings");
1372
1384
  const { observeClaudeDesktopMode } = await import("../../claude/desktop-first-party");
1373
1385
  const { claudeInterceptEnabled, getClaudeInterceptState } = await import("../../claude/intercept/runtime");
@@ -1411,11 +1423,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1411
1423
  autoCompactWindow: config.claudeCode?.autoCompactWindow ?? null,
1412
1424
  blockedSkills: config.claudeCode?.blockedSkills ?? null,
1413
1425
  injectAgents: config.claudeCode?.injectAgents !== false,
1426
+ sidecarPools: {
1427
+ webSearchSidecar: sidecarAnthropicPoolOptions(config, { ...config.webSearchSidecar, ...webSearchOverride }, helperParent),
1428
+ visionSidecar: sidecarAnthropicPoolOptions(config, { ...helperVision, backend: helperVisionBackend }, helperParent),
1429
+ },
1414
1430
  ...(webSearchOverride && Object.keys(webSearchOverride).length > 0
1415
- ? { webSearchSidecar: { backend: webSearchOverride.backend, model: webSearchOverride.model } }
1431
+ ? { webSearchSidecar: { backend: webSearchOverride.backend, model: webSearchOverride.model, ...(webSearchOverride.anthropicInstance ? { anthropicInstance: webSearchOverride.anthropicInstance } : {}) } }
1416
1432
  : {}),
1417
1433
  ...(visionOverride && Object.keys(visionOverride).length > 0
1418
- ? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model } }
1434
+ ? { visionSidecar: { backend: visionOverride.backend, model: visionOverride.model, ...(visionOverride.anthropicInstance ? { anthropicInstance: visionOverride.anthropicInstance } : {}) } }
1419
1435
  : {}),
1420
1436
  fastMode: config.fastMode,
1421
1437
  forceAvailable: [...visibleNativeSlugs(config), ...filterCatalogVisibleModels(models, config).map(catalogModelSlug)],
@@ -1563,6 +1579,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1563
1579
  ...(residual ? ["settings_residual"] : []),
1564
1580
  ] });
1565
1581
  }
1582
+ const instanceError = anthropicSidecarPatchError(config, body, true);
1583
+ if (instanceError) return jsonResponse({ error: instanceError }, 400);
1566
1584
  for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
1567
1585
  const section = body[field];
1568
1586
  if (section === undefined || section === null) continue;
@@ -1587,7 +1605,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1587
1605
  // gates cannot drift.
1588
1606
  if (field === "visionSidecar" && typeof section.model === "string" && section.model !== "") {
1589
1607
  const requested = section.model;
1590
- const candidates = await visionCandidateRows(config);
1608
+ const candidates = await visionCandidateRows({ ...config, visionSidecar: {
1609
+ ...config.visionSidecar, ...sidecarSettingsAfterPatch(config.claudeCode?.visionSidecar, section),
1610
+ } });
1591
1611
  const hint = section.backend === "anthropic" || section.backend === "openai"
1592
1612
  || section.backend === "routed"
1593
1613
  ? section.backend
@@ -1610,7 +1630,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1610
1630
  // module as /api/sidecar-settings — a gate on one route and a stale copy on
1611
1631
  // the other is no gate at all.
1612
1632
  if (field === "webSearchSidecar"
1613
- && (section.model !== undefined || section.backend !== undefined)) {
1633
+ && (section.model !== undefined || section.backend !== undefined || section.anthropicInstance !== undefined)) {
1614
1634
  const stored = config.claudeCode?.webSearchSidecar;
1615
1635
  // Validate against the SUBMITTED backend across the whole union, not
1616
1636
  // just openai/anthropic (#2457). allowedBackends above already refused
@@ -1629,7 +1649,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1629
1649
  : typeof section.model === "string"
1630
1650
  ? section.model
1631
1651
  : stored?.model ?? config.webSearchSidecar?.model;
1632
- const candidates = await webSearchCandidateRows(config);
1652
+ const candidates = await webSearchCandidateRows({ ...config, webSearchSidecar: {
1653
+ ...config.webSearchSidecar, ...sidecarSettingsAfterPatch(config.claudeCode?.webSearchSidecar, section),
1654
+ } });
1633
1655
  if (effectiveModel && webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
1634
1656
  return jsonResponse(webSearchModelRejection(
1635
1657
  "webSearchSidecar.model",
@@ -1650,8 +1672,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1650
1672
  }
1651
1673
  // The per-field validation above guarantees vision only ever carries the two-member
1652
1674
  // union; the cast is the loop's shared-shape compromise, not a wider write path.
1653
- const requested = section as { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa" | null; model?: string };
1675
+ const requested = section as { backend?: "openai" | "anthropic" | "xai" | "gemini" | "exa" | null; model?: string; anthropicInstance?: AnthropicInstanceId | null };
1654
1676
  const override = { ...next[field] } as NonNullable<OcxClaudeCodeConfig[typeof field]>;
1677
+ if (requested.anthropicInstance === null) delete override.anthropicInstance;
1678
+ else if (isAnthropicInstanceId(requested.anthropicInstance)) override.anthropicInstance = requested.anthropicInstance;
1655
1679
  if (requested.backend === null) delete override.backend;
1656
1680
  else if (requested.backend !== undefined) override.backend = requested.backend as never;
1657
1681
  if (requested.model === "") delete override.model;
@@ -1,7 +1,8 @@
1
+ import { isAnthropicOAuthInstance, configuredAnthropicInstance } from "../../providers/anthropic-instance";
1
2
  import type { OcxConfig } from "../../types";
2
- import { effectiveAnthropicAccountThreshold, parseAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
3
+ import { effectiveAnthropicAccountThresholdForInstance, parseAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
3
4
  import { OAUTH_PROVIDERS } from "../../oauth";
4
- import { setAnthropicAccountThreshold } from "../../oauth/store";
5
+ import { setAnthropicAccountThresholdForInstance } from "../../oauth/store";
5
6
  import { jsonResponse } from "../auth-cors";
6
7
  import { readManagementJsonBodyOr } from "./body";
7
8
 
@@ -10,18 +11,20 @@ export async function handleAnthropicAccountThreshold(req: Request, config: OcxC
10
11
  const body = await readManagementJsonBodyOr(req, {});
11
12
  if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "body must be an object" }, 400);
12
13
  const fields = body as Record<string, unknown>;
13
- const definition = OAUTH_PROVIDERS.anthropic;
14
- const effectiveProvider = config.providers.anthropic
14
+ const provider = fields.provider;
15
+ if (typeof provider !== "string" || !isAnthropicOAuthInstance(provider)) return jsonResponse({ error: "account threshold requires Anthropic OAuth" }, 400);
16
+ const definition = OAUTH_PROVIDERS[provider];
17
+ const effectiveProvider = config.providers[provider]
15
18
  ?? definition?.resolveProviderConfig?.(config) ?? definition?.providerConfig;
16
- if (fields.provider !== "anthropic" || effectiveProvider?.authMode !== "oauth") {
19
+ if (effectiveProvider?.authMode !== "oauth" || (provider === "anthropic2" && !configuredAnthropicInstance(config, provider))) {
17
20
  return jsonResponse({ error: "account threshold requires Anthropic OAuth" }, 400);
18
21
  }
19
22
  if (typeof fields.accountId !== "string" || !fields.accountId.trim()) return jsonResponse({ error: "missing accountId" }, 400);
20
23
  const threshold = parseAnthropicAccountThreshold(fields.threshold);
21
24
  if (fields.threshold !== null && threshold === null) return jsonResponse({ error: "threshold must be an integer 0-100 or null" }, 400);
22
- if (!await setAnthropicAccountThreshold(fields.accountId, threshold)) return jsonResponse({ error: "account not found" }, 404);
23
- return jsonResponse({ ok: true, provider: "anthropic", accountId: fields.accountId,
25
+ if (!await setAnthropicAccountThresholdForInstance(provider, fields.accountId, threshold)) return jsonResponse({ error: "account not found" }, 404);
26
+ return jsonResponse({ ok: true, provider, accountId: fields.accountId,
24
27
  autoSwitchThresholdOverride: threshold,
25
- effectiveAutoSwitchThreshold: effectiveAnthropicAccountThreshold(config, { autoSwitchThresholdOverride: threshold ?? undefined }),
26
- autoSwitchThreshold: effectiveAnthropicAccountThreshold(config) });
28
+ effectiveAutoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider, config, { autoSwitchThresholdOverride: threshold ?? undefined }),
29
+ autoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider, config) });
27
30
  }
@@ -0,0 +1,72 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import type { AnthropicAccountPoolConfig } from "../../types/anthropic-account-pool";
3
+ import { configuredAnthropicInstance, type AnthropicInstanceId } from "../../providers/anthropic-instance";
4
+ import { rawAnthropicAccountPool, resolveAnthropicAccountPoolConfig } from "../../oauth/anthropic-pool-config";
5
+ import { readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
6
+ import { ConfigWritePublishedError } from "../../config/persist-unlocked";
7
+ import { mutatePersistedConfig } from "../../config";
8
+ import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
9
+ import { jsonResponse } from "../auth-cors";
10
+
11
+ /** Required instance; never synthesizes a deleted B provider row. */
12
+ export function writeAnthropicPoolSettings(config: OcxConfig, instance: AnthropicInstanceId, value: AnthropicAccountPoolConfig): void {
13
+ if (instance === "anthropic") config.anthropicAccountPool = value;
14
+ else {
15
+ const row = config.providers[instance];
16
+ if (!configuredAnthropicInstance(config, instance)) throw new Error("Anthropic pool instance is no longer configured");
17
+ row!.anthropicAccountPool = value;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Both Anthropic writers share publication-aware recovery from the atomic mutation owner.
23
+ * Recovery covers only the durable write. Once the file holds the new value, a stale live row
24
+ * or a failed reconcile is reported as saved with the fixed bookkeeping warning, never a 500.
25
+ */
26
+ export function persistAnthropicPoolPatch(
27
+ config: OcxConfig,
28
+ instance: AnthropicInstanceId,
29
+ patch: (target: OcxConfig) => { changed: boolean; value: NonNullable<OcxConfig["anthropicAccountPool"]> },
30
+ ): { status: "saved"; warning?: "config_bookkeeping_failed" } | { status: "failed"; response: Response } {
31
+ const before = readConfigFileSnapshot();
32
+ const unknown = () => ({ status: "failed" as const, response: jsonResponse({
33
+ error: "Pool settings save state is unknown; reload settings before editing again",
34
+ code: "config_save_state_unknown",
35
+ }, 409) });
36
+ const publish = (value: AnthropicAccountPoolConfig, warned: boolean): { status: "saved"; warning?: "config_bookkeeping_failed" } => {
37
+ let warning = warned;
38
+ try { writeAnthropicPoolSettings(config, instance, value); } catch { warning = true; }
39
+ try { reconcileLiveStateStores(); } catch { warning = true; }
40
+ return warning ? { status: "saved", warning: "config_bookkeeping_failed" } : { status: "saved" };
41
+ };
42
+ let durable: AnthropicAccountPoolConfig | undefined;
43
+ try {
44
+ const saved = mutatePersistedConfig(target => {
45
+ if (instance === "anthropic2" && !configuredAnthropicInstance(target, instance)) {
46
+ throw new Error("Anthropic pool instance is no longer configured");
47
+ }
48
+ return patch(target);
49
+ });
50
+ if (saved.status === "unavailable") return unknown();
51
+ durable = saved.value;
52
+ } catch (error) {
53
+ const persisted = readConfigFileSnapshot();
54
+ if (persisted.diagnostics.source !== "file" || persisted.raw === undefined) return unknown();
55
+ // File reads may salvage hand edits; recovery success requires a strict, authoritative document.
56
+ let validated: ReturnType<typeof validateConfigCandidate>;
57
+ try { validated = validateConfigCandidate(JSON.parse(persisted.raw.replace(/^\uFEFF/, ""))); }
58
+ catch { return unknown(); }
59
+ if (!validated.ok) return unknown();
60
+ const current = validated.config;
61
+ if (instance === "anthropic2" && !configuredAnthropicInstance(current, instance)) return unknown();
62
+ let expected: AnthropicAccountPoolConfig;
63
+ try { expected = patch(structuredClone(current)).value; } catch { return unknown(); }
64
+ const matches = JSON.stringify(rawAnthropicAccountPool(current, instance)) === JSON.stringify(expected);
65
+ // A failed live write or reconcile must not turn confirmed publication back into a rollback.
66
+ if (matches) return publish(resolveAnthropicAccountPoolConfig(current, instance), true);
67
+ if (error instanceof ConfigWritePublishedError || persisted.raw !== before.raw) return unknown();
68
+ return { status: "failed", response: jsonResponse({ error: "Pool settings could not be saved" }, 500) };
69
+ }
70
+ if (durable === undefined) return unknown();
71
+ return publish(durable, false);
72
+ }