@bitkyc08/opencodex 2.89.0 → 2.91.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 (242) hide show
  1. package/gui/dist/assets/App-DMps4OLk.css +1 -0
  2. package/gui/dist/assets/App-nZ34cR5G.js +58 -0
  3. package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-BuLy7hL1.js} +1 -1
  4. package/gui/dist/assets/{index-uchD-ngf.js → index-DGXS51sp.js} +15 -15
  5. package/gui/dist/assets/quota-summary-DO1Sx5TM.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/package.json +2 -2
  8. package/src/adapters/anthropic.ts +2 -5
  9. package/src/adapters/base.ts +4 -0
  10. package/src/adapters/claude-cli/adapter.ts +13 -7
  11. package/src/adapters/claude-cli/usage-admission.ts +28 -2
  12. package/src/adapters/coding-agent/turn.ts +14 -4
  13. package/src/adapters/cursor/protobuf-request.ts +3 -0
  14. package/src/adapters/google-progress-history.ts +19 -1
  15. package/src/adapters/google.ts +28 -9
  16. package/src/adapters/image.ts +5 -3
  17. package/src/adapters/input-media-guard.ts +5 -2
  18. package/src/adapters/kiro-refusal.ts +41 -1
  19. package/src/adapters/ollama-native.ts +1 -0
  20. package/src/adapters/openai-chat/messages.ts +34 -4
  21. package/src/adapters/openai-chat/passthrough.ts +37 -4
  22. package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
  23. package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
  24. package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
  25. package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
  26. package/src/adapters/openai-chat.ts +7 -6
  27. package/src/adapters/openai-responses/opengateway-input.ts +139 -0
  28. package/src/adapters/openai-responses/passthrough.ts +7 -2
  29. package/src/adapters/openai-responses/request-strips.ts +32 -0
  30. package/src/adapters/openai-responses/transport.ts +19 -1
  31. package/src/chat/inbound.ts +20 -2
  32. package/src/chat/outbound.ts +36 -4
  33. package/src/chat/reasoning-details.ts +137 -0
  34. package/src/claude/desktop-3p-library.ts +43 -3
  35. package/src/claude/desktop-3p.ts +20 -10
  36. package/src/claude/desktop-picker-profile.ts +18 -9
  37. package/src/claude/desktop-remote-store-io.ts +2 -2
  38. package/src/cli/capabilities-agents-routing.ts +50 -0
  39. package/src/cli/capabilities-base.ts +15 -2
  40. package/src/cli/capabilities-observe-system.ts +4 -4
  41. package/src/cli/codex-shim-autorestore.ts +5 -0
  42. package/src/cli/dispatch.ts +30 -0
  43. package/src/cli/ensure-desktop-cold-start.ts +2 -2
  44. package/src/cli/fallback.ts +127 -0
  45. package/src/cli/help.ts +1 -0
  46. package/src/cli/index.ts +1 -2
  47. package/src/cli/link-apply-restart.ts +72 -0
  48. package/src/cli/link-child.ts +6 -2
  49. package/src/cli/link-cleanup.ts +95 -0
  50. package/src/cli/link-enrollment.ts +4 -0
  51. package/src/cli/link.ts +84 -9
  52. package/src/cli/provider.ts +44 -2
  53. package/src/cli/registry.ts +13 -1
  54. package/src/cli/restart-failure.ts +4 -0
  55. package/src/cli/runtime-api.ts +1 -1
  56. package/src/cli/status-codex-settings.ts +56 -0
  57. package/src/cli/status-fallback.ts +29 -0
  58. package/src/cli/status-install.ts +52 -0
  59. package/src/cli/status.ts +20 -1
  60. package/src/cli/system-restart-client.ts +32 -1
  61. package/src/cli/update-restart-child.ts +4 -3
  62. package/src/cli/update-restart-home.ts +20 -8
  63. package/src/client/connect.ts +82 -6
  64. package/src/client/home-unreachable.ts +16 -0
  65. package/src/client/hub-relay.ts +8 -2
  66. package/src/client/link-home.ts +119 -6
  67. package/src/client/link-parity-reporter.ts +107 -0
  68. package/src/client/link-relay-transport.ts +8 -1
  69. package/src/client/link-relay.ts +27 -3
  70. package/src/client/machine-listener.ts +83 -1
  71. package/src/client/runtime-restart.ts +127 -0
  72. package/src/client/runtime.ts +137 -25
  73. package/src/codex/account-lifecycle.ts +19 -6
  74. package/src/codex/account-store.ts +27 -8
  75. package/src/codex/account-usability.ts +4 -1
  76. package/src/codex/auth-api/account-list.ts +10 -5
  77. package/src/codex/auth-api/account-pause-group.ts +2 -22
  78. package/src/codex/auth-api/login-collision.ts +59 -0
  79. package/src/codex/auth-api/login-flow.ts +12 -7
  80. package/src/codex/auth-api/pool-mode-gate.ts +81 -13
  81. package/src/codex/auth-collision.ts +31 -4
  82. package/src/codex/auth-context.ts +58 -12
  83. package/src/codex/catalog/parsing.ts +4 -0
  84. package/src/codex/cli-update-plan.ts +18 -3
  85. package/src/codex/collision-identity.ts +78 -0
  86. package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
  87. package/src/codex/main-account-hard-lock.ts +14 -0
  88. package/src/codex/quota-history.ts +7 -0
  89. package/src/codex/quota.ts +10 -0
  90. package/src/codex/routing/cooldown-math.ts +5 -0
  91. package/src/codex/routing/failure-window.ts +61 -9
  92. package/src/codex/routing.ts +4 -3
  93. package/src/codex/shim-diagnostics.ts +2 -1
  94. package/src/codex/shim-fingerprint.ts +11 -2
  95. package/src/codex/shim-templates.ts +16 -5
  96. package/src/codex/shim-windows-refresh.ts +224 -0
  97. package/src/codex/shim.ts +45 -21
  98. package/src/codex/subagent-model-fallback.ts +24 -1
  99. package/src/combos/failover.ts +8 -1
  100. package/src/combos/index.ts +1 -0
  101. package/src/combos/resolve.ts +12 -0
  102. package/src/config/diagnostics.ts +5 -0
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/provider-validation.ts +7 -2
  105. package/src/config/schema/config-schema.ts +3 -0
  106. package/src/config/schema/fallback-policy.ts +40 -0
  107. package/src/generated/compatibility-version.json +360 -192
  108. package/src/generated/model-metadata.ts +1 -1
  109. package/src/images/loop.ts +2 -2
  110. package/src/lib/authorization-scheme.ts +95 -0
  111. package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
  112. package/src/lib/provider-admission-wait.ts +14 -0
  113. package/src/lib/provider-client-headers.ts +1 -0
  114. package/src/lib/redact.ts +2 -2
  115. package/src/lib/service-secrets.ts +37 -2
  116. package/src/lib/system-restart-contract.ts +24 -0
  117. package/src/lib/test-home-guard.ts +78 -11
  118. package/src/lib/translator-budget.ts +1 -1
  119. package/src/lib/upstream-retry.ts +15 -0
  120. package/src/link/child-parity.ts +124 -0
  121. package/src/link/codex-settings.ts +37 -0
  122. package/src/link/compatibility.ts +119 -0
  123. package/src/link/management-routes.ts +2 -0
  124. package/src/link/peer-report.ts +100 -0
  125. package/src/link/remote-platform.ts +78 -0
  126. package/src/link/ssh-argv.ts +19 -0
  127. package/src/link/status-projection.ts +24 -2
  128. package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
  129. package/src/oauth/account-import/parser.ts +15 -2
  130. package/src/oauth/account-import/types.ts +7 -0
  131. package/src/oauth/chatgpt-shared.ts +10 -1
  132. package/src/oauth/dsh.ts +45 -2
  133. package/src/oauth/index.ts +1 -1
  134. package/src/oauth/kimi.ts +2 -0
  135. package/src/oauth/kiro.ts +44 -1
  136. package/src/oauth/login-flow-state.ts +11 -2
  137. package/src/oauth/nous.ts +4 -1
  138. package/src/oauth/store.ts +2 -1
  139. package/src/protocols/codecs/chat.ts +3 -3
  140. package/src/protocols/encoders/adapter-events.ts +26 -2
  141. package/src/protocols/encoders/chat.ts +55 -3
  142. package/src/providers/claude-cli-usage.ts +20 -5
  143. package/src/providers/initial-model-selection.ts +2 -2
  144. package/src/providers/key-failover.ts +10 -2
  145. package/src/providers/key-store.ts +8 -3
  146. package/src/providers/mistral-transport.ts +240 -0
  147. package/src/providers/openai-sidecar.ts +5 -0
  148. package/src/providers/quota/anthropic-account-quota.ts +18 -6
  149. package/src/providers/quota/antigravity.ts +8 -10
  150. package/src/providers/quota/failure-codes.ts +18 -0
  151. package/src/providers/quota/vendor-probes-oauth.ts +12 -3
  152. package/src/providers/quota-routing-cache.ts +1 -1
  153. package/src/providers/quota.ts +5 -1
  154. package/src/providers/registry/entries-extended.ts +6 -1
  155. package/src/providers/routed-config.ts +1 -1
  156. package/src/providers/xai-transport.ts +2 -1
  157. package/src/quota-pool/assemble.ts +326 -0
  158. package/src/quota-pool/contract.ts +58 -0
  159. package/src/quota-pool/identity.ts +203 -0
  160. package/src/remote/features.ts +74 -0
  161. package/src/remote/protocol.ts +2 -0
  162. package/src/remote/served-by.ts +57 -0
  163. package/src/responses/audio.ts +46 -0
  164. package/src/responses/input-media.ts +37 -15
  165. package/src/responses/parser-content.ts +22 -27
  166. package/src/responses/parser.ts +7 -0
  167. package/src/responses/reasoning-envelope.ts +12 -0
  168. package/src/responses/reasoning-retention.ts +218 -11
  169. package/src/responses/schema.ts +3 -4
  170. package/src/responses/web-search-replay.ts +11 -2
  171. package/src/routing/fallback-policy.ts +290 -0
  172. package/src/routing/fallback-state.ts +179 -0
  173. package/src/server/audio-client.ts +2 -1
  174. package/src/server/audio-dictation.ts +2 -1
  175. package/src/server/audio-upstream.ts +2 -1
  176. package/src/server/auth-cors.ts +6 -5
  177. package/src/server/background-lifecycle.ts +4 -0
  178. package/src/server/chat-completions.ts +8 -1
  179. package/src/server/chat-native.ts +8 -7
  180. package/src/server/gui-session.ts +2 -1
  181. package/src/server/index/link-listener.ts +11 -1
  182. package/src/server/index/link-management.ts +47 -0
  183. package/src/server/index/link-relay-sessions.ts +11 -2
  184. package/src/server/index/optional-listeners.ts +7 -2
  185. package/src/server/index/serve-options.ts +27 -4
  186. package/src/server/index/websocket-handler.ts +10 -0
  187. package/src/server/lifecycle.ts +17 -1
  188. package/src/server/live.ts +5 -0
  189. package/src/server/management/context.ts +1 -1
  190. package/src/server/management/fallback-routes.ts +75 -0
  191. package/src/server/management/link-enrollment.ts +21 -3
  192. package/src/server/management/link-route-state.ts +2 -0
  193. package/src/server/management/link-routes.ts +12 -1
  194. package/src/server/management/oauth-account-routes.ts +2 -1
  195. package/src/server/management/quota-pool-routes.ts +24 -0
  196. package/src/server/management/route-registry.ts +3 -0
  197. package/src/server/management-api.ts +16 -0
  198. package/src/server/management-auth.ts +2 -1
  199. package/src/server/messages-native.ts +11 -0
  200. package/src/server/request-log.ts +16 -7
  201. package/src/server/responses/adapter-continuation.ts +2 -2
  202. package/src/server/responses/adapter-dispatch.ts +2 -2
  203. package/src/server/responses/agent-task-recovery.ts +1 -1
  204. package/src/server/responses/codex-ws-exchange.ts +3 -0
  205. package/src/server/responses/codex-ws-metadata.ts +44 -0
  206. package/src/server/responses/codex-ws-session.ts +21 -1
  207. package/src/server/responses/combo-stream-preflight.ts +7 -2
  208. package/src/server/responses/compact.ts +4 -2
  209. package/src/server/responses/core-auth.ts +31 -1
  210. package/src/server/responses/core-codex-account.ts +21 -14
  211. package/src/server/responses/core-combo.ts +75 -1
  212. package/src/server/responses/core-opaque-recovery.ts +20 -11
  213. package/src/server/responses/deepseek-image-files.ts +121 -0
  214. package/src/server/responses/empty-completion-guard.ts +8 -0
  215. package/src/server/responses/encrypted-payload.ts +3 -3
  216. package/src/server/responses/fetch-helpers.ts +30 -10
  217. package/src/server/responses/input-admission.ts +6 -2
  218. package/src/server/responses/main-hard-lock-routing.ts +85 -8
  219. package/src/server/responses/opengateway-completion-guard.ts +232 -0
  220. package/src/server/responses/passthrough-completion.ts +41 -0
  221. package/src/server/responses/passthrough-delivery.ts +2 -0
  222. package/src/server/responses/passthrough-dispatch.ts +13 -13
  223. package/src/server/responses/passthrough-key-rotation.ts +40 -0
  224. package/src/server/responses/policy-fallback.ts +44 -1
  225. package/src/server/responses/reasoning-archive-error.ts +21 -0
  226. package/src/server/responses/request-prepare.ts +19 -1
  227. package/src/server/responses/request-transport.ts +61 -3
  228. package/src/server/restart-replacement.ts +50 -2
  229. package/src/server/ws-bridge.ts +5 -0
  230. package/src/service/cli.ts +25 -2
  231. package/src/service/diagnostics.ts +20 -6
  232. package/src/service/orchestration.ts +17 -0
  233. package/src/service/status-json.ts +116 -0
  234. package/src/types/config.ts +7 -0
  235. package/src/types/fallback.ts +38 -0
  236. package/src/types/provider.ts +2 -0
  237. package/src/types/request.ts +16 -1
  238. package/src/types.ts +1 -0
  239. package/src/web-search/loop.ts +2 -2
  240. package/gui/dist/assets/App-BnRSujOq.js +0 -58
  241. package/gui/dist/assets/App-dnkJ85te.css +0 -1
  242. package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
