@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
|
@@ -1,12 +1,63 @@
|
|
|
1
|
-
import { namespacedToolName } from "../types";
|
|
1
|
+
import { namespacedToolName, normalizeDeclaredToolName } from "../types";
|
|
2
2
|
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
3
3
|
|
|
4
|
+
/** Item types the client executes through a request-declared wire name. */
|
|
5
|
+
const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* Hosted declarations whose response items the PROVIDER executes, keyed by the request
|
|
9
|
+
* declaration type. These need no client answer, so their names are deliberately absent from
|
|
10
|
+
* the request catalog and must not be read as an undeclared client tool.
|
|
11
|
+
*
|
|
12
|
+
* xAI surfaces hosted `x_search` as `custom_tool_call`. Probed 2026-08-23 against the OAuth CLI
|
|
13
|
+
* destination: its hosted calls use an `xs_call-` call-id prefix. Observed call names were
|
|
14
|
+
* `x_keyword_search`, `x_semantic_search`, and `x_user_search` — three literals for one tool,
|
|
15
|
+
* which is why authorization keys on the declaration, item type, and call-id prefix, never on
|
|
16
|
+
* the name.
|
|
8
17
|
*/
|
|
9
|
-
|
|
18
|
+
export type ProviderExecutedCallType = Readonly<{
|
|
19
|
+
itemType: string;
|
|
20
|
+
callIdPrefix: string;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
type ProviderExecutedCallTypes = ReadonlySet<ProviderExecutedCallType>;
|
|
24
|
+
|
|
25
|
+
export const PROVIDER_EXECUTED_DECLARATION_CALL_TYPES = new Map<string, ProviderExecutedCallType>([
|
|
26
|
+
["x_search", { itemType: "custom_tool_call", callIdPrefix: "xs_call-" }],
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/** Nameless declaration kinds whose response items still require client execution. */
|
|
30
|
+
const NAMELESS_CLIENT_DECLARATION_CALL_TYPES = new Map([
|
|
31
|
+
["local_shell", "local_shell_call"],
|
|
32
|
+
["tool_search", "tool_search_call"],
|
|
33
|
+
["computer_use_preview", "computer_call"],
|
|
34
|
+
["computer_use", "computer_call"],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const NAMELESS_CLIENT_CALL_DISPLAY_NAMES = new Map([
|
|
38
|
+
["local_shell_call", "local_shell"],
|
|
39
|
+
["tool_search_call", "tool_search"],
|
|
40
|
+
["computer_call", "computer_use"],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES: ReadonlySet<string> = new Set();
|
|
44
|
+
const EMPTY_PROVIDER_EXECUTED_CALL_TYPES: ReadonlySet<ProviderExecutedCallType> = new Set();
|
|
45
|
+
|
|
46
|
+
/** Supported hosted/private declarations that carry no client-executable wire name. */
|
|
47
|
+
const NAMELESS_TOOL_SPEC_TYPES = new Set([
|
|
48
|
+
"web_search",
|
|
49
|
+
"web_search_preview",
|
|
50
|
+
"file_search",
|
|
51
|
+
"computer_use_preview",
|
|
52
|
+
"computer_use",
|
|
53
|
+
"code_interpreter",
|
|
54
|
+
"image_generation",
|
|
55
|
+
"image_gen",
|
|
56
|
+
"mcp",
|
|
57
|
+
"tool_search",
|
|
58
|
+
"local_shell",
|
|
59
|
+
"x_search",
|
|
60
|
+
]);
|
|
10
61
|
|
|
11
62
|
/** An upstream-supplied name reaches the error message; keep it bounded. */
|
|
12
63
|
const MAX_REPORTED_NAME_CHARS = 100;
|
|
@@ -18,12 +69,40 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
18
69
|
}
|
|
19
70
|
|
|
20
71
|
function addWireToolName(names: Set<string>, tool: unknown, namespace?: string): void {
|
|
21
|
-
if (!isPlainObject(tool)
|
|
22
|
-
|
|
72
|
+
if (!isPlainObject(tool)) return;
|
|
73
|
+
const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
|
|
74
|
+
? tool.function
|
|
75
|
+
: undefined;
|
|
76
|
+
const name = typeof tool.name === "string" && tool.name.length > 0
|
|
77
|
+
? tool.name
|
|
78
|
+
: typeof nestedFunction?.name === "string" && nestedFunction.name.length > 0
|
|
79
|
+
? nestedFunction.name
|
|
80
|
+
: undefined;
|
|
81
|
+
if (!name) return;
|
|
82
|
+
names.add(name);
|
|
23
83
|
// Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
|
|
24
84
|
// as a bare inner name or as the flattened form; accept both rather than guess which
|
|
25
85
|
// coordinate system this provider echoes back.
|
|
26
|
-
if (namespace) names.add(namespacedToolName(namespace,
|
|
86
|
+
if (namespace) names.add(namespacedToolName(namespace, name));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Catalog view owned by the current Responses turn.
|
|
91
|
+
*
|
|
92
|
+
* `previous_response_id` expansion prepends stored input items, including historical
|
|
93
|
+
* `additional_tools` declarations. Those items remain conversation history but cannot grant
|
|
94
|
+
* execution authority to this turn. Top-level `tools` always belongs to the current request;
|
|
95
|
+
* only input catalogs at or after the replay boundary are current.
|
|
96
|
+
*/
|
|
97
|
+
export function currentTurnWireToolCatalogBody(
|
|
98
|
+
body: unknown,
|
|
99
|
+
replayPrefixLength: number | undefined,
|
|
100
|
+
): unknown {
|
|
101
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
102
|
+
if (typeof replayPrefixLength !== "number" || !Number.isFinite(replayPrefixLength)) return body;
|
|
103
|
+
const start = Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)));
|
|
104
|
+
if (start === 0) return body;
|
|
105
|
+
return { ...body, input: body.input.slice(start) };
|
|
27
106
|
}
|
|
28
107
|
|
|
29
108
|
function addWireToolSpecs(names: Set<string>, specs: unknown): void {
|
|
@@ -53,21 +132,159 @@ export function collectDeclaredWireToolNames(body: unknown): Set<string> {
|
|
|
53
132
|
addWireToolSpecs(names, body.tools);
|
|
54
133
|
if (Array.isArray(body.input)) {
|
|
55
134
|
for (const item of body.input) {
|
|
56
|
-
if (
|
|
135
|
+
if (
|
|
136
|
+
isPlainObject(item)
|
|
137
|
+
&& (item.type === "additional_tools" || item.type === "tool_search_output")
|
|
138
|
+
) addWireToolSpecs(names, item.tools);
|
|
57
139
|
}
|
|
58
140
|
}
|
|
59
141
|
return names;
|
|
60
142
|
}
|
|
61
143
|
|
|
62
|
-
function
|
|
144
|
+
function addNamelessClientCallTypes(callTypes: Set<string>, specs: unknown): void {
|
|
145
|
+
if (!Array.isArray(specs)) return;
|
|
146
|
+
for (const spec of specs) {
|
|
147
|
+
if (!isPlainObject(spec) || typeof spec.type !== "string") continue;
|
|
148
|
+
const callType = NAMELESS_CLIENT_DECLARATION_CALL_TYPES.get(spec.type);
|
|
149
|
+
if (callType) callTypes.add(callType);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function addProviderExecutedCallTypes(
|
|
154
|
+
callTypes: Set<ProviderExecutedCallType>,
|
|
155
|
+
specs: unknown,
|
|
156
|
+
): void {
|
|
157
|
+
if (!Array.isArray(specs)) return;
|
|
158
|
+
for (const spec of specs) {
|
|
159
|
+
if (!isPlainObject(spec) || typeof spec.type !== "string") continue;
|
|
160
|
+
const callType = PROVIDER_EXECUTED_DECLARATION_CALL_TYPES.get(spec.type);
|
|
161
|
+
if (callType) callTypes.add(callType);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Item types this turn's hosted declarations authorize the PROVIDER to emit unnamed.
|
|
167
|
+
*
|
|
168
|
+
* Caller must gate this on the destination actually being that provider; a declaration alone
|
|
169
|
+
* is not authority, or any upstream could claim a hosted shape it never serves.
|
|
170
|
+
*/
|
|
171
|
+
export function collectProviderExecutedCallTypes(body: unknown): Set<ProviderExecutedCallType> {
|
|
172
|
+
const callTypes = new Set<ProviderExecutedCallType>();
|
|
173
|
+
if (!isPlainObject(body)) return callTypes;
|
|
174
|
+
addProviderExecutedCallTypes(callTypes, body.tools);
|
|
175
|
+
if (Array.isArray(body.input)) {
|
|
176
|
+
for (const item of body.input) {
|
|
177
|
+
if (
|
|
178
|
+
isPlainObject(item)
|
|
179
|
+
&& (item.type === "additional_tools" || item.type === "tool_search_output")
|
|
180
|
+
) addProviderExecutedCallTypes(callTypes, item.tools);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return callTypes;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isAuthorizedProviderExecutedCall(
|
|
187
|
+
item: Record<string, unknown>,
|
|
188
|
+
callTypes: ProviderExecutedCallTypes,
|
|
189
|
+
): boolean {
|
|
190
|
+
if (typeof item.call_id !== "string") return false;
|
|
191
|
+
for (const callType of callTypes) {
|
|
192
|
+
if (
|
|
193
|
+
item.type === callType.itemType
|
|
194
|
+
&& item.call_id.startsWith(callType.callIdPrefix)
|
|
195
|
+
) return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Nameless client-call item types authorized by supported request tool declarations. */
|
|
201
|
+
export function collectDeclaredNamelessClientCallTypes(body: unknown): Set<string> {
|
|
202
|
+
const callTypes = new Set<string>();
|
|
203
|
+
if (!isPlainObject(body)) return callTypes;
|
|
204
|
+
addNamelessClientCallTypes(callTypes, body.tools);
|
|
205
|
+
if (Array.isArray(body.input)) {
|
|
206
|
+
for (const item of body.input) {
|
|
207
|
+
if (
|
|
208
|
+
isPlainObject(item)
|
|
209
|
+
&& (item.type === "additional_tools" || item.type === "tool_search_output")
|
|
210
|
+
) {
|
|
211
|
+
addNamelessClientCallTypes(callTypes, item.tools);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return callTypes;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isReadableWireToolSpec(spec: unknown): boolean {
|
|
219
|
+
if (!isPlainObject(spec) || typeof spec.type !== "string" || spec.type.length === 0) return false;
|
|
220
|
+
if (spec.type === "function") {
|
|
221
|
+
return (typeof spec.name === "string" && spec.name.length > 0)
|
|
222
|
+
|| (isPlainObject(spec.function)
|
|
223
|
+
&& typeof spec.function.name === "string"
|
|
224
|
+
&& spec.function.name.length > 0);
|
|
225
|
+
}
|
|
226
|
+
if (spec.type === "custom") return typeof spec.name === "string" && spec.name.length > 0;
|
|
227
|
+
if (spec.type === "namespace") {
|
|
228
|
+
return typeof spec.name === "string"
|
|
229
|
+
&& spec.name.length > 0
|
|
230
|
+
&& Array.isArray(spec.tools)
|
|
231
|
+
&& (spec.tools.length === 0 || spec.tools.some(inner =>
|
|
232
|
+
isPlainObject(inner)
|
|
233
|
+
&& (inner.type === "function" || inner.type === "custom")
|
|
234
|
+
&& typeof inner.name === "string"
|
|
235
|
+
&& inner.name.length > 0
|
|
236
|
+
));
|
|
237
|
+
}
|
|
238
|
+
if (NAMELESS_TOOL_SPEC_TYPES.has(spec.type)) return true;
|
|
239
|
+
return typeof spec.name === "string" && spec.name.length > 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isReadableWireToolCatalog(value: unknown): boolean {
|
|
243
|
+
return Array.isArray(value)
|
|
244
|
+
&& (value.length === 0 || value.some(isReadableWireToolSpec));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Whether a request contains a supported catalog, including an explicit empty deny-all array. */
|
|
248
|
+
export function hasExplicitWireToolCatalog(body: unknown): boolean {
|
|
249
|
+
if (!isPlainObject(body)) return false;
|
|
250
|
+
if (isReadableWireToolCatalog(body.tools)) return true;
|
|
251
|
+
if (!Array.isArray(body.input)) return false;
|
|
252
|
+
return body.input.some(item =>
|
|
253
|
+
isPlainObject(item)
|
|
254
|
+
&& item.type === "additional_tools"
|
|
255
|
+
&& isReadableWireToolCatalog(item.tools)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function undeclaredNameInItem(
|
|
260
|
+
item: unknown,
|
|
261
|
+
declared: ReadonlySet<string>,
|
|
262
|
+
declaredNamelessClientCallTypes: ReadonlySet<string>,
|
|
263
|
+
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
|
|
264
|
+
): string | undefined {
|
|
63
265
|
if (!isPlainObject(item)) return undefined;
|
|
64
|
-
if (typeof item.type !== "string"
|
|
266
|
+
if (typeof item.type !== "string") return undefined;
|
|
267
|
+
// The provider executes this exact measured shape itself, so there is no client name to
|
|
268
|
+
// authorize. The caller supplies these signatures only for the matching destination and
|
|
269
|
+
// declarations; the item must additionally carry the hosted call-id prefix.
|
|
270
|
+
if (isAuthorizedProviderExecutedCall(item, providerExecutedCallTypes)) return undefined;
|
|
271
|
+
const namelessDisplayName = NAMELESS_CLIENT_CALL_DISPLAY_NAMES.get(item.type);
|
|
272
|
+
if (namelessDisplayName !== undefined) {
|
|
273
|
+
// Only Codex's explicit `execution: "client"` form delegates tool search to the client.
|
|
274
|
+
if (item.type === "tool_search_call" && item.execution !== "client") return undefined;
|
|
275
|
+
return declaredNamelessClientCallTypes.has(item.type) ? undefined : namelessDisplayName;
|
|
276
|
+
}
|
|
277
|
+
if (!CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
|
|
65
278
|
const name = item.name;
|
|
66
279
|
if (typeof name !== "string" || name.length === 0) return undefined;
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
280
|
+
if (typeof item.namespace === "string") {
|
|
281
|
+
// Namespaced calls are matched by their full wire name only — never legacy-normalize
|
|
282
|
+
// them, or an undeclared namespaced `exec_command` could slip through as bare `exec`.
|
|
283
|
+
if (declared.has(namespacedToolName(item.namespace, name))) return undefined;
|
|
284
|
+
return name;
|
|
70
285
|
}
|
|
286
|
+
const effectiveName = normalizeDeclaredToolName(name, declared);
|
|
287
|
+
if (declared.has(effectiveName)) return undefined;
|
|
71
288
|
return name;
|
|
72
289
|
}
|
|
73
290
|
|
|
@@ -75,14 +292,16 @@ function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): str
|
|
|
75
292
|
export function undeclaredToolCallName(
|
|
76
293
|
payload: unknown,
|
|
77
294
|
declared: ReadonlySet<string>,
|
|
295
|
+
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
|
|
296
|
+
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
|
|
78
297
|
): string | undefined {
|
|
79
298
|
if (!isPlainObject(payload)) return undefined;
|
|
80
299
|
if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
|
|
81
|
-
return undeclaredNameInItem(payload.item, declared);
|
|
300
|
+
return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
|
|
82
301
|
}
|
|
83
302
|
// Sparse gateways skip incremental items and only ever ship the terminal snapshot.
|
|
84
303
|
if (payload.type === "response.completed" || payload.type === "response.incomplete") {
|
|
85
|
-
return undeclaredToolCallNameInResponse(payload.response, declared);
|
|
304
|
+
return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
|
|
86
305
|
}
|
|
87
306
|
return undefined;
|
|
88
307
|
}
|
|
@@ -91,10 +310,12 @@ export function undeclaredToolCallName(
|
|
|
91
310
|
export function undeclaredToolCallNameInResponse(
|
|
92
311
|
response: unknown,
|
|
93
312
|
declared: ReadonlySet<string>,
|
|
313
|
+
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
|
|
314
|
+
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
|
|
94
315
|
): string | undefined {
|
|
95
316
|
if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
|
|
96
317
|
for (const item of response.output) {
|
|
97
|
-
const name = undeclaredNameInItem(item, declared);
|
|
318
|
+
const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
|
|
98
319
|
if (name !== undefined) return name;
|
|
99
320
|
}
|
|
100
321
|
return undefined;
|
|
@@ -133,6 +354,8 @@ function failedBlocks(name: string, newline: string): readonly string[] {
|
|
|
133
354
|
*/
|
|
134
355
|
export function createUndeclaredToolCallGuardBlockRewrite(
|
|
135
356
|
declared: ReadonlySet<string>,
|
|
357
|
+
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
|
|
358
|
+
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
|
|
136
359
|
): SseBlockRewrite {
|
|
137
360
|
let tripped = false;
|
|
138
361
|
return (block: string) => {
|
|
@@ -145,7 +368,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
|
|
|
145
368
|
} catch {
|
|
146
369
|
return [block];
|
|
147
370
|
}
|
|
148
|
-
const name = undeclaredToolCallName(parsed, declared);
|
|
371
|
+
const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
|
|
149
372
|
if (name === undefined) return [block];
|
|
150
373
|
tripped = true;
|
|
151
374
|
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
|
|
@@ -551,7 +555,7 @@ export function installedServiceListenPort(): number {
|
|
|
551
555
|
?? resolveServiceListenPort();
|
|
552
556
|
}
|
|
553
557
|
|
|
554
|
-
export const SERVICE_INSTALL_HEALTH_MS =
|
|
558
|
+
export const SERVICE_INSTALL_HEALTH_MS = Number(process.env.OCX_SERVICE_HEALTH_TIMEOUT_MS) || 30_000;
|
|
555
559
|
|
|
556
560
|
/**
|
|
557
561
|
* Whether a proxy actually answers on the port this install/start just produced.
|
package/src/types/config.ts
CHANGED
|
@@ -259,6 +259,19 @@ export interface OcxConfig {
|
|
|
259
259
|
port: number;
|
|
260
260
|
/** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
|
|
261
261
|
emptyCompletionRetry?: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Whether a login may open a browser on the machine running the proxy.
|
|
264
|
+
*
|
|
265
|
+
* Absent and `true` both mean "open", which is what every existing install
|
|
266
|
+
* already does. Only an explicit `false` declines — for an operator who wants
|
|
267
|
+
* to paste the authorization URL into a different browser profile, or who is
|
|
268
|
+
* driving the dashboard from a different machine than the proxy.
|
|
269
|
+
*
|
|
270
|
+
* Deliberately a boolean and not an "auto" mode: inferring headlessness from
|
|
271
|
+
* SSH_CONNECTION or a missing DISPLAY breaks a working login silently when
|
|
272
|
+
* the guess is wrong.
|
|
273
|
+
*/
|
|
274
|
+
oauthOpenBrowser?: boolean;
|
|
262
275
|
/** Maximum usage-log bytes read for one management snapshot. */
|
|
263
276
|
managementUsageMaxReadBytes?: number;
|
|
264
277
|
providers: Record<string, OcxProviderConfig>;
|
|
@@ -455,10 +468,12 @@ export interface OcxConfig {
|
|
|
455
468
|
* Routed parents get v2 tools; Sol/Terra can still spawn Grok/Claude (issue #92).
|
|
456
469
|
*/
|
|
457
470
|
keepNativeChatGptOnV1?: boolean;
|
|
471
|
+
/** Experimental routed target for eligible native V2 root parents. */
|
|
472
|
+
v2NativeParentOverride?: { enabled?: boolean; model?: string };
|
|
458
473
|
/** Experimental, default-off ChatGPT recovery for encrypted V2 routed tasks. */
|
|
459
474
|
agentTaskRecovery?: {
|
|
460
475
|
enabled?: boolean;
|
|
461
|
-
/** ChatGPT model used by the recovery request. Default: gpt-5.6-
|
|
476
|
+
/** ChatGPT model used by the recovery request. Default: gpt-5.6-luna. */
|
|
462
477
|
model?: string;
|
|
463
478
|
/** Recovery request timeout in milliseconds. Default: 45000. */
|
|
464
479
|
timeoutMs?: number;
|
package/src/types/provider.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode }
|
|
|
8
8
|
*/
|
|
9
9
|
export type RefreshPolicy = "proactive" | "lazy-only" | "disabled";
|
|
10
10
|
|
|
11
|
+
export type ProviderTlsProfile = "antigravity-browser";
|
|
12
|
+
|
|
11
13
|
export interface OpenRouterProviderRouting {
|
|
12
14
|
/** OpenRouter provider slugs to try first, in priority order. */
|
|
13
15
|
order?: string[];
|
|
@@ -68,6 +70,8 @@ export interface RequestPacingRule {
|
|
|
68
70
|
requestsPerMinute?: number;
|
|
69
71
|
/** Minimum delay between request starts. The slower configured value wins. */
|
|
70
72
|
minIntervalMs?: number;
|
|
73
|
+
/** Positive-only random delay added to each request-start slot. */
|
|
74
|
+
jitterMs?: number;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
export interface ProviderRequestPacingConfig extends RequestPacingRule {
|
|
@@ -136,6 +140,8 @@ export interface OcxProviderConfig {
|
|
|
136
140
|
codexToolMode?: "code_mode_only" | "shell";
|
|
137
141
|
/** Optional outbound request-start pacing shared by this provider and its model overrides. */
|
|
138
142
|
requestPacing?: ProviderRequestPacingConfig;
|
|
143
|
+
/** Explicitly acknowledged experimental browser-compatible transport profile. */
|
|
144
|
+
tlsProfile?: ProviderTlsProfile;
|
|
139
145
|
/** Cursor MCP compatibility bounds; positive integers when configured. */
|
|
140
146
|
mcpMaxTools?: number;
|
|
141
147
|
mcpMaxSchemaBytes?: number;
|
|
@@ -245,6 +251,11 @@ export interface OcxProviderConfig {
|
|
|
245
251
|
*/
|
|
246
252
|
codexAccountMode?: CodexAccountMode;
|
|
247
253
|
apiKey?: string;
|
|
254
|
+
/** Azure OpenAI identity authentication; mutually exclusive with API-key fields. */
|
|
255
|
+
azureCredential?: {
|
|
256
|
+
type: "default-azure-credential";
|
|
257
|
+
managedIdentityClientId?: string;
|
|
258
|
+
};
|
|
248
259
|
/**
|
|
249
260
|
* Key-auth header style for Anthropic-compatible providers.
|
|
250
261
|
* Defaults to the native Anthropic `x-api-key`; gateways may require
|
|
@@ -281,6 +292,11 @@ export interface OcxProviderConfig {
|
|
|
281
292
|
modelInputModalities?: Record<string, string[]>;
|
|
282
293
|
/** Model-specific max input token limits. Values cap auto_compact_token_limit. */
|
|
283
294
|
modelMaxInputTokens?: Record<string, number>;
|
|
295
|
+
/**
|
|
296
|
+
* Per-model soft compaction budgets. Values may only lower the effective
|
|
297
|
+
* context/max-input envelope; they never raise hard admission limits.
|
|
298
|
+
*/
|
|
299
|
+
modelAutoCompactTokenLimits?: Record<string, number>;
|
|
284
300
|
/**
|
|
285
301
|
* Provider-wide fallback for chat-completions `max_tokens` when the caller omits
|
|
286
302
|
* Responses `max_output_tokens`. Adapters still let an explicit request win.
|
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,9 @@ 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
|
+
/** True when promptCacheKey is a shared cache cohort rather than a conversation identity. */
|
|
72
|
+
_promptCacheKeyIsSharedCohort?: boolean;
|
|
73
|
+
/** Conversation/provider/account/model-bound namespace for reasoning replay state. */
|
|
67
74
|
_reasoningReplayScope?: OcxReasoningReplayScopeRef;
|
|
68
75
|
/**
|
|
69
76
|
* Set by bindRouteReasoningReplayScope after a proven serving-identity change, or by
|
|
@@ -266,6 +273,32 @@ export interface OcxRequestOptions {
|
|
|
266
273
|
schema?: Record<string, unknown>;
|
|
267
274
|
strict?: boolean;
|
|
268
275
|
};
|
|
276
|
+
providerOptions?: { google?: GoogleProviderOptions };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export type GoogleSafetyCategory =
|
|
280
|
+
| "HARM_CATEGORY_HATE_SPEECH"
|
|
281
|
+
| "HARM_CATEGORY_SEXUALLY_EXPLICIT"
|
|
282
|
+
| "HARM_CATEGORY_DANGEROUS_CONTENT"
|
|
283
|
+
| "HARM_CATEGORY_HARASSMENT"
|
|
284
|
+
| "HARM_CATEGORY_CIVIC_INTEGRITY"
|
|
285
|
+
| "HARM_CATEGORY_JAILBREAK";
|
|
286
|
+
export type GoogleSafetyThreshold =
|
|
287
|
+
| "HARM_BLOCK_THRESHOLD_UNSPECIFIED"
|
|
288
|
+
| "BLOCK_LOW_AND_ABOVE"
|
|
289
|
+
| "BLOCK_MEDIUM_AND_ABOVE"
|
|
290
|
+
| "BLOCK_ONLY_HIGH"
|
|
291
|
+
| "BLOCK_NONE"
|
|
292
|
+
| "OFF";
|
|
293
|
+
export interface GoogleSafetySetting {
|
|
294
|
+
category: GoogleSafetyCategory;
|
|
295
|
+
threshold: GoogleSafetyThreshold;
|
|
296
|
+
}
|
|
297
|
+
export interface GoogleProviderOptions {
|
|
298
|
+
thinkingBudget?: number;
|
|
299
|
+
includeThoughts?: boolean;
|
|
300
|
+
safetySettings?: GoogleSafetySetting[];
|
|
301
|
+
cachedContent?: string;
|
|
269
302
|
}
|
|
270
303
|
|
|
271
304
|
export type OcxMessagePhase = "commentary" | "final_answer";
|
package/src/types/tools.ts
CHANGED
|
@@ -31,6 +31,33 @@ export function namespacedToolName(namespace: string | undefined, name: string):
|
|
|
31
31
|
return namespace ? `${namespace}__${name}` : name;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Codex 0.149 unified-exec name normalization.
|
|
36
|
+
*
|
|
37
|
+
* Codex's code-mode shell tool is declared as `exec` (a freeform custom tool whose own
|
|
38
|
+
* description mentions the nested `await tools.exec_command(...)` helper). Routed models —
|
|
39
|
+
* DeepSeek in particular — sometimes echo that helper name as the tool-call name, emitting
|
|
40
|
+
* `exec_command` instead of the declared `exec`. Accept the legacy shell bridge names only
|
|
41
|
+
* when the request catalog actually declares `exec` and does not itself declare the legacy
|
|
42
|
+
* name (an MCP server may legitimately advertise `exec_command` under its own namespace).
|
|
43
|
+
*/
|
|
44
|
+
const LEGACY_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
|
|
45
|
+
|
|
46
|
+
export function normalizeDeclaredToolName(
|
|
47
|
+
name: string,
|
|
48
|
+
declared: ReadonlySet<string> | undefined,
|
|
49
|
+
): string {
|
|
50
|
+
if (!declared || !declared.has("exec")) return name;
|
|
51
|
+
if (declared.has(name)) return name;
|
|
52
|
+
// When the catalog explicitly declares any legacy shell bridge name, the environment
|
|
53
|
+
// genuinely exposes that tool — turn normalization off so a call is never mis-routed
|
|
54
|
+
// to `exec`.
|
|
55
|
+
if ((LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).some(legacy => declared.has(legacy))) {
|
|
56
|
+
return name;
|
|
57
|
+
}
|
|
58
|
+
return (LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(name) ? "exec" : name;
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
export function toolChoiceAliases(tool: Pick<OcxTool, "namespace" | "name">): string[] {
|
|
35
62
|
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
36
63
|
return tool.namespace ? [wireName, `${tool.namespace}.${tool.name}`] : [wireName];
|
|
@@ -43,6 +70,74 @@ function sameToolIdentity(
|
|
|
43
70
|
return left.namespace === right.namespace && left.name === right.name;
|
|
44
71
|
}
|
|
45
72
|
|
|
73
|
+
type ToolIdentity = Readonly<Pick<OcxTool, "namespace" | "name">>;
|
|
74
|
+
|
|
75
|
+
function snapshotToolIdentity(tool: Pick<OcxTool, "namespace" | "name">): ToolIdentity {
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
name: tool.name,
|
|
78
|
+
...(tool.namespace === undefined ? {} : { namespace: tool.namespace }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildToolChoiceCatalog(
|
|
83
|
+
tools: readonly ToolIdentity[],
|
|
84
|
+
): {
|
|
85
|
+
candidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>;
|
|
86
|
+
sourceCandidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>;
|
|
87
|
+
identitiesByTool: WeakMap<object, ToolIdentity>;
|
|
88
|
+
} {
|
|
89
|
+
const index = new Map<string, ToolIdentity[]>();
|
|
90
|
+
const sourceIndex = new Map<string, ToolIdentity[]>();
|
|
91
|
+
const identities = new Map<string, Set<string>>();
|
|
92
|
+
const identitiesByTool = new WeakMap<object, ToolIdentity>();
|
|
93
|
+
for (const tool of tools) {
|
|
94
|
+
const snapshot = snapshotToolIdentity(tool);
|
|
95
|
+
identitiesByTool.set(tool, snapshot);
|
|
96
|
+
const identity = JSON.stringify([snapshot.namespace ?? null, snapshot.name]);
|
|
97
|
+
for (const selector of [...toolChoiceAliases(snapshot), snapshot.name]) {
|
|
98
|
+
const candidates = index.get(selector);
|
|
99
|
+
if (!candidates) {
|
|
100
|
+
index.set(selector, [snapshot]);
|
|
101
|
+
sourceIndex.set(selector, [tool]);
|
|
102
|
+
identities.set(selector, new Set([identity]));
|
|
103
|
+
} else if (!identities.get(selector)!.has(identity)) {
|
|
104
|
+
candidates.push(snapshot);
|
|
105
|
+
sourceIndex.get(selector)!.push(tool);
|
|
106
|
+
identities.get(selector)!.add(identity);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { candidatesByName: index, sourceCandidatesByName: sourceIndex, identitiesByTool };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Compile one immutable view of a request's tool catalog for repeated policy checks. */
|
|
114
|
+
export function createToolChoiceResolver(tools: readonly ToolIdentity[] | undefined) {
|
|
115
|
+
const compiled = tools ? buildToolChoiceCatalog(tools) : undefined;
|
|
116
|
+
const candidatesByName = compiled?.candidatesByName;
|
|
117
|
+
const snapshotFor = (tool: ToolIdentity): ToolIdentity | undefined => {
|
|
118
|
+
const snapshot = compiled?.identitiesByTool.get(tool);
|
|
119
|
+
return snapshot && sameToolIdentity(snapshot, tool) ? snapshot : undefined;
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
candidates(name: string): ToolIdentity[] {
|
|
123
|
+
return (candidatesByName?.get(name) ?? []).map(candidate => ({ ...candidate }));
|
|
124
|
+
},
|
|
125
|
+
candidateCount(name: string): number {
|
|
126
|
+
return candidatesByName?.get(name)?.length ?? 0;
|
|
127
|
+
},
|
|
128
|
+
allows(tool: ToolIdentity, allowedTools: ReadonlySet<string>): boolean {
|
|
129
|
+
if (!candidatesByName) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
|
|
130
|
+
const snapshot = snapshotFor(tool);
|
|
131
|
+
return snapshot ? toolAllowedByChoiceFromIndex(snapshot, allowedTools, candidatesByName) : false;
|
|
132
|
+
},
|
|
133
|
+
selects(tool: ToolIdentity, name: string): boolean {
|
|
134
|
+
const snapshot = snapshotFor(tool);
|
|
135
|
+
const candidates = candidatesByName?.get(name);
|
|
136
|
+
return !!snapshot && candidates?.length === 1 && sameToolIdentity(candidates[0], snapshot);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
46
141
|
/**
|
|
47
142
|
* All tools that could be selected by one client-facing name. Bare logical names are included
|
|
48
143
|
* here because they are a compatibility selector for namespaced tools, while wire and dotted
|
|
@@ -53,12 +148,7 @@ export function toolChoiceCandidates(
|
|
|
53
148
|
name: string,
|
|
54
149
|
): Pick<OcxTool, "namespace" | "name">[] {
|
|
55
150
|
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;
|
|
151
|
+
return [...(buildToolChoiceCatalog(tools).sourceCandidatesByName.get(name) ?? [])];
|
|
62
152
|
}
|
|
63
153
|
|
|
64
154
|
/**
|
|
@@ -72,10 +162,22 @@ export function toolAllowedByChoice(
|
|
|
72
162
|
tools?: readonly Pick<OcxTool, "namespace" | "name">[],
|
|
73
163
|
): boolean {
|
|
74
164
|
if (!tools) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
|
|
165
|
+
return toolAllowedByChoiceFromIndex(
|
|
166
|
+
snapshotToolIdentity(tool),
|
|
167
|
+
allowedTools,
|
|
168
|
+
buildToolChoiceCatalog(tools).candidatesByName,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toolAllowedByChoiceFromIndex(
|
|
173
|
+
tool: ToolIdentity,
|
|
174
|
+
allowedTools: ReadonlySet<string>,
|
|
175
|
+
candidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>,
|
|
176
|
+
): boolean {
|
|
75
177
|
for (const name of [...toolChoiceAliases(tool), tool.name]) {
|
|
76
178
|
if (!allowedTools.has(name)) continue;
|
|
77
|
-
const candidates =
|
|
78
|
-
if (candidates
|
|
179
|
+
const candidates = candidatesByName.get(name);
|
|
180
|
+
if (candidates?.length === 1 && sameToolIdentity(candidates[0], tool)) return true;
|
|
79
181
|
}
|
|
80
182
|
return false;
|
|
81
183
|
}
|
|
@@ -123,9 +225,10 @@ export function toolChoiceToolPredicate(
|
|
|
123
225
|
if (choice === "none") return () => false;
|
|
124
226
|
if (isAllowedToolChoice(choice)) {
|
|
125
227
|
const allowed = new Set(choice.allowedTools);
|
|
126
|
-
|
|
228
|
+
const resolver = createToolChoiceResolver(tools);
|
|
229
|
+
return tool => resolver.allows(tool, allowed);
|
|
127
230
|
}
|
|
128
231
|
if (!tools) return tool => toolChoiceAliases(tool).includes(choice.name);
|
|
129
|
-
const
|
|
130
|
-
return tool =>
|
|
232
|
+
const resolver = createToolChoiceResolver(tools);
|
|
233
|
+
return tool => resolver.selects(tool, choice.name);
|
|
131
234
|
}
|