@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.
- package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- 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
|
+
}
|