@bitkyc08/opencodex 2.80.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 (227) 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/devin/live-models.ts +2 -0
  20. package/src/adapters/kiro-retry.ts +9 -4
  21. package/src/adapters/openai-chat/messages.ts +7 -6
  22. package/src/adapters/physical-send.ts +8 -5
  23. package/src/adapters/qoder/adapter.ts +41 -6
  24. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  25. package/src/adapters/zed.ts +16 -1
  26. package/src/claude/inbound-cache-stabilize.ts +4 -9
  27. package/src/claude/inbound.ts +1 -0
  28. package/src/claude/intercept/model-bindings.ts +1 -0
  29. package/src/claude/intercept/picker-listener.ts +71 -4
  30. package/src/cli/account-anthropic-threshold.ts +14 -5
  31. package/src/cli/account-api.ts +3 -1
  32. package/src/cli/account-extended.ts +13 -9
  33. package/src/cli/account-policy-dto.ts +1 -1
  34. package/src/cli/account-policy.ts +13 -4
  35. package/src/cli/account.ts +3 -3
  36. package/src/cli/capabilities-accounts.ts +2 -2
  37. package/src/cli/capabilities-agents-routing.ts +24 -0
  38. package/src/cli/capabilities-integrations.ts +16 -0
  39. package/src/cli/codex-shim-autorestore.ts +3 -0
  40. package/src/cli/dispatch.ts +10 -1
  41. package/src/cli/help.ts +4 -1
  42. package/src/cli/index.ts +11 -5
  43. package/src/cli/integration-plan-dto.ts +1 -0
  44. package/src/cli/integration-preview.ts +8 -1
  45. package/src/cli/integrations.ts +32 -3
  46. package/src/cli/message-args.ts +34 -0
  47. package/src/cli/message-command.ts +43 -0
  48. package/src/cli/message-runtime.ts +6 -0
  49. package/src/cli/observe.ts +3 -1
  50. package/src/cli/provider.ts +29 -6
  51. package/src/cli/registry.ts +24 -2
  52. package/src/clients/config-export/commandcode.ts +131 -0
  53. package/src/clients/config-export/contracts.ts +12 -0
  54. package/src/clients/config-export.ts +64 -1
  55. package/src/codex/auth-api/account-list.ts +11 -3
  56. package/src/codex/auth-api/main-account-probe.ts +25 -8
  57. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  58. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  59. package/src/codex/auth-api/routes.ts +1 -0
  60. package/src/codex/catalog/auto-review.ts +13 -6
  61. package/src/codex/catalog/build-entries.ts +15 -7
  62. package/src/codex/catalog/control-plane.ts +45 -0
  63. package/src/codex/catalog/effort.ts +11 -1
  64. package/src/codex/catalog/gather-capture.ts +5 -3
  65. package/src/codex/catalog/parsing.ts +2 -1
  66. package/src/codex/catalog/provider-models.ts +15 -9
  67. package/src/codex/control-plane-models.ts +6 -0
  68. package/src/combos/jev-decision-contract.ts +25 -2
  69. package/src/combos/jev.ts +3 -2
  70. package/src/combos/types.ts +1 -1
  71. package/src/config/diagnostics.ts +27 -16
  72. package/src/config/load-degrade.ts +34 -0
  73. package/src/config/schema/anthropic-account-pool.ts +143 -0
  74. package/src/config/schema/config-schema.ts +7 -1
  75. package/src/config/schema/leaf-validators.ts +4 -0
  76. package/src/config.ts +2 -0
  77. package/src/generated/compatibility-version.json +320 -184
  78. package/src/generated/model-metadata.ts +5 -4
  79. package/src/images/loop.ts +2 -1
  80. package/src/integrations/catalog-refresh.ts +1 -1
  81. package/src/integrations/mutation-plan.ts +7 -0
  82. package/src/integrations/registry.ts +8 -0
  83. package/src/integrations/state.ts +9 -2
  84. package/src/integrations/writer.ts +8 -2
  85. package/src/lib/lab-live-route-production.ts +9 -1
  86. package/src/lib/request-execution-budget.ts +236 -58
  87. package/src/lib/spend-pool-alias-validation.ts +79 -0
  88. package/src/lib/spend-pool-continuity.ts +34 -0
  89. package/src/lib/spend-reservation-ledger.ts +412 -50
  90. package/src/lib/state-store-registrations.ts +20 -3
  91. package/src/lib/upstream-retry.ts +17 -5
  92. package/src/lib/workflow-budget.ts +53 -11
  93. package/src/messaging/budget.ts +45 -0
  94. package/src/messaging/discovery.ts +57 -0
  95. package/src/messaging/envelope.ts +49 -0
  96. package/src/messaging/input.ts +37 -0
  97. package/src/messaging/rpc.ts +196 -0
  98. package/src/messaging/send.ts +58 -0
  99. package/src/messaging/socket.ts +72 -0
  100. package/src/messaging/types.ts +29 -0
  101. package/src/oauth/anthropic-account-refusal.ts +51 -17
  102. package/src/oauth/anthropic-account-threshold.ts +8 -2
  103. package/src/oauth/anthropic-continuity.ts +15 -1
  104. package/src/oauth/anthropic-model-quota.ts +120 -48
  105. package/src/oauth/anthropic-model-routes.ts +11 -3
  106. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  107. package/src/oauth/anthropic-pool-config.ts +30 -0
  108. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  109. package/src/oauth/anthropic-routing.ts +1202 -866
  110. package/src/oauth/anthropic-send-ownership.ts +41 -0
  111. package/src/oauth/anthropic.ts +4 -1
  112. package/src/oauth/generic-account-failover.ts +3 -1
  113. package/src/oauth/health.ts +8 -6
  114. package/src/oauth/index.ts +90 -173
  115. package/src/oauth/kiro.ts +22 -9
  116. package/src/oauth/login-cli.ts +2 -0
  117. package/src/oauth/model-discovery-auth.ts +43 -0
  118. package/src/oauth/pool-kernel.ts +11 -5
  119. package/src/oauth/pool-settings-capability.ts +5 -3
  120. package/src/oauth/store-anthropic-instance.ts +87 -0
  121. package/src/oauth/store.ts +67 -9
  122. package/src/oauth/token-guardian.ts +2 -0
  123. package/src/oauth/types.ts +2 -0
  124. package/src/protocols/plan-snapshot.ts +5 -0
  125. package/src/protocols/settings.ts +20 -5
  126. package/src/providers/anthropic-instance-id.ts +47 -0
  127. package/src/providers/anthropic-instance.ts +48 -0
  128. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  129. package/src/providers/derive.ts +2 -0
  130. package/src/providers/kiro-models.ts +3 -0
  131. package/src/providers/label.ts +1 -1
  132. package/src/providers/model-rename-fields.ts +2 -0
  133. package/src/providers/quota/account-cache.ts +37 -11
  134. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  135. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  136. package/src/providers/quota/report-cache.ts +24 -1
  137. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  138. package/src/providers/quota.ts +22 -32
  139. package/src/providers/registry/entries-core.ts +35 -30
  140. package/src/providers/registry/model-ids.ts +1 -0
  141. package/src/providers/registry/model-seeds.ts +7 -4
  142. package/src/providers/registry/types.ts +3 -0
  143. package/src/providers/registry.ts +3 -3
  144. package/src/responses/spill-store.ts +5 -1
  145. package/src/responses/state/metrics.ts +87 -0
  146. package/src/responses/state/snapshot-codec.ts +6 -0
  147. package/src/responses/state/spill-queue.ts +1 -0
  148. package/src/responses/state/unforced-store-false.ts +55 -0
  149. package/src/responses/state.ts +50 -66
  150. package/src/router.ts +28 -4
  151. package/src/routing/compatibility/assemble.ts +1 -1
  152. package/src/routing/quota.ts +7 -3
  153. package/src/server/auth-cors.ts +5 -0
  154. package/src/server/chat-completions.ts +6 -1
  155. package/src/server/chat-native.ts +45 -25
  156. package/src/server/claude-messages.ts +16 -2
  157. package/src/server/gui-static.ts +38 -4
  158. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  159. package/src/server/index.ts +4 -2
  160. package/src/server/inference/context.ts +1 -1
  161. package/src/server/lifecycle.ts +1 -1
  162. package/src/server/management/agent-settings-routes.ts +30 -6
  163. package/src/server/management/anthropic-account-threshold.ts +12 -9
  164. package/src/server/management/anthropic-pool-settings.ts +72 -0
  165. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  166. package/src/server/management/config-routes.ts +27 -7
  167. package/src/server/management/decision-routes.ts +2 -1
  168. package/src/server/management/integration-routes.ts +25 -2
  169. package/src/server/management/oauth-account-routes.ts +79 -98
  170. package/src/server/management/provider-instance-ownership.ts +44 -0
  171. package/src/server/management/provider-routes.ts +21 -28
  172. package/src/server/management/route-registry.ts +3 -1
  173. package/src/server/management/routing-profile-routes.ts +4 -4
  174. package/src/server/management/system-restart.ts +44 -14
  175. package/src/server/management/system-routes.ts +24 -2
  176. package/src/server/management/vision-sidecar-options.ts +9 -6
  177. package/src/server/management/web-search-sidecar-options.ts +51 -2
  178. package/src/server/management-auth.ts +6 -0
  179. package/src/server/messages-native-eligibility.ts +3 -2
  180. package/src/server/messages-native-oauth.ts +86 -41
  181. package/src/server/messages-native-selector.ts +39 -0
  182. package/src/server/messages-native.ts +84 -25
  183. package/src/server/request-log.ts +5 -0
  184. package/src/server/responses/adapter-continuation.ts +29 -17
  185. package/src/server/responses/adapter-dispatch.ts +47 -17
  186. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  187. package/src/server/responses/compact.ts +25 -8
  188. package/src/server/responses/compaction-recovery.ts +1 -1
  189. package/src/server/responses/core-combo-native.ts +4 -1
  190. package/src/server/responses/core-combo.ts +16 -11
  191. package/src/server/responses/core-normalize.ts +1 -0
  192. package/src/server/responses/core-options.ts +3 -1
  193. package/src/server/responses/core-replay.ts +5 -0
  194. package/src/server/responses/passthrough-delivery.ts +7 -3
  195. package/src/server/responses/passthrough-dispatch.ts +21 -8
  196. package/src/server/responses/request-prepare.ts +37 -5
  197. package/src/server/responses/request-send-budget.ts +74 -37
  198. package/src/server/responses/request-spend.ts +325 -33
  199. package/src/server/responses/request-transport.ts +132 -52
  200. package/src/server/responses/run-turn-execution.ts +11 -3
  201. package/src/server/responses/sidecar-execution.ts +96 -14
  202. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  203. package/src/server/workflow-refusal.ts +30 -0
  204. package/src/service/ownership-mutation-lease.d.mts +1 -1
  205. package/src/service/ownership-mutation-lease.mjs +5 -7
  206. package/src/sidecar/anthropic-binding.ts +79 -0
  207. package/src/sidecar/auth.ts +76 -2
  208. package/src/types/anthropic-account-pool.ts +33 -0
  209. package/src/types/config.ts +12 -28
  210. package/src/types/provider.ts +5 -0
  211. package/src/types/tools.ts +7 -0
  212. package/src/types.ts +4 -0
  213. package/src/usage/cost.ts +3 -2
  214. package/src/usage/expected-prices.ts +66 -14
  215. package/src/vision/anthropic-describe.ts +16 -3
  216. package/src/vision/index.ts +3 -0
  217. package/src/vision/plan.ts +19 -6
  218. package/src/web-search/alpha-search.ts +7 -1
  219. package/src/web-search/anthropic-executor.ts +16 -4
  220. package/src/web-search/index.ts +7 -3
  221. package/src/web-search/loop.ts +27 -5
  222. package/src/web-search/passthrough-bridge.ts +4 -1
  223. package/src/web-search/sidecar-providers.ts +4 -3
  224. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  225. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  226. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  227. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -10,11 +10,34 @@ export const JEV_DECISION_TIMEOUT_MAX_MS = 120_000;
