@bitkyc08/opencodex 2.75.0 → 2.76.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 (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -66,9 +66,11 @@ import { handleRoutingAnalyticsRoutes } from "./management/routing-analytics-rou
66
66
  import { handleMetricsRoutes } from "./management/metrics-routes";
67
67
  import { handleProviderRoutes } from "./management/provider-routes";
68
68
  import { handleModelRoutes } from "./management/model-routes";
69
+ import { handleClaudeInterceptRoutes } from "./management/claude-intercept-routes";
69
70
  import { handleAgentSettingsRoutes } from "./management/agent-settings-routes";
70
71
  import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
71
72
  import { handleComboRoutes } from "./management/combo-routes";
73
+ import { handleDecisionRoutes } from "./management/decision-routes";
72
74
  import { handleSystemRoutes } from "./management/system-routes";
73
75
  import { handleSidebarRoutes } from "./management/sidebar-routes";
74
76
  import { handleUsageTimelineRoutes } from "./management/usage-timeline-routes";
@@ -166,6 +168,12 @@ async function handleWorkflowBudgetRoutesOnDemand(ctx: ManagementContext): Promi
166
168
  return handleWorkflowBudgetRoutes(ctx);
167
169
  }
168
170
 
171
+ async function handleCodexAgentRoleRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
172
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/codex-agent-roles")) return null;
173
+ const { handleCodexAgentRoleRoutes } = await import("./management/codex-agent-role-routes");
174
+ return handleCodexAgentRoleRoutes(ctx);
175
+ }
176
+
169
177
  /**
170
178
  * Lazy like the Lab and routing-profile handlers: the protocol planner reaches the router and
171
179
  * the ingress eligibility rules, which no other dashboard request needs.
@@ -326,7 +334,8 @@ export async function handleManagementAPI(
326
334
  }
327
335
  let routed: Response | null | undefined;
328
336
  try {
329
- routed = handleSessionRoutes(ctx)
337
+ routed = await handleClaudeInterceptRoutes(ctx)
338
+ ?? handleSessionRoutes(ctx)
330
339
  ?? (await handleLinkRoutesOnDemand(ctx))
331
340
  ?? (await handleRemoteWorkspaceRoutesOnDemand(ctx))
332
341
  ?? (await handleConfigRoutes(ctx))
@@ -336,6 +345,7 @@ export async function handleManagementAPI(
336
345
  ?? (await handleQuotaResetRoutesOnDemand(ctx))
337
346
  ?? (await handleLowQuotaRoutesOnDemand(ctx))
338
347
  ?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
348
+ ?? (await handleCodexAgentRoleRoutesOnDemand(ctx))
339
349
  ?? (await handleProtocolRoutesOnDemand(ctx))
340
350
  ?? (await handleGrokCouponRoutesOnDemand(ctx))
341
351
  ?? (await handleAnthropicResetGrantRoutesOnDemand(ctx))
@@ -351,6 +361,7 @@ export async function handleManagementAPI(
351
361
  ?? (await handleAgentSettingsRoutes(ctx))
352
362
  ?? (await handleCodexPromptRoutes(ctx))
353
363
  ?? (await handleOauthAccountRoutes(ctx))
364
+ ?? (await handleDecisionRoutes(ctx))
354
365
  ?? (await handleComboRoutes(ctx))
355
366
  ?? (await handleSystemRoutes(ctx))
356
367
  ?? (await handleLabRoutesOnDemand(ctx))
@@ -55,6 +55,8 @@ function isRec(value: unknown): value is Rec {
55
55
  export interface NativeOAuthBinding {
56
56
  readonly selection: Selection;
57
57
  readonly snapshot: OAuthAccessSnapshot;
58
+ /** Anthropic account UUID from the same stored generation, distinct from snapshot.accountId. */
59
+ readonly providerAccountUuid?: string;
58
60
  }
59
61
 
60
62
  /** The selection moved or became pooled while it was being resolved. Maps to a 409 retry. */
