@bitkyc08/opencodex 2.90.0 → 2.92.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 (250) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/App-DMps4OLk.css +1 -0
  3. package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
  4. package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
  5. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  6. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  9. package/package.json +4 -3
  10. package/src/adapters/anthropic.ts +2 -5
  11. package/src/adapters/claude-cli/usage-admission.ts +14 -0
  12. package/src/adapters/google-vertex-endpoint.ts +9 -1
  13. package/src/adapters/google-vertex-key-project.ts +223 -0
  14. package/src/adapters/google.ts +13 -3
  15. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  16. package/src/adapters/openai-chat.ts +2 -2
  17. package/src/adapters/openai-responses/passthrough.ts +2 -1
  18. package/src/adapters/openai-responses/request-strips.ts +92 -0
  19. package/src/adapters/openai-responses/transport.ts +6 -0
  20. package/src/adapters/registry.ts +5 -0
  21. package/src/adapters/workbuddy-schema.ts +61 -0
  22. package/src/adapters/workbuddy.ts +68 -0
  23. package/src/bridge/response-json.ts +1 -1
  24. package/src/bridge/sse.ts +1 -1
  25. package/src/chat/inbound.ts +20 -2
  26. package/src/chat/outbound.ts +47 -8
  27. package/src/chat/reasoning-details.ts +137 -0
  28. package/src/cli/capabilities-agents-routing.ts +53 -3
  29. package/src/cli/capabilities-base.ts +15 -2
  30. package/src/cli/capabilities-observe-system.ts +4 -4
  31. package/src/cli/codex-cli-update.ts +8 -0
  32. package/src/cli/codex-shim-autorestore.ts +5 -0
  33. package/src/cli/combo-result.ts +6 -0
  34. package/src/cli/combo-stats.ts +3 -0
  35. package/src/cli/combo.ts +66 -42
  36. package/src/cli/dispatch.ts +30 -0
  37. package/src/cli/fallback.ts +127 -0
  38. package/src/cli/help.ts +1 -0
  39. package/src/cli/index.ts +1 -2
  40. package/src/cli/launcher-context.ts +49 -2
  41. package/src/cli/link-apply-restart.ts +72 -0
  42. package/src/cli/link-child.ts +6 -2
  43. package/src/cli/link-cleanup.ts +95 -0
  44. package/src/cli/link-enrollment.ts +4 -0
  45. package/src/cli/link.ts +123 -35
  46. package/src/cli/provider.ts +46 -3
  47. package/src/cli/registry.ts +13 -1
  48. package/src/cli/restart-failure.ts +4 -0
  49. package/src/cli/runtime-api.ts +1 -1
  50. package/src/cli/status-codex-settings.ts +56 -0
  51. package/src/cli/status-fallback.ts +29 -0
  52. package/src/cli/status-install.ts +52 -0
  53. package/src/cli/status.ts +20 -1
  54. package/src/cli/system-restart-client.ts +32 -1
  55. package/src/cli/update-restart-child.ts +4 -3
  56. package/src/cli/update-restart-home.ts +20 -8
  57. package/src/cli/usage-compare.ts +68 -12
  58. package/src/client/connect.ts +82 -6
  59. package/src/client/home-unreachable.ts +16 -0
  60. package/src/client/hub-relay.ts +8 -2
  61. package/src/client/link-home.ts +119 -6
  62. package/src/client/link-ingress.ts +1 -1
  63. package/src/client/link-parity-reporter.ts +107 -0
  64. package/src/client/link-readiness.ts +97 -0
  65. package/src/client/link-relay-transport.ts +84 -9
  66. package/src/client/link-relay.ts +98 -15
  67. package/src/client/link-tunnel.ts +45 -18
  68. package/src/client/machine-listener.ts +103 -3
  69. package/src/client/runtime-restart.ts +127 -0
  70. package/src/client/runtime.ts +155 -25
  71. package/src/codex/account-store.ts +17 -3
  72. package/src/codex/app-server-restart-service.ts +6 -5
  73. package/src/codex/auth-context.ts +10 -6
  74. package/src/codex/catalog/effort.ts +9 -2
  75. package/src/codex/catalog/gather-capture.ts +4 -2
  76. package/src/codex/catalog/parsing.ts +4 -0
  77. package/src/codex/catalog/provider-models.ts +14 -8
  78. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  79. package/src/codex/cli-install-provenance.ts +418 -0
  80. package/src/codex/cli-installation-targets.ts +5 -1
  81. package/src/codex/cli-update-plan.ts +43 -16
  82. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  83. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  84. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  85. package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
  86. package/src/codex/pool-refresh-backoff.ts +3 -1
  87. package/src/codex/quota-history.ts +7 -0
  88. package/src/codex/quota.ts +10 -0
  89. package/src/codex/subagent-model-fallback.ts +24 -1
  90. package/src/codex/user-identity.ts +35 -6
  91. package/src/codex/windows-current-sid.ts +162 -0
  92. package/src/combos/index.ts +1 -0
  93. package/src/combos/jev-quota-activation.ts +5 -1
  94. package/src/combos/jev-quota-route.ts +1 -1
  95. package/src/combos/jev-quota.ts +9 -1
  96. package/src/combos/jev-session-pin-config.ts +51 -0
  97. package/src/combos/jev.ts +6 -0
  98. package/src/combos/resolve.ts +12 -0
  99. package/src/combos/types.ts +19 -0
  100. package/src/config/diagnostics.ts +5 -0
  101. package/src/config/load-degrade.ts +2 -0
  102. package/src/config/provider-validation.ts +7 -0
  103. package/src/config/schema/config-schema.ts +3 -0
  104. package/src/config/schema/fallback-policy.ts +40 -0
  105. package/src/config/schema/leaf-validators.ts +1 -0
  106. package/src/generated/compatibility-version.json +375 -195
  107. package/src/lib/authorization-scheme.ts +95 -0
  108. package/src/lib/decision-quota-sink.ts +10 -0
  109. package/src/lib/provider-client-headers.ts +1 -0
  110. package/src/lib/redact.ts +2 -2
  111. package/src/lib/system-restart-contract.ts +24 -0
  112. package/src/lib/test-home-guard.ts +78 -11
  113. package/src/lib/translator-budget.ts +4 -4
  114. package/src/lib/upstream-retry.ts +4 -1
  115. package/src/link/child-parity.ts +124 -0
  116. package/src/link/codex-settings.ts +37 -0
  117. package/src/link/compatibility.ts +119 -0
  118. package/src/link/diagnostics.ts +193 -0
  119. package/src/link/management-routes.ts +2 -0
  120. package/src/link/peer-report.ts +100 -0
  121. package/src/link/relay-auth.ts +6 -0
  122. package/src/link/remote-platform.ts +78 -0
  123. package/src/link/ssh-argv.ts +19 -0
  124. package/src/link/status-projection.ts +63 -2
  125. package/src/link/supervisor.ts +45 -12
  126. package/src/link/tunnel-history.ts +109 -0
  127. package/src/link/tunnel-state.ts +80 -17
  128. package/src/messaging/remote-files.ts +24 -1
  129. package/src/oauth/generic-account-failover.ts +91 -7
  130. package/src/oauth/index.ts +16 -1
  131. package/src/oauth/kimi.ts +2 -0
  132. package/src/oauth/model-discovery-auth.ts +5 -0
  133. package/src/oauth/nous.ts +4 -1
  134. package/src/oauth/store.ts +6 -2
  135. package/src/oauth/token-guardian.ts +7 -0
  136. package/src/oauth/types.ts +9 -0
  137. package/src/oauth/workbuddy.ts +139 -0
  138. package/src/protocols/codecs/chat.ts +3 -3
  139. package/src/protocols/encoders/adapter-events.ts +26 -2
  140. package/src/protocols/encoders/chat.ts +55 -3
  141. package/src/providers/antigravity-quota-identity.ts +37 -0
  142. package/src/providers/base-url-choices.ts +8 -5
  143. package/src/providers/derive.ts +23 -3
  144. package/src/providers/mistral-transport.ts +37 -4
  145. package/src/providers/model-discovery.ts +2 -0
  146. package/src/providers/model-rename-fields.ts +1 -0
  147. package/src/providers/quota/account-cache.ts +3 -2
  148. package/src/providers/quota/antigravity.ts +27 -22
  149. package/src/providers/quota/report-cache.ts +13 -4
  150. package/src/providers/quota/vendor-probes-key.ts +60 -0
  151. package/src/providers/quota/workbuddy.ts +83 -0
  152. package/src/providers/quota-decision-antigravity.ts +64 -0
  153. package/src/providers/quota-decision-publication.ts +11 -0
  154. package/src/providers/quota-decision-snapshot.ts +21 -4
  155. package/src/providers/quota-types.ts +2 -0
  156. package/src/providers/quota.ts +3 -1
  157. package/src/providers/registry/entries-core.ts +9 -0
  158. package/src/providers/registry/entries-extended.ts +20 -7
  159. package/src/providers/registry/model-ids.ts +3 -0
  160. package/src/providers/registry/model-seeds.ts +27 -28
  161. package/src/providers/registry/types.ts +19 -1
  162. package/src/providers/resolved-model-policy.ts +4 -4
  163. package/src/providers/routed-config.ts +15 -0
  164. package/src/providers/stale-model-roster-migration.ts +83 -15
  165. package/src/providers/workbuddy-credit-balance.ts +81 -0
  166. package/src/providers/workbuddy-models.ts +37 -0
  167. package/src/providers/workbuddy.ts +60 -0
  168. package/src/quota-pool/assemble.ts +326 -0
  169. package/src/quota-pool/contract.ts +58 -0
  170. package/src/quota-pool/identity.ts +203 -0
  171. package/src/remote/features.ts +74 -0
  172. package/src/remote/protocol.ts +2 -0
  173. package/src/remote/served-by.ts +57 -0
  174. package/src/responses/parser.ts +7 -0
  175. package/src/responses/reasoning-envelope.ts +12 -0
  176. package/src/responses/reasoning-replay-cache.ts +18 -14
  177. package/src/router.ts +2 -0
  178. package/src/routing/analytics.ts +2 -3
  179. package/src/routing/fallback-policy.ts +290 -0
  180. package/src/routing/fallback-state.ts +179 -0
  181. package/src/server/audio-client.ts +2 -1
  182. package/src/server/audio-dictation.ts +2 -1
  183. package/src/server/audio-upstream.ts +2 -1
  184. package/src/server/auth-cors.ts +7 -5
  185. package/src/server/background-lifecycle.ts +4 -0
  186. package/src/server/chat-completions.ts +22 -2
  187. package/src/server/chat-native-reasoning.ts +10 -3
  188. package/src/server/chat-native.ts +13 -2
  189. package/src/server/claude-messages.ts +3 -0
  190. package/src/server/gui-session.ts +2 -1
  191. package/src/server/index/link-listener.ts +11 -1
  192. package/src/server/index/link-management.ts +47 -0
  193. package/src/server/index/link-relay-sessions.ts +20 -7
  194. package/src/server/index/optional-listeners.ts +11 -2
  195. package/src/server/index/serve-options.ts +19 -2
  196. package/src/server/live.ts +5 -0
  197. package/src/server/local-management-read-client.ts +3 -1
  198. package/src/server/management/combo-routes.ts +10 -4
  199. package/src/server/management/context.ts +1 -1
  200. package/src/server/management/fallback-routes.ts +75 -0
  201. package/src/server/management/link-enrollment.ts +21 -3
  202. package/src/server/management/link-route-state.ts +2 -0
  203. package/src/server/management/link-routes.ts +16 -2
  204. package/src/server/management/provider-routes.ts +1 -1
  205. package/src/server/management/quota-pool-routes.ts +24 -0
  206. package/src/server/management/route-registry.ts +3 -0
  207. package/src/server/management-api.ts +16 -0
  208. package/src/server/management-auth.ts +2 -1
  209. package/src/server/port-reclaim.ts +51 -9
  210. package/src/server/request-log-conversation.ts +49 -0
  211. package/src/server/request-log.ts +2 -0
  212. package/src/server/responses/agent-task-recovery.ts +1 -1
  213. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  214. package/src/server/responses/compact.ts +12 -4
  215. package/src/server/responses/core-auth.ts +11 -1
  216. package/src/server/responses/core-combo.ts +125 -32
  217. package/src/server/responses/core-lifetime.ts +2 -2
  218. package/src/server/responses/core-normalize.ts +6 -3
  219. package/src/server/responses/core-opaque-recovery.ts +1 -0
  220. package/src/server/responses/core-options.ts +5 -0
  221. package/src/server/responses/core-replay.ts +2 -1
  222. package/src/server/responses/encrypted-payload.ts +3 -3
  223. package/src/server/responses/jev-combo-choice.ts +39 -0
  224. package/src/server/responses/jev-session-pin.ts +203 -0
  225. package/src/server/responses/main-hard-lock-routing.ts +54 -2
  226. package/src/server/responses/passthrough-delivery.ts +4 -0
  227. package/src/server/responses/policy-fallback.ts +44 -1
  228. package/src/server/responses/request-prepare.ts +19 -6
  229. package/src/server/responses/request-transport.ts +25 -6
  230. package/src/server/responses/run-turn-execution.ts +2 -4
  231. package/src/server/responses/sidecar-execution.ts +2 -4
  232. package/src/server/responses-phase-inference.ts +267 -0
  233. package/src/server/restart-replacement.ts +50 -2
  234. package/src/server/sse-payload-rewrite.ts +14 -1
  235. package/src/server/ws-bridge.ts +5 -0
  236. package/src/service/cli.ts +25 -2
  237. package/src/service/definition.ts +20 -8
  238. package/src/service/diagnostics.ts +20 -6
  239. package/src/service/status-json.ts +116 -0
  240. package/src/types/config.ts +13 -0
  241. package/src/types/fallback.ts +38 -0
  242. package/src/types/provider.ts +9 -1
  243. package/src/types/request.ts +1 -0
  244. package/src/types/wire.ts +1 -0
  245. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  246. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  247. package/src/usage/jev-stats.ts +54 -14
  248. package/gui/dist/assets/App-dnkJ85te.css +0 -1
  249. package/gui/dist/assets/index-TmaHa54u.js +0 -94
  250. package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { effectiveAzureOutputBudget } from "../../adapters/azure";
