@bitkyc08/opencodex 2.58.0 → 2.60.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 +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local memo pairing a bridged hosted `web_search` cell with the destination's own
|
|
3
|
+
* call and the result the proxy executed for it (issue #4587).
|
|
4
|
+
*
|
|
5
|
+
* The web-search passthrough bridge (`src/web-search/passthrough-bridge.ts`) intercepts the
|
|
6
|
+
* destination's `function_call` named `web_search`, runs the search itself, and shows the
|
|
7
|
+
* caller a hosted `web_search_call` cell under a proxy-minted `ws_<uuid>` id. The caller
|
|
8
|
+
* stores that cell and replays it on every later turn. The destination, which never produced a
|
|
9
|
+
* `web_search_call` in its life, then sees an unknown item type carrying a query and sources
|
|
10
|
+
* but no result — so it usually just searches again.
|
|
11
|
+
*
|
|
12
|
+
* This memo is what lets the pre-dispatch rewrite in the Responses adapter put the destination's
|
|
13
|
+
* own `function_call` and `function_call_output` back in that item's place. It records exactly
|
|
14
|
+
* what `appendBridgeSearchTurn` would have written onto a continuation leg, so a replayed turn
|
|
15
|
+
* and a continued turn show the destination the same conversation.
|
|
16
|
+
*
|
|
17
|
+
* Scope. Entries are keyed by the upstream destination in addition to the cell id. The cell id is
|
|
18
|
+
* a v4 UUID minted here, so it cannot collide across conversations, but an unscoped key would let
|
|
19
|
+
* a history replayed against a DIFFERENT provider resurrect a call that provider never made.
|
|
20
|
+
*
|
|
21
|
+
* Bounds and privacy. Result text is web content the caller already received, but it is still
|
|
22
|
+
* request-derived data: it lives in memory only, is never logged, serialized, or exported, and is
|
|
23
|
+
* bounded by entry count, total bytes, and TTL so a long-lived proxy cannot grow without limit.
|
|
24
|
+
*
|
|
25
|
+
* A miss is deliberately indistinguishable from "no entry": the caller leaves the replayed item
|
|
26
|
+
* alone. Neither re-running the search nor inventing a result is an acceptable recovery.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { reasoningReplayDestinationIdentity } from "./reasoning-replay-cache";
|
|
30
|
+
|
|
31
|
+
const MAX_ENTRIES = 64;
|
|
32
|
+
const MAX_TOTAL_BYTES = 512 * 1024;
|
|
33
|
+
const TTL_MS = 60 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
export interface BridgeSearchReplayEntry {
|
|
36
|
+
/** The destination's own call id, as it appeared on the intercepted item. */
|
|
37
|
+
callId: string;
|
|
38
|
+
/** The destination's own item id, replayed when the upstream supplied one. */
|
|
39
|
+
sourceItemId?: string;
|
|
40
|
+
/** The tool name the destination called, recorded rather than assumed. */
|
|
41
|
+
name: string;
|
|
42
|
+
/** The intercepted call's complete arguments text. */
|
|
43
|
+
argumentsText: string;
|
|
44
|
+
/** The tool result the bridge produced for that call. */
|
|
45
|
+
output: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface StoredEntry {
|
|
49
|
+
entry: BridgeSearchReplayEntry;
|
|
50
|
+
bytes: number;
|
|
51
|
+
at: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const entries = new Map<string, StoredEntry>();
|
|
55
|
+
let totalBytes = 0;
|
|
56
|
+
let clockForTests: (() => number) | null = null;
|
|
57
|
+
|
|
58
|
+
const now = (): number => clockForTests?.() ?? Date.now();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Identify the upstream destination a bridged search belongs to.
|
|
62
|
+
*
|
|
63
|
+
* Reuses the salted process-local destination digest the reasoning replay cache already defines,
|
|
64
|
+
* so both stores agree on what "the same upstream" means and neither invents a second notion of
|
|
65
|
+
* destination identity.
|
|
66
|
+
*/
|
|
67
|
+
export function bridgeSearchReplayScope(baseUrl: string | undefined): string | undefined {
|
|
68
|
+
return reasoningReplayDestinationIdentity(baseUrl);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function keyFor(scope: string, cellItemId: string): string {
|
|
72
|
+
return scope + "\u0000" + cellItemId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function drop(key: string, stored: StoredEntry): void {
|
|
76
|
+
entries.delete(key);
|
|
77
|
+
totalBytes -= stored.bytes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sweep(at: number): void {
|
|
81
|
+
for (const [key, stored] of entries) {
|
|
82
|
+
if (at - stored.at >= TTL_MS) drop(key, stored);
|
|
83
|
+
}
|
|
84
|
+
// Map iteration is insertion-ordered, so the oldest surviving entry is always the first one.
|
|
85
|
+
while (entries.size > MAX_ENTRIES || totalBytes > MAX_TOTAL_BYTES) {
|
|
86
|
+
const oldest = entries.entries().next();
|
|
87
|
+
if (oldest.done) {
|
|
88
|
+
totalBytes = 0;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
drop(oldest.value[0], oldest.value[1]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Record one executed bridged search.
|
|
97
|
+
*
|
|
98
|
+
* An entry with no call id is not recorded: the restore would have to emit a `function_call`
|
|
99
|
+
* without one, which is not a valid item and could not be paired with its output anyway.
|
|
100
|
+
*/
|
|
101
|
+
export function rememberBridgeSearchReplay(
|
|
102
|
+
scope: string | undefined,
|
|
103
|
+
cellItemId: string,
|
|
104
|
+
entry: BridgeSearchReplayEntry,
|
|
105
|
+
): void {
|
|
106
|
+
if (!scope || cellItemId.length === 0 || entry.callId.length === 0) return;
|
|
107
|
+
const key = keyFor(scope, cellItemId);
|
|
108
|
+
const existing = entries.get(key);
|
|
109
|
+
if (existing) drop(key, existing);
|
|
110
|
+
const bytes = 2 * (
|
|
111
|
+
key.length
|
|
112
|
+
+ entry.callId.length
|
|
113
|
+
+ (entry.sourceItemId?.length ?? 0)
|
|
114
|
+
+ entry.name.length
|
|
115
|
+
+ entry.argumentsText.length
|
|
116
|
+
+ entry.output.length
|
|
117
|
+
);
|
|
118
|
+
// A single oversized result is refused outright rather than evicting the whole store for it.
|
|
119
|
+
if (bytes > MAX_TOTAL_BYTES) return;
|
|
120
|
+
entries.set(key, { entry: { ...entry }, bytes, at: now() });
|
|
121
|
+
totalBytes += bytes;
|
|
122
|
+
sweep(now());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Look up one recorded search without consuming it.
|
|
127
|
+
*
|
|
128
|
+
* The same cell is replayed on every subsequent turn of the conversation, so a consuming read
|
|
129
|
+
* would restore the pair once and then silently stop. Expiry stays absolute: a conversation that
|
|
130
|
+
* outlives the TTL degrades to today's behaviour (the hosted cell replays unchanged) rather than
|
|
131
|
+
* pinning entries open for as long as anyone keeps talking.
|
|
132
|
+
*/
|
|
133
|
+
export function peekBridgeSearchReplay(
|
|
134
|
+
scope: string | undefined,
|
|
135
|
+
cellItemId: string,
|
|
136
|
+
): BridgeSearchReplayEntry | undefined {
|
|
137
|
+
if (!scope || cellItemId.length === 0) return undefined;
|
|
138
|
+
const key = keyFor(scope, cellItemId);
|
|
139
|
+
const stored = entries.get(key);
|
|
140
|
+
if (!stored) return undefined;
|
|
141
|
+
if (now() - stored.at >= TTL_MS) {
|
|
142
|
+
drop(key, stored);
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return stored.entry;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function clearBridgeSearchReplayCacheForTests(clock?: (() => number) | null): void {
|
|
149
|
+
entries.clear();
|
|
150
|
+
totalBytes = 0;
|
|
151
|
+
clockForTests = clock ?? null;
|
|
152
|
+
}
|
|
@@ -9,33 +9,43 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
9
9
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function unwrapPatchInput(value: string): string {
|
|
13
|
-
try {
|
|
14
|
-
const parsed: unknown = JSON.parse(value);
|
|
15
|
-
if (isPlainObject(parsed)) {
|
|
16
|
-
if (typeof parsed.input === "string") return parsed.input;
|
|
17
|
-
if (typeof parsed.patch === "string") return parsed.patch;
|
|
18
|
-
}
|
|
19
|
-
} catch {
|
|
20
|
-
// Native custom calls carry the patch body directly.
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
12
|
/**
|
|
26
13
|
* Convert a nested Code Mode helper call into unified-exec JavaScript.
|
|
27
14
|
*
|
|
28
15
|
* Parsed values are serialized as data, never interpolated as source, so command and patch text
|
|
29
16
|
* cannot escape the generated call. Invalid structured helper payloads are also passed as data so
|
|
30
17
|
* nested-tool validation can reject them without evaluating provider text as JavaScript.
|
|
18
|
+
*
|
|
19
|
+
* `wireToolName` is the name the body actually arrived under, which is not always the helper:
|
|
20
|
+
* a provider that got the NAME right and the BODY wrong sends `exec`, and the apply-patch
|
|
21
|
+
* helper is inferred from the payload. Recognition and compilation must read ONE canonical
|
|
22
|
+
* body, so both unwrap under that same name — see the apply-patch branch below. It defaults to
|
|
23
|
+
* the helper name, which is correct for the name-based path where the wire name IS the helper.
|
|
31
24
|
*/
|
|
32
|
-
export function compileCodeModeHelperInput(
|
|
25
|
+
export function compileCodeModeHelperInput(
|
|
26
|
+
argumentsText: unknown,
|
|
27
|
+
toolName: string,
|
|
28
|
+
wireToolName?: string,
|
|
29
|
+
): string {
|
|
33
30
|
if (typeof argumentsText !== "string") return "";
|
|
34
31
|
const helperName = toolName.startsWith("default.")
|
|
35
32
|
? toolName.slice("default.".length)
|
|
36
33
|
: toolName;
|
|
37
34
|
if (helperName === "apply_patch") {
|
|
38
|
-
|
|
35
|
+
// `resolveCodeModeHelperName` decides this IS an apply-patch call by reading
|
|
36
|
+
// `unwrapFreeformToolInput(argumentsText, wireToolName)`, which strips an outer Markdown
|
|
37
|
+
// fence and accepts that name's fallback fields (#4983). Compiling from a narrower unwrap
|
|
38
|
+
// meant a body accepted through a fence or a fallback field reached `tools.apply_patch`
|
|
39
|
+
// still wrapped, so the host rejected the JSON or the fence instead of applying the patch
|
|
40
|
+
// (#5046). One unwrap, one body, one decision.
|
|
41
|
+
//
|
|
42
|
+
// The vocabulary is the WIRE name rather than the helper name on purpose. `{"patch": ...}`
|
|
43
|
+
// is an apply_patch wrapper and is not an `exec` fallback field, and the recognizer already
|
|
44
|
+
// declines it under `exec`; reading it here would compile a body that recognition rejected,
|
|
45
|
+
// which is exactly the drift a second, looser unwrap introduces.
|
|
46
|
+
const patch = normalizeApplyPatchDelimiters(
|
|
47
|
+
unwrapFreeformToolInput(argumentsText, wireToolName ?? helperName),
|
|
48
|
+
);
|
|
39
49
|
return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
|
|
40
50
|
}
|
|
41
51
|
let parsed: unknown = argumentsText;
|
|
@@ -106,5 +116,5 @@ export function resolveCodeModeHelperName(
|
|
|
106
116
|
// `tools.apply_patch(...)` JavaScript would be the mis-route this repair exists to avoid.
|
|
107
117
|
if (!declaresCodeModeExec(declaredNames)) return undefined;
|
|
108
118
|
if (typeof argumentsText !== "string" || argumentsText === "") return undefined;
|
|
109
|
-
return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText)) ? "apply_patch" : undefined;
|
|
119
|
+
return isCompletePatchEnvelope(unwrapFreeformToolInput(argumentsText, "exec")) ? "apply_patch" : undefined;
|
|
110
120
|
}
|
|
@@ -326,7 +326,7 @@ export function restoreRoutedCustomCalls(
|
|
|
326
326
|
id: customToolItemId(item.id),
|
|
327
327
|
name: aliased ? targetName : item.name,
|
|
328
328
|
input: helper
|
|
329
|
-
? compileCodeModeHelperInput(sourceInput, helper)
|
|
329
|
+
? compileCodeModeHelperInput(sourceInput, helper, aliased ? String(item.name) : targetName)
|
|
330
330
|
: repairFreeformToolInput(
|
|
331
331
|
sourceInput,
|
|
332
332
|
targetName,
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Bounded classification of a PARTIAL freeform tool-call wrapper.
|
|
2
|
+
//
|
|
3
|
+
// `unwrapFreeformToolInput` decides the completed input with `JSON.parse`, which does not care
|
|
4
|
+
// what order an object's properties arrive in or how their names are spelled. The streaming side
|
|
5
|
+
// has to reach the same answer from a prefix, and it used to do that by comparing the buffer
|
|
6
|
+
// against the literal `{"input":"`. Spellings `JSON.parse` calls identical therefore matched no
|
|
7
|
+
// wrapper at all, streamed as raw JSON, and then completed as the unwrapped body:
|
|
8
|
+
// `{"metadata":1,"input":"cmd"}` and `{"\u0069nput":"cmd"}` both previewed the whole object and
|
|
9
|
+
// completed as `cmd` (#5151). #5047 and #5129 closed the compact and whitespace spellings of the
|
|
10
|
+
// same disagreement; reordering and escaping are the two the literal matcher could never see,
|
|
11
|
+
// because RFC 8259 objects are unordered and their names are strings with escapes.
|
|
12
|
+
//
|
|
13
|
+
// So the prefix is scanned as JSON instead of matched as text. The scan answers one question —
|
|
14
|
+
// which wrapper, if any, the completed text will unwrap to — and it answers it three ways:
|
|
15
|
+
//
|
|
16
|
+
// `input` the canonical key is present with a string value, whatever preceded it and however
|
|
17
|
+
// its name was spelled. Its value can be decoded progressively, because
|
|
18
|
+
// `unwrapFreeformToolInput` gives an own `input` precedence over every other property.
|
|
19
|
+
// `raw` no wrapper can apply, because the text is not an object or because it is one that
|
|
20
|
+
// `JSON.parse` will reject. Completion returns the buffer, so streaming it agrees.
|
|
21
|
+
// `hold` undecided. A key that has not arrived yet can still change the answer, so nothing
|
|
22
|
+
// is published until the object parses and completion's own rule decides.
|
|
23
|
+
//
|
|
24
|
+
// The bound matters as much as the classification. A scan that walks the whole buffer on every
|
|
25
|
+
// delta is quadratic in the argument size, so classification gives up after
|
|
26
|
+
// `MAX_FREEFORM_WRAPPER_SCAN_CHARS` and holds. Giving up costs preview, never agreement: a held
|
|
27
|
+
// buffer is still resolved by the authoritative parse at completion.
|
|
28
|
+
|
|
29
|
+
/** The insignificant whitespace `JSON.parse` accepts between tokens. */
|
|
30
|
+
export const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
31
|
+
|
|
32
|
+
/** The two-character escapes JSON defines, and nothing else. */
|
|
33
|
+
export const JSON_ESCAPES = new Map<string, string>([
|
|
34
|
+
['"', '"'], ["\\", "\\"], ["/", "/"],
|
|
35
|
+
["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How far a classification scan walks before it gives up and holds.
|
|
40
|
+
*
|
|
41
|
+
* A wrapper's own structure — its property names and the small scalar siblings a model puts
|
|
42
|
+
* beside the body — lives at the front of the object. A buffer still undecided after this much
|
|
43
|
+
* scanning is one whose preceding values are large, and holding it is what the routed
|
|
44
|
+
* restoration path already does for every unrecognized object.
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_FREEFORM_WRAPPER_SCAN_CHARS = 4096;
|
|
47
|
+
|
|
48
|
+
const CANONICAL_KEY = "input";
|
|
49
|
+
|
|
50
|
+
/** The buffer ends inside a token: what follows can still change the answer. */
|
|
51
|
+
const HOLD = -1;
|
|
52
|
+
/** `JSON.parse` will reject this text no matter what is appended to it. */
|
|
53
|
+
const NEVER = -2;
|
|
54
|
+
|
|
55
|
+
export type FreeformWrapperScan =
|
|
56
|
+
| { kind: "hold"; parse: boolean }
|
|
57
|
+
| { kind: "raw" }
|
|
58
|
+
| { kind: "input"; valueStart: number };
|
|
59
|
+
|
|
60
|
+
/** Index of the first non-whitespace character at or after `from`, or HOLD past the end. */
|
|
61
|
+
function skipWhitespace(text: string, from: number): number {
|
|
62
|
+
let i = from;
|
|
63
|
+
while (i < text.length && JSON_WHITESPACE.has(text[i]!)) i++;
|
|
64
|
+
return i < text.length ? i : HOLD;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Index just past the complete JSON string opening at `from`, or HOLD / NEVER.
|
|
69
|
+
*
|
|
70
|
+
* This reports a BOUNDARY where `decodeJsonStringPrefix` reports a decoded PREFIX: one has to
|
|
71
|
+
* fail on a truncated string and the other has to return what it decoded so far. They answer
|
|
72
|
+
* different questions about the same bytes and share one escape table so they cannot disagree
|
|
73
|
+
* about which escapes exist.
|
|
74
|
+
*/
|
|
75
|
+
function scanString(text: string, from: number): number {
|
|
76
|
+
let i = from + 1;
|
|
77
|
+
while (i < text.length) {
|
|
78
|
+
const c = text[i]!;
|
|
79
|
+
if (c === '"') return i + 1;
|
|
80
|
+
if (c === "\\") {
|
|
81
|
+
const n = text[i + 1];
|
|
82
|
+
if (n === undefined) return HOLD;
|
|
83
|
+
if (n === "u") {
|
|
84
|
+
const hex = text.slice(i + 2, i + 6);
|
|
85
|
+
if (hex.length < 4) return HOLD;
|
|
86
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return NEVER;
|
|
87
|
+
i += 6;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!JSON_ESCAPES.has(n)) return NEVER;
|
|
91
|
+
i += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// A literal control character is not legal inside a JSON string.
|
|
95
|
+
if (c.charCodeAt(0) <= 0x1f) return NEVER;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return HOLD;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const JSON_NUMBER_CHARS = /[-+0-9.eE]/;
|
|
102
|
+
const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
|
|
103
|
+
|
|
104
|
+
/** Index just past one complete scalar at `from`, or HOLD / NEVER. */
|
|
105
|
+
function scanScalar(text: string, from: number): number {
|
|
106
|
+
const c = text[from]!;
|
|
107
|
+
if (c === '"') return scanString(text, from);
|
|
108
|
+
if (c === "t" || c === "f" || c === "n") {
|
|
109
|
+
const literal = c === "t" ? "true" : c === "f" ? "false" : "null";
|
|
110
|
+
if (text.startsWith(literal, from)) return from + literal.length;
|
|
111
|
+
return literal.startsWith(text.slice(from)) ? HOLD : NEVER;
|
|
112
|
+
}
|
|
113
|
+
let end = from;
|
|
114
|
+
while (end < text.length && JSON_NUMBER_CHARS.test(text[end]!)) end++;
|
|
115
|
+
if (end === from) return NEVER;
|
|
116
|
+
// A number running to the end of the buffer can still grow another digit or exponent.
|
|
117
|
+
if (end >= text.length) return HOLD;
|
|
118
|
+
return JSON_NUMBER.test(text.slice(from, end)) ? end : NEVER;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Index where the value of the member whose name opens at `from` begins, or HOLD / NEVER. */
|
|
122
|
+
function scanMemberKey(text: string, from: number): number {
|
|
123
|
+
if (text[from] !== '"') return NEVER;
|
|
124
|
+
const nameEnd = scanString(text, from);
|
|
125
|
+
if (nameEnd < 0) return nameEnd;
|
|
126
|
+
const colon = skipWhitespace(text, nameEnd);
|
|
127
|
+
if (colon === HOLD) return HOLD;
|
|
128
|
+
if (text[colon] !== ":") return NEVER;
|
|
129
|
+
return skipWhitespace(text, colon + 1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Index just past one complete JSON value at `from`, or HOLD / NEVER.
|
|
134
|
+
*
|
|
135
|
+
* Containers are walked with an explicit stack rather than recursion: the value being skipped
|
|
136
|
+
* is provider-controlled, and a deeply nested one must not be able to exhaust the call stack.
|
|
137
|
+
*/
|
|
138
|
+
function scanValue(text: string, from: number): number {
|
|
139
|
+
const closers: string[] = [];
|
|
140
|
+
let i = from;
|
|
141
|
+
for (;;) {
|
|
142
|
+
const c = text[i]!;
|
|
143
|
+
if (c === "{" || c === "[") {
|
|
144
|
+
const closer = c === "{" ? "}" : "]";
|
|
145
|
+
closers.push(closer);
|
|
146
|
+
const first = skipWhitespace(text, i + 1);
|
|
147
|
+
if (first === HOLD) return HOLD;
|
|
148
|
+
if (text[first] === closer) {
|
|
149
|
+
closers.pop();
|
|
150
|
+
i = first + 1;
|
|
151
|
+
} else if (closer === "}") {
|
|
152
|
+
const value = scanMemberKey(text, first);
|
|
153
|
+
if (value < 0) return value;
|
|
154
|
+
i = value;
|
|
155
|
+
continue;
|
|
156
|
+
} else {
|
|
157
|
+
i = first;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
const end = scanScalar(text, i);
|
|
162
|
+
if (end < 0) return end;
|
|
163
|
+
i = end;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// One value is complete: close whatever it finished and find the next value, if any.
|
|
167
|
+
for (;;) {
|
|
168
|
+
if (closers.length === 0) return i;
|
|
169
|
+
const at = skipWhitespace(text, i);
|
|
170
|
+
if (at === HOLD) return HOLD;
|
|
171
|
+
const closer = closers[closers.length - 1]!;
|
|
172
|
+
if (text[at] === closer) {
|
|
173
|
+
closers.pop();
|
|
174
|
+
i = at + 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (text[at] !== ",") return NEVER;
|
|
178
|
+
const next = skipWhitespace(text, at + 1);
|
|
179
|
+
if (next === HOLD) return HOLD;
|
|
180
|
+
if (closer === "}") {
|
|
181
|
+
const value = scanMemberKey(text, next);
|
|
182
|
+
if (value < 0) return value;
|
|
183
|
+
i = value;
|
|
184
|
+
} else {
|
|
185
|
+
i = next;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Which wrapper the completed text will unwrap to, as far as this prefix can say.
|
|
194
|
+
*
|
|
195
|
+
* Fallback keys are deliberately not recognized here. They only unwrap when exactly one of them
|
|
196
|
+
* carries a string, and a second one can still arrive, so no prefix decides them — which makes
|
|
197
|
+
* them indistinguishable from any other undecided object and lets one HOLD cover both.
|
|
198
|
+
*/
|
|
199
|
+
export function scanFreeformWrapper(text: string): FreeformWrapperScan {
|
|
200
|
+
// One clamp rather than a budget threaded through every helper. Every helper already holds
|
|
201
|
+
// when it runs off the end of what it can see, so a buffer whose classification needs more
|
|
202
|
+
// than this holds for exactly the right reason, and no scan can cost more than this many
|
|
203
|
+
// characters however large the arguments grow. Indices into the clamp are indices into the
|
|
204
|
+
// full text, because the clamp is a prefix of it.
|
|
205
|
+
const bounded = text.length > MAX_FREEFORM_WRAPPER_SCAN_CHARS
|
|
206
|
+
? text.slice(0, MAX_FREEFORM_WRAPPER_SCAN_CHARS)
|
|
207
|
+
: text;
|
|
208
|
+
// `parse` is true only where this scan actually SAW the object close. Every other hold ran
|
|
209
|
+
// out of buffer or out of budget, and in both cases asking `JSON.parse` is work with no
|
|
210
|
+
// possible payoff: the first is provably incomplete, and the second would re-read a growing
|
|
211
|
+
// buffer on every delta that happens to end in a brace — repeated braces inside a long
|
|
212
|
+
// unterminated string are enough to make that quadratic. Holding a budget-exhausted prefix
|
|
213
|
+
// costs nothing that matters, because the value it would release arrives in the same instant
|
|
214
|
+
// as the authoritative completion that follows it.
|
|
215
|
+
const hold = (): FreeformWrapperScan => ({ kind: "hold", parse: false });
|
|
216
|
+
const open = skipWhitespace(bounded, 0);
|
|
217
|
+
if (open === HOLD) return hold();
|
|
218
|
+
// Not an object, so no wrapper rule reaches it: arrays, scalars and ordinary bodies all
|
|
219
|
+
// complete as themselves.
|
|
220
|
+
if (bounded[open] !== "{") return { kind: "raw" };
|
|
221
|
+
|
|
222
|
+
let i = open + 1;
|
|
223
|
+
for (;;) {
|
|
224
|
+
const at = skipWhitespace(bounded, i);
|
|
225
|
+
if (at === HOLD) return hold();
|
|
226
|
+
if (bounded[at] === "}") return afterTopLevelClose(bounded, at + 1);
|
|
227
|
+
if (bounded[at] !== '"') return { kind: "raw" };
|
|
228
|
+
|
|
229
|
+
const nameEnd = scanString(bounded, at);
|
|
230
|
+
if (nameEnd === HOLD) return hold();
|
|
231
|
+
if (nameEnd === NEVER) return { kind: "raw" };
|
|
232
|
+
let name: string;
|
|
233
|
+
try {
|
|
234
|
+
// The authoritative decoder for the name, so `{"\u0069nput":...}` resolves to the same
|
|
235
|
+
// key completion sees. Decoding it by hand here is the second implementation that would
|
|
236
|
+
// drift from `JSON.parse`, which is how this defect existed in the first place.
|
|
237
|
+
name = JSON.parse(bounded.slice(at, nameEnd)) as string;
|
|
238
|
+
} catch {
|
|
239
|
+
return { kind: "raw" };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const colon = skipWhitespace(bounded, nameEnd);
|
|
243
|
+
if (colon === HOLD) return hold();
|
|
244
|
+
if (bounded[colon] !== ":") return { kind: "raw" };
|
|
245
|
+
const valueAt = skipWhitespace(bounded, colon + 1);
|
|
246
|
+
if (valueAt === HOLD) return hold();
|
|
247
|
+
|
|
248
|
+
if (name === CANONICAL_KEY) {
|
|
249
|
+
// An own `input` wins over everything else in the object, so this is decidable now. A
|
|
250
|
+
// non-string value is decidable too, in the other direction: completion hands back the
|
|
251
|
+
// argument text unchanged rather than unwrapping a value it cannot use.
|
|
252
|
+
return bounded[valueAt] === '"'
|
|
253
|
+
? { kind: "input", valueStart: valueAt + 1 }
|
|
254
|
+
: { kind: "raw" };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const valueEnd = scanValue(bounded, valueAt);
|
|
258
|
+
if (valueEnd === HOLD) return hold();
|
|
259
|
+
if (valueEnd === NEVER) return { kind: "raw" };
|
|
260
|
+
|
|
261
|
+
const next = skipWhitespace(bounded, valueEnd);
|
|
262
|
+
if (next === HOLD) return hold();
|
|
263
|
+
if (bounded[next] === ",") {
|
|
264
|
+
i = next + 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (bounded[next] === "}") return afterTopLevelClose(bounded, next + 1);
|
|
268
|
+
return { kind: "raw" };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The object closed without a canonical key. Only whitespace may follow one that parses, so
|
|
274
|
+
* anything else makes the text raw; otherwise the completed parse decides between a fallback
|
|
275
|
+
* wrapper and no wrapper at all.
|
|
276
|
+
*/
|
|
277
|
+
function afterTopLevelClose(text: string, from: number): FreeformWrapperScan {
|
|
278
|
+
return skipWhitespace(text, from) === HOLD ? { kind: "hold", parse: true } : { kind: "raw" };
|
|
279
|
+
}
|
|
@@ -10,7 +10,90 @@ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
|
|
|
10
10
|
},
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Hosted-tool declaration names an operator may list in `unsupportedHostedTools`.
|
|
15
|
+
*
|
|
16
|
+
* A gateway must be able to deny anything a client can send it, so this is the set of
|
|
17
|
+
* nameless hosted/private declaration types the proxy recognizes on a Responses request.
|
|
18
|
+
* It is deliberately a closed vocabulary: the provider config schema ends in
|
|
19
|
+
* `.passthrough()`, so an unvalidated misspelling would be accepted, persisted, and then
|
|
20
|
+
* silently strip nothing -- which is the exact 400 the operator set the field to avoid
|
|
21
|
+
* (the `codexToolMode` lesson in #2106).
|
|
22
|
+
*/
|
|
23
|
+
export const DECLARABLE_HOSTED_TOOL_TYPES: ReadonlySet<string> = new Set([
|
|
24
|
+
"web_search",
|
|
25
|
+
"web_search_preview",
|
|
26
|
+
"file_search",
|
|
27
|
+
"computer_use_preview",
|
|
28
|
+
"computer_use",
|
|
29
|
+
"code_interpreter",
|
|
30
|
+
"image_generation",
|
|
31
|
+
"image_gen",
|
|
32
|
+
"mcp",
|
|
33
|
+
"tool_search",
|
|
34
|
+
"local_shell",
|
|
35
|
+
"x_search",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Spellings that name one capability. Declaring either member denies both, because the
|
|
40
|
+
* rest of the proxy already treats these as a single tool: the parser folds
|
|
41
|
+
* `web_search_preview` onto one name (`src/responses/parser-tools.ts`), Chat ingress
|
|
42
|
+
* accepts the pair together (`src/chat/inbound.ts`), and the canonical-field strip lists
|
|
43
|
+
* the pair in a single `toolTypes` set
|
|
44
|
+
* (`src/adapters/openai-responses/request-strips.ts`).
|
|
45
|
+
*
|
|
46
|
+
* Without the alias a capability declaration would be honoured for the spelling the
|
|
47
|
+
* operator happened to write and ignored for the one the client happened to send, which
|
|
48
|
+
* reproduces the original rejection while the config claims to have prevented it.
|
|
49
|
+
*/
|
|
50
|
+
const HOSTED_TOOL_ALIAS_GROUPS: ReadonlyArray<ReadonlySet<string>> = [
|
|
51
|
+
new Set(["web_search", "web_search_preview"]),
|
|
52
|
+
new Set(["image_generation", "image_gen"]),
|
|
53
|
+
new Set(["computer_use_preview", "computer_use"]),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const NO_DECLARED_HOSTED_TOOLS: ReadonlySet<string> = new Set();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Expand a provider's declared denials through the alias groups once per request, so the
|
|
60
|
+
* per-tool predicate stays a set lookup. Returns a shared empty set when the provider
|
|
61
|
+
* declares nothing, keeping the common path allocation-free.
|
|
62
|
+
*/
|
|
63
|
+
export function declaredUnsupportedHostedTools(
|
|
64
|
+
provider?: { unsupportedHostedTools?: readonly string[] },
|
|
65
|
+
): ReadonlySet<string> {
|
|
66
|
+
const declared = provider?.unsupportedHostedTools;
|
|
67
|
+
if (!Array.isArray(declared) || declared.length === 0) return NO_DECLARED_HOSTED_TOOLS;
|
|
68
|
+
const out = new Set<string>();
|
|
69
|
+
for (const raw of declared) {
|
|
70
|
+
if (typeof raw !== "string") continue;
|
|
71
|
+
const tool = raw.trim();
|
|
72
|
+
if (!tool) continue;
|
|
73
|
+
out.add(tool);
|
|
74
|
+
for (const group of HOSTED_TOOL_ALIAS_GROUPS) {
|
|
75
|
+
if (!group.has(tool)) continue;
|
|
76
|
+
for (const alias of group) out.add(alias);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out.size > 0 ? out : NO_DECLARED_HOSTED_TOOLS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* True when forwarding this hosted tool to the model would be rejected upstream.
|
|
84
|
+
*
|
|
85
|
+
* `declaredUnsupported` is the provider's own capability declaration, expanded by
|
|
86
|
+
* `declaredUnsupportedHostedTools`. It is additive to the built-in table rather than a
|
|
87
|
+
* replacement for it: the table covers destinations that reject a tool regardless of how
|
|
88
|
+
* the operator configured them, so an operator who never heard of the field stays
|
|
89
|
+
* protected.
|
|
90
|
+
*/
|
|
91
|
+
export function isHostedToolUnsupportedForModel(
|
|
92
|
+
modelId: string,
|
|
93
|
+
tool: string,
|
|
94
|
+
baseUrl?: string,
|
|
95
|
+
declaredUnsupported?: ReadonlySet<string>,
|
|
96
|
+
): boolean {
|
|
97
|
+
if (declaredUnsupported?.has(tool)) return true;
|
|
15
98
|
return UNSUPPORTED_HOSTED_TOOLS.some(entry => entry.match(modelId, baseUrl) && entry.tools.has(tool));
|
|
16
99
|
}
|