@@ -95,7 +97,9 @@ export async function resolveNativeOAuthBinding(config: OcxConfig): Promise<Nati
95
97
  expectedCredentialGeneration: candidate.generation,
96
98
  })) continue;
97
99
  if (pooled(config)) break;
98
- const binding = { selection: committed, snapshot: candidate };
100
+ const row = getAccountCredentialWithStatus(PROVIDER, candidate.accountId);
101
+ if (!row || credentialGeneration(row.credential) !== candidate.generation) continue;
102
+ const binding = { selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId };
99
103
  if (nativeOAuthBindingIsCurrent(binding)) return binding;
100
104
  }
101
105
  // Re-read after a lost commit or a health change during credential resolution.
@@ -114,7 +118,8 @@ export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolea
114
118
  && selected?.revision === binding.selection.revision
115
119
  && !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
116
120
  && !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
117
- && credentialGeneration(row.credential) === binding.snapshot.generation;
121
+ && credentialGeneration(row.credential) === binding.snapshot.generation
122
+ && row.credential.accountId === binding.providerAccountUuid;
118
123
  }
119
124
 
120
125
  /** Map a `tool_use` block's wire name back to the caller's name; other blocks are untouched. */
@@ -10,8 +10,8 @@
10
10
  * key selection, key failover and 429 replay, connection policy — so nothing the Responses
11
11
  * pipeline enforces is bypassed.
12
12
  *