@@ -27,8 +27,8 @@ const VIDEO_UNSUPPORTED_MARKER = "[video omitted: the translated Chat route has
27
27
  /**
28
28
  * Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
29
29
  * content is text-only on every chat provider, so these ride in a follow-up user message instead of
30
- * being flattened to the "[image]" marker the model can't actually see. Data URLs and remote https
31
- * URLs are both valid in image_url.url, unlike Gemini inline_data which needs base64.
30
+ * being flattened to the "[image]" marker the model can't actually see. The same follow-up carries
31
+ * inline audio as input_audio, which is also user-content-only on this wire.
32
32
  */
33
33
  export function toolResultTextForWire(content: string | OcxContentPart[], annotateEmpty = false): string {
34
34
  // An empty content array is a present-but-empty result; `contentPartsToText` would
@@ -63,6 +63,14 @@ export function toolResultImageChatParts(content: string | OcxContentPart[]): un
63
63
  return parts;
64
64
  }
65
65
 
66
+ /** Chat input_audio parts for audio carried inside a tool result. */
67
+ export function toolResultAudioChatParts(content: string | OcxContentPart[]): unknown[] {
68
+ if (typeof content === "string") return [];
69
+ return content
70
+ .filter((p): p is Extract<OcxContentPart, { type: "audio" }> => p.type === "audio")
71
+ .map(p => ({ type: "input_audio", input_audio: { data: p.data, format: p.format } }));
72
+ }
73
+
66
74
  /**
67
75
  * Whether this wire serializes a replayed thinking block for the given model.
68
76
  *
@@ -98,6 +106,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
98
106
  let pendingToolCalls: PendingToolCall[] = [];
99
107
  let deferredBarrierMessages: unknown[] = [];
100
108
  let pendingToolResultImageParts: unknown[] = [];
109
+ let pendingToolResultAudioParts: unknown[] = [];
101
110
  let mintedIdSeq = 0;
102
111
  const seenWireCallIds = new Set<string>();
103
112
 
@@ -128,6 +137,18 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
128
137
  pendingToolResultImageParts = [];
129
138
  };
130
139
 
140
+ const flushToolResultAudio = (): void => {
141
+ if (pendingToolResultAudioParts.length === 0) return;
142
+ out.push({
143
+ role: "user",
144
+ content: [
145
+ { type: "text", text: "[ocx] audio output from the preceding tool result(s):" },
146
+ ...pendingToolResultAudioParts,
147
+ ],
148
+ });
149
+ pendingToolResultAudioParts = [];
150
+ };
151
+
131
152
  const flushPendingToolCalls = (): void => {
132
153
  if (pendingToolCalls.length === 0) return;
133
154
  for (const call of pendingToolCalls) {
@@ -139,6 +160,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
139
160
  }
140
161
  pendingToolCalls = [];
141
162
  flushToolResultImages();
163
+ flushToolResultAudio();
142
164
  releaseDeferredBarriers();
143
165
  };
144
166
 
@@ -185,9 +207,10 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
185
207
  case "developer": {
186
208
  const parts = typeof msg.content === "string" ? undefined : msg.content as OcxContentPart[];
187
209
  const hasImages = parts?.some(p => p.type === "image") ?? false;
210
+ const hasAudio = parts?.some(p => p.type === "audio") ?? false;
188
211
  // A document has a structured counterpart on this wire, so it needs the parts array for
189
212
  // the same reason an image does: flattening it to a string would drop the bytes.
190
- const hasStructured = hasImages || (parts?.some(p => p.type === "document") ?? false);
213
+ const hasStructured = hasImages || hasAudio || (parts?.some(p => p.type === "document") ?? false);
191
214
  let chatMsg: Record<string, unknown>;
192
215
  if (msg.role === "developer" && !hasStructured) {
193
216
  const text = typeof msg.content === "string"
@@ -219,6 +242,9 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
219
242
  if (p.type === "image") {
220
243
  return { type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } };
221
244
  }
245
+ if (p.type === "audio") {
246
+ return { type: "input_audio", input_audio: { data: p.data, format: p.format } };
247
+ }
222
248
  // Chat Completions carries an attached document as a file part with inline bytes,
223
249
  // the direct counterpart of the Anthropic document block the caller sent.
224
250
  if (p.type === "document") {
@@ -240,7 +266,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
240
266
  // this wire. One carrying only a document has no such precedent, and demoting it would
241
267
  // undo the role this adapter just finished preserving.
242
268
  chatMsg = {
243
- role: msg.role === "developer" && !hasImages ? developerWireRole : "user",
269
+ role: msg.role === "developer" && !hasImages && !hasAudio ? developerWireRole : "user",
244
270
  content: chatParts,
245
271
  };
246
272
  }
@@ -330,9 +356,11 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
330
356
  content: toolResultTextForWire(msg.content, provider.annotateEmptyToolOutputs === true),
331
357
  });
332
358
  pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
359
+ pendingToolResultAudioParts.push(...toolResultAudioChatParts(msg.content));
333
360
  pendingToolCalls.splice(matchIdx, 1);
334
361
  if (pendingToolCalls.length === 0) {
335
362
  flushToolResultImages();
363
+ flushToolResultAudio();
336
364
  releaseDeferredBarriers();
337
365
  }
338
366
  } else {
@@ -380,7 +408,9 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
380
408
  content: toolResultTextForWire(msg.content, provider.annotateEmptyToolOutputs === true),
381
409
  });
382
410
  pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
411
+ pendingToolResultAudioParts.push(...toolResultAudioChatParts(msg.content));
383
412
  flushToolResultImages();
413
+ flushToolResultAudio();
384
414
  }
385
415
  break;
386
416
  }
@@ -14,8 +14,11 @@ import { modelRecordValue } from "../../reasoning-effort";
14
14
  import { modelInList, type OcxProviderConfig } from "../../types";
15
15
  import { chatParallelToolCallsWireValue } from "./parallel-tool-calls";
16
16
  import { applyExplicitChatReasoningWirePolicy } from "./reasoning-wire";
17
- import { replayNativeChatReasoning } from "./reasoning-replay";
17
+ import { planNativeChatReasoningReplay } from "./reasoning-replay";
18
18
  import type { OcxReasoningReplayScopeRef } from "../../types";
19
+ import { jsonUtf8Bytes } from "../../lib/json-byte-size";
20
+ import { isTranslatorBudgetExceededError, TRANSLATOR_MAX_TURN_BYTES, TranslatorBudgetExceededError,
21
+ type TranslatorBudget } from "../../lib/translator-budget";
19
22
 
20
23
  const CHAT_PASSTHROUGH_FIELDS = [
21
24
  "audio",
@@ -58,6 +61,8 @@ export function buildOpenAIChatPassthroughRequest(
58
61
  fastMode?: boolean,
59
62
  providerName?: string,
60
63
  replayScope?: OcxReasoningReplayScopeRef,
64
+ maxSerializedBytes = TRANSLATOR_MAX_TURN_BYTES,
65
+ translatorBudget?: TranslatorBudget,
61
66
  ): AdapterRequest {
62
67
  const { url, headers, hasCredential } = openAIChatTransport(provider);
63
68
 
@@ -79,8 +84,8 @@ export function buildOpenAIChatPassthroughRequest(
79
84
  for (const field of CHAT_PASSTHROUGH_FIELDS) {
80
85
  if (rawBody[field] !== undefined) body[field] = rawBody[field];
81
86
  }
82
- body.messages = replayNativeChatReasoning(body.messages, provider, modelId, replayScope,
83
- Array.isArray(rawBody.tools) && rawBody.tools.length > 0);
87
+ const replayPlan = planNativeChatReasoningReplay(body.messages, provider, modelId, replayScope,
88
+ Array.isArray(rawBody.tools) && rawBody.tools.length > 0, maxSerializedBytes);
84
89
  if (protectGlmSummaryBudget(body, provider.baseUrl, body.reasoning_effort)) body.reasoning_effort = "low";
85
90
  const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
86
91
  const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
@@ -161,7 +166,35 @@ export function buildOpenAIChatPassthroughRequest(
161
166
  body.stream_options = rawBody.stream_options;
162
167
  }
163
168
 
164
- const bodyJson = JSON.stringify(applyGithubCopilotContextTier(body, provider, modelId, providerName));
169
+ const transformedBody = () => applyGithubCopilotContextTier(body, provider, modelId, providerName);
170
+ // The binding constraint is the final size base + net, so a shrinking replay may legitimately
171
+ // measure a base above the cap; only a base above max - net makes the result unserializable.
172
+ let baseBytes = 0;
173
+ if (replayPlan.hasChanges) {
174
+ try {
175
+ baseBytes = jsonUtf8Bytes(transformedBody(), maxSerializedBytes - replayPlan.serializedBytes);
176
+ } catch (error) {
177
+ if (isTranslatorBudgetExceededError(error)) {
178
+ throw new TranslatorBudgetExceededError("request_copies", maxSerializedBytes);
179
+ }
180
+ throw error;
181
+ }
182
+ }
183
+ if (replayPlan.serializedBytes > maxSerializedBytes - baseBytes) {
184
+ throw new TranslatorBudgetExceededError("request_copies", maxSerializedBytes);
185
+ }
186
+ const preSerialization = translatorBudget?.reserveTransient(baseBytes + replayPlan.serializedBytes, {
187
+ kind: "request_copies",
188
+ });
189
+ let bodyJson: string;
190
+ try {
191
+ if (replayPlan.hasChanges) {
192
+ body.messages = replayPlan.apply();
193
+ }
194
+ bodyJson = JSON.stringify(transformedBody());
195
+ } finally {
196
+ preSerialization?.release();
197
+ }
165
198
 
166
199
  if (isDebugEnabled()) {
167
200
  let host = "upstream";
@@ -1,10 +1,14 @@
1
1
  import { modelInList, type OcxProviderConfig, type OcxReasoningReplayScopeRef } from "../../types";
2
2
  import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
3
+ import { jsonStringPartsUtf8Bytes, jsonUtf8Bytes } from "../../lib/json-byte-size";
4
+ import { isTranslatorBudgetExceededError, TranslatorBudgetExceededError } from "../../lib/translator-budget";
3
5
 
4
6
  type Rec = Record<string, unknown>;
5
7
  export const isChatRecord = (value: unknown): value is Rec =>
6
8
  value !== null && typeof value === "object" && !Array.isArray(value);
7
9
 
10
+ const REASONING_CONTENT_FIELD_BYTES = Buffer.byteLength('"reasoning_content":', "utf8");
11
+
8
12
  const isDeepSeekThinkingModel = (modelId: string): boolean =>
9
13
  /(?:^|\/)deepseek-(?:v\d+(?:\.\d+)*-(?:flash|pro)|flash|pro)(?:$|[-_:])/i.test(modelId);
10
14
 
@@ -25,28 +29,109 @@ export function chatReasoningReplayPolicy(provider: OcxProviderConfig, modelId:
25
29
  };
26
30
  }
27
31
 
28
- /** Repair missing native history without mutating caller bodies used by retries/combo siblings. */
29
- export function replayNativeChatReasoning(
32
+ interface ReplayMessagePlan {
33
+ message: Rec;
34
+ parts: string[];
35
+ }
36
+
37
+ export interface NativeChatReasoningReplayPlan {
38
+ /** Net byte change in the final escaped JSON object representation; may be zero or negative. */
39
+ readonly serializedBytes: number;
40
+ /** Whether apply() rewrites any message, independent of the net byte change. */
41
+ readonly hasChanges: boolean;
42
+ apply(): unknown[];
43
+ }
44
+
45
+ /** Preflight escaped replay growth before allocating the copied messages or serialized body. */
46
+ export function planNativeChatReasoningReplay(
30
47
  messages: unknown,
31
48
  provider: OcxProviderConfig,
32
49
  modelId: string,
33
50
  scope?: OcxReasoningReplayScopeRef,
34
51
  hasTools = false,
35
- ): unknown {
52
+ maxSerializedBytes = Number.MAX_SAFE_INTEGER,
53
+ ): NativeChatReasoningReplayPlan {
36
54
  const policy = chatReasoningReplayPolicy(provider, modelId);
37
55
  if (!policy.preserve || !Array.isArray(messages)
38
- || modelInList(provider.reasoningDetailsModels, modelId)) return messages;
39
- return messages.map(message => {
56
+ || modelInList(provider.reasoningDetailsModels, modelId)) {
57
+ return { serializedBytes: 0, hasChanges: false, apply: () => messages as unknown[] };
58
+ }
59
+ const plans: ReplayMessagePlan[] = [];
60
+ let serializedBytes = 0;
61
+ for (const message of messages) {
40
62
  if (!isChatRecord(message) || message.role !== "assistant"
41
- || (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0)) return message;
63
+ || (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0)) continue;
42
64
  const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
43
65
  // DeepSeek's tools contract covers all prior assistant turns, even plain answers.
44
66
  // Those turns have no call id for original replay; only the configured fallback applies.
45
- if (calls.length === 0 && !(hasTools && isDeepSeekThinkingModel(modelId))) return message;
46
- const cached = calls.filter(isChatRecord)
47
- .map(tool => typeof tool.id === "string" ? peekReasoningForCall(tool.id, scope) : undefined)
48
- .filter((text): text is string => typeof text === "string" && text.length > 0);
49
- const reasoning = cached.length > 0 ? [...new Set(cached)].join("\n") : policy.placeholder ? " " : undefined;
50
- return reasoning === undefined ? message : { ...message, reasoning_content: reasoning };
51
- });
67
+ if (calls.length === 0 && !(hasTools && isDeepSeekThinkingModel(modelId))) continue;
68
+ const cached = new Set<string>();
69
+ for (const tool of calls) {
70
+ if (!isChatRecord(tool) || typeof tool.id !== "string") continue;
71
+ const text = peekReasoningForCall(tool.id, scope);
72
+ if (text) cached.add(text);
73
+ }
74
+ const parts = cached.size > 0
75
+ ? [...cached]
76
+ : policy.placeholder ? [" "] : [];
77
+ if (parts.length === 0) continue;
78
+
79
+ // The envelope delta around the joined value is exact for the plain JSON the Chat
80
+ // ingress produces: an absent or skipped key adds the reasoning_content field bytes
81
+ // plus one separator when another field serializes, while an already-serialized key
82
+ // ("" included) keeps its position and commas, so only the value size changes.
83
+ const oldReasoning = message.reasoning_content;
84
+ try {
85
+ const envelopeDelta = oldReasoning === undefined
86
+ ? REASONING_CONTENT_FIELD_BYTES
87
+ + (Object.keys(message).some(key => message[key] !== undefined) ? 1 : 0)
88
+ : -jsonUtf8Bytes(oldReasoning, Number.MAX_SAFE_INTEGER);
89
+ const fragments: string[] = [];
90
+ for (let index = 0; index < parts.length; index++) {
91
+ if (index > 0) fragments.push("\n");
92
+ fragments.push(parts[index]!);
93
+ }
94
+ const valueBytes = jsonStringPartsUtf8Bytes(
95
+ fragments,
96
+ maxSerializedBytes - serializedBytes - envelopeDelta,
97
+ );
98
+ const added = valueBytes + envelopeDelta;
99
+ if (added > maxSerializedBytes - serializedBytes) {
100
+ throw new TranslatorBudgetExceededError("request_copies", maxSerializedBytes);
101
+ }
102
+ serializedBytes += added;
103
+ } catch (error) {
104
+ // Inner measurements cap at the remaining budget; report the caller's real limit.
105
+ if (isTranslatorBudgetExceededError(error)) {
106
+ throw new TranslatorBudgetExceededError("request_copies", maxSerializedBytes);
107
+ }
108
+ throw error;
109
+ }
110
+ plans.push({ message, parts });
111
+ }
112
+ return {
113
+ serializedBytes,
114
+ hasChanges: plans.length > 0,
115
+ apply: () => {
116
+ if (plans.length === 0) return messages;
117
+ const byMessage = new Map(plans.map(plan => [plan.message, plan.parts]));
118
+ return (messages as unknown[]).map(message => {
119
+ if (!isChatRecord(message)) return message;
120
+ const parts = byMessage.get(message);
121
+ if (!parts) return message;
122
+ return { ...message, reasoning_content: parts.join("\n") };
123
+ });
124
+ },
125
+ };
126
+ }
127
+
128
+ /** Repair missing native history without mutating caller bodies used by retries/combo siblings. */
129
+ export function replayNativeChatReasoning(
130
+ messages: unknown,
131
+ provider: OcxProviderConfig,
132
+ modelId: string,
133
+ scope?: OcxReasoningReplayScopeRef,
134
+ hasTools = false,
135
+ ): unknown {
136
+ return planNativeChatReasoningReplay(messages, provider, modelId, scope, hasTools).apply();
52
137
  }