@bitkyc08/opencodex 2.58.0 → 2.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Process-local memo pairing a bridged hosted `web_search` cell with the destination's own
3
+ * call and the result the proxy executed for it (issue #4587).
4
+ *
5
+ * The web-search passthrough bridge (`src/web-search/passthrough-bridge.ts`) intercepts the
6
+ * destination's `function_call` named `web_search`, runs the search itself, and shows the
7
+ * caller a hosted `web_search_call` cell under a proxy-minted `ws_<uuid>` id. The caller
8
+ * stores that cell and replays it on every later turn. The destination, which never produced a
9
+ * `web_search_call` in its life, then sees an unknown item type carrying a query and sources
10
+ * but no result — so it usually just searches again.
11
+ *
12
+ * This memo is what lets the pre-dispatch rewrite in the Responses adapter put the destination's
13
+ * own `function_call` and `function_call_output` back in that item's place. It records exactly
14
+ * what `appendBridgeSearchTurn` would have written onto a continuation leg, so a replayed turn
15
+ * and a continued turn show the destination the same conversation.
16
+ *
17
+ * Scope. Entries are keyed by the upstream destination in addition to the cell id. The cell id is
18
+ * a v4 UUID minted here, so it cannot collide across conversations, but an unscoped key would let
19
+ * a history replayed against a DIFFERENT provider resurrect a call that provider never made.
20
+ *
21
+ * Bounds and privacy. Result text is web content the caller already received, but it is still
22
+ * request-derived data: it lives in memory only, is never logged, serialized, or exported, and is
23
+ * bounded by entry count, total bytes, and TTL so a long-lived proxy cannot grow without limit.
24
+ *
25
+ * A miss is deliberately indistinguishable from "no entry": the caller leaves the replayed item
26
+ * alone. Neither re-running the search nor inventing a result is an acceptable recovery.
27
+ */
28
+
29
+ import { reasoningReplayDestinationIdentity } from "./reasoning-replay-cache";
30
+
31
+ const MAX_ENTRIES = 64;
32
+ const MAX_TOTAL_BYTES = 512 * 1024;
33
+ const TTL_MS = 60 * 60 * 1000;
34
+
35
+ export interface BridgeSearchReplayEntry {
36
+ /** The destination's own call id, as it appeared on the intercepted item. */
37
+ callId: string;
38
+ /** The destination's own item id, replayed when the upstream supplied one. */
39
+ sourceItemId?: string;
40
+ /** The tool name the destination called, recorded rather than assumed. */
41
+ name: string;
42
+ /** The intercepted call's complete arguments text. */
43
+ argumentsText: string;
44
+ /** The tool result the bridge produced for that call. */
45
+ output: string;
46
+ }
47
+
48
+ interface StoredEntry {
49
+ entry: BridgeSearchReplayEntry;
50
+ bytes: number;
51
+ at: number;
52
+ }
53
+
54
+ const entries = new Map<string, StoredEntry>();
55
+ let totalBytes = 0;
56
+ let clockForTests: (() => number) | null = null;
57
+
58
+ const now = (): number => clockForTests?.() ?? Date.now();
59
+
60
+ /**
61
+ * Identify the upstream destination a bridged search belongs to.
62
+ *
63
+ * Reuses the salted process-local destination digest the reasoning replay cache already defines,
64
+ * so both stores agree on what "the same upstream" means and neither invents a second notion of
65
+ * destination identity.
66
+ */
67
+ export function bridgeSearchReplayScope(baseUrl: string | undefined): string | undefined {
68
+ return reasoningReplayDestinationIdentity(baseUrl);
69
+ }
70
+
71
+ function keyFor(scope: string, cellItemId: string): string {
72
+ return scope + "\u0000" + cellItemId;
73
+ }
74
+
75
+ function drop(key: string, stored: StoredEntry): void {
76
+ entries.delete(key);
77
+ totalBytes -= stored.bytes;
78
+ }
79
+
80
+ function sweep(at: number): void {
81
+ for (const [key, stored] of entries) {
82
+ if (at - stored.at >= TTL_MS) drop(key, stored);
83
+ }
84
+ // Map iteration is insertion-ordered, so the oldest surviving entry is always the first one.
85
+ while (entries.size > MAX_ENTRIES || totalBytes > MAX_TOTAL_BYTES) {
86
+ const oldest = entries.entries().next();
87
+ if (oldest.done) {
88
+ totalBytes = 0;
89
+ return;
90
+ }
91
+ drop(oldest.value[0], oldest.value[1]);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Record one executed bridged search.
97
+ *
98
+ * An entry with no call id is not recorded: the restore would have to emit a `function_call`
99
+ * without one, which is not a valid item and could not be paired with its output anyway.
100
+ */
101
+ export function rememberBridgeSearchReplay(
102
+ scope: string | undefined,
103
+ cellItemId: string,
104
+ entry: BridgeSearchReplayEntry,
105
+ ): void {
106
+ if (!scope || cellItemId.length === 0 || entry.callId.length === 0) return;
107
+ const key = keyFor(scope, cellItemId);
108
+ const existing = entries.get(key);
109
+ if (existing) drop(key, existing);
110
+ const bytes = 2 * (
111
+ key.length
112
+ + entry.callId.length
113
+ + (entry.sourceItemId?.length ?? 0)
114
+ + entry.name.length
115
+ + entry.argumentsText.length
116
+ + entry.output.length
117
+ );
118
+ // A single oversized result is refused outright rather than evicting the whole store for it.
119
+ if (bytes > MAX_TOTAL_BYTES) return;
120
+ entries.set(key, { entry: { ...entry }, bytes, at: now() });
121
+ totalBytes += bytes;
122
+ sweep(now());
123
+ }
124
+
125
+ /**
126
+ * Look up one recorded search without consuming it.
127
+ *
128
+ * The same cell is replayed on every subsequent turn of the conversation, so a consuming read
129
+ * would restore the pair once and then silently stop. Expiry stays absolute: a conversation that
130
+ * outlives the TTL degrades to today's behaviour (the hosted cell replays unchanged) rather than
131
+ * pinning entries open for as long as anyone keeps talking.
132
+ */
133
+ export function peekBridgeSearchReplay(
134
+ scope: string | undefined,
135
+ cellItemId: string,
136
+ ): BridgeSearchReplayEntry | undefined {
137
+ if (!scope || cellItemId.length === 0) return undefined;
138
+ const key = keyFor(scope, cellItemId);
139
+ const stored = entries.get(key);
140
+ if (!stored) return undefined;
141
+ if (now() - stored.at >= TTL_MS) {
142
+ drop(key, stored);
143
+ return undefined;
144
+ }
145
+ return stored.entry;
146
+ }
147
+
148
+ export function clearBridgeSearchReplayCacheForTests(clock?: (() => number) | null): void {
149
+ entries.clear();
150
+ totalBytes = 0;
151
+ clockForTests = clock ?? null;
152
+ }
@@ -9,33 +9,43 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
9
9
  return !!value && typeof value === "object" && !Array.isArray(value);
10
10
  }
11
11
 
12
- function unwrapPatchInput(value: string): string {
13
- try {
14
- const parsed: unknown = JSON.parse(value);
15
- if (isPlainObject(parsed)) {
16
- if (typeof parsed.input === "string") return parsed.input;
17
- if (typeof parsed.patch === "string") return parsed.patch;
18
- }
19
- } catch {
20
- // Native custom calls carry the patch body directly.
21
- }
22
- return value;
23
- }
24
-
25
12
  /**
26
13
  * Convert a nested Code Mode helper call into unified-exec JavaScript.
27
14
  *
28
15
  * Parsed values are serialized as data, never interpolated as source, so command and patch text
29
16
  * cannot escape the generated call. Invalid structured helper payloads are also passed as data so
30
17
  * nested-tool validation can reject them without evaluating provider text as JavaScript.
18
+ *
19
+ * `wireToolName` is the name the body actually arrived under, which is not always the helper:
20
+ * a provider that got the NAME right and the BODY wrong sends `exec`, and the apply-patch
21
+ * helper is inferred from the payload. Recognition and compilation must read ONE canonical
22
+ * body, so both unwrap under that same name — see the apply-patch branch below. It defaults to
23
+ * the helper name, which is correct for the name-based path where the wire name IS the helper.
31
24
  */
32
- export function compileCodeModeHelperInput(argumentsText: unknown, toolName: string): string {
25
+ export function compileCodeModeHelperInput(
26
+ argumentsText: unknown,
27
+ toolName: string,
28
+ wireToolName?: string,
29
+ ): string {
33
30
  if (typeof argumentsText !== "string") return "";
34
31
  const helperName = toolName.startsWith("default.")
35
32
  ? toolName.slice("default.".length)
36
33
  : toolName;
37
34
  if (helperName === "apply_patch") {
38
- const patch = normalizeApplyPatchDelimiters(unwrapPatchInput(argumentsText));
35
+ // `resolveCodeModeHelperName` decides this IS an apply-patch call by reading
36
+ // `unwrapFreeformToolInput(argumentsText, wireToolName)`, which strips an outer Markdown
37
+ // fence and accepts that name's fallback fields (#4983). Compiling from a narrower unwrap
38
+ // meant a body accepted through a fence or a fallback field reached `tools.apply_patch`
39
+ // still wrapped, so the host rejected the JSON or the fence instead of applying the patch
40
+ // (#5046). One unwrap, one body, one decision.
41
+ //
42
+ // The vocabulary is the WIRE name rather than the helper name on purpose. `{"patch": ...}`
43
+ // is an apply_patch wrapper and is not an `exec` fallback field, and the recognizer already
44
+ // declines it under `exec`; reading it here would compile a body that recognition rejected,
45
+ // which is exactly the drift a second, looser unwrap introduces.
46
+ const patch = normalizeApplyPatchDelimiters(
47
+ unwrapFreeformToolInput(argumentsText, wireToolName ?? helperName),
48
+ );
39
49
  return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
40
50
  }
41
51
  let parsed: unknown = argumentsText;
@@ -106,5 +116,5 @@ export function resolveCodeModeHelperName(
106
116
  // `tools.apply_patch(...)` JavaScript would be the mis-route this repair exists to avoid.
107
117
  if (!declaresCodeModeExec(declaredNames)) return undefined;
108
118
  if (typeof argumentsText !== "string" || argumentsText === "") return undefined;
109
- return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText)) ? "apply_patch" : undefined;
119
+ return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText, "exec")) ? "apply_patch" : undefined;
110
120
  }