13
- * Authority. This lane reads no caller header. The ingress hands over one value, the caller's
14
- * `anthropic-beta`, which the builder reduces to an allowlist. The caller-forward passthrough in
13
+ * Authority. The ingress hands over caller `anthropic-beta` for allowlisting and an opaque
14
+ * observed CLI identity handle for first-party compatibility. Neither grants credential authority. The caller-forward passthrough in
15
15
  * `claude-messages.ts` (the caller's own Anthropic credential) is a different branch decided
16
16
  * before this one, and nothing here can reach it or be reached from it.
17
17
  *
@@ -67,6 +67,7 @@ import type { OcxProviderTransport } from "../providers/xai-transport";
67
67
  import { preservesPhysicalComboProvider, resolveComboId } from "../combos";
68
68
  import { captureRouteStaticPolicy, routeModel, type RouteResult } from "../router";
69
69
  import { POLICY_NAMESPACE, resolvePolicyProfileId } from "../routing/profile";
70
+ import type { AnthropicClientIdentity } from "../adapters/anthropic/client-identity";
70
71
  import type { OcxConfig, OcxProviderConfig, OcxUsage } from "../types";
71
72
  import { resolveWireProtocolOverride } from "./adapter-resolve";
72
73
  import {
@@ -145,9 +146,10 @@ export interface HandleNativeMessagesOptions {
145
146
  selector?: NativeMessagesSelector;
146
147
  /**
147
148
  * The caller's `anthropic-beta` header, handed over by the ingress. The builder keeps only
148
- * allowlisted values; no other caller header reaches this lane.
149
+ * allowlisted values. Client identity is a separate bounded opaque handle.
149
150
  */
150
151
  callerAnthropicBeta?: string | null;
152
+ clientIdentity?: AnthropicClientIdentity;
151
153
  }
152
154
 
153
155
  type FinishLog = (status: number, message?: string, meta?: FinalRequestLogMeta) => void;
@@ -410,6 +412,8 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
410
412
  recordAttemptCredentialSource(attempt, route.providerName, activeProvider, "anthropic");
411
413
  const built = buildAnthropicMessagesPassthroughRequest(activeProvider, route.modelId, body, config, {
412
414
  callerAnthropicBeta: options.callerAnthropicBeta,
415
+ providerAccountUuid: oauthBinding?.providerAccountUuid,
416
+ clientIdentity: options.clientIdentity,
413
417
  });
414
418
  if (built.strippedOpaqueState && rejectUnrepresentable) throw new NativeOpaqueStateRefusal();
415
419
  if (built.droppedBetas) addProtocolEntryReason(logCtx, "anthropic-beta-dropped");
@@ -779,7 +779,9 @@ export function firstOutputFromParsed(parsed: unknown): boolean {
779
779
  const event = parsed as { type?: unknown; delta?: unknown };
780
780
  return (event.type === "response.output_text.delta"
781
781
  || event.type === "response.reasoning_summary_text.delta"
782
- || event.type === "response.reasoning_text.delta")
782
+ || event.type === "response.reasoning_text.delta"
783
+ || event.type === "response.function_call_arguments.delta"
784
+ || event.type === "response.custom_tool_call_input.delta")
783
785
  && typeof event.delta === "string"
784
786
  && event.delta.length > 0;
785
787
  }
@@ -247,6 +247,8 @@ export interface RequestLogContext {
247
247
  terminalSource?: "upstream" | "synthetic";
248
248
  /** Bounded route-decision trace (RI-01); never contains secrets. */
249
249
  routeDecision?: RouteDecisionTraceV1;
250
+ /** Full eligible provider/model membership from a policy route; never logged. */
251
+ policyEligibility?: ReadonlySet<string>;
250
252
  /** Privacy-bounded JEV selection metadata; downstream usage is recorded on attempts[]. */
251
253
  jevDecision?: PersistedJevDecisionV1;
252
254
  /** Opt-in shadow evidence, normalized again at the logging boundary. */
@@ -396,9 +396,9 @@ export function createAdapterContinuations(
396
396
  ) {
397
397
  const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
398
398
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
399
- decision: transportState.anthropicRouteDecision, signal: upstream.signal,
400
- canRetry: transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
401
- allowAccountRefusal,
399
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: upstream.signal,
400
+ canRetry: !sendBudgetExhausted() && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
401
+ allowAccountRefusal, allow429Recovery: allowAccountRefusal,
402
402
  });
403
403
  if (nextAccountId) {
404
404
  try {
@@ -1,3 +1,4 @@
1
+ import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
1
2
  import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
2
3
  import { isNonReplayableResponse } from "../../lib/upstream-retry";
3
4
  import { isLocalUpstream } from "../../lib/local-upstream";
@@ -691,7 +692,8 @@ export async function prepareAdapterExchange(
691
692
  refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
692
693
  } catch (err) {
693
694
  const failed = transportState.sentOAuthSnapshot;
694
- if (route.providerName === "google-antigravity" && err instanceof OAuthLoginRequiredError && failed
695
+ if (route.providerName === "google-antigravity" && !antigravityAuthRotationAttempted
696
+ && err instanceof OAuthLoginRequiredError && failed
695
697
  && getAccountSet(route.providerName)?.accounts.some(row =>
696
698
  row.id === failed.accountId && row.needsReauth === true)) {
697
699
  antigravityAuthRotationAttempted = true;
@@ -993,8 +995,8 @@ export async function prepareAdapterExchange(
993
995
  ) {
994
996
  const nextAccountId = await rotateAnthropicAccountOnResponse(upstreamResponse, {
995
997
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
996
- decision: transportState.anthropicRouteDecision, signal: upstream.signal,
997
- canRetry: transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
998
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: upstream.signal,
999
+ canRetry: !sendBudgetExhausted() && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
998
1000
  });
999
1001
  if (!nextAccountId) break;
1000
1002
  try {
@@ -1091,6 +1093,11 @@ export async function prepareAdapterExchange(
1091
1093
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1092
1094
  config.cacheRetention,
1093
1095
  );
1096
+ bindRouteReasoningReplayScope({
1097
+ parsed, providerName: route.providerName, provider: route.provider,
1098
+ adapterName: transportState.activeAdapter.name,
1099
+ oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1100
+ });
1094
1101
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
1095
1102
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
1096
1103
  // The replay IS this hop's send, so hand the reservation down and let the layer that
@@ -1190,6 +1197,13 @@ export async function prepareAdapterExchange(
1190
1197
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1191
1198
  config.cacheRetention,
1192
1199
  );
1200
+ // Same contract as the Kiro refusal arm: the replay must carry the replacement
1201
+ // account's continuation owner and reasoning scope, never the account that just 429'd.
1202
+ bindRouteReasoningReplayScope({
1203
+ parsed, providerName: route.providerName, provider: route.provider,
1204
+ adapterName: transportState.activeAdapter.name,
1205
+ oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
1206
+ });
1193
1207
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
1194
1208
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
1195
1209
  // The replay IS this hop's send, so hand the reservation down and let the layer that
@@ -1414,6 +1428,7 @@ export async function prepareAdapterExchange(
1414
1428
  status: upstreamResponse.status,
1415
1429
  message,
1416
1430
  upstreamRetryAfter,
1431
+ includeDefault: !(transportState.anthropicPoolAccountId && classifyAnthropic429(upstreamResponse.headers) === "request-scoped-unknown"),
1417
1432
  });
1418
1433
  return formatErrorResponse(
1419
1434
  upstreamResponse.status,
@@ -1,4 +1,5 @@
1
1
  import { capturePoolQuotaWriter } from "../../codex/account-store";
2
+ import { previewXaiOauthWireModel } from "./core-normalize";
2
3
  import {
3
4
  admissionModelDeniedResponse,
4
5
  AdmissionModelDeniedError,
@@ -196,6 +197,7 @@ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel }
196
197
  import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
197
198
  import { linkRequestSessionLane, sessionLaneIdFromRequest } from "../request-log-conversation";
198
199
  import { recallComboForLane } from "./combo-session-recall";
200
+ import { redactHostedImageDisplayPaths } from "../responses-hosted-image-display";
199
201
 
200
202
  export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
201
203
 
@@ -622,6 +624,8 @@ export async function handleResponsesCompact(
622
624
  if (!body || typeof body !== "object" || Array.isArray(body)) {
623
625
  return formatErrorResponse(400, "invalid_request_error", "Invalid compaction request body");
624
626
  }
627
+ // Compaction forwards client history upstream too; keep local artifact paths out of it.
628
+ redactHostedImageDisplayPaths(body);
625
629
  if (!options.compactionRoutingOverride) {
626
630
  options = { ...options, compactionRoutingOverride: applyCompactionRoutingOverride(body, req.headers, config, { endpoint: "compact" }) };
627
631
  }
@@ -670,7 +674,15 @@ export async function handleResponsesCompact(
670
674
  // A compaction override picks the model, not the caller, so the key's scope
671
675
  // is applied to what the override resolved to rather than to the selector
672
676
  // the client sent.
673
- assertRouteAllowedByScope(resolveAdmissionModelScope(config, admission), compactRequestedModel, route);
677
+ // Routed xAI OAuth compaction must preview its Fast wire model here;
678
+ // native compact keeps checking the destination it dispatches directly.
679
+ const callerTier = (raw as Record<string, unknown>).service_tier;
680
+ assertRouteAllowedByScope(resolveAdmissionModelScope(config, admission), compactRequestedModel, {
681
+ providerName: route.providerName,
682
+ modelId: previewXaiOauthWireModel({ options: {
683
+ serviceTier: compactFastRow ? "priority" : typeof callerTier === "string" ? callerTier : undefined,
684
+ } }, route, config, "responses"),
685
+ });
674
686
  } catch (err) {
675
687
  if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
676
688
  if (err instanceof NoEligiblePolicyCandidateError) {
@@ -691,6 +703,7 @@ export async function handleResponsesCompact(
691
703
  logCtx.requestedModel = compactRequestedModel;
692
704
  logCtx.model = selectedModelId;
693
705
  logCtx.routeDecision = route.routeDecision;
706
+ logCtx.policyEligibility = route.policyEligibility;
694
707
  logCtx.provider = route.codexAccountNamespace
695
708
  ? `${route.providerName}-${route.codexAccountNamespace}`
696
709
  : route.providerName;
@@ -89,6 +89,8 @@ export function codexRouteCredentialDomainHeaders(
89
89
  // Route-changing recursion retains typed admission, never an unscoped raw
90
90
  // caller credential. Bearer admission is substituted or stripped below.
91
91
  const routeMayChangeCredentialDomain = options.comboAttempt === true
92
+ // A JEV decision-model turn is a new credential domain even though it starts headerless.
93
+ || options.internalDecisionCall === true
92
94
  || route.routeKind === "policy"
93
95
  || route.credentialDomainRewrite === true
94
96
  || credentialDomainWasRewritten;
@@ -31,7 +31,8 @@ import {
31
31
  advanceComboAfterFailure,
32
32
  comboFailureCooldownScope,
33
33
  JEV_PROVIDER_ID,
34
- resolveJevDecision,
34
+ jevDecisionBackendFor,
35
+ resolveJevComboDecision,
35
36
  type ComboPick,
36
37
  type JevCandidate,
37
38
  type JevDecision,
@@ -48,7 +49,12 @@ import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
48
49
  import { memoryModelRouteReason } from "./memory-models";
49
50
  import { poolAccountProviderLabel } from "../../providers/label";
50
51
  import { getAccountSet } from "../../oauth/store";
51
- import { formatAnthropicProviderForLog, getAnthropicAccountHealthSnapshot } from "../../oauth/anthropic-routing";
52
+ import {
53
+ formatAnthropicProviderForLog,
54
+ getAnthropicAccountHealthSnapshot,
55
+ getAnthropicPoolRetryAfterSeconds,
56
+ getEligibleAnthropicAccounts,
57
+ } from "../../oauth/anthropic-routing";
52
58
  import { codexAccountLogLabel } from "../../codex/account-label";
53
59
  import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
54
60
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
@@ -93,6 +99,7 @@ import { streamingContextOverflowResponse, jsonContextOverflowResponse } from ".
93
99
  import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
94
100
  import { settleOperatorReplacement } from "../../lib/upstream-retry";
95
101
  import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo-native";
102
+ import { createJevModelInvoker } from "./jev-model-invoke";
96
103
  import { clientWireOf } from "../inference/client-wire";
97
104
 
98
105
  /**
@@ -119,6 +126,33 @@ function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId
119
126
  }
120
127
 
121
128
 
129
+ /**
130
+ * True only for the Anthropic OAuth pool's OWN "all accounts cooled" 429. Its Retry-After is the
131
+ * earliest per-account cooldown restated (`getAnthropicPoolRetryAfterSeconds`), which the pool
132
+ * keeps enforcing locally, so it must not also park the combo target: an account added, resumed or
133
+ * cleared a minute later would otherwise sit ignored until the target expired, up to the 24h
134
+ * server-delay ceiling.
135
+ *
136
+ * Provenance, not just current state: the target must be the OAuth pool, no account may have been
137
+ * identified for the failure, and the pool must be holding the wait right now. An upstream 429 on
138
+ * a pool account always cools and names that account, and an API-key provider never qualifies, so
139
+ * a genuine upstream Retry-After still parks the target in full.
140
+ */
141
+ export function isAnthropicPoolLocalRefusal(
142
+ config: OcxConfig,
143
+ providerName: string,
144
+ status: number,
145
+ failedAccount: string | undefined,
146
+ now = Date.now(),
147
+ ): boolean {
148
+ return providerName === "anthropic"
149
+ && config.providers[providerName]?.authMode === "oauth"
150
+ && status === 429
151
+ && failedAccount === undefined
152
+ && getEligibleAnthropicAccounts(now).length === 0
153
+ && getAnthropicPoolRetryAfterSeconds(now) !== null;
154
+ }
155
+
122
156
  /**
123
157
  * A combo's execution policy is DECLARED by the combo, not inherited from the single-target
124
158
  * profile.
@@ -533,16 +567,30 @@ export async function executeComboResponses(
533
567
  const decisionStartedAt = Date.now();
534
568
  let decision: JevDecision;
535
569
  try {
536
- decision = await resolveJevDecision({
570
+ decision = await resolveJevComboDecision({
537
571
  body,
538
572
  candidates: choices.map(choice => choice.candidate),
539
573
  fallback,
540
574
  config,
575
+ ...(combo.decisionProvider ? { decisionProvider: combo.decisionProvider } : {}),
576
+ ...(combo.decisionModel
577
+ ? {
578
+ decisionModel: combo.decisionModel,
579
+ invokeModel: createJevModelInvoker({
580
+ req,
581
+ config,
582
+ options,
583
+ handleResponses: requestDispatchers.handleResponses,
584
+ }),
585
+ }
586
+ : {}),
587
+ ...(combo.decisionTimeoutMs !== undefined ? { timeoutMs: combo.decisionTimeoutMs } : {}),
541
588
  signal: options.abortSignal,
542
589
  });
543
590
  } catch (error) {
544
591
  if (options.abortSignal?.aborted) return clientCancelledResponse();
545
592
  decision = {
593
+ backend: jevDecisionBackendFor(combo),
546
594
  ...fallback,
547
595
  gate: "network",
548
596
  latencyMs: Math.max(0, Date.now() - decisionStartedAt),
@@ -561,6 +609,7 @@ export async function executeComboResponses(
561
609
  },
562
610
  gate: decision.gate,
563
611
  latencyMs: decision.latencyMs,
612
+ backend: decision.backend,
564
613
  ...(decision.confidence !== undefined ? { confidence: decision.confidence } : {}),
565
614
  ...(decision.chosenProbability !== undefined
566
615
  ? { chosenProbability: decision.chosenProbability }
@@ -568,6 +617,7 @@ export async function executeComboResponses(
568
617
  ...(decision.usage ? { usage: decision.usage } : {}),
569
618
  });
570
619
  console.debug("[combo] JEV decision", {
620
+ backend: decision.backend,
571
621
  targetKey: decision.targetKey,
572
622
  effort: decision.effort,
573
623
  gate: decision.gate,
@@ -988,8 +1038,20 @@ export async function executeComboResponses(
988
1038
  });
989
1039
  const failedTargetKey = targetKey(pick.target);
990
1040
  let failedTargetCooldownRecorded = false;
1041
+ // The dispatch rewrote this to name the pool account that actually served the turn.
1042
+ const failedAccount = cooledPoolAccountLabel(
1043
+ config,
1044
+ pick.target.provider,
1045
+ pick.target.model,
1046
+ poolAccountProviderLabel(childLog.provider, pick.target.provider),
1047
+ );
1048
+ // The target still cools on the pool's own refusal (the anti-hammer guard), but for the local
1049
+ // fallback rather than the pool's restated per-account Retry-After.
1050
+ const poolLocalRefusal = isAnthropicPoolLocalRefusal(
1051
+ config, pick.target.provider, failure.response.status, failedAccount, failureNow,
1052
+ );
991
1053
  const nextPick = advanceComboAfterFailure(config, pick, {
992
- retryAfter: failure.retryAfter,
1054
+ retryAfter: poolLocalRefusal ? undefined : failure.retryAfter,
993
1055
  resetAt: failure.resetAt,
994
1056
  cooldownMs: combo.cooldownMs,
995
1057
  now: failureNow,
@@ -998,13 +1060,7 @@ export async function executeComboResponses(
998
1060
  status: failure.response.status,
999
1061
  code: failure.upstreamCode,
1000
1062
  message: failure.classificationText,
1001
- // The dispatch rewrote this to name the pool account that actually served the turn.
1002
- failedAccount: cooledPoolAccountLabel(
1003
- config,
1004
- pick.target.provider,
1005
- pick.target.model,
1006
- poolAccountProviderLabel(childLog.provider, pick.target.provider),
1007
- ),
1063
+ failedAccount,
1008
1064
  onCooldownRecorded: target => {
1009
1065
  failedTargetCooldownRecorded ||= targetKey(target) === failedTargetKey;
1010
1066
  },
@@ -38,7 +38,26 @@ import { isInjectionDebugEnabled } from "../../lib/debug-settings";
38
38
  import { injectionDebugLog } from "../../lib/injection-debug-log";
39
39
  import { recordAttemptRequestedEffort } from "../request-log";
40
40
  import type { ResolvedFastPolicy } from "../../providers/fastwire";
41
- import { applyXaiOauthFastModel } from "../../providers/xai-fast-model";
41
+ import { applyXaiOauthFastModel, xaiOauthFastModelForDecision } from "../../providers/xai-fast-model";
42
+
43
+ /** Preview the billed xAI lane without mutating request state or resolving credentials. */
44
+ export function previewXaiOauthWireModel(
45
+ parsed: { options: Pick<OcxParsedRequest["options"], "serviceTier"> },
46
+ route: RouteResult,
47
+ config: OcxConfig,
48
+ inboundWire: InboundWire,
49
+ ): string {
50
+ if (route.providerName !== "xai" || route.provider.authMode !== "oauth") return route.modelId;
51
+ const provider = resolveWireProtocolOverride(
52
+ route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy,
53
+ );
54
+ const policy = fastPolicyForModel(
55
+ provider, route.modelId, route.providerName, inboundWire, config.providers[route.providerName],
56
+ );
57
+ return xaiOauthFastModelForDecision(
58
+ { ...route, provider }, decideTier(policy, config.fastMode, parsed.options.serviceTier),
59
+ ) ?? route.modelId;
60
+ }
42
61
 
43
62
  export const MAX_FAST_WIRE_CAPABILITY_WARNINGS = 256;
44
63
 
@@ -186,6 +205,7 @@ export async function applyFinalRouteRequestNormalization(args: {
186
205
  logCtx.provider = route.providerName;
187
206
  logCtx.providerAdapter = route.provider.adapter;
188
207
  logCtx.routeDecision = route.routeDecision;
208
+ logCtx.policyEligibility = route.policyEligibility;
189
209
  if (route.routeReason === "model-alias" || route.modelId !== responseModelId && responseModelId.includes("/")) logCtx.requestedAlias = responseModelId;
190
210
 
191
211
  if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
@@ -19,6 +19,7 @@ import type { TransientSendBudget } from "../../lib/upstream-retry";
19
19
  import type { RequestLogContext } from "../request-log";
20
20
  import type { UpstreamHostAdmissionLease } from "../../codex/upstream-host-health";
21
21
  import type { AccountLease } from "../../oauth/kiro-account-load";
22
+ import type { PolicyRequestScope } from "./policy-request-scope";
22
23
 
23
24
  export interface ConsumedComboFailure {
24
25
  response: Response;
@@ -57,6 +58,10 @@ export interface ClientEncoderOption {
57
58
  }
58
59
 
59
60
  export interface HandleResponsesOptions {
61
+ /** Internal request-owned policy authorization; never read from client headers or body. */
62
+ policyRequestScope?: PolicyRequestScope;
63
+ /** Internal concrete selector chosen from the original policy evaluation. */
64
+ policyFallbackCandidate?: { provider: string; model: string };
60
65
  /** Internal routed-compaction recovery: one logical request, one emergency target. */
61
66
  compactionRecoveryAttempted?: boolean;
62
67
  compactionRecoveryPermit?: SingleUseDispatchPermit;
@@ -66,7 +71,7 @@ export interface HandleResponsesOptions {
66
71
  /** Physical-send reports already delivered to the shared used setter, including booking settlement. */
67
72
  onCompactionRecoverySendsReported?: (count: number) => void;
68
73
  /** Private holder for the Kiro serving-account lease. */
69
- accountLoad?: { lease: AccountLease | null };
74
+ accountLoad?: { lease: AccountLease | null; cancelled: boolean };
70
75
  /** Internal Claude replay identity; consumed only by the final canonical Go transport. */
71
76
  claudeGoAffinity?: { sessionLane?: string };
72
77
  /** Validated Claude metadata identity; projected only into final canonical attempt headers. */
@@ -74,6 +79,11 @@ export interface HandleResponsesOptions {
74
79
  /** Original live policy owner; separate from caller-specific routing/sidecar snapshots. */
75
80
  codexAuthPolicy?: CodexAuthPolicyConfig;
76
81
  turnAdmissionLease?: AdmissionLease;
82
+ /**
83
+ * A JEV decision-model call issued by a combo. It never carries caller credentials, is never
84
+ * rewritten by memory or shadow-call routing, and may not dispatch into a JEV combo.
85
+ */
86
+ internalDecisionCall?: boolean;
77
87
  /**
78
88
  * How the caller proved data-plane admission (#1686).
79
89
  *
@@ -43,10 +43,12 @@ export async function handleResponses(
43
43
  ): Promise<Response> {
44
44
  const ownsBudget = options.translatorBudget === undefined;
45
45
  const translatorBudget = options.translatorBudget ?? createTranslatorBudget();
46
- const accountLoad = { lease: null as import("../../oauth/kiro-account-load").AccountLease | null };
47
46
  const abortSignal = options.abortSignal ?? req.signal;
48
- const release = () => { accountLoad.lease?.release(); accountLoad.lease = null; abortSignal.removeEventListener("abort", release); };
49
- abortSignal.addEventListener("abort", release, { once: true });
47
+ const accountLoad = { lease: null as import("../../oauth/kiro-account-load").AccountLease | null,
48
+ cancelled: abortSignal.aborted };
49
+ function release() { accountLoad.lease?.release(); accountLoad.lease = null; abortSignal.removeEventListener("abort", cancel); }
50
+ function cancel() { accountLoad.cancelled = true; release(); }
51
+ abortSignal.addEventListener("abort", cancel, { once: true });
50
52
  try {
51
53
  const response = await runWithCompactionRecovery(req, config, logCtx, {
52
54
  ...options,
@@ -15,6 +15,7 @@ import {
15
15
  type ProviderRequestSlot,
16
16
  } from "../../providers/request-pacing";
17
17
  import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
18
+ import { providerTlsFetch } from "../../lib/provider-tls-profile";
18
19
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
19
20
  import { configuredOutboundFetch } from "../../lib/proxy-env";
20
21
  import { isLoopbackUrl, rewriteUpstream } from "../../plugins/upstream-hooks";
@@ -252,8 +253,10 @@ export function providerFetch(
252
253
  { preconnect: globalThis.fetch.preconnect?.bind(globalThis.fetch) },
253
254
  ) as typeof globalThis.fetch);
254
255
  const base = customExecutor ?? configuredFetch;
256
+ const transport = options.providerName ? providerTlsFetch(options.providerName, provider, base) : base;
255
257
  const preconnect = (...args: Parameters<typeof globalThis.fetch.preconnect>): void => {
256
- base.preconnect?.(...args);
258
+ // A TLS profile owns the handshake; a Bun preconnect would open a differently fingerprinted one.
259
+ if (transport === base) base.preconnect?.(...args);
257
260
  };
258
261
  // Rebuilt dispatches must use the same physical-send boundary as ordinary HTTP sends.
259
262
  // Return the original 3xx so the response owner retains its retry/health/relay contract.
@@ -266,7 +269,7 @@ export function providerFetch(
266
269
  // that decided for itself has already marked the init and this pass defers to that decision.
267
270
  const dispatch = markEgressTransparentExecutor(Object.assign(
268
271
  (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
269
- sendWithConnectionPolicy(base, input, init, egressBinding),
272
+ sendWithConnectionPolicy(transport, input, init, egressBinding),
270
273
  { preconnect },
271
274
  ) as typeof globalThis.fetch);
272
275
  const httpFetch = Object.assign(
@@ -281,7 +284,7 @@ export function providerFetch(
281
284
  // the override may rebuild against a different host and select a different transport, and
282
285
  // refusing on this destination would reject a request whose real route is fine.
283
286
  if (options.dispatchOverride) egressFor(input);
284
- else providerEgressSendInit(egressBinding, base, input);
287
+ else providerEgressSendInit(egressBinding, transport, input);
285
288
  // The hook inspects the outgoing headers and refuses the send by throwing; it is not a
286
289
  // mutator, and the copy it receives is deliberately not threaded onward. `Connection`
287
290
  // is decided inside `dispatch`, which runs after this, so the fresh-connection policy