@bitkyc08/opencodex 2.55.0 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,293 @@
1
+ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
2
+ import { isHostedToolUnsupportedForModel } from "../../responses/hosted-tool-policy";
3
+ import { debugProviderDiagnostic } from "../../lib/debug";
4
+ import { stripUnicodePropertyPatterns } from "../responses-tool-schema";
5
+ import {
6
+ isXaiSchemaTarget,
7
+ normalizeXaiToolParameters,
8
+ XaiToolSchemaCompatibilityError,
9
+ } from "../xai-tool-schema";
10
+ import { isPlainObject } from "./internal";
11
+
12
+ function normalizeFunctionToolSchema(tool: unknown, xaiTarget: boolean): unknown | undefined {
13
+ if (!isPlainObject(tool) || tool.type !== "function") return tool;
14
+ // Runs for every Responses destination, forward auth included: the ChatGPT backend is where
15
+ // the `\p{…}` rejection was observed, and it reaches this function through the same seam.
16
+ const compatible = stripUnicodePropertyPatterns(tool);
17
+ const source = isPlainObject(compatible) ? compatible : tool;
18
+ if (xaiTarget) {
19
+ const parameters = normalizeXaiToolParameters(isPlainObject(source.parameters) ? source.parameters : {});
20
+ return parameters === undefined ? undefined : { ...source, parameters };
21
+ }
22
+ if (isPlainObject(source.parameters) && source.parameters.type === "object") return source;
23
+ return {
24
+ ...source,
25
+ parameters: { ...(isPlainObject(source.parameters) ? source.parameters : {}), type: "object" },
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Re-point `tool_choice` after an incompatible function was dropped from the catalog. Names here
31
+ * are already wire names, because namespace lowering rewrote the declarations and the selector
32
+ * together before this runs. A selector left naming an omitted tool reaches Grok as a dangling
33
+ * reference it rejects, and silently relaxing it to `auto` is worse: the turn would quietly
34
+ * proceed without the tool the caller required. So an `allowed_tools` list drops the omitted
35
+ * entries while any remain, and a selection with nothing left to point at fails locally with the
36
+ * same 400 the caller gets for a tool catalog this proxy cannot lower.
37
+ */
38
+ function reconcileToolChoiceForOmittedTools(
39
+ body: Record<string, unknown>,
40
+ omittedFunctionNames: ReadonlySet<string>,
41
+ ): Record<string, unknown> {
42
+ if (omittedFunctionNames.size === 0) return body;
43
+ const toolChoice = body.tool_choice;
44
+ if (!isPlainObject(toolChoice)) return body;
45
+
46
+ const refuse = (name: string): never => {
47
+ throw new XaiToolSchemaCompatibilityError(
48
+ `tool_choice requires function "${name}", but its parameter schema cannot be represented for this destination; `
49
+ + "relax tool_choice or simplify the tool's parameter schema",
50
+ );
51
+ };
52
+
53
+ if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
54
+ return omittedFunctionNames.has(toolChoice.name) ? refuse(toolChoice.name) : body;
55
+ }
56
+
57
+ if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
58
+ const omitted = toolChoice.tools.filter(tool =>
59
+ isPlainObject(tool)
60
+ && tool.type === "function"
61
+ && typeof tool.name === "string"
62
+ && omittedFunctionNames.has(tool.name));
63
+ if (omitted.length === 0) return body;
64
+ const kept = toolChoice.tools.filter(tool => !omitted.includes(tool));
65
+ if (kept.length === 0) {
66
+ const first = omitted[0];
67
+ return refuse(isPlainObject(first) && typeof first.name === "string" ? first.name : "unknown");
68
+ }
69
+ return { ...body, tool_choice: { ...toolChoice, tools: kept } };
70
+ }
71
+
72
+ return body;
73
+ }
74
+
75
+ export function normalizeToolSchemas(body: unknown, xaiTarget: boolean): unknown {
76
+ if (!isPlainObject(body)) return body;
77
+
78
+ const omittedFunctionNames = new Set<string>();
79
+ const normalizeTools = (tools: unknown[]): unknown[] => {
80
+ let changed = false;
81
+ const normalized: unknown[] = [];
82
+ for (const tool of tools) {
83
+ const fixed = normalizeFunctionToolSchema(tool, xaiTarget);
84
+ if (fixed === undefined) {
85
+ changed = true;
86
+ if (isPlainObject(tool) && typeof tool.name === "string") omittedFunctionNames.add(tool.name);
87
+ continue;
88
+ }
89
+ if (fixed !== tool) changed = true;
90
+ normalized.push(fixed);
91
+ }
92
+ return changed ? normalized : tools;
93
+ };
94
+
95
+ let normalizedBody = body;
96
+ if (Array.isArray(body.tools)) {
97
+ const tools = normalizeTools(body.tools);
98
+ if (tools !== body.tools) normalizedBody = { ...normalizedBody, tools };
99
+ }
100
+ if (Array.isArray(normalizedBody.input)) {
101
+ let inputChanged = false;
102
+ const input = normalizedBody.input.map((item) => {
103
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
104
+ const tools = normalizeTools(item.tools);
105
+ if (tools === item.tools) return item;
106
+ inputChanged = true;
107
+ return { ...item, tools };
108
+ });
109
+ if (inputChanged) normalizedBody = { ...normalizedBody, input };
110
+ }
111
+ if (omittedFunctionNames.size > 0) {
112
+ // A dropped tool is a capability the caller declared and will not get, and the only other
113
+ // trace of it is a turn that never makes the call. Name them so the cause is recoverable.
114
+ debugProviderDiagnostic("openai-responses", "tool-schema-omitted", {
115
+ omitted: [...omittedFunctionNames],
116
+ });
117
+ }
118
+ return reconcileToolChoiceForOmittedTools(normalizedBody, omittedFunctionNames);
119
+ }
120
+
121
+ export function activateDeferredTool(tool: Record<string, unknown>): Record<string, unknown> {
122
+ const { defer_loading: _, ...activeTool } = tool;
123
+ if (tool.type !== "namespace" || !Array.isArray(tool.tools)) return activeTool;
124
+ return {
125
+ ...activeTool,
126
+ tools: tool.tools.map(inner => isPlainObject(inner) ? activateDeferredTool(inner) : inner),
127
+ };
128
+ }
129
+
130
+ function mergeLoadedTools(declaredTools: unknown[], loadedTools: unknown[]): unknown[] {
131
+ const merged = [...declaredTools];
132
+ let changed = false;
133
+
134
+ for (const candidate of loadedTools) {
135
+ if (!isPlainObject(candidate) || typeof candidate.name !== "string") continue;
136
+ const loaded = activateDeferredTool(candidate);
137
+ if (loaded.type === "namespace" && Array.isArray(loaded.tools)) {
138
+ const namespaceIndex = merged.findIndex(tool =>
139
+ isPlainObject(tool) && tool.type === "namespace" && tool.name === loaded.name
140
+ );
141
+ if (namespaceIndex < 0) {
142
+ merged.push(loaded);
143
+ changed = true;
144
+ continue;
145
+ }
146
+
147
+ const namespace = merged[namespaceIndex];
148
+ if (!isPlainObject(namespace)) continue;
149
+ const namespaceTools = Array.isArray(namespace.tools) ? namespace.tools : [];
150
+ const nextNamespaceTools = [...namespaceTools];
151
+ let namespaceChanged = "defer_loading" in namespace;
152
+ for (const tool of loaded.tools) {
153
+ if (!isPlainObject(tool) || typeof tool.name !== "string") continue;
154
+ const declaredIndex = nextNamespaceTools.findIndex(declared =>
155
+ isPlainObject(declared) && declared.name === tool.name
156
+ );
157
+ if (declaredIndex < 0) {
158
+ nextNamespaceTools.push(tool);
159
+ namespaceChanged = true;
160
+ continue;
161
+ }
162
+ const declared = nextNamespaceTools[declaredIndex];
163
+ if (isPlainObject(declared) && "defer_loading" in declared) {
164
+ nextNamespaceTools[declaredIndex] = activateDeferredTool(declared);
165
+ namespaceChanged = true;
166
+ }
167
+ }
168
+ if (!namespaceChanged) continue;
169
+ const { defer_loading: _, ...activeNamespace } = namespace;
170
+ merged[namespaceIndex] = { ...activeNamespace, tools: nextNamespaceTools };
171
+ changed = true;
172
+ continue;
173
+ }
174
+
175
+ const declaredIndex = merged.findIndex(tool =>
176
+ isPlainObject(tool) && tool.type !== "namespace" && tool.name === loaded.name
177
+ );
178
+ if (declaredIndex < 0) {
179
+ merged.push(loaded);
180
+ changed = true;
181
+ } else {
182
+ const declared = merged[declaredIndex];
183
+ if (isPlainObject(declared) && "defer_loading" in declared) {
184
+ merged[declaredIndex] = activateDeferredTool(declared);
185
+ changed = true;
186
+ }
187
+ }
188
+ }
189
+
190
+ return changed ? merged : declaredTools;
191
+ }
192
+
193
+ /**
194
+ * Client-executed tool search only changes Codex's parsed tool context. Routed passthrough keeps
195
+ * serializing the raw request, so activate those returned definitions for upstreams that do not
196
+ * implement the native deferred-loading handshake themselves.
197
+ */
198
+ export function promoteClientLoadedTools(body: unknown): unknown {
199
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
200
+
201
+ const loadedTools = body.input.flatMap(item =>
202
+ isPlainObject(item) && item.type === "tool_search_output" && Array.isArray(item.tools)
203
+ ? item.tools
204
+ : []
205
+ );
206
+ if (loadedTools.length === 0) return body;
207
+
208
+ if (Array.isArray(body.tools)) {
209
+ const tools = mergeLoadedTools(body.tools, loadedTools);
210
+ return tools === body.tools ? body : { ...body, tools };
211
+ }
212
+
213
+ const additionalToolsIndex = body.input.findIndex(item =>
214
+ isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)
215
+ );
216
+ if (additionalToolsIndex < 0) return { ...body, tools: mergeLoadedTools([], loadedTools) };
217
+
218
+ const additionalTools = body.input[additionalToolsIndex];
219
+ if (!isPlainObject(additionalTools) || !Array.isArray(additionalTools.tools)) return body;
220
+ const tools = mergeLoadedTools(additionalTools.tools, loadedTools);
221
+ if (tools === additionalTools.tools) return body;
222
+ const input = [...body.input];
223
+ input[additionalToolsIndex] = { ...additionalTools, tools };
224
+ return { ...body, input };
225
+ }
226
+
227
+ /**
228
+ * Remove hosted tool entries the target native slug rejects, so the OAuth-passthrough body never
229
+ * carries a tool the upstream model 400s on. No-op (returns the original reference) when nothing
230
+ * matches, keeping the common path allocation-free.
231
+ */
232
+ export function stripUnsupportedHostedTools(body: unknown, provider: Pick<OcxProviderConfig, "baseUrl">): unknown {
233
+ if (!isPlainObject(body)) return body;
234
+ const model = typeof body.model === "string" ? body.model : "";
235
+ const filterTools = (tools: unknown[]): unknown[] => {
236
+ const filtered = tools.filter(t => {
237
+ const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
238
+ return !type || !isHostedToolUnsupportedForModel(model, type, provider.baseUrl);
239
+ });
240
+ return filtered.length === tools.length ? tools : filtered;
241
+ };
242
+
243
+ let next: Record<string, unknown> = body;
244
+ let changed = false;
245
+ if (Array.isArray(body.tools)) {
246
+ const tools = filterTools(body.tools);
247
+ if (tools !== body.tools) {
248
+ next = { ...next, tools };
249
+ changed = true;
250
+ }
251
+ }
252
+ if (Array.isArray(body.input)) {
253
+ let inputChanged = false;
254
+ const input = body.input.map(item => {
255
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
256
+ const tools = filterTools(item.tools);
257
+ if (tools === item.tools) return item;
258
+ inputChanged = true;
259
+ return { ...item, tools };
260
+ });
261
+ if (inputChanged) {
262
+ next = { ...next, input };
263
+ changed = true;
264
+ }
265
+ }
266
+
267
+ const toolChoice = next.tool_choice;
268
+ if (isPlainObject(toolChoice) && toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
269
+ const tools = filterTools(toolChoice.tools);
270
+ if (tools !== toolChoice.tools) {
271
+ next = { ...next, tool_choice: tools.length > 0 ? { ...toolChoice, tools } : "none" };
272
+ changed = true;
273
+ }
274
+ } else if (
275
+ isPlainObject(toolChoice)
276
+ && typeof toolChoice.type === "string"
277
+ && isHostedToolUnsupportedForModel(model, toolChoice.type, provider.baseUrl)
278
+ ) {
279
+ next = { ...next, tool_choice: "none" };
280
+ changed = true;
281
+ } else if (changed && toolChoice === "required") {
282
+ const hasDeclaredTools = (Array.isArray(next.tools) && next.tools.length > 0)
283
+ || (Array.isArray(next.input) && next.input.some(item =>
284
+ isPlainObject(item)
285
+ && item.type === "additional_tools"
286
+ && Array.isArray(item.tools)
287
+ && item.tools.length > 0));
288
+ if (!hasDeclaredTools) {
289
+ next = { ...next, tool_choice: "none" };
290
+ }
291
+ }
292
+ return changed ? next : body;
293
+ }
@@ -0,0 +1,156 @@
1
+ import { isPlainObject } from "./internal";
2
+
3
+ /**
4
+ * OpenAI hosted web_search config fields that a capability-classified Responses
5
+ * upstream may reject wholesale. xAI's /v1/responses 400s the entire request on
6
+ * `external_web_access` and `search_context_size` ("Argument not supported"),
7
+ * which killed every routed Grok turn whose client (Codex) attaches its
8
+ * default web_search tool config (probe 2026-08-21: both fields 400
9
+ * individually; `user_location` and `filters` are accepted and kept).
10
+ * The caller decides whether to apply this compatibility transform from explicit
11
+ * provider capability metadata; an unclassified upstream keeps the fields.
12
+ */
13
+ const OPENAI_ONLY_WEB_SEARCH_FIELDS = ["external_web_access", "search_context_size"] as const;
14
+
15
+ function stripOpenAiOnlyWebSearchFieldsFromTools(tools: unknown[]): {
16
+ tools: unknown[];
17
+ changed: boolean;
18
+ } {
19
+ let changed = false;
20
+ const stripped = tools.map(tool => {
21
+ if (!isPlainObject(tool) || (tool.type !== "web_search" && tool.type !== "web_search_preview")) {
22
+ return tool;
23
+ }
24
+ if (!OPENAI_ONLY_WEB_SEARCH_FIELDS.some(field => Object.hasOwn(tool, field))) return tool;
25
+ const { external_web_access: _access, search_context_size: _size, ...rest } = tool;
26
+ changed = true;
27
+ return rest;
28
+ });
29
+ return { tools: changed ? stripped : tools, changed };
30
+ }
31
+
32
+ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
33
+ if (!isPlainObject(body)) return body;
34
+
35
+ let next: Record<string, unknown> = body;
36
+ let changed = false;
37
+ if (Array.isArray(body.tools)) {
38
+ const stripped = stripOpenAiOnlyWebSearchFieldsFromTools(body.tools);
39
+ if (stripped.changed) {
40
+ next = { ...next, tools: stripped.tools };
41
+ changed = true;
42
+ }
43
+ }
44
+
45
+ if (Array.isArray(body.input)) {
46
+ let inputChanged = false;
47
+ const input = body.input.map(item => {
48
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) {
49
+ return item;
50
+ }
51
+ const stripped = stripOpenAiOnlyWebSearchFieldsFromTools(item.tools);
52
+ if (!stripped.changed) return item;
53
+ inputChanged = true;
54
+ return { ...item, tools: stripped.tools };
55
+ });
56
+ if (inputChanged) {
57
+ next = { ...next, input };
58
+ changed = true;
59
+ }
60
+ }
61
+
62
+ return changed ? next : body;
63
+ }
64
+
65
+ /**
66
+ * Muse Spark ids whose Responses gateway refuses provider-specific fields on a plain
67
+ * `web_search` tool. Membership, not equality: 1.3 shipped 2026-09-02 as the
68
+ * same-shaped successor to 1.2 on the same Zen wire, and an equality check would
69
+ * have let a Codex-emitted `web_search` body reach the
70
+ * gateway and come back 400 for every request the moment 1.3 was selected.
71
+ */
72
+ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([
73
+ "muse-spark-1.3-contributor",
74
+ "muse-spark-1.3-contributor-free",
75
+ "muse-spark-1.2-contributor",
76
+ "muse-spark-1.2-contributor-free",
77
+ ]);
78
+
79
+ const MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS = new Set([
80
+ "https://opencode.ai/zen/v1/responses",
81
+ "https://opencode.ai/zen/go/v1/responses",
82
+ "https://api.meta.ai/v1/responses",
83
+ ]);
84
+
85
+ const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [
86
+ "search_content_types",
87
+ "indexed_web_access",
88
+ ] as const;
89
+
90
+ /**
91
+ * OpenCode Zen / Go and the direct Meta Muse Spark Responses gateways refuse a
92
+ * short list of Codex `web_search` fields. `web_search_preview` keeps its accepted
93
+ * shape, and Luna remains untouched. Match the exact effective request URL;
94
+ * malformed, credentialed, or parameterized destinations keep their original body
95
+ * instead of assuming this gateway contract. Keep the rejected names together so a
96
+ * newly identified field is a one-line compatibility update rather than another
97
+ * bespoke rewrite.
98
+ */
99
+ export function stripMuseSparkUnsupportedWebSearchFields(
100
+ body: unknown,
101
+ modelId: unknown,
102
+ responseUrl: string,
103
+ ): unknown {
104
+ if (!isPlainObject(body)) return body;
105
+ if (typeof modelId !== "string") return body;
106
+ if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body;
107
+ let destination: string;
108
+ try {
109
+ const url = new URL(responseUrl);
110
+ if (url.username || url.password || url.search || url.hash) return body;
111
+ destination = `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
112
+ } catch {
113
+ return body;
114
+ }
115
+ if (!MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS.has(destination)) return body;
116
+
117
+ const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => {
118
+ let changed = false;
119
+ const rewritten = tools.map(tool => {
120
+ if (!isPlainObject(tool) || tool.type !== "web_search") return tool;
121
+ if (!MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS.some(field => Object.hasOwn(tool, field))) {
122
+ return tool;
123
+ }
124
+ const rest = { ...tool };
125
+ for (const field of MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS) delete rest[field];
126
+ changed = true;
127
+ return rest;
128
+ });
129
+ return { tools: changed ? rewritten : tools, changed };
130
+ };
131
+
132
+ let next: Record<string, unknown> = body;
133
+ let changed = false;
134
+ if (Array.isArray(body.tools)) {
135
+ const rewritten = rewriteTools(body.tools);
136
+ if (rewritten.changed) {
137
+ next = { ...next, tools: rewritten.tools };
138
+ changed = true;
139
+ }
140
+ }
141
+ if (Array.isArray(next.input)) {
142
+ let inputChanged = false;
143
+ const input = next.input.map(item => {
144
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
145
+ const rewritten = rewriteTools(item.tools);
146
+ if (!rewritten.changed) return item;
147
+ inputChanged = true;
148
+ return { ...item, tools: rewritten.tools };
149
+ });
150
+ if (inputChanged) {
151
+ next = { ...next, input };
152
+ changed = true;
153
+ }
154
+ }
155
+ return changed ? next : body;
156
+ }