@bitkyc08/opencodex 2.55.0-preview.20260914 → 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-DH2PUHqr.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,202 @@
|
|
|
1
|
+
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
|
|
2
|
+
import { stripItemIdsWhenUnstored } from "./request-strips";
|
|
3
|
+
import { isPlainObject } from "./internal";
|
|
4
|
+
import { stripPromptCacheBreakpoints } from "./prompt-cache";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Remove `previous_response_id` before forwarding. Two triggers:
|
|
8
|
+
* - the proxy expanded the request into a full input replay (the id is now redundant), or
|
|
9
|
+
* - the target is the ChatGPT backend (`authMode: "forward"`), whose Codex REST endpoint
|
|
10
|
+
* categorically rejects the parameter with `{"detail":"Unsupported parameter:
|
|
11
|
+
* previous_response_id"}` (strict allowlist; it also rejects `metadata` and
|
|
12
|
+
* `max_output_tokens`). Codex only sends the id on WS turns, and ocx converts those to
|
|
13
|
+
* internal HTTP requests, so forwarding it upstream is a guaranteed 400 — stripping is
|
|
14
|
+
* strictly better even when the local replay state missed. API-key mode keeps the field on
|
|
15
|
+
* unexpanded requests: the platform `/v1/responses` supports real server-side storage.
|
|
16
|
+
*/
|
|
17
|
+
export function stripPreviousResponseId(body: unknown, strip: boolean): unknown {
|
|
18
|
+
if (!strip || !isPlainObject(body) || !Object.prototype.hasOwnProperty.call(body, "previous_response_id")) return body;
|
|
19
|
+
const { previous_response_id: _previousResponseId, ...rest } = body;
|
|
20
|
+
return rest;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Apply the settled tier only to a fresh outbound object; `_rawBody` remains caller-owned. */
|
|
24
|
+
export function applyTierDecisionToResponsesBody(body: unknown, decision: TierDecision | undefined): unknown {
|
|
25
|
+
if (!decision || decision.kind === "forward-caller" || !isPlainObject(body)) return body;
|
|
26
|
+
const next: Record<string, unknown> = { ...body };
|
|
27
|
+
if (decision.kind === "set") next.service_tier = decision.value;
|
|
28
|
+
else delete next.service_tier;
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Drop request parameters a stateless Responses upstream cannot implement, and pin
|
|
34
|
+
* `store` false.
|
|
35
|
+
*
|
|
36
|
+
* `previous_response_id` is listed here as well as in `stripPreviousResponseId`
|
|
37
|
+
* because that helper's strip is conditional on replay expansion, and it keeps the
|
|
38
|
+
* field for API-key providers on the premise that the platform offers real
|
|
39
|
+
* server-side storage. DeepSeek documents the opposite: "the API is stateless:
|
|
40
|
+
* responses and conversations are not stored on the server", so the field can never
|
|
41
|
+
* be honoured regardless of expansion state.
|
|
42
|
+
*
|
|
43
|
+
* `prompt` is a reference to a server-stored prompt template — the most stateful
|
|
44
|
+
* field in the accepted schema.
|
|
45
|
+
*
|
|
46
|
+
* `service_tier` is deliberately NOT dropped: the final TierDecision is applied to a
|
|
47
|
+
* detached outbound body before this sanitizer chain, and silently deleting a configured knob is
|
|
48
|
+
* worse than forwarding a parameter the upstream ignores.
|
|
49
|
+
*
|
|
50
|
+
* MUST run before the composed sanitize chain below: `stripItemIdsWhenUnstored` keys
|
|
51
|
+
* off `store === false`, and a stateless upstream cannot resolve a stored item id.
|
|
52
|
+
* Returns a copy, so `parsed._rawBody` keeps the client's original `store` value and
|
|
53
|
+
* the local replay cache still records the turn.
|
|
54
|
+
*/
|
|
55
|
+
export function stripStatefulResponsesParams(body: unknown): unknown {
|
|
56
|
+
if (!isPlainObject(body)) return body;
|
|
57
|
+
const drop = ["previous_response_id", "conversation", "background", "metadata", "prompt"] as const;
|
|
58
|
+
const present = drop.some(key => Object.prototype.hasOwnProperty.call(body, key));
|
|
59
|
+
if (!present && body.store === false) return body;
|
|
60
|
+
const next: Record<string, unknown> = { ...body };
|
|
61
|
+
for (const key of drop) delete next[key];
|
|
62
|
+
next.store = false;
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Remove top-level parameters the ChatGPT backend (`authMode: "forward"`) rejects
|
|
68
|
+
* with `{"detail":"Unsupported parameter: …"}` (strict allowlist). Codex CLI never
|
|
69
|
+
* sends these — it controls output length via `reasoning.effort` — but third-party
|
|
70
|
+
* Responses API clients (GJC, SDK wrappers) include `max_output_tokens` per the
|
|
71
|
+
* public spec. `metadata` is likewise absent from the allowlist. No-op when the
|
|
72
|
+
* body carries neither field, keeping the common Codex path allocation-free.
|
|
73
|
+
*/
|
|
74
|
+
export function stripUnsupportedForwardParams(body: unknown): unknown {
|
|
75
|
+
if (!isPlainObject(body)) return body;
|
|
76
|
+
const hasMot = Object.prototype.hasOwnProperty.call(body, "max_output_tokens");
|
|
77
|
+
const hasMeta = Object.prototype.hasOwnProperty.call(body, "metadata");
|
|
78
|
+
if (!hasMot && !hasMeta) return body;
|
|
79
|
+
const { max_output_tokens: _mot, metadata: _meta, ...rest } = body;
|
|
80
|
+
return rest;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Sampling controls the canonical ChatGPT backend rejects; other forward gateways accept them. */
|
|
84
|
+
const CANONICAL_FORWARD_UNSUPPORTED_SAMPLING = ["temperature", "top_p", "stop", "user"] as const;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove sampling controls only the canonical ChatGPT backend rejects.
|
|
88
|
+
*
|
|
89
|
+
* A translated Chat turn used to lose these at the Chat ingress for every provider on
|
|
90
|
+
* the `openai-responses` adapter, which silently discarded caller intent on generic
|
|
91
|
+
* key gateways that accept them. Deciding at the ingress was also unsound for combo
|
|
92
|
+
* and policy routes, whose concrete child is chosen later — so the decision belongs
|
|
93
|
+
* here, on the provider that actually receives the body.
|
|
94
|
+
*
|
|
95
|
+
* Returns a copy and never mutates, so `parsed._rawBody` stays caller-owned, and
|
|
96
|
+
* no-ops when the body carries none of these keys.
|
|
97
|
+
*/
|
|
98
|
+
export function stripCanonicalForwardSamplingParams(body: unknown): unknown {
|
|
99
|
+
if (!isPlainObject(body)) return body;
|
|
100
|
+
if (!CANONICAL_FORWARD_UNSUPPORTED_SAMPLING.some(key => Object.prototype.hasOwnProperty.call(body, key))) {
|
|
101
|
+
return body;
|
|
102
|
+
}
|
|
103
|
+
const next: Record<string, unknown> = { ...body };
|
|
104
|
+
for (const key of CANONICAL_FORWARD_UNSUPPORTED_SAMPLING) delete next[key];
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Return the lossless text represented by one system message, or null when it is multimodal. */
|
|
109
|
+
function canonicalForwardSystemText(item: Record<string, unknown>): string | null {
|
|
110
|
+
const content = item.content;
|
|
111
|
+
if (content === undefined) return "";
|
|
112
|
+
if (typeof content === "string") return content;
|
|
113
|
+
if (!Array.isArray(content)) return null;
|
|
114
|
+
let text = "";
|
|
115
|
+
for (const block of content) {
|
|
116
|
+
if (!isPlainObject(block)) return null;
|
|
117
|
+
if (block.type !== "input_text" && block.type !== "text") return null;
|
|
118
|
+
if (typeof block.text !== "string") return null;
|
|
119
|
+
text += block.text;
|
|
120
|
+
}
|
|
121
|
+
return text;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Only message items may carry privileged system instructions. */
|
|
125
|
+
function isCanonicalForwardSystemMessage(item: unknown): item is Record<string, unknown> {
|
|
126
|
+
return isPlainObject(item)
|
|
127
|
+
&& (item.type === undefined || item.type === "message")
|
|
128
|
+
&& item.role === "system";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The public Responses API accepts input system messages and `truncation`, but the canonical
|
|
133
|
+
* ChatGPT Codex forward endpoint rejects both. Fold only fully textual system messages into the
|
|
134
|
+
* existing top-level instructions and remove the unsupported flag at this destination boundary.
|
|
135
|
+
*
|
|
136
|
+
* The fold is atomic: if any system message contains a non-text block, keep every message in
|
|
137
|
+
* place so the proxy never silently drops multimodal content. The backend may still reject that
|
|
138
|
+
* unsupported shape, but it will not receive a partially rewritten prompt.
|
|
139
|
+
*/
|
|
140
|
+
export function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown {
|
|
141
|
+
if (!isPlainObject(body)) return body;
|
|
142
|
+
const stripTruncation = Object.hasOwn(body, "truncation");
|
|
143
|
+
const input = Array.isArray(body.input) ? body.input : undefined;
|
|
144
|
+
if (!input) {
|
|
145
|
+
if (!stripTruncation) return body;
|
|
146
|
+
const { truncation: _truncation, ...rest } = body;
|
|
147
|
+
return rest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const foldedText: string[] = [];
|
|
151
|
+
let sawSystemMessage = false;
|
|
152
|
+
let canFoldAllSystemMessages = true;
|
|
153
|
+
for (const item of input) {
|
|
154
|
+
if (!isCanonicalForwardSystemMessage(item)) continue;
|
|
155
|
+
sawSystemMessage = true;
|
|
156
|
+
const text = canonicalForwardSystemText(item);
|
|
157
|
+
if (text === null) {
|
|
158
|
+
canFoldAllSystemMessages = false;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
foldedText.push(text);
|
|
162
|
+
}
|
|
163
|
+
if (!stripTruncation && (!sawSystemMessage || !canFoldAllSystemMessages)) return body;
|
|
164
|
+
|
|
165
|
+
const next: Record<string, unknown> = { ...body };
|
|
166
|
+
if (stripTruncation) delete next.truncation;
|
|
167
|
+
if (sawSystemMessage && canFoldAllSystemMessages) {
|
|
168
|
+
next.input = input.filter(item => !isCanonicalForwardSystemMessage(item));
|
|
169
|
+
const folded = foldedText.join("\n\n");
|
|
170
|
+
if (folded !== "") {
|
|
171
|
+
const existing = typeof body.instructions === "string" ? body.instructions : "";
|
|
172
|
+
next.instructions = existing !== "" ? `${existing}\n\n${folded}` : folded;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Posit Assistant can replay client-only cache markers and stored-item references on a
|
|
180
|
+
* `store: false` continuation. The canonical ChatGPT Codex backend rejects both. Remove the
|
|
181
|
+
* markers recursively and drop only `item_reference` rows that cannot name persisted state;
|
|
182
|
+
* ordinary item ids are handled later by stripItemIdsWhenUnstored and tool call_id pairs remain.
|
|
183
|
+
*/
|
|
184
|
+
export function normalizeCanonicalForwardContinuationEnvelope(body: unknown): unknown {
|
|
185
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
186
|
+
let input: unknown[] = body.input;
|
|
187
|
+
let changed = false;
|
|
188
|
+
if (body.store === false) {
|
|
189
|
+
const withoutReferences = input.filter(item => !isPlainObject(item) || item.type !== "item_reference");
|
|
190
|
+
if (withoutReferences.length !== input.length) {
|
|
191
|
+
input = withoutReferences;
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const markerRewrite = stripPromptCacheBreakpoints(input, { nodes: 0 });
|
|
197
|
+
if (markerRewrite.complete && markerRewrite.changed) {
|
|
198
|
+
input = markerRewrite.value as unknown[];
|
|
199
|
+
changed = true;
|
|
200
|
+
}
|
|
201
|
+
return changed ? { ...body, input } : body;
|
|
202
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
|
|
2
|
+
import { collectResponsesToolGroups } from "../../responses/tool-groups";
|
|
3
|
+
import { isPlainObject } from "./internal";
|
|
4
|
+
|
|
5
|
+
const IMAGE_GEN_NAMESPACE = "image_gen";
|
|
6
|
+
const HOSTED_IMAGE_GENERATION_TOOL = "image_generation";
|
|
7
|
+
const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
|
|
8
|
+
const IMAGE_GEN_WIRE_PREFIX = `${IMAGE_GEN_NAMESPACE}__`;
|
|
9
|
+
|
|
10
|
+
/** Remove a supported client prefix before constructing the canonical image-gen wire alias. */
|
|
11
|
+
function imageGenLocalName(name: string): string {
|
|
12
|
+
if (name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) return name.slice(IMAGE_GEN_DOTTED_PREFIX.length);
|
|
13
|
+
if (name.startsWith(IMAGE_GEN_WIRE_PREFIX)) return name.slice(IMAGE_GEN_WIRE_PREFIX.length);
|
|
14
|
+
return name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Build the flat public-Responses name used only on the upstream wire. */
|
|
18
|
+
function imageGenWireName(name: string): string {
|
|
19
|
+
return namespacedToolName(IMAGE_GEN_NAMESPACE, imageGenLocalName(name));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Match client image-gen declarations across namespace, legacy dotted, and canonical wire forms. */
|
|
23
|
+
function isImageGenClientName(name: string): boolean {
|
|
24
|
+
return name === IMAGE_GEN_NAMESPACE
|
|
25
|
+
|| name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
|
|
26
|
+
|| name.startsWith(IMAGE_GEN_WIRE_PREFIX);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Identify declarations that should activate image-gen request normalization. */
|
|
30
|
+
function declaresImageGenClientTool(tool: unknown): boolean {
|
|
31
|
+
if (!isPlainObject(tool) || typeof tool.name !== "string") return false;
|
|
32
|
+
if (tool.type === "namespace") return tool.name === IMAGE_GEN_NAMESPACE;
|
|
33
|
+
return isImageGenClientName(tool.name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Rewrite client image-gen selectors to the hosted tool without widening caller restrictions. */
|
|
37
|
+
function preferHostedImageGenToolChoice(toolChoice: unknown): unknown {
|
|
38
|
+
if (!isPlainObject(toolChoice)) return toolChoice;
|
|
39
|
+
if ((toolChoice.type === "function" || toolChoice.type === "custom") && typeof toolChoice.name === "string") {
|
|
40
|
+
return isImageGenClientName(toolChoice.name) ? { type: HOSTED_IMAGE_GENERATION_TOOL } : toolChoice;
|
|
41
|
+
}
|
|
42
|
+
if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
|
|
43
|
+
const hasHostedImageTool = toolChoice.tools.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
|
|
44
|
+
let changed = false;
|
|
45
|
+
let addedHostedImageTool = false;
|
|
46
|
+
const tools: unknown[] = [];
|
|
47
|
+
for (const tool of toolChoice.tools) {
|
|
48
|
+
const isClientImageTool = isPlainObject(tool)
|
|
49
|
+
&& (tool.type === "function" || tool.type === "custom")
|
|
50
|
+
&& typeof tool.name === "string"
|
|
51
|
+
&& isImageGenClientName(tool.name);
|
|
52
|
+
if (!isClientImageTool) {
|
|
53
|
+
tools.push(tool);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
changed = true;
|
|
57
|
+
if (!hasHostedImageTool && !addedHostedImageTool) {
|
|
58
|
+
tools.push({ type: HOSTED_IMAGE_GENERATION_TOOL });
|
|
59
|
+
addedHostedImageTool = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return changed ? { ...toolChoice, tools } : toolChoice;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Some Responses-compatible gateways reserve the hosted image namespace even when the request
|
|
67
|
+
* does not explicitly declare `image_generation`. For an explicitly configured model, remove only
|
|
68
|
+
* colliding client declarations so the gateway's hosted tool can take precedence.
|
|
69
|
+
*/
|
|
70
|
+
export function preferConfiguredHostedTools(
|
|
71
|
+
body: unknown,
|
|
72
|
+
provider: OcxProviderConfig,
|
|
73
|
+
modelId: string,
|
|
74
|
+
selectedModelId?: string,
|
|
75
|
+
): unknown {
|
|
76
|
+
// A virtual model's advertised id takes precedence over its resolved wire-model id.
|
|
77
|
+
// Read own properties only: a routed model id of `constructor`/`toString` would
|
|
78
|
+
// otherwise resolve to an inherited Object.prototype function and throw on the
|
|
79
|
+
// membership test below, failing the request before it is dispatched.
|
|
80
|
+
const preferenceMap = provider.modelPreferHostedTools;
|
|
81
|
+
const ownPreference = (key: string | undefined): string[] | undefined => {
|
|
82
|
+
if (!key || !preferenceMap || !Object.prototype.hasOwnProperty.call(preferenceMap, key)) return undefined;
|
|
83
|
+
const entry = preferenceMap[key];
|
|
84
|
+
return Array.isArray(entry) ? entry : undefined;
|
|
85
|
+
};
|
|
86
|
+
const preferredTools = ownPreference(selectedModelId) ?? ownPreference(modelId);
|
|
87
|
+
if (!preferredTools?.includes(HOSTED_IMAGE_GENERATION_TOOL) || !isPlainObject(body)) return body;
|
|
88
|
+
|
|
89
|
+
const stripGroup = (tools: unknown[]): unknown[] => {
|
|
90
|
+
const filtered = tools.filter(tool => !declaresImageGenClientTool(tool));
|
|
91
|
+
return filtered.length === tools.length ? tools : filtered;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let changed = false;
|
|
95
|
+
let tools = body.tools;
|
|
96
|
+
let strippedTopLevelImageGenTool = false;
|
|
97
|
+
if (Array.isArray(body.tools)) {
|
|
98
|
+
tools = stripGroup(body.tools);
|
|
99
|
+
strippedTopLevelImageGenTool = tools !== body.tools;
|
|
100
|
+
changed ||= strippedTopLevelImageGenTool;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let input = body.input;
|
|
104
|
+
const strippedAdditionalToolsIndices = new Set<number>();
|
|
105
|
+
if (Array.isArray(body.input)) {
|
|
106
|
+
let nestedChanged = false;
|
|
107
|
+
const mappedInput = body.input.map((item, index) => {
|
|
108
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
|
|
109
|
+
const nestedTools = stripGroup(item.tools);
|
|
110
|
+
if (nestedTools === item.tools) return item;
|
|
111
|
+
strippedAdditionalToolsIndices.add(index);
|
|
112
|
+
nestedChanged = true;
|
|
113
|
+
return { ...item, tools: nestedTools };
|
|
114
|
+
});
|
|
115
|
+
if (nestedChanged) {
|
|
116
|
+
input = mappedInput;
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const hasToolChoice = Object.hasOwn(body, "tool_choice");
|
|
122
|
+
const toolChoice = hasToolChoice ? preferHostedImageGenToolChoice(body.tool_choice) : body.tool_choice;
|
|
123
|
+
const toolChoiceChanged = hasToolChoice && toolChoice !== body.tool_choice;
|
|
124
|
+
const hasHostedImageGenTool = (toolGroup: unknown): boolean => Array.isArray(toolGroup)
|
|
125
|
+
&& toolGroup.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
|
|
126
|
+
const hasHostedImageGenDeclaration = hasHostedImageGenTool(tools)
|
|
127
|
+
|| (Array.isArray(input) && input.some(item => isPlainObject(item)
|
|
128
|
+
&& item.type === "additional_tools"
|
|
129
|
+
&& hasHostedImageGenTool(item.tools)));
|
|
130
|
+
if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndices.size > 0) && !hasHostedImageGenDeclaration) {
|
|
131
|
+
if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
|
|
132
|
+
tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
|
|
133
|
+
} else if (strippedAdditionalToolsIndices.size > 0 && Array.isArray(input)) {
|
|
134
|
+
// Restore into the FIRST stripped container only. Tool declarations are
|
|
135
|
+
// request-scoped, not container-scoped — the containers are separate carriers for
|
|
136
|
+
// one tool set, so a single hosted declaration covers the request. An earlier
|
|
137
|
+
// revision restored into every stripped container and put `image_generation` on
|
|
138
|
+
// the wire twice; review caught it.
|
|
139
|
+
const firstStripped = Math.min(...strippedAdditionalToolsIndices);
|
|
140
|
+
input = input.map((item, index) => index === firstStripped
|
|
141
|
+
&& isPlainObject(item)
|
|
142
|
+
&& Array.isArray(item.tools)
|
|
143
|
+
? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
|
|
144
|
+
: item);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
changed ||= toolChoiceChanged;
|
|
148
|
+
if (!changed) return body;
|
|
149
|
+
const next: Record<string, unknown> = {
|
|
150
|
+
...body,
|
|
151
|
+
...(Array.isArray(body.tools) ? { tools } : {}),
|
|
152
|
+
...(Array.isArray(body.input) ? { input } : {}),
|
|
153
|
+
};
|
|
154
|
+
if (toolChoiceChanged) next.tool_choice = toolChoice;
|
|
155
|
+
return next;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Lower one complete Codex image-gen namespace to public Responses function tools.
|
|
160
|
+
*
|
|
161
|
+
* The public API reserves the `image_gen` namespace and restricts function names to a flat safe
|
|
162
|
+
* alphabet. `image_gen__<tool>` is therefore an upstream-only alias; client-facing responses are
|
|
163
|
+
* restored to explicit `{ namespace: "image_gen", name: "<tool>" }` calls by the server. Only a
|
|
164
|
+
* non-empty namespace containing named function tools is safe to lower. Malformed, empty, and
|
|
165
|
+
* future namespace shapes stay untouched instead of silently losing client capabilities.
|
|
166
|
+
*/
|
|
167
|
+
function flattenImageGenNamespace(tool: unknown): Record<string, unknown>[] | undefined {
|
|
168
|
+
if (
|
|
169
|
+
!isPlainObject(tool)
|
|
170
|
+
|| tool.type !== "namespace"
|
|
171
|
+
|| tool.name !== IMAGE_GEN_NAMESPACE
|
|
172
|
+
|| !Array.isArray(tool.tools)
|
|
173
|
+
|| tool.tools.length === 0
|
|
174
|
+
) return undefined;
|
|
175
|
+
|
|
176
|
+
for (const innerTool of tool.tools) {
|
|
177
|
+
if (
|
|
178
|
+
!isPlainObject(innerTool)
|
|
179
|
+
|| innerTool.type !== "function"
|
|
180
|
+
|| typeof innerTool.name !== "string"
|
|
181
|
+
|| innerTool.name.length === 0
|
|
182
|
+
) return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return tool.tools.map(innerTool => {
|
|
186
|
+
const functionTool = innerTool as Record<string, unknown> & { name: string };
|
|
187
|
+
return {
|
|
188
|
+
...functionTool,
|
|
189
|
+
name: imageGenWireName(functionTool.name),
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Convert a legacy dotted function declaration while preserving all other function metadata. */
|
|
195
|
+
function normalizeFlatImageGenFunction(tool: unknown): unknown {
|
|
196
|
+
if (
|
|
197
|
+
!isPlainObject(tool)
|
|
198
|
+
|| tool.type !== "function"
|
|
199
|
+
|| typeof tool.name !== "string"
|
|
200
|
+
|| !tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
|
|
201
|
+
) return tool;
|
|
202
|
+
return { ...tool, name: imageGenWireName(tool.name) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Return the image-gen function name used for stable cross-container deduplication. */
|
|
206
|
+
function imageGenFunctionName(tool: unknown): string | undefined {
|
|
207
|
+
if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return isImageGenClientName(tool.name) ? tool.name : undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** True only when a declaration can yield a callable upstream-safe image-gen function alias. */
|
|
214
|
+
function declaresUsableImageGenAlias(tool: unknown): boolean {
|
|
215
|
+
if (flattenImageGenNamespace(tool)) return true;
|
|
216
|
+
if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
|
|
220
|
+
return tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length;
|
|
221
|
+
}
|
|
222
|
+
return tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
|
|
223
|
+
&& tool.name.length > IMAGE_GEN_WIRE_PREFIX.length;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Collect client tool-choice names and the exact upstream aliases declared for them. */
|
|
227
|
+
function imageGenToolChoiceAliases(toolGroups: unknown[][]): Map<string, string> {
|
|
228
|
+
const aliases = new Map<string, string>();
|
|
229
|
+
|
|
230
|
+
for (const group of toolGroups) {
|
|
231
|
+
for (const tool of group) {
|
|
232
|
+
const flattened = flattenImageGenNamespace(tool);
|
|
233
|
+
if (flattened) {
|
|
234
|
+
for (const candidate of flattened) {
|
|
235
|
+
const wireName = candidate.name as string;
|
|
236
|
+
aliases.set(`${IMAGE_GEN_DOTTED_PREFIX}${imageGenLocalName(wireName)}`, wireName);
|
|
237
|
+
aliases.set(wireName, wireName);
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (
|
|
245
|
+
tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
|
|
246
|
+
&& tool.name.length > IMAGE_GEN_DOTTED_PREFIX.length
|
|
247
|
+
) {
|
|
248
|
+
aliases.set(tool.name, imageGenWireName(tool.name));
|
|
249
|
+
} else if (
|
|
250
|
+
tool.name.startsWith(IMAGE_GEN_WIRE_PREFIX)
|
|
251
|
+
&& tool.name.length > IMAGE_GEN_WIRE_PREFIX.length
|
|
252
|
+
) {
|
|
253
|
+
aliases.set(tool.name, tool.name);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return aliases;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Rewrite function selectors only when their corresponding declaration receives a wire alias. */
|
|
262
|
+
function normalizeImageGenToolChoice(
|
|
263
|
+
toolChoice: unknown,
|
|
264
|
+
aliases: ReadonlyMap<string, string>,
|
|
265
|
+
): unknown {
|
|
266
|
+
if (!isPlainObject(toolChoice)) return toolChoice;
|
|
267
|
+
|
|
268
|
+
if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
|
|
269
|
+
const alias = aliases.get(toolChoice.name);
|
|
270
|
+
return alias && alias !== toolChoice.name ? { ...toolChoice, name: alias } : toolChoice;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
|
|
274
|
+
let changed = false;
|
|
275
|
+
const tools = toolChoice.tools.map(tool => {
|
|
276
|
+
if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") {
|
|
277
|
+
return tool;
|
|
278
|
+
}
|
|
279
|
+
const alias = aliases.get(tool.name);
|
|
280
|
+
if (!alias || alias === tool.name) return tool;
|
|
281
|
+
changed = true;
|
|
282
|
+
return { ...tool, name: alias };
|
|
283
|
+
});
|
|
284
|
+
return changed ? { ...toolChoice, tools } : toolChoice;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Identify replayed image-gen calls that require upstream wire encoding. */
|
|
288
|
+
function declaresImageGenFunctionCall(item: unknown): boolean {
|
|
289
|
+
if (!isPlainObject(item) || item.type !== "function_call" || typeof item.name !== "string") {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
return item.namespace === IMAGE_GEN_NAMESPACE || isImageGenClientName(item.name);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Encode native or legacy replay calls to the same flat name used by tool declarations. */
|
|
296
|
+
function normalizeImageGenFunctionCall(item: unknown): unknown {
|
|
297
|
+
if (!declaresImageGenFunctionCall(item) || !isPlainObject(item) || typeof item.name !== "string") {
|
|
298
|
+
return item;
|
|
299
|
+
}
|
|
300
|
+
if (item.namespace === IMAGE_GEN_NAMESPACE) {
|
|
301
|
+
const { namespace: _namespace, ...rest } = item;
|
|
302
|
+
return { ...rest, name: imageGenWireName(item.name) };
|
|
303
|
+
}
|
|
304
|
+
if (item.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)) {
|
|
305
|
+
return { ...item, name: imageGenWireName(item.name) };
|
|
306
|
+
}
|
|
307
|
+
return item;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Normalize Codex's private image-gen tool declaration for API-key Responses providers.
|
|
312
|
+
*
|
|
313
|
+
* A complete `image_gen` namespace is flattened to safe `image_gen__<tool>` aliases even when it is
|
|
314
|
+
* the only image tool in the request. Replayed client calls are encoded to the same alias, including
|
|
315
|
+
* legacy dotted calls from older compatibility attempts. When a usable alias replaces a client
|
|
316
|
+
* image-gen declaration, the duplicate hosted `image_generation` entry is removed. Duplicate aliases
|
|
317
|
+
* are resolved in stable container order: top-level tools first, then Responses Lite
|
|
318
|
+
* `additional_tools` entries.
|
|
319
|
+
*
|
|
320
|
+
* This function is called only on the API-key path. ChatGPT forward mode understands the private
|
|
321
|
+
* namespace and must keep it. Copy-on-write preserves the original request reference when no
|
|
322
|
+
* namespace is flattened, hosted tool removed, or duplicate function discarded.
|
|
323
|
+
*/
|
|
324
|
+
export function normalizeImageGenClientTools(body: unknown): unknown {
|
|
325
|
+
if (!isPlainObject(body)) return body;
|
|
326
|
+
|
|
327
|
+
const toolGroups = collectResponsesToolGroups(body);
|
|
328
|
+
const hasImageGenClientTool = toolGroups.some(group => group.some(declaresImageGenClientTool))
|
|
329
|
+
|| (Array.isArray(body.input) && body.input.some(declaresImageGenFunctionCall));
|
|
330
|
+
if (!hasImageGenClientTool) return body;
|
|
331
|
+
const hasUsableImageGenAlias = toolGroups.some(group => group.some(declaresUsableImageGenAlias));
|
|
332
|
+
const toolChoiceAliases = imageGenToolChoiceAliases(toolGroups);
|
|
333
|
+
|
|
334
|
+
const seenFunctionNames = new Set<string>();
|
|
335
|
+
const normalizeGroup = (tools: unknown[]): unknown[] => {
|
|
336
|
+
const normalized: unknown[] = [];
|
|
337
|
+
let groupChanged = false;
|
|
338
|
+
|
|
339
|
+
for (const tool of tools) {
|
|
340
|
+
if (
|
|
341
|
+
hasUsableImageGenAlias
|
|
342
|
+
&& isPlainObject(tool)
|
|
343
|
+
&& tool.type === HOSTED_IMAGE_GENERATION_TOOL
|
|
344
|
+
) {
|
|
345
|
+
groupChanged = true;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const flattened = flattenImageGenNamespace(tool);
|
|
350
|
+
const candidates = flattened ?? [tool];
|
|
351
|
+
if (flattened) groupChanged = true;
|
|
352
|
+
|
|
353
|
+
for (const candidate of candidates) {
|
|
354
|
+
const normalizedCandidate = normalizeFlatImageGenFunction(candidate);
|
|
355
|
+
if (normalizedCandidate !== candidate) groupChanged = true;
|
|
356
|
+
const functionName = imageGenFunctionName(normalizedCandidate);
|
|
357
|
+
if (functionName && seenFunctionNames.has(functionName)) {
|
|
358
|
+
groupChanged = true;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (functionName) seenFunctionNames.add(functionName);
|
|
362
|
+
normalized.push(normalizedCandidate);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return groupChanged ? normalized : tools;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
let changed = false;
|
|
370
|
+
let tools = body.tools;
|
|
371
|
+
if (Array.isArray(body.tools)) {
|
|
372
|
+
tools = normalizeGroup(body.tools);
|
|
373
|
+
changed ||= tools !== body.tools;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let input = body.input;
|
|
377
|
+
if (Array.isArray(body.input)) {
|
|
378
|
+
let nestedChanged = false;
|
|
379
|
+
const mappedInput = body.input.map(item => {
|
|
380
|
+
if (isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
|
|
381
|
+
const nestedTools = normalizeGroup(item.tools);
|
|
382
|
+
if (nestedTools === item.tools) return item;
|
|
383
|
+
nestedChanged = true;
|
|
384
|
+
return { ...item, tools: nestedTools };
|
|
385
|
+
}
|
|
386
|
+
const normalizedCall = normalizeImageGenFunctionCall(item);
|
|
387
|
+
if (normalizedCall !== item) nestedChanged = true;
|
|
388
|
+
return normalizedCall;
|
|
389
|
+
});
|
|
390
|
+
if (nestedChanged) {
|
|
391
|
+
input = mappedInput;
|
|
392
|
+
changed = true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const toolChoice = normalizeImageGenToolChoice(body.tool_choice, toolChoiceAliases);
|
|
397
|
+
changed ||= toolChoice !== body.tool_choice;
|
|
398
|
+
|
|
399
|
+
if (!changed) return body;
|
|
400
|
+
return {
|
|
401
|
+
...body,
|
|
402
|
+
...(Array.isArray(body.tools) ? { tools } : {}),
|
|
403
|
+
...(Array.isArray(body.input) ? { input } : {}),
|
|
404
|
+
...(Object.prototype.hasOwnProperty.call(body, "tool_choice") ? { tool_choice: toolChoice } : {}),
|
|
405
|
+
};
|
|
406
|
+
}
|