10
10
  /** Decision deadline when a combo sets no `decisionTimeoutMs`. */
11
11
  export const JEV_DECISION_TIMEOUT_DEFAULT_MS = 4_000;
12
12
 
13
- /** Whether a self-hosted decision endpoint follows the documented Jev `/systemone` path. */
13
+ /** HTTPS decision services may use any path; local cleartext keeps the `/systemone` contract. */
14
14
  export function isSystemOneEndpoint(baseUrl: string): boolean {
15
15
  try {
16
- return new URL(baseUrl.trim()).pathname.replace(/\/+$/, "").endsWith("/systemone");
16
+ const raw = baseUrl.trim();
17
+ // URL drops empty delimiters ("?", "#", "@") and strips tab/CR/LF, so check the raw text first.
18
+ if (/[\u0000-\u001f\u007f]/.test(raw)) return false;
19
+ const authority = raw.replace(/^[a-z][a-z\d+.-]*:[/\\]*/i, "").split(/[/\\]/, 1)[0] ?? "";
20
+ if (/[?#]/.test(raw) || authority.includes("@")) return false;
21
+ const url = new URL(raw);
22
+ if (url.username || url.password || url.search || url.hash) return false;
23
+ // URL canonicalizes address literals; keep this import-free for the dashboard bundle.
24
+ const host = url.hostname;
25
+ const local = host === "localhost" || host === "[::1]"
26
+ || /^(?:(?:127|10)\.\d+|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d+\.\d+$/.test(host)
27
+ || /^\[f[cd][\da-f]{2}:/.test(host) || /^\[::ffff:7f[\da-f]{2}:/.test(host);
28
+ return url.protocol === "https:"
29
+ || url.protocol === "http:" && local && url.pathname.replace(/\/+$/, "").endsWith("/systemone");
17
30
  } catch {
18
31
  return false;
19
32
  }
20
33
  }
34
+
35
+ /**
36
+ * The URL a decision row is sent to. A `/systemone` path keeps its historical trailing-slash
37
+ * normalization; any other HTTPS path is the operator's exact endpoint.
38
+ */
39
+ export function jevDecisionEndpointUrl(baseUrl: string): string {
40
+ const raw = baseUrl.trim();
41
+ const stripped = raw.replace(/\/+$/, "");
42
+ return stripped.endsWith("/systemone") ? stripped : raw;
43
+ }
package/src/combos/jev.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  } from "../providers/api-key-resolve";
11
11
  import { providerMatchesRegistryTransport } from "../providers/registry";
12
12
  import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
13
+ import { jevDecisionEndpointUrl } from "./jev-decision-contract";
13
14
  import {
14
15
  isSystemOneEndpoint,
15
16
  JEV_DECISION_TIMEOUT_DEFAULT_MS,
@@ -676,7 +677,7 @@ function selfHostedApiKey(name: string, apiKey: string | undefined): string | un
676
677
  * only while the row still matches the registry transport, and the environment fallbacks exist
677
678
  * only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
678
679
  * custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
679
- * `/systemone` endpoint and which names its own model; only its own key may accompany it, so no
680
+ * full HTTPS decision endpoint (or a local HTTP `/systemone` endpoint) and names its own model; only its own key may accompany it, so no
680
681
  * TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
681
682
  * service (reported through the existing `missing_key` gate); `null` means the request's
682
683
  * destination scope refused it before any credential access.
@@ -709,7 +710,7 @@ function jevDecisionEndpoint(
709
710
  };
710
711
  }
711
712
  if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
712
- const url = configured.baseUrl.trim().replace(/\/+$/, "");
713
+ const url = jevDecisionEndpointUrl(configured.baseUrl);
713
714
  if (!url || !isSystemOneEndpoint(url)) return undefined;
714
715
  // `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
715
716
  const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
@@ -302,7 +302,7 @@ export function comboConfigIssues(
302
302
  } else if (!isSystemOneEndpoint(String(providers[decisionProvider]?.baseUrl ?? ""))) {
303
303
  issues.push({
304
304
  path: ["decisionProvider"],
305
- message: `decisionProvider "${decisionProvider}" baseUrl must be the full decision endpoint ending in /systemone`,
305
+ message: `decisionProvider "${decisionProvider}" baseUrl must be a full HTTPS decision endpoint or an HTTP /systemone endpoint`,
306
306
  });
307
307
  } else if (options.requireUsableDecisionService && providers[decisionProvider]?.disabled === true) {
308
308
  issues.push({
@@ -1,3 +1,4 @@
1
+ import { readExistingSpendSalt, spendPoolAliasesError, validatePoolAliasOwners } from "../lib/spend-pool-alias-validation";
1
2
  import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
2
3
  import { protocolConfigSchema } from "./schema/config-schema";
3
4
  import { createHash } from "node:crypto";
@@ -7,13 +8,13 @@ import * as z from "zod/v4";
7
8
  import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
8
9
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
9
10
  import type { OcxConfig } from "../types";
10
- import { parseAnthropicModelRoutes } from "../oauth/anthropic-model-routes";
11
+ import { anthropicAccountPoolConfigError, anthropicOAuthInstanceConfigError, anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
11
12
  import { configReasoningPinsConfigError } from "./provider-validation";
12
13
  import { loopbackCompanionAllowed } from "../codex/loopback-target";
13
14
  import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "../codex/upstream-host-health";
14
15
  import { MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../lib/app-owned-memory";
15
16
  import { isMissingPathError } from "./atomic-write";
16
- import { getConfigPath } from "./paths";
17
+ import { getConfigDir, getConfigPath } from "./paths";
17
18
  import { getDefaultConfig } from "./proxy-env";
18
19
  import { salvageConfigCandidate } from "./salvage";
19
20
  import {
@@ -81,6 +82,23 @@ export type ConfigDiagnostics = {
81
82
  warnings?: string[];
82
83
  };
83
84
 
85
+ function rawPoolAliasesError(value: unknown): string | undefined {
86
+ const record = rawConfigRecord(value);
87
+ if (!record) return undefined;
88
+ const aliases = record.spendPoolAliases;
89
+ const shapeError = spendPoolAliasesError(aliases);
90
+ if (shapeError) return shapeError;
91
+ const mapping = rawConfigRecord(aliases);
92
+ if (!mapping || Object.keys(mapping).length === 0) return undefined;
93
+ const providers = rawConfigRecord(record.providers ?? {});
94
+ if (!providers) return "providers must be an object to validate spendPoolAliases";
95
+ const targetError = spendPoolAliasesError(aliases, Object.keys(providers));
96
+ if (targetError) return targetError;
97
+ const salt = readExistingSpendSalt(getConfigDir());
98
+ if (!salt) return "spendPoolAliases requires a safely readable existing spend salt";
99
+ return validatePoolAliasOwners(aliases, salt, Object.keys(providers));
100
+ }
101
+
84
102
  export type ConfigFileSnapshot = {
85
103
  diagnostics: ConfigDiagnostics;
86
104
  /** Exact file contents, including a possible BOM, used as the optimistic revision. */
@@ -145,6 +163,9 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
145
163
  if (codexPoolWarning) warnings.push(codexPoolWarning);
146
164
  const spendWarning = malformedSpendWarning(rawParsed);
147
165
  if (spendWarning) warnings.push(spendWarning);
166
+ if (rawPoolAliasesError(rawParsed)) {
167
+ warnings.push("spendPoolAliases invalid: configured pool admission is refused until the mapping is corrected");
168
+ }
148
169
  const plaintextWarning = malformedPlaintextV2AgentMessagesWarning(rawParsed);
149
170
  if (plaintextWarning) warnings.push(plaintextWarning);
150
171
  if (syncDisabledReason) {
@@ -153,7 +174,7 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
153
174
  return {
154
175
  config: normalized,
155
176
  source: "file",
156
- error: null,
177
+ error: anthropicOAuthInstanceConfigError(rawParsed) ?? anthropicAccountPoolConfigError(rawParsed) ?? anthropicSidecarConfigError(rawParsed) ?? null,
157
178
  ...(warnings.length > 0 ? { warnings } : {}),
158
179
  };
159
180
  }
@@ -655,19 +676,8 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
655
676
  if (protocols !== undefined && !protocolConfigSchema.safeParse(protocols).success) {
656
677
  return { ok: false, error: "schema_invalid: protocols: expected valid protocol policy and boolean rollout flags" };
657
678
  }
658
- const rawAnthropicPool = rawConfigRecord(value)?.anthropicAccountPool;
659
- const anthropicPool = rawConfigRecord(rawAnthropicPool);
660
- if (rawAnthropicPool !== undefined && !anthropicPool) {
661
- return { ok: false, error: "schema_invalid: anthropicAccountPool: must be an object" };
662
- }
663
- if (anthropicPool && Object.hasOwn(anthropicPool, "nativeMessages") && typeof anthropicPool.nativeMessages !== "boolean") {
664
- return { ok: false, error: "schema_invalid: anthropicAccountPool.nativeMessages: must be a boolean" };
665
- }
666
- const routeValue = anthropicPool?.routes;
667
- if (routeValue !== undefined) {
668
- const parsed = parseAnthropicModelRoutes(routeValue);
669
- if (!parsed.ok) return { ok: false, error: `schema_invalid: anthropicAccountPool.routes: ${parsed.error}` };
670
- }
679
+ const anthropicError = anthropicOAuthInstanceConfigError(value) ?? anthropicAccountPoolConfigError(value) ?? anthropicSidecarConfigError(value);
680
+ if (anthropicError) return { ok: false, error: anthropicError };
671
681
  const boundaryError = blockedModelRedirectsError(value)
672
682
  ?? compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
673
683
  ?? blankHostnameError(value)
@@ -679,6 +689,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
679
689
  ?? agentTaskRecoveryError(value)
680
690
  ?? quotaResetNotifyError(value)
681
691
  ?? catalogAutoRefreshError(value)
692
+ ?? (rawPoolAliasesError(value) ? "schema_invalid: spendPoolAliases: invalid pool identity mapping" : null)
682
693
  ?? spendError(value)
683
694
  ?? codexPoolError(value)
684
695
  ?? googleAntigravityStaticCatalogVersionError(value)
@@ -3,6 +3,7 @@ import { chmodSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
5
5
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
6
+ import { anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
6
7
  import {
7
8
  modelPinnedEffortsConfigError,
8
9
  pinnedReasoningEffortConfigError,
@@ -160,6 +161,39 @@ export function warnDegradedMemoryModels(rawParsed: unknown, validated: OcxConfi
160
161
  }
161
162
  }
162
163
 
164
+ /**
165
+ * Load-time degradation for helper `anthropicInstance` (loadConfig only). A malformed id, or one
166
+ * that conflicts with its block's backend or model, drops just that key with a warning, so the
167
+ * helper inherits the request's pool and every provider/key survives. Validated writes still
168
+ * reject the value and file diagnostics still report it from the raw document.
169
+ *
170
+ * Each location is checked in isolation, global blocks first, so a Claude override is judged
171
+ * against the already-degraded global block it inherits from. An override that owns no
172
+ * `anthropicInstance` of its own is left untouched: the conflicting key lives in the global
173
+ * block, which is valid on its own, and the inherited value is still a well-formed instance.
174
+ */
175
+ export function sanitizeAnthropicSidecarInstanceForLoad(parsed: unknown): void {
176
+ const asRecord = (value: unknown): Record<string, unknown> | undefined =>
177
+ value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
178
+ const root = asRecord(parsed);
179
+ if (!root) return;
180
+ for (const scope of ["global", "claude"] as const) {
181
+ const owner = scope === "global" ? root : asRecord(root.claudeCode);
182
+ for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
183
+ const sidecar = asRecord(owner?.[field]);
184
+ if (!sidecar || !Object.hasOwn(sidecar, "anthropicInstance")) continue;
185
+ const probe = scope === "global"
186
+ ? { [field]: sidecar }
187
+ : { [field]: root[field], claudeCode: { [field]: sidecar } };
188
+ const error = anthropicSidecarConfigError(probe);
189
+ if (!error) continue;
190
+ delete sidecar.anthropicInstance;
191
+ // The message names a static path and reason only; the hand-edited value is never echoed.
192
+ console.warn(`⚠️ config.json ${error} — ignoring that anthropicInstance; the helper inherits the request's Anthropic pool`);
193
+ }
194
+ }
195
+ }
196
+
163
197
  /**
164
198
  * Load-time degradation for `retryOn429` (loadConfig only): one hand-edited invalid optional
165
199
  * field (e.g. `attempts: 0` or a string) must not trip the whole provider schema and hide every
@@ -0,0 +1,143 @@
1
+ import * as z from "zod/v4";
2
+ import { parseAnthropicModelRoutes } from "../../oauth/anthropic-model-routes";
3
+ import { isAnthropicInstanceId } from "../../providers/anthropic-instance-id";
4
+ import { redactSecretString } from "../../lib/redact";
5
+
6
+ /** Historical A load contract, shared verbatim with B: recover only the native preference. */
7
+ export const anthropicAccountPoolSchema = z.object({
8
+ nativeMessages: z.boolean().optional().catch(false),
9
+ }).passthrough().optional().catch(undefined);
10
+
11
+ function record(value: unknown): Record<string, unknown> | undefined {
12
+ return value && typeof value === "object" && !Array.isArray(value)
13
+ ? value as Record<string, unknown> : undefined;
14
+ }
15
+
16
+ /** Write/diagnostic contract preserves A's historical nativeMessages and route validation. */
17
+ function poolError(value: unknown, path: string): string | undefined {
18
+ if (value === undefined) return undefined;
19
+ const pool = record(value);
20
+ if (!pool) return `schema_invalid: ${path}: must be an object`;
21
+ if (Object.hasOwn(pool, "nativeMessages") && typeof pool.nativeMessages !== "boolean") {
22
+ return `schema_invalid: ${path}.nativeMessages: must be a boolean`;
23
+ }
24
+ if (pool.routes !== undefined) {
25
+ const parsed = parseAnthropicModelRoutes(pool.routes);
26
+ if (!parsed.ok) return `schema_invalid: ${path}.routes: ${parsed.error}`;
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ /** Inspect raw locations before the tolerant load schema can discard a malformed field. */
32
+ export function anthropicAccountPoolConfigError(value: unknown): string | undefined {
33
+ const config = record(value);
34
+ const primaryError = poolError(config?.anthropicAccountPool, "anthropicAccountPool");
35
+ if (primaryError) return primaryError;
36
+ for (const [name, raw] of Object.entries(record(config?.providers) ?? {})) {
37
+ const provider = record(raw);
38
+ if (!provider || !Object.hasOwn(provider, "anthropicAccountPool")) continue;
39
+ const path = `providers.${redactSecretString(name)}.anthropicAccountPool`;
40
+ if (name !== "anthropic2") {
41
+ return `schema_invalid: ${path}: misplaced field; provider-local anthropicAccountPool is valid only on anthropic2 (anthropic uses the top-level field)`;
42
+ }
43
+ const error = poolError(provider.anthropicAccountPool, path);
44
+ if (error) return error;
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ /** Raw provenance validation runs before tolerant parsing and never adopts or edits a row. */
50
+ export function anthropicOAuthInstanceConfigError(value: unknown): string | undefined {
51
+ const seen = new WeakSet<object>();
52
+ const inspect = (raw: unknown, location: string[]): string | undefined => {
53
+ if (!raw || typeof raw !== "object" || seen.has(raw)) return undefined;
54
+ seen.add(raw);
55
+ for (const [field, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(raw))) {
56
+ const path = [...location, field];
57
+ if (field === "anthropicOAuthInstance") {
58
+ const label = redactSecretString(path.join("."));
59
+ if (!("value" in descriptor)) return `schema_invalid: ${label}: must be an own data property`;
60
+ if (location.length !== 2 || location[0] !== "providers" || location[1] !== "anthropic2") {
61
+ return `schema_invalid: ${label}: misplaced field; valid only on providers.anthropic2`;
62
+ }
63
+ if (descriptor.value !== "anthropic2") return `schema_invalid: ${label}: must be anthropic2`;
64
+ const row = Object.getOwnPropertyDescriptors(raw);
65
+ if (row.adapter?.value !== "anthropic" || row.authMode?.value !== "oauth") {
66
+ return `schema_invalid: ${label}: requires Anthropic adapter and OAuth authMode`;
67
+ }
68
+ }
69
+ if (!("value" in descriptor)) continue;
70
+ const error = inspect(descriptor.value, path);
71
+ if (error) return error;
72
+ }
73
+ seen.delete(raw);
74
+ return undefined;
75
+ };
76
+ return inspect(value, []);
77
+ }
78
+
79
+ /** Validate explicit helper identity without materializing an absent instance preference. */
80
+ export function anthropicSidecarConfigError(value: unknown): string | undefined {
81
+ const config = record(value);
82
+ const claude = record(config?.claudeCode);
83
+ for (const [prefix, owner] of [["", config], ["claudeCode.", claude]] as const) {
84
+ for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
85
+ const rawSidecar = record(owner?.[field]);
86
+ if (!rawSidecar) continue;
87
+ // Claude overrides inherit individual global fields, just like buildClaudeReplayConfig.
88
+ const sidecar = prefix ? { ...record(config?.[field]), ...rawSidecar } : rawSidecar;
89
+ if (!Object.hasOwn(sidecar, "anthropicInstance")) continue;
90
+ const path = `${prefix}${field}.anthropicInstance`;
91
+ if (!isAnthropicInstanceId(sidecar.anthropicInstance)) {
92
+ return `schema_invalid: ${path}: must be anthropic or anthropic2`;
93
+ }
94
+ // Web search defaults to OpenAI; vision's absent backend keeps its credential-based auto mode.
95
+ const backend = sidecar.backend === undefined
96
+ ? (field === "webSearchSidecar" ? "openai" : undefined) : sidecar.backend;
97
+ // An instance inherited from the global block is inert once the override's effective backend
98
+ // leaves Anthropic (the helper resolver only reads it for the Anthropic family), so it is not
99
+ // a conflict. An instance the override sets itself is still validated in full.
100
+ if (prefix && !Object.hasOwn(rawSidecar, "anthropicInstance") && backend !== undefined && backend !== "anthropic") continue;
101
+ const modelProvider = typeof sidecar.model === "string" && sidecar.model.includes("/")
102
+ ? sidecar.model.slice(0, sidecar.model.indexOf("/")) : undefined;
103
+ if (modelProvider && isAnthropicInstanceId(modelProvider) && modelProvider !== sidecar.anthropicInstance) {
104
+ return `schema_invalid: ${path}: conflicts with the model's Anthropic instance`;
105
+ }
106
+ if (backend !== undefined && backend !== "anthropic") {
107
+ return `schema_invalid: ${path}: requires an anthropic backend`;
108
+ }
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ /** Validate the effective settings before either management route mutates live state. */
115
+ export function anthropicSidecarPatchError(
116
+ config: unknown,
117
+ patches: Record<string, unknown>,
118
+ claude = false,
119
+ ): string | undefined {
120
+ const source = record(config) ?? {};
121
+ const next = { ...source };
122
+ const owner = claude ? { ...record(source.claudeCode) } : next;
123
+ for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
124
+ if (!Object.hasOwn(patches, field)) continue;
125
+ const patch = patches[field];
126
+ if (patch === null) { delete owner[field]; continue; }
127
+ const section = record(patch);
128
+ if (!section) continue; // Shape validation belongs to the route.
129
+ if (section.anthropicInstance !== undefined && section.anthropicInstance !== null
130
+ && !isAnthropicInstanceId(section.anthropicInstance)) {
131
+ return `${field}.anthropicInstance must be anthropic, anthropic2, or null`;
132
+ }
133
+ const settings = { ...record(owner[field]) };
134
+ for (const key of ["backend", "model", "anthropicInstance"] as const) {
135
+ if (section[key] === null || (key === "model" && section[key] === "")) delete settings[key];
136
+ else if (section[key] !== undefined) settings[key] = section[key];
137
+ }
138
+ if (claude && (Object.keys(section).length === 0 || Object.keys(settings).length === 0)) delete owner[field];
139
+ else owner[field] = settings;
140
+ }
141
+ if (claude) next.claudeCode = owner;
142
+ return anthropicSidecarConfigError(next);
143
+ }
@@ -1,5 +1,6 @@
1
1
  import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
2
2
  import * as z from "zod/v4";
3
+ import { anthropicAccountPoolSchema } from "./anthropic-account-pool";
3
4
  import { compactionRecoverySchema } from "./compaction-recovery";
4
5
  import { blockedModelRedirectsSchema } from "./blocked-model-redirects";
5
6
  import {
@@ -327,7 +328,7 @@ export const configSchema = z.object({
327
328
  blockedModelRedirects: blockedModelRedirectsSchema.optional().catch(undefined),
328
329
  // Degrade malformed hand edits locally; candidate writes reject them before parsing.
329
330
  // An invalid native preference retains the legacy route instead of enabling native by default.
330
- anthropicAccountPool: z.object({ nativeMessages: z.boolean().optional().catch(false) }).passthrough().optional().catch(undefined),
331
+ anthropicAccountPool: anthropicAccountPoolSchema,
331
332
  // Same degrade-don't-reject rationale as the fields above: a hand-edited
332
333
  // non-string must not trip the backup-and-defaults repair path. Unset then
333
334
  // takes the canonical sideband path (src/server/live.ts normalizeSidebandRoot).
@@ -350,6 +351,11 @@ export const configSchema = z.object({
350
351
  .map(row => apiKeyEntrySchema.parse(row) as OcxApiKeyEntry);
351
352
  }),
352
353
  }).passthrough().superRefine((config, ctx) => {
354
+ // Helper `anthropicInstance` is deliberately not refined here. A document-level issue would send
355
+ // loadConfig through salvage (which refuses pathless issues) to the backup-and-defaults path, so
356
+ // one hand-edited helper preference would hide every provider. Writes reject it explicitly in
357
+ // validateConfigCandidate, diagnostics report it from the raw file, and loadConfig drops only the
358
+ // offending key (sanitizeAnthropicSidecarInstanceForLoad).
353
359
  const claudeCode = (config as { claudeCode?: unknown }).claudeCode;
354
360
  if (claudeCode !== undefined && (!claudeCode || typeof claudeCode !== "object" || Array.isArray(claudeCode))) {
355
361
  ctx.addIssue({ code: "custom", path: ["claudeCode"], message: "claudeCode must be an object" });
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod/v4";
2
+ import { anthropicAccountPoolSchema } from "./anthropic-account-pool";
2
3
  import { join } from "node:path";
3
4
  import { isValidProviderName } from "../provider-name";
4
5
  import {
@@ -285,6 +286,9 @@ const providerNoProxySchema = z.unknown().superRefine((value, ctx) => {
285
286
  * fields pass through (preserved for runtime extensions).
286
287
  */
287
288
  export const providerConfigSchema = z.object({
289
+ anthropicAccountPool: anthropicAccountPoolSchema,
290
+ // Raw diagnostics/writes reject malformed provenance; tolerant loads preserve the custom row.
291
+ anthropicOAuthInstance: z.literal("anthropic2").optional().catch(undefined),
288
292
  modelCapabilities: modelCapabilitiesSchema.optional(),
289
293
  modelContextTiers: z.unknown().superRefine((value, ctx) => {
290
294
  const error = contextTierRecordConfigError(value);
package/src/config.ts CHANGED
@@ -172,6 +172,7 @@ import {
172
172
  sanitizeModelDisplayNamesForLoad,
173
173
  sanitizeAutoReviewForLoad,
174
174
  sanitizeRetryOn429ForLoad,
175
+ sanitizeAnthropicSidecarInstanceForLoad,
175
176
  sanitizeModelCostsForLoad,
176
177
  sanitizeCapabilityDeclarationsForLoad,
177
178
  warnInheritedFastWireConflicts,
@@ -226,6 +227,7 @@ export function loadConfig(): OcxConfig {
226
227
  sanitizeModelDisplayNamesForLoad(parsed);
227
228
  sanitizeAutoReviewForLoad(parsed);
228
229
  sanitizeRetryOn429ForLoad(parsed);
230
+ sanitizeAnthropicSidecarInstanceForLoad(parsed);
229
231
  sanitizeModelCostsForLoad(parsed);
230
232
  sanitizeCapabilityDeclarationsForLoad(parsed);
231
233
  const result = configSchema.safeParse(parsed);