@bitkyc08/opencodex 2.65.0-preview.20260925 → 2.66.0-preview.20260925

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-BVjA0T6O.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-DUvc_Wul.js → Tray-BCFtiWwD.js} +1 -1
  5. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  6. package/gui/dist/assets/index-vO4dgfiU.js +86 -0
  7. package/gui/dist/assets/{tray-data-f0lTZ4sF.js → tray-data-C4FiNaxk.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-CsYvvpr3.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-BB0iHG8-.js +0 -86
@@ -0,0 +1,790 @@
1
+ /**
2
+ * Managed native Messages lane (PF-08, behind `protocols.rollout.managedMessagesNative`).
3
+ *
4
+ * A Messages request whose settled route is a proxy-managed key on the `anthropic` adapter is
5
+ * sent as Messages: the source body (after the ingress's managed-client steps) cut to a field
6
+ * allowlist, the wire model, and the provider's own key. With `managedMessagesNativeOAuth` on
7
+ * (PF-10), an unpooled Anthropic OAuth account is sent the same way with the access token the
8
+ * existing OAuth selection resolves at dispatch (`messages-native-oauth.ts`). Modelled on the native Chat lane and
9
+ * built on the same shared pieces — attempt row, finish-once final log, spend tracker, proactive
10
+ * key selection, key failover and 429 replay, connection policy — so nothing the Responses
11
+ * pipeline enforces is bypassed.
12
+ *
13
+ * Authority. This lane reads no caller header. The ingress hands over one value, the caller's
14
+ * `anthropic-beta`, which the builder reduces to an allowlist. The caller-forward passthrough in
15
+ * `claude-messages.ts` (the caller's own Anthropic credential) is a different branch decided
16
+ * before this one, and nothing here can reach it or be reached from it.
17
+ *
18
+ * Loaded lazily by `claude-messages.ts` only when the switch is on and a route is eligible.
19
+ */
20
+ import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
21
+ import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
22
+ import { formatAnthropicErrorBody } from "../adapters/anthropic";
23
+ import {
24
+ anthropicMessagesNativeWireBody,
25
+ buildAnthropicMessagesPassthroughRequest,
26
+ type AnthropicMessagesPassthroughRequest,
27
+ } from "../adapters/anthropic/passthrough";
28
+ import { resolveInboundModel } from "../claude/inbound";
29
+ import { anthropicErrorBody, anthropicErrorResponse, collectAnthropicMessage } from "../claude/outbound";
30
+ import type { AdmissionLease } from "../lib/admission";
31
+ import { readBoundedResponseBody } from "../lib/bounded-body";
32
+ import { classifyError } from "../lib/errors";
33
+ import { redactSecretString } from "../lib/redact";
34
+ import { resolveClientRetryAfter } from "../lib/retry-after";
35
+ import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
36
+ import {
37
+ applyReplayRefusalClientHeaders,
38
+ applyUpstreamRecoveryInit,
39
+ fetchWithResetRetry,
40
+ fetchWithTransientRetry,
41
+ isNonReplayableResponse,
42
+ isReplayRefusalResponse,
43
+ isTransientUpstreamStatus,
44
+ prepareSameTarget429Wait,
45
+ REPLAY_REFUSED_STATUS,
46
+ retainReplayRefusal,
47
+ UpstreamRetryEvidenceError,
48
+ type UpstreamSendRecovery,
49
+ UPSTREAM_RESET_REPLAY_REFUSED_CODE,
50
+ } from "../lib/upstream-retry";
51
+ import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
52
+ import {
53
+ hasKeyPoolFailover,
54
+ rateLimitRetryDelayMs,
55
+ rateLimitRetryPolicyFor,
56
+ rotateProviderTransportOn401,
57
+ rotateProviderTransportOn429,
58
+ selectProactiveApiKeyTransport,
59
+ transientRetryPolicyFor,
60
+ } from "../providers/key-failover";
61
+ import { stampApiKeyAccountLabel, stampOAuthAccountLabel } from "../providers/label";
62
+ import { publicOAuthAuthenticationErrorMessage } from "../oauth";
63
+ import { hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
64
+ import { resolveProtocolSettings } from "../protocols/settings";
65
+ import { addProtocolEntryReason, markProtocolBlocked } from "../protocols/trace";
66
+ import type { OcxProviderTransport } from "../providers/xai-transport";
67
+ import { preservesPhysicalComboProvider, resolveComboId } from "../combos";
68
+ import { captureRouteStaticPolicy, routeModel, type RouteResult } from "../router";
69
+ import { POLICY_NAMESPACE, resolvePolicyProfileId } from "../routing/profile";
70
+ import type { OcxConfig, OcxProviderConfig, OcxUsage } from "../types";
71
+ import { resolveWireProtocolOverride } from "./adapter-resolve";
72
+ import {
73
+ anthropicUsageToOcx,
74
+ estimateClaudeRequestTokens,
75
+ resolvePassthroughBodyGuard,
76
+ sanitizePassthroughToolCallIds,
77
+ tapAnthropicSseForLog,
78
+ } from "./claude-messages";
79
+ import { beginInferenceAttempt } from "./inference/attempt";
80
+ import { createFinalRequestLog, type FinalRequestLogMeta } from "./inference/final-log";
81
+ import { registerTurn, unregisterTurn } from "./lifecycle";
82
+ import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility";
83
+ import {
84
+ nativeOAuthBindingIsCurrent,
85
+ NativeOAuthSelectionChangedError,
86
+ resolveNativeOAuthBinding,
87
+ restoreOAuthToolNamesInMessage,
88
+ restoreOAuthToolNamesInSse,
89
+ type NativeOAuthBinding,
90
+ } from "./messages-native-oauth";
91
+ import {
92
+ noteProviderAttemptSend,
93
+ recordAttemptCredentialSource,
94
+ recordFirstOutput,
95
+ recordKeyAttemptFailure,
96
+ recordKeyWireAttemptUsage,
97
+ type RequestLogContext,
98
+ } from "./request-log";
99
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
100
+ import { linkAbortSignal } from "./responses/core-lifetime";
101
+ import { attachRequestSpendTracker } from "./responses/request-spend";
102
+ import { workflowRefusalResponse } from "./workflow-refusal";
103
+ import { sseFieldValue } from "../lib/sse-decoder";
104
+
105
+ export {
106
+ isNativeMessagesRouteEligible,
107
+ nativeMessagesDeclineReason,
108
+ type NativeMessagesDeclineReason,
109
+ } from "./messages-native-eligibility";
110
+
111
+ type Rec = Record<string, unknown>;
112
+
113
+ const MAX_NATIVE_MESSAGES_JSON_BYTES = 32 * 1024 * 1024;
114
+ const MAX_NATIVE_MESSAGES_ERROR_BYTES = 64 * 1024;
115
+
116
+ class NativeMessagesSpendRefusal extends Error {}
117
+
118
+ /** A rebuild for a destination that cannot carry the body's opaque state, under `reject`. */
119
+ class NativeOpaqueStateRefusal extends Error {
120
+ constructor() {
121
+ super("The selected route cannot carry thinking signatures or redacted_thinking blocks");
122
+ }
123
+ }
124
+
125
+ function isRec(value: unknown): value is Rec {
126
+ return value !== null && typeof value === "object" && !Array.isArray(value);
127
+ }
128
+
129
+ export interface HandleNativeMessagesOptions {
130
+ req: Request;
131
+ config: OcxConfig;
132
+ logCtx: RequestLogContext;
133
+ logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease };
134
+ /** The settled, eligible route (see `isNativeMessagesRouteEligible`). */
135
+ route: RouteResult;
136
+ /**
137
+ * The Messages body after the ingress's managed-client steps. Owned by this lane from here
138
+ * on: a fresh envelope copy when an envelope exists, otherwise the ingress's own body.
139
+ */
140
+ body: Rec;
141
+ /** The selector the client sent, echoed on the request log. */
142
+ requestedModel: string;
143
+ translatorBudget: TranslatorBudget;
144
+ /** The facts the ingress judged eligibility with; re-applied if key selection changes. */
145
+ selector?: NativeMessagesSelector;
146
+ /**
147
+ * The caller's `anthropic-beta` header, handed over by the ingress. The builder keeps only
148
+ * allowlisted values; no other caller header reaches this lane.
149
+ */
150
+ callerAnthropicBeta?: string | null;
151
+ }
152
+
153
+ type FinishLog = (status: number, message?: string, closeReason?: FinalRequestLogMeta["closeReason"]) => void;
154
+
155
+ /** Relay an upstream body unchanged, recording first output on its first non-empty chunk. */
156
+ function observeFirstChunk(body: ReadableStream<Uint8Array>, onFirst: () => void): ReadableStream<Uint8Array> {
157
+ const reader = body.getReader();
158
+ let seen = false;
159
+ return new ReadableStream<Uint8Array>({
160
+ async pull(controller) {
161
+ const { done, value } = await reader.read();
162
+ if (done) {
163
+ controller.close();
164
+ return;
165
+ }
166
+ if (!seen && value.byteLength > 0) {
167
+ seen = true;
168
+ onFirst();
169
+ }
170
+ controller.enqueue(value);
171
+ },
172
+ cancel(reason) {
173
+ return reader.cancel(reason);
174
+ },
175
+ });
176
+ }
177
+
178
+ /** How far into a stream the `message_start` frame is looked for before relaying untouched. */
179
+ const MODEL_ECHO_SCAN_BYTES = 64 * 1024;
180
+
181
+ /** The `message_start` frame with `message.model` set to `model`, or undefined for any other frame. */
182
+ function messageStartWithModel(frame: string, model: string): string | undefined {
183
+ const data = frame
184
+ .split("\n")
185
+ .map(line => sseFieldValue(line, "data"))
186
+ .filter((value): value is string => value !== null)
187
+ .join("");
188
+ if (!data) return undefined;
189
+ let parsed: unknown;
190
+ try { parsed = JSON.parse(data); } catch { return undefined; }
191
+ if (!isRec(parsed) || parsed.type !== "message_start" || !isRec(parsed.message)) return undefined;
192
+ parsed.message.model = model;
193
+ return `event: message_start\ndata: ${JSON.stringify(parsed)}\n\n`;
194
+ }
195
+
196
+ /**
197
+ * Echo the client's selector in `message_start.message.model`, as the translated lane does
198
+ * (`responsesSseToAnthropicSse` is given the requested model). Works on bytes: frames before and
199
+ * including `message_start` are split on the blank line, and everything after is relayed as is.
200
+ */
201
+ function echoRequestedModel(body: ReadableStream<Uint8Array>, model: string): ReadableStream<Uint8Array> {
202
+ const reader = body.getReader();
203
+ const encoder = new TextEncoder();
204
+ const decoder = new TextDecoder();
205
+ let pending = new Uint8Array(0);
206
+ let scanning = true;
207
+ let scanned = 0;
208
+ const frameEnd = (bytes: Uint8Array): number => {
209
+ for (let i = 0; i + 1 < bytes.length; i++) if (bytes[i] === 10 && bytes[i + 1] === 10) return i + 2;
210
+ return -1;
211
+ };
212
+ return new ReadableStream<Uint8Array>({
213
+ async pull(controller) {
214
+ const { done, value } = await reader.read();
215
+ if (done) {
216
+ if (pending.length > 0) controller.enqueue(pending);
217
+ pending = new Uint8Array(0);
218
+ controller.close();
219
+ return;
220
+ }
221
+ if (!scanning) {
222
+ controller.enqueue(value);
223
+ return;
224
+ }
225
+ scanned += value.byteLength;
226
+ const joined = new Uint8Array(pending.length + value.byteLength);
227
+ joined.set(pending);
228
+ joined.set(value, pending.length);
229
+ pending = joined;
230
+ let end: number;
231
+ while (scanning && (end = frameEnd(pending)) !== -1) {
232
+ const frame = pending.subarray(0, end);
233
+ pending = pending.subarray(end);
234
+ const rewritten = messageStartWithModel(decoder.decode(frame), model);
235
+ if (rewritten !== undefined) {
236
+ controller.enqueue(encoder.encode(rewritten));
237
+ scanning = false;
238
+ } else {
239
+ controller.enqueue(frame);
240
+ }
241
+ }
242
+ if (scanning && scanned > MODEL_ECHO_SCAN_BYTES) scanning = false;
243
+ if (!scanning && pending.length > 0) {
244
+ controller.enqueue(pending);
245
+ pending = new Uint8Array(0);
246
+ }
247
+ },
248
+ cancel(reason) {
249
+ return reader.cancel(reason);
250
+ },
251
+ });
252
+ }
253
+
254
+ /** A minimal valid Messages stream for a streaming caller whose upstream answered JSON. */
255
+ function messageAsSse(message: Rec): string {
256
+ const frames: string[] = [];
257
+ const emit = (name: string, data: Rec) => frames.push(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
258
+ emit("message_start", { type: "message_start", message: { ...message, content: [], stop_reason: null } });
259
+ const blocks = Array.isArray(message.content) ? message.content.filter(isRec) : [];
260
+ blocks.forEach((block, index) => {
261
+ emit("content_block_start", { type: "content_block_start", index, content_block: block });
262
+ emit("content_block_stop", { type: "content_block_stop", index });
263
+ });
264
+ emit("message_delta", {
265
+ type: "message_delta",
266
+ delta: { stop_reason: message.stop_reason ?? "end_turn", stop_sequence: message.stop_sequence ?? null },
267
+ usage: message.usage ?? {},
268
+ });
269
+ emit("message_stop", { type: "message_stop" });
270
+ return frames.join("");
271
+ }
272
+
273
+ /** Reject a body the Messages API cannot carry before anything is sent. */
274
+ async function prepareNativeBody(body: Rec, signal: AbortSignal): Promise<void> {
275
+ if (!Array.isArray(body.messages)) return;
276
+ // The adapter normally owns these; this lane bypasses it, so it runs the same steps as the
277
+ // caller-forward passthrough: tier-normalize and guard images, then repair tool-call ids.
278
+ await normalizeAnthropicImages(body.messages, { abortSignal: signal });
279
+ enforceAnthropicImageLimits(body.messages);
280
+ sanitizePassthroughToolCallIds(body.messages);
281
+ }
282
+
283
+ /**
284
+ * Handle one eligible Messages request natively. Opens the attempt, owns the request's final
285
+ * log row, and answers in the Messages wire (SSE or JSON per the caller's own `stream`).
286
+ */
287
+ export async function handleNativeMessages(options: HandleNativeMessagesOptions): Promise<Response> {
288
+ const { req, config, logCtx, logIds, route, body, requestedModel, translatorBudget } = options;
289
+ const requestedStream = body.stream === true;
290
+ logCtx.inboundProtocol = "messages";
291
+ logCtx.model = route.modelId;
292
+ logCtx.provider = route.providerName;
293
+ logCtx.providerAdapter = route.provider.adapter;
294
+ logCtx.requestedModel = requestedModel;
295
+ if (route.routeReason === "model-alias" || route.modelId !== requestedModel) logCtx.requestedAlias = requestedModel;
296
+ logCtx.requestedServiceTier = typeof body.service_tier === "string" ? body.service_tier : undefined;
297
+ // Reserve spend the way native Chat does: an input estimate that never enters usage, and the
298
+ // caller's own output ceiling.
299
+ if (logCtx.usageLogInputTokens === undefined) {
300
+ logCtx.spendInputEstimateTokens = estimateClaudeRequestTokens(body, requestedModel);
301
+ }
302
+ if (typeof body.max_tokens === "number" && body.max_tokens > 0) {
303
+ logCtx.spendOutputCeilingTokens = Math.trunc(body.max_tokens);
304
+ }
305
+
306
+ const attemptHandle = beginInferenceAttempt(logCtx, {
307
+ provider: route.providerName,
308
+ model: route.modelId,
309
+ adapter: "anthropic",
310
+ });
311
+ attemptHandle.seal(logCtx.accountLogLabel);
312
+ const { attempt } = attemptHandle;
313
+ const finalLog = createFinalRequestLog(logIds, logCtx);
314
+ const finishLog: FinishLog = (status, message, closeReason = "non_stream") => {
315
+ if (finalLog.finished()) return;
316
+ if (message) logCtx.upstreamError = redactSecretString(message).slice(0, 500);
317
+ finalLog.finish(status, { closeReason });
318
+ };
319
+ const bindUsage = (usage: OcxUsage | undefined) => {
320
+ if (!usage) return;
321
+ if (!recordKeyWireAttemptUsage(logCtx, usage)) {
322
+ logCtx.usage = usage;
323
+ attempt.usage = usage;
324
+ }
325
+ };
326
+ const fail = (status: number, message: string, type?: string, code?: string): Response => {
327
+ const safeMessage = redactSecretString(message);
328
+ finishLog(status, safeMessage);
329
+ return anthropicErrorResponse(status, safeMessage, type, code);
330
+ };
331
+
332
+ try {
333
+ await prepareNativeBody(body, req.signal);
334
+ } catch (error) {
335
+ if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
336
+ if (isTranslatorBudgetExceededError(error)) {
337
+ return fail(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
338
+ }
339
+ // An AnthropicRequestError (empty tool id) or an image the guard cannot carry.
340
+ return fail(400, error instanceof Error ? error.message : String(error), "invalid_request_error");
341
+ }
342
+
343
+ const upstream = new AbortController();
344
+ const cleanupAbort = linkAbortSignal(upstream, req.signal);
345
+ let streamTurnRegistered = false;
346
+ const transferTurnToStream = () => {
347
+ const lease = logIds?.turnAdmissionLease;
348
+ if (!lease || typeof (lease as { bindAbortController?: unknown }).bindAbortController !== "function") return;
349
+ registerTurn(upstream, lease);
350
+ streamTurnRegistered = true;
351
+ };
352
+ const releaseStreamTurn = () => {
353
+ if (!streamTurnRegistered) return;
354
+ streamTurnRegistered = false;
355
+ unregisterTurn(upstream);
356
+ };
357
+ const connectMs = config.connectTimeoutMs ?? 200_000;
358
+ // An OAuth route is served by the account the existing OAuth selection commits now, at
359
+ // dispatch; the token lives on this lane's provider copy only, never on the shared route.
360
+ let oauthBinding: NativeOAuthBinding | undefined;
361
+ const oauthProvider = (binding: NativeOAuthBinding): OcxProviderConfig => ({ ...route.provider, apiKey: binding.snapshot.accessToken });
362
+ if (route.provider.authMode === "oauth") {
363
+ try {
364
+ oauthBinding = await resolveNativeOAuthBinding(config);
365
+ } catch (error) {
366
+ cleanupAbort();
367
+ upstream.abort();
368
+ if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
369
+ if (error instanceof NativeOAuthSelectionChangedError) return fail(409, error.message, "api_error");
370
+ return fail(401, publicOAuthAuthenticationErrorMessage(error), "authentication_error");
371
+ }
372
+ } else {
373
+ // Same pre-dispatch key preference every direct send path applies (see chat-native.ts).
374
+ const proactiveKeyProvider = selectProactiveApiKeyTransport(config, route.providerName, route.provider);
375
+ if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
376
+ }
377
+ let activeProvider: OcxProviderConfig = oauthBinding ? oauthProvider(oauthBinding) : route.provider;
378
+ stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
379
+ if (oauthBinding) stampOAuthAccountLabel(logCtx, route.providerName, activeProvider, oauthBinding.snapshot.accountId);
380
+ const spendTracker = attachRequestSpendTracker(req, logCtx);
381
+ let activeRequest: AnthropicMessagesPassthroughRequest;
382
+ let retainedRequestBytes = 0;
383
+ const releaseRetainedRequest = () => {
384
+ if (retainedRequestBytes === 0) return;
385
+ translatorBudget.releaseRetained(retainedRequestBytes, { kind: "request_copies" });
386
+ retainedRequestBytes = 0;
387
+ };
388
+ const retainRequest = (request: AnthropicMessagesPassthroughRequest) => {
389
+ const bytes = Buffer.byteLength(request.body);
390
+ translatorBudget.chargeRetained(bytes, { kind: "request_copies" });
391
+ retainedRequestBytes = bytes;
392
+ };
393
+ // Every rebuild reads the same `body`; the builder copies and never mutates it, so no
394
+ // credential, header or stripped block from an earlier build reaches the next one, and a build
395
+ // for another credential domain decides opaque state from the full source again.
396
+ const rejectUnrepresentable = resolveProtocolSettings(config).unrepresentable === "reject";
397
+ const buildActiveRequest = () => {
398
+ recordAttemptCredentialSource(attempt, route.providerName, activeProvider, "anthropic");
399
+ const built = buildAnthropicMessagesPassthroughRequest(activeProvider, route.modelId, body, config, {
400
+ callerAnthropicBeta: options.callerAnthropicBeta,
401
+ });
402
+ if (built.strippedOpaqueState && rejectUnrepresentable) throw new NativeOpaqueStateRefusal();
403
+ if (built.droppedBetas) addProtocolEntryReason(logCtx, "anthropic-beta-dropped");
404
+ if (built.strippedOpaqueState) addProtocolEntryReason(logCtx, "opaque-state-stripped");
405
+ return built;
406
+ };
407
+ const rebuildFor = (provider: OcxProviderConfig) => {
408
+ activeProvider = provider;
409
+ stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
410
+ releaseRetainedRequest();
411
+ activeRequest = buildActiveRequest();
412
+ retainRequest(activeRequest);
413
+ };
414
+ try {
415
+ activeRequest = buildActiveRequest();
416
+ retainRequest(activeRequest);
417
+ } catch (error) {
418
+ releaseRetainedRequest();
419
+ cleanupAbort();
420
+ upstream.abort();
421
+ if (isTranslatorBudgetExceededError(error)) {
422
+ return fail(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
423
+ }
424
+ if (error instanceof NativeOpaqueStateRefusal) {
425
+ // Nothing was sent: this is a refusal before any upstream send.
426
+ markProtocolBlocked(logCtx, { inbound: "messages", reasonCodes: ["feature-unrepresentable", "opaque-state-stripped"] });
427
+ logCtx.errorCode = "unsupported_feature";
428
+ return fail(400, error.message, "invalid_request_error", "unsupported_feature");
429
+ }
430
+ return fail(400, error instanceof Error ? error.message : String(error), "invalid_request_error");
431
+ }
432
+
433
+ // One inbound request owns one transient send allowance, captured before any key rotation.
434
+ const requestTransientPolicy = transientRetryPolicyFor(activeProvider);
435
+ let transientSendsUsed = 0;
436
+ const remainingTransientSends = (): number => requestTransientPolicy
437
+ ? Math.max(0, requestTransientPolicy.attempts - transientSendsUsed)
438
+ : Number.POSITIVE_INFINITY;
439
+ const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
440
+ const selector: NativeMessagesSelector = options.selector ?? {};
441
+
442
+ const send = async (recovery?: "rate-limit-429" | "key-429" | "key-401"): Promise<Response> => {
443
+ const remaining = remainingTransientSends();
444
+ if (requestTransientPolicy && remaining <= 0) {
445
+ throw new Error("native Messages transient send budget exhausted before recovery dispatch");
446
+ }
447
+ const fetchWithPolicy = requestTransientPolicy ? fetchWithTransientRetry : fetchWithResetRetry;
448
+ const request = activeRequest;
449
+ return await fetchWithPolicy(
450
+ (transportRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
451
+ request.url,
452
+ applyUpstreamRecoveryInit({ method: "POST", headers: request.headers, body: request.body }, transportRecovery),
453
+ upstream.signal,
454
+ connectMs,
455
+ requestedStream,
456
+ providerFetch(activeProvider, undefined, {
457
+ providerName: route.providerName,
458
+ modelId: route.modelId,
459
+ dispatchOverride: async (_input, init, execute) => {
460
+ if (oauthBinding) {
461
+ // The OAuth twin of the key check below: re-resolve through the same selection
462
+ // owner when the committed account or its credential moved since the build.
463
+ if (!nativeOAuthBindingIsCurrent(oauthBinding)) {
464
+ try {
465
+ oauthBinding = await resolveNativeOAuthBinding(config);
466
+ } catch {
467
+ throw new NativeOAuthSelectionChangedError();
468
+ }
469
+ rebuildFor(oauthProvider(oauthBinding));
470
+ stampOAuthAccountLabel(logCtx, route.providerName, activeProvider, oauthBinding.snapshot.accountId);
471
+ }
472
+ } else if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
473
+ const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
474
+ if (!current || nativeMessagesDeclineReason({ ...route, provider: current }, body, config, selector) !== undefined) {
475
+ throw new Error("Provider key selection is no longer available for native Messages");
476
+ }
477
+ rebuildFor(current);
478
+ }
479
+ // The retry closure may hold a pre-reselection request: send the current one whole.
480
+ const wire = activeRequest;
481
+ const headers = new Headers(wire.headers);
482
+ const encoding = new Headers(init.headers).get("accept-encoding");
483
+ if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
484
+ if (init.signal?.aborted) throw init.signal.reason;
485
+ if (!spendTracker.charge()) throw new NativeMessagesSpendRefusal();
486
+ noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
487
+ const dispatched = await sendWithConnectionPolicy(
488
+ (activeProvider as OcxProviderTransport).fetch ?? execute,
489
+ wire.url,
490
+ applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
491
+ { providerName: route.providerName, provider: activeProvider },
492
+ );
493
+ if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
494
+ return dispatched;
495
+ },
496
+ }),
497
+ ),
498
+ {
499
+ abortSignal: upstream.signal,
500
+ label: safeHostLabel(request.url),
501
+ ...(requestTransientPolicy
502
+ ? {
503
+ attempts: remaining,
504
+ onSendsConsumed: (sends: number) => { transientSendsUsed += Math.max(0, sends); },
505
+ }
506
+ : {}),
507
+ },
508
+ );
509
+ };
510
+ const discard = (response: Response) => {
511
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
512
+ };
513
+
514
+ let response: Response;
515
+ try {
516
+ response = await send();
517
+ // A credential-scoped 401 on a static key pool says nothing about the sibling keys.
518
+ while (response.status === 401 && hasKeyPoolFailover(activeProvider) && transientSendAvailable()) {
519
+ const rotated = rotateProviderTransportOn401(config, route.providerName, activeProvider, {
520
+ now: Date.now(),
521
+ attemptedKey: activeProvider.apiKey,
522
+ });
523
+ if (!rotated) break;
524
+ discard(response);
525
+ rebuildFor(rotated);
526
+ response = await send("key-401");
527
+ }
528
+ const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
529
+ let retries = 0;
530
+ // A refusal this proxy synthesized for a reset replay is not a provider rate limit.
531
+ while (
532
+ response.status === 429
533
+ && !isNonReplayableResponse(response)
534
+ && retryPolicy
535
+ && retries < retryPolicy.attempts
536
+ && transientSendAvailable()
537
+ ) {
538
+ retries += 1;
539
+ for await (const _ of prepareSameTarget429Wait({
540
+ body: response.body,
541
+ signal: upstream.signal,
542
+ delayMs: rateLimitRetryDelayMs(retryPolicy, response.headers.get("retry-after"), Date.now()),
543
+ })) { /* pre-stream wait */ }
544
+ if (upstream.signal.aborted) throw upstream.signal.reason;
545
+ response = await send("rate-limit-429");
546
+ }
547
+ while (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
548
+ const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
549
+ retryAfter: response.headers.get("retry-after"),
550
+ now: Date.now(),
551
+ attemptedKey: activeProvider.apiKey,
552
+ attemptedSelection: activeProvider._apiKeyAttempt,
553
+ });
554
+ if (!rotated) break;
555
+ // Rotation already recorded the cooldown; keep the terminal 429 when no send remains.
556
+ if (!transientSendAvailable()) break;
557
+ discard(response);
558
+ rebuildFor(rotated);
559
+ response = await send("key-429");
560
+ }
561
+ } catch (error) {
562
+ releaseRetainedRequest();
563
+ cleanupAbort();
564
+ upstream.abort();
565
+ if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
566
+ const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
567
+ if (sendError instanceof NativeMessagesSpendRefusal) {
568
+ const refusal = workflowRefusalResponse("workflow-spend-exhausted", logCtx);
569
+ finishLog(429);
570
+ return refusal;
571
+ }
572
+ if (sendError instanceof NativeOAuthSelectionChangedError) return fail(409, sendError.message, "api_error");
573
+ if (sendError instanceof NativeOpaqueStateRefusal) {
574
+ logCtx.errorCode = "unsupported_feature";
575
+ return fail(400, sendError.message, "invalid_request_error", "unsupported_feature");
576
+ }
577
+ if (isTranslatorBudgetExceededError(error)) {
578
+ return fail(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
579
+ }
580
+ return fail(502, error instanceof Error ? error.message : String(error), "api_error");
581
+ }
582
+ releaseRetainedRequest();
583
+
584
+ if (!response.ok) {
585
+ let bodyText = "";
586
+ try {
587
+ const read = await readBoundedResponseBody(response, { signal: upstream.signal, maxBytes: MAX_NATIVE_MESSAGES_ERROR_BYTES });
588
+ if (read.displaySafe) bodyText = read.text;
589
+ } catch { /* status-only fallback */ }
590
+ cleanupAbort();
591
+ if (req.signal.aborted) {
592
+ upstream.abort();
593
+ return fail(499, "Client cancelled request", "api_error");
594
+ }
595
+ return nativeMessagesErrorResponse(response, bodyText, finishLog);
596
+ }
597
+
598
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
599
+ if (contentType.includes("text/event-stream") && response.body) {
600
+ const bodyGuard = resolvePassthroughBodyGuard(config, req.signal);
601
+ const observed = logIds ? observeFirstChunk(response.body, () => recordFirstOutput(logCtx, logIds.start)) : response.body;
602
+ const renamed = activeRequest.oauthToolNames
603
+ ? restoreOAuthToolNamesInSse(observed, activeRequest.oauthToolNames, translatorBudget)
604
+ : observed;
605
+ const source = echoRequestedModel(renamed, requestedModel);
606
+ if (requestedStream) {
607
+ transferTurnToStream();
608
+ const relayed = tapAnthropicSseForLog(source, logCtx, (status, meta) => {
609
+ try {
610
+ cleanupAbort();
611
+ bindUsage(logCtx.usage);
612
+ finishLog(status, undefined, meta.closeReason);
613
+ if (meta.closeReason !== "terminal") upstream.abort();
614
+ } finally {
615
+ releaseStreamTurn();
616
+ }
617
+ }, bodyGuard);
618
+ return new Response(relayed, {
619
+ status: 200,
620
+ headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive" },
621
+ });
622
+ }
623
+ // A non-streaming caller whose upstream streamed anyway: fold the stream into one message.
624
+ const tapState: { closeReason?: FinalRequestLogMeta["closeReason"] } = {};
625
+ const tapped = tapAnthropicSseForLog(source, logCtx, (_status, meta) => {
626
+ tapState.closeReason = meta.closeReason;
627
+ }, bodyGuard);
628
+ try {
629
+ const message = await collectAnthropicMessage(tapped, requestedModel, translatorBudget);
630
+ cleanupAbort();
631
+ if (tapState.closeReason === "client_cancel" || req.signal.aborted) {
632
+ return fail(499, "Client cancelled request", "api_error");
633
+ }
634
+ bindUsage(logCtx.usage);
635
+ if (message.type === "error") {
636
+ const error = isRec(message.error) ? message.error : {};
637
+ return fail(502, typeof error.message === "string" ? error.message : "upstream stream failed", "api_error");
638
+ }
639
+ finishLog(200);
640
+ return Response.json(message);
641
+ } catch (error) {
642
+ cleanupAbort();
643
+ upstream.abort();
644
+ if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
645
+ if (isTranslatorBudgetExceededError(error)) {
646
+ return fail(413, "upstream translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
647
+ }
648
+ return fail(502, error instanceof Error ? error.message : String(error), "api_error");
649
+ }
650
+ }
651
+
652
+ let read;
653
+ try {
654
+ read = await readBoundedResponseBody(response, {
655
+ signal: upstream.signal,
656
+ maxBytes: MAX_NATIVE_MESSAGES_JSON_BYTES,
657
+ totalTimeoutMs: Math.max(connectMs, 5_000),
658
+ inactivityTimeoutMs: Math.max(connectMs, 5_000),
659
+ });
660
+ } catch (error) {
661
+ cleanupAbort();
662
+ upstream.abort();
663
+ if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
664
+ return fail(502, error instanceof Error ? error.message : String(error), "api_error");
665
+ }
666
+ cleanupAbort();
667
+ if (read.oversized) {
668
+ upstream.abort();
669
+ return fail(502, "upstream response exceeded the safe limit", "api_error", "translation_buffer_limit");
670
+ }
671
+ let parsedMessage: unknown;
672
+ try {
673
+ parsedMessage = JSON.parse(read.text);
674
+ } catch {
675
+ return fail(502, "upstream returned malformed Messages JSON", "api_error");
676
+ }
677
+ if (!isRec(parsedMessage) || parsedMessage.type !== "message") {
678
+ return fail(502, "upstream response was not a Messages result", "api_error");
679
+ }
680
+ const message = activeRequest.oauthToolNames
681
+ ? restoreOAuthToolNamesInMessage(parsedMessage, activeRequest.oauthToolNames)
682
+ : parsedMessage;
683
+ bindUsage(anthropicUsageToOcx(isRec(message.usage) ? message.usage : undefined));
684
+ if (logIds) recordFirstOutput(logCtx, logIds.start);
685
+ try {
686
+ translatorBudget.chargeRetained(Buffer.byteLength(read.text) * 2, { kind: "live_transient" });
687
+ } catch (error) {
688
+ if (isTranslatorBudgetExceededError(error)) {
689
+ return fail(502, "upstream translation buffer exceeded the safe limit", "api_error", "translation_buffer_limit");
690
+ }
691
+ throw error;
692
+ }
693
+ // The client's selector, not the wire id, as the translated lane answers.
694
+ message.model = requestedModel;
695
+ const serialized = JSON.stringify(message);
696
+ finishLog(200);
697
+ if (requestedStream) {
698
+ return new Response(messageAsSse(message), {
699
+ status: 200,
700
+ headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
701
+ });
702
+ }
703
+ return new Response(serialized, { status: 200, headers: { "Content-Type": "application/json" } });
704
+ }
705
+
706
+ /**
707
+ * A non-OK upstream answer in Anthropic shape. Status and retry policy follow the translated
708
+ * Messages path, so a client sees the same contract on either lane: transient 5xx become 529
709
+ * with a retry hint, and a proxy-side replay refusal keeps its no-retry marking.
710
+ */
711
+ function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog): Response {
712
+ let upstreamType: string | undefined;
713
+ let upstreamMessage: string | undefined;
714
+ try {
715
+ const parsed = JSON.parse(bodyText) as Rec;
716
+ const details = isRec(parsed.error) ? parsed.error : parsed;
717
+ if (typeof details.type === "string") upstreamType = details.type;
718
+ if (typeof details.message === "string" && details.message.trim()) {
719
+ upstreamMessage = redactSecretString(details.message.trim());
720
+ }
721
+ } catch { /* keep generic classification */ }
722
+ const detail = formatAnthropicErrorBody(response.status, response.headers, bodyText);
723
+ const message = upstreamMessage
724
+ ?? (detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`);
725
+ const classified = classifyError(
726
+ response.status,
727
+ upstreamType ?? (response.status === 401 ? "authentication_error"
728
+ : response.status === 429 ? "rate_limit_error"
729
+ : response.status >= 500 ? "api_error" : "invalid_request_error"),
730
+ message,
731
+ );
732
+ const safeMessage = redactSecretString(classified.message);
733
+ const replayRefusal = isReplayRefusalResponse(response);
734
+ const transient = !replayRefusal && isTransientUpstreamStatus(response.status);
735
+ const status = replayRefusal ? REPLAY_REFUSED_STATUS : transient ? 529 : response.status;
736
+ const upstreamRetryAfter = response.headers.get("retry-after");
737
+ const retryAfter = replayRefusal
738
+ ? undefined
739
+ : resolveClientRetryAfter({ status: response.status, message: safeMessage, upstreamRetryAfter })
740
+ ?? (upstreamRetryAfter?.trim() === "0" ? "0" : undefined);
741
+ finishLog(response.status, safeMessage);
742
+ const headers = new Headers({ "Content-Type": "application/json" });
743
+ if (retryAfter) headers.set("Retry-After", retryAfter);
744
+ else if (transient) headers.set("Retry-After", "2");
745
+ if (replayRefusal) applyReplayRefusalClientHeaders(headers);
746
+ const out = new Response(JSON.stringify(anthropicErrorBody(
747
+ status,
748
+ safeMessage,
749
+ transient ? "overloaded_error" : upstreamType,
750
+ replayRefusal ? UPSTREAM_RESET_REPLAY_REFUSED_CODE : undefined,
751
+ )), { status, headers });
752
+ return replayRefusal ? retainReplayRefusal(out) : out;
753
+ }
754
+
755
+ /**
756
+ * The body `count_tokens` should count for this selector: what the native lane would send when
757
+ * the route is eligible, else `undefined` so the caller keeps its existing estimate. Reads config
758
+ * and the router's deterministic branches only; sends nothing.
759
+ */
760
+ export function nativeMessagesCountBody(
761
+ config: OcxConfig,
762
+ cc: OcxConfig["claudeCode"],
763
+ body: Rec,
764
+ rows: Pick<NativeMessagesSelector, "effortRow" | "fastRow">,
765
+ ): Rec | undefined {
766
+ if (typeof body.model !== "string") return undefined;
767
+ try {
768
+ const selectorId = resolveInboundModel(body.model, cc);
769
+ // Combo and policy selectors are never eligible, and routing them would advance round-robin
770
+ // state or run the policy evaluator for a request that sends nothing.
771
+ if (resolvePolicyProfileId(config, selectorId) !== null || selectorId.startsWith(`${POLICY_NAMESPACE}/`)) return undefined;
772
+ if (!preservesPhysicalComboProvider(config) && resolveComboId(config, selectorId) !== null) return undefined;
773
+ const route = routeModel(config, selectorId);
774
+ route.staticPolicy = captureRouteStaticPolicy(
775
+ route.providerName, route.modelId, route.provider, route.staticPolicy.effectiveAlias, "anthropic",
776
+ );
777
+ route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
778
+ const selector: NativeMessagesSelector = {
779
+ ...rows,
780
+ routeSelector: selectorId,
781
+ claudeCode: cc,
782
+ ...(route.provider.authMode === "oauth" ? { oauthFailoverQuorum: hasAnthropicFailoverQuorum() } : {}),
783
+ };
784
+ if (nativeMessagesDeclineReason(route, body, config, selector) !== undefined) return undefined;
785
+ // The body without a credential: counting never resolves or refreshes an OAuth account.
786
+ return anthropicMessagesNativeWireBody(route.provider, route.modelId, body).wireBody;
787
+ } catch {
788
+ return undefined;
789
+ }
790
+ }