@bitkyc08/opencodex 2.57.0 → 2.59.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-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- 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 +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- 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 +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -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/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- 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/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- 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 +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- 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 +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -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-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -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 +282 -12
- 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 +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -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 +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- 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 +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- 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 +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -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/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- 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/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -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,
|
|
@@ -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
|
}
|
package/src/responses/schema.ts
CHANGED
|
@@ -126,10 +126,17 @@ export const toolSchema = z.object({
|
|
|
126
126
|
|
|
127
127
|
const builtinToolSchema = z.object({ type: z.string() }).loose();
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Hosted tool types a client may declare on an inbound Responses request. Exported so the
|
|
131
|
+
* provider-side capability vocabulary in `src/responses/hosted-tool-policy.ts` can be
|
|
132
|
+
* asserted to cover all of them: a gateway must be able to deny anything it can be sent.
|
|
133
|
+
*/
|
|
134
|
+
export const HOSTED_TOOL_TYPES = [
|
|
130
135
|
"web_search", "web_search_preview", "file_search", "computer_use_preview",
|
|
131
136
|
"code_interpreter", "image_generation", "mcp",
|
|
132
|
-
]
|
|
137
|
+
] as const;
|
|
138
|
+
|
|
139
|
+
const hostedToolType = z.enum(HOSTED_TOOL_TYPES);
|
|
133
140
|
|
|
134
141
|
const allowedToolEntrySchema = z.object({ type: z.string(), name: z.string().optional() });
|
|
135
142
|
|
|
@@ -159,6 +159,23 @@ function spillNow(): number {
|
|
|
159
159
|
return spillNowOverride?.() ?? Date.now();
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The spill deadline clock, shared with the shutdown drain in `state/spill-queue.ts`.
|
|
164
|
+
*
|
|
165
|
+
* Every deadline the shutdown path enforces has to read the same clock the work it
|
|
166
|
+
* budgets reads. When the drain measured its reserve on `Date.now()` while the ACL
|
|
167
|
+
* harden it was budgeting ran on this injected clock, a test could freeze the clock,
|
|
168
|
+
* believe it had removed wall time from the case, and still lose an 80 ms reserve to
|
|
169
|
+
* real elapsed time on a loaded runner — which is what turned
|
|
170
|
+
* `shutdown fallback prices the job-owned superseded generation before publishing`
|
|
171
|
+
* red on macOS 2/2 in run 35137850114 while the assertion it was written for never ran.
|
|
172
|
+
*
|
|
173
|
+
* Production is unchanged: with no override installed this is `Date.now()`.
|
|
174
|
+
*/
|
|
175
|
+
export function responseSpillNow(): number {
|
|
176
|
+
return spillNow();
|
|
177
|
+
}
|
|
178
|
+
|
|
162
179
|
function record(event: "write" | "fsync" | "close" | "harden" | "publish" | "dir-fsync" | "stub-swap"): void {
|
|
163
180
|
spillIoForTest?.record?.(event);
|
|
164
181
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request bodies that must never enter the continuation cache.
|
|
3
|
+
*
|
|
4
|
+
* The cache is persisted to `responses-state.json`, so anything recorded here reaches disk.
|
|
5
|
+
* Encrypted-agent-task recovery decrypts task text into the request body and promises
|
|
6
|
+
* in-memory, TTL-bounded retention; recording that body would put the plaintext on disk with
|
|
7
|
+
* no TTL and break the promise.
|
|
8
|
+
*
|
|
9
|
+
* A WeakSet rather than a body field on purpose: `_rawBody` is serialized verbatim by the
|
|
10
|
+
* native passthrough, so any marker written into the body itself would be sent upstream.
|
|
11
|
+
* Marking is enforced once here rather than at each call site, because every recording path
|
|
12
|
+
* (streaming, non-streaming, passthrough, forced) funnels through `rememberResponseState` —
|
|
13
|
+
* a new call site cannot reintroduce the leak by forgetting a guard.
|
|
14
|
+
*/
|
|
15
|
+
const nonPersistableBodies = new WeakSet<object>();
|
|
16
|
+
|
|
17
|
+
/** Bar this exact request body from the continuation cache, and therefore from disk. */
|
|
18
|
+
export function markBodyNonPersistable(body: unknown): void {
|
|
19
|
+
if (body && typeof body === "object") nonPersistableBodies.add(body as object);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Test the body's in-memory persistence restriction without adding a wire marker. */
|
|
23
|
+
export function isBodyNonPersistable(body: unknown): boolean {
|
|
24
|
+
return !!body && typeof body === "object" && nonPersistableBodies.has(body);
|
|
25
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MAX_RESPONSE_SPILL_PAYLOAD_BYTES,
|
|
8
8
|
prospectiveResponseSpillBytes,
|
|
9
9
|
responseSpillPayloadCap,
|
|
10
|
+
responseSpillNow,
|
|
10
11
|
type ResponseSpillPublicationControl,
|
|
11
12
|
type ResponseSpillRef,
|
|
12
13
|
writeResponseSpillDurably,
|
|
@@ -377,7 +378,7 @@ function responseSpillShutdownBudget(): { totalMs: number; fallbackReserveMs: nu
|
|
|
377
378
|
}
|
|
378
379
|
|
|
379
380
|
function awaitResponseSpillTailUntil(observed: Promise<void>, deadline: number): Promise<boolean> {
|
|
380
|
-
const remaining = deadline -
|
|
381
|
+
const remaining = deadline - responseSpillNow();
|
|
381
382
|
if (remaining <= 0) return Promise.resolve(false);
|
|
382
383
|
return new Promise(resolve => {
|
|
383
384
|
let finished = false;
|
|
@@ -554,12 +555,13 @@ function terminalizeExhaustedShutdownFallback(
|
|
|
554
555
|
}
|
|
555
556
|
|
|
556
557
|
function fallbackPendingResponseSpills(reserveMs: number): Error[] {
|
|
557
|
-
|
|
558
|
+
// Same clock as the harden work this reserve is budgeting — see `responseSpillNow`.
|
|
559
|
+
const deadline = responseSpillNow() + reserveMs;
|
|
558
560
|
const failures: Error[] = [];
|
|
559
561
|
for (;;) {
|
|
560
562
|
const pending = pendingShutdownFallbackCandidates();
|
|
561
563
|
if (pending.length === 0) return failures;
|
|
562
|
-
if (
|
|
564
|
+
if (responseSpillNow() >= deadline) {
|
|
563
565
|
terminalizeExhaustedShutdownFallback(pending, failures);
|
|
564
566
|
return failures;
|
|
565
567
|
}
|
|
@@ -569,7 +571,7 @@ function fallbackPendingResponseSpills(reserveMs: number): Error[] {
|
|
|
569
571
|
for (let index = 0; index < pending.length; index += 1) {
|
|
570
572
|
const { job, candidate } = pending[index]!;
|
|
571
573
|
if (requireStore().currentEntry(job.id) !== candidate) continue;
|
|
572
|
-
const remaining = deadline -
|
|
574
|
+
const remaining = deadline - responseSpillNow();
|
|
573
575
|
if (remaining <= 0) {
|
|
574
576
|
reserveExhausted = true;
|
|
575
577
|
for (const exhausted of pending.slice(index)) {
|
|
@@ -587,7 +589,7 @@ function fallbackPendingResponseSpills(reserveMs: number): Error[] {
|
|
|
587
589
|
requireStore().recomputeOldestResident();
|
|
588
590
|
requireStore().pruneResponses();
|
|
589
591
|
enforceAppOwnedMemoryBudget();
|
|
590
|
-
if (reserveExhausted ||
|
|
592
|
+
if (reserveExhausted || responseSpillNow() >= deadline) {
|
|
591
593
|
terminalizeExhaustedShutdownFallback(pendingShutdownFallbackCandidates(), failures);
|
|
592
594
|
return failures;
|
|
593
595
|
}
|
|
@@ -597,7 +599,7 @@ function fallbackPendingResponseSpills(reserveMs: number): Error[] {
|
|
|
597
599
|
export async function drainResponseSpillPublications(): Promise<void> {
|
|
598
600
|
const budget = responseSpillShutdownBudget();
|
|
599
601
|
const fallbackReserveMs = Math.min(budget.totalMs, Math.max(1, budget.fallbackReserveMs));
|
|
600
|
-
const drainDeadline =
|
|
602
|
+
const drainDeadline = responseSpillNow() + Math.max(0, budget.totalMs - fallbackReserveMs);
|
|
601
603
|
|
|
602
604
|
for (;;) {
|
|
603
605
|
if (pendingResponseSpills.size === 0) return;
|
package/src/responses/state.ts
CHANGED
|
@@ -23,6 +23,8 @@ import type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseS
|
|
|
23
23
|
export { responseAdmissionCountersForTests } from "./state/spill-failure";
|
|
24
24
|
import { admissionCounters, noteSpillWriteFailure, noteSpillWriteSuccess, spillCounters, spillWriteHealth } from "./state/spill-failure";
|
|
25
25
|
import { loadSnapshotEntry } from "./state/snapshot-codec";
|
|
26
|
+
import { isBodyNonPersistable } from "./state/body-policy";
|
|
27
|
+
export { isBodyNonPersistable, markBodyNonPersistable } from "./state/body-policy";
|
|
26
28
|
export { flushPendingResponseSpillsForTests, awaitResponseSpillPublicationTailForTests, pendingResponseSpillMetricsForTests, setResponseSpillShutdownBudgetForTests, setResponseSpillAsyncAclAttemptBudgetForTests, setResponseSpillShutdownTerminalizationPassLimitForTests } from "./state/spill-queue";
|
|
27
29
|
import {
|
|
28
30
|
bindSpillQueueStore,
|
|
@@ -1235,27 +1237,6 @@ export function responseStateMetrics(): ResponseStateMetrics {
|
|
|
1235
1237
|
* Cache completed output and max_output_tokens partial output for previous_response_id replay.
|
|
1236
1238
|
* Content-filtered incomplete and failed output are not authoritative replay history.
|
|
1237
1239
|
*/
|
|
1238
|
-
/**
|
|
1239
|
-
* Request bodies that must never enter the continuation cache.
|
|
1240
|
-
*
|
|
1241
|
-
* The cache is persisted to `responses-state.json`, so anything recorded here reaches disk.
|
|
1242
|
-
* Encrypted-agent-task recovery decrypts task text into the request body and promises
|
|
1243
|
-
* in-memory, TTL-bounded retention; recording that body would put the plaintext on disk with
|
|
1244
|
-
* no TTL and break the promise.
|
|
1245
|
-
*
|
|
1246
|
-
* A WeakSet rather than a body field on purpose: `_rawBody` is serialized verbatim by the
|
|
1247
|
-
* native passthrough, so any marker written into the body itself would be sent upstream.
|
|
1248
|
-
* Marking is enforced once here rather than at each call site, because every recording path
|
|
1249
|
-
* (streaming, non-streaming, passthrough, forced) funnels through `rememberResponseState` —
|
|
1250
|
-
* a new call site cannot reintroduce the leak by forgetting a guard.
|
|
1251
|
-
*/
|
|
1252
|
-
const nonPersistableBodies = new WeakSet<object>();
|
|
1253
|
-
|
|
1254
|
-
/** Bar this exact request body from the continuation cache, and therefore from disk. */
|
|
1255
|
-
export function markBodyNonPersistable(body: unknown): void {
|
|
1256
|
-
if (body && typeof body === "object") nonPersistableBodies.add(body as object);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
1240
|
export function rememberResponseState(
|
|
1260
1241
|
requestBody: unknown,
|
|
1261
1242
|
response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
|
|
@@ -1264,7 +1245,7 @@ export function rememberResponseState(
|
|
|
1264
1245
|
): void {
|
|
1265
1246
|
if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
|
|
1266
1247
|
const request = requestBody as Record<string, unknown>;
|
|
1267
|
-
if (
|
|
1248
|
+
if (isBodyNonPersistable(request)) return;
|
|
1268
1249
|
// `force` bypasses only the store:false skip: Codex sends `store:false` on every non-Azure
|
|
1269
1250
|
// HTTP request (and WS inherits it), yet its WS turns still chain with previous_response_id.
|
|
1270
1251
|
// The passthrough branch records with force so those chains can be expanded locally; the
|
package/src/router.ts
CHANGED
|
@@ -384,6 +384,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
384
384
|
&& registryEntry.requiresAdjacentResponsesToolResults !== undefined
|
|
385
385
|
? { requiresAdjacentResponsesToolResults: registryEntry.requiresAdjacentResponsesToolResults }
|
|
386
386
|
: {}),
|
|
387
|
+
...(provider.requiresPairedResponsesToolResults === undefined
|
|
388
|
+
&& registryEntry.requiresPairedResponsesToolResults !== undefined
|
|
389
|
+
? { requiresPairedResponsesToolResults: registryEntry.requiresPairedResponsesToolResults }
|
|
390
|
+
: {}),
|
|
387
391
|
...(provider.annotateEmptyToolOutputs === undefined
|
|
388
392
|
&& registryEntry.annotateEmptyToolOutputs !== undefined
|
|
389
393
|
? { annotateEmptyToolOutputs: registryEntry.annotateEmptyToolOutputs }
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { providerRelativeSendPathConfigError } from "../config/provider-relative-send-path";
|
|
1
2
|
import { modelCapabilitiesConfigError } from "../config/provider-validation";
|
|
2
3
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
4
|
import { initialModelSelection } from "../providers/initial-model-selection";
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
} from "../config/provider-validation";
|
|
30
31
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
31
32
|
import { redactSecretString } from "../lib/redact";
|
|
33
|
+
import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
|
|
32
34
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
|
|
33
35
|
import { providerConfigSeed } from "../providers/derive";
|
|
34
36
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
@@ -754,6 +756,10 @@ export function providerManagementConfigError(
|
|
|
754
756
|
}
|
|
755
757
|
const destinationError = providerDestinationConfigError(name, typed);
|
|
756
758
|
if (destinationError) return `provider ${name} ${destinationError}`;
|
|
759
|
+
for (const field of ["responsesPath", "chatCompletionsPath"] as const) {
|
|
760
|
+
const sendPathError = providerRelativeSendPathConfigError(field, raw[field]);
|
|
761
|
+
if (sendPathError) return `provider ${JSON.stringify(redactSecretString(name))} ${sendPathError}`;
|
|
762
|
+
}
|
|
757
763
|
const headersError = providerHeadersConfigError(typed.headers);
|
|
758
764
|
if (headersError) return `provider ${name} ${headersError}`;
|
|
759
765
|
const retryOn429Error = retryOn429PolicyConfigError(raw.retryOn429);
|
|
@@ -793,6 +799,8 @@ export function providerManagementConfigError(
|
|
|
793
799
|
}
|
|
794
800
|
const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
|
|
795
801
|
if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
|
|
802
|
+
const suppressSyntheticMaxError = booleanRecordConfigError(raw.modelSuppressSyntheticMax, "modelSuppressSyntheticMax");
|
|
803
|
+
if (suppressSyntheticMaxError) return `provider ${name} ${suppressSyntheticMaxError}`;
|
|
796
804
|
const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
|
|
797
805
|
raw.modelReasoningSummaryDelivery,
|
|
798
806
|
raw.modelSupportsReasoningSummaries,
|
|
@@ -834,6 +842,22 @@ export function providerManagementConfigError(
|
|
|
834
842
|
"omitReasoningEffortWithToolsModels",
|
|
835
843
|
);
|
|
836
844
|
if (toolReasoningOptOutError) return `provider ${name} ${toolReasoningOptOutError}`;
|
|
845
|
+
const unsupportedHostedToolsError = nonBlankStringArrayConfigError(
|
|
846
|
+
raw.unsupportedHostedTools,
|
|
847
|
+
"unsupportedHostedTools",
|
|
848
|
+
);
|
|
849
|
+
if (unsupportedHostedToolsError) return `provider ${name} ${unsupportedHostedToolsError}`;
|
|
850
|
+
if (Array.isArray(raw.unsupportedHostedTools)) {
|
|
851
|
+
// Closed vocabulary, same reason as the config schema: an unrecognized name would be
|
|
852
|
+
// stored and then strip nothing, so the operator would keep getting the upstream 400
|
|
853
|
+
// this field exists to prevent.
|
|
854
|
+
const unknownTool = (raw.unsupportedHostedTools as unknown[])
|
|
855
|
+
.find(tool => typeof tool === "string" && !DECLARABLE_HOSTED_TOOL_TYPES.has(tool.trim()));
|
|
856
|
+
if (unknownTool !== undefined) {
|
|
857
|
+
return `provider ${name} unsupportedHostedTools must name only hosted tool types: `
|
|
858
|
+
+ `${[...DECLARABLE_HOSTED_TOOL_TYPES].join(", ")}`;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
837
861
|
const openRouterError = openRouterRoutingConfigError(typed);
|
|
838
862
|
if (openRouterError) return `provider ${name} ${openRouterError}`;
|
|
839
863
|
const vercelError = vercelGatewayRoutingConfigError(typed);
|
|
@@ -913,6 +937,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
913
937
|
commandCodeVersion: "editor",
|
|
914
938
|
statelessResponses: "editor",
|
|
915
939
|
requiresAdjacentResponsesToolResults: "editor",
|
|
940
|
+
requiresPairedResponsesToolResults: "editor",
|
|
916
941
|
annotateEmptyToolOutputs: "editor",
|
|
917
942
|
supportsServiceTier: "editor",
|
|
918
943
|
modelSupportsServiceTier: "editor",
|
|
@@ -963,6 +988,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
963
988
|
refreshPolicy: "editor",
|
|
964
989
|
reasoningEfforts: "editor",
|
|
965
990
|
modelReasoningEfforts: "editor",
|
|
991
|
+
modelSuppressSyntheticMax: "editor",
|
|
966
992
|
modelDefaultReasoningEfforts: "editor",
|
|
967
993
|
pinnedReasoningEffort: "editor",
|
|
968
994
|
modelPinnedReasoningEfforts: "editor",
|
|
@@ -978,6 +1004,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
978
1004
|
xaiResponsesDefaultVersion: "runtime",
|
|
979
1005
|
zaiResponsesDefaultVersion: "runtime",
|
|
980
1006
|
supportsResponsesCustomTools: "editor",
|
|
1007
|
+
unsupportedHostedTools: "editor",
|
|
981
1008
|
responsesSnapshotRepair: "editor",
|
|
982
1009
|
webSearchBridge: "editor",
|
|
983
1010
|
reasoningEffortMap: "editor",
|
|
@@ -150,10 +150,15 @@ async function handleChatCompletionsWithBudget(
|
|
|
150
150
|
let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
|
|
151
151
|
try {
|
|
152
152
|
const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody));
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
// Preserve the routed destination for Go recognition, then settle the wire before
|
|
154
|
+
// deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
|
|
155
|
+
const routedProvider = route.provider;
|
|
156
|
+
const wireProvider = resolveWireProtocolOverride(route.providerName, route.modelId, routedProvider, "chat");
|
|
157
|
+
route.provider = resolveOpenCodeGoTransport(
|
|
158
|
+
wireProvider,
|
|
159
|
+
getOrAllocateRequestSessionLane(req),
|
|
160
|
+
routedProvider,
|
|
161
|
+
);
|
|
157
162
|
logCtx.model = route.modelId;
|
|
158
163
|
logCtx.providerAdapter = route.provider.adapter;
|
|
159
164
|
logCtx.requestedModel = requestedModel;
|