@bitkyc08/opencodex 2.57.0 → 2.59.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 (241) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  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 +2 -2
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
  10. package/src/adapters/command-code.ts +13 -4
  11. package/src/adapters/cursor/catalog.ts +11 -0
  12. package/src/adapters/cursor/cursor-errors.ts +15 -0
  13. package/src/adapters/cursor/discovery.ts +65 -1
  14. package/src/adapters/cursor/effort-map.ts +16 -2
  15. package/src/adapters/cursor/envelope-echo.ts +55 -2
  16. package/src/adapters/cursor/live-transport.ts +5 -1
  17. package/src/adapters/cursor/message-mapper.ts +3 -2
  18. package/src/adapters/cursor/protobuf-events.ts +110 -11
  19. package/src/adapters/cursor/protobuf-request.ts +27 -6
  20. package/src/adapters/cursor/request-builder.ts +14 -3
  21. package/src/adapters/cursor/text-toolcall.ts +230 -0
  22. package/src/adapters/cursor/thread-continuity.ts +141 -0
  23. package/src/adapters/cursor/tool-guidance.ts +5 -4
  24. package/src/adapters/cursor/types.ts +5 -0
  25. package/src/adapters/cursor.ts +97 -6
  26. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  27. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  28. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  29. package/src/adapters/devin.ts +75 -13
  30. package/src/adapters/google-antigravity-wire.ts +29 -2
  31. package/src/adapters/google-http.ts +45 -13
  32. package/src/adapters/google.ts +23 -4
  33. package/src/adapters/mimo-free.ts +32 -17
  34. package/src/adapters/ollama-native.ts +42 -8
  35. package/src/adapters/openai-chat/response-events.ts +61 -0
  36. package/src/adapters/openai-chat.ts +5 -10
  37. package/src/adapters/openai-responses/passthrough.ts +40 -5
  38. package/src/adapters/openai-responses/request-strips.ts +43 -0
  39. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  40. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  41. package/src/adapters/physical-send.ts +50 -0
  42. package/src/adapters/responses-tool-schema.ts +76 -46
  43. package/src/adapters/run-turn-queue.ts +17 -4
  44. package/src/bridge/response-json.ts +2 -2
  45. package/src/bridge/sse.ts +166 -25
  46. package/src/claude/context-windows.ts +22 -0
  47. package/src/claude/outbound.ts +46 -5
  48. package/src/cli/account-api.ts +4 -3
  49. package/src/cli/account-extended.ts +22 -2
  50. package/src/cli/account-orca-import.ts +63 -0
  51. package/src/cli/account.ts +32 -4
  52. package/src/cli/capabilities.ts +40 -0
  53. package/src/cli/claude.ts +29 -1
  54. package/src/cli/codex-cli-update.ts +97 -2
  55. package/src/cli/config-command.ts +35 -18
  56. package/src/cli/dispatch.ts +71 -4
  57. package/src/cli/doctor.ts +197 -2
  58. package/src/cli/help.ts +4 -1
  59. package/src/cli/index.ts +132 -22
  60. package/src/cli/models-runtime.ts +33 -4
  61. package/src/cli/registry.ts +11 -1
  62. package/src/cli/runtime-api.ts +44 -0
  63. package/src/cli/start-args.ts +94 -0
  64. package/src/cli/system-command.ts +72 -1
  65. package/src/cli/uninstall-client-state.ts +12 -0
  66. package/src/client/machine-api.ts +4 -3
  67. package/src/client/machine-listener.ts +14 -1
  68. package/src/clients/config-export/constants.ts +2 -3
  69. package/src/clients/config-export.ts +5 -5
  70. package/src/codex/account-store.ts +81 -5
  71. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  72. package/src/codex/auth-api/routes.ts +17 -2
  73. package/src/codex/auth-context.ts +58 -20
  74. package/src/codex/catalog/build-entries.ts +25 -4
  75. package/src/codex/catalog/derive-entry.ts +8 -1
  76. package/src/codex/catalog/effort.ts +10 -6
  77. package/src/codex/catalog/gather-capture.ts +1 -0
  78. package/src/codex/catalog/model-hints.ts +37 -5
  79. package/src/codex/catalog/parsing.ts +83 -5
  80. package/src/codex/catalog/reserve-warn.ts +96 -0
  81. package/src/codex/catalog/retained-sync.ts +19 -0
  82. package/src/codex/catalog/routed-gather.ts +42 -3
  83. package/src/codex/cli-installation-identity.ts +210 -0
  84. package/src/codex/cli-installation-targets.ts +158 -0
  85. package/src/codex/convergence.ts +5 -0
  86. package/src/codex/desktop-switches.ts +145 -0
  87. package/src/codex/history-job.ts +5 -1
  88. package/src/codex/history-provider.ts +37 -5
  89. package/src/codex/history-state-open.ts +105 -0
  90. package/src/codex/history-worker.ts +14 -1
  91. package/src/codex/inject/config-toml.ts +44 -2
  92. package/src/codex/inject/remove.ts +145 -7
  93. package/src/codex/inject/restore.ts +204 -32
  94. package/src/codex/inject.ts +6 -9
  95. package/src/codex/lineage.ts +83 -32
  96. package/src/codex/loopback-target.ts +40 -0
  97. package/src/codex/main-account-hard-lock.ts +2 -1
  98. package/src/codex/main-account.ts +10 -3
  99. package/src/codex/main-device-reauth.ts +17 -9
  100. package/src/codex/model-entitlements.ts +60 -1
  101. package/src/codex/native-profile-startup.ts +64 -20
  102. package/src/codex/observed-model-denials.ts +137 -0
  103. package/src/codex/orca-auth-source.ts +94 -0
  104. package/src/codex/orca-import.ts +219 -0
  105. package/src/codex/prompt-text-probe.ts +282 -12
  106. package/src/codex/quota-401-recovery.ts +12 -0
  107. package/src/codex/quota-types.ts +65 -0
  108. package/src/codex/quota.ts +24 -19
  109. package/src/codex/routing/cooldown-math.ts +8 -47
  110. package/src/codex/routing/pin-drain.ts +57 -0
  111. package/src/codex/routing.ts +13 -15
  112. package/src/codex/subagent-model-fallback.ts +94 -0
  113. package/src/codex/windows-installation-files.ts +224 -0
  114. package/src/combos/failover.ts +122 -5
  115. package/src/config/atomic-write.ts +83 -8
  116. package/src/config/diagnostics.ts +21 -0
  117. package/src/config/load-degrade.ts +15 -0
  118. package/src/config/pending-teardown.ts +8 -0
  119. package/src/config/process-state.ts +36 -3
  120. package/src/config/provider-relative-send-path.ts +16 -0
  121. package/src/config/proxy-env.ts +23 -5
  122. package/src/config/schema/config-schema.ts +23 -0
  123. package/src/config/schema/leaf-validators.ts +65 -17
  124. package/src/generated/compatibility-version.json +337 -201
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/lib/bounded-body.ts +4 -2
  127. package/src/lib/bounded-subprocess.ts +62 -10
  128. package/src/lib/destination-policy.ts +48 -6
  129. package/src/lib/errors.ts +3 -15
  130. package/src/lib/local-destinations.ts +32 -5
  131. package/src/lib/provider-outbound.ts +3 -3
  132. package/src/lib/proxy-env.ts +70 -3
  133. package/src/lib/request-execution-budget.ts +11 -3
  134. package/src/lib/response-body-inactivity.ts +193 -0
  135. package/src/lib/retry-delay.ts +69 -0
  136. package/src/lib/socks5-fetch.ts +631 -0
  137. package/src/lib/spend-reservation-ledger.ts +115 -9
  138. package/src/lib/windows-secret-acl.ts +151 -15
  139. package/src/lib/windows-user-principal.ts +5 -1
  140. package/src/lib/workflow-budget.ts +145 -8
  141. package/src/oauth/account-quota-rank.ts +72 -15
  142. package/src/oauth/generic-account-failover.ts +40 -27
  143. package/src/oauth/orcarouter.ts +15 -2
  144. package/src/oauth/store.ts +8 -0
  145. package/src/providers/codex-capacity.ts +9 -0
  146. package/src/providers/derive.ts +6 -0
  147. package/src/providers/devin-provider-merge-migration.ts +33 -12
  148. package/src/providers/free-directory.ts +20 -2
  149. package/src/providers/key-failover.ts +261 -7
  150. package/src/providers/model-discovery.ts +19 -7
  151. package/src/providers/model-rename-migration.ts +1 -0
  152. package/src/providers/openai-sidecar.ts +4 -0
  153. package/src/providers/opencode-go-transport.ts +14 -5
  154. package/src/providers/quota/report-cache.ts +3 -0
  155. package/src/providers/registry/entries-core.ts +11 -0
  156. package/src/providers/registry/entries-extended.ts +146 -28
  157. package/src/providers/registry/model-seeds.ts +136 -29
  158. package/src/providers/registry/types.ts +9 -0
  159. package/src/responses/apply-patch-envelope.ts +44 -11
  160. package/src/responses/bridge-search-replay-cache.ts +152 -0
  161. package/src/responses/code-mode-helper-compat.ts +26 -16
  162. package/src/responses/custom-tool-compat.ts +1 -1
  163. package/src/responses/hosted-tool-policy.ts +85 -2
  164. package/src/responses/schema.ts +9 -2
  165. package/src/responses/spill-store.ts +17 -0
  166. package/src/responses/state/body-policy.ts +25 -0
  167. package/src/responses/state/spill-queue.ts +8 -6
  168. package/src/responses/state.ts +3 -22
  169. package/src/router.ts +4 -0
  170. package/src/server/auth-cors.ts +27 -0
  171. package/src/server/chat-completions.ts +9 -4
  172. package/src/server/chat-native-sse.ts +26 -9
  173. package/src/server/chat-native.ts +10 -4
  174. package/src/server/claude-messages.ts +24 -2
  175. package/src/server/gui-static.ts +36 -2
  176. package/src/server/inbound-body-admission.ts +187 -0
  177. package/src/server/index/websocket-handler.ts +48 -1
  178. package/src/server/index.ts +15 -19
  179. package/src/server/management/api-access.ts +3 -4
  180. package/src/server/management/config-routes.ts +57 -10
  181. package/src/server/management/provider-capability-config.ts +35 -7
  182. package/src/server/management/provider-routes.ts +70 -18
  183. package/src/server/models-capabilities.ts +24 -3
  184. package/src/server/proxy-liveness.ts +97 -2
  185. package/src/server/relay.ts +17 -24
  186. package/src/server/request-log.ts +25 -1
  187. package/src/server/responses/adapter-continuation.ts +71 -27
  188. package/src/server/responses/adapter-delivery.ts +39 -8
  189. package/src/server/responses/adapter-dispatch.ts +52 -24
  190. package/src/server/responses/codex-ws-exchange.ts +65 -4
  191. package/src/server/responses/combo-stream-preflight.ts +68 -5
  192. package/src/server/responses/compact.ts +60 -11
  193. package/src/server/responses/core-codex-account.ts +83 -22
  194. package/src/server/responses/core-combo.ts +26 -0
  195. package/src/server/responses/core-normalize.ts +12 -5
  196. package/src/server/responses/core-options.ts +3 -0
  197. package/src/server/responses/fetch-helpers.ts +72 -3
  198. package/src/server/responses/native-injection-protocol.ts +42 -0
  199. package/src/server/responses/native-injection-replay.ts +105 -0
  200. package/src/server/responses/native-injection.ts +242 -0
  201. package/src/server/responses/native-response-control.ts +56 -0
  202. package/src/server/responses/native-response-json.ts +14 -0
  203. package/src/server/responses/native-response-output.ts +37 -0
  204. package/src/server/responses/native-steering-log.ts +44 -0
  205. package/src/server/responses/native-steering-policy.ts +49 -0
  206. package/src/server/responses/native-steering-replay.ts +126 -0
  207. package/src/server/responses/native-steering-settings.ts +76 -0
  208. package/src/server/responses/native-steering.ts +400 -0
  209. package/src/server/responses/native-tool-results.ts +130 -0
  210. package/src/server/responses/passthrough-delivery.ts +21 -1
  211. package/src/server/responses/passthrough-dispatch.ts +146 -49
  212. package/src/server/responses/passthrough-execution.ts +11 -1
  213. package/src/server/responses/request-prepare.ts +70 -0
  214. package/src/server/responses/request-send-budget.ts +84 -7
  215. package/src/server/responses/request-sidecar-auth.ts +16 -8
  216. package/src/server/responses/request-spend.ts +38 -9
  217. package/src/server/responses/request-transport.ts +13 -10
  218. package/src/server/responses/run-turn-execution.ts +20 -5
  219. package/src/server/responses/sidecar-execution.ts +2 -0
  220. package/src/server/responses/ws-upstream.ts +23 -2
  221. package/src/server/responses-custom-tool-repair.ts +2 -2
  222. package/src/server/sse-frame-buffer.ts +12 -10
  223. package/src/server/sse-payload-rewrite.ts +36 -9
  224. package/src/server/stop-teardown.ts +8 -1
  225. package/src/server/system-env-shell.ts +5 -1
  226. package/src/server/system-env.ts +7 -1
  227. package/src/server/workflow-refusal.ts +56 -2
  228. package/src/server/ws-bridge.ts +16 -1
  229. package/src/service/cli.ts +29 -7
  230. package/src/service/guards.ts +10 -0
  231. package/src/service/health.ts +43 -0
  232. package/src/service/state.ts +7 -2
  233. package/src/types/accounts.ts +4 -0
  234. package/src/types/config.ts +104 -3
  235. package/src/types/provider.ts +32 -0
  236. package/src/types/request.ts +7 -1
  237. package/src/types/wire.ts +9 -1
  238. package/src/usage/expected-prices.ts +28 -0
  239. package/src/usage/log.ts +87 -4
  240. package/src/web-search/passthrough-bridge.ts +39 -5
  241. package/gui/dist/assets/index-Cz7CLdif.js +0 -128
