@bitkyc08/opencodex 2.59.0 → 2.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -13,7 +13,6 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
13
13
  import type { RequestLogContext } from "../request-log";
14
14
  import type { HandleResponsesOptions } from "./core-options";
15
15
  import { prepareEffortNormalization } from "../effort-policy";
16
- import { providerModelResponsesUpstreamStreaming } from "../../providers/registry";
17
16
  import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
18
17
  import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
19
18
  import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
@@ -80,6 +79,7 @@ export function canPassThroughEncryptedV2AgentTask(
80
79
  route.modelId,
81
80
  provider,
82
81
  inboundWire,
82
+ route.staticPolicy,
83
83
  ).adapter === "openai-responses";
84
84
  }
85
85
 
@@ -126,6 +126,8 @@ export async function applyFinalRouteRequestNormalization(args: {
126
126
  const responseModelId = parsed.modelId;
127
127
  const preserveAnthropicResponseModel = route.providerName === "anthropic"
128
128
  || route.provider.adapter === "anthropic";
129
+ const finalSelectedModelId = route.modelId;
130
+ const virtualModel = applyOpenAiVirtualModel(parsed, route, logCtx, inboundWire);
129
131
 
130
132
  // Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
131
133
  if (route.modelId !== parsed.modelId) {
@@ -136,11 +138,7 @@ export async function applyFinalRouteRequestNormalization(args: {
136
138
  }
137
139
  // Transport-neutral reliability policy (#875): applies to any Responses
138
140
  // upstream whose final adapter is openai-responses, not only WS turns.
139
- const responsesUpstreamStreaming = providerModelResponsesUpstreamStreaming(
140
- route.providerName,
141
- route.provider,
142
- route.modelId,
143
- );
141
+ const responsesUpstreamStreaming = route.staticPolicy.model.responsesUpstreamStreaming;
144
142
 
145
143
  // Preserve the routed destination for Go recognition, then settle the wire before
146
144
  // deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
@@ -150,6 +148,7 @@ export async function applyFinalRouteRequestNormalization(args: {
150
148
  route.modelId,
151
149
  routedProvider,
152
150
  inboundWire,
151
+ route.staticPolicy,
153
152
  );
154
153
  route.provider = resolveOpenCodeGoTransport(wireProvider,
155
154
  args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req),
@@ -168,7 +167,7 @@ export async function applyFinalRouteRequestNormalization(args: {
168
167
  || (!summary && route.provider.showThinkingSummary !== true);
169
168
  }
170
169
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
171
- logCtx.model = route.modelId;
170
+ logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
172
171
  logCtx.provider = route.providerName;
173
172
  logCtx.providerAdapter = route.provider.adapter;
174
173
  logCtx.routeDecision = route.routeDecision;
@@ -194,11 +193,6 @@ export async function applyFinalRouteRequestNormalization(args: {
194
193
  (parsed._rawBody as Record<string, unknown>).store = false;
195
194
  }
196
195
 
197
- // Final selected model before virtual wire-model rewriting (Pro aliases).
198
- const finalSelectedModelId = route.modelId;
199
-
200
- // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
201
- applyOpenAiVirtualModel(parsed, route, logCtx);
202
196
  if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
203
197
  logCtx.resolvedModel = route.modelId;
204
198
  logCtx.preserveResolvedModelFromRoute = true;
@@ -88,6 +88,7 @@ export function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
88
88
  try {
89
89
  const payload = JSON.parse(bodyText) as unknown;
90
90
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
91
+ if (upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
91
92
  const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
92
93
  if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
93
94
  if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
@@ -11,6 +11,7 @@ import type { NativeMainRefreshDependencies } from "../../codex/main-account";
11
11
  import type { InboundWire } from "../../providers/registry";
12
12
  import type { ExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
13
13
  import type { CallerDirectAuth } from "../../providers/caller-authorization";
14
+ import type { CompactionRoutingOverride } from "./compaction-routing";
14
15
  import type { TranslatorBudget } from "../../lib/translator-budget";
15
16
  import type { TransientSendBudget } from "../../lib/upstream-retry";
16
17
  import type { RequestLogContext } from "../request-log";
@@ -51,6 +52,8 @@ export interface HandleResponsesOptions {
51
52
  admission?: DataPlaneAdmission;
52
53
  /** Called at most once after the complete client body is read and accepted for dispatch. */
53
54
  onRequestBodyRead?: () => void;
55
+ /** Internal handoff for retry wrappers that must reuse the already-accounted request body. */
56
+ onRequestBodyParsed?: (body: unknown) => void;
54
57
  forceEmptyResponseId?: boolean;
55
58
  /** Internal, connection-owned control channel; never reconstructed from headers. */
56
59
  nativeControl?: NativeResponseControl;
@@ -105,6 +108,7 @@ export interface HandleResponsesOptions {
105
108
  callerDirectAuth?: CallerDirectAuth | null;
106
109
  /** Internal recursion guard; callers outside this module must not set it. */
107
110
  comboAttempt?: boolean;
111
+ compactionRoutingOverride?: CompactionRoutingOverride | null;
108
112
  /** Internal combo handoff for one parent-validated continuation snapshot. */
109
113
  comboReplaySnapshot?: {
110
114
  sourceBody: unknown;
@@ -98,6 +98,19 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
98
98
 
99
99
 
100
100
  export function looksLikeBackendCiphertext(payload: string): boolean {
101
+ // Unknown replay history has no authenticity proof. Require the key-independent Fernet wire
102
+ // structure instead of granting ciphertext authority to any long base64-like model output.
103
+ // Proven backend bytes still remain opaque and byte-identical; malformed/plaintext slots are
104
+ // lowered by the compatibility path below rather than poisoning every later native replay.
105
+ return isStructurallyValidFernetToken(payload);
106
+ }
107
+
108
+ /**
109
+ * Pre-route compatibility keeps an encoded-looking unknown slot opaque until the destination is
110
+ * known. A routed destination strips that slot instead of exposing possible truncated ciphertext;
111
+ * the canonical backend later applies the stricter structural classifier.
112
+ */
113
+ function looksLikeUnknownOpaqueSlot(payload: string): boolean {
101
114
  return payload.length >= 64 && /^[A-Za-z0-9+/=_-]+$/.test(payload);
102
115
  }
103
116
 
@@ -480,7 +493,10 @@ function contentWithoutCiphertext(content: unknown[]): unknown[] {
480
493
 
481
494
 
482
495
 
483
- export function sanitizeEncryptedContentInPlace(input: unknown): number {
496
+ export function sanitizeEncryptedContentInPlace(
497
+ input: unknown,
498
+ options: { preserveUnknownOpaqueSlots?: boolean } = {},
499
+ ): number {
484
500
  if (!Array.isArray(input)) return 0;
485
501
  let rewritten = 0;
486
502
  const protectedFragments = new WeakSet<object>();
@@ -512,7 +528,9 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
512
528
  && typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
513
529
  ) {
514
530
  const payload = (child as { encrypted_content: string }).encrypted_content;
515
- if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload)) {
531
+ const preserveUnknown = options.preserveUnknownOpaqueSlots === true
532
+ && looksLikeUnknownOpaqueSlot(payload);
533
+ if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload) && !preserveUnknown) {
516
534
  const parts = encryptedSlotParts(payload);
517
535
  frame.node.splice(frame.index, 1, ...parts);
518
536
  rewritten += 1;
@@ -35,10 +35,6 @@ import { consumeComboFailure } from "./core-combo-failure";
35
35
  import { readDisplaySafeErrorText } from "./core-errors";
36
36
  import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
37
37
  import { formatPassthroughUpstreamError } from "./passthrough-error";
38
- import {
39
- providerModelResponsesTerminalRepair,
40
- providerModelResponsesUpstreamStreaming,
41
- } from "../../providers/registry";
42
38
  import {
43
39
  resolvePassthroughWebSearchBridgeAuth,
44
40
  planPassthroughWebSearchBridge,
@@ -83,7 +79,10 @@ import {
83
79
  import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
84
80
  import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
85
81
  import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
86
- import { createGrokResponsesControlFrameBlockRewrite } from "../grok-responses-control-frame";
82
+ import {
83
+ createGrokResponsesControlFrameBlockRewrite,
84
+ createGrokResponsesTimestampBlockRewrite,
85
+ } from "../grok-responses-control-frame";
87
86
  import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
88
87
  import {
89
88
  createPlaintextV2AgentMessageCallRestoreRewrite,
@@ -99,6 +98,24 @@ import {
99
98
  normalizeDefaultNamespaceInJson,
100
99
  } from "../responses-undeclared-tool-guard";
101
100
  import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
101
+
102
+ /**
103
+ * Platform override for the two relay-path policy calls below. Tests only.
104
+ *
105
+ * The eager relay is reachable only on win32 and darwin, so a Linux shard cannot exercise it
106
+ * without claiming to be one of them. Overwriting `process.platform` globally does that, and a
107
+ * great deal more: every filesystem, ACL and state-directory decision in the process follows it,
108
+ * and the spend-ledger owner lowercases its home on win32, which on a case-sensitive filesystem
109
+ * names a DIFFERENT directory. A row that did that stopped being able to reserve its send and
110
+ * delivered no terminal at all, reporting as a relay defect. This narrows the claim to the two
111
+ * calls that actually choose the relay path.
112
+ */
113
+ let relayPlatformForTests: NodeJS.Platform | undefined;
114
+
115
+ /** Internal test contract, not operator configuration: no config key reaches this. */
116
+ export function setRelayPlatformForTests(platform: NodeJS.Platform | undefined): void {
117
+ relayPlatformForTests = platform;
118
+ }
102
119
  import { linkAbortSignal, UPSTREAM_JSON_BODY_READ_OPTIONS } from "./core-lifetime";
103
120
  import { registerTurn, unregisterTurn, trackStreamLifetime } from "../lifecycle";
104
121
  import { relaySseEagerBounded } from "../relay-eager";
@@ -340,11 +357,7 @@ export async function deliverPassthroughResponse(
340
357
  // before relay starts. Waiting for SSE completion would retain request state across the whole
341
358
  // stream; a later body failure does not undo that this destination accepted and served the turn.
342
359
  commitReasoningReplayServingRoute(nativeExchange.request.headers);
343
- const terminalRepairPolicy = providerModelResponsesTerminalRepair(
344
- route.providerName,
345
- route.provider,
346
- route.modelId,
347
- );
360
+ const terminalRepairPolicy = route.staticPolicy.model.responsesTerminalRepair;
348
361
  // #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
349
362
  // and this branch relays that declaration on the assumption the destination executes it.
350
363
  // A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
@@ -498,6 +511,9 @@ export async function deliverPassthroughResponse(
498
511
  grokClientCompatibilityEnabled
499
512
  ? createGrokResponsesControlFrameBlockRewrite()
500
513
  : undefined,
514
+ grokClientCompatibilityEnabled
515
+ ? createGrokResponsesTimestampBlockRewrite()
516
+ : undefined,
501
517
  grokClientCompatibilityEnabled
502
518
  ? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
503
519
  : undefined,
@@ -529,12 +545,13 @@ export async function deliverPassthroughResponse(
529
545
  ? composeSseBlockRewrites(...blockRewrites)
530
546
  : undefined;
531
547
  const needsClientRewrite = clientBlockRewrite !== undefined;
548
+ const relayPlatform = relayPlatformForTests ?? process.platform;
532
549
  // #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
533
550
  // (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
534
551
  // lost). The eager single reader applies the same rewrites inline.
535
- const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
552
+ const win32EagerRewrite = isWin32EagerRewrite(relayPlatform, needsClientRewrite);
536
553
  const eagerPath = selectEagerPath(
537
- process.platform,
554
+ relayPlatform,
538
555
  needsClientRewrite,
539
556
  config.streamMode ?? "auto",
540
557
  );
@@ -805,7 +822,7 @@ export async function deliverPassthroughResponse(
805
822
  // stream that never closes. Non-streaming clients keep the plain JSON.
806
823
  if (clientRequestedStream === true
807
824
  && options.inboundTransport !== "websocket"
808
- && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
825
+ && route.staticPolicy.model.responsesUpstreamStreaming === false
809
826
  && route.provider.adapter === "openai-responses") {
810
827
  let completed: Record<string, unknown> | undefined;
811
828
  try {
@@ -852,7 +869,7 @@ export async function deliverPassthroughResponse(
852
869
  // WS turns reframe this JSON into events in the bridge, which is the
853
870
  // other relay-free path — normalize ids so both bounded-JSON paths agree.
854
871
  const outboundJson = options.inboundTransport === "websocket"
855
- && providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
872
+ && route.staticPolicy.model.responsesUpstreamStreaming === false
856
873
  && hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
857
874
  ? (() => {
858
875
  try {
@@ -121,6 +121,7 @@ import { recordCodexUpstreamOutcome } from "../../codex/routing";
121
121
  import { describeUpstreamConnectFailure } from "./upstream-error";
122
122
  import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
123
123
  import {
124
+ isOpenCodeGoDestination,
124
125
  rateLimitRetryPolicyFor,
125
126
  rateLimitRetryDelayMs,
126
127
  transientRetryPolicyFor,
@@ -142,6 +143,7 @@ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
142
143
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
143
144
  import {
144
145
  attemptOpaqueBlobRecovery,
146
+ isEncryptedFunctionOutputRejection,
145
147
  outboundResponsesBodyCarriesEncryptedFunctionOutput,
146
148
  resetStreamedOpaqueBlobLogContext,
147
149
  consoleGoUploadRejectionBody,
@@ -841,7 +843,15 @@ export async function preparePassthroughExchange(
841
843
  // retry wrapper replaces — proves the host was reached (#914 review).
842
844
  .then(adoptObservedResponse);
843
845
  },
844
- { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends },
846
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url),
847
+ attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends,
848
+ // The OpenCode Go destination stalls-then-drops inference sends (ambiguous
849
+ // pre-header resets surfacing as refused 429s); its subscription traffic is
850
+ // inference-only, so a bounded reset replay here absorbs the blip instead of
851
+ // failing the turn. Recovery legs keep the fail-closed refusal; only this
852
+ // initial send is replay-eligible. Attempts stay budget-bounded via attempts.
853
+ replaySafe: isOpenCodeGoDestination(route.provider),
854
+ },
845
855
  );
846
856
  } catch (err) {
847
857
  return transportFailureResponse(err);
@@ -863,7 +873,7 @@ export async function preparePassthroughExchange(
863
873
  recovery: AttemptRecoveryKind,
864
874
  ): Promise<Response | { failed: Response }> => {
865
875
  const retryAdapter = resolveSelectionAdapter(
866
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
876
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
867
877
  config.cacheRetention,
868
878
  );
869
879
  if (!("passthrough" in retryAdapter) || !retryAdapter.passthrough) {
@@ -989,7 +999,7 @@ export async function preparePassthroughExchange(
989
999
  route.provider = replay.provider;
990
1000
  requestState.selectedForwardHeaders = withClaudeNativeSession(replay.headers, replay.provider, options.claudeNativeSessionId);
991
1001
  const replayAdapter = resolveSelectionAdapter(
992
- resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
1002
+ resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire, route.staticPolicy),
993
1003
  config.cacheRetention,
994
1004
  );
995
1005
  if (!("passthrough" in replayAdapter) || !replayAdapter.passthrough) {
@@ -1110,7 +1120,7 @@ export async function preparePassthroughExchange(
1110
1120
  );
1111
1121
  route.provider = refreshedProvider;
1112
1122
  const refreshedAdapter = resolveSelectionAdapter(
1113
- resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
1123
+ resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
1114
1124
  config.cacheRetention,
1115
1125
  );
1116
1126
  if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
@@ -1340,8 +1350,16 @@ export async function preparePassthroughExchange(
1340
1350
  // refusal: whatever the entitlement was when upstream declined, it is not that now. Both
1341
1351
  // ids are cleared because the wire model can differ from the routed one.
1342
1352
  if (upstreamResponse.ok) {
1343
- clearCodexModelDenialEvidence(admissionState.authCtx.accountId, route.modelId);
1344
- clearCodexModelDenialEvidence(admissionState.authCtx.accountId, parsed.modelId);
1353
+ clearCodexModelDenialEvidence(
1354
+ admissionState.authCtx.accountId,
1355
+ route.modelId,
1356
+ admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
1357
+ );
1358
+ clearCodexModelDenialEvidence(
1359
+ admissionState.authCtx.accountId,
1360
+ parsed.modelId,
1361
+ admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
1362
+ );
1345
1363
  }
1346
1364
  const model400Denial = await codexPoolAccountModel400Denial(
1347
1365
  upstreamResponse,
@@ -1354,7 +1372,11 @@ export async function preparePassthroughExchange(
1354
1372
  // answer about this model, and the roster cache that selection otherwise reads expires
1355
1373
  // five minutes after a catalog sync fills it -- so without remembering this, the next
1356
1374
  // request selects the same account on quota alone and takes the same 400 (#4906).
1357
- recordCodexModelDenialEvidence(admissionState.authCtx.accountId, model400Denial);
1375
+ recordCodexModelDenialEvidence(
1376
+ admissionState.authCtx.accountId,
1377
+ model400Denial,
1378
+ admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
1379
+ );
1358
1380
  poolRetryOutcome = 400;
1359
1381
  } else if (!admissionState.authCtx.fixedAccount && await shouldRetryCodexPoolAccountQuota(
1360
1382
  upstreamResponse,
@@ -1453,8 +1475,13 @@ export async function preparePassthroughExchange(
1453
1475
  payload => {
1454
1476
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
1455
1477
  const type = (payload as { type?: unknown }).type;
1456
- return (type === "error" || type === "response.failed" || type === "response.incomplete")
1457
- && upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
1478
+ const decryptRejection = (type === "error" || type === "response.failed" || type === "response.incomplete")
1479
+ && isEncryptedFunctionOutputRejection(JSON.stringify(payload));
1480
+ // `detail` is a real WebSocket error shape but the generic preflight failure projector
1481
+ // intentionally understands only Responses error/message fields. Preserve only this
1482
+ // exact identity so the projected 502 can enter the existing single-shot recovery.
1483
+ if (decryptRejection) preflightLog.upstreamError = ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
1484
+ return decryptRejection;
1458
1485
  }, {
1459
1486
  allowMissingContentType: !recoveryContentType && parsed.stream,
1460
1487
  replayReadErrors: true,
@@ -1,6 +1,5 @@
1
1
  import { comboFailureDecision } from "../../combos/failover";
2
2
  import { readBoundedResponseBody } from "../../lib/bounded-body";
3
- import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "../request-decompress";
4
3
  import { finishRequestAttempt, type RequestLogContext } from "../request-log";
5
4
  import { linkRequestSessionLane } from "../request-log-conversation";
6
5
  import type { OcxConfig } from "../../types";
@@ -129,6 +128,7 @@ export async function handleResponsesWithPolicyFallback(
129
128
  const runCore = deps.runCore ?? handleResponsesCore;
130
129
  let requestBodyReadNotified = false;
131
130
  let storedPool401ReplayDispatched = false;
131
+ let rawBody: Record<string, unknown> | null = null;
132
132
  const coreOptions: CoreOptions = {
133
133
  ...options,
134
134
  openAiSidecarAuth: options.openAiSidecarAuth === undefined
@@ -144,23 +144,15 @@ export async function handleResponsesWithPolicyFallback(
144
144
  options.onRequestBodyRead?.();
145
145
  },
146
146
  } : {}),
147
+ onRequestBodyParsed: body => {
148
+ options.onRequestBodyParsed?.(body);
149
+ if (body && typeof body === "object" && !Array.isArray(body)) rawBody = body as Record<string, unknown>;
150
+ },
147
151
  onStoredPool401ReplayDispatched: () => {
148
152
  storedPool401ReplayDispatched = true;
149
153
  options.onStoredPool401ReplayDispatched?.();
150
154
  },
151
155
  };
152
- let rawBody: Record<string, unknown> | null = null;
153
- try {
154
- const parsed = await readJsonRequestBody(
155
- req.clone(),
156
- undefined,
157
- resolveInboundBodyLimitBytes(config.maxInboundBodyBytes),
158
- );
159
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) rawBody = parsed as Record<string, unknown>;
160
- } catch {
161
- // Core owns the client-facing parse/decompression error.
162
- }
163
-
164
156
  let response: Response;
165
157
  try {
166
158
  response = await runCore(req, config, logCtx, coreOptions);
@@ -41,7 +41,6 @@ import {
41
41
  import {
42
42
  copyPreviousResponseReplayProvenance,
43
43
  expandPreviousResponseInput,
44
- previousResponseScopeMismatch,
45
44
  previousResponseReplayFailure,
46
45
  markBodyNonPersistable,
47
46
  previousResponseProviderState,
@@ -56,6 +55,7 @@ import { bindTurnTerminationScope, rememberDeliveredFinalAnswer } from "../../re
56
55
  import { requestLogSpeedLabel, readConfiguredCodexServiceTier } from "../request-log";
57
56
  import type { RouteResult } from "../../router";
58
57
  import {
58
+ captureRouteStaticPolicy,
59
59
  routeConcreteModel,
60
60
  routeCompactionModel,
61
61
  routeModel,
@@ -119,6 +119,7 @@ import {
119
119
  codexLogAccountId,
120
120
  } from "./core-codex-account";
121
121
  import { acquireUpstreamHostAdmission } from "../../codex/upstream-host-health";
122
+ import { applyCompactionRoutingOverride, compactionRoutingKeepsProviderIdentity } from "./compaction-routing";
122
123
  import { codexAuthContextLogLabel } from "../../codex/account-label";
123
124
  import {
124
125
  conversationStateBindingFromAuth,
@@ -149,6 +150,13 @@ export async function prepareResponsesRequest(
149
150
  }
150
151
  return decodeRequestErrorResponse(err, "responses");
151
152
  }
153
+ if (!options.comboAttempt && !options.compactionRoutingOverride && inboundWire === "responses") {
154
+ options.compactionRoutingOverride = applyCompactionRoutingOverride(body, req.headers, config, {
155
+ endpoint: "responses",
156
+ transport: options.inboundTransport,
157
+ });
158
+ }
159
+ options.onRequestBodyParsed?.(body);
152
160
  // An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
153
161
  // as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
154
162
  const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
@@ -180,7 +188,7 @@ export async function prepareResponsesRequest(
180
188
  }
181
189
  // Compaction may send the last client-visible bare model after a combo switch.
182
190
  // Configured selectors take precedence; otherwise recall before combo dispatch (#3891).
183
- if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
191
+ if (!options.comboAttempt && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
184
192
  const rawModel = (body as { model?: unknown }).model;
185
193
  const rawInput = (body as { input?: unknown }).input;
186
194
  const isCompactionTrigger = Array.isArray(rawInput)
@@ -202,7 +210,7 @@ export async function prepareResponsesRequest(
202
210
  // hops — which only exist inside that loop — are unreachable (#4129). Rewrite the selector
203
211
  // here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
204
212
  // LOOKUP so the check can never observe a one-candidate collapse.
205
- if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
213
+ if (!options.comboAttempt && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
206
214
  const shadowIntercept = config.shadowCallIntercept;
207
215
  const rawShadowModel = (body as { model?: unknown }).model;
208
216
  if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
@@ -243,10 +251,13 @@ export async function prepareResponsesRequest(
243
251
  copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
244
252
  } else {
245
253
  body = expandPreviousResponseInput(body, inboundClientThreadId);
246
- if (previousResponseScopeMismatch(body)) {
247
- console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
254
+ const replayFailure = previousResponseReplayFailure(body);
255
+ if (replayFailure?.reason === "scope_mismatch") {
256
+ // Bounded and content-free: no task scope and nothing about the retained entry.
257
+ console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
248
258
  }
249
- if (previousResponseReplayFailure(body)) {
259
+ // Local replay failures require full client replay.
260
+ if (replayFailure) {
250
261
  return formatErrorResponse(
251
262
  400,
252
263
  "previous_response_not_found",
@@ -262,10 +273,14 @@ export async function prepareResponsesRequest(
262
273
  // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
263
274
  // parsing so every consumer sees the payload: parseRequest (routed/translated providers read
264
275
  // the parsed messages) and the native passthrough (_rawBody is this same object, serialized
265
- // verbatim). Genuine backend ciphertext is left byte-identical (looksLikeBackendCiphertext).
276
+ // verbatim). Structurally valid backend ciphertext stays byte-identical; encoded-looking unknown
277
+ // slots remain opaque only until final-route handling can preserve or strip them safely.
266
278
  {
267
279
  const rewritten = sanitizeEncryptedContentInPlace(
268
280
  (body as { input?: unknown } | undefined)?.input,
281
+ // The final destination is not known yet. Keep ambiguous encoded slots opaque until route
282
+ // selection can either strip them for a third party or apply strict native classification.
283
+ { preserveUnknownOpaqueSlots: true },
269
284
  );
270
285
  if (rewritten > 0)
271
286
  console.warn(
@@ -378,7 +393,7 @@ export async function prepareResponsesRequest(
378
393
  if (!logCtx.conversationId) {
379
394
  logCtx.conversationId = resolvedConversationId;
380
395
  }
381
- logCtx.requestedModel = parsed.modelId;
396
+ logCtx.requestedModel = options.compactionRoutingOverride?.sourceModel ?? parsed.modelId;
382
397
  logCtx.requestedEffort = parsed.options.reasoning;
383
398
  // What this request may spend beyond its input, for the durable spend reservation (#4707).
384
399
  // Read from the caller rather than from the adapter's serialized body, because the
@@ -396,19 +411,29 @@ export async function prepareResponsesRequest(
396
411
 
397
412
  let route: RouteResult;
398
413
  let credentialDomainWasRewritten = false;
414
+ const captureInboundRoutePolicy = (candidate: RouteResult): RouteResult => {
415
+ candidate.staticPolicy = captureRouteStaticPolicy(
416
+ candidate.providerName,
417
+ candidate.modelId,
418
+ candidate.provider,
419
+ candidate.staticPolicy.effectiveAlias,
420
+ inboundWire,
421
+ );
422
+ return candidate;
423
+ };
399
424
  try {
400
425
  // A `compaction_trigger` turn may name a bare native model the operator has
401
426
  // no canonical OpenAI route for (#2901). Only the initial compaction route
402
427
  // may fall back to the configured default provider; combo attempts and the
403
428
  // later fallback/recovery re-routes keep the ordinary reservation.
404
- const resolveRoute = (modelId: string) => options.comboAttempt
429
+ const resolveRoute = (modelId: string) => captureInboundRoutePolicy(options.comboAttempt
405
430
  ? routeConcreteModel(config, modelId)
406
431
  : parsed._compactionRequest === true
407
432
  ? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
408
- : routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
433
+ : routeModel(config, modelId, evidenceFromBody(parsed._rawBody)));
409
434
  const _sci = config.shadowCallIntercept;
410
435
  let shadowRoute: RouteResult | undefined;
411
- if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
436
+ if (!options.compactionRoutingOverride && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
412
437
  const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
413
438
  let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
414
439
  try {
@@ -436,8 +461,17 @@ export async function prepareResponsesRequest(
436
461
  shadowRoute = targetRoute;
437
462
  }
438
463
  }
439
- if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
464
+ if (parsed._compactionRequest === true || options.compactionRoutingOverride) parsed._cursorIsolateConversation = true;
440
465
  route = shadowRoute ?? resolveRoute(parsed.modelId);
466
+ if (options.compactionRoutingOverride && !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)) {
467
+ credentialDomainWasRewritten = true;
468
+ // The destination does not share the conversation's credential domain, so it can neither
469
+ // verify the source backend's reasoning ciphertext nor decode its native compaction blob.
470
+ // This is the same condition an account change already reports (account-change-state.ts),
471
+ // and the serializer turns a stored summary into readable text instead of dropping it.
472
+ parsed._stripReasoningEncryptedContent = true;
473
+ if (parsed._compactionRequest === true) parsed._portableCompaction = true;
474
+ }
441
475
  logCtx.routeDecision = route.routeDecision;
442
476
  } catch (err) {
443
477
  if (err instanceof NoAvailableComboTargetsError) {
@@ -633,7 +667,9 @@ export async function prepareResponsesRequest(
633
667
 
634
668
  if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
635
669
  try {
636
- route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
670
+ route = captureInboundRoutePolicy(
671
+ routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody)),
672
+ );
637
673
  credentialDomainWasRewritten = true;
638
674
  logCtx.routeDecision = route.routeDecision;
639
675
  } catch (err) {
@@ -831,7 +867,9 @@ export async function prepareResponsesRequest(
831
867
 
832
868
  if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
833
869
  try {
834
- route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
870
+ route = captureInboundRoutePolicy(
871
+ routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody)),
872
+ );
835
873
  credentialDomainWasRewritten = true;
836
874
  logCtx.routeDecision = route.routeDecision;
837
875
  } catch (err) {
@@ -857,6 +895,17 @@ export async function prepareResponsesRequest(
857
895
 
858
896
  if (options.abortSignal?.aborted) return clientCancelledResponse();
859
897
 
898
+ if (inboundWire === "responses" && isCanonicalOpenAiForwardProvider(route.provider)) {
899
+ const rewritten = sanitizeEncryptedContentInPlace(
900
+ (body as { input?: unknown } | undefined)?.input,
901
+ );
902
+ if (rewritten > 0) {
903
+ console.warn(
904
+ `[opencodex] rewrote ${rewritten} non-Fernet encrypted_content part(s) before canonical native replay`,
905
+ );
906
+ }
907
+ }
908
+
860
909
  // Encrypted child tasks may reach the canonical native backend or an explicitly trusted
861
910
  // direct Responses route. This runs against the FINAL route so native-only fallback can
862
911
  // rescue an incompatible primary without weakening combo behavior.
@@ -902,6 +951,7 @@ export async function prepareResponsesRequest(
902
951
  route.modelId,
903
952
  route.provider,
904
953
  inboundWire,
954
+ route.staticPolicy,
905
955
  );
906
956
  if (wireProvider.adapter === "openai-responses" && !isCanonicalOpenAiForwardProvider(wireProvider)) {
907
957
  const repaired = stripAgentMessageCiphertextInPlace((body as { input?: unknown } | undefined)?.input);
@@ -925,12 +975,12 @@ export async function prepareResponsesRequest(
925
975
  return formatErrorResponse(
926
976
  400,
927
977
  "previous_response_not_found",
928
- "OpenAI forward continuation state is unavailable or expired; resend the full conversation without previous_response_id.",
978
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
929
979
  );
930
980
  }
931
981
 
932
982
  if (hasUnexpandedPreviousResponse) {
933
- const continuationProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
983
+ const continuationProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy);
934
984
  // Can the DESTINATION see the history this process failed to restore? Only the native
935
985
  // Responses passthrough can: it forwards previous_response_id to a backend that stored the
936
986
  // chain. Every translated wire rebuilds the conversation from this request's input alone —
@@ -951,7 +1001,7 @@ export async function prepareResponsesRequest(
951
1001
  return formatErrorResponse(
952
1002
  400,
953
1003
  "previous_response_not_found",
954
- "Routed continuation requires unavailable local history; resend the full conversation without previous_response_id.",
1004
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
955
1005
  );
956
1006
  }
957
1007
  }
@@ -109,8 +109,12 @@ export function createResponsesSendBudget(
109
109
  const noteAdapterPhysicalSend = (
110
110
  inputTokens: number | undefined,
111
111
  send: { ordinal: number; recovery?: AttemptRecoveryKind },
112
+ options: { readonly includeFirst?: boolean } = {},
112
113
  ): void => {
113
- if (send.ordinal <= 1) return;
114
+ // Ordinal 1 is skipped because the caller normally records it before dispatch. An adapter
115
+ // that reports every send asks for it to be counted here instead, so that the first send is
116
+ // logged where it actually happens rather than before admission could still refuse it.
117
+ if (send.ordinal <= 1 && options.includeFirst !== true) return;
114
118
  noteAttemptSend(logCtx.activeAttempt, inputTokens, send.recovery);
115
119
  };
116
120
  /**
@@ -50,7 +50,7 @@ export async function prepareResponsesSidecarAuth(
50
50
  let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
51
51
  const visionDescribeTerminal = options.visionDescribeTerminal === true;
52
52
  const routedCompaction = parsed._compactionRequest === true
53
- && !isCanonicalOpenAiForwardProvider(route.provider);
53
+ && (!isCanonicalOpenAiForwardProvider(route.provider) || parsed._portableCompaction === true);
54
54
  const needsOpenAiVision = !visionDescribeTerminal
55
55
  && shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed, route.providerName);
56
56
  const needsOpenAiSearch = !routedCompaction && !transportState.adapter.runTurn