@bitkyc08/opencodex 2.65.0 → 2.66.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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-DyHlFeNB.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
  5. package/gui/dist/assets/index-BiwnVAUm.js +86 -0
  6. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  7. package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-DGLte6IR.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
@@ -1,6 +1,6 @@
1
1
  import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
2
- import { chatBodyCarriesImage, chatBodyCarriesToolResultImage } from "../chat/image-parts";
3
2
  import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
3
+ import { isNativeChatRouteEligible } from "./chat-native-eligibility";
4
4
  import {
5
5
  chatCompletionsErrorBody,
6
6
  chatCompletionsErrorResponse,
@@ -15,12 +15,13 @@ import {
15
15
  CYBER_POLICY_ERROR_CODE,
16
16
  isCyberPolicyCode,
17
17
  isCyberPolicyMessage,
18
+ SEND_BUDGET_EXHAUSTED_CODE,
18
19
  } from "../lib/errors";
20
+ import type { RequestExecutionBudget } from "../lib/request-execution-budget";
19
21
  import type { AdmissionLease } from "../lib/admission";
20
22
  import { readBoundedResponseBody } from "../lib/bounded-body";
21
23
  import { redactSecretString } from "../lib/redact";
22
24
  import { resolveClientRetryAfter } from "../lib/retry-after";
23
- import { isModelTextOnly, requiresVisionPreprocessing } from "../vision";
24
25
  import {
25
26
  applyUpstreamRecoveryInit,
26
27
  fetchWithResetRetry,
@@ -32,6 +33,8 @@ import {
32
33
  REPLAY_REFUSAL_CLIENT_HEADERS,
33
34
  REPLAY_REFUSED_STATUS,
34
35
  retainReplayRefusal,
36
+ SendBudgetExhaustedError,
37
+ TRANSIENT_RETRY_MAX_ATTEMPTS,
35
38
  UpstreamRetryEvidenceError,
36
39
  type UpstreamSendRecovery,
37
40
  UPSTREAM_RESET_REPLAY_REFUSED_CODE,
@@ -58,20 +61,23 @@ import type { OcxConfig, OcxProviderConfig } from "../types";
58
61
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
59
62
  import { linkAbortSignal } from "./responses";
60
63
  import {
61
- addFinalRequestLog,
62
- beginRequestAttempt,
63
64
  noteProviderAttemptSend,
64
65
  recordKeyAttemptFailure,
65
66
  recordKeyWireAttemptUsage,
66
67
  recordFirstOutput,
67
68
  recordAttemptCredentialSource,
68
- sealRequestAttemptIdentity,
69
69
  type RequestLogContext,
70
70
  } from "./request-log";
71
71
  import { jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
72
+ import { beginInferenceAttempt, type InferenceAttempt } from "./inference/attempt";
73
+ import { createFinalRequestLog } from "./inference/final-log";
72
74
  import { registerTurn, unregisterTurn } from "./lifecycle";
73
75
  import { attachRequestSpendTracker } from "./responses/request-spend";
74
76
  import { workflowRefusalResponse } from "./workflow-refusal";
77
+ import type { ComboProtocolSource } from "./responses/core-combo-native";
78
+ import type { ProtocolEnvelope } from "../protocols/envelope";
79
+
80
+ export { isNativeChatRouteEligible, nativeChatDeclineReason } from "./chat-native-eligibility";
75
81
 
76
82
  type Rec = Record<string, unknown>;
77
83
 
@@ -154,50 +160,12 @@ function isRec(value: unknown): value is Rec {
154
160
  return value !== null && typeof value === "object" && !Array.isArray(value);
155
161
  }
156
162
 
157
- export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec, config?: OcxConfig): boolean {
158
- const provider = route.provider;
159
- if (provider.adapter !== "openai-chat") return false;
160
- if (provider.authMode !== undefined && provider.authMode !== "key" && provider.authMode !== "local") return false;
161
- // Combo and policy execution own multi-candidate retries in the Responses pipeline.
162
- if (route.combo || route.routeKind === "combo" || route.routeKind === "policy") return false;
163
- if (rawBody.store === true || rawBody.background === true) return false;
164
- if (typeof rawBody.previous_response_id === "string" && rawBody.previous_response_id.length > 0) return false;
165
- if (rawBody.compaction_trigger !== undefined) return false;
166
- // A standard Chat tool message accepts a string or text parts, not image_url, so
167
- // normalizing a Pi/Anthropic tool image into image_url is not enough on its own —
168
- // the part is still inside a tool message. The translated adapter already places
169
- // tool-result images in a following user carrier after the complete paired batch
170
- // (flushToolResultImages), so divert these requests there. Ordinary user images and
171
- // text-only tool results keep the native fast path.
172
- if (chatBodyCarriesToolResultImage(rawBody)) return false;
173
- // Vision sidecar coverage (roadmap 180): a text-only routed model with an
174
- // image-bearing body must go through the Responses pipeline, whose plan
175
- // site describes or strips the image. The native fast path has no vision
176
- // handling, so letting it keep such a request forwards raw pixels to a
177
- // model the operator declared blind.
178
- if (chatBodyCarriesImage(rawBody)) {
179
- const needsVision = config
180
- ? requiresVisionPreprocessing(config, provider, route.modelId, route.providerName)
181
- : isModelTextOnly(provider, route.modelId);
182
- if (needsVision) return false;
183
- }
184
- if (Array.isArray(rawBody.tools)) {
185
- for (const tool of rawBody.tools) {
186
- if (!isRec(tool)) continue;
187
- if (tool.type === "web_search" || tool.type === "web_search_preview" || tool.type === "image_generation") {
188
- return false;
189
- }
190
- }
191
- }
192
- return true;
193
- }
194
-
195
163
  function chatCompletionJson(value: unknown): Rec | null {
196
164
  if (!isRec(value) || !Array.isArray(value.choices) || value.choices.length === 0) return null;
197
165
  return value;
198
166
  }
199
167
 
200
- interface HandleNativeChatOptions {
168
+ export interface HandleNativeChatOptions {
201
169
  req: Request;
202
170
  config: OcxConfig;
203
171
  logCtx: RequestLogContext;
@@ -209,36 +177,108 @@ interface HandleNativeChatOptions {
209
177
  translatorBudget: TranslatorBudget;
210
178
  }
211
179
 
180
+ /** Records the outcome on whichever final row owns this attempt; the first call wins. */
181
+ export type NativeChatFinishLog = (
182
+ status: number,
183
+ message?: string,
184
+ closeReason?: "non_stream" | "terminal" | "client_cancel",
185
+ ) => void;
186
+
187
+ export interface NativeChatExecution extends HandleNativeChatOptions {
188
+ finishLog: NativeChatFinishLog;
189
+ /**
190
+ * A combo child's per-target budget (PF-07). With it the attempt opens no spend tracker of its
191
+ * own: the combo's hop reservation already booked the first send on the request's shared
192
+ * counter, and every later physical send is reported to that counter, whose observer is the
193
+ * request's one spend tracker. A second tracker would book each send twice and, merged into
194
+ * the parent row, replace the one the final log settles.
195
+ */
196
+ sendBudget?: RequestExecutionBudget;
197
+ /** Replaces the request-relative first-output mark; a combo child records its own. */
198
+ onFirstOutput?: () => void;
199
+ /** The lease a streamed body holds; defaults to `logIds.turnAdmissionLease`. */
200
+ turnAdmissionLease?: AdmissionLease;
201
+ }
202
+
203
+ /**
204
+ * The native Chat ingress: opens the attempt on the request's log context and owns the
205
+ * request's final log row, then runs the attempt.
206
+ */
212
207
  export async function handleNativeChatCompletions(options: HandleNativeChatOptions): Promise<Response> {
213
- const { req, config, logCtx, logIds, route, requestedModel, requestedStream, translatorBudget } = options;
208
+ const { logCtx, logIds, route } = options;
214
209
  logCtx.inboundProtocol = "chat";
215
- const attempt = beginRequestAttempt(
216
- (logCtx.attempts?.length ?? 0) + 1,
217
- route.providerName,
218
- route.modelId,
219
- "openai-chat",
220
- );
221
- logCtx.activeAttempt = attempt;
222
- logCtx.activeAttemptStartedAt = Date.now();
223
- (logCtx.attempts ??= []).push(attempt);
224
- sealRequestAttemptIdentity(attempt, route.providerName, "openai-chat", logCtx.accountLogLabel);
210
+ const attemptHandle = beginInferenceAttempt(logCtx, {
211
+ provider: route.providerName,
212
+ model: route.modelId,
213
+ adapter: "openai-chat",
214
+ });
215
+ attemptHandle.seal(logCtx.accountLogLabel);
225
216
 
226
- let logged = false;
227
- const finishLog = (status: number, message?: string, closeReason: "non_stream" | "terminal" | "client_cancel" = "non_stream") => {
228
- if (logged) return;
229
- logged = true;
217
+ const finalLog = createFinalRequestLog(logIds, logCtx);
218
+ const finishLog: NativeChatFinishLog = (status, message, closeReason = "non_stream") => {
219
+ if (finalLog.finished()) return;
220
+ // The failure text lands on the context before the row is written, and only once.
230
221
  if (message) logCtx.upstreamError = redactSecretString(message).slice(0, 500);
231
- if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason });
222
+ finalLog.finish(status, { closeReason });
232
223
  };
224
+ return runNativeChatAttempt({ ...options, finishLog }, attemptHandle);
225
+ }
226
+
227
+ /**
228
+ * The Chat source a combo hands its native children (PF-07). A child runs on the attempt the
229
+ * combo opened and reports through the combo's callbacks, so the final row stays the parent's.
230
+ */
231
+ export function createNativeChatComboSource(input: {
232
+ req: Request;
233
+ config: OcxConfig;
234
+ envelope: ProtocolEnvelope;
235
+ requestedModel: string;
236
+ requestedStream: boolean;
237
+ translatorBudget: TranslatorBudget;
238
+ }): ComboProtocolSource {
239
+ return {
240
+ inbound: "chat",
241
+ envelope: input.envelope,
242
+ dispatchNativeChild: child => runNativeChatAttempt({
243
+ req: input.req,
244
+ config: input.config,
245
+ logCtx: child.childLog,
246
+ route: child.route,
247
+ chatBody: child.body,
248
+ requestedModel: input.requestedModel,
249
+ requestedStream: input.requestedStream,
250
+ translatorBudget: input.translatorBudget,
251
+ finishLog: child.finishLog,
252
+ sendBudget: child.sendBudget,
253
+ onFirstOutput: child.onFirstOutput,
254
+ ...(child.turnAdmissionLease ? { turnAdmissionLease: child.turnAdmissionLease } : {}),
255
+ }, child.attemptHandle),
256
+ };
257
+ }
258
+
259
+ /**
260
+ * One native Chat attempt on an already-open attempt row: effort normalization, the send
261
+ * loop with key failover and 429 replay, relay and usage. Every outcome is reported through
262
+ * `execution.finishLog`, so the caller decides which final row it lands on.
263
+ */
264
+ export async function runNativeChatAttempt(
265
+ execution: NativeChatExecution,
266
+ attemptHandle: InferenceAttempt,
267
+ ): Promise<Response> {
268
+ const { req, config, logCtx, logIds, route, requestedModel, requestedStream, translatorBudget, finishLog } = execution;
269
+ const { sendBudget } = execution;
270
+ const { attempt } = attemptHandle;
271
+ const onFirstOutput = execution.onFirstOutput
272
+ ?? (logIds ? () => recordFirstOutput(logCtx, logIds.start) : undefined);
233
273
  const fail = (status: number, message: string, type?: string, code?: string | null): Response => {
234
274
  const safeMessage = redactSecretString(message);
235
275
  finishLog(status, safeMessage);
236
276
  return chatCompletionsErrorResponse(status, safeMessage, type, code);
237
277
  };
238
278
 
239
- normalizePinnedChatEffort(options);
240
- logCtx.requestedServiceTier = typeof options.chatBody.service_tier === "string"
241
- ? options.chatBody.service_tier
279
+ normalizePinnedChatEffort(execution);
280
+ logCtx.requestedServiceTier = typeof execution.chatBody.service_tier === "string"
281
+ ? execution.chatBody.service_tier
242
282
  : undefined;
243
283
 
244
284
  const upstream = new AbortController();
@@ -248,7 +288,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
248
288
  // of adding another trackStreamLifetime wrapper (unsafe on bundled Bun#32111).
249
289
  let streamTurnRegistered = false;
250
290
  const transferTurnToStream = () => {
251
- const lease = logIds?.turnAdmissionLease;
291
+ const lease = execution.turnAdmissionLease ?? logIds?.turnAdmissionLease;
252
292
  if (!lease || typeof (lease as { bindAbortController?: unknown }).bindAbortController !== "function") return;
253
293
  registerTurn(upstream, lease);
254
294
  streamTurnRegistered = true;
@@ -269,7 +309,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
269
309
  if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
270
310
  let activeProvider: OcxProviderConfig = route.provider;
271
311
  stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
272
- const spendTracker = attachRequestSpendTracker(req, logCtx);
312
+ const spendTracker = sendBudget ? undefined : attachRequestSpendTracker(req, logCtx);
273
313
  let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
274
314
  let activeRequest: AdapterRequest;
275
315
  let retainedRequestBytes = 0;
@@ -287,7 +327,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
287
327
  recordAttemptCredentialSource(attempt, route.providerName, activeProvider, activeAdapter.name);
288
328
  return buildOpenAIChatPassthroughRequest(
289
329
  activeProvider,
290
- options.chatBody,
330
+ execution.chatBody,
291
331
  route.modelId,
292
332
  requestedStream,
293
333
  fastPolicyForModel(activeProvider, route.modelId, route.providerName, "chat"),
@@ -311,9 +351,21 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
311
351
  // key rotation so recovery cannot replace the ceiling along with the active credential.
312
352
  const requestTransientPolicy = transientRetryPolicyFor(activeProvider);
313
353
  let transientSendsUsed = 0;
314
- const remainingTransientSends = (): number => requestTransientPolicy
315
- ? Math.max(0, requestTransientPolicy.attempts - transientSendsUsed)
316
- : Number.POSITIVE_INFINITY;
354
+ // A combo child also answers to the request's shared base allowance, at the cap its own ladder
355
+ // uses. Its first send is exempt: the combo reserved it before dispatching this target.
356
+ const sharedSendCap = requestTransientPolicy?.attempts ?? TRANSIENT_RETRY_MAX_ATTEMPTS;
357
+ let physicalSends = 0;
358
+ const remainingSharedSends = (): number => {
359
+ if (!sendBudget) return Number.POSITIVE_INFINITY;
360
+ const remaining = sendBudget.remainingBaseSends(sharedSendCap);
361
+ return physicalSends === 0 ? Math.max(1, remaining) : remaining;
362
+ };
363
+ const remainingTransientSends = (): number => Math.min(
364
+ requestTransientPolicy
365
+ ? Math.max(0, requestTransientPolicy.attempts - transientSendsUsed)
366
+ : Number.POSITIVE_INFINITY,
367
+ remainingSharedSends(),
368
+ );
317
369
  const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
318
370
 
319
371
  const send = async (request: AdapterRequest, recovery?: "rate-limit-429" | "key-429"): Promise<Response> => {
@@ -321,6 +373,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
321
373
  // #2643: opted-in key-auth openai-chat providers retry pre-stream transient statuses on
322
374
  // the native chat lane too; everyone else keeps reset-only semantics.
323
375
  const remaining = remainingTransientSends();
376
+ if (sendBudget && remaining <= 0) throw new SendBudgetExhaustedError(safeHostLabel(request.url));
324
377
  if (requestTransientPolicy && remaining <= 0) {
325
378
  throw new Error("native Chat transient send budget exhausted before recovery dispatch");
326
379
  }
@@ -343,7 +396,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
343
396
  dispatchOverride: async (_input, init, execute) => {
344
397
  if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
345
398
  const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
346
- if (!current || !isNativeChatRouteEligible({ ...route, provider: current }, options.chatBody, config)) {
399
+ if (!current || !isNativeChatRouteEligible({ ...route, provider: current }, execution.chatBody, config)) {
347
400
  throw new Error("Provider key selection is no longer available for native Chat");
348
401
  }
349
402
  activeProvider = current;
@@ -362,7 +415,15 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
362
415
  const encoding = new Headers(init.headers).get("accept-encoding");
363
416
  if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
364
417
  if (init.signal?.aborted) throw init.signal.reason;
365
- if (!spendTracker.charge()) throw new NativeChatSpendRefusal();
418
+ if (sendBudget) {
419
+ // Backstop for sends the helper cannot see coming (a reset replay). The first
420
+ // report settles the combo's booking; each later one is charged and booked.
421
+ if (physicalSends > 0 && sendBudget.remainingBaseSends(sharedSendCap) <= 0) {
422
+ throw new SendBudgetExhaustedError(safeHostLabel(request.url));
423
+ }
424
+ physicalSends += 1;
425
+ sendBudget.used += 1;
426
+ } else if (!spendTracker?.charge()) throw new NativeChatSpendRefusal();
366
427
  noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
367
428
  // A reselected provider transport is still a physical send: the connection policy
368
429
  // and manual-redirect ownership wrap the selected implementation (#4992).
@@ -433,7 +494,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
433
494
  now: Date.now(),
434
495
  attemptedKey: activeProvider.apiKey,
435
496
  attemptedSelection: activeProvider._apiKeyAttempt,
436
- promptCacheKey: typeof options.chatBody.prompt_cache_key === "string" ? options.chatBody.prompt_cache_key : undefined,
497
+ promptCacheKey: typeof execution.chatBody.prompt_cache_key === "string" ? execution.chatBody.prompt_cache_key : undefined,
437
498
  });
438
499
  if (!rotated) break;
439
500
  // Rotation also records the failed key's cooldown and persists the next healthy key.
@@ -455,6 +516,10 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
455
516
  upstream.abort();
456
517
  if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
457
518
  const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
519
+ if (sendBudget && sendError instanceof SendBudgetExhaustedError) {
520
+ // A decision this process made, answered as the Responses path answers it: 429, not 502.
521
+ return fail(429, sendError.message, SEND_BUDGET_EXHAUSTED_CODE, SEND_BUDGET_EXHAUSTED_CODE);
522
+ }
458
523
  if (sendError instanceof NativeChatSpendRefusal) {
459
524
  const refusal = workflowRefusalResponse("workflow-spend-exhausted", logCtx);
460
525
  finishLog(429);
@@ -569,7 +634,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
569
634
  translatorBudget,
570
635
  signal: upstream.signal,
571
636
  stallTimeoutSec: config.stallTimeoutSec,
572
- onFirstOutput: logIds ? () => recordFirstOutput(logCtx, logIds.start) : undefined,
637
+ onFirstOutput,
573
638
  onUsage: usage => {
574
639
  if (!recordKeyWireAttemptUsage(logCtx, usage)) {
575
640
  logCtx.usage = usage;
@@ -669,7 +734,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
669
734
  attempt.usage = usage;
670
735
  }
671
736
  }
672
- if (logIds) recordFirstOutput(logCtx, logIds.start);
737
+ onFirstOutput?.();
673
738
  try {
674
739
  const serialized = requestedStream
675
740
  ? jsonCompletionSse(completion, requestedModel, translatorBudget)
@@ -18,7 +18,8 @@ import { sseFieldValue } from "../lib/sse-decoder";
18
18
  import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
19
19
  import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
20
20
  import { createToolCallIdAllocator } from "../adapters/tool-call-id";
21
- import { AnthropicRequestError, DesktopModelMappingUnavailableError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
21
+ import { messagesToResponsesTranslation } from "../protocols/codecs/messages";
22
+ import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
22
23
  import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
23
24
  import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
24
25
  import { recordDesktopRequest } from "../claude/desktop-health";
@@ -49,7 +50,8 @@ import { evidenceFromBody } from "../routing/request-evidence";
49
50
  import { resolveWireProtocolOverride } from "./adapter-resolve";
50
51
  import type { OcxConfig } from "../types";
51
52
  import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
52
- import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
53
+ import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext } from "./request-log";
54
+ import { createFinalRequestLog } from "./inference/final-log";
53
55
  import {
54
56
  conversationIdFromClaudeMetadata,
55
57
  getOrAllocateRequestSessionLane,
@@ -58,7 +60,21 @@ import {
58
60
  sessionLaneIdFromRequest,
59
61
  } from "./request-log-conversation";
60
62
  import { responseWithDeferredRequestLog } from "./relay";
63
+ import { clientWireOf } from "./inference/client-wire";
64
+ import { directEncodersApply } from "./inference/client-encoder-delivery";
65
+ import type { ClientEncoderOption } from "./responses/core-options";
61
66
  import { handleResponses } from "./responses";
67
+ import { upstreamWireForAdapter } from "../protocols/contract";
68
+ import { createProtocolEnvelope, type ProtocolEnvelope } from "../protocols/envelope";
69
+ import { featuresFromMessagesBody, type ProtocolFeature } from "../protocols/features";
70
+ import { credentialDomainFor, messagesBodyHasOpaqueState } from "../protocols/opaque-state";
71
+ import { hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
72
+ import { checkRepresentable, unrepresentableMessage } from "../protocols/guard";
73
+ import { requestPathForLane } from "../protocols/path";
74
+ import { resolveApiSurfaceSettings, resolveProtocolSettings } from "../protocols/settings";
75
+ import { markProtocolBlocked, markProtocolEntry } from "../protocols/trace";
76
+ import { recordProtocolShadowPlan } from "../protocols/shadow-plan";
77
+ import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility";
62
78
  import {
63
79
  isApiAuthRequired,
64
80
  isDataPlaneAdmissionSecret,
@@ -148,11 +164,19 @@ export function buildClaudeReplayConfig(config: OcxConfig): OcxConfig {
148
164
  };
149
165
  }
150
166
 
167
+ /**
168
+ * Messages exposure, shared by /v1/messages and /v1/messages/count_tokens so the two can never
169
+ * disagree. `resolveApiSurfaceSettings` is the only reader: an explicit
170
+ * `apiSurfaces.messages.enabled` wins, a malformed one closes the surface, and absence inherits
171
+ * `claudeCode.enabled`.
172
+ */
151
173
  function claudeInboundDisabled(config: OcxConfig): Response | null {
152
- if (config.claudeCode?.enabled === false) {
153
- return anthropicErrorResponse(403, "Claude inbound is disabled (GUI: Claude ON toggle / config.claudeCode.enabled)", "permission_error");
154
- }
155
- return null;
174
+ const messages = resolveApiSurfaceSettings(config).messages;
175
+ if (messages.enabled) return null;
176
+ const detail = messages.source === "invalid"
177
+ ? "config.apiSurfaces.messages is not a valid setting, so the surface stays closed"
178
+ : "GUI: API page Messages toggle / config.apiSurfaces.messages.enabled / config.claudeCode.enabled";
179
+ return anthropicErrorResponse(403, `Messages API is disabled (${detail})`, "permission_error");
156
180
  }
157
181
 
158
182
  async function readAnthropicBody(req: Request, budget: TranslatorBudget, maxBytes: number): Promise<unknown> {
@@ -234,7 +258,7 @@ function uuidFromHex(hex32: string): string {
234
258
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
235
259
  }
236
260
 
237
- function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
261
+ export function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
238
262
  if (!usage) return undefined;
239
263
  const num = (v: unknown) => typeof v === "number" ? v : 0;
240
264
  const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
@@ -428,7 +452,7 @@ export function tapAnthropicSseForLog(
428
452
  * An empty id has no representable wire form, and forwarding `""` is what Anthropic
429
453
  * rejects (#1767), so the request fails locally with a 400 before any upstream fetch.
430
454
  */
431
- function sanitizePassthroughToolCallIds(messages: unknown[]): void {
455
+ export function sanitizePassthroughToolCallIds(messages: unknown[]): void {
432
456
  const blocks: Rec[] = [];
433
457
  for (const message of messages) {
434
458
  if (!isRec(message) || !Array.isArray(message.content)) continue;
@@ -466,12 +490,7 @@ async function anthropicNativePassthrough(
466
490
  logCtx.model = model;
467
491
  logCtx.provider = "anthropic-native";
468
492
  logCtx.requestedModel = model;
469
- let logged = false;
470
- const finalize = (status: number, meta: { closeReason: PassthroughCloseReason | "non_stream" }) => {
471
- if (!logIds || logged) return;
472
- logged = true;
473
- addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
474
- };
493
+ const finalize = createFinalRequestLog(logIds, logCtx).finish;
475
494
 
476
495
  const base = (config.claudeCode?.anthropicBaseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
477
496
  const search = new URL(req.url).search;
@@ -718,6 +737,7 @@ async function handleClaudeMessagesWithBudget(
718
737
  logCtx.surface = "claude";
719
738
  const disabled = claudeInboundDisabled(config);
720
739
  if (disabled) {
740
+ markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: ["surface-disabled"] });
721
741
  if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
722
742
  return disabled;
723
743
  }
@@ -731,6 +751,9 @@ async function handleClaudeMessagesWithBudget(
731
751
  let effortRow: ParsedEffortRowId | null = null;
732
752
  let fastRow: ParsedFastRowId | null = null;
733
753
  let requestedModel = "";
754
+ // Built only under the reject policy; the legacy default leaves this request untouched.
755
+ let envelope: ProtocolEnvelope | undefined;
756
+ let messagesFeatures: () => Iterable<ProtocolFeature> = () => [];
734
757
  try {
735
758
  anthropicBody = await readAnthropicBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
736
759
  // Defensive [1m] strip (devlog 138): clients normally remove the context-variant
@@ -797,7 +820,21 @@ async function handleClaudeMessagesWithBudget(
797
820
  // caller's body with the caller's credential and never runs the Anthropic adapter, so the
798
821
  // proxy-owned `speed` + beta (anthropic-speed wire) and its usage.speed observation would be
799
822
  // silently skipped. Translation reaches the adapter, which owns both.
823
+ const messagesBody = anthropicBody;
824
+ if (isRec(messagesBody) && resolveProtocolSettings(config).unrepresentable === "reject") {
825
+ envelope = createProtocolEnvelope({ inbound: "messages", body: messagesBody, translatorBudget });
826
+ }
827
+ const sourceEnvelope = envelope;
828
+ // The bridge entry mark below reads these before an effort override rewrites `thinking`,
829
+ // which also fixes the envelope's cached features on the caller's own settings.
830
+ // Scanned once, like the envelope's cache, so a later mark reports the same features.
831
+ let scannedFeatures: ReadonlySet<ProtocolFeature> | undefined;
832
+ messagesFeatures = sourceEnvelope
833
+ ? () => sourceEnvelope.features()
834
+ : () => (scannedFeatures ??= featuresFromMessagesBody(messagesBody));
800
835
  if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model, cc)) {
836
+ markProtocolEntry(logCtx, { inbound: "messages", lane: "native", features: messagesFeatures });
837
+ recordProtocolShadowPlan(logCtx, config, { inbound: "messages", model: requestedModel });
801
838
  return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
802
839
  }
803
840
  // Capture source semantics before effort rewriting or translation drops fields.
@@ -814,6 +851,7 @@ async function handleClaudeMessagesWithBudget(
814
851
  anthropicBeta: req.headers.get("anthropic-beta") ?? undefined,
815
852
  });
816
853
  if (compatibility.decision === "reject") {
854
+ markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: ["compatibility-reject"], features: messagesFeatures });
817
855
  logCtx.errorCode = "claude_compatibility_unsupported";
818
856
  if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
819
857
  return anthropicErrorResponse(400, compatibility.reason!, "invalid_request_error");
@@ -826,6 +864,14 @@ async function handleClaudeMessagesWithBudget(
826
864
  };
827
865
  }
828
866
  }
867
+ // Features are read here, before an effort override rewrites the thinking settings.
868
+ markProtocolEntry(logCtx, {
869
+ inbound: "messages",
870
+ lane: "bridge",
871
+ reasonCodes: effortRow ? ["effort-row"] : fastRow ? ["fast-row"] : [],
872
+ features: messagesFeatures,
873
+ });
874
+ recordProtocolShadowPlan(logCtx, config, { inbound: "messages", model: requestedModel });
829
875
  if (isRec(anthropicBody) && effortOverride) {
830
876
  anthropicBody.output_config = {
831
877
  ...(isRec(anthropicBody.output_config) ? anthropicBody.output_config : {}),
@@ -833,7 +879,7 @@ async function handleClaudeMessagesWithBudget(
833
879
  };
834
880
  delete anthropicBody.thinking;
835
881
  }
836
- const translation = anthropicToResponsesTranslation(anthropicBody, cc, translatorBudget);
882
+ const translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
837
883
  internalBody = translation.body;
838
884
  // The Anthropic translator builds its body from model/input/store/stream plus sampling
839
885
  // fields only, so the caller intent is applied to the TRANSLATED body rather than the
@@ -877,9 +923,11 @@ async function handleClaudeMessagesWithBudget(
877
923
  // the translated Anthropic SSE into a message JSON for non-streaming clients.
878
924
  internalBody.stream = true;
879
925
 
926
+ let clientEncoder: ClientEncoderOption | undefined;
880
927
  // Native ChatGPT passthrough (openai-responses forward) accepts only Codex-shaped
881
928
  // bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens",
882
929
  // verified live 2026-07-11). Strip them for that route; routed providers keep them.
930
+ let settledRoute: ReturnType<typeof routeModel> | undefined;
883
931
  try {
884
932
  const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
885
933
  // Same reason as the native Chat lane: this route can be sent from here, so
@@ -896,6 +944,10 @@ async function handleClaudeMessagesWithBudget(
896
944
  );
897
945
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
898
946
  logCtx.routeDecision = route.routeDecision;
947
+ settledRoute = route;
948
+ if (directEncodersApply(config, route)) {
949
+ clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor() };
950
+ }
899
951
  if (route.provider.adapter === "openai-responses") {
900
952
  delete internalBody.max_output_tokens;
901
953
  delete internalBody.temperature;
@@ -939,6 +991,70 @@ async function handleClaudeMessagesWithBudget(
939
991
  /* unknown model: let handleResponses shape the 404 */
940
992
  }
941
993
 
994
+ // PF-08: a managed-key Anthropic route sends its Messages body natively. The caller-forward
995
+ // passthrough above was decided on the caller's own credential and never reaches this point.
996
+ const nativeSelector: NativeMessagesSelector = {
997
+ effortRow: !!effortRow, fastRow: !!fastRow, routeSelector: String(internalBody.model ?? ""), claudeCode: cc,
998
+ // PF-10: a stored second OAuth account turns on the bridge's rotation, so it stays there.
999
+ ...(settledRoute?.provider.authMode === "oauth" ? { oauthFailoverQuorum: hasAnthropicFailoverQuorum() } : {}),
1000
+ };
1001
+ const nativeDecline = settledRoute && isRec(anthropicBody)
1002
+ ? nativeMessagesDeclineReason(settledRoute, anthropicBody, config, nativeSelector)
1003
+ : "rollout-disabled";
1004
+ const nativeMessagesRoute = settledRoute && nativeDecline === undefined ? settledRoute : undefined;
1005
+ // With the switch on, a declined route says why on its bridge mark, as native Chat does.
1006
+ if (nativeDecline && nativeDecline !== "rollout-disabled") {
1007
+ markProtocolEntry(logCtx, {
1008
+ inbound: "messages",
1009
+ lane: "bridge",
1010
+ reasonCodes: [...(effortRow ? ["effort-row" as const] : fastRow ? ["fast-row" as const] : []), nativeDecline],
1011
+ features: messagesFeatures,
1012
+ });
1013
+ }
1014
+ // Combo and policy children are judged per candidate (PF-07); an unknown model has no route.
1015
+ if (envelope && settledRoute && !settledRoute.combo && settledRoute.routeKind !== "policy") {
1016
+ const verdict = checkRepresentable({
1017
+ inbound: "messages",
1018
+ requestPath: requestPathForLane("messages", nativeMessagesRoute ? "native" : "bridge", upstreamWireForAdapter(settledRoute.provider.adapter)),
1019
+ features: envelope.features(),
1020
+ policy: "reject",
1021
+ });
1022
+ if (!verdict.ok) {
1023
+ markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: verdict.reasonCodes, features: verdict.features });
1024
+ logCtx.errorCode = "unsupported_feature";
1025
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
1026
+ return anthropicErrorResponse(400, unrepresentableMessage(verdict.features), "invalid_request_error");
1027
+ }
1028
+ }
1029
+ // PF-10: opaque thinking state reaches only first-party Anthropic; under `reject` a native
1030
+ // route to anyone else refuses the request instead of sending it without that state.
1031
+ if (envelope && nativeMessagesRoute && !credentialDomainFor(nativeMessagesRoute.provider)?.firstPartyAnthropic
1032
+ && messagesBodyHasOpaqueState(anthropicBody as Rec)) {
1033
+ markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: ["feature-unrepresentable", "opaque-state-stripped"], features: messagesFeatures });
1034
+ logCtx.errorCode = "unsupported_feature";
1035
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
1036
+ return anthropicErrorResponse(400, "The selected route cannot carry thinking signatures or redacted_thinking blocks", "invalid_request_error");
1037
+ }
1038
+ if (nativeMessagesRoute) {
1039
+ markProtocolEntry(logCtx, { inbound: "messages", lane: "native", features: messagesFeatures });
1040
+ let nativeBody: Rec;
1041
+ try {
1042
+ // Built from the source envelope when there is one, after the managed-client steps above.
1043
+ nativeBody = envelope ? envelope.freshBody() : anthropicBody as Rec;
1044
+ } catch (err) {
1045
+ if (!isTranslatorBudgetExceededError(err)) throw err;
1046
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 413, { closeReason: "non_stream" });
1047
+ return anthropicErrorResponse(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
1048
+ }
1049
+ const { handleNativeMessages } = await import("./messages-native");
1050
+ return await handleNativeMessages({
1051
+ req, config, logCtx, ...(logIds ? { logIds } : {}),
1052
+ route: nativeMessagesRoute, body: nativeBody, requestedModel, translatorBudget, selector: nativeSelector,
1053
+ // The one caller header the native lane is given; the builder allowlists it.
1054
+ callerAnthropicBeta: req.headers.get("anthropic-beta"),
1055
+ });
1056
+ }
1057
+
942
1058
  const headers = new Headers({ "content-type": "application/json" });
943
1059
  let trustedClaudeMainAuth: { authorization: string; chatgptAccountId?: string } | undefined;
944
1060
  for (const name of FORWARD_HEADERS) {
@@ -1008,12 +1124,7 @@ async function handleClaudeMessagesWithBudget(
1008
1124
  // via the terminal callbacks; routed streams get the Responses-vocabulary log tap
1009
1125
  // BEFORE translation (the translated Anthropic stream has no response.completed
1010
1126
  // frame, so tapping it records a bogus 502 with no usage/cache detail).
1011
- let nativeLogged = false;
1012
- const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" }) => {
1013
- if (!logIds || nativeLogged) return;
1014
- nativeLogged = true;
1015
- addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
1016
- };
1127
+ const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish;
1017
1128
  const upstream = await handleResponses(internalReq, buildClaudeReplayConfig(config), logCtx, {
1018
1129
  // Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
1019
1130
  codexAuthPolicy: config,
@@ -1036,8 +1147,11 @@ async function handleClaudeMessagesWithBudget(
1036
1147
  ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
1037
1148
  onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
1038
1149
  onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
1150
+ ...(clientEncoder ? { clientEncoder } : {}),
1039
1151
  });
1040
1152
  const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;
1153
+ // Already in the Messages wire (direct encoder): no conversion.
1154
+ if (clientWireOf(upstream) === "messages") return response;
1041
1155
 
1042
1156
  if (!response.ok) {
1043
1157
  // Read the shared provenance verdict before consuming and re-wrapping the body. A refusal
@@ -1317,7 +1431,11 @@ export async function handleClaudeCountTokens(
1317
1431
  if (wantsNativePassthrough(req, config, requestPolicy, model, cc)) {
1318
1432
  return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
1319
1433
  }
1320
- const inputTokens = estimateClaudeRequestTokens(raw, model);
1434
+ // PF-08: an eligible managed-key route counts the body the native lane would send.
1435
+ const nativeCountBody = resolveProtocolSettings(config).rollout.managedMessagesNative
1436
+ ? (await import("./messages-native")).nativeMessagesCountBody(config, cc, raw, { fastRow: countFastRow !== null })
1437
+ : undefined;
1438
+ const inputTokens = estimateClaudeRequestTokens(nativeCountBody ?? raw, model);
1321
1439
  return new Response(JSON.stringify({ input_tokens: inputTokens }), {
1322
1440
  status: 200,
1323
1441
  headers: { "Content-Type": "application/json" },