@yansigit/opencodex 2.31.3 → 2.33.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/README.md +2 -2
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-DKLr4LTE.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -5
- package/src/adapters/anthropic.ts +25 -13
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +5 -1
- package/src/adapters/command-code.ts +42 -10
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/protobuf-events.ts +158 -7
- package/src/adapters/cursor/protobuf-request.ts +33 -15
- package/src/adapters/cursor/request-builder.ts +4 -3
- package/src/adapters/cursor/tool-definitions.ts +27 -1
- package/src/adapters/cursor/types.ts +4 -3
- package/src/adapters/cursor.ts +9 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +154 -33
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +4 -1
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +133 -24
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +26 -13
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +20 -6
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +8 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +25 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -0
- package/src/codex/catalog/effort.ts +42 -12
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +593 -0
- package/src/codex/catalog/parsing.ts +71 -27
- package/src/codex/catalog/provider-fetch.ts +189 -33
- package/src/codex/catalog/sync.ts +6 -5
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/shim.ts +56 -3
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +214 -0
- package/src/config.ts +153 -814
- package/src/generated/compatibility-version.json +232 -148
- package/src/images/loop.ts +37 -6
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +15 -3
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +59 -13
- package/src/providers/registry.ts +4 -2
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +14 -2
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +8 -2
- package/src/server/auth-cors.ts +37 -7
- package/src/server/images.ts +19 -35
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +206 -16
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +35 -6
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +25 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/request-log.ts +21 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +36 -22
- package/src/server/responses/core.ts +584 -247
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +20 -102
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/service.ts +9 -5
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +34 -1
- package/src/types/tools.ts +114 -11
- package/src/types.ts +7 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
- package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
|
@@ -33,6 +33,8 @@ interface Waiter {
|
|
|
33
33
|
modelId?: string;
|
|
34
34
|
providerIntervalMs: number;
|
|
35
35
|
modelIntervalMs: number;
|
|
36
|
+
providerJitterMs: number;
|
|
37
|
+
modelJitterMs: number;
|
|
36
38
|
queuedAt: number;
|
|
37
39
|
signal?: AbortSignal;
|
|
38
40
|
resolve: () => void;
|
|
@@ -54,6 +56,7 @@ export interface RequestPacingRuntime {
|
|
|
54
56
|
setTimer: (callback: () => void, delayMs: number) => unknown;
|
|
55
57
|
clearTimer: (handle: unknown) => void;
|
|
56
58
|
enqueueMicrotask: (callback: () => void) => void;
|
|
59
|
+
random: () => number;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export interface ProviderRequestPacingStatus {
|
|
@@ -71,6 +74,7 @@ const defaultRuntime: RequestPacingRuntime = {
|
|
|
71
74
|
setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
72
75
|
clearTimer: handle => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
73
76
|
enqueueMicrotask: queueMicrotask,
|
|
77
|
+
random: Math.random,
|
|
74
78
|
};
|
|
75
79
|
let runtime = defaultRuntime;
|
|
76
80
|
let lastReconciledGeneration = 0;
|
|
@@ -90,6 +94,19 @@ function normalizedInterval(rule: RequestPacingRule | undefined): number {
|
|
|
90
94
|
return Math.max(rpmInterval, fixedInterval);
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
function normalizedJitter(rule: RequestPacingRule | undefined): number {
|
|
98
|
+
return rule?.jitterMs !== undefined && Number.isInteger(rule.jitterMs)
|
|
99
|
+
? Math.min(60_000, Math.max(0, rule.jitterMs))
|
|
100
|
+
: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function jitterDelay(jitterMs: number): number {
|
|
104
|
+
if (jitterMs <= 0) return 0;
|
|
105
|
+
const sample = runtime.random();
|
|
106
|
+
if (!Number.isFinite(sample)) return 0;
|
|
107
|
+
return Math.min(jitterMs, Math.floor(Math.max(0, sample) * jitterMs));
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
export function requestPacingIntervalMs(provider: OcxProviderConfig, modelId?: string): number {
|
|
94
111
|
const policy = provider.requestPacing;
|
|
95
112
|
if (!policy?.enabled) return 0;
|
|
@@ -100,12 +117,17 @@ export function requestPacingIntervalMs(provider: OcxProviderConfig, modelId?: s
|
|
|
100
117
|
function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string): {
|
|
101
118
|
providerIntervalMs: number;
|
|
102
119
|
modelIntervalMs: number;
|
|
120
|
+
providerJitterMs: number;
|
|
121
|
+
modelJitterMs: number;
|
|
103
122
|
} {
|
|
104
123
|
const policy = provider.requestPacing;
|
|
105
|
-
if (!policy?.enabled) return { providerIntervalMs: 0, modelIntervalMs: 0 };
|
|
124
|
+
if (!policy?.enabled) return { providerIntervalMs: 0, modelIntervalMs: 0, providerJitterMs: 0, modelJitterMs: 0 };
|
|
125
|
+
const model = modelId ? policy.models?.[modelId] : undefined;
|
|
106
126
|
return {
|
|
107
127
|
providerIntervalMs: normalizedInterval(policy),
|
|
108
|
-
modelIntervalMs:
|
|
128
|
+
modelIntervalMs: normalizedInterval(model),
|
|
129
|
+
providerJitterMs: normalizedJitter(policy),
|
|
130
|
+
modelJitterMs: normalizedJitter(model),
|
|
109
131
|
};
|
|
110
132
|
}
|
|
111
133
|
|
|
@@ -186,9 +208,9 @@ function runQueue(providerName: string, state: ProviderPacer): void {
|
|
|
186
208
|
const startedAt = runtime.now();
|
|
187
209
|
state.lastStartedAt = startedAt;
|
|
188
210
|
state.lastModelId = waiter.modelId;
|
|
189
|
-
state.providerNextStartAt = startedAt + waiter.providerIntervalMs;
|
|
190
|
-
if (waiter.modelId && waiter.modelIntervalMs > 0) {
|
|
191
|
-
state.modelNextStartAt.set(waiter.modelId, startedAt + waiter.modelIntervalMs);
|
|
211
|
+
state.providerNextStartAt = startedAt + waiter.providerIntervalMs + jitterDelay(waiter.providerJitterMs);
|
|
212
|
+
if (waiter.modelId && (waiter.modelIntervalMs > 0 || waiter.modelJitterMs > 0)) {
|
|
213
|
+
state.modelNextStartAt.set(waiter.modelId, startedAt + waiter.modelIntervalMs + jitterDelay(waiter.modelJitterMs));
|
|
192
214
|
}
|
|
193
215
|
waiter.resolve();
|
|
194
216
|
runtime.enqueueMicrotask(() => runQueue(providerName, state));
|
|
@@ -201,7 +223,12 @@ export async function waitForProviderRequestSlot(
|
|
|
201
223
|
signal?: AbortSignal,
|
|
202
224
|
): Promise<void> {
|
|
203
225
|
const intervals = requestPacingIntervals(provider, modelId);
|
|
204
|
-
if (Math.max(
|
|
226
|
+
if (Math.max(
|
|
227
|
+
intervals.providerIntervalMs,
|
|
228
|
+
intervals.modelIntervalMs,
|
|
229
|
+
intervals.providerJitterMs,
|
|
230
|
+
intervals.modelJitterMs,
|
|
231
|
+
) <= 0) return;
|
|
205
232
|
if (signal?.aborted) throw abortReason(signal);
|
|
206
233
|
|
|
207
234
|
const state = pacers.get(providerName) ?? {
|
|
@@ -4,6 +4,27 @@ import { resolveGithubCopilotTransport } from "./github-copilot-transport";
|
|
|
4
4
|
|
|
5
5
|
export const XAI_GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
6
6
|
|
|
7
|
+
/** The two hosts that serve xAI's Responses API: the public API and the Grok CLI proxy. */
|
|
8
|
+
const XAI_RESPONSES_HOSTS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* True when this provider's Responses traffic terminates at xAI itself.
|
|
12
|
+
*
|
|
13
|
+
* Probed 2026-08-22 one field per request: the two hosts accept and refuse exactly the same
|
|
14
|
+
* web_search fields, so they are one dialect rather than two. Matching is exact-host over
|
|
15
|
+
* https, which keeps lookalikes (`api.x.ai.evil.test`) and nonstandard ports out.
|
|
16
|
+
*/
|
|
17
|
+
export function isXaiResponsesDestination(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(provider.baseUrl);
|
|
20
|
+
return url.protocol === "https:"
|
|
21
|
+
&& XAI_RESPONSES_HOSTS.has(url.hostname.toLowerCase())
|
|
22
|
+
&& (url.port === "" || url.port === "443");
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
export const XAI_GROK_COMPATIBILITY = {
|
|
8
29
|
version: "0.2.93",
|
|
9
30
|
userAgent: "opencodex-grok/0.2.93",
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -14,6 +14,16 @@ export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] =
|
|
|
14
14
|
const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort);
|
|
15
15
|
const CODEX_REASONING_SET = new Set(CODEX_REASONING_ORDER);
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Sentinel wire value in reasoningEffortMap / modelReasoningEffortMap to explicitly
|
|
19
|
+
* omit the reasoning_effort field from the upstream wire request (issue #2356).
|
|
20
|
+
*/
|
|
21
|
+
export const REASONING_EFFORT_OMIT_SENTINEL = "__omit__";
|
|
22
|
+
|
|
23
|
+
export function isReasoningEffortOmitted(wireEffort: string | undefined): boolean {
|
|
24
|
+
return wireEffort === REASONING_EFFORT_OMIT_SENTINEL;
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
/** True when `effort` is a member of the Codex reasoning ladder (low..ultra). */
|
|
18
28
|
export function isCodexReasoningEffort(effort: string): boolean {
|
|
19
29
|
return CODEX_REASONING_SET.has(effort);
|
|
@@ -170,7 +180,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
|
|
|
170
180
|
const boundary = requested === "ultra" ? "max" : requested;
|
|
171
181
|
|
|
172
182
|
const wireMap = reasoningEffortMapFor(provider, modelId);
|
|
173
|
-
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary))
|
|
183
|
+
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) {
|
|
184
|
+
const mapped = wireMap[boundary];
|
|
185
|
+
return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped;
|
|
186
|
+
}
|
|
174
187
|
|
|
175
188
|
const supported = configuredReasoningEfforts(provider, modelId);
|
|
176
189
|
const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary);
|
|
@@ -178,6 +191,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
|
|
|
178
191
|
|
|
179
192
|
// Belt for the odd config where the supported ladder is ultra-only and the clamp lands on it.
|
|
180
193
|
const wire = codexEffort === "ultra" ? "max" : codexEffort;
|
|
181
|
-
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire))
|
|
194
|
+
if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) {
|
|
195
|
+
const mapped = wireMap[wire];
|
|
196
|
+
return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped;
|
|
197
|
+
}
|
|
198
|
+
if (wire === REASONING_EFFORT_OMIT_SENTINEL) return undefined;
|
|
182
199
|
return wire;
|
|
183
200
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Representation repair for top-level Codex apply_patch custom-tool payloads.
|
|
2
|
+
//
|
|
3
|
+
// Some routed models decorate the first and last lines as
|
|
4
|
+
// `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
|
|
5
|
+
// valid custom-tool payloads. Repair is deliberately limited to a complete,
|
|
6
|
+
// structurally recognizable top-level patch: arbitrary `exec` JavaScript is
|
|
7
|
+
// caller-authored executable input and must remain byte-identical.
|
|
8
|
+
//
|
|
9
|
+
// This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
|
|
10
|
+
// repair the one faithful reading, leave genuine patch content alone.
|
|
11
|
+
|
|
12
|
+
const PATCH_BEGIN = "*** Begin Patch";
|
|
13
|
+
const PATCH_END = "*** End Patch";
|
|
14
|
+
const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
|
|
15
|
+
const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;
|
|
16
|
+
|
|
17
|
+
/** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
|
|
18
|
+
export function unwrapFreeformToolInput(argumentsText: unknown): string {
|
|
19
|
+
if (typeof argumentsText !== "string") return "";
|
|
20
|
+
try {
|
|
21
|
+
const parsed: unknown = JSON.parse(argumentsText);
|
|
22
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
23
|
+
const input = (parsed as { input?: unknown }).input;
|
|
24
|
+
if (typeof input === "string") return input;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// The string is the freeform body, not nested JSON.
|
|
28
|
+
}
|
|
29
|
+
return argumentsText;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Strip trailing `***` only from the outer lines of one complete patch.
|
|
34
|
+
* Internal patch content, incomplete envelopes, and non-patch text are exact
|
|
35
|
+
* pass-throughs.
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeApplyPatchDelimiters(text: string): string {
|
|
38
|
+
const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
|
|
39
|
+
if (!match) return text;
|
|
40
|
+
const [, begin, beginBreak, body, endBreak, end, trailingBreak = ""] = match;
|
|
41
|
+
if (!PATCH_OPERATION_LINE.test(body)) return text;
|
|
42
|
+
if (begin === PATCH_BEGIN && end === PATCH_END) return text;
|
|
43
|
+
return `${PATCH_BEGIN}${beginBreak}${body}${endBreak}${PATCH_END}${trailingBreak}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Repair freeform input before Codex sees it.
|
|
48
|
+
*
|
|
49
|
+
* Only a bare or reserved-`functions` `apply_patch` payload may receive delimiter
|
|
50
|
+
* repair. Remote namespaces own their grammar; those bodies and every other
|
|
51
|
+
* freeform input are unwrapped and left byte-exact.
|
|
52
|
+
*/
|
|
53
|
+
export function repairFreeformToolInput(
|
|
54
|
+
argumentsText: unknown,
|
|
55
|
+
toolName = "",
|
|
56
|
+
namespace?: string,
|
|
57
|
+
): string {
|
|
58
|
+
const unwrapped = unwrapFreeformToolInput(argumentsText);
|
|
59
|
+
const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
|
|
60
|
+
return ownsApplyPatchGrammar && toolName === "apply_patch"
|
|
61
|
+
? normalizeApplyPatchDelimiters(unwrapped)
|
|
62
|
+
: unwrapped;
|
|
63
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { namespacedToolName } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
normalizeApplyPatchDelimiters,
|
|
4
|
+
repairFreeformToolInput,
|
|
5
|
+
unwrapFreeformToolInput,
|
|
6
|
+
} from "./apply-patch-envelope";
|
|
2
7
|
import { collectResponsesToolGroups } from "./tool-groups";
|
|
3
8
|
|
|
4
9
|
const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
|
|
@@ -15,19 +20,42 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
15
20
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
function customToolInput(argumentsText: unknown): string {
|
|
19
|
-
if (typeof argumentsText !== "string") return "";
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(argumentsText) as unknown;
|
|
22
|
-
if (isPlainObject(parsed) && typeof parsed.input === "string") return parsed.input;
|
|
23
|
-
} catch { /* malformed arguments stay visible to the client */ }
|
|
24
|
-
return argumentsText;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
function customToolWireName(namespace: string | undefined, name: string): string {
|
|
28
24
|
return namespace === BUILTIN_FUNCTIONS_NAMESPACE ? name : namespacedToolName(namespace, name);
|
|
29
25
|
}
|
|
30
26
|
|
|
27
|
+
function toolChoiceAllowsRoutedCustomTool(
|
|
28
|
+
body: unknown,
|
|
29
|
+
wireName: string,
|
|
30
|
+
candidateNames: ReadonlySet<string>,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (!isPlainObject(body)) return true;
|
|
33
|
+
const choice = body.tool_choice;
|
|
34
|
+
if (choice === undefined || choice === null || choice === "auto" || choice === "required") {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (choice === "none") return false;
|
|
38
|
+
if (!isPlainObject(choice)) return true;
|
|
39
|
+
|
|
40
|
+
const selectorAllows = (selector: unknown): boolean => {
|
|
41
|
+
if (!isPlainObject(selector) || typeof selector.name !== "string") return false;
|
|
42
|
+
if (selector.type !== "custom") return false;
|
|
43
|
+
if (typeof selector.namespace === "string") {
|
|
44
|
+
return customToolWireName(selector.namespace, selector.name) === wireName;
|
|
45
|
+
}
|
|
46
|
+
if (selector.name === wireName) return true;
|
|
47
|
+
const suffix = `__${selector.name}`;
|
|
48
|
+
const candidates = [...candidateNames].filter(name => name.endsWith(suffix));
|
|
49
|
+
return candidates.length === 1 && candidates[0] === wireName;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (choice.type === "function" || choice.type === "custom") return selectorAllows(choice);
|
|
53
|
+
if (choice.type === "allowed_tools" && Array.isArray(choice.tools)) {
|
|
54
|
+
return choice.tools.some(selectorAllows);
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
/** Final upstream identity of a call, including a namespace restored by an earlier rewrite. */
|
|
32
60
|
export function routedCustomToolWireName(value: unknown): string | undefined {
|
|
33
61
|
if (!isPlainObject(value) || typeof value.name !== "string") return undefined;
|
|
@@ -38,12 +66,13 @@ export function routedCustomToolWireName(value: unknown): string | undefined {
|
|
|
38
66
|
}
|
|
39
67
|
|
|
40
68
|
/**
|
|
41
|
-
* Names of
|
|
42
|
-
*
|
|
69
|
+
* Names of custom declarations after namespace lowering. The selection flag separates converted
|
|
70
|
+
* names from native passthrough names while keeping same-named function and custom children distinct.
|
|
43
71
|
*/
|
|
44
72
|
function collectRoutedCustomToolWireNames(
|
|
45
73
|
body: unknown,
|
|
46
74
|
supportsResponsesCustomTools?: boolean,
|
|
75
|
+
passthrough = false,
|
|
47
76
|
): Set<string> {
|
|
48
77
|
const names = new Set<string>();
|
|
49
78
|
const groups = collectResponsesToolGroups(body);
|
|
@@ -64,7 +93,7 @@ function collectRoutedCustomToolWireNames(
|
|
|
64
93
|
if (
|
|
65
94
|
tool.type === "custom"
|
|
66
95
|
&& typeof tool.name === "string"
|
|
67
|
-
&&
|
|
96
|
+
&& routedCustomToolPassesThrough(tool.name, supportsResponsesCustomTools) === passthrough
|
|
68
97
|
) {
|
|
69
98
|
names.add(tool.name);
|
|
70
99
|
continue;
|
|
@@ -77,7 +106,8 @@ function collectRoutedCustomToolWireNames(
|
|
|
77
106
|
isPlainObject(child)
|
|
78
107
|
&& child.type === "custom"
|
|
79
108
|
&& typeof child.name === "string"
|
|
80
|
-
&&
|
|
109
|
+
&& routedCustomToolPassesThrough(child.name, supportsResponsesCustomTools) === passthrough
|
|
110
|
+
&& (!passthrough || tool.name === BUILTIN_FUNCTIONS_NAMESPACE)
|
|
81
111
|
&& !(tool.name === BUILTIN_FUNCTIONS_NAMESPACE && bareWireNames.has(child.name))
|
|
82
112
|
) names.add(customToolWireName(tool.name, child.name));
|
|
83
113
|
}
|
|
@@ -85,7 +115,6 @@ function collectRoutedCustomToolWireNames(
|
|
|
85
115
|
}
|
|
86
116
|
return names;
|
|
87
117
|
}
|
|
88
|
-
|
|
89
118
|
export function customToolItemId(id: unknown): unknown {
|
|
90
119
|
if (typeof id !== "string") return id;
|
|
91
120
|
return id.startsWith("fc_") ? `ctc_${id.slice(3)}` : id;
|
|
@@ -203,64 +232,129 @@ export function rewriteRoutedCustomToolsForUpstream(
|
|
|
203
232
|
): {
|
|
204
233
|
body: unknown;
|
|
205
234
|
names: Set<string>;
|
|
235
|
+
repairNames: Set<string>;
|
|
206
236
|
} {
|
|
207
237
|
const conversionNames = collectRoutedCustomToolNames(body, supportsResponsesCustomTools);
|
|
208
238
|
const names = collectRoutedCustomToolWireNames(body, supportsResponsesCustomTools);
|
|
209
|
-
|
|
239
|
+
const repairNames = collectRoutedCustomToolWireNames(body, supportsResponsesCustomTools, true);
|
|
240
|
+
for (const name of repairNames) {
|
|
241
|
+
if (!toolChoiceAllowsRoutedCustomTool(body, name, repairNames)) repairNames.delete(name);
|
|
242
|
+
}
|
|
243
|
+
if (conversionNames.size === 0) return { body, names, repairNames };
|
|
210
244
|
const callIds = new Set<string>();
|
|
211
245
|
collectConvertedCallIds(body, conversionNames, callIds);
|
|
212
|
-
return { body: rewriteForUpstream(body, conversionNames, callIds), names };
|
|
246
|
+
return { body: rewriteForUpstream(body, conversionNames, callIds), names, repairNames };
|
|
213
247
|
}
|
|
214
248
|
|
|
215
249
|
export function restoreRoutedCustomCalls(
|
|
216
250
|
value: unknown,
|
|
217
251
|
names: ReadonlySet<string>,
|
|
252
|
+
repairNames: ReadonlySet<string> = new Set(),
|
|
218
253
|
): { value: unknown; changed: boolean } {
|
|
219
|
-
if (
|
|
254
|
+
if (!isPlainObject(value)) return { value, changed: false };
|
|
255
|
+
|
|
256
|
+
const restoreItem = (item: unknown): { value: unknown; changed: boolean } => {
|
|
257
|
+
if (!isPlainObject(item)) return { value: item, changed: false };
|
|
258
|
+
const wireName = routedCustomToolWireName(item);
|
|
259
|
+
if (
|
|
260
|
+
item.type === "function_call"
|
|
261
|
+
&& typeof item.name === "string"
|
|
262
|
+
&& wireName !== undefined
|
|
263
|
+
&& names.has(wireName)
|
|
264
|
+
) {
|
|
265
|
+
const restored: Record<string, unknown> = {
|
|
266
|
+
...item,
|
|
267
|
+
type: "custom_tool_call",
|
|
268
|
+
id: customToolItemId(item.id),
|
|
269
|
+
input: repairFreeformToolInput(
|
|
270
|
+
item.arguments,
|
|
271
|
+
item.name,
|
|
272
|
+
typeof item.namespace === "string" ? item.namespace : undefined,
|
|
273
|
+
),
|
|
274
|
+
};
|
|
275
|
+
delete restored.arguments;
|
|
276
|
+
return { value: restored, changed: true };
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
item.type === "custom_tool_call"
|
|
280
|
+
&& typeof item.name === "string"
|
|
281
|
+
&& wireName !== undefined
|
|
282
|
+
&& repairNames.has(wireName)
|
|
283
|
+
&& typeof item.input === "string"
|
|
284
|
+
) {
|
|
285
|
+
const input = normalizeApplyPatchDelimiters(item.input);
|
|
286
|
+
if (input !== item.input) return { value: { ...item, input }, changed: true };
|
|
287
|
+
}
|
|
288
|
+
return { value: item, changed: false };
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const restoreOutput = (output: unknown): { value: unknown; changed: boolean } => {
|
|
292
|
+
if (!Array.isArray(output)) return { value: output, changed: false };
|
|
220
293
|
let changed = false;
|
|
221
|
-
const restored =
|
|
222
|
-
const result =
|
|
294
|
+
const restored = output.map(item => {
|
|
295
|
+
const result = restoreItem(item);
|
|
223
296
|
changed ||= result.changed;
|
|
224
297
|
return result.value;
|
|
225
298
|
});
|
|
226
|
-
return changed ? { value: restored, changed: true } : { value, changed: false };
|
|
227
|
-
}
|
|
228
|
-
if (!isPlainObject(value)) return { value, changed: false };
|
|
299
|
+
return changed ? { value: restored, changed: true } : { value: output, changed: false };
|
|
300
|
+
};
|
|
229
301
|
|
|
230
302
|
let changed = false;
|
|
231
|
-
const restored: Record<string, unknown> = {};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
restored
|
|
235
|
-
changed
|
|
303
|
+
const restored: Record<string, unknown> = { ...value };
|
|
304
|
+
const output = restoreOutput(value.output);
|
|
305
|
+
if (output.changed) {
|
|
306
|
+
restored.output = output.value;
|
|
307
|
+
changed = true;
|
|
236
308
|
}
|
|
237
309
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
310
|
+
if (
|
|
311
|
+
(value.type === "response.output_item.added" || value.type === "response.output_item.done")
|
|
312
|
+
&& isPlainObject(value.item)
|
|
313
|
+
) {
|
|
314
|
+
const item = restoreItem(value.item);
|
|
315
|
+
if (item.changed) {
|
|
316
|
+
restored.item = item.value;
|
|
317
|
+
changed = true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
typeof value.type === "string"
|
|
323
|
+
&& value.type.startsWith("response.")
|
|
324
|
+
&& isPlainObject(value.response)
|
|
325
|
+
) {
|
|
326
|
+
const response = restoreRoutedCustomCalls(value.response, names, repairNames);
|
|
327
|
+
if (response.changed) {
|
|
328
|
+
restored.response = response.value;
|
|
329
|
+
changed = true;
|
|
330
|
+
}
|
|
245
331
|
}
|
|
332
|
+
|
|
246
333
|
return changed ? { value: restored, changed: true } : { value, changed: false };
|
|
247
334
|
}
|
|
248
335
|
|
|
249
336
|
export function restoreRoutedCustomCallsInJson(
|
|
250
337
|
text: string,
|
|
251
338
|
names: ReadonlySet<string>,
|
|
339
|
+
repairNames: ReadonlySet<string> = new Set(),
|
|
252
340
|
): string {
|
|
253
|
-
if (names.size === 0) return text;
|
|
341
|
+
if (names.size === 0 && repairNames.size === 0) return text;
|
|
254
342
|
let payload: unknown;
|
|
255
343
|
try {
|
|
256
344
|
payload = JSON.parse(text);
|
|
257
345
|
} catch {
|
|
258
346
|
return text;
|
|
259
347
|
}
|
|
260
|
-
const restored = restoreRoutedCustomCalls(payload, names);
|
|
348
|
+
const restored = restoreRoutedCustomCalls(payload, names, repairNames);
|
|
261
349
|
return restored.changed ? JSON.stringify(restored.value) : text;
|
|
262
350
|
}
|
|
263
351
|
|
|
264
|
-
export function unwrapRoutedCustomToolArguments(
|
|
265
|
-
|
|
352
|
+
export function unwrapRoutedCustomToolArguments(
|
|
353
|
+
argumentsText: unknown,
|
|
354
|
+
toolName = "",
|
|
355
|
+
namespace?: string,
|
|
356
|
+
): string {
|
|
357
|
+
return toolName
|
|
358
|
+
? repairFreeformToolInput(argumentsText, toolName, namespace)
|
|
359
|
+
: unwrapFreeformToolInput(argumentsText);
|
|
266
360
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { effectiveGoogleMode } from "../providers/registry";
|
|
2
|
+
import type { OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
const AI_STUDIO_CACHED_CONTENT = /^cachedContents\/[^/?#\s]+$/;
|
|
5
|
+
const VERTEX_CACHED_CONTENT = /^projects\/[^/?#\s]+\/locations\/[^/?#\s]+\/cachedContents\/[^/?#\s]+$/;
|
|
6
|
+
|
|
7
|
+
export function googleProviderOptionsRouteError(
|
|
8
|
+
parsed: Pick<OcxParsedRequest, "options">,
|
|
9
|
+
route: {
|
|
10
|
+
providerName: string;
|
|
11
|
+
provider: OcxProviderConfig;
|
|
12
|
+
adapterName: string;
|
|
13
|
+
},
|
|
14
|
+
): string | undefined {
|
|
15
|
+
if (!parsed.options.providerOptions?.google) return undefined;
|
|
16
|
+
const mode = effectiveGoogleMode(route.providerName, route.provider);
|
|
17
|
+
if (mode === "cloud-code-assist") {
|
|
18
|
+
return "provider_options.google is not supported on Google Cloud Code Assist routes";
|
|
19
|
+
}
|
|
20
|
+
if (route.adapterName !== "google" || (mode !== "ai-studio" && mode !== "vertex")) {
|
|
21
|
+
return "provider_options.google is supported only on Google AI Studio or Vertex routes";
|
|
22
|
+
}
|
|
23
|
+
if (mode === "ai-studio") {
|
|
24
|
+
const cachedContent = parsed.options.providerOptions.google.cachedContent;
|
|
25
|
+
if (cachedContent && VERTEX_CACHED_CONTENT.test(cachedContent)) {
|
|
26
|
+
return "provider_options.google.cached_content must use cachedContents/{id} on Google AI Studio routes";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (mode === "vertex") {
|
|
30
|
+
const cachedContent = parsed.options.providerOptions.google.cachedContent;
|
|
31
|
+
if (cachedContent && AI_STUDIO_CACHED_CONTENT.test(cachedContent)) {
|
|
32
|
+
return "provider_options.google.cached_content must use projects/{project}/locations/{location}/cachedContents/{id} on Vertex routes";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
@@ -4,6 +4,14 @@ import { collectResponsesToolGroups } from "./tool-groups";
|
|
|
4
4
|
export interface RoutedNamespaceToolIdentity {
|
|
5
5
|
namespace: string;
|
|
6
6
|
name: string;
|
|
7
|
+
/**
|
|
8
|
+
* The kind the tool was DECLARED as. Restoration and `tool_choice` matching both
|
|
9
|
+
* need it: a wire name identifies which tool, not which kind of call may carry it,
|
|
10
|
+
* so without this a tool declared `function` could be selected by a `custom`
|
|
11
|
+
* selector and come back as a `custom_tool_call` — the same name/kind mismatch
|
|
12
|
+
* that motivated narrowing the alias map in the first place.
|
|
13
|
+
*/
|
|
14
|
+
kind: "function" | "custom";
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
export type RoutedNamespaceToolAliases = ReadonlyMap<string, RoutedNamespaceToolIdentity>;
|
|
@@ -138,7 +146,10 @@ function buildRewritePlan(groups: readonly unknown[][]): NamespaceRewritePlan {
|
|
|
138
146
|
addSelector(selectors, `${parsed.namespace}.${childName}`, wireName);
|
|
139
147
|
addSelector(selectors, childName, wireName);
|
|
140
148
|
if (parsed.namespace !== BUILTIN_FUNCTIONS_NAMESPACE) {
|
|
141
|
-
|
|
149
|
+
// A child declared `custom` stays custom; everything else lowers to a
|
|
150
|
+
// function, which is how `buildTools` flattens it upstream.
|
|
151
|
+
const kind = child.type === "custom" ? "custom" : "function";
|
|
152
|
+
aliases.set(wireName, { namespace: parsed.namespace, name: childName, kind });
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
}
|
|
@@ -206,18 +217,34 @@ function rewriteToolList(
|
|
|
206
217
|
* the failure this layer exists to prevent, and this layer's own response restoration is what put
|
|
207
218
|
* the key on the item.
|
|
208
219
|
*/
|
|
220
|
+
/**
|
|
221
|
+
* A selector's `namespace` is either absent — meaning "unqualified, resolve the bare
|
|
222
|
+
* name" — or a string naming the group. A present-but-non-string value is neither, and
|
|
223
|
+
* a malformed selector must not authorize anything: not through the unqualified
|
|
224
|
+
* fallback, and not by happening to carry an already-flattened wire name, which would
|
|
225
|
+
* otherwise match the alias map exactly and arm it anyway.
|
|
226
|
+
*/
|
|
227
|
+
function hasMalformedNamespace(value: Record<string, unknown>): boolean {
|
|
228
|
+
return "namespace" in value && typeof value.namespace !== "string";
|
|
229
|
+
}
|
|
230
|
+
|
|
209
231
|
function rewriteNamedSelector(
|
|
210
232
|
value: unknown,
|
|
211
233
|
plan: NamespaceRewritePlan,
|
|
212
234
|
bareFallback: boolean,
|
|
213
235
|
): unknown {
|
|
214
236
|
if (!isPlainObject(value) || typeof value.name !== "string") return value;
|
|
215
|
-
|
|
237
|
+
// Malformed: hand it back untouched so no rewrite occurs. Authorization rejects it
|
|
238
|
+
// separately — returning it unchanged is not by itself enough, because the name it
|
|
239
|
+
// carries may already BE a wire name.
|
|
240
|
+
if (hasMalformedNamespace(value)) return value;
|
|
241
|
+
const namespace = value.namespace;
|
|
242
|
+
if (typeof namespace !== "string") {
|
|
216
243
|
if (!bareFallback) return value;
|
|
217
244
|
const wireName = plan.selectors.get(value.name) ?? undefined;
|
|
218
245
|
return wireName === undefined || wireName === value.name ? value : { ...value, name: wireName };
|
|
219
246
|
}
|
|
220
|
-
const { namespace, ...rest } = value;
|
|
247
|
+
const { namespace: _dropped, ...rest } = value;
|
|
221
248
|
const wireName = plan.identities.get(loweredIdentity(namespace, value.name))
|
|
222
249
|
?? loweredWireName(namespace, value.name);
|
|
223
250
|
return { ...rest, name: wireName };
|
|
@@ -239,6 +266,59 @@ function rewriteToolChoice(value: unknown, plan: NamespaceRewritePlan): unknown
|
|
|
239
266
|
return changed ? { ...value, tools } : value;
|
|
240
267
|
}
|
|
241
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Keep response restoration inside the caller's per-turn tool authorization boundary. The
|
|
271
|
+
* upstream sees every flattened declaration even when `tool_choice` narrows the tools it may call,
|
|
272
|
+
* so its output cannot be trusted merely because a wire name appeared in that catalog.
|
|
273
|
+
*/
|
|
274
|
+
function authorizedAliases(
|
|
275
|
+
aliases: Map<string, RoutedNamespaceToolIdentity>,
|
|
276
|
+
toolChoice: unknown,
|
|
277
|
+
): Map<string, RoutedNamespaceToolIdentity> {
|
|
278
|
+
if (toolChoice === undefined || toolChoice === "auto" || toolChoice === "required") return aliases;
|
|
279
|
+
if (toolChoice === "none" || !isPlainObject(toolChoice)) return new Map();
|
|
280
|
+
|
|
281
|
+
// name -> the kind the selector claimed. A selector authorizes a tool only when it
|
|
282
|
+
// names it AND agrees about what kind of tool it is.
|
|
283
|
+
let authorized: Map<string, "function" | "custom">;
|
|
284
|
+
if (
|
|
285
|
+
(toolChoice.type === "function" || toolChoice.type === "custom")
|
|
286
|
+
&& typeof toolChoice.name === "string"
|
|
287
|
+
) {
|
|
288
|
+
// A malformed namespace makes the whole selector untrustworthy, even when its
|
|
289
|
+
// name is already a flattened wire name that would match the alias map exactly.
|
|
290
|
+
if (hasMalformedNamespace(toolChoice)) return new Map();
|
|
291
|
+
authorized = new Map([[toolChoice.name, toolChoice.type]]);
|
|
292
|
+
} else if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
|
|
293
|
+
authorized = new Map();
|
|
294
|
+
for (const tool of toolChoice.tools) {
|
|
295
|
+
// Match the top-level branch above: only a function/custom selector can
|
|
296
|
+
// authorize a client namespace call. `allowed_tools` entries are typed
|
|
297
|
+
// `{type: string}` by the schema, so the accepted set is open-ended and
|
|
298
|
+
// an allowlist is the only closure that also covers kinds added later.
|
|
299
|
+
// Without this, `{type: "file_search", name: "<wire-name>"}` keeps the
|
|
300
|
+
// alias, and an upstream `function_call` carrying that name is restored
|
|
301
|
+
// into a namespace call the caller never permitted.
|
|
302
|
+
if (!isPlainObject(tool)) continue;
|
|
303
|
+
if (tool.type !== "function" && tool.type !== "custom") continue;
|
|
304
|
+
if (typeof tool.name !== "string") continue;
|
|
305
|
+
if (hasMalformedNamespace(tool)) continue;
|
|
306
|
+
authorized.set(tool.name, tool.type);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// An explicit selector for another tool kind does not authorize a client namespace call.
|
|
310
|
+
return new Map();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// The kind must agree too. A wire name says WHICH tool, not what kind of call may
|
|
314
|
+
// carry it, so a `custom` selector naming a tool declared `function` is the same
|
|
315
|
+
// name/kind mismatch as a `file_search` selector naming it — narrower, but the
|
|
316
|
+
// same class, and `allowed_tools[].type` accepts any string so both are reachable.
|
|
317
|
+
return new Map(
|
|
318
|
+
[...aliases].filter(([wireName, identity]) => authorized.get(wireName) === identity.kind),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
242
322
|
function rewriteInputItem(item: unknown, plan: NamespaceRewritePlan, emitted: Set<string>): unknown {
|
|
243
323
|
if (!isPlainObject(item)) return item;
|
|
244
324
|
if (item.type === "additional_tools" && Array.isArray(item.tools)) {
|
|
@@ -292,7 +372,7 @@ export function rewriteRoutedNamespaceToolsForUpstream(body: unknown): {
|
|
|
292
372
|
...(input !== body.input ? { input } : {}),
|
|
293
373
|
...(toolChoice !== body.tool_choice ? { tool_choice: toolChoice } : {}),
|
|
294
374
|
},
|
|
295
|
-
aliases: plan.aliases,
|
|
375
|
+
aliases: authorizedAliases(plan.aliases, toolChoice),
|
|
296
376
|
};
|
|
297
377
|
}
|
|
298
378
|
|