@bitkyc08/opencodex 2.56.0 → 2.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- 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/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- 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 +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -1
- package/src/types/provider.ts +13 -0
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { namespacedToolName, type OcxParsedRequest, type OcxTool } from "../../types";
|
|
3
|
+
import { frameAgentRouterMessages } from "../agentrouter";
|
|
4
|
+
|
|
5
|
+
const MAX_CHAT_TOOL_NAME_LENGTH = 64;
|
|
6
|
+
const ALIAS_HINT_CHARS = 16;
|
|
7
|
+
const RESERVED_ALIAS_PATTERN = /^ocx_[a-zA-Z0-9_-]{16}_[a-zA-Z0-9_-]{43}$/;
|
|
8
|
+
type ToolIdentity = Readonly<Pick<OcxTool, "namespace" | "name">>;
|
|
9
|
+
|
|
10
|
+
export interface OpenAIChatToolNameRegistry {
|
|
11
|
+
alias(tool: ToolIdentity): string;
|
|
12
|
+
aliasWireName(wireName: string): string;
|
|
13
|
+
restore(wireName: string): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface OpenAIChatToolNameScope {
|
|
17
|
+
messages(parsed: OcxParsedRequest, baseUrl: string, messages: readonly unknown[]): unknown;
|
|
18
|
+
registry(): OpenAIChatToolNameRegistry;
|
|
19
|
+
restore(wireName: string): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identityKey(tool: ToolIdentity): string {
|
|
23
|
+
return JSON.stringify([tool.namespace ?? null, tool.name]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function boundedAlias(tool: ToolIdentity, wireName: string): string {
|
|
27
|
+
const hint = wireName
|
|
28
|
+
.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
29
|
+
.slice(-ALIAS_HINT_CHARS)
|
|
30
|
+
.padStart(ALIAS_HINT_CHARS, "_");
|
|
31
|
+
const key = identityKey(tool);
|
|
32
|
+
const digest = createHash("sha256")
|
|
33
|
+
.update(key)
|
|
34
|
+
.digest("base64url");
|
|
35
|
+
return `ocx_${hint}_${digest}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Catalog declarations plus structured calls retained in replay history. */
|
|
39
|
+
export function openAIChatToolNameIdentities(parsed: OcxParsedRequest): ToolIdentity[] {
|
|
40
|
+
const identities: ToolIdentity[] = [...(parsed.context.tools ?? [])];
|
|
41
|
+
for (const message of parsed.context.messages) {
|
|
42
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
43
|
+
for (const part of message.content) {
|
|
44
|
+
if (part.type !== "toolCall") continue;
|
|
45
|
+
identities.push({
|
|
46
|
+
name: part.name,
|
|
47
|
+
...(part.namespace === undefined ? {} : { namespace: part.namespace }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return identities;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One collision domain for a translated Chat Completions request.
|
|
56
|
+
*
|
|
57
|
+
* Namespaced names whose flattened spelling exceeds Chat Completions' 64-character function-name
|
|
58
|
+
* bound are rewritten. Ordinary names and bare names pass through byte-for-byte unless they occupy
|
|
59
|
+
* the reserved alias spelling; those are re-aliased so no declaration can shadow another identity's
|
|
60
|
+
* deterministic alias. Distinct identities sharing one flattened spelling each keep an identity
|
|
61
|
+
* alias, while replay rewriting leaves that ambiguous spelling untouched. Echoed aliases restore to
|
|
62
|
+
* the original flattened name consumed by the Responses bridge's existing namespace map.
|
|
63
|
+
*/
|
|
64
|
+
export function createOpenAIChatToolNameRegistry(
|
|
65
|
+
tools: readonly ToolIdentity[] | undefined,
|
|
66
|
+
): OpenAIChatToolNameRegistry {
|
|
67
|
+
const identities = new Map<string, ToolIdentity>();
|
|
68
|
+
for (const tool of tools ?? []) identities.set(identityKey(tool), tool);
|
|
69
|
+
const sortedIdentities = [...identities.entries()]
|
|
70
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
71
|
+
|
|
72
|
+
const aliasesByIdentity = new Map<string, string>();
|
|
73
|
+
const aliasesByWireName = new Map<string, string>();
|
|
74
|
+
const originalsByAlias = new Map<string, string>();
|
|
75
|
+
const wireOwners = new Map<string, string | null>();
|
|
76
|
+
for (const [key, tool] of sortedIdentities) {
|
|
77
|
+
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
78
|
+
const owner = wireOwners.get(wireName);
|
|
79
|
+
if (owner === undefined) wireOwners.set(wireName, key);
|
|
80
|
+
else if (owner !== key) wireOwners.set(wireName, null);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const aliasOwners = new Map<string, string>();
|
|
84
|
+
const wireClaims = new Map<string, { key: string; alias: string } | null>();
|
|
85
|
+
for (const [key, tool] of sortedIdentities) {
|
|
86
|
+
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
87
|
+
const candidate = (tool.namespace !== undefined && wireName.length > MAX_CHAT_TOOL_NAME_LENGTH)
|
|
88
|
+
|| RESERVED_ALIAS_PATTERN.test(wireName)
|
|
89
|
+
|| wireOwners.get(wireName) === null
|
|
90
|
+
? boundedAlias(tool, wireName)
|
|
91
|
+
: wireName;
|
|
92
|
+
// A full SHA-256 collision is not safely attributable. Keep the later identity's native
|
|
93
|
+
// spelling instead of failing the request or stealing the first identity's restore entry.
|
|
94
|
+
const alias = aliasOwners.has(candidate) ? wireName : candidate;
|
|
95
|
+
aliasesByIdentity.set(key, alias);
|
|
96
|
+
if (!aliasOwners.has(alias)) aliasOwners.set(alias, key);
|
|
97
|
+
if (alias !== wireName) originalsByAlias.set(alias, wireName);
|
|
98
|
+
|
|
99
|
+
const claim = wireClaims.get(wireName);
|
|
100
|
+
if (claim === undefined) wireClaims.set(wireName, { key, alias });
|
|
101
|
+
else if (claim !== null && claim.key !== key) wireClaims.set(wireName, null);
|
|
102
|
+
}
|
|
103
|
+
for (const [wireName, claim] of wireClaims) {
|
|
104
|
+
if (claim !== null) aliasesByWireName.set(wireName, claim.alias);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
alias(tool: ToolIdentity): string {
|
|
109
|
+
const key = identityKey(tool);
|
|
110
|
+
const known = aliasesByIdentity.get(key);
|
|
111
|
+
if (known !== undefined) return known;
|
|
112
|
+
return namespacedToolName(tool.namespace, tool.name);
|
|
113
|
+
},
|
|
114
|
+
aliasWireName(wireName: string): string {
|
|
115
|
+
return aliasesByWireName.get(wireName) ?? wireName;
|
|
116
|
+
},
|
|
117
|
+
restore(wireName: string): string {
|
|
118
|
+
return originalsByAlias.get(wireName) ?? wireName;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function restoreOpenAIChatToolName(
|
|
124
|
+
registry: OpenAIChatToolNameRegistry,
|
|
125
|
+
wireName: string,
|
|
126
|
+
): string {
|
|
127
|
+
return registry.restore(wireName);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
131
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Rewrite replayed assistant tool calls after the ordinary message converter has flattened them. */
|
|
135
|
+
export function aliasOpenAIChatMessageToolNames(
|
|
136
|
+
messages: readonly unknown[],
|
|
137
|
+
registry: OpenAIChatToolNameRegistry,
|
|
138
|
+
): unknown[] {
|
|
139
|
+
return messages.map(message => {
|
|
140
|
+
if (!isRecord(message) || !Array.isArray(message.tool_calls)) return message;
|
|
141
|
+
let changed = false;
|
|
142
|
+
const toolCalls = message.tool_calls.map(toolCall => {
|
|
143
|
+
if (!isRecord(toolCall) || !isRecord(toolCall.function)
|
|
144
|
+
|| typeof toolCall.function.name !== "string") return toolCall;
|
|
145
|
+
const name = registry.aliasWireName(toolCall.function.name);
|
|
146
|
+
if (name === toolCall.function.name) return toolCall;
|
|
147
|
+
changed = true;
|
|
148
|
+
return { ...toolCall, function: { ...toolCall.function, name } };
|
|
149
|
+
});
|
|
150
|
+
return changed ? { ...message, tool_calls: toolCalls } : message;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function withOpenAIChatToolNames<T>(
|
|
155
|
+
build: (scope: OpenAIChatToolNameScope) => T,
|
|
156
|
+
): T {
|
|
157
|
+
let registry = createOpenAIChatToolNameRegistry(undefined);
|
|
158
|
+
return build({
|
|
159
|
+
messages(parsed, baseUrl, messages): unknown {
|
|
160
|
+
registry = createOpenAIChatToolNameRegistry(openAIChatToolNameIdentities(parsed));
|
|
161
|
+
return frameAgentRouterMessages(baseUrl, aliasOpenAIChatMessageToolNames(messages, registry));
|
|
162
|
+
},
|
|
163
|
+
registry: () => registry,
|
|
164
|
+
restore: wireName => restoreOpenAIChatToolName(registry, wireName),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isNativeOpenAIChatTarget } from "./wire";
|
|
2
|
+
import { createOpenAIChatToolNameRegistry, type OpenAIChatToolNameRegistry } from "./tool-name-registry";
|
|
2
3
|
import { isXaiSchemaTarget, lookupLocalJsonPointer, normalizeXaiToolParameters } from "../xai-tool-schema";
|
|
3
4
|
import { stripResponsesOnlyEncryptedMarker, stripUnicodePropertyPatterns } from "../responses-tool-schema";
|
|
4
|
-
import { isAllowedToolChoice,
|
|
5
|
+
import { isAllowedToolChoice, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../../types";
|
|
5
6
|
import type { OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
6
7
|
|
|
7
8
|
const ZEN_SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]);
|
|
@@ -409,7 +410,11 @@ function normalizeMoonshotToolParameters(parameters: unknown): Record<string, un
|
|
|
409
410
|
return isXaiObjectSchema(normalized) ? normalized : rooted;
|
|
410
411
|
}
|
|
411
412
|
|
|
412
|
-
export function toolsToChatFormat(
|
|
413
|
+
export function toolsToChatFormat(
|
|
414
|
+
parsed: OcxParsedRequest,
|
|
415
|
+
provider: OcxProviderConfig,
|
|
416
|
+
registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(parsed.context.tools),
|
|
417
|
+
): unknown[] | undefined {
|
|
413
418
|
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
414
419
|
const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
|
|
415
420
|
if (tools.length === 0) return undefined;
|
|
@@ -427,7 +432,7 @@ export function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProvide
|
|
|
427
432
|
return [{
|
|
428
433
|
type: "function",
|
|
429
434
|
function: {
|
|
430
|
-
name:
|
|
435
|
+
name: registry.alias(t),
|
|
431
436
|
...(t.description ? { description: t.description } : {}),
|
|
432
437
|
parameters,
|
|
433
438
|
...(t.strict !== undefined ? { strict: t.strict } : {}),
|
|
@@ -437,8 +442,12 @@ export function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProvide
|
|
|
437
442
|
return formatted.length > 0 ? formatted : undefined;
|
|
438
443
|
}
|
|
439
444
|
|
|
440
|
-
export function toolsToChatFormatForProvider(
|
|
441
|
-
|
|
445
|
+
export function toolsToChatFormatForProvider(
|
|
446
|
+
parsed: OcxParsedRequest,
|
|
447
|
+
provider: OcxProviderConfig,
|
|
448
|
+
registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(parsed.context.tools),
|
|
449
|
+
): unknown[] | undefined {
|
|
450
|
+
const base = toolsToChatFormat(parsed, provider, registry);
|
|
442
451
|
const azureChat = isAzureOpenAiChatTarget(provider);
|
|
443
452
|
const zenChat = shouldSanitizeZenToolParameters(provider);
|
|
444
453
|
if (!base || (!zenChat && !azureChat)) return base;
|
|
@@ -463,15 +472,24 @@ export function toolChoiceToChatFormat(
|
|
|
463
472
|
tc: OcxParsedRequest["options"]["toolChoice"],
|
|
464
473
|
tools: OcxParsedRequest["context"]["tools"],
|
|
465
474
|
provider: OcxProviderConfig,
|
|
475
|
+
registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(tools),
|
|
466
476
|
): unknown {
|
|
467
477
|
if (!tc) return undefined;
|
|
468
478
|
if (isAllowedToolChoice(tc)) {
|
|
469
479
|
if (tc.mode === "required" && tc.allowedTools.length === 1 && isNativeOpenAIChatTarget(provider)) {
|
|
470
|
-
return {
|
|
480
|
+
return {
|
|
481
|
+
type: "function",
|
|
482
|
+
function: { name: registry.aliasWireName(resolveToolChoiceWireName(tools, tc.allowedTools[0])) },
|
|
483
|
+
};
|
|
471
484
|
}
|
|
472
485
|
return tc.mode === "required" ? "required" : "auto";
|
|
473
486
|
}
|
|
474
487
|
if (tc === "auto" || tc === "none" || tc === "required") return tc;
|
|
475
|
-
if ("name" in tc)
|
|
488
|
+
if ("name" in tc) {
|
|
489
|
+
return {
|
|
490
|
+
type: "function",
|
|
491
|
+
function: { name: registry.aliasWireName(resolveToolChoiceWireName(tools, tc.name)) },
|
|
492
|
+
};
|
|
493
|
+
}
|
|
476
494
|
return undefined;
|
|
477
495
|
}
|
|
@@ -6,7 +6,6 @@ import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
|
6
6
|
import { debugProviderDiagnostic } from "../lib/debug";
|
|
7
7
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
8
8
|
import { isDebugEnabled } from "../lib/debug-settings";
|
|
9
|
-
import { frameAgentRouterMessages } from "./agentrouter";
|
|
10
9
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
11
10
|
import { resolveVercelGatewayRouting, vercelGatewayProviderPayload } from "../providers/vercel-gateway-routing";
|
|
12
11
|
import { fastPolicyForModel } from "../providers/service-tier";
|
|
@@ -39,6 +38,7 @@ import {
|
|
|
39
38
|
upstreamErrorEvent,
|
|
40
39
|
} from "./openai-chat/errors";
|
|
41
40
|
import { messagesToChatFormat } from "./openai-chat/messages";
|
|
41
|
+
import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry";
|
|
42
42
|
import { isNativeOpenAIChatTarget, openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire";
|
|
43
43
|
import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema";
|
|
44
44
|
|
|
@@ -88,7 +88,7 @@ function canSerializeOpenAIChatServiceTier(
|
|
|
88
88
|
|
|
89
89
|
export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
90
90
|
let lastRequestedModelId: string | undefined;
|
|
91
|
-
return {
|
|
91
|
+
return withOpenAIChatToolNames(toolNames => ({
|
|
92
92
|
name: "openai-chat",
|
|
93
93
|
|
|
94
94
|
formatErrorBody: formatOpenAIChatErrorBody,
|
|
@@ -96,10 +96,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
96
96
|
buildRequest(parsed: OcxParsedRequest, incoming?: IncomingMeta) {
|
|
97
97
|
lastRequestedModelId = parsed.modelId;
|
|
98
98
|
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
99
|
-
const messages =
|
|
99
|
+
const messages = toolNames.messages(parsed, provider.baseUrl, messagesToChatFormat(parsed, provider));
|
|
100
100
|
const finish = (): AdapterRequest => {
|
|
101
|
-
const tools = toolsToChatFormatForProvider(parsed, provider);
|
|
102
|
-
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider);
|
|
101
|
+
const tools = toolsToChatFormatForProvider(parsed, provider, toolNames.registry());
|
|
102
|
+
const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider, toolNames.registry());
|
|
103
103
|
|
|
104
104
|
const body: Record<string, unknown> = {
|
|
105
105
|
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(parsed.modelId) : parsed.modelId,
|
|
@@ -365,7 +365,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
365
365
|
return "terminate";
|
|
366
366
|
}
|
|
367
367
|
if (!call.id) call.id = `call_${++toolCallSeq}`;
|
|
368
|
-
yield { type: "tool_call_start", id: call.id, name: call.name };
|
|
368
|
+
yield { type: "tool_call_start", id: call.id, name: toolNames.restore(call.name) };
|
|
369
369
|
if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
|
|
370
370
|
yield { type: "tool_call_end" };
|
|
371
371
|
}
|
|
@@ -801,7 +801,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
801
801
|
logInvalidToolCalls("response", rawToolCalls);
|
|
802
802
|
return [invalidToolCallsEvent(rawToolCalls, "response", usage)];
|
|
803
803
|
}
|
|
804
|
-
events.push({ type: "tool_call_start", id, name });
|
|
804
|
+
events.push({ type: "tool_call_start", id, name: toolNames.restore(name) });
|
|
805
805
|
events.push({ type: "tool_call_delta", arguments: args });
|
|
806
806
|
events.push({ type: "tool_call_end" });
|
|
807
807
|
}
|
|
@@ -818,5 +818,5 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
818
818
|
budget.releaseRetained(responseBytes, { kind: "retained_collectors" });
|
|
819
819
|
}
|
|
820
820
|
},
|
|
821
|
-
};
|
|
821
|
+
}));
|
|
822
822
|
}
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
createAdapterTierMetadata,
|
|
33
33
|
} from "../../providers/fastwire";
|
|
34
34
|
import { mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
|
|
35
|
-
import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
|
|
35
|
+
import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
|
|
36
36
|
import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
|
|
37
37
|
import { isPlainObject } from "./internal";
|
|
38
38
|
import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
|
|
@@ -41,6 +41,19 @@ import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuation
|
|
|
41
41
|
import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
|
|
42
42
|
import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Identifies DeepSeek's strict Responses replay contract: tool-bearing continuations need
|
|
46
|
+
* plaintext reasoning and cannot consume opaque reasoning state. The two existing flags are
|
|
47
|
+
* current evidence for that one provider contract, not equivalent capabilities: preservation
|
|
48
|
+
* keeps plaintext reasoning on the wire, while adjacency marks its strict tool-history shape.
|
|
49
|
+
* The moment a second provider needs this behavior, replace this derivation with an explicit
|
|
50
|
+
* registry capability rather than extending the inference.
|
|
51
|
+
*/
|
|
52
|
+
export function requiresPlaintextReasoningReplay(provider: OcxProviderConfig): boolean {
|
|
53
|
+
return provider.preserveResponsesReasoningContent === true
|
|
54
|
+
&& provider.requiresAdjacentResponsesToolResults === true;
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
// Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
|
|
45
58
|
// Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
|
|
46
59
|
export const FORWARD_HEADERS = [
|
|
@@ -64,6 +77,16 @@ export const FORWARD_HEADERS = [
|
|
|
64
77
|
CODEX_RESPONSES_LITE_HEADER,
|
|
65
78
|
];
|
|
66
79
|
|
|
80
|
+
/** Preserve the caller fingerprint unless the provider explicitly owns that header. */
|
|
81
|
+
function applyCallerUserAgentFallback(
|
|
82
|
+
headers: Record<string, string>,
|
|
83
|
+
incoming: IncomingMeta,
|
|
84
|
+
): void {
|
|
85
|
+
if (Object.keys(headers).some(name => name.toLowerCase() === "user-agent")) return;
|
|
86
|
+
const userAgent = incoming.headers.get("user-agent");
|
|
87
|
+
if (userAgent) headers["User-Agent"] = userAgent;
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
/** Replace every `input_image` part under a routed-compaction body with a short marker. */
|
|
68
91
|
function stripInputImagesDeep(value: unknown): unknown {
|
|
69
92
|
if (Array.isArray(value)) return value.map(stripInputImagesDeep);
|
|
@@ -221,6 +244,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
221
244
|
if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
222
245
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
223
246
|
}
|
|
247
|
+
// Some Responses-compatible gateways select their Codex compatibility path from the real
|
|
248
|
+
// client fingerprint. This is a single non-credential fallback, not broader caller-header
|
|
249
|
+
// forwarding. Static provider headers remain authoritative in either auth mode.
|
|
250
|
+
applyCallerUserAgentFallback(headers, incoming);
|
|
224
251
|
|
|
225
252
|
const forward = provider.authMode === "forward";
|
|
226
253
|
let convertedRoutedCustomToolNames: Set<string> | undefined;
|
|
@@ -243,19 +270,31 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
243
270
|
// tier write so a force-fast/default decision can never mutate parsed._rawBody.
|
|
244
271
|
outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
|
|
245
272
|
const stateless = provider.statelessResponses === true;
|
|
273
|
+
const adjacentToolResults = provider.requiresAdjacentResponsesToolResults === true;
|
|
274
|
+
// Adjacency reorders items the upstream would accept in some order. Pairing synthesizes an
|
|
275
|
+
// item the client never sent, which is a larger claim about the conversation, so it is its
|
|
276
|
+
// own capability: Kimi carries the adjacency flag but accepts a dangling call (#4726) and
|
|
277
|
+
// must not start receiving placeholders it never needed.
|
|
278
|
+
const pairedToolResults = provider.requiresPairedResponsesToolResults === true;
|
|
246
279
|
if (stateless) outBody = stripStatefulResponsesParams(outBody);
|
|
247
280
|
// A replay miss can leave a function_call_output whose paired function_call sat
|
|
248
281
|
// in the prefix that was never expanded. A stateless upstream cannot resolve the
|
|
249
282
|
// pair from its own storage either, so it needs the same repair the forward
|
|
250
283
|
// backend gets — dropping previous_response_id is not much use if the body that
|
|
251
284
|
// reaches the wire is unparseable.
|
|
285
|
+
// A parser can also 400 on a function_call with no matching output at all. DeepSeek gets
|
|
286
|
+
// that repair through statelessResponses. xAI cannot be marked stateless: its Responses API
|
|
287
|
+
// stores conversations for 30 days and documents previous_response_id. So it carries the
|
|
288
|
+
// pairing capability instead, which reuses the orphan-call placeholder without touching
|
|
289
|
+
// store or previous_response_id.
|
|
252
290
|
if (provider.annotateEmptyToolOutputs === true) {
|
|
253
291
|
outBody = annotateEmptyResponsesToolOutputs(outBody, true);
|
|
254
292
|
}
|
|
255
|
-
|
|
256
|
-
|
|
293
|
+
const synthesizeMissingCallOutputs = !forward && (stateless || pairedToolResults);
|
|
294
|
+
if (forward || stateless || pairedToolResults) {
|
|
295
|
+
outBody = repairOrphanedInputItems(outBody, unexpandedMiss, synthesizeMissingCallOutputs);
|
|
257
296
|
}
|
|
258
|
-
if (
|
|
297
|
+
if (adjacentToolResults) {
|
|
259
298
|
outBody = normalizeResponsesToolResultAdjacency(outBody);
|
|
260
299
|
}
|
|
261
300
|
if (forward) {
|
|
@@ -289,6 +328,20 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
289
328
|
outBody = backfillWebSearchQueries(outBody);
|
|
290
329
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
291
330
|
outBody = stripInternalChatMessageMetadataPassthrough(outBody);
|
|
331
|
+
// The same class of private field, one level up, but keyed on the DESTINATION rather than
|
|
332
|
+
// on the canonical surface alone. `src/server/responses/compact.ts` spreads the caller's
|
|
333
|
+
// raw body into the native `/responses/compact` request without passing through this
|
|
334
|
+
// adapter, and that endpoint is offered only to OpenAI-operated destinations
|
|
335
|
+
// (supportsNativeResponsesCompactEndpoint). Stripping on the canonical predicate here
|
|
336
|
+
// would make the two paths disagree for `openai-apikey`; stripping on the destination
|
|
337
|
+
// keeps every OpenAI-operated route byte-identical and removes the field exactly where it
|
|
338
|
+
// is known to break, which is a gateway this proxy does not operate.
|
|
339
|
+
//
|
|
340
|
+
// Placed before the routed compaction body is built and before serialization, so the HTTP,
|
|
341
|
+
// routed-compaction and WebSocket outbounds are all covered by this one call.
|
|
342
|
+
if (!isOpenAiOperatedResponsesDestination(provider)) {
|
|
343
|
+
outBody = stripCanonicalOnlyTopLevelFields(outBody);
|
|
344
|
+
}
|
|
292
345
|
outBody = promoteClientLoadedTools(outBody);
|
|
293
346
|
}
|
|
294
347
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
@@ -357,6 +410,10 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
357
410
|
}
|
|
358
411
|
}
|
|
359
412
|
const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
|
|
413
|
+
// Providers with the strict plaintext tool-continuation contract cannot consume any
|
|
414
|
+
// encrypted reasoning blob, including one whose provenance is unknown. Combo routing
|
|
415
|
+
// separately refuses a proven cross-route replay when no plaintext exists; this final
|
|
416
|
+
// serializer guard ensures the foreign opaque state is never forwarded regardless.
|
|
360
417
|
const sanitizedBody = normalizeToolSchemas(
|
|
361
418
|
stripItemIdsWhenUnstored(
|
|
362
419
|
stripInvalidItemIds(
|
|
@@ -370,7 +427,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
370
427
|
{
|
|
371
428
|
preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
|
|
372
429
|
dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
|
|
373
|
-
stripEncryptedContent: threadServingIdentityChanged,
|
|
430
|
+
stripEncryptedContent: threadServingIdentityChanged || requiresPlaintextReasoningReplay(provider),
|
|
374
431
|
},
|
|
375
432
|
),
|
|
376
433
|
provider,
|
|
@@ -120,6 +120,49 @@ export function stripInternalChatMessageMetadataPassthrough(body: unknown): unkn
|
|
|
120
120
|
return changed ? { ...body, input } : body;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* OpenAI-private TOP-LEVEL request keys, the sibling of `CANONICAL_ONLY_TOOL_FIELDS` one level up.
|
|
125
|
+
*
|
|
126
|
+
* Codex attaches these on the request body itself rather than on a tool or an input item, and gates
|
|
127
|
+
* them on its own auth rather than on the destination URL. Loopback injection keeps Codex pointed at
|
|
128
|
+
* its built-in `openai` provider, so the client still believes it is addressing the canonical
|
|
129
|
+
* ChatGPT backend and keeps the key no matter where this proxy routes the turn. A Responses gateway
|
|
130
|
+
* that validates its top-level schema then rejects the whole request before inference.
|
|
131
|
+
*
|
|
132
|
+
* Keep this a table, and keep it to keys a client is OBSERVED to send. It is not an unknown-field
|
|
133
|
+
* sanitizer: a top-level key nobody has traced to a client is forwarded untouched, because deleting
|
|
134
|
+
* it would silently drop a parameter some other caller means.
|
|
135
|
+
*/
|
|
136
|
+
const CANONICAL_ONLY_TOP_LEVEL_FIELDS: readonly string[] = [
|
|
137
|
+
// Cyber access program selector, new in Codex 0.155. codex-rs mints it from
|
|
138
|
+
// `cyber_access_program::for_auth`, which filters on ChatGPT auth alone and never on the
|
|
139
|
+
// destination base URL, and serializes it on the Responses request, the compaction input and the
|
|
140
|
+
// WebSocket `response.create` envelope. No public specification defines it, so a strict
|
|
141
|
+
// third-party gateway answers with an unknown-parameter error naming it, and every turn of that
|
|
142
|
+
// thread fails (#4853).
|
|
143
|
+
//
|
|
144
|
+
// `codex_output_schema` is deliberately NOT here. In codex-rs it is the `name` of the JSON-schema
|
|
145
|
+
// `text.format` object, not a top-level key, so listing it would delete a field this client never
|
|
146
|
+
// sends and discard it for any client that does send it meaningfully.
|
|
147
|
+
"access_programs",
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Remove the OpenAI-private top-level keys.
|
|
152
|
+
*
|
|
153
|
+
* The caller decides the boundary; see the call site in `passthrough.ts`, which applies this only
|
|
154
|
+
* to a destination OpenCodex does not operate. Returns the input unchanged when no listed key is
|
|
155
|
+
* present, so the common path allocates nothing and the caller-owned raw body is never mutated.
|
|
156
|
+
*/
|
|
157
|
+
export function stripCanonicalOnlyTopLevelFields(body: unknown): unknown {
|
|
158
|
+
if (!isPlainObject(body)) return body;
|
|
159
|
+
if (!CANONICAL_ONLY_TOP_LEVEL_FIELDS.some(field => Object.hasOwn(body, field))) return body;
|
|
160
|
+
|
|
161
|
+
const next = { ...body };
|
|
162
|
+
for (const field of CANONICAL_ONLY_TOP_LEVEL_FIELDS) delete next[field];
|
|
163
|
+
return next;
|
|
164
|
+
}
|
|
165
|
+
|
|
123
166
|
/**
|
|
124
167
|
* When `store` is false, the upstream API does not persist response items. Any item ID
|
|
125
168
|
* forwarded in `input` is then interpreted as a reference to a stored item that does not
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AdapterFetchContext } from "./base";
|
|
2
|
+
import type { SendClass } from "../lib/request-execution-budget";
|
|
3
|
+
import type { AttemptRecoveryKind } from "../usage/log";
|
|
4
|
+
import { abortError, SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
5
|
+
|
|
6
|
+
type PacedFetch = typeof globalThis.fetch & {
|
|
7
|
+
waitForPacing?: (signal?: AbortSignal) => Promise<void>;
|
|
8
|
+
unpacedFetch?: typeof globalThis.fetch;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** One ordinal sequence per adapter fetchResponse call, across all of its inference retries.
|
|
12
|
+
* Consumption starts at underlying executor invocation; its own later preflight may still fail. */
|
|
13
|
+
export function createAdapterPhysicalSend(ctx: AdapterFetchContext = {}, fallback = globalThis.fetch) {
|
|
14
|
+
const executor = (ctx.executor ?? fallback) as PacedFetch;
|
|
15
|
+
let ordinal = 0;
|
|
16
|
+
return async (options: {
|
|
17
|
+
url: string;
|
|
18
|
+
sendClass?: SendClass;
|
|
19
|
+
recovery?: AttemptRecoveryKind;
|
|
20
|
+
/** Runs only after admission, e.g. backoff and cancellation of a superseded response. */
|
|
21
|
+
beforeDispatch?: () => void | Promise<void>;
|
|
22
|
+
dispatch: (executor: typeof globalThis.fetch) => Promise<Response>;
|
|
23
|
+
}): Promise<Response> => {
|
|
24
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
25
|
+
const decision = ctx.sendBudget?.reserveDispatch({
|
|
26
|
+
sendClass: options.sendClass ?? "transient", targetKey: options.url,
|
|
27
|
+
});
|
|
28
|
+
if (decision && !decision.allowed) throw new SendBudgetExhaustedError(options.url);
|
|
29
|
+
const permit = decision?.allowed ? decision.permit : undefined;
|
|
30
|
+
let dispatched = false;
|
|
31
|
+
const physicalExecutor = (async (input, init) => {
|
|
32
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
33
|
+
if (init?.signal?.aborted) throw abortError(init.signal);
|
|
34
|
+
if (dispatched || (permit && !permit.use())) throw new SendBudgetExhaustedError(options.url);
|
|
35
|
+
dispatched = true;
|
|
36
|
+
ordinal += 1;
|
|
37
|
+
ctx.onPhysicalSend?.({ ordinal, ...(options.recovery ? { recovery: options.recovery } : {}) });
|
|
38
|
+
return (executor.unpacedFetch ?? executor)(input, init);
|
|
39
|
+
}) as typeof globalThis.fetch;
|
|
40
|
+
try {
|
|
41
|
+
await executor.waitForPacing?.(ctx.abortSignal);
|
|
42
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
43
|
+
await options.beforeDispatch?.();
|
|
44
|
+
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
45
|
+
return await options.dispatch(physicalExecutor);
|
|
46
|
+
} finally {
|
|
47
|
+
permit?.release();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
package/src/bridge/errors.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNonReplayableUpstreamCode,
|
|
3
|
+
isReplayRefusalCode,
|
|
4
|
+
markReplayRefusalResponse,
|
|
5
|
+
markResponseNonReplayable,
|
|
6
|
+
REPLAY_REFUSED_STATUS,
|
|
7
|
+
} from "../lib/upstream-retry";
|
|
1
8
|
import {
|
|
2
9
|
adapterFailureFromMessage,
|
|
3
10
|
classifyError,
|
|
@@ -18,17 +25,34 @@ export function formatErrorResponse(
|
|
|
18
25
|
error.code = CYBER_POLICY_ERROR_CODE;
|
|
19
26
|
error.type = cyberPolicyErrorType(type);
|
|
20
27
|
}
|
|
21
|
-
|
|
28
|
+
// Only the allowlisted transport verdicts survive this formatter. Do not forward
|
|
29
|
+
// arbitrary provider codes, and preserve the existing cyber-policy precedence.
|
|
30
|
+
const replayBlocked = error.code !== CYBER_POLICY_ERROR_CODE
|
|
31
|
+
&& isNonReplayableUpstreamCode(options?.code);
|
|
32
|
+
if (replayBlocked) error.code = options!.code!;
|
|
33
|
+
// The replay refusal owns its status as well as its code. A combo or adapter formatter
|
|
34
|
+
// reaches here holding the upstream-shaped status it was about to report, and inheriting
|
|
35
|
+
// that would hand the client a 5xx it is configured to retry four times.
|
|
36
|
+
const finalStatus = error.code === CYBER_POLICY_ERROR_CODE
|
|
37
|
+
? 400
|
|
38
|
+
: isReplayRefusalCode(error.code) ? REPLAY_REFUSED_STATUS : status;
|
|
22
39
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
23
40
|
const retryAfter = options?.retryAfter?.trim();
|
|
24
41
|
if (error.code !== CYBER_POLICY_ERROR_CODE
|
|
42
|
+
&& !replayBlocked
|
|
25
43
|
&& retryAfter
|
|
26
44
|
&& retryAfter.length > 0
|
|
27
45
|
&& retryAfter.length <= 128) {
|
|
28
46
|
headers.set("Retry-After", retryAfter);
|
|
29
47
|
}
|
|
30
|
-
|
|
48
|
+
const response = new Response(JSON.stringify({ error }), {
|
|
31
49
|
status: finalStatus,
|
|
32
50
|
headers,
|
|
33
51
|
});
|
|
52
|
+
if (replayBlocked) markResponseNonReplayable(response);
|
|
53
|
+
// Re-wrapping is where the refusal loses its provenance: combo failure consumption parses
|
|
54
|
+
// the JSON and builds a new Response, and the code alone does not tell a later quota
|
|
55
|
+
// recorder that no upstream produced this status. Carry the narrower marker across too.
|
|
56
|
+
if (replayBlocked && isReplayRefusalCode(error.code)) markReplayRefusalResponse(response);
|
|
57
|
+
return response;
|
|
34
58
|
}
|
|
@@ -68,6 +68,8 @@ function buildResponseJSONWithBudget(
|
|
|
68
68
|
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
|
|
69
69
|
/** Request-visible tool names. When present, an upstream call outside this set fails closed. */
|
|
70
70
|
declaredToolNames?: ReadonlySet<string>;
|
|
71
|
+
/** See `bridgeToResponsesSSE`: enforcement is separate from normalization (#4735). */
|
|
72
|
+
enforceDeclaredToolNames?: boolean;
|
|
71
73
|
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
|
|
72
74
|
toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
|
|
73
75
|
freeformToolNames?: Set<string>;
|
|
@@ -432,7 +434,11 @@ function buildResponseJSONWithBudget(
|
|
|
432
434
|
}
|
|
433
435
|
flushToolCall();
|
|
434
436
|
const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
|
|
435
|
-
if (
|
|
437
|
+
if (
|
|
438
|
+
options?.declaredToolNames
|
|
439
|
+
&& options.enforceDeclaredToolNames !== false
|
|
440
|
+
&& !options.declaredToolNames.has(effectiveName)
|
|
441
|
+
) {
|
|
436
442
|
errorEvent = {
|
|
437
443
|
type: "error",
|
|
438
444
|
message: `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
|
|
@@ -521,7 +527,7 @@ function buildResponseJSONWithBudget(
|
|
|
521
527
|
compactionEncryptedContent = e.compactionEncryptedContent;
|
|
522
528
|
sawTerminal = true;
|
|
523
529
|
endTurn = e.endTurn;
|
|
524
|
-
cleanDone = e.stopReason
|
|
530
|
+
cleanDone = !isTruncatedStopReason(e.stopReason);
|
|
525
531
|
rawStopReason = e.stopReason;
|
|
526
532
|
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
527
533
|
// Match streaming: max_tokens and content_filter both terminate as incomplete.
|
package/src/bridge/sse.ts
CHANGED
|
@@ -88,6 +88,20 @@ export function bridgeToResponsesSSE(
|
|
|
88
88
|
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
89
89
|
/** Request-visible tool names. When present, an upstream call outside this set fails closed. */
|
|
90
90
|
declaredToolNames?: ReadonlySet<string>;
|
|
91
|
+
/**
|
|
92
|
+
* Whether `declaredToolNames` is an authorization boundary this proxy enforces, or only the
|
|
93
|
+
* catalog used to normalize provider-invented names back to declared ones.
|
|
94
|
+
*
|
|
95
|
+
* Defaults to enforcing. The chat and Anthropic inbound wires set it false: those specs make
|
|
96
|
+
* the server relay a tool call and leave execution or refusal to the client's own runner, and
|
|
97
|
+
* harnesses on them legitimately defer part of their catalog (#4735).
|
|
98
|
+
*
|
|
99
|
+
* It is a separate flag rather than simply withholding `declaredToolNames`, because the set
|
|
100
|
+
* also drives `normalizeDeclaredToolName` and `declaresCodeModeExec`. Passing `undefined`
|
|
101
|
+
* turns those off too, so a provider that invents `default.lookup` for a declared `lookup`
|
|
102
|
+
* would reach the client under the invented name instead of the normalized one.
|
|
103
|
+
*/
|
|
104
|
+
enforceDeclaredToolNames?: boolean;
|
|
91
105
|
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
|
|
92
106
|
toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
|
|
93
107
|
/**
|
|
@@ -1008,7 +1022,11 @@ export function bridgeToResponsesSSE(
|
|
|
1008
1022
|
: undefined;
|
|
1009
1023
|
const mapped = toolNsMap?.get(effectiveName);
|
|
1010
1024
|
const realName = mapped?.name ?? effectiveName;
|
|
1011
|
-
if (
|
|
1025
|
+
if (
|
|
1026
|
+
options?.declaredToolNames
|
|
1027
|
+
&& options.enforceDeclaredToolNames !== false
|
|
1028
|
+
&& !options.declaredToolNames.has(effectiveName)
|
|
1029
|
+
) {
|
|
1012
1030
|
const failure = responseError(
|
|
1013
1031
|
502,
|
|
1014
1032
|
"upstream_error",
|
|
@@ -1153,7 +1171,7 @@ export function bridgeToResponsesSSE(
|
|
|
1153
1171
|
break;
|
|
1154
1172
|
}
|
|
1155
1173
|
case "done": {
|
|
1156
|
-
if (currentMsg) closeCurrentMessage(event.stopReason ? undefined : "final_answer");
|
|
1174
|
+
if (currentMsg) closeCurrentMessage(isTruncatedStopReason(event.stopReason) ? undefined : "final_answer");
|
|
1157
1175
|
if (currentReasoning) closeCurrentReasoning();
|
|
1158
1176
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
1159
1177
|
flushHiddenRawReasoning();
|