@yansigit/opencodex 2.31.2 → 2.32.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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +377 -0
- package/src/adapters/command-code.ts +7 -4
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +21 -0
- package/src/adapters/cursor/native-exec-bridge.ts +141 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google-http.ts +12 -2
- package/src/adapters/google-wire-compiler.ts +83 -2
- package/src/adapters/google.ts +62 -21
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -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/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- 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/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 +177 -0
- package/src/config.ts +77 -812
- package/src/generated/compatibility-version.json +162 -94
- 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/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +3 -0
- package/src/providers/registry.ts +1 -1
- 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/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/auth-cors.ts +9 -7
- 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 +1 -1
- package/src/server/management/config-routes.ts +4 -1
- 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/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- 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/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +242 -101
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +12 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -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 +35 -13
- package/src/web-search/index.ts +85 -1
- package/gui/dist/assets/index-BAMgarF9.js +0 -102
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
|
@@ -5,107 +5,12 @@ import {
|
|
|
5
5
|
shouldUseCodexWsUpstream,
|
|
6
6
|
type BunRuntimeGateInput,
|
|
7
7
|
} from "./ws-upstream";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
getConfigPath,
|
|
11
|
-
multiAgentGuidanceEnabled,
|
|
12
|
-
resolveEnvValue,
|
|
13
|
-
} from "../../config";
|
|
14
|
-
import { parseRequest } from "../../responses/parser";
|
|
15
|
-
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
16
|
-
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
17
|
-
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
18
|
-
import { routeModel } from "../../router";
|
|
19
|
-
import {
|
|
20
|
-
advanceComboAfterFailure,
|
|
21
|
-
comboDefaultEffort,
|
|
22
|
-
comboFailureDecision,
|
|
23
|
-
comboIdFromRawBody,
|
|
24
|
-
concreteComboRequestBody,
|
|
25
|
-
getCombo,
|
|
26
|
-
isComboTargetInCooldown,
|
|
27
|
-
NoAvailableComboTargetsError,
|
|
28
|
-
noteComboSuccess,
|
|
29
|
-
parseRetryAfterMs,
|
|
30
|
-
pickComboTarget,
|
|
31
|
-
targetKey,
|
|
32
|
-
} from "../../combos";
|
|
33
|
-
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
34
|
-
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
35
|
-
import { modelInList, namespacedToolName } from "../../types";
|
|
36
|
-
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
|
|
37
|
-
import {
|
|
38
|
-
forceRefreshOAuthAccessSnapshot,
|
|
39
|
-
getOAuthCredentialApiBaseUrl,
|
|
40
|
-
getOAuthCredentialProjectId,
|
|
41
|
-
getValidAccessTokenSnapshot,
|
|
42
|
-
type OAuthAccessSnapshot,
|
|
43
|
-
UnsupportedOAuthProviderError,
|
|
44
|
-
} from "../../oauth";
|
|
45
|
-
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
46
|
-
import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
47
|
-
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
|
|
48
|
-
import {
|
|
49
|
-
applyCodexAuthContextToProvider,
|
|
50
|
-
CodexAccountCooldownError,
|
|
51
|
-
CodexAuthContextError,
|
|
52
|
-
CodexDirectAuthenticationError,
|
|
53
|
-
CodexPoolAuthenticationError,
|
|
54
|
-
CodexThreadAffinityExpiredError,
|
|
55
|
-
headersForCodexAuthContext,
|
|
56
|
-
isCodexAuthContextUsable,
|
|
57
|
-
resolveCodexAuthContext,
|
|
58
|
-
type CodexAuthContext,
|
|
59
|
-
} from "../../codex/auth-context";
|
|
60
|
-
import {
|
|
61
|
-
formatCodexProviderForLog,
|
|
62
|
-
recordCodexUpstreamOutcome,
|
|
63
|
-
type CodexUpstreamOutcome,
|
|
64
|
-
} from "../../codex/routing";
|
|
65
|
-
import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../../lib/upstream-retry";
|
|
66
|
-
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
|
|
67
|
-
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
68
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
69
|
-
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
70
|
-
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
71
|
-
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
72
|
-
import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
|
|
73
|
-
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
74
|
-
import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover";
|
|
75
|
-
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
76
|
-
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
8
|
+
import type { OcxProviderConfig } from "../../types";
|
|
77
9
|
import type { WsData } from "../ws-bridge";
|
|
78
|
-
import { registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
|
|
79
|
-
import { redactSecretString } from "../../lib/redact";
|
|
80
|
-
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
81
|
-
import { supportedLadderFor } from "../effort-policy";
|
|
82
|
-
import {
|
|
83
|
-
beginRequestAttempt,
|
|
84
|
-
catalogModelSupportsServiceTier,
|
|
85
|
-
finishRequestAttempt,
|
|
86
|
-
inspectResponseLogJson,
|
|
87
|
-
noteAttemptSend,
|
|
88
|
-
readConfiguredCodexServiceTier,
|
|
89
|
-
requestLogSpeedLabel,
|
|
90
|
-
sealRequestAttemptIdentity,
|
|
91
|
-
usageFromResponsesPayload,
|
|
92
|
-
type RequestLogContext,
|
|
93
|
-
} from "../request-log";
|
|
94
|
-
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
95
|
-
import {
|
|
96
|
-
consumeForInspection,
|
|
97
|
-
consumeForResponseLogMetadata,
|
|
98
|
-
markNativePassthroughSseResponse,
|
|
99
|
-
relaySseWithFailedTail,
|
|
100
|
-
relayWithAbort,
|
|
101
|
-
sanitizePassthroughHeaders,
|
|
102
|
-
} from "../relay";
|
|
103
|
-
import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
|
|
104
|
-
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
105
10
|
import { waitForProviderRequestSlot } from "../../providers/request-pacing";
|
|
106
11
|
import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
|
|
107
12
|
|
|
108
|
-
export { withUpstreamHttpVersion }
|
|
13
|
+
export { withUpstreamHttpVersion };
|
|
109
14
|
|
|
110
15
|
export function disableResponsesRequestTimeout(req: Request, server: Pick<Server<WsData>, "timeout"> | undefined): boolean {
|
|
111
16
|
if (!server) return false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
2
|
+
import { normalizeApplyPatchDelimiters } from "../responses/apply-patch-envelope";
|
|
2
3
|
import {
|
|
3
4
|
customToolItemId,
|
|
4
5
|
restoreRoutedCustomCalls,
|
|
@@ -85,8 +86,10 @@ type PendingArgumentBlock = {
|
|
|
85
86
|
export function createRoutedCustomToolRestoreBlockRewrite(
|
|
86
87
|
names: ReadonlySet<string>,
|
|
87
88
|
budget?: TranslatorBudget,
|
|
89
|
+
repairNames: ReadonlySet<string> = new Set(),
|
|
88
90
|
): SseBlockRewrite {
|
|
89
|
-
const itemNames = new Map<string, string>();
|
|
91
|
+
const itemNames = new Map<string, { name: string; namespace?: string }>();
|
|
92
|
+
const repairItemNames = new Map<string, string>();
|
|
90
93
|
const ordinaryItemIds = new Set<string>();
|
|
91
94
|
const openCalls = new Map<string, OpenCustomCall>();
|
|
92
95
|
let pendingArguments: PendingArgumentBlock[] = [];
|
|
@@ -111,6 +114,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
|
|
|
111
114
|
}
|
|
112
115
|
pendingArguments = [];
|
|
113
116
|
itemNames.clear();
|
|
117
|
+
repairItemNames.clear();
|
|
114
118
|
ordinaryItemIds.clear();
|
|
115
119
|
};
|
|
116
120
|
|
|
@@ -175,6 +179,23 @@ export function createRoutedCustomToolRestoreBlockRewrite(
|
|
|
175
179
|
&& parsed.output_index >= 0
|
|
176
180
|
? parsed.output_index
|
|
177
181
|
: undefined;
|
|
182
|
+
if (
|
|
183
|
+
(type === "response.output_item.added" || type === "response.output_item.done")
|
|
184
|
+
&& isPlainObject(parsed.item)
|
|
185
|
+
&& parsed.item.type === "custom_tool_call"
|
|
186
|
+
&& typeof parsed.item.name === "string"
|
|
187
|
+
) {
|
|
188
|
+
const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
|
|
189
|
+
const wireName = routedCustomToolWireName(parsed.item);
|
|
190
|
+
const repairable = wireName !== undefined && repairNames.has(wireName);
|
|
191
|
+
if (upstreamItemId && repairable) repairItemNames.set(upstreamItemId, parsed.item.name);
|
|
192
|
+
const restored = repairable
|
|
193
|
+
? restoreRoutedCustomCalls(parsed, names, repairNames)
|
|
194
|
+
: { value: parsed, changed: false };
|
|
195
|
+
return restored.changed
|
|
196
|
+
? [replaceSseDataPayload(block, JSON.stringify(restored.value))]
|
|
197
|
+
: [block];
|
|
198
|
+
}
|
|
178
199
|
if (
|
|
179
200
|
(type === "response.output_item.added" || type === "response.output_item.done")
|
|
180
201
|
&& isPlainObject(parsed.item)
|
|
@@ -186,7 +207,10 @@ export function createRoutedCustomToolRestoreBlockRewrite(
|
|
|
186
207
|
const routed = wireName !== undefined && names.has(wireName);
|
|
187
208
|
if (upstreamItemId) {
|
|
188
209
|
if (routed) {
|
|
189
|
-
itemNames.set(upstreamItemId,
|
|
210
|
+
itemNames.set(upstreamItemId, {
|
|
211
|
+
name: parsed.item.name,
|
|
212
|
+
...(typeof parsed.item.namespace === "string" ? { namespace: parsed.item.namespace } : {}),
|
|
213
|
+
});
|
|
190
214
|
ordinaryItemIds.delete(upstreamItemId);
|
|
191
215
|
} else {
|
|
192
216
|
ordinaryItemIds.add(upstreamItemId);
|
|
@@ -203,7 +227,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
|
|
|
203
227
|
if (upstreamItemId && pending.length > 0 && !openCalls.has(upstreamItemId)) {
|
|
204
228
|
openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
|
|
205
229
|
}
|
|
206
|
-
const restored = restoreRoutedCustomCalls(parsed, names);
|
|
230
|
+
const restored = restoreRoutedCustomCalls(parsed, names, repairNames);
|
|
207
231
|
const restoredBlock = restored.changed
|
|
208
232
|
? replaceSseDataPayload(block, JSON.stringify(restored.value))
|
|
209
233
|
: block;
|
|
@@ -215,6 +239,17 @@ export function createRoutedCustomToolRestoreBlockRewrite(
|
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
const upstreamItemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
|
|
242
|
+
if (
|
|
243
|
+
type === "response.custom_tool_call_input.done"
|
|
244
|
+
&& upstreamItemId
|
|
245
|
+
&& repairItemNames.has(upstreamItemId)
|
|
246
|
+
&& typeof parsed.input === "string"
|
|
247
|
+
) {
|
|
248
|
+
const input = normalizeApplyPatchDelimiters(parsed.input);
|
|
249
|
+
if (input !== parsed.input) {
|
|
250
|
+
return [replaceSseDataPayload(block, JSON.stringify({ ...parsed, input }))];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
218
253
|
const argumentEvent = type === "response.function_call_arguments.delta"
|
|
219
254
|
|| type === "response.function_call_arguments.done";
|
|
220
255
|
if (argumentEvent && (!upstreamItemId || (!itemNames.has(upstreamItemId) && !ordinaryItemIds.has(upstreamItemId)))) {
|
|
@@ -261,16 +296,17 @@ export function createRoutedCustomToolRestoreBlockRewrite(
|
|
|
261
296
|
? parsed.arguments
|
|
262
297
|
: openCalls.get(upstreamItemId)?.argumentsText ?? "";
|
|
263
298
|
const { arguments: _arguments, ...rest } = parsed;
|
|
299
|
+
const itemName = itemNames.get(upstreamItemId);
|
|
264
300
|
const next = {
|
|
265
301
|
...rest,
|
|
266
302
|
type: nextType,
|
|
267
303
|
item_id: customToolItemId(upstreamItemId),
|
|
268
|
-
input: unwrapRoutedCustomToolArguments(source),
|
|
304
|
+
input: unwrapRoutedCustomToolArguments(source, itemName?.name ?? "", itemName?.namespace),
|
|
269
305
|
};
|
|
270
306
|
return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))];
|
|
271
307
|
}
|
|
272
308
|
|
|
273
|
-
const restored = restoreRoutedCustomCalls(parsed, names);
|
|
309
|
+
const restored = restoreRoutedCustomCalls(parsed, names, repairNames);
|
|
274
310
|
const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete";
|
|
275
311
|
if (terminal) releaseAll();
|
|
276
312
|
return restored.changed
|
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
import { namespacedToolName } from "../types";
|
|
2
2
|
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Item types the CLIENT executes by name. Hosted calls (`web_search_call`,
|
|
6
|
-
* `image_generation_call`, `local_shell_call`, `tool_search_call`, …) are run upstream or
|
|
7
|
-
* carry no tool name, so they are never matched against the request catalog.
|
|
8
|
-
*/
|
|
4
|
+
/** Item types the client executes through a request-declared wire name. */
|
|
9
5
|
const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
|
|
10
6
|
|
|
7
|
+
/** Nameless declaration kinds whose response items still require client execution. */
|
|
8
|
+
const NAMELESS_CLIENT_DECLARATION_CALL_TYPES = new Map([
|
|
9
|
+
["local_shell", "local_shell_call"],
|
|
10
|
+
["tool_search", "tool_search_call"],
|
|
11
|
+
["computer_use_preview", "computer_call"],
|
|
12
|
+
["computer_use", "computer_call"],
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const NAMELESS_CLIENT_CALL_DISPLAY_NAMES = new Map([
|
|
16
|
+
["local_shell_call", "local_shell"],
|
|
17
|
+
["tool_search_call", "tool_search"],
|
|
18
|
+
["computer_call", "computer_use"],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES: ReadonlySet<string> = new Set();
|
|
22
|
+
|
|
23
|
+
/** Supported hosted/private declarations that carry no client-executable wire name. */
|
|
24
|
+
const NAMELESS_TOOL_SPEC_TYPES = new Set([
|
|
25
|
+
"web_search",
|
|
26
|
+
"web_search_preview",
|
|
27
|
+
"file_search",
|
|
28
|
+
"computer_use_preview",
|
|
29
|
+
"computer_use",
|
|
30
|
+
"code_interpreter",
|
|
31
|
+
"image_generation",
|
|
32
|
+
"image_gen",
|
|
33
|
+
"mcp",
|
|
34
|
+
"tool_search",
|
|
35
|
+
"local_shell",
|
|
36
|
+
"x_search",
|
|
37
|
+
]);
|
|
38
|
+
|
|
11
39
|
/** An upstream-supplied name reaches the error message; keep it bounded. */
|
|
12
40
|
const MAX_REPORTED_NAME_CHARS = 100;
|
|
13
41
|
|
|
@@ -18,12 +46,40 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
18
46
|
}
|
|
19
47
|
|
|
20
48
|
function addWireToolName(names: Set<string>, tool: unknown, namespace?: string): void {
|
|
21
|
-
if (!isPlainObject(tool)
|
|
22
|
-
|
|
49
|
+
if (!isPlainObject(tool)) return;
|
|
50
|
+
const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
|
|
51
|
+
? tool.function
|
|
52
|
+
: undefined;
|
|
53
|
+
const name = typeof tool.name === "string" && tool.name.length > 0
|
|
54
|
+
? tool.name
|
|
55
|
+
: typeof nestedFunction?.name === "string" && nestedFunction.name.length > 0
|
|
56
|
+
? nestedFunction.name
|
|
57
|
+
: undefined;
|
|
58
|
+
if (!name) return;
|
|
59
|
+
names.add(name);
|
|
23
60
|
// Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
|
|
24
61
|
// as a bare inner name or as the flattened form; accept both rather than guess which
|
|
25
62
|
// coordinate system this provider echoes back.
|
|
26
|
-
if (namespace) names.add(namespacedToolName(namespace,
|
|
63
|
+
if (namespace) names.add(namespacedToolName(namespace, name));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Catalog view owned by the current Responses turn.
|
|
68
|
+
*
|
|
69
|
+
* `previous_response_id` expansion prepends stored input items, including historical
|
|
70
|
+
* `additional_tools` declarations. Those items remain conversation history but cannot grant
|
|
71
|
+
* execution authority to this turn. Top-level `tools` always belongs to the current request;
|
|
72
|
+
* only input catalogs at or after the replay boundary are current.
|
|
73
|
+
*/
|
|
74
|
+
export function currentTurnWireToolCatalogBody(
|
|
75
|
+
body: unknown,
|
|
76
|
+
replayPrefixLength: number | undefined,
|
|
77
|
+
): unknown {
|
|
78
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
79
|
+
if (typeof replayPrefixLength !== "number" || !Number.isFinite(replayPrefixLength)) return body;
|
|
80
|
+
const start = Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)));
|
|
81
|
+
if (start === 0) return body;
|
|
82
|
+
return { ...body, input: body.input.slice(start) };
|
|
27
83
|
}
|
|
28
84
|
|
|
29
85
|
function addWireToolSpecs(names: Set<string>, specs: unknown): void {
|
|
@@ -53,15 +109,97 @@ export function collectDeclaredWireToolNames(body: unknown): Set<string> {
|
|
|
53
109
|
addWireToolSpecs(names, body.tools);
|
|
54
110
|
if (Array.isArray(body.input)) {
|
|
55
111
|
for (const item of body.input) {
|
|
56
|
-
if (
|
|
112
|
+
if (
|
|
113
|
+
isPlainObject(item)
|
|
114
|
+
&& (item.type === "additional_tools" || item.type === "tool_search_output")
|
|
115
|
+
) addWireToolSpecs(names, item.tools);
|
|
57
116
|
}
|
|
58
117
|
}
|
|
59
118
|
return names;
|
|
60
119
|
}
|
|
61
120
|
|
|
62
|
-
function
|
|
121
|
+
function addNamelessClientCallTypes(callTypes: Set<string>, specs: unknown): void {
|
|
122
|
+
if (!Array.isArray(specs)) return;
|
|
123
|
+
for (const spec of specs) {
|
|
124
|
+
if (!isPlainObject(spec) || typeof spec.type !== "string") continue;
|
|
125
|
+
const callType = NAMELESS_CLIENT_DECLARATION_CALL_TYPES.get(spec.type);
|
|
126
|
+
if (callType) callTypes.add(callType);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Nameless client-call item types authorized by supported request tool declarations. */
|
|
131
|
+
export function collectDeclaredNamelessClientCallTypes(body: unknown): Set<string> {
|
|
132
|
+
const callTypes = new Set<string>();
|
|
133
|
+
if (!isPlainObject(body)) return callTypes;
|
|
134
|
+
addNamelessClientCallTypes(callTypes, body.tools);
|
|
135
|
+
if (Array.isArray(body.input)) {
|
|
136
|
+
for (const item of body.input) {
|
|
137
|
+
if (
|
|
138
|
+
isPlainObject(item)
|
|
139
|
+
&& (item.type === "additional_tools" || item.type === "tool_search_output")
|
|
140
|
+
) {
|
|
141
|
+
addNamelessClientCallTypes(callTypes, item.tools);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return callTypes;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isReadableWireToolSpec(spec: unknown): boolean {
|
|
149
|
+
if (!isPlainObject(spec) || typeof spec.type !== "string" || spec.type.length === 0) return false;
|
|
150
|
+
if (spec.type === "function") {
|
|
151
|
+
return (typeof spec.name === "string" && spec.name.length > 0)
|
|
152
|
+
|| (isPlainObject(spec.function)
|
|
153
|
+
&& typeof spec.function.name === "string"
|
|
154
|
+
&& spec.function.name.length > 0);
|
|
155
|
+
}
|
|
156
|
+
if (spec.type === "custom") return typeof spec.name === "string" && spec.name.length > 0;
|
|
157
|
+
if (spec.type === "namespace") {
|
|
158
|
+
return typeof spec.name === "string"
|
|
159
|
+
&& spec.name.length > 0
|
|
160
|
+
&& Array.isArray(spec.tools)
|
|
161
|
+
&& (spec.tools.length === 0 || spec.tools.some(inner =>
|
|
162
|
+
isPlainObject(inner)
|
|
163
|
+
&& (inner.type === "function" || inner.type === "custom")
|
|
164
|
+
&& typeof inner.name === "string"
|
|
165
|
+
&& inner.name.length > 0
|
|
166
|
+
));
|
|
167
|
+
}
|
|
168
|
+
if (NAMELESS_TOOL_SPEC_TYPES.has(spec.type)) return true;
|
|
169
|
+
return typeof spec.name === "string" && spec.name.length > 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isReadableWireToolCatalog(value: unknown): boolean {
|
|
173
|
+
return Array.isArray(value)
|
|
174
|
+
&& (value.length === 0 || value.some(isReadableWireToolSpec));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Whether a request contains a supported catalog, including an explicit empty deny-all array. */
|
|
178
|
+
export function hasExplicitWireToolCatalog(body: unknown): boolean {
|
|
179
|
+
if (!isPlainObject(body)) return false;
|
|
180
|
+
if (isReadableWireToolCatalog(body.tools)) return true;
|
|
181
|
+
if (!Array.isArray(body.input)) return false;
|
|
182
|
+
return body.input.some(item =>
|
|
183
|
+
isPlainObject(item)
|
|
184
|
+
&& item.type === "additional_tools"
|
|
185
|
+
&& isReadableWireToolCatalog(item.tools)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function undeclaredNameInItem(
|
|
190
|
+
item: unknown,
|
|
191
|
+
declared: ReadonlySet<string>,
|
|
192
|
+
declaredNamelessClientCallTypes: ReadonlySet<string>,
|
|
193
|
+
): string | undefined {
|
|
63
194
|
if (!isPlainObject(item)) return undefined;
|
|
64
|
-
if (typeof item.type !== "string"
|
|
195
|
+
if (typeof item.type !== "string") return undefined;
|
|
196
|
+
const namelessDisplayName = NAMELESS_CLIENT_CALL_DISPLAY_NAMES.get(item.type);
|
|
197
|
+
if (namelessDisplayName !== undefined) {
|
|
198
|
+
// Only Codex's explicit `execution: "client"` form delegates tool search to the client.
|
|
199
|
+
if (item.type === "tool_search_call" && item.execution !== "client") return undefined;
|
|
200
|
+
return declaredNamelessClientCallTypes.has(item.type) ? undefined : namelessDisplayName;
|
|
201
|
+
}
|
|
202
|
+
if (!CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
|
|
65
203
|
const name = item.name;
|
|
66
204
|
if (typeof name !== "string" || name.length === 0) return undefined;
|
|
67
205
|
if (declared.has(name)) return undefined;
|
|
@@ -75,14 +213,15 @@ function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): str
|
|
|
75
213
|
export function undeclaredToolCallName(
|
|
76
214
|
payload: unknown,
|
|
77
215
|
declared: ReadonlySet<string>,
|
|
216
|
+
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
|
|
78
217
|
): string | undefined {
|
|
79
218
|
if (!isPlainObject(payload)) return undefined;
|
|
80
219
|
if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
|
|
81
|
-
return undeclaredNameInItem(payload.item, declared);
|
|
220
|
+
return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes);
|
|
82
221
|
}
|
|
83
222
|
// Sparse gateways skip incremental items and only ever ship the terminal snapshot.
|
|
84
223
|
if (payload.type === "response.completed" || payload.type === "response.incomplete") {
|
|
85
|
-
return undeclaredToolCallNameInResponse(payload.response, declared);
|
|
224
|
+
return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes);
|
|
86
225
|
}
|
|
87
226
|
return undefined;
|
|
88
227
|
}
|
|
@@ -91,10 +230,11 @@ export function undeclaredToolCallName(
|
|
|
91
230
|
export function undeclaredToolCallNameInResponse(
|
|
92
231
|
response: unknown,
|
|
93
232
|
declared: ReadonlySet<string>,
|
|
233
|
+
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
|
|
94
234
|
): string | undefined {
|
|
95
235
|
if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
|
|
96
236
|
for (const item of response.output) {
|
|
97
|
-
const name = undeclaredNameInItem(item, declared);
|
|
237
|
+
const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes);
|
|
98
238
|
if (name !== undefined) return name;
|
|
99
239
|
}
|
|
100
240
|
return undefined;
|
|
@@ -133,6 +273,7 @@ function failedBlocks(name: string, newline: string): readonly string[] {
|
|
|
133
273
|
*/
|
|
134
274
|
export function createUndeclaredToolCallGuardBlockRewrite(
|
|
135
275
|
declared: ReadonlySet<string>,
|
|
276
|
+
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
|
|
136
277
|
): SseBlockRewrite {
|
|
137
278
|
let tripped = false;
|
|
138
279
|
return (block: string) => {
|
|
@@ -145,7 +286,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
|
|
|
145
286
|
} catch {
|
|
146
287
|
return [block];
|
|
147
288
|
}
|
|
148
|
-
const name = undeclaredToolCallName(parsed, declared);
|
|
289
|
+
const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes);
|
|
149
290
|
if (name === undefined) return [block];
|
|
150
291
|
tripped = true;
|
|
151
292
|
return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
|
package/src/service.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/
|
|
|
10
10
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir, tmpdir } from "node:os";
|
|
12
12
|
import { dirname, join, posix, resolve, win32 } from "node:path";
|
|
13
|
-
import { expandUserPath, getConfigDir,
|
|
14
|
-
import {
|
|
13
|
+
import { expandUserPath, getConfigDir, loadConfig } from "./config";
|
|
14
|
+
import { readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config/process-state";
|
|
15
15
|
import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
|
|
16
16
|
import { stripGrokConfig } from "./grok/inject";
|
|
17
17
|
import { isWslRuntime, resolveCodexHomeDir, type CodexHomeDeps } from "./codex/home";
|
|
@@ -97,13 +97,17 @@ function defaultOpenCodexHome(): string {
|
|
|
97
97
|
return resolve(join(homedir(), ".opencodex"));
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function
|
|
101
|
-
const paths = [
|
|
100
|
+
export function serviceStatePathsForOpenCodexHome(opencodexHome: string): string[] {
|
|
101
|
+
const paths = [join(opencodexHome, "service-state.json")];
|
|
102
102
|
const defaultPath = join(defaultOpenCodexHome(), "service-state.json");
|
|
103
103
|
if (normalizePathForCompare(defaultPath) !== normalizePathForCompare(paths[0])) paths.push(defaultPath);
|
|
104
104
|
return paths;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function serviceStatePaths(): string[] {
|
|
108
|
+
return serviceStatePathsForOpenCodexHome(currentOpenCodexHome());
|
|
109
|
+
}
|
|
110
|
+
|
|
107
111
|
function currentCodexHome(deps: CodexHomeDeps = {}): string {
|
|
108
112
|
// Service ownership must identify the same home as the runtime. In WSL an
|
|
109
113
|
// unset CODEX_HOME can resolve to the single Windows Desktop home rather than
|
package/src/types/provider.ts
CHANGED
|
@@ -169,6 +169,13 @@ export interface OcxProviderConfig {
|
|
|
169
169
|
* version here instead of waiting for a code change. Absent uses the adapter's current default.
|
|
170
170
|
*/
|
|
171
171
|
commandCodeVersion?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Command Code OAuth `/alpha/generate` project-context envelope. When `"on"`, the adapter
|
|
174
|
+
* fills `memory`, `taste`, and `skills` from bounded files under `process.cwd()`. Absent or
|
|
175
|
+
* `"off"` keeps the empty envelope (`memory: ""`, `taste: null`, `skills: null`). Does not
|
|
176
|
+
* enable taste learning (`x-taste-learning` stays `"false"`).
|
|
177
|
+
*/
|
|
178
|
+
projectContext?: "off" | "on";
|
|
172
179
|
/**
|
|
173
180
|
* Responses upstream that stores nothing server-side (DeepSeek documents "the API
|
|
174
181
|
* is stateless"). Stateful request parameters are dropped, `store` is pinned false,
|
package/src/types/request.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface OcxReasoningReplayIdentity {
|
|
|
30
30
|
* the holder, so late tool-call cache writes see the active physical identity.
|
|
31
31
|
*/
|
|
32
32
|
export interface OcxReasoningReplayScopeRef {
|
|
33
|
+
/**
|
|
34
|
+
* Conversation namespace for replay state. Historically this was always the Codex parent-thread
|
|
35
|
+
* id; headerless Responses callers use a raw sanitized thread/Cursor/session fallback, never the
|
|
36
|
+
* hashed request-log conversation id.
|
|
37
|
+
*/
|
|
33
38
|
readonly clientThreadId: string;
|
|
34
39
|
current?: Readonly<OcxReasoningReplayIdentity>;
|
|
35
40
|
}
|
|
@@ -63,7 +68,7 @@ export interface OcxParsedRequest {
|
|
|
63
68
|
_cursorConversationId?: string;
|
|
64
69
|
/** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
|
|
65
70
|
_clientThreadId?: string;
|
|
66
|
-
/**
|
|
71
|
+
/** Conversation/provider/account/model-bound namespace for reasoning replay state. */
|
|
67
72
|
_reasoningReplayScope?: OcxReasoningReplayScopeRef;
|
|
68
73
|
/**
|
|
69
74
|
* Set by bindRouteReasoningReplayScope after a proven serving-identity change, or by
|
|
@@ -94,6 +99,12 @@ export interface OcxParsedRequest {
|
|
|
94
99
|
* executes searches via the gpt-5.4-mini sidecar (see src/web-search). Absent when not requested.
|
|
95
100
|
*/
|
|
96
101
|
_webSearch?: Record<string, unknown>;
|
|
102
|
+
/**
|
|
103
|
+
* Antigravity Gemini in-turn CCA grounding: google_search and optional url_context ride the main
|
|
104
|
+
* routed fetch instead of the web-search sidecar loop. Set by core.ts when resolveCcaInTurnGrounding
|
|
105
|
+
* matches; consumed by the Google adapter at buildRequest/parseStream time.
|
|
106
|
+
*/
|
|
107
|
+
_ccaInTurnGrounding?: { search: boolean; urlContext: boolean };
|
|
97
108
|
/** Hosted image_generation tool config stashed for the image bridge sidecar (see src/images). */
|
|
98
109
|
_imageGeneration?: { toolNames: Set<string>; originalTool?: Record<string, unknown> };
|
|
99
110
|
/**
|
package/src/types/tools.ts
CHANGED
|
@@ -43,6 +43,74 @@ function sameToolIdentity(
|
|
|
43
43
|
return left.namespace === right.namespace && left.name === right.name;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
type ToolIdentity = Readonly<Pick<OcxTool, "namespace" | "name">>;
|
|
47
|
+
|
|
48
|
+
function snapshotToolIdentity(tool: Pick<OcxTool, "namespace" | "name">): ToolIdentity {
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
name: tool.name,
|
|
51
|
+
...(tool.namespace === undefined ? {} : { namespace: tool.namespace }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildToolChoiceCatalog(
|
|
56
|
+
tools: readonly ToolIdentity[],
|
|
57
|
+
): {
|
|
58
|
+
candidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>;
|
|
59
|
+
sourceCandidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>;
|
|
60
|
+
identitiesByTool: WeakMap<object, ToolIdentity>;
|
|
61
|
+
} {
|
|
62
|
+
const index = new Map<string, ToolIdentity[]>();
|
|
63
|
+
const sourceIndex = new Map<string, ToolIdentity[]>();
|
|
64
|
+
const identities = new Map<string, Set<string>>();
|
|
65
|
+
const identitiesByTool = new WeakMap<object, ToolIdentity>();
|
|
66
|
+
for (const tool of tools) {
|
|
67
|
+
const snapshot = snapshotToolIdentity(tool);
|
|
68
|
+
identitiesByTool.set(tool, snapshot);
|
|
69
|
+
const identity = JSON.stringify([snapshot.namespace ?? null, snapshot.name]);
|
|
70
|
+
for (const selector of [...toolChoiceAliases(snapshot), snapshot.name]) {
|
|
71
|
+
const candidates = index.get(selector);
|
|
72
|
+
if (!candidates) {
|
|
73
|
+
index.set(selector, [snapshot]);
|
|
74
|
+
sourceIndex.set(selector, [tool]);
|
|
75
|
+
identities.set(selector, new Set([identity]));
|
|
76
|
+
} else if (!identities.get(selector)!.has(identity)) {
|
|
77
|
+
candidates.push(snapshot);
|
|
78
|
+
sourceIndex.get(selector)!.push(tool);
|
|
79
|
+
identities.get(selector)!.add(identity);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { candidatesByName: index, sourceCandidatesByName: sourceIndex, identitiesByTool };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Compile one immutable view of a request's tool catalog for repeated policy checks. */
|
|
87
|
+
export function createToolChoiceResolver(tools: readonly ToolIdentity[] | undefined) {
|
|
88
|
+
const compiled = tools ? buildToolChoiceCatalog(tools) : undefined;
|
|
89
|
+
const candidatesByName = compiled?.candidatesByName;
|
|
90
|
+
const snapshotFor = (tool: ToolIdentity): ToolIdentity | undefined => {
|
|
91
|
+
const snapshot = compiled?.identitiesByTool.get(tool);
|
|
92
|
+
return snapshot && sameToolIdentity(snapshot, tool) ? snapshot : undefined;
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
candidates(name: string): ToolIdentity[] {
|
|
96
|
+
return (candidatesByName?.get(name) ?? []).map(candidate => ({ ...candidate }));
|
|
97
|
+
},
|
|
98
|
+
candidateCount(name: string): number {
|
|
99
|
+
return candidatesByName?.get(name)?.length ?? 0;
|
|
100
|
+
},
|
|
101
|
+
allows(tool: ToolIdentity, allowedTools: ReadonlySet<string>): boolean {
|
|
102
|
+
if (!candidatesByName) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
|
|
103
|
+
const snapshot = snapshotFor(tool);
|
|
104
|
+
return snapshot ? toolAllowedByChoiceFromIndex(snapshot, allowedTools, candidatesByName) : false;
|
|
105
|
+
},
|
|
106
|
+
selects(tool: ToolIdentity, name: string): boolean {
|
|
107
|
+
const snapshot = snapshotFor(tool);
|
|
108
|
+
const candidates = candidatesByName?.get(name);
|
|
109
|
+
return !!snapshot && candidates?.length === 1 && sameToolIdentity(candidates[0], snapshot);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
46
114
|
/**
|
|
47
115
|
* All tools that could be selected by one client-facing name. Bare logical names are included
|
|
48
116
|
* here because they are a compatibility selector for namespaced tools, while wire and dotted
|
|
@@ -53,12 +121,7 @@ export function toolChoiceCandidates(
|
|
|
53
121
|
name: string,
|
|
54
122
|
): Pick<OcxTool, "namespace" | "name">[] {
|
|
55
123
|
if (!tools) return [];
|
|
56
|
-
|
|
57
|
-
for (const tool of tools) {
|
|
58
|
-
if (tool.name !== name && !toolChoiceAliases(tool).includes(name)) continue;
|
|
59
|
-
if (!candidates.some(candidate => sameToolIdentity(candidate, tool))) candidates.push(tool);
|
|
60
|
-
}
|
|
61
|
-
return candidates;
|
|
124
|
+
return [...(buildToolChoiceCatalog(tools).sourceCandidatesByName.get(name) ?? [])];
|
|
62
125
|
}
|
|
63
126
|
|
|
64
127
|
/**
|
|
@@ -72,10 +135,22 @@ export function toolAllowedByChoice(
|
|
|
72
135
|
tools?: readonly Pick<OcxTool, "namespace" | "name">[],
|
|
73
136
|
): boolean {
|
|
74
137
|
if (!tools) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
|
|
138
|
+
return toolAllowedByChoiceFromIndex(
|
|
139
|
+
snapshotToolIdentity(tool),
|
|
140
|
+
allowedTools,
|
|
141
|
+
buildToolChoiceCatalog(tools).candidatesByName,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function toolAllowedByChoiceFromIndex(
|
|
146
|
+
tool: ToolIdentity,
|
|
147
|
+
allowedTools: ReadonlySet<string>,
|
|
148
|
+
candidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>,
|
|
149
|
+
): boolean {
|
|
75
150
|
for (const name of [...toolChoiceAliases(tool), tool.name]) {
|
|
76
151
|
if (!allowedTools.has(name)) continue;
|
|
77
|
-
const candidates =
|
|
78
|
-
if (candidates
|
|
152
|
+
const candidates = candidatesByName.get(name);
|
|
153
|
+
if (candidates?.length === 1 && sameToolIdentity(candidates[0], tool)) return true;
|
|
79
154
|
}
|
|
80
155
|
return false;
|
|
81
156
|
}
|
|
@@ -123,9 +198,10 @@ export function toolChoiceToolPredicate(
|
|
|
123
198
|
if (choice === "none") return () => false;
|
|
124
199
|
if (isAllowedToolChoice(choice)) {
|
|
125
200
|
const allowed = new Set(choice.allowedTools);
|
|
126
|
-
|
|
201
|
+
const resolver = createToolChoiceResolver(tools);
|
|
202
|
+
return tool => resolver.allows(tool, allowed);
|
|
127
203
|
}
|
|
128
204
|
if (!tools) return tool => toolChoiceAliases(tool).includes(choice.name);
|
|
129
|
-
const
|
|
130
|
-
return tool =>
|
|
205
|
+
const resolver = createToolChoiceResolver(tools);
|
|
206
|
+
return tool => resolver.selects(tool, choice.name);
|
|
131
207
|
}
|