2
2
  import { resolveWireProtocolOverride } from "../adapter-resolve";
3
- import type { CodexAccountModelRefusal } from "../../combos/failover";
3
+ import { comboCooldownRetryAfterSeconds, type CodexAccountModelRefusal } from "../../combos/failover";
4
+ import { fallbackTriggerForStatus, guardFallbackHop } from "../../routing/fallback-policy";
4
5
  import {
5
6
  isCodexReasoningEffort,
6
7
  isDeclaredReasoningEffort,
@@ -32,6 +33,7 @@ import {
32
33
  isComboTargetInCooldown,
33
34
  snapshotComboQuotaCooldowns,
34
35
  comboExclusionReasons,
36
+ comboTargetSkipTrigger,
35
37
  noteComboSuccess,
36
38
  comboFailureDecision,
37
39
  advanceComboAfterFailure,
@@ -66,7 +68,7 @@ import {
66
68
  import { codexAccountLogLabel, oauthAccountLogLabel } from "../../codex/account-label";
67
69
  import { isGenericFailoverProvider, rotateGenericOAuthAccountOn429 } from "../../oauth/generic-account-failover";
68
70
  import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
69
- import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
71
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
70
72
  import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
71
73
  import {
72
74
  agentTaskRecoveryConfig,
@@ -74,6 +76,7 @@ import {
74
76
  recoverEncryptedAgentTaskWithResult,
75
77
  } from "./agent-task-recovery";
76
78
  import { isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
79
+ import { jevChoiceEfforts } from "./jev-combo-choice";
77
80
  import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
78
81
  import { isPlainObject } from "../../lib/plain-data";
79
82
  import {
@@ -100,6 +103,7 @@ import type { ResponsesTerminalStatus } from "../../bridge";
100
103
  import { beginRequestAttempt, sealRequestAttemptIdentity, finishRequestAttempt } from "../request-log";
101
104
  import { rememberComboForLane } from "./combo-session-recall";
102
105
  import { COMBO_TOOL_LOOP_PIN_REASON, toolLoopPinnedPick } from "./combo-tool-loop-pin";
106
+ import { JEV_SESSION_PIN_ROUTE_REASON, jevSessionPinScope } from "./jev-session-pin";
103
107
  import { runTurnAdapterSseResponses } from "./core-lifetime";
104
108
  import { normalizePersistedJevDecision } from "../../usage/jev-stats";
105
109
  import {
@@ -116,6 +120,7 @@ import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo
116
120
  import { createJevModelInvoker } from "./jev-model-invoke";
117
121
  import { comboRequestedEffortLabel } from "./combo-requested-effort";
118
122
  import { clientWireOf } from "../inference/client-wire";
123
+ import { resolveContextPrincipal } from "../auth-cors";
119
124
 
120
125
  /**
121
126
  * Sends one combo target may run on its own before the ladder moves on. A target is a whole
@@ -124,6 +129,20 @@ import { clientWireOf } from "../inference/client-wire";
124
129
  */
125
130
  export const COMBO_TARGET_BASE_SENDS = CODEX_TEXT_GUARDED_BUDGET_POLICY.baseSendAllowance;
126
131
 
132
+ /**
133
+ * OCX-11: where a combo target physically dispatches, after provider aliases and
134
+ * blockedModelRedirects. A target that does not resolve keeps its configured selector; its
135
+ * dispatch fails on its own.
136
+ */
137
+ function resolvedComboDestination(config: OcxConfig, target: { provider: string; model: string }): { provider: string; model: string } {
138
+ try {
139
+ const route = routeConcreteModel(config, `${target.provider}/${target.model}`);
140
+ return { provider: route.providerName, model: route.modelId };
141
+ } catch {
142
+ return { provider: target.provider, model: target.model };
143
+ }
144
+ }
145
+
127
146
  function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId: string, label: string | undefined): string | undefined {
128
147
  if (!label) return undefined;
129
148
  const instance = configuredAnthropicInstance(config, providerName);
@@ -300,30 +319,6 @@ interface JevComboChoice {
300
319
  candidate: JevCandidate;
301
320
  }
302
321
 
303
- /** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
304
- function jevChoiceEfforts(
305
- config: OcxConfig,
306
- target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
307
- ): OcxComboDefaultEffort[] | undefined {
308
- let ladder: string[] | undefined;
309
- try {
310
- const route = routeConcreteModel(config, targetKey(target));
311
- ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
312
- } catch {
313
- // Preserve the existing routing-failure surface. Unknown capability becomes the explicit
314
- // no-effort choice rather than broadening JEV's effort allowlist.
315
- ladder = undefined;
316
- }
317
- const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
318
- const configuredEfforts = target.reasoningEfforts;
319
- const reasoningEfforts = configuredEfforts === undefined
320
- ? supportedEfforts
321
- : configuredEfforts.filter(effort => supportedEfforts.includes(effort));
322
- // An explicit allowlist is restrictive. If catalog capabilities drift until no configured
323
- // effort remains supported, omit the target instead of silently broadening JEV's choices.
324
- return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : reasoningEfforts;
325
- }
326
-
327
322
  /** Enumerate the current ordinary Combo eligibility set without retaining attempted picks. */
328
323
  function eligibleJevComboChoices(
329
324
  config: OcxConfig,
@@ -342,15 +337,15 @@ function eligibleJevComboChoices(
342
337
  excluded.add(key);
343
338
  // The TypeSafe row owns a decision credential, not an inference transport.
344
339
  if (pick.target.provider === JEV_PROVIDER_ID) continue;
345
- const reasoningEfforts = jevChoiceEfforts(config, pick.target);
346
- if (!reasoningEfforts) continue;
340
+ const identity = jevChoiceEfforts(config, pick.target, combo.decisionQuotaSignals === true);
341
+ if (!identity) continue;
347
342
  choices.push({
348
343
  pick: { ...pick, attempted: [key] },
349
344
  candidate: {
350
345
  key,
351
346
  provider: pick.target.provider,
352
347
  model: pick.target.model,
353
- reasoningEfforts,
348
+ ...identity,
354
349
  modelProfile: pick.target.modelProfile,
355
350
  },
356
351
  });
@@ -389,6 +384,10 @@ export async function executeComboResponses(
389
384
  if (!combo) {
390
385
  return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
391
386
  }
387
+ // Resolve against the original request once; every bridge candidate receives the same trusted
388
+ // result after its provisional Authorization is removed, including an explicitly keyless result.
389
+ const replayPrincipal = options.replayPrincipal
390
+ ?? { clientPrincipalId: resolveContextPrincipal(req, config, options.admission) };
392
391
  // PF-07: present only for a Chat combo with `nativeChatCombos` on; otherwise every child
393
392
  // takes the bridge below exactly as before.
394
393
  const protocolLanes = createComboProtocolLanes({
@@ -465,6 +464,7 @@ export async function executeComboResponses(
465
464
  const unavailable = mandatoryResponsesReasoningReplayUnavailable({
466
465
  body,
467
466
  clientThreadId: reasoningReplayConversationId,
467
+ clientPrincipalId: replayPrincipal.clientPrincipalId,
468
468
  providerName: route.providerName,
469
469
  provider: route.provider,
470
470
  adapterName: route.provider.adapter,
@@ -607,6 +607,10 @@ export async function executeComboResponses(
607
607
  eligible: targetEligible,
608
608
  now: initialNow,
609
609
  });
610
+ // Opt-in JEV session pin (#7525); override routes neither read nor write it, like the lane recall.
611
+ const jevPin = options.compactionRoutingOverride || options.mainHardLockFallbackOverride
612
+ ? undefined
613
+ : jevSessionPinScope({ combo, comboId, admission: options.admission, lane: sessionLaneIdFromRequest(req.headers) });
610
614
 
611
615
  if (unreadableEncryptedAgentTask && !pick) {
612
616
  pick = await pickWithWait({ now: initialNow });
@@ -638,13 +642,37 @@ export async function executeComboResponses(
638
642
  ? clientCancelledResponse()
639
643
  : comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, targetEligible));
640
644
  }
645
+ // OCX-11: an ordered failover combo that starts on a later target than its first one, for any
646
+ // reason (cooling down, provider disabled, cached quota spent, ineligible for this request),
647
+ // makes a proactive hop, so the fallback policy decides before anything is sent. Both ends are
648
+ // the resolved destinations, after provider aliases and blockedModelRedirects.
649
+ const primaryTarget = combo.targets[0];
650
+ // An allowed hop is recorded only once its dispatch is admitted below.
651
+ let pendingHopCommit: (() => void) | undefined;
652
+ if (!toolLoopPin && (combo.strategy ?? "failover") === "failover" && primaryTarget
653
+ && targetKey(primaryTarget) !== targetKey(pick.target)) {
654
+ const primaryCooling = isComboTargetInCooldown(comboId, primaryTarget, initialNow);
655
+ const proactive = guardFallbackHop(config, {
656
+ site: "combo",
657
+ route: `combo/${comboId}`,
658
+ aliases: [requestedModel],
659
+ from: resolvedComboDestination(config, primaryTarget),
660
+ to: resolvedComboDestination(config, pick.target),
661
+ trigger: primaryCooling ? "cooldown" : comboTargetSkipTrigger(config, primaryTarget, initialNow),
662
+ admission: options.admission,
663
+ retryAfter: primaryCooling ? comboCooldownRetryAfterSeconds(comboId, initialNow) ?? null : null,
664
+ now: initialNow,
665
+ });
666
+ if (!proactive.ok) return proactive.response;
667
+ pendingHopCommit = proactive.commit;
668
+ }
641
669
  let jevDecision: JevDecision | undefined;
642
670
  if (combo.strategy === "jev" && !toolLoopPin) {
643
671
  const choices = eligibleJevComboChoices(config, comboId, targetEligible, Date.now());
644
672
  const first = choices[0];
645
673
  if (!first) {
646
674
  return comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, target =>
647
- targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target) !== undefined));
675
+ targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target, false) !== undefined));
648
676
  }
649
677
  const resolvedFailOpenEffort = resolveEffortAtOrBelow(
650
678
  "medium",
@@ -657,8 +685,13 @@ export async function executeComboResponses(
657
685
  : null,
658
686
  };
659
687
  const decisionStartedAt = Date.now();
688
+ const pinReuse = jevPin?.reuse(
689
+ choices.map(choice => choice.candidate),
690
+ combo.decisionQuotaSignals === true ? { config, tiers: combo.decisionQuotaTiers } : undefined,
691
+ );
660
692
  let decision: JevDecision;
661
- try {
693
+ if (pinReuse?.decision) decision = pinReuse.decision;
694
+ else try {
662
695
  decision = await resolveJevComboDecision({
663
696
  body,
664
697
  candidates: choices.map(choice => choice.candidate),
@@ -692,6 +725,7 @@ export async function executeComboResponses(
692
725
  latencyMs: Math.max(0, Date.now() - decisionStartedAt),
693
726
  };
694
727
  }
728
+ if (pinReuse?.released) decision = { ...decision, pinReason: pinReuse.released };
695
729
  jevDecision = decision;
696
730
  const selected = choices.find(choice => choice.candidate.key === decision.targetKey) ?? first;
697
731
  pick = { ...selected.pick, attempted: [targetKey(selected.pick.target)] };
@@ -712,6 +746,8 @@ export async function executeComboResponses(
712
746
  : {}),
713
747
  ...(decision.usage ? { usage: decision.usage } : {}),
714
748
  ...(decision.quota ? { quota: decision.quota } : {}),
749
+ ...(decision.pinned ? { pinned: true } : {}),
750
+ ...(decision.pinReason ? { pinReason: decision.pinReason } : {}),
715
751
  });
716
752
  console.debug("[combo] JEV decision", {
717
753
  backend: decision.backend,
@@ -719,6 +755,7 @@ export async function executeComboResponses(
719
755
  effort: decision.effort,
720
756
  gate: decision.gate,
721
757
  latencyMs: decision.latencyMs,
758
+ ...(decision.pinReason ? { pinReason: decision.pinReason } : {}),
722
759
  ...(decision.confidence !== undefined ? { confidence: decision.confidence } : {}),
723
760
  ...(decision.chosenProbability !== undefined
724
761
  ? { chosenProbability: decision.chosenProbability }
@@ -731,7 +768,7 @@ export async function executeComboResponses(
731
768
  const decision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
732
769
  const selectedReason = options.memoryModelPhase
733
770
  ? memoryModelRouteReason(options.memoryModelPhase)
734
- : toolLoopPin ? COMBO_TOOL_LOOP_PIN_REASON : undefined;
771
+ : toolLoopPin ? COMBO_TOOL_LOOP_PIN_REASON : jevDecision?.pinned ? JEV_SESSION_PIN_ROUTE_REASON : undefined;
735
772
  logCtx.routeDecision = selectedReason
736
773
  ? { ...decision, selected: { ...decision.selected, reason: selectedReason } }
737
774
  : decision;
@@ -760,6 +797,9 @@ export async function executeComboResponses(
760
797
  // no-more-targets branch; a budget refusal ends the ladder one iteration later, where that
761
798
  // iteration's own `childLog` is already out of scope.
762
799
  let lastFailedChildLog: RequestLogContext | undefined;
800
+ // The target behind `lastFailure`, so the next dispatch can tell a hop from a same-target retry.
801
+ let lastFailedTarget: { provider: string; model: string } | undefined;
802
+ let lastFailedRetryAfter: string | undefined;
763
803
  // The exhausted-combo mapping below runs outside the loop, where `failure.upstreamCode`
764
804
  // is gone, so carry the loop's own classification decision instead of re-deriving a
765
805
  // weaker one from the status alone (#4149).
@@ -812,7 +852,45 @@ export async function executeComboResponses(
812
852
  while (pick) {
813
853
  if (options.abortSignal?.aborted) return clientCancelledResponse();
814
854
  const firstComboTarget = comboTargetsDispatched === 0;
855
+ // The physical destination after provider aliases and blockedModelRedirects: hops are
856
+ // classified by where the request really goes, not by the selector the combo names.
815
857
  const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
858
+ // The hard-lock rewrite may have previewed a different random pick. Enforce this actual
859
+ // first destination against the locked source, after aliases and redirects resolve.
860
+ if (firstComboTarget && options.mainHardLockFallbackOverride) {
861
+ const hop = guardFallbackHop(config, {
862
+ site: "combo",
863
+ route: `combo/${comboId}`,
864
+ aliases: [requestedModel],
865
+ from: { provider: OPENAI_CODEX_PROVIDER_ID, model: options.mainHardLockFallbackOverride.sourceModel },
866
+ to: { provider: targetRoute.providerName, model: targetRoute.modelId },
867
+ trigger: "hard_lock",
868
+ admission: options.admission,
869
+ });
870
+ if (!hop.ok) return hop.response;
871
+ const declaredCommit = pendingHopCommit;
872
+ pendingHopCommit = declaredCommit ? () => { declaredCommit(); hop.commit(); } : hop.commit;
873
+ }
874
+ // OCX-11: every later dispatch after a failure is a hop when the target changed. One check
875
+ // here covers the ordinary failover pick and the recovery picks alike.
876
+ if (!firstComboTarget && lastFailure && lastFailedTarget) {
877
+ const hop = guardFallbackHop(config, {
878
+ site: "combo",
879
+ route: `combo/${comboId}`,
880
+ aliases: [requestedModel],
881
+ from: lastFailedTarget,
882
+ to: { provider: targetRoute.providerName, model: targetRoute.modelId },
883
+ trigger: fallbackTriggerForStatus(lastFailure.status),
884
+ admission: options.admission,
885
+ failureStatus: lastFailure.status,
886
+ retryAfter: lastFailedRetryAfter ?? null,
887
+ });
888
+ if (!hop.ok) {
889
+ if (lastFailedChildLog) adoptFailedChildLog(lastFailedChildLog);
890
+ return hop.response;
891
+ }
892
+ pendingHopCommit = hop.commit;
893
+ }
816
894
  // The inherited spend tracker observes this parent log, before the child has its own label.
817
895
  logCtx.spendPoolId = targetRoute.providerName;
818
896
  // Combo prepays before its child normalizes the route; the parent tracker must
@@ -851,6 +929,9 @@ export async function executeComboResponses(
851
929
  // an earlier one already recorded one.
852
930
  return exhaustedFailure();
853
931
  }
932
+ // Admitted: only now is a guarded hop to this target recorded as taken.
933
+ pendingHopCommit?.();
934
+ pendingHopCommit = undefined;
854
935
  const initialSend = hopDecision?.allowed ? { permit: hopDecision.permit, producerOwned: false } : undefined;
855
936
  try {
856
937
  comboTargetsDispatched += 1;
@@ -946,6 +1027,7 @@ export async function executeComboResponses(
946
1027
  && provider && provider.disabled !== true
947
1028
  && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
948
1029
  rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
1030
+ jevPin?.complete(targetKey(completedTarget), initialJevDecision, writerGeneration);
949
1031
  }
950
1032
  options.onResponseComplete?.(model);
951
1033
  },
@@ -958,6 +1040,8 @@ export async function executeComboResponses(
958
1040
  if (childLog.terminalIncompleteReason !== undefined) logCtx.terminalIncompleteReason = childLog.terminalIncompleteReason;
959
1041
  if (childLog.terminalErrorCode !== undefined) logCtx.terminalErrorCode = childLog.terminalErrorCode;
960
1042
  if (childLog.upstreamError !== undefined) logCtx.upstreamError = childLog.upstreamError;
1043
+ // A committed stream that ends failed is a target failure too; incomplete is an answer.
1044
+ if (status === "failed") jevPin?.forget();
961
1045
  options.onNativePassthroughTerminal?.(status);
962
1046
  },
963
1047
  });
@@ -997,6 +1081,7 @@ export async function executeComboResponses(
997
1081
  },
998
1082
  }) : await requestDispatchers.handleResponses(childRequest, config, childLog, {
999
1083
  ...options,
1084
+ replayPrincipal,
1000
1085
  // A bridge child is a concrete route; the native source belongs to this loop only.
1001
1086
  ...(options.protocolSource ? { protocolSource: undefined } : {}),
1002
1087
  // After the spread: the child must run on THIS target's ladder, not on the holder the
@@ -1031,6 +1116,7 @@ export async function executeComboResponses(
1031
1116
  retainCancelledAttempt();
1032
1117
  return clientCancelledResponse();
1033
1118
  }
1119
+ jevPin?.forget();
1034
1120
  finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
1035
1121
  throw error;
1036
1122
  }
@@ -1055,6 +1141,7 @@ export async function executeComboResponses(
1055
1141
  retainCancelledAttempt();
1056
1142
  return clientCancelledResponse();
1057
1143
  }
1144
+ jevPin?.forget();
1058
1145
  finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
1059
1146
  throw error;
1060
1147
  }
@@ -1111,6 +1198,7 @@ export async function executeComboResponses(
1111
1198
  retainCancelledAttempt();
1112
1199
  return clientCancelledResponse();
1113
1200
  }
1201
+ jevPin?.forget();
1114
1202
  finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
1115
1203
  throw error;
1116
1204
  }
@@ -1142,11 +1230,16 @@ export async function executeComboResponses(
1142
1230
  }
1143
1231
  attemptRetained = true;
1144
1232
  lastFailure = failure.response;
1233
+ // A real target failure ends the session pin; the next turn is decided afresh (#7525).
1234
+ jevPin?.forget();
1145
1235
  if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
1146
1236
  quotaFailure = lastFailure;
1147
1237
  quotaFailedChildLog = childLog;
1148
1238
  }
1149
1239
  lastFailedChildLog = childLog;
1240
+ // The failed child's resolved destination, so the next hop compares physical targets.
1241
+ lastFailedTarget = { provider: targetRoute.providerName, model: targetRoute.modelId };
1242
+ lastFailedRetryAfter = failure.retryAfter;
1150
1243
  // A replacement that answers 200 is unmarked, and its zero-output failure only exists once
1151
1244
  // preflight has rebuilt the stream as a fresh Response. A spent grant never hops: a status the
1152
1245
  // client would resend becomes the refusal, and anything else reaches the client as it is.
@@ -1,7 +1,7 @@
1
1
  import type { TranslatorBudget } from "../../lib/translator-budget";
2
2
  import { carryResponseToolOwnership } from "./response-tool-ownership";
3
3
  import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
4
- import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
4
+ import { carryReplayRefusal, isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
5
5
  import {
6
6
  isNativePassthroughSseResponse,
7
7
  markNativePassthroughSseResponse,
@@ -76,7 +76,7 @@ export function finalizeAccountLease(response: Response, release: () => void, on
76
76
  if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
77
77
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
78
78
  if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(wrapped);
79
- return carryResponseToolOwnership(response, wrapped);
79
+ return carryResponseToolOwnership(response, carryReplayRefusal(response, wrapped));
80
80
  }
81
81
 
82
82
 
@@ -358,14 +358,17 @@ export async function applyFinalRouteRequestNormalization(args: {
358
358
 
359
359
  {
360
360
  const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
361
+ const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
362
+ // The parser folds `ultra` to `max` only in options.reasoning; the passthrough adapter
363
+ // serializes the raw body, so a raw `ultra` must reach the clamp itself (#7590).
364
+ const rawUltra = raw?.reasoning && typeof raw.reasoning === "object" && raw.reasoning.effort === "ultra";
361
365
  const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
362
- ? nativeEffortClamp(route.modelId, parsed.options.reasoning)
366
+ ? nativeEffortClamp(route.modelId, rawUltra ? "ultra" : parsed.options.reasoning)
363
367
  : null;
364
368
  if (clamped) {
365
369
  parsed.options.reasoning = clamped;
366
- const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
367
370
  if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
368
- logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
371
+ logCtx.requestedEffort = `${logCtx.requestedEffort ?? (rawUltra ? "ultra" : "max")}->${clamped}`;
369
372
  }
370
373
  }
371
374
  // Chat ingress cannot strip effort against a provisional policy pick. Apply the
@@ -547,6 +547,7 @@ export async function attemptOpaqueBlobRecovery<Failure = Response>(
547
547
  const rejectedScope = args.parsed._reasoningReplayScope
548
548
  ? {
549
549
  clientThreadId: args.parsed._reasoningReplayScope.clientThreadId,
550
+ clientPrincipalId: args.parsed._reasoningReplayScope.clientPrincipalId,
550
551
  ...(args.parsed._reasoningReplayScope.current
551
552
  ? { current: { ...args.parsed._reasoningReplayScope.current } }
552
553
  : {}),
@@ -128,6 +128,11 @@ export interface HandleResponsesOptions {
128
128
  * which is why it refused the whole env_key flow instead of serving it.
129
129
  */
130
130
  admission?: DataPlaneAdmission;
131
+ /**
132
+ * Internal caller identity captured before Combo strips provisional credentials. An existing
133
+ * holder with no principal preserves keyless replay; never populate it from client data.
134
+ */
135
+ replayPrincipal?: Readonly<{ clientPrincipalId: string | undefined }>;
131
136
  /** Called at most once after the complete client body is read and accepted for dispatch. */
132
137
  onRequestBodyRead?: () => void;
133
138
  /** Internal handoff for retry wrappers that must reuse the already-accounted request body. */
@@ -344,6 +344,7 @@ function routeReasoningReplayIdentity(args: {
344
344
  export function mandatoryResponsesReasoningReplayUnavailable(args: {
345
345
  body: unknown;
346
346
  clientThreadId: string | undefined;
347
+ clientPrincipalId?: string;
347
348
  providerName: string;
348
349
  provider: OcxProviderConfig;
349
350
  adapterName: string;
@@ -359,7 +360,7 @@ export function mandatoryResponsesReasoningReplayUnavailable(args: {
359
360
  ) return false;
360
361
 
361
362
  const current = routeReasoningReplayIdentity(args);
362
- return reasoningReplayServingIdentityChanged({ clientThreadId, current });
363
+ return reasoningReplayServingIdentityChanged({ clientThreadId, clientPrincipalId: args.clientPrincipalId, current });
363
364
  }
364
365
 
365
366
 
@@ -265,13 +265,13 @@ export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
265
265
  // codex-rs appends one agent_message (any of the four codex-rs message types) at the
266
266
  // current input tail. Historical
267
267
  // agent messages may be adjacent in full-history bodies; they must not poison the
268
- // later task. compaction_trigger/additional_tools are trailing metadata rather than
269
- // a newer user turn.
268
+ // later task. compaction_trigger/additional_tools/configuration_update are trailing
269
+ // metadata rather than a newer user turn; Codex may record an effort update after the task.
270
270
  let index = input.length - 1;
271
271
  while (index >= 0) {
272
272
  const item = input[index];
273
273
  const type = item && typeof item === "object" ? (item as { type?: unknown }).type : undefined;
274
- if (type !== "compaction_trigger" && type !== "additional_tools") break;
274
+ if (type !== "compaction_trigger" && type !== "additional_tools" && type !== "configuration_update") break;
275
275
  index -= 1;
276
276
  }
277
277
  const item = input[index];
@@ -0,0 +1,39 @@
1
+ import { getCombo, targetKey, type JevCandidate } from "../../combos";
2
+ import { isCodexReasoningEffort } from "../../reasoning-effort";
3
+ import { routeConcreteModel } from "../../router";
4
+ import { supportedLadderFor } from "../effort-policy";
5
+ import type { OcxComboDefaultEffort, OcxConfig } from "../../types";
6
+
7
+ /** A JEV target's effort choices plus, when quota signals are on, its resolved native identity. */
8
+ export interface JevChoiceIdentity {
9
+ reasoningEfforts: OcxComboDefaultEffort[];
10
+ quotaIdentity?: JevCandidate["quotaIdentity"];
11
+ }
12
+
13
+ /** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
14
+ export function jevChoiceEfforts(
15
+ config: OcxConfig,
16
+ target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
17
+ quotaSignals: boolean,
18
+ ): JevChoiceIdentity | undefined {
19
+ let ladder: string[] | undefined;
20
+ // null tells the quota reader the route did not resolve, so it reads unknown rather than the configured alias.
21
+ let quotaIdentity: JevCandidate["quotaIdentity"] = quotaSignals ? null : undefined;
22
+ try {
23
+ const route = routeConcreteModel(config, targetKey(target));
24
+ ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
25
+ if (quotaSignals) quotaIdentity = { provider: route.providerName, model: route.modelId };
26
+ } catch {
27
+ // Preserve the existing routing-failure surface. Unknown capability becomes the explicit
28
+ // no-effort choice rather than broadening JEV's effort allowlist.
29
+ ladder = undefined;
30
+ }
31
+ const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
32
+ const configuredEfforts = target.reasoningEfforts;
33
+ const reasoningEfforts = configuredEfforts === undefined
34
+ ? supportedEfforts
35
+ : configuredEfforts.filter(effort => supportedEfforts.includes(effort));
36
+ // An explicit allowlist is restrictive. If catalog capabilities drift until no configured
37
+ // effort remains supported, omit the target instead of silently broadening JEV's choices.
38
+ return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : { reasoningEfforts, quotaIdentity };
39
+ }