@bitkyc08/opencodex 2.60.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +5 -1
- package/src/adapters/anthropic.ts +16 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +10 -2
- package/src/adapters/command-code.ts +2 -1
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +13 -3
- package/src/adapters/cursor.ts +11 -2
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin.ts +75 -23
- package/src/adapters/google-antigravity-wire.ts +5 -2
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google.ts +29 -5
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat.ts +18 -57
- package/src/adapters/openai-responses/passthrough.ts +2 -0
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +10 -0
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +43 -4
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/index.ts +200 -61
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-listener.ts +2 -5
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/provider-models.ts +24 -15
- package/src/codex/catalog/retained-sync.ts +2 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/history-provider.ts +12 -1
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +52 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +87 -44
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +5 -41
- package/src/codex/shim-templates.ts +29 -3
- package/src/companion/settings.ts +132 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/load-degrade.ts +34 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/schema/config-schema.ts +27 -1
- package/src/config/schema/leaf-validators.ts +47 -0
- package/src/config.ts +1 -1
- package/src/generated/compatibility-version.json +418 -174
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +124 -18
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +49 -11
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug.ts +40 -0
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +59 -14
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/index.ts +24 -1
- package/src/oauth/login-cli.ts +80 -29
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/key-failover.ts +31 -1
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +124 -37
- package/src/providers/quota/vendor-probes-key.ts +37 -22
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +9 -4
- package/src/providers/registry/entries-extended.ts +29 -4
- package/src/providers/registry/model-seeds.ts +47 -10
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/schema.ts +3 -0
- package/src/router.ts +17 -2
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +55 -2
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +55 -17
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +56 -10
- package/src/server/index/spend-ledger-lifecycle.ts +34 -8
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index.ts +21 -28
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +6 -0
- package/src/server/management/shared.ts +18 -1
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +8 -9
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/relay.ts +19 -2
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +114 -0
- package/src/server/request-metrics.ts +92 -30
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +90 -0
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +14 -1
- package/src/server/responses/passthrough-dispatch.ts +179 -35
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/request-prepare.ts +42 -1
- package/src/server/responses/request-send-budget.ts +12 -0
- package/src/server/responses/request-transport.ts +24 -4
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +4 -1
- package/src/server/search.ts +25 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +1 -1
- package/src/types/config.ts +37 -0
- package/src/types/provider.ts +73 -0
- package/src/types/request.ts +28 -2
- package/src/types/tools.ts +19 -0
- package/src/types.ts +3 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/web-search/alpha-search.ts +21 -1
- package/gui/dist/assets/index-BTuCbqQd.css +0 -1
- package/gui/dist/assets/index-DoBVdPHP.js +0 -134
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { openAIChatTransport, stripBracketedModelSuffix } from "./wire";
|
|
2
2
|
import type { AdapterRequest } from "../base";
|
|
3
3
|
import { frameAgentRouterMessages } from "../agentrouter";
|
|
4
|
+
import { applyExplicitChatDeveloperRole } from "./developer-role";
|
|
4
5
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../../providers/openrouter-routing";
|
|
5
6
|
import { resolveVercelGatewayRouting, vercelGatewayProviderPayload } from "../../providers/vercel-gateway-routing";
|
|
6
7
|
import { fastPolicyForModel } from "../../providers/service-tier";
|
|
@@ -9,6 +10,8 @@ import { debugProviderDiagnostic } from "../../lib/debug";
|
|
|
9
10
|
import { isDebugEnabled } from "../../lib/debug-settings";
|
|
10
11
|
import { modelRecordValue } from "../../reasoning-effort";
|
|
11
12
|
import { modelInList, type OcxProviderConfig } from "../../types";
|
|
13
|
+
import { chatParallelToolCallsWireValue } from "./parallel-tool-calls";
|
|
14
|
+
import { applyExplicitChatReasoningWirePolicy } from "./reasoning-wire";
|
|
12
15
|
|
|
13
16
|
const CHAT_PASSTHROUGH_FIELDS = [
|
|
14
17
|
"audio",
|
|
@@ -54,16 +57,37 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
54
57
|
|
|
55
58
|
const body: Record<string, unknown> = {
|
|
56
59
|
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
|
|
57
|
-
messages
|
|
60
|
+
// The caller's messages are forwarded as they arrived, with one exception: an operator who
|
|
61
|
+
// recorded that this destination rejects the `developer` role gets that role converted in
|
|
62
|
+
// place. Verbatim was not neutral there — it sent the role anyway and the turn failed
|
|
63
|
+
// upstream with a 400 before the model saw it. An unrecorded destination is still verbatim,
|
|
64
|
+
// and the conversion changes the role of those messages and nothing else, so the position
|
|
65
|
+
// of every message and every other field survive unchanged.
|
|
66
|
+
messages: applyExplicitChatDeveloperRole(
|
|
67
|
+
frameAgentRouterMessages(provider.baseUrl, rawBody.messages),
|
|
68
|
+
provider,
|
|
69
|
+
),
|
|
58
70
|
stream,
|
|
59
71
|
};
|
|
60
72
|
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
61
73
|
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
62
74
|
}
|
|
63
75
|
const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
|
|
64
|
-
|
|
76
|
+
const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
|
|
77
|
+
if (reasoningDisabled) {
|
|
65
78
|
delete body.reasoning_effort;
|
|
66
79
|
}
|
|
80
|
+
const hasTools = Array.isArray(rawBody.tools) && rawBody.tools.length > 0;
|
|
81
|
+
const requestedEffort = typeof body.reasoning_effort === "string" ? body.reasoning_effort : undefined;
|
|
82
|
+
applyExplicitChatReasoningWirePolicy({
|
|
83
|
+
provider,
|
|
84
|
+
modelId,
|
|
85
|
+
hasTools,
|
|
86
|
+
requestedEffort,
|
|
87
|
+
wireEffort: requestedEffort,
|
|
88
|
+
reasoningDisabled,
|
|
89
|
+
body,
|
|
90
|
+
});
|
|
67
91
|
|
|
68
92
|
const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
|
|
69
93
|
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
|
|
@@ -107,13 +131,13 @@ export function buildOpenAIChatPassthroughRequest(
|
|
|
107
131
|
if (provider.promptCacheKey && rawBody.prompt_cache_key !== undefined) {
|
|
108
132
|
body.prompt_cache_key = rawBody.prompt_cache_key;
|
|
109
133
|
}
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
if (hasTools) {
|
|
135
|
+
// Same three provider states as the translated path, and the same defect in the unset one:
|
|
136
|
+
// a caller's explicit false was dropped here too (#5211). The native route reads the bit off
|
|
137
|
+
// the raw request rather than the parsed options, since nothing projects this body.
|
|
138
|
+
const requested = typeof rawBody.parallel_tool_calls === "boolean" ? rawBody.parallel_tool_calls : undefined;
|
|
139
|
+
const parallelToolCalls = chatParallelToolCallsWireValue(provider, requested);
|
|
140
|
+
if (parallelToolCalls !== undefined) body.parallel_tool_calls = parallelToolCalls;
|
|
117
141
|
}
|
|
118
142
|
if (stream) {
|
|
119
143
|
const callerOptions = rawBody.stream_options !== null
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { modelInList, type OcxProviderConfig } from "../../types";
|
|
2
|
+
import type { AdapterRequest } from "../base";
|
|
3
|
+
import { isNativeOpenAIChatTarget } from "./wire";
|
|
4
|
+
|
|
5
|
+
export type ExplicitChatReasoningWireResult =
|
|
6
|
+
| { handled: false }
|
|
7
|
+
| { handled: true; reasoningLog?: AdapterRequest["reasoningLog"] };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Apply provider-declared reasoning wire policy after the effective provider is resolved.
|
|
11
|
+
* Unset declarations are a no-op so native Chat keeps forwarding the caller's raw field.
|
|
12
|
+
*/
|
|
13
|
+
export function applyExplicitChatReasoningWirePolicy(options: {
|
|
14
|
+
provider: OcxProviderConfig;
|
|
15
|
+
modelId: string;
|
|
16
|
+
hasTools: boolean;
|
|
17
|
+
requestedEffort: string | undefined;
|
|
18
|
+
wireEffort: string | undefined;
|
|
19
|
+
reasoningDisabled: boolean;
|
|
20
|
+
body: Record<string, unknown>;
|
|
21
|
+
}): ExplicitChatReasoningWireResult {
|
|
22
|
+
const {
|
|
23
|
+
provider,
|
|
24
|
+
modelId,
|
|
25
|
+
hasTools,
|
|
26
|
+
requestedEffort,
|
|
27
|
+
wireEffort,
|
|
28
|
+
reasoningDisabled,
|
|
29
|
+
body,
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
if (reasoningDisabled) return { handled: false };
|
|
33
|
+
if (hasTools && modelInList(provider.omitReasoningEffortWithToolsModels, modelId)) {
|
|
34
|
+
delete body.reasoning_effort;
|
|
35
|
+
delete body.reasoning;
|
|
36
|
+
return { handled: true };
|
|
37
|
+
}
|
|
38
|
+
if (provider.reasoningWireFormat !== "gateway-object") return { handled: false };
|
|
39
|
+
|
|
40
|
+
const nativeOpenAI = isNativeOpenAIChatTarget(provider);
|
|
41
|
+
if (requestedEffort === "none") {
|
|
42
|
+
if (nativeOpenAI) {
|
|
43
|
+
delete body.reasoning;
|
|
44
|
+
body.reasoning_effort = "none";
|
|
45
|
+
return {
|
|
46
|
+
handled: true,
|
|
47
|
+
reasoningLog: {
|
|
48
|
+
effectiveEffort: "none",
|
|
49
|
+
wireField: "reasoning_effort",
|
|
50
|
+
wireValue: "none",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
delete body.reasoning_effort;
|
|
55
|
+
body.reasoning = { enabled: false };
|
|
56
|
+
return {
|
|
57
|
+
handled: true,
|
|
58
|
+
reasoningLog: {
|
|
59
|
+
effectiveEffort: "none",
|
|
60
|
+
wireField: "reasoning.enabled",
|
|
61
|
+
wireValue: false,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (wireEffort === undefined) return { handled: false };
|
|
66
|
+
|
|
67
|
+
if (nativeOpenAI) {
|
|
68
|
+
delete body.reasoning;
|
|
69
|
+
body.reasoning_effort = wireEffort;
|
|
70
|
+
return {
|
|
71
|
+
handled: true,
|
|
72
|
+
reasoningLog: {
|
|
73
|
+
effectiveEffort: wireEffort,
|
|
74
|
+
wireField: "reasoning_effort",
|
|
75
|
+
wireValue: wireEffort,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
delete body.reasoning_effort;
|
|
80
|
+
body.reasoning = { enabled: true, effort: wireEffort };
|
|
81
|
+
return {
|
|
82
|
+
handled: true,
|
|
83
|
+
reasoningLog: {
|
|
84
|
+
effectiveEffort: wireEffort,
|
|
85
|
+
wireField: "reasoning.effort",
|
|
86
|
+
wireValue: wireEffort,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { hasShrinkableOpenAIChatImages, normalizeOpenAIChatImages } from "./openai-chat-images";
|
|
2
|
+
import { chatParallelToolCallsWireValue } from "./openai-chat/parallel-tool-calls";
|
|
3
|
+
import { applyExplicitChatReasoningWirePolicy } from "./openai-chat/reasoning-wire";
|
|
2
4
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
|
|
3
5
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
|
|
4
6
|
import { modelInList } from "../types";
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
} from "./openai-chat/errors";
|
|
41
43
|
import { messagesToChatFormat } from "./openai-chat/messages";
|
|
42
44
|
import { withOpenAIChatToolNames } from "./openai-chat/tool-name-registry";
|
|
43
|
-
import {
|
|
45
|
+
import { openAIChatTransport, stripBracketedModelSuffix } from "./openai-chat/wire";
|
|
44
46
|
import { toolChoiceToChatFormat, toolsToChatFormatForProvider } from "./openai-chat/tool-schema";
|
|
45
47
|
|
|
46
48
|
export { stripBracketedModelSuffix } from "./openai-chat/wire";
|
|
@@ -142,51 +144,21 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
142
144
|
}
|
|
143
145
|
if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
|
|
144
146
|
const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
148
|
+
const explicitReasoning = applyExplicitChatReasoningWirePolicy({
|
|
149
|
+
provider,
|
|
150
|
+
modelId: parsed.modelId,
|
|
151
|
+
hasTools: !!tools,
|
|
152
|
+
requestedEffort: parsed.options.reasoning,
|
|
153
|
+
wireEffort: reasoningEffort,
|
|
154
|
+
reasoningDisabled,
|
|
155
|
+
body,
|
|
156
|
+
});
|
|
155
157
|
let reasoningLog: AdapterRequest["reasoningLog"];
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
body.reasoning_effort = "none";
|
|
159
|
-
reasoningLog = {
|
|
160
|
-
effectiveEffort: "none",
|
|
161
|
-
wireField: "reasoning_effort",
|
|
162
|
-
wireValue: "none",
|
|
163
|
-
};
|
|
164
|
-
} else {
|
|
165
|
-
body.reasoning = { enabled: false };
|
|
166
|
-
reasoningLog = {
|
|
167
|
-
effectiveEffort: "none",
|
|
168
|
-
wireField: "reasoning.enabled",
|
|
169
|
-
wireValue: false,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
158
|
+
if (explicitReasoning.handled) {
|
|
159
|
+
reasoningLog = explicitReasoning.reasoningLog;
|
|
172
160
|
} else if (reasoningEffort !== undefined) {
|
|
173
|
-
if (provider.
|
|
174
|
-
if (nativeOpenAI) {
|
|
175
|
-
body.reasoning_effort = reasoningEffort;
|
|
176
|
-
reasoningLog = {
|
|
177
|
-
effectiveEffort: reasoningEffort,
|
|
178
|
-
wireField: "reasoning_effort",
|
|
179
|
-
wireValue: reasoningEffort,
|
|
180
|
-
};
|
|
181
|
-
} else {
|
|
182
|
-
body.reasoning = { enabled: true, effort: reasoningEffort };
|
|
183
|
-
reasoningLog = {
|
|
184
|
-
effectiveEffort: reasoningEffort,
|
|
185
|
-
wireField: "reasoning.effort",
|
|
186
|
-
wireValue: reasoningEffort,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
} else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
|
|
161
|
+
if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
|
|
190
162
|
const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
|
|
191
163
|
if (budget !== undefined) {
|
|
192
164
|
body.thinking_budget = budget;
|
|
@@ -249,19 +221,8 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
249
221
|
}
|
|
250
222
|
|
|
251
223
|
if (tools) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// wire bit so Codex cannot opt in via request.options. Other opted-out providers
|
|
255
|
-
// omit the field by default so strict OpenAI-compatible hosts never see an
|
|
256
|
-
// unsupported knob, but a self-hosted gateway that DOES honor the field and keeps
|
|
257
|
-
// emitting parallel calls without it can opt in via pinParallelToolCallsFalse.
|
|
258
|
-
if (provider.baseUrl === "https://integrate.api.nvidia.com/v1"
|
|
259
|
-
|| provider.pinParallelToolCallsFalse === true) {
|
|
260
|
-
body.parallel_tool_calls = false;
|
|
261
|
-
}
|
|
262
|
-
} else if (provider.parallelToolCalls === true) {
|
|
263
|
-
body.parallel_tool_calls = parsed.options.parallelToolCalls !== false;
|
|
264
|
-
}
|
|
224
|
+
const parallelToolCalls = chatParallelToolCallsWireValue(provider, parsed.options.parallelToolCalls);
|
|
225
|
+
if (parallelToolCalls !== undefined) body.parallel_tool_calls = parallelToolCalls;
|
|
265
226
|
}
|
|
266
227
|
if (parsed.stream) body.stream_options = { include_usage: true };
|
|
267
228
|
|
|
@@ -42,6 +42,7 @@ import { bridgeSearchReplayScope } from "../../responses/bridge-search-replay-ca
|
|
|
42
42
|
import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuationEnvelope, normalizeCanonicalForwardPromptEnvelope, stripCanonicalForwardSamplingParams, stripPreviousResponseId, stripStatefulResponsesParams, stripUnsupportedForwardParams } from "./canonical-forward";
|
|
43
43
|
import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
|
|
44
44
|
import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
|
|
45
|
+
import { observeOutbound } from "../../usage/cache-diagnostic";
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Identifies DeepSeek's strict Responses replay contract: tool-bearing continuations need
|
|
@@ -501,6 +502,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
501
502
|
// here, on the serialized body, not on the parsed selector. One place covers both the
|
|
502
503
|
// HTTP and the WebSocket outbound, because the WS path transports this same request
|
|
503
504
|
// instead of rebuilding it.
|
|
505
|
+
observeOutbound(parsed._rawBody, finalBody, headers);
|
|
504
506
|
const body = JSON.stringify(finalBody);
|
|
505
507
|
const releaseBodyObservation = translatorBudget.observeExternallyCapped(
|
|
506
508
|
"passthrough_serialization",
|
package/src/adapters/registry.ts
CHANGED
|
@@ -181,8 +181,9 @@ export function createRegisteredAdapter(
|
|
|
181
181
|
const definition = getAdapterDefinition(provider.adapter);
|
|
182
182
|
if (!definition) throw new Error(`Unknown adapter: ${provider.adapter}`);
|
|
183
183
|
const adapter = definition.create(provider, context);
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
const wire = effectiveAdapterContract(provider.adapter).wire;
|
|
185
|
+
if (wire !== "openai-responses") {
|
|
186
|
+
withInputMediaGuard(adapter, wire);
|
|
186
187
|
}
|
|
187
188
|
const buildRequest = adapter.buildRequest.bind(adapter);
|
|
188
189
|
adapter.buildRequest = (parsed, incoming) => {
|
|
@@ -6,11 +6,107 @@ export const PREFLIGHT_HEARTBEAT_RETAIN_LIMIT = 16;
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Coalescing threshold for adjacent text/thinking deltas buffered with no
|
|
9
|
-
* waiting reader (UTF-16 code units). This is a merge-size ceiling
|
|
10
|
-
*
|
|
9
|
+
* waiting reader (UTF-16 code units). This is a merge-size ceiling; the two
|
|
10
|
+
* retention budgets below are what bound memory.
|
|
11
11
|
*/
|
|
12
12
|
export const COALESCE_MAX_CHUNK_LENGTH = 64 * 1024;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Retained-string budget for the WHOLE queue, in UTF-16 code units.
|
|
16
|
+
*
|
|
17
|
+
* This bounds what the queue is holding at one moment, not how much a turn
|
|
18
|
+
* streams: every dequeue gives its charge back, so a long healthy stream with a
|
|
19
|
+
* consumer attached never accumulates and is never capped by total length.
|
|
20
|
+
*
|
|
21
|
+
* 32 MiB is deliberately far above any single legitimate burst. A synchronous
|
|
22
|
+
* producer can legally fill the queue before its consumer is scheduled — the
|
|
23
|
+
* image loop does exactly that with over a million one-character deltas, which
|
|
24
|
+
* coalesce into roughly 1.2 MB of retained text — so a budget near that size
|
|
25
|
+
* aborts healthy turns rather than stalled ones. The previous effective bound
|
|
26
|
+
* was the 1024-event cap times the 64 KiB merge ceiling, so 64 MiB; this halves
|
|
27
|
+
* it while leaving that legitimate burst an order of magnitude of headroom.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_MAX_BACKLOG_CODE_UNITS = 32 * 1024 * 1024;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Retained-string budget for ONE queued event, in UTF-16 code units.
|
|
33
|
+
*
|
|
34
|
+
* Separate from the aggregate on purpose, because the two describe different
|
|
35
|
+
* failures. Passing the aggregate means the consumer is not keeping up. Passing
|
|
36
|
+
* this one means a single event is malformed or unbounded, which stays true
|
|
37
|
+
* however empty the queue is, so it must be refused even with the whole
|
|
38
|
+
* aggregate free. They also report different terminal messages, so an operator
|
|
39
|
+
* reading the turn's error learns which happened.
|
|
40
|
+
*
|
|
41
|
+
* Like the aggregate, this governs what the queue RETAINS. An event handed
|
|
42
|
+
* straight to a waiting consumer is never held here, so neither budget applies
|
|
43
|
+
* to it: refusing it would abort a turn over memory this queue does not own,
|
|
44
|
+
* and the consumer's own per-event bound governs that payload instead.
|
|
45
|
+
*/
|
|
46
|
+
export const DEFAULT_MAX_EVENT_CODE_UNITS = 8 * 1024 * 1024;
|
|
47
|
+
|
|
48
|
+
const BACKLOG_EXCEEDED_MESSAGE = "consumer stalled: adapter event backlog exceeded — turn aborted";
|
|
49
|
+
const EVENT_TOO_LARGE_MESSAGE = "adapter event exceeds the single-event retained-string budget — turn aborted";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Bound on how far the retention measure walks into one event. AdapterEvent is
|
|
53
|
+
* a plain-data union, but two of its members carry open provider-shaped bags
|
|
54
|
+
* (`providerState`, `usage.rawUsage`) whose depth no type here controls. The
|
|
55
|
+
* ceilings keep a single push O(1)-ish rather than O(whatever an adapter
|
|
56
|
+
* attached), and under-counting a pathological object is the safe direction:
|
|
57
|
+
* the event-count cap still bounds how many of them can be retained.
|
|
58
|
+
*/
|
|
59
|
+
const RETENTION_MAX_DEPTH = 8;
|
|
60
|
+
const RETENTION_MAX_NODES = 4096;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Retained UTF-16 code units carried by one event's string payload.
|
|
64
|
+
*
|
|
65
|
+
* Measured by walking own enumerable properties rather than by naming each
|
|
66
|
+
* variant's string fields: a hand-written per-variant table is exhaustive over
|
|
67
|
+
* a union, so adding an event type on one branch while a consumer lands on
|
|
68
|
+
* another produces a measure that silently stops counting the new payload.
|
|
69
|
+
* The walk is the derived answer and needs no update when the union grows.
|
|
70
|
+
*
|
|
71
|
+
* `type` is skipped because it is the discriminant, identical for every event
|
|
72
|
+
* of a kind and not payload anyone is buffering.
|
|
73
|
+
*/
|
|
74
|
+
export function retainedEventCodeUnits(event: AdapterEvent): number {
|
|
75
|
+
// Defensive: this measures values an adapter produced. A malformed emission
|
|
76
|
+
// has to become a terminal event, not a TypeError thrown out of push() with
|
|
77
|
+
// the queue half-updated.
|
|
78
|
+
if (!event || typeof event !== "object") return 0;
|
|
79
|
+
let total = 0;
|
|
80
|
+
let nodes = 0;
|
|
81
|
+
const seen = new Set<object>();
|
|
82
|
+
const visit = (value: unknown, depth: number): void => {
|
|
83
|
+
if (typeof value === "string") {
|
|
84
|
+
total += value.length;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!value || typeof value !== "object" || depth >= RETENTION_MAX_DEPTH || seen.has(value)) return;
|
|
88
|
+
seen.add(value);
|
|
89
|
+
for (const nested of Object.values(value)) {
|
|
90
|
+
if (nodes++ >= RETENTION_MAX_NODES) return;
|
|
91
|
+
visit(nested, depth + 1);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
for (const [key, value] of Object.entries(event)) {
|
|
95
|
+
if (key === "type") continue;
|
|
96
|
+
if (nodes++ >= RETENTION_MAX_NODES) break;
|
|
97
|
+
visit(value, 1);
|
|
98
|
+
}
|
|
99
|
+
return total;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function positiveBudget(value: number | undefined, fallback: number, name: string): number {
|
|
103
|
+
const resolved = value ?? fallback;
|
|
104
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
105
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
106
|
+
}
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
|
|
14
110
|
export interface AdapterEventQueue {
|
|
15
111
|
/**
|
|
16
112
|
* Returns true when the event was merged into the buffered tail instead of
|
|
@@ -22,6 +118,13 @@ export interface AdapterEventQueue {
|
|
|
22
118
|
close(): void;
|
|
23
119
|
stream(): AsyncIterable<AdapterEvent>;
|
|
24
120
|
collect(): Promise<AdapterEvent[]>;
|
|
121
|
+
/**
|
|
122
|
+
* Retained string payload the queue is currently holding, in UTF-16 code
|
|
123
|
+
* units. Exposed so a caller — and a regression — can assert the counter
|
|
124
|
+
* returns to zero on every terminal path instead of inferring it from an
|
|
125
|
+
* abort that happened to fire.
|
|
126
|
+
*/
|
|
127
|
+
retainedCodeUnits(): number;
|
|
25
128
|
}
|
|
26
129
|
|
|
27
130
|
export interface AdapterEventPreflight {
|
|
@@ -73,47 +176,74 @@ export async function preflightAdapterEvents(
|
|
|
73
176
|
|
|
74
177
|
export function createAdapterEventQueue(opts?: {
|
|
75
178
|
maxBacklog?: number;
|
|
179
|
+
maxBacklogCodeUnits?: number;
|
|
180
|
+
maxEventCodeUnits?: number;
|
|
76
181
|
onBacklogExceeded?: () => void;
|
|
77
182
|
}): AdapterEventQueue {
|
|
78
183
|
const queued: AdapterEvent[] = [];
|
|
184
|
+
/**
|
|
185
|
+
* What each queued item was charged, in lockstep with `queued`. Releasing the
|
|
186
|
+
* recorded charge rather than re-measuring is what makes the accounting exact
|
|
187
|
+
* on every path: a merge, a terminal record admitted past the budget and a
|
|
188
|
+
* plain event all give back precisely what they took, so the counter cannot
|
|
189
|
+
* drift positive (a leak) or negative (a budget the next turn gets for free).
|
|
190
|
+
*/
|
|
191
|
+
const charged: number[] = [];
|
|
79
192
|
const readers: QueueReader[] = [];
|
|
80
193
|
const maxBacklog = opts?.maxBacklog ?? 1_024;
|
|
194
|
+
const maxBacklogCodeUnits = positiveBudget(opts?.maxBacklogCodeUnits, DEFAULT_MAX_BACKLOG_CODE_UNITS, "maxBacklogCodeUnits");
|
|
195
|
+
const maxEventCodeUnits = positiveBudget(opts?.maxEventCodeUnits, DEFAULT_MAX_EVENT_CODE_UNITS, "maxEventCodeUnits");
|
|
196
|
+
let retained = 0;
|
|
81
197
|
let closed = false;
|
|
82
198
|
|
|
83
199
|
// Merge an incoming delta into the buffered tail when no reader is waiting.
|
|
84
|
-
// The
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
200
|
+
// The event cap counts events, not tokens, so a detached or briefly stalled
|
|
201
|
+
// consumer (e.g. a Codex app mid-reconnect whose disconnect Bun has not yet
|
|
202
|
+
// delivered) used to hit it within seconds of token-granular streaming and
|
|
203
|
+
// abort a healthy turn. Adjacent same-phase text deltas, adjacent thinking
|
|
204
|
+
// deltas, and consecutive heartbeats carry no ordering information between
|
|
205
|
+
// themselves, so merging them preserves every consumer contract while making
|
|
206
|
+
// the cap approximate buffered items again.
|
|
91
207
|
// Pushed objects may be retained by adapters, so the tail is REPLACED with
|
|
92
|
-
// a fresh object — never mutated (alias safety).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!tail) return false;
|
|
208
|
+
// a fresh object — never mutated (alias safety). Returning the replacement
|
|
209
|
+
// instead of installing it lets push price the merge before committing to it.
|
|
210
|
+
const planTailMerge = (tail: AdapterEvent, event: AdapterEvent): AdapterEvent | null => {
|
|
96
211
|
if (event.type === "heartbeat") {
|
|
97
|
-
if (tail.type !== "heartbeat") return
|
|
212
|
+
if (tail.type !== "heartbeat") return null;
|
|
98
213
|
// Heartbeats carry no ordering between themselves, but the replay-unsafe
|
|
99
214
|
// marker is not ordering — it is a latch. Dropping the incoming event
|
|
100
215
|
// would discard the only record that Cursor already performed a local
|
|
101
216
|
// side effect, and preflight would then permit an OAuth replay of it.
|
|
102
217
|
if (event.replayUnsafe === true && tail.replayUnsafe !== true) {
|
|
103
|
-
|
|
218
|
+
return { type: "heartbeat", replayUnsafe: true };
|
|
104
219
|
}
|
|
105
|
-
return
|
|
220
|
+
return tail;
|
|
106
221
|
}
|
|
107
222
|
if (event.type === "text_delta" && tail.type === "text_delta" && tail.phase === event.phase) {
|
|
108
|
-
if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return
|
|
109
|
-
|
|
110
|
-
return true;
|
|
223
|
+
if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return null;
|
|
224
|
+
return { type: "text_delta", text: tail.text + event.text, phase: tail.phase };
|
|
111
225
|
}
|
|
112
226
|
if (event.type === "thinking_delta" && tail.type === "thinking_delta") {
|
|
113
|
-
if (tail.thinking.length + event.thinking.length > COALESCE_MAX_CHUNK_LENGTH) return
|
|
114
|
-
|
|
115
|
-
return true;
|
|
227
|
+
if (tail.thinking.length + event.thinking.length > COALESCE_MAX_CHUNK_LENGTH) return null;
|
|
228
|
+
return { type: "thinking_delta", thinking: tail.thinking + event.thinking };
|
|
116
229
|
}
|
|
230
|
+
return null;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Record why the turn is ending and close. The terminal error is admitted
|
|
235
|
+
* past both budgets — refusing to retain the explanation of a refusal would
|
|
236
|
+
* leave the consumer with a silent truncation — but it is charged like any
|
|
237
|
+
* other item so the counter stays exact through the final drain.
|
|
238
|
+
*/
|
|
239
|
+
const abortWith = (message: string): false => {
|
|
240
|
+
opts?.onBacklogExceeded?.();
|
|
241
|
+
const terminal: AdapterEvent = { type: "error", message };
|
|
242
|
+
const cost = retainedEventCodeUnits(terminal);
|
|
243
|
+
queued.push(terminal);
|
|
244
|
+
charged.push(cost);
|
|
245
|
+
retained += cost;
|
|
246
|
+
close();
|
|
117
247
|
return false;
|
|
118
248
|
};
|
|
119
249
|
|
|
@@ -121,17 +251,35 @@ export function createAdapterEventQueue(opts?: {
|
|
|
121
251
|
if (closed) return false;
|
|
122
252
|
const reader = readers.shift();
|
|
123
253
|
if (reader) {
|
|
254
|
+
// Handed straight to a waiting consumer, so the queue retains nothing and
|
|
255
|
+
// charges nothing. Neither budget applies to an event it never holds.
|
|
124
256
|
reader({ done: false, value: event });
|
|
125
257
|
return false;
|
|
126
258
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
259
|
+
const tail = queued[queued.length - 1];
|
|
260
|
+
const merged = tail ? planTailMerge(tail, event) : null;
|
|
261
|
+
if (merged && tail) {
|
|
262
|
+
const replacement = retainedEventCodeUnits(merged);
|
|
263
|
+
if (replacement > maxEventCodeUnits) return abortWith(EVENT_TOO_LARGE_MESSAGE);
|
|
264
|
+
// Charge only what the backlog actually gains. A merge keeps the tail's
|
|
265
|
+
// own fields, so the incoming event's duplicated phase is never retained
|
|
266
|
+
// twice and an unchanged tail costs nothing at all.
|
|
267
|
+
const delta = replacement - charged[charged.length - 1]!;
|
|
268
|
+
if (delta > maxBacklogCodeUnits - retained) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
|
|
269
|
+
queued[queued.length - 1] = merged;
|
|
270
|
+
charged[charged.length - 1] = replacement;
|
|
271
|
+
retained += delta;
|
|
272
|
+
return true;
|
|
133
273
|
}
|
|
274
|
+
const cost = retainedEventCodeUnits(event);
|
|
275
|
+
if (cost > maxEventCodeUnits) return abortWith(EVENT_TOO_LARGE_MESSAGE);
|
|
276
|
+
// Both refusals are priced before anything is retained, so an event that is
|
|
277
|
+
// turned away is never charged for.
|
|
278
|
+
if (queued.length >= maxBacklog) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
|
|
279
|
+
if (cost > maxBacklogCodeUnits - retained) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
|
|
134
280
|
queued.push(event);
|
|
281
|
+
charged.push(cost);
|
|
282
|
+
retained += cost;
|
|
135
283
|
return false;
|
|
136
284
|
};
|
|
137
285
|
|
|
@@ -147,6 +295,7 @@ export function createAdapterEventQueue(opts?: {
|
|
|
147
295
|
while (true) {
|
|
148
296
|
const next = queued.shift();
|
|
149
297
|
if (next) {
|
|
298
|
+
retained -= charged.shift() ?? 0;
|
|
150
299
|
yield next;
|
|
151
300
|
continue;
|
|
152
301
|
}
|
|
@@ -165,5 +314,5 @@ export function createAdapterEventQueue(opts?: {
|
|
|
165
314
|
return events;
|
|
166
315
|
};
|
|
167
316
|
|
|
168
|
-
return { push, close, stream, collect };
|
|
317
|
+
return { push, close, stream, collect, retainedCodeUnits: () => retained };
|
|
169
318
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
|
+
import { debugProviderDiagnostic } from "../lib/debug";
|
|
2
3
|
import { isXaiResponsesDestination } from "../providers/xai-transport";
|
|
3
4
|
|
|
4
5
|
const CODEX_WEB_SEARCH_TOOL = "web_search";
|
|
@@ -192,7 +193,21 @@ export function normalizeXaiResponsesWebSearch(
|
|
|
192
193
|
if (inputChanged) next = { ...next, input };
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
const normalized = normalizeToolChoice(next);
|
|
197
|
+
const choice = normalized.tool_choice;
|
|
198
|
+
if ((choice === "auto" || choice === "none") && !hasAnyDeclaredTool(normalized)) {
|
|
199
|
+
debugProviderDiagnostic("xai", "tool-choice-omitted", { choice });
|
|
200
|
+
const { tool_choice: _toolChoice, ...rest } = normalized;
|
|
201
|
+
// `auto` selects from the catalog, so a catalog with nothing in it makes it meaningless and
|
|
202
|
+
// the omission says nothing the request did not already say. `none` is the opposite: it is a
|
|
203
|
+
// prohibition, and on a request whose catalog this normalizer just emptied it is the only
|
|
204
|
+
// place the turn's client-call boundary is written down. Downstream repair reads that
|
|
205
|
+
// boundary off the final outbound body, so omitting the word alone would hand back a call the
|
|
206
|
+
// caller ruled out. Restate it as the explicit empty catalog, which carries the same deny-all
|
|
207
|
+
// and which this destination already receives whenever a caller sends one itself.
|
|
208
|
+
return choice === "none" && !Array.isArray(rest.tools) ? { ...rest, tools: [] } : rest;
|
|
209
|
+
}
|
|
210
|
+
return normalized;
|
|
196
211
|
}
|
|
197
212
|
|
|
198
213
|
function isLiveWebSearchTool(tool: unknown): boolean {
|
package/src/bridge/errors.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
applyReplayRefusalClientHeaders,
|
|
2
3
|
isNonReplayableUpstreamCode,
|
|
3
4
|
isReplayRefusalCode,
|
|
4
|
-
markReplayRefusalResponse,
|
|
5
5
|
markResponseNonReplayable,
|
|
6
6
|
REPLAY_REFUSED_STATUS,
|
|
7
|
+
retainReplayRefusal,
|
|
7
8
|
} from "../lib/upstream-retry";
|
|
8
9
|
import {
|
|
9
10
|
adapterFailureFromMessage,
|
|
@@ -45,6 +46,11 @@ export function formatErrorResponse(
|
|
|
45
46
|
&& retryAfter.length <= 128) {
|
|
46
47
|
headers.set("Retry-After", retryAfter);
|
|
47
48
|
}
|
|
49
|
+
// The refusal's client policy, restated here for the same reason its status is: this
|
|
50
|
+
// formatter is the last thing several adapter and combo paths touch before the client,
|
|
51
|
+
// and no wait of its own does not stop a client that retries every 429 by default.
|
|
52
|
+
const refusal = isReplayRefusalCode(error.code) && replayBlocked;
|
|
53
|
+
if (refusal) applyReplayRefusalClientHeaders(headers);
|
|
48
54
|
const response = new Response(JSON.stringify({ error }), {
|
|
49
55
|
status: finalStatus,
|
|
50
56
|
headers,
|
|
@@ -53,6 +59,6 @@ export function formatErrorResponse(
|
|
|
53
59
|
// Re-wrapping is where the refusal loses its provenance: combo failure consumption parses
|
|
54
60
|
// the JSON and builds a new Response, and the code alone does not tell a later quota
|
|
55
61
|
// recorder that no upstream produced this status. Carry the narrower marker across too.
|
|
56
|
-
if (
|
|
62
|
+
if (refusal) retainReplayRefusal(response);
|
|
57
63
|
return response;
|
|
58
64
|
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
OcxUsage,
|
|
8
8
|
} from "../types";
|
|
9
9
|
import { coerceIntegerToolArguments } from "../lib/tool-argument-integers";
|
|
10
|
+
import { attemptDeliveryRecorder } from "../usage/attempt-delivery";
|
|
10
11
|
import {
|
|
11
12
|
adapterFailureFromMessage,
|
|
12
13
|
classifyError,
|
|
@@ -51,7 +52,14 @@ export function buildResponseJSON(
|
|
|
51
52
|
): Record<string, unknown> {
|
|
52
53
|
// Default-budget safety net: a caller that omits the budget gets a bounded
|
|
53
54
|
// default (disposed with the call), never the unbounded append path.
|
|
54
|
-
if (options?.translatorBudget)
|
|
55
|
+
if (options?.translatorBudget) {
|
|
56
|
+
const body = buildResponseJSONWithBudget(events, modelId, options);
|
|
57
|
+
// A buffered turn delivers its whole answer as one body, so nothing calls the per-frame
|
|
58
|
+
// recorder on the SSE bridge. Without this the attempt would persist adapter events with
|
|
59
|
+
// zero relayed ones, which is the loss signal -- raised on every non-streaming request.
|
|
60
|
+
attemptDeliveryRecorder(options.translatorBudget)?.noteBufferedDelivery(body);
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
55
63
|
const budget = createTranslatorBudget();
|
|
56
64
|
try {
|
|
57
65
|
return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
|