@@ -326,7 +326,7 @@ export function restoreRoutedCustomCalls(
326
326
  id: customToolItemId(item.id),
327
327
  name: aliased ? targetName : item.name,
328
328
  input: helper
329
- ? compileCodeModeHelperInput(sourceInput, helper)
329
+ ? compileCodeModeHelperInput(sourceInput, helper, aliased ? String(item.name) : targetName)
330
330
  : repairFreeformToolInput(
331
331
  sourceInput,
332
332
  targetName,
@@ -0,0 +1,279 @@
1
+ // Bounded classification of a PARTIAL freeform tool-call wrapper.
2
+ //
3
+ // `unwrapFreeformToolInput` decides the completed input with `JSON.parse`, which does not care
4
+ // what order an object's properties arrive in or how their names are spelled. The streaming side
5
+ // has to reach the same answer from a prefix, and it used to do that by comparing the buffer
6
+ // against the literal `{"input":"`. Spellings `JSON.parse` calls identical therefore matched no
7
+ // wrapper at all, streamed as raw JSON, and then completed as the unwrapped body:
8
+ // `{"metadata":1,"input":"cmd"}` and `{"\u0069nput":"cmd"}` both previewed the whole object and
9
+ // completed as `cmd` (#5151). #5047 and #5129 closed the compact and whitespace spellings of the
10
+ // same disagreement; reordering and escaping are the two the literal matcher could never see,
11
+ // because RFC 8259 objects are unordered and their names are strings with escapes.
12
+ //
13
+ // So the prefix is scanned as JSON instead of matched as text. The scan answers one question —
14
+ // which wrapper, if any, the completed text will unwrap to — and it answers it three ways:
15
+ //
16
+ // `input` the canonical key is present with a string value, whatever preceded it and however
17
+ // its name was spelled. Its value can be decoded progressively, because
18
+ // `unwrapFreeformToolInput` gives an own `input` precedence over every other property.
19
+ // `raw` no wrapper can apply, because the text is not an object or because it is one that
20
+ // `JSON.parse` will reject. Completion returns the buffer, so streaming it agrees.
21
+ // `hold` undecided. A key that has not arrived yet can still change the answer, so nothing
22
+ // is published until the object parses and completion's own rule decides.
23
+ //
24
+ // The bound matters as much as the classification. A scan that walks the whole buffer on every
25
+ // delta is quadratic in the argument size, so classification gives up after
26
+ // `MAX_FREEFORM_WRAPPER_SCAN_CHARS` and holds. Giving up costs preview, never agreement: a held
27
+ // buffer is still resolved by the authoritative parse at completion.
28
+
29
+ /** The insignificant whitespace `JSON.parse` accepts between tokens. */
30
+ export const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
31
+
32
+ /** The two-character escapes JSON defines, and nothing else. */
33
+ export const JSON_ESCAPES = new Map<string, string>([
34
+ ['"', '"'], ["\\", "\\"], ["/", "/"],
35
+ ["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
36
+ ]);
37
+
38
+ /**
39
+ * How far a classification scan walks before it gives up and holds.
40
+ *
41
+ * A wrapper's own structure — its property names and the small scalar siblings a model puts
42
+ * beside the body — lives at the front of the object. A buffer still undecided after this much
43
+ * scanning is one whose preceding values are large, and holding it is what the routed
44
+ * restoration path already does for every unrecognized object.
45
+ */
46
+ export const MAX_FREEFORM_WRAPPER_SCAN_CHARS = 4096;
47
+
48
+ const CANONICAL_KEY = "input";
49
+
50
+ /** The buffer ends inside a token: what follows can still change the answer. */
51
+ const HOLD = -1;
52
+ /** `JSON.parse` will reject this text no matter what is appended to it. */
53
+ const NEVER = -2;
54
+
55
+ export type FreeformWrapperScan =
56
+ | { kind: "hold"; parse: boolean }
57
+ | { kind: "raw" }
58
+ | { kind: "input"; valueStart: number };
59
+
60
+ /** Index of the first non-whitespace character at or after `from`, or HOLD past the end. */
61
+ function skipWhitespace(text: string, from: number): number {
62
+ let i = from;
63
+ while (i < text.length && JSON_WHITESPACE.has(text[i]!)) i++;
64
+ return i < text.length ? i : HOLD;
65
+ }
66
+
67
+ /**
68
+ * Index just past the complete JSON string opening at `from`, or HOLD / NEVER.
69
+ *
70
+ * This reports a BOUNDARY where `decodeJsonStringPrefix` reports a decoded PREFIX: one has to
71
+ * fail on a truncated string and the other has to return what it decoded so far. They answer
72
+ * different questions about the same bytes and share one escape table so they cannot disagree
73
+ * about which escapes exist.
74
+ */
75
+ function scanString(text: string, from: number): number {
76
+ let i = from + 1;
77
+ while (i < text.length) {
78
+ const c = text[i]!;
79
+ if (c === '"') return i + 1;
80
+ if (c === "\\") {
81
+ const n = text[i + 1];
82
+ if (n === undefined) return HOLD;
83
+ if (n === "u") {
84
+ const hex = text.slice(i + 2, i + 6);
85
+ if (hex.length < 4) return HOLD;
86
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return NEVER;
87
+ i += 6;
88
+ continue;
89
+ }
90
+ if (!JSON_ESCAPES.has(n)) return NEVER;
91
+ i += 2;
92
+ continue;
93
+ }
94
+ // A literal control character is not legal inside a JSON string.
95
+ if (c.charCodeAt(0) <= 0x1f) return NEVER;
96
+ i++;
97
+ }
98
+ return HOLD;
99
+ }
100
+
101
+ const JSON_NUMBER_CHARS = /[-+0-9.eE]/;
102
+ const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
103
+
104
+ /** Index just past one complete scalar at `from`, or HOLD / NEVER. */
105
+ function scanScalar(text: string, from: number): number {
106
+ const c = text[from]!;
107
+ if (c === '"') return scanString(text, from);
108
+ if (c === "t" || c === "f" || c === "n") {
109
+ const literal = c === "t" ? "true" : c === "f" ? "false" : "null";
110
+ if (text.startsWith(literal, from)) return from + literal.length;
111
+ return literal.startsWith(text.slice(from)) ? HOLD : NEVER;
112
+ }
113
+ let end = from;
114
+ while (end < text.length && JSON_NUMBER_CHARS.test(text[end]!)) end++;
115
+ if (end === from) return NEVER;
116
+ // A number running to the end of the buffer can still grow another digit or exponent.
117
+ if (end >= text.length) return HOLD;
118
+ return JSON_NUMBER.test(text.slice(from, end)) ? end : NEVER;
119
+ }
120
+
121
+ /** Index where the value of the member whose name opens at `from` begins, or HOLD / NEVER. */
122
+ function scanMemberKey(text: string, from: number): number {
123
+ if (text[from] !== '"') return NEVER;
124
+ const nameEnd = scanString(text, from);
125
+ if (nameEnd < 0) return nameEnd;
126
+ const colon = skipWhitespace(text, nameEnd);
127
+ if (colon === HOLD) return HOLD;
128
+ if (text[colon] !== ":") return NEVER;
129
+ return skipWhitespace(text, colon + 1);
130
+ }
131
+
132
+ /**
133
+ * Index just past one complete JSON value at `from`, or HOLD / NEVER.
134
+ *
135
+ * Containers are walked with an explicit stack rather than recursion: the value being skipped
136
+ * is provider-controlled, and a deeply nested one must not be able to exhaust the call stack.
137
+ */
138
+ function scanValue(text: string, from: number): number {
139
+ const closers: string[] = [];
140
+ let i = from;
141
+ for (;;) {
142
+ const c = text[i]!;
143
+ if (c === "{" || c === "[") {
144
+ const closer = c === "{" ? "}" : "]";
145
+ closers.push(closer);
146
+ const first = skipWhitespace(text, i + 1);
147
+ if (first === HOLD) return HOLD;
148
+ if (text[first] === closer) {
149
+ closers.pop();
150
+ i = first + 1;
151
+ } else if (closer === "}") {
152
+ const value = scanMemberKey(text, first);
153
+ if (value < 0) return value;
154
+ i = value;
155
+ continue;
156
+ } else {
157
+ i = first;
158
+ continue;
159
+ }
160
+ } else {
161
+ const end = scanScalar(text, i);
162
+ if (end < 0) return end;
163
+ i = end;
164
+ }
165
+
166
+ // One value is complete: close whatever it finished and find the next value, if any.
167
+ for (;;) {
168
+ if (closers.length === 0) return i;
169
+ const at = skipWhitespace(text, i);
170
+ if (at === HOLD) return HOLD;
171
+ const closer = closers[closers.length - 1]!;
172
+ if (text[at] === closer) {
173
+ closers.pop();
174
+ i = at + 1;
175
+ continue;
176
+ }
177
+ if (text[at] !== ",") return NEVER;
178
+ const next = skipWhitespace(text, at + 1);
179
+ if (next === HOLD) return HOLD;
180
+ if (closer === "}") {
181
+ const value = scanMemberKey(text, next);
182
+ if (value < 0) return value;
183
+ i = value;
184
+ } else {
185
+ i = next;
186
+ }
187
+ break;
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Which wrapper the completed text will unwrap to, as far as this prefix can say.
194
+ *
195
+ * Fallback keys are deliberately not recognized here. They only unwrap when exactly one of them
196
+ * carries a string, and a second one can still arrive, so no prefix decides them — which makes
197
+ * them indistinguishable from any other undecided object and lets one HOLD cover both.
198
+ */
199
+ export function scanFreeformWrapper(text: string): FreeformWrapperScan {
200
+ // One clamp rather than a budget threaded through every helper. Every helper already holds
201
+ // when it runs off the end of what it can see, so a buffer whose classification needs more
202
+ // than this holds for exactly the right reason, and no scan can cost more than this many
203
+ // characters however large the arguments grow. Indices into the clamp are indices into the
204
+ // full text, because the clamp is a prefix of it.
205
+ const bounded = text.length > MAX_FREEFORM_WRAPPER_SCAN_CHARS
206
+ ? text.slice(0, MAX_FREEFORM_WRAPPER_SCAN_CHARS)
207
+ : text;
208
+ // `parse` is true only where this scan actually SAW the object close. Every other hold ran
209
+ // out of buffer or out of budget, and in both cases asking `JSON.parse` is work with no
210
+ // possible payoff: the first is provably incomplete, and the second would re-read a growing
211
+ // buffer on every delta that happens to end in a brace — repeated braces inside a long
212
+ // unterminated string are enough to make that quadratic. Holding a budget-exhausted prefix
213
+ // costs nothing that matters, because the value it would release arrives in the same instant
214
+ // as the authoritative completion that follows it.
215
+ const hold = (): FreeformWrapperScan => ({ kind: "hold", parse: false });
216
+ const open = skipWhitespace(bounded, 0);
217
+ if (open === HOLD) return hold();
218
+ // Not an object, so no wrapper rule reaches it: arrays, scalars and ordinary bodies all
219
+ // complete as themselves.
220
+ if (bounded[open] !== "{") return { kind: "raw" };
221
+
222
+ let i = open + 1;
223
+ for (;;) {
224
+ const at = skipWhitespace(bounded, i);
225
+ if (at === HOLD) return hold();
226
+ if (bounded[at] === "}") return afterTopLevelClose(bounded, at + 1);
227
+ if (bounded[at] !== '"') return { kind: "raw" };
228
+
229
+ const nameEnd = scanString(bounded, at);
230
+ if (nameEnd === HOLD) return hold();
231
+ if (nameEnd === NEVER) return { kind: "raw" };
232
+ let name: string;
233
+ try {
234
+ // The authoritative decoder for the name, so `{"\u0069nput":...}` resolves to the same
235
+ // key completion sees. Decoding it by hand here is the second implementation that would
236
+ // drift from `JSON.parse`, which is how this defect existed in the first place.
237
+ name = JSON.parse(bounded.slice(at, nameEnd)) as string;
238
+ } catch {
239
+ return { kind: "raw" };
240
+ }
241
+
242
+ const colon = skipWhitespace(bounded, nameEnd);
243
+ if (colon === HOLD) return hold();
244
+ if (bounded[colon] !== ":") return { kind: "raw" };
245
+ const valueAt = skipWhitespace(bounded, colon + 1);
246
+ if (valueAt === HOLD) return hold();
247
+
248
+ if (name === CANONICAL_KEY) {
249
+ // An own `input` wins over everything else in the object, so this is decidable now. A
250
+ // non-string value is decidable too, in the other direction: completion hands back the
251
+ // argument text unchanged rather than unwrapping a value it cannot use.
252
+ return bounded[valueAt] === '"'
253
+ ? { kind: "input", valueStart: valueAt + 1 }
254
+ : { kind: "raw" };
255
+ }
256
+
257
+ const valueEnd = scanValue(bounded, valueAt);
258
+ if (valueEnd === HOLD) return hold();
259
+ if (valueEnd === NEVER) return { kind: "raw" };
260
+
261
+ const next = skipWhitespace(bounded, valueEnd);
262
+ if (next === HOLD) return hold();
263
+ if (bounded[next] === ",") {
264
+ i = next + 1;
265
+ continue;
266
+ }
267
+ if (bounded[next] === "}") return afterTopLevelClose(bounded, next + 1);
268
+ return { kind: "raw" };
269
+ }
270
+ }
271
+
272
+ /**
273
+ * The object closed without a canonical key. Only whitespace may follow one that parses, so
274
+ * anything else makes the text raw; otherwise the completed parse decides between a fallback
275
+ * wrapper and no wrapper at all.
276
+ */
277
+ function afterTopLevelClose(text: string, from: number): FreeformWrapperScan {
278
+ return skipWhitespace(text, from) === HOLD ? { kind: "hold", parse: true } : { kind: "raw" };
279
+ }
@@ -10,7 +10,90 @@ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
10
10
  },
11
11
  ];
12
12
 
13
- /** True when forwarding this hosted tool to the model would be rejected upstream. */
14
- export function isHostedToolUnsupportedForModel(modelId: string, tool: string, baseUrl?: string): boolean {
13
+ /**
14
+ * Hosted-tool declaration names an operator may list in `unsupportedHostedTools`.
15
+ *
16
+ * A gateway must be able to deny anything a client can send it, so this is the set of
17
+ * nameless hosted/private declaration types the proxy recognizes on a Responses request.
18
+ * It is deliberately a closed vocabulary: the provider config schema ends in
19
+ * `.passthrough()`, so an unvalidated misspelling would be accepted, persisted, and then
20
+ * silently strip nothing -- which is the exact 400 the operator set the field to avoid
21
+ * (the `codexToolMode` lesson in #2106).
22
+ */
23
+ export const DECLARABLE_HOSTED_TOOL_TYPES: ReadonlySet<string> = new Set([
24
+ "web_search",
25
+ "web_search_preview",
26
+ "file_search",
27
+ "computer_use_preview",
28
+ "computer_use",
29
+ "code_interpreter",
30
+ "image_generation",
31
+ "image_gen",
32
+ "mcp",
33
+ "tool_search",
34
+ "local_shell",
35
+ "x_search",
36
+ ]);
37
+
38
+ /**
39
+ * Spellings that name one capability. Declaring either member denies both, because the
40
+ * rest of the proxy already treats these as a single tool: the parser folds
41
+ * `web_search_preview` onto one name (`src/responses/parser-tools.ts`), Chat ingress
42
+ * accepts the pair together (`src/chat/inbound.ts`), and the canonical-field strip lists
43
+ * the pair in a single `toolTypes` set
44
+ * (`src/adapters/openai-responses/request-strips.ts`).
45
+ *
46
+ * Without the alias a capability declaration would be honoured for the spelling the
47
+ * operator happened to write and ignored for the one the client happened to send, which
48
+ * reproduces the original rejection while the config claims to have prevented it.
49
+ */
50
+ const HOSTED_TOOL_ALIAS_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
51
+ new Set(["web_search", "web_search_preview"]),
52
+ new Set(["image_generation", "image_gen"]),
53
+ new Set(["computer_use_preview", "computer_use"]),
54
+ ];
55
+
56
+ const NO_DECLARED_HOSTED_TOOLS: ReadonlySet<string> = new Set();
57
+
58
+ /**
59
+ * Expand a provider's declared denials through the alias groups once per request, so the
60
+ * per-tool predicate stays a set lookup. Returns a shared empty set when the provider
61
+ * declares nothing, keeping the common path allocation-free.
62
+ */
63
+ export function declaredUnsupportedHostedTools(
64
+ provider?: { unsupportedHostedTools?: readonly string[] },
65
+ ): ReadonlySet<string> {
66
+ const declared = provider?.unsupportedHostedTools;
67
+ if (!Array.isArray(declared) || declared.length === 0) return NO_DECLARED_HOSTED_TOOLS;
68
+ const out = new Set<string>();
69
+ for (const raw of declared) {
70
+ if (typeof raw !== "string") continue;
71
+ const tool = raw.trim();
72
+ if (!tool) continue;
73
+ out.add(tool);
74
+ for (const group of HOSTED_TOOL_ALIAS_GROUPS) {
75
+ if (!group.has(tool)) continue;
76
+ for (const alias of group) out.add(alias);
77
+ }
78
+ }
79
+ return out.size > 0 ? out : NO_DECLARED_HOSTED_TOOLS;
80
+ }
81
+
82
+ /**
83
+ * True when forwarding this hosted tool to the model would be rejected upstream.
84
+ *
85
+ * `declaredUnsupported` is the provider's own capability declaration, expanded by
86
+ * `declaredUnsupportedHostedTools`. It is additive to the built-in table rather than a
87
+ * replacement for it: the table covers destinations that reject a tool regardless of how
88
+ * the operator configured them, so an operator who never heard of the field stays
89
+ * protected.
90
+ */
91
+ export function isHostedToolUnsupportedForModel(
92
+ modelId: string,
93
+ tool: string,
94
+ baseUrl?: string,
95
+ declaredUnsupported?: ReadonlySet<string>,
96
+ ): boolean {
97
+ if (declaredUnsupported?.has(tool)) return true;
15
98
  return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId, baseUrl) && entry.tools.has(tool));
16
99
  }