@@ -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,
@@ -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
  }
@@ -126,10 +126,17 @@ export const toolSchema = z.object({
126
126
 
127
127
  const builtinToolSchema = z.object({ type: z.string() }).loose();
128
128
 
129
- const hostedToolType = z.enum([
129
+ /**
130
+ * Hosted tool types a client may declare on an inbound Responses request. Exported so the
131
+ * provider-side capability vocabulary in `src/responses/hosted-tool-policy.ts` can be
132
+ * asserted to cover all of them: a gateway must be able to deny anything it can be sent.
133
+ */
134
+ export const HOSTED_TOOL_TYPES = [
130
135
  "web_search", "web_search_preview", "file_search", "computer_use_preview",
131
136
  "code_interpreter", "image_generation", "mcp",
132
- ]);
137
+ ] as const;
138
+
139
+ const hostedToolType = z.enum(HOSTED_TOOL_TYPES);
133
140
 
134
141
  const allowedToolEntrySchema = z.object({ type: z.string(), name: z.string().optional() });
135
142
 
@@ -159,6 +159,23 @@ function spillNow(): number {
159
159
  return spillNowOverride?.() ?? Date.now();
160
160
  }
161
161
 
162
+ /**
163
+ * The spill deadline clock, shared with the shutdown drain in `state/spill-queue.ts`.
164
+ *
165
+ * Every deadline the shutdown path enforces has to read the same clock the work it
166
+ * budgets reads. When the drain measured its reserve on `Date.now()` while the ACL
167
+ * harden it was budgeting ran on this injected clock, a test could freeze the clock,
168
+ * believe it had removed wall time from the case, and still lose an 80 ms reserve to
169
+ * real elapsed time on a loaded runner — which is what turned
170
+ * `shutdown fallback prices the job-owned superseded generation before publishing`
171
+ * red on macOS 2/2 in run 35137850114 while the assertion it was written for never ran.
172
+ *
173
+ * Production is unchanged: with no override installed this is `Date.now()`.
174
+ */
175
+ export function responseSpillNow(): number {
176
+ return spillNow();
177
+ }
178
+
162
179
  function record(event: "write" | "fsync" | "close" | "harden" | "publish" | "dir-fsync" | "stub-swap"): void {
163
180
  spillIoForTest?.record?.(event);
164
181
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Request bodies that must never enter the continuation cache.
3
+ *
4
+ * The cache is persisted to `responses-state.json`, so anything recorded here reaches disk.
5
+ * Encrypted-agent-task recovery decrypts task text into the request body and promises
6
+ * in-memory, TTL-bounded retention; recording that body would put the plaintext on disk with
7
+ * no TTL and break the promise.
8
+ *
9
+ * A WeakSet rather than a body field on purpose: `_rawBody` is serialized verbatim by the
10
+ * native passthrough, so any marker written into the body itself would be sent upstream.
11
+ * Marking is enforced once here rather than at each call site, because every recording path
12
+ * (streaming, non-streaming, passthrough, forced) funnels through `rememberResponseState` —
13
+ * a new call site cannot reintroduce the leak by forgetting a guard.
14
+ */
15
+ const nonPersistableBodies = new WeakSet<object>();
16
+
17
+ /** Bar this exact request body from the continuation cache, and therefore from disk. */
18
+ export function markBodyNonPersistable(body: unknown): void {
19
+ if (body && typeof body === "object") nonPersistableBodies.add(body as object);
20
+ }
21
+
22
+ /** Test the body's in-memory persistence restriction without adding a wire marker. */
23
+ export function isBodyNonPersistable(body: unknown): boolean {
24
+ return !!body && typeof body === "object" && nonPersistableBodies.has(body);
25
+ }
@@ -7,6 +7,7 @@ import {
7
7
  MAX_RESPONSE_SPILL_PAYLOAD_BYTES,
8
8
  prospectiveResponseSpillBytes,
9
9
  responseSpillPayloadCap,
10
+ responseSpillNow,
10
11
  type ResponseSpillPublicationControl,
11
12
  type ResponseSpillRef,
12
13
  writeResponseSpillDurably,
@@ -377,7 +378,7 @@ function responseSpillShutdownBudget(): { totalMs: number; fallbackReserveMs: nu
377
378
  }
378
379
 
379
380
  function awaitResponseSpillTailUntil(observed: Promise<void>, deadline: number): Promise<boolean> {
380
- const remaining = deadline - Date.now();
381
+ const remaining = deadline - responseSpillNow();
381
382
  if (remaining <= 0) return Promise.resolve(false);
382
383
  return new Promise(resolve => {
383
384
  let finished = false;
@@ -554,12 +555,13 @@ function terminalizeExhaustedShutdownFallback(
554
555
  }
555
556
 
556
557
  function fallbackPendingResponseSpills(reserveMs: number): Error[] {
557
- const deadline = Date.now() + reserveMs;
558
+ // Same clock as the harden work this reserve is budgeting — see `responseSpillNow`.
559
+ const deadline = responseSpillNow() + reserveMs;
558
560
  const failures: Error[] = [];
559
561
  for (;;) {
560
562
  const pending = pendingShutdownFallbackCandidates();
561
563
  if (pending.length === 0) return failures;
562
- if (Date.now() >= deadline) {
564
+ if (responseSpillNow() >= deadline) {
563
565
  terminalizeExhaustedShutdownFallback(pending, failures);
564
566
  return failures;
565
567
  }
@@ -569,7 +571,7 @@ function fallbackPendingResponseSpills(reserveMs: number): Error[] {
569
571
  for (let index = 0; index < pending.length; index += 1) {
570
572
  const { job, candidate } = pending[index]!;
571
573
  if (requireStore().currentEntry(job.id) !== candidate) continue;
572
- const remaining = deadline - Date.now();
574
+ const remaining = deadline - responseSpillNow();
573
575
  if (remaining <= 0) {
574
576
  reserveExhausted = true;
575
577
  for (const exhausted of pending.slice(index)) {
@@ -587,7 +589,7 @@ function fallbackPendingResponseSpills(reserveMs: number): Error[] {
587
589
  requireStore().recomputeOldestResident();
588
590
  requireStore().pruneResponses();
589
591
  enforceAppOwnedMemoryBudget();
590
- if (reserveExhausted || Date.now() >= deadline) {
592
+ if (reserveExhausted || responseSpillNow() >= deadline) {
591
593
  terminalizeExhaustedShutdownFallback(pendingShutdownFallbackCandidates(), failures);
592
594
  return failures;
593
595
  }
@@ -597,7 +599,7 @@ function fallbackPendingResponseSpills(reserveMs: number): Error[] {
597
599
  export async function drainResponseSpillPublications(): Promise<void> {
598
600
  const budget = responseSpillShutdownBudget();
599
601
  const fallbackReserveMs = Math.min(budget.totalMs, Math.max(1, budget.fallbackReserveMs));
600
- const drainDeadline = Date.now() + Math.max(0, budget.totalMs - fallbackReserveMs);
602
+ const drainDeadline = responseSpillNow() + Math.max(0, budget.totalMs - fallbackReserveMs);
601
603
 
602
604
  for (;;) {
603
605
  if (pendingResponseSpills.size === 0) return;
@@ -23,6 +23,8 @@ import type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseS
23
23
  export { responseAdmissionCountersForTests } from "./state/spill-failure";
24
24
  import { admissionCounters, noteSpillWriteFailure, noteSpillWriteSuccess, spillCounters, spillWriteHealth } from "./state/spill-failure";
25
25
  import { loadSnapshotEntry } from "./state/snapshot-codec";
26
+ import { isBodyNonPersistable } from "./state/body-policy";
27
+ export { isBodyNonPersistable, markBodyNonPersistable } from "./state/body-policy";
26
28
  export { flushPendingResponseSpillsForTests, awaitResponseSpillPublicationTailForTests, pendingResponseSpillMetricsForTests, setResponseSpillShutdownBudgetForTests, setResponseSpillAsyncAclAttemptBudgetForTests, setResponseSpillShutdownTerminalizationPassLimitForTests } from "./state/spill-queue";
27
29
  import {
28
30
  bindSpillQueueStore,
@@ -1235,27 +1237,6 @@ export function responseStateMetrics(): ResponseStateMetrics {
1235
1237
  * Cache completed output and max_output_tokens partial output for previous_response_id replay.
1236
1238
  * Content-filtered incomplete and failed output are not authoritative replay history.
1237
1239
  */
1238
- /**
1239
- * Request bodies that must never enter the continuation cache.
1240
- *
1241
- * The cache is persisted to `responses-state.json`, so anything recorded here reaches disk.
1242
- * Encrypted-agent-task recovery decrypts task text into the request body and promises
1243
- * in-memory, TTL-bounded retention; recording that body would put the plaintext on disk with
1244
- * no TTL and break the promise.
1245
- *
1246
- * A WeakSet rather than a body field on purpose: `_rawBody` is serialized verbatim by the
1247
- * native passthrough, so any marker written into the body itself would be sent upstream.
1248
- * Marking is enforced once here rather than at each call site, because every recording path
1249
- * (streaming, non-streaming, passthrough, forced) funnels through `rememberResponseState` —
1250
- * a new call site cannot reintroduce the leak by forgetting a guard.
1251
- */
1252
- const nonPersistableBodies = new WeakSet<object>();
1253
-
1254
- /** Bar this exact request body from the continuation cache, and therefore from disk. */
1255
- export function markBodyNonPersistable(body: unknown): void {
1256
- if (body && typeof body === "object") nonPersistableBodies.add(body as object);
1257
- }
1258
-
1259
1240
  export function rememberResponseState(
1260
1241
  requestBody: unknown,
1261
1242
  response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
@@ -1264,7 +1245,7 @@ export function rememberResponseState(
1264
1245
  ): void {
1265
1246
  if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
1266
1247
  const request = requestBody as Record<string, unknown>;
1267
- if (nonPersistableBodies.has(request)) return;
1248
+ if (isBodyNonPersistable(request)) return;
1268
1249
  // `force` bypasses only the store:false skip: Codex sends `store:false` on every non-Azure
1269
1250
  // HTTP request (and WS inherits it), yet its WS turns still chain with previous_response_id.
1270
1251
  // The passthrough branch records with force so those chains can be expanded locally; the
package/src/router.ts CHANGED
@@ -384,6 +384,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
384
384
  && registryEntry.requiresAdjacentResponsesToolResults !== undefined
385
385
  ? { requiresAdjacentResponsesToolResults: registryEntry.requiresAdjacentResponsesToolResults }
386
386
  : {}),
387
+ ...(provider.requiresPairedResponsesToolResults === undefined
388
+ && registryEntry.requiresPairedResponsesToolResults !== undefined
389
+ ? { requiresPairedResponsesToolResults: registryEntry.requiresPairedResponsesToolResults }
390
+ : {}),
387
391
  ...(provider.annotateEmptyToolOutputs === undefined
388
392
  && registryEntry.annotateEmptyToolOutputs !== undefined
389
393
  ? { annotateEmptyToolOutputs: registryEntry.annotateEmptyToolOutputs }
@@ -1,3 +1,4 @@
1
+ import { providerRelativeSendPathConfigError } from "../config/provider-relative-send-path";
1
2
  import { modelCapabilitiesConfigError } from "../config/provider-validation";
2
3
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3
4
  import { initialModelSelection } from "../providers/initial-model-selection";
@@ -29,6 +30,7 @@ import {
29
30
  } from "../config/provider-validation";
30
31
  import { providerDestinationConfigError } from "../lib/destination-policy";
31
32
  import { redactSecretString } from "../lib/redact";
33
+ import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
32
34
  import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
33
35
  import { providerConfigSeed } from "../providers/derive";
34
36
  import type { OcxConfig, OcxProviderConfig } from "../types";
@@ -754,6 +756,10 @@ export function providerManagementConfigError(
754
756
  }
755
757
  const destinationError = providerDestinationConfigError(name, typed);
756
758
  if (destinationError) return `provider ${name} ${destinationError}`;
759
+ for (const field of ["responsesPath", "chatCompletionsPath"] as const) {
760
+ const sendPathError = providerRelativeSendPathConfigError(field, raw[field]);
761
+ if (sendPathError) return `provider ${JSON.stringify(redactSecretString(name))} ${sendPathError}`;
762
+ }
757
763
  const headersError = providerHeadersConfigError(typed.headers);
758
764
  if (headersError) return `provider ${name} ${headersError}`;
759
765
  const retryOn429Error = retryOn429PolicyConfigError(raw.retryOn429);
@@ -793,6 +799,8 @@ export function providerManagementConfigError(
793
799
  }
794
800
  const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
795
801
  if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
802
+ const suppressSyntheticMaxError = booleanRecordConfigError(raw.modelSuppressSyntheticMax, "modelSuppressSyntheticMax");
803
+ if (suppressSyntheticMaxError) return `provider ${name} ${suppressSyntheticMaxError}`;
796
804
  const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
797
805
  raw.modelReasoningSummaryDelivery,
798
806
  raw.modelSupportsReasoningSummaries,
@@ -834,6 +842,22 @@ export function providerManagementConfigError(
834
842
  "omitReasoningEffortWithToolsModels",
835
843
  );
836
844
  if (toolReasoningOptOutError) return `provider ${name} ${toolReasoningOptOutError}`;
845
+ const unsupportedHostedToolsError = nonBlankStringArrayConfigError(
846
+ raw.unsupportedHostedTools,
847
+ "unsupportedHostedTools",
848
+ );
849
+ if (unsupportedHostedToolsError) return `provider ${name} ${unsupportedHostedToolsError}`;
850
+ if (Array.isArray(raw.unsupportedHostedTools)) {
851
+ // Closed vocabulary, same reason as the config schema: an unrecognized name would be
852
+ // stored and then strip nothing, so the operator would keep getting the upstream 400
853
+ // this field exists to prevent.
854
+ const unknownTool = (raw.unsupportedHostedTools as unknown[])
855
+ .find(tool => typeof tool === "string" && !DECLARABLE_HOSTED_TOOL_TYPES.has(tool.trim()));
856
+ if (unknownTool !== undefined) {
857
+ return `provider ${name} unsupportedHostedTools must name only hosted tool types: `
858
+ + `${[...DECLARABLE_HOSTED_TOOL_TYPES].join(", ")}`;
859
+ }
860
+ }
837
861
  const openRouterError = openRouterRoutingConfigError(typed);
838
862
  if (openRouterError) return `provider ${name} ${openRouterError}`;
839
863
  const vercelError = vercelGatewayRoutingConfigError(typed);
@@ -913,6 +937,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
913
937
  commandCodeVersion: "editor",
914
938
  statelessResponses: "editor",
915
939
  requiresAdjacentResponsesToolResults: "editor",
940
+ requiresPairedResponsesToolResults: "editor",
916
941
  annotateEmptyToolOutputs: "editor",
917
942
  supportsServiceTier: "editor",
918
943
  modelSupportsServiceTier: "editor",
@@ -963,6 +988,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
963
988
  refreshPolicy: "editor",
964
989
  reasoningEfforts: "editor",
965
990
  modelReasoningEfforts: "editor",
991
+ modelSuppressSyntheticMax: "editor",
966
992
  modelDefaultReasoningEfforts: "editor",
967
993
  pinnedReasoningEffort: "editor",
968
994
  modelPinnedReasoningEfforts: "editor",
@@ -978,6 +1004,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
978
1004
  xaiResponsesDefaultVersion: "runtime",
979
1005
  zaiResponsesDefaultVersion: "runtime",
980
1006
  supportsResponsesCustomTools: "editor",
1007
+ unsupportedHostedTools: "editor",
981
1008
  responsesSnapshotRepair: "editor",
982
1009
  webSearchBridge: "editor",
983
1010
  reasoningEffortMap: "editor",
@@ -150,10 +150,15 @@ async function handleChatCompletionsWithBudget(
150
150
  let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
151
151
  try {
152
152
  const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody));
153
- route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req));
154
- // Settle the wire once so every branch below reads the adapter this model will
155
- // actually use, not the provider-wide default (#404).
156
- route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat");
153
+ // Preserve the routed destination for Go recognition, then settle the wire before
154
+ // deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
155
+ const routedProvider = route.provider;
156
+ const wireProvider = resolveWireProtocolOverride(route.providerName, route.modelId, routedProvider, "chat");
157
+ route.provider = resolveOpenCodeGoTransport(
158
+ wireProvider,
159
+ getOrAllocateRequestSessionLane(req),
160
+ routedProvider,
161
+ );
157
162
  logCtx.model = route.modelId;
158
163
  logCtx.providerAdapter = route.provider.adapter;
159
164
  logCtx.requestedModel = requestedModel;