@bitkyc08/opencodex 2.84.0 → 2.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -22,6 +22,8 @@ import type { RequestLogContext } from "../request-log";
|
|
|
22
22
|
import type { UpstreamHostAdmissionLease } from "../../codex/upstream-host-health";
|
|
23
23
|
import type { AccountLease } from "../../oauth/kiro-account-load";
|
|
24
24
|
import type { PolicyRequestScope } from "./policy-request-scope";
|
|
25
|
+
import type { OcxParsedRequest } from "../../types";
|
|
26
|
+
import type { PolicyRequestEvidence } from "../../routing/evaluator";
|
|
25
27
|
|
|
26
28
|
export interface ConsumedComboFailure {
|
|
27
29
|
response: Response;
|
|
@@ -61,6 +63,32 @@ export interface ClientEncoderOption {
|
|
|
61
63
|
stream: boolean;
|
|
62
64
|
model: string;
|
|
63
65
|
inputTokenFloor?: number;
|
|
66
|
+
/**
|
|
67
|
+
* `direct` when the request side also skipped the internal Responses body (the Antigravity
|
|
68
|
+
* direct lane), so the attempt's request path is traced as `[inbound, ir, upstream]`.
|
|
69
|
+
*/
|
|
70
|
+
requestLane?: "bridge" | "direct";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The Antigravity direct lane (`protocols.rollout.antigravityDirect`). The ingress sends the core
|
|
75
|
+
* a Responses body that carries only the request settings; the conversation is decoded straight
|
|
76
|
+
* from the client body. Request preparation applies `decode` at the parse seam, reads routing
|
|
77
|
+
* evidence from `evidence`, and calls `restoreBridgeConversation` if the final route is not the
|
|
78
|
+
* one the lane was decided for, so every `_rawBody` reader of another adapter sees the full
|
|
79
|
+
* bridge body.
|
|
80
|
+
*/
|
|
81
|
+
export interface DirectRequestOption {
|
|
82
|
+
protocol: "chat" | "messages";
|
|
83
|
+
/** The concrete route the ingress decided the lane for. */
|
|
84
|
+
providerName: string;
|
|
85
|
+
modelId: string;
|
|
86
|
+
/** Policy-routing evidence from the client body; the settings-only body has no `input`. */
|
|
87
|
+
evidence: PolicyRequestEvidence;
|
|
88
|
+
/** The settings-only parse with the directly decoded conversation in place of its empty one. */
|
|
89
|
+
decode(base: OcxParsedRequest): OcxParsedRequest;
|
|
90
|
+
/** The bridge projection's conversation fields (`input`, `instructions`, `tools`). */
|
|
91
|
+
bridgeConversation(): Record<string, unknown>;
|
|
64
92
|
}
|
|
65
93
|
|
|
66
94
|
export interface HandleResponsesOptions {
|
|
@@ -137,6 +165,13 @@ export interface HandleResponsesOptions {
|
|
|
137
165
|
* it. Omitted means a genuine Responses inbound.
|
|
138
166
|
*/
|
|
139
167
|
inboundWire?: InboundWire;
|
|
168
|
+
/**
|
|
169
|
+
* Set by a translating ingress (Chat / Anthropic) when the client sent an explicit empty
|
|
170
|
+
* tool catalog. Those wires may defer tool delivery, so their translated body carries no
|
|
171
|
+
* readable catalog boundary; an explicit `[]` is still a deny-all declaration and the
|
|
172
|
+
* undeclared-tool guard enforces it. Absent catalogs and partial catalogs stay deferred.
|
|
173
|
+
*/
|
|
174
|
+
clientDeclaredEmptyToolCatalog?: boolean;
|
|
140
175
|
nativeReasoningReplay?: ReadonlyMap<string, string>;
|
|
141
176
|
nativeReasoningMint?: { owner?: NativeReasoningOwner };
|
|
142
177
|
/** Droid's per-request effort default; each concrete combo or policy target applies it only if its ladder allows it. */
|
|
@@ -202,6 +237,15 @@ export interface HandleResponsesOptions {
|
|
|
202
237
|
* rebuilds headers and carries the fact through this flag.
|
|
203
238
|
*/
|
|
204
239
|
visionDescribeTerminal?: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Terminal advisor marker: true when the inbound request IS the advisor sidecar's own loopback
|
|
242
|
+
* consultation. The advisor planner then never plans a consultation — a depth cap of 1 that
|
|
243
|
+
* keeps Worker → Advisor → Advisor recursion impossible (same structure as
|
|
244
|
+
* {@linkcode visionDescribeTerminal}). The Chat surface detects the raw
|
|
245
|
+
* `x-opencodex-advisor-internal` header before its bridge rebuilds headers and carries the
|
|
246
|
+
* fact through this flag.
|
|
247
|
+
*/
|
|
248
|
+
advisorInternal?: boolean;
|
|
205
249
|
/**
|
|
206
250
|
* Set only by the Chat and Messages ingresses when `protocols.rollout.directEncoders` is on and
|
|
207
251
|
* the settled route is one non-Responses target. Adapter delivery then encodes the adapter
|
|
@@ -210,6 +254,8 @@ export interface HandleResponsesOptions {
|
|
|
210
254
|
* returning a Responses body, which the ingress converts as before.
|
|
211
255
|
*/
|
|
212
256
|
clientEncoder?: ClientEncoderOption;
|
|
257
|
+
/** Set only by the Chat or Messages ingress on an eligible Antigravity route; see {@link DirectRequestOption}. */
|
|
258
|
+
directRequest?: DirectRequestOption;
|
|
213
259
|
}
|
|
214
260
|
|
|
215
261
|
/** Values shared by the call, not a bag of mutable pipeline state. */
|
|
@@ -125,11 +125,13 @@ export function bindRouteReasoningReplayScope(args: {
|
|
|
125
125
|
if (owner) parsed._nativeReasoningBoundOwner = owner;
|
|
126
126
|
else delete parsed._nativeReasoningBoundOwner;
|
|
127
127
|
if (parsed._nativeReasoningReplay?.size) {
|
|
128
|
+
const rejected = new Set<string>();
|
|
128
129
|
for (const [blob, tag] of parsed._nativeReasoningReplay) {
|
|
129
130
|
if (!nativeReasoningTagMatches(nativeReasoningTag(owner, blob), tag)) {
|
|
130
|
-
|
|
131
|
+
rejected.add(blob);
|
|
131
132
|
}
|
|
132
133
|
}
|
|
134
|
+
if (rejected.size) stripNativeReasoningBlobs(parsed, rejected);
|
|
133
135
|
}
|
|
134
136
|
const continuationDestinationIdentity = providerContinuationDestinationIdentity(parsed, provider);
|
|
135
137
|
const continuationOwner = providerContinuationOwnerFromReplayIdentity(
|
|
@@ -169,11 +171,11 @@ export function bindRouteReasoningReplayScope(args: {
|
|
|
169
171
|
bindProviderContinuationForRoute(parsed, continuationOwner);
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
function
|
|
174
|
+
function stripNativeReasoningBlobs(parsed: OcxParsedRequest, rejected: ReadonlySet<string>): void {
|
|
173
175
|
const input = (parsed._rawBody as { input?: unknown } | undefined)?.input;
|
|
174
176
|
if (Array.isArray(input)) {
|
|
175
177
|
for (const item of input) {
|
|
176
|
-
if (item && typeof item === "object" && item.type === "reasoning" && item.encrypted_content
|
|
178
|
+
if (item && typeof item === "object" && item.type === "reasoning" && rejected.has(item.encrypted_content)) {
|
|
177
179
|
delete item.encrypted_content;
|
|
178
180
|
delete item.id;
|
|
179
181
|
}
|
|
@@ -187,7 +189,8 @@ function stripNativeReasoningBlob(parsed: OcxParsedRequest, blob: string): void
|
|
|
187
189
|
const item: unknown = JSON.parse(part.signature);
|
|
188
190
|
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
189
191
|
const reasoning = item as Record<string, unknown>;
|
|
190
|
-
if (reasoning.type !== "reasoning" || reasoning.encrypted_content !==
|
|
192
|
+
if (reasoning.type !== "reasoning" || typeof reasoning.encrypted_content !== "string"
|
|
193
|
+
|| !rejected.has(reasoning.encrypted_content)) continue;
|
|
191
194
|
delete reasoning.encrypted_content;
|
|
192
195
|
delete reasoning.id;
|
|
193
196
|
part.signature = JSON.stringify(reasoning);
|
|
@@ -48,7 +48,7 @@ export async function handleResponses(
|
|
|
48
48
|
abortSignal.addEventListener("abort", cancel, { once: true });
|
|
49
49
|
try {
|
|
50
50
|
const response = await runWithCompactionRecovery(req, config, logCtx, {
|
|
51
|
-
...options,
|
|
51
|
+
...options, abortSignal,
|
|
52
52
|
openAiSidecarAuth: options.openAiSidecarAuth === undefined
|
|
53
53
|
? captureExplicitOpenAiCallerAuth(req.headers, config) : options.openAiSidecarAuth,
|
|
54
54
|
nativeCallerAuth: options.nativeCallerAuth === undefined
|
|
@@ -108,6 +108,7 @@ import { createResponsesFunctionToolRepairBlockRewrite } from "../responses-func
|
|
|
108
108
|
import {
|
|
109
109
|
createUndeclaredToolCallGuardBlockRewrite,
|
|
110
110
|
currentTurnWireToolCatalogBody,
|
|
111
|
+
UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
111
112
|
undeclaredToolCallNameInResponse,
|
|
112
113
|
undeclaredToolCallMessage,
|
|
113
114
|
normalizeDefaultNamespaceInJson,
|
|
@@ -437,8 +438,7 @@ export async function deliverPassthroughResponse(
|
|
|
437
438
|
normalizeFunctionCompletionJson,
|
|
438
439
|
} = nativeExchange;
|
|
439
440
|
const { commitReasoningReplayServingRoute, recordTerminalOutcomes } = responseEffects;
|
|
440
|
-
const { parsed, subagentQuotaFailureModel, clientRequestedStream, translatorBudget
|
|
441
|
-
const enforceDeclaredToolNames = inboundWire !== "chat" && inboundWire !== "anthropic";
|
|
441
|
+
const { parsed, subagentQuotaFailureModel, clientRequestedStream, translatorBudget } = requestState;
|
|
442
442
|
const { openAiSidecar } = sidecarState;
|
|
443
443
|
const { requestBindings } = transportState;
|
|
444
444
|
|
|
@@ -857,14 +857,17 @@ export async function deliverPassthroughResponse(
|
|
|
857
857
|
: undefined,
|
|
858
858
|
// Last: every rewrite above can still rename or reshape a call item, so the guard must
|
|
859
859
|
// compare the names the client will actually receive against the declared catalog.
|
|
860
|
-
|
|
860
|
+
// Deferred-catalog wires keep the rewrite for its normalization half only: a provider's
|
|
861
|
+
// `default.X` call still collapses to the declared `X`, but an undeclared name is the
|
|
862
|
+
// client's own deferred registry's business, not this proxy's verdict.
|
|
863
|
+
nativeExchange.undeclaredToolGuardActive || declaredWireToolNames.size > 0
|
|
861
864
|
? createUndeclaredToolCallGuardBlockRewrite(
|
|
862
865
|
declaredWireToolNames,
|
|
863
866
|
declaredNamelessClientCallTypes,
|
|
864
867
|
providerExecutedCallTypes,
|
|
865
868
|
declaredBareWireToolNames,
|
|
866
869
|
recoverableBareCustomWireToolNames,
|
|
867
|
-
|
|
870
|
+
{ refuseUndeclared: nativeExchange.undeclaredToolGuardActive },
|
|
868
871
|
)
|
|
869
872
|
: undefined,
|
|
870
873
|
grokUpstreamEchoEnabled
|
|
@@ -1322,10 +1325,7 @@ export async function deliverPassthroughResponse(
|
|
|
1322
1325
|
if (grokUpstreamEchoEnabled) {
|
|
1323
1326
|
clientJson = stripGrokUpstreamEnvelopeEchoFromResponsesJson(clientJson);
|
|
1324
1327
|
}
|
|
1325
|
-
//
|
|
1326
|
-
// the reframed-SSE branch below are built from this body, so one check covers them. This
|
|
1327
|
-
// runs BEFORE the continuation cache write below: a refused turn must not become state a
|
|
1328
|
-
// later `previous_response_id` replay can expand from.
|
|
1328
|
+
// Both response formats use this body, before a refused turn can enter continuation state.
|
|
1329
1329
|
if (nativeExchange.undeclaredToolGuardActive) {
|
|
1330
1330
|
const undeclared = (() => {
|
|
1331
1331
|
try {
|
|
@@ -1341,9 +1341,15 @@ export async function deliverPassthroughResponse(
|
|
|
1341
1341
|
return undefined;
|
|
1342
1342
|
}
|
|
1343
1343
|
})();
|
|
1344
|
-
if (
|
|
1345
|
-
return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared)
|
|
1344
|
+
if (undeclared !== undefined) {
|
|
1345
|
+
return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared), {
|
|
1346
|
+
code: UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
1347
|
+
});
|
|
1346
1348
|
}
|
|
1349
|
+
}
|
|
1350
|
+
// Same split as the SSE rewrite: normalization applies whenever a declared name exists,
|
|
1351
|
+
// even on wires whose deferred catalog stands the refusal down.
|
|
1352
|
+
if (declaredWireToolNames.size > 0) {
|
|
1347
1353
|
clientJson = normalizeDefaultNamespaceInJson(
|
|
1348
1354
|
clientJson,
|
|
1349
1355
|
declaredWireToolNames,
|
|
@@ -21,6 +21,7 @@ import { imageGenToolCallAliases } from "../responses-image-gen-repair";
|
|
|
21
21
|
import { rememberResponseState, isBodyNonPersistable } from "../../responses/state";
|
|
22
22
|
import {
|
|
23
23
|
currentTurnWireToolCatalogBody,
|
|
24
|
+
hasExplicitEmptyToolCatalog,
|
|
24
25
|
hasExplicitWireToolCatalog,
|
|
25
26
|
collectDeclaredWireToolNames,
|
|
26
27
|
collectDeclaredBareWireToolNames,
|
|
@@ -187,6 +188,7 @@ export async function preparePassthroughExchange(
|
|
|
187
188
|
| "selectedForwardHeaders"
|
|
188
189
|
| "clientRequestedStream"
|
|
189
190
|
| "inboundWire"
|
|
191
|
+
| "clientDeclaredEmptyToolCatalog"
|
|
190
192
|
| "substituteMainCredential"
|
|
191
193
|
| "callerAuthHeaders"
|
|
192
194
|
| "subagentFallbackAccountId"
|
|
@@ -253,6 +255,7 @@ export async function preparePassthroughExchange(
|
|
|
253
255
|
responseStateOptions,
|
|
254
256
|
clientRequestedStream,
|
|
255
257
|
inboundWire,
|
|
258
|
+
clientDeclaredEmptyToolCatalog,
|
|
256
259
|
substituteMainCredential,
|
|
257
260
|
callerAuthHeaders,
|
|
258
261
|
} = requestState;
|
|
@@ -531,10 +534,18 @@ export async function preparePassthroughExchange(
|
|
|
531
534
|
declaredWireToolNames.add(name);
|
|
532
535
|
}
|
|
533
536
|
}
|
|
537
|
+
// Chat and Anthropic inbounds may deliver tools deferred/partially: their client registry
|
|
538
|
+
// owns the full catalog, so this guard cannot read the translated body as the authorization
|
|
539
|
+
// boundary. An explicit empty catalog — flagged by the translating ingress or present
|
|
540
|
+
// verbatim in a replayed body — is still a deny-all the caller declared; everything else
|
|
541
|
+
// stays deferred to the caller's own tool handling.
|
|
534
542
|
undeclaredToolGuardActive = (
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
543
|
+
inboundWire === "responses"
|
|
544
|
+
? declaredWireToolNames.size > 0
|
|
545
|
+
|| clientDeclaredNamelessCallTypes.size > 0
|
|
546
|
+
|| clientExplicitWireToolCatalog
|
|
547
|
+
: clientDeclaredEmptyToolCatalog
|
|
548
|
+
|| hasExplicitEmptyToolCatalog(clientToolAuthorizationBody)
|
|
538
549
|
) && route.provider.authMode !== "forward";
|
|
539
550
|
options.nativeControl?.configureToolAuthorization?.(
|
|
540
551
|
undeclaredToolGuardActive,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { ResponsesRequestContext, ResponsesAdmissionState, ResponsesDispatchers } from "./core-options";
|
|
1
|
+
import type { HandleResponsesOptions, ResponsesRequestContext, ResponsesAdmissionState, ResponsesDispatchers } from "./core-options";
|
|
2
|
+
import type { PolicyRequestEvidence } from "../../routing/evaluator";
|
|
3
|
+
import type { RequestLogContext } from "../request-log";
|
|
4
|
+
import { markProtocolEntry, protocolMarkFeatures } from "../../protocols/trace";
|
|
5
|
+
import { isAntigravityRoute } from "../antigravity-direct-eligibility";
|
|
2
6
|
import { capturePolicyRequestRoute, assertPolicyNormalizedRoute, recordPolicyPreparedDestination, policyCandidateRefusalResponse } from "./policy-request-scope";
|
|
3
7
|
import {
|
|
4
8
|
agentTaskRecoveryConfig,
|
|
@@ -155,6 +159,48 @@ import {
|
|
|
155
159
|
|
|
156
160
|
import { redactHostedImageDisplayPaths } from "../responses-hosted-image-display";
|
|
157
161
|
|
|
162
|
+
/** Policy-routing evidence. The direct lane's settings-only body has no conversation to scan. */
|
|
163
|
+
function routeEvidence(options: Pick<HandleResponsesOptions, "directRequest">, parsed: OcxParsedRequest): PolicyRequestEvidence {
|
|
164
|
+
return options.directRequest?.evidence ?? evidenceFromBody(parsed._rawBody);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Leave the Antigravity direct lane for this request: put the bridge conversation back on the
|
|
169
|
+
* Responses body so every `_rawBody` reader sees what the bridge would have sent, and trace the
|
|
170
|
+
* request as the bridge. The IR conversation already decoded stays: it equals what
|
|
171
|
+
* `parseRequest` builds from the restored body (`tests/protocols/chat-ir-parity.test.ts`).
|
|
172
|
+
*/
|
|
173
|
+
function restoreDirectRequestConversation(options: HandleResponsesOptions, body: unknown, logCtx: RequestLogContext): void {
|
|
174
|
+
const direct = options.directRequest;
|
|
175
|
+
if (!direct) return;
|
|
176
|
+
options.directRequest = undefined;
|
|
177
|
+
if (body && typeof body === "object" && !Array.isArray(body)) Object.assign(body, direct.bridgeConversation());
|
|
178
|
+
if (options.clientEncoder) options.clientEncoder = { ...options.clientEncoder, requestLane: "bridge" };
|
|
179
|
+
markProtocolEntry(logCtx, {
|
|
180
|
+
inbound: direct.protocol,
|
|
181
|
+
lane: "bridge",
|
|
182
|
+
reasonCodes: ["not-migrated"],
|
|
183
|
+
features: protocolMarkFeatures(logCtx) ?? [],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Leave the direct lane as soon as the route stops being the Antigravity route it was decided
|
|
189
|
+
* for. Called after every route assignment, so readers that run between a reroute and dispatch
|
|
190
|
+
* (canonical sanitization, ciphertext stripping, final normalization) see the bridge body.
|
|
191
|
+
*/
|
|
192
|
+
function leaveDirectLaneIfRerouted(
|
|
193
|
+
options: HandleResponsesOptions,
|
|
194
|
+
route: Pick<RouteResult, "providerName" | "modelId" | "provider">,
|
|
195
|
+
parsed: OcxParsedRequest,
|
|
196
|
+
logCtx: RequestLogContext,
|
|
197
|
+
): void {
|
|
198
|
+
const direct = options.directRequest;
|
|
199
|
+
if (!direct) return;
|
|
200
|
+
if (route.providerName === direct.providerName && route.modelId === direct.modelId && isAntigravityRoute(route)) return;
|
|
201
|
+
restoreDirectRequestConversation(options, parsed._rawBody, logCtx);
|
|
202
|
+
}
|
|
203
|
+
|
|
158
204
|
/** Parses, selects, and admits one request without changing the dispatch policy. */
|
|
159
205
|
export async function prepareResponsesRequest(
|
|
160
206
|
requestContext: Pick<ResponsesRequestContext, "options" | "config" | "req" | "logCtx">,
|
|
@@ -306,6 +352,9 @@ export async function prepareResponsesRequest(
|
|
|
306
352
|
}
|
|
307
353
|
if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
|
|
308
354
|
options.onRequestBodyRead?.();
|
|
355
|
+
// The Antigravity direct lane is decided for one concrete route; a body that reached a combo
|
|
356
|
+
// goes through the combo's own child loop with the full bridge conversation.
|
|
357
|
+
restoreDirectRequestConversation(options, body, logCtx);
|
|
309
358
|
return requestDispatchers.handleComboResponses(req, body, comboId, config, logCtx, {
|
|
310
359
|
...options,
|
|
311
360
|
// Concrete combo child selectors no longer match the shadow source model. Carry the
|
|
@@ -383,6 +432,10 @@ export async function prepareResponsesRequest(
|
|
|
383
432
|
let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
|
|
384
433
|
try {
|
|
385
434
|
parsed = parseRequest(body);
|
|
435
|
+
// Antigravity direct lane: the body carried only the request settings, so the parse above
|
|
436
|
+
// produced them with an empty conversation; the conversation itself comes straight from the
|
|
437
|
+
// client body. Inside this try so a decode error answers exactly like a parse error.
|
|
438
|
+
if (options.directRequest) parsed = options.directRequest.decode(parsed);
|
|
386
439
|
if (options.inboundWire === "anthropic") {
|
|
387
440
|
parsed._nativeReasoningReplay = options.nativeReasoningReplay;
|
|
388
441
|
parsed._nativeReasoningMint = options.nativeReasoningMint;
|
|
@@ -582,8 +635,8 @@ export async function prepareResponsesRequest(
|
|
|
582
635
|
const resolved = concreteSelection
|
|
583
636
|
? routeConcreteModel(config, modelId)
|
|
584
637
|
: parsed._compactionRequest === true
|
|
585
|
-
? routeCompactionModel(config, modelId,
|
|
586
|
-
: routeModel(config, modelId,
|
|
638
|
+
? routeCompactionModel(config, modelId, routeEvidence(options, parsed))
|
|
639
|
+
: routeModel(config, modelId, routeEvidence(options, parsed));
|
|
587
640
|
// A Pool 2 selector may leave Pool 2 only through an operator-configured cross-provider
|
|
588
641
|
// blocked-model redirect, which the router marks as a credential-domain rewrite. Any other
|
|
589
642
|
// destination means the selector fell through to default resolution, which stays refused.
|
|
@@ -654,6 +707,7 @@ export async function prepareResponsesRequest(
|
|
|
654
707
|
if (parsed._compactionRequest === true || options.compactionRoutingOverride) parsed._cursorIsolateConversation = true;
|
|
655
708
|
route = memoryRoute ?? shadowRoute ?? resolveRoute(options.policyFallbackCandidate
|
|
656
709
|
? `${options.policyFallbackCandidate.provider}/${options.policyFallbackCandidate.model}` : parsed.modelId);
|
|
710
|
+
leaveDirectLaneIfRerouted(options, route, parsed, logCtx);
|
|
657
711
|
// Name the phase in the persisted route decision, so the request log says why this turn went to
|
|
658
712
|
// the memory destination instead of leaving it looking like a plain user selection. Set here, on
|
|
659
713
|
// the resolved route, so a combo child's own route carries it too.
|
|
@@ -893,8 +947,9 @@ export async function prepareResponsesRequest(
|
|
|
893
947
|
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
894
948
|
try {
|
|
895
949
|
route = captureInboundRoutePolicy(
|
|
896
|
-
routeModel(config, fallback.to,
|
|
950
|
+
routeModel(config, fallback.to, routeEvidence(options, parsed)),
|
|
897
951
|
);
|
|
952
|
+
leaveDirectLaneIfRerouted(options, route, parsed, logCtx);
|
|
898
953
|
credentialDomainWasRewritten = true;
|
|
899
954
|
logCtx.routeDecision = route.routeDecision;
|
|
900
955
|
logCtx.policyEligibility = route.policyEligibility;
|
|
@@ -1109,8 +1164,9 @@ export async function prepareResponsesRequest(
|
|
|
1109
1164
|
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
1110
1165
|
try {
|
|
1111
1166
|
route = captureInboundRoutePolicy(
|
|
1112
|
-
routeModel(config, fallback.to,
|
|
1167
|
+
routeModel(config, fallback.to, routeEvidence(options, parsed)),
|
|
1113
1168
|
);
|
|
1169
|
+
leaveDirectLaneIfRerouted(options, route, parsed, logCtx);
|
|
1114
1170
|
credentialDomainWasRewritten = true;
|
|
1115
1171
|
logCtx.routeDecision = route.routeDecision;
|
|
1116
1172
|
logCtx.policyEligibility = route.policyEligibility;
|
|
@@ -1257,6 +1313,8 @@ export async function prepareResponsesRequest(
|
|
|
1257
1313
|
// for streaming clients.
|
|
1258
1314
|
const clientRequestedStream = parsed.stream;
|
|
1259
1315
|
const admittedPolicyRoute = { providerName: route.providerName, modelId: route.modelId };
|
|
1316
|
+
// Final guard before normalization may inject guidance into the raw body.
|
|
1317
|
+
leaveDirectLaneIfRerouted(options, route, parsed, logCtx);
|
|
1260
1318
|
await applyFinalRouteRequestNormalization({
|
|
1261
1319
|
parsed,
|
|
1262
1320
|
route,
|
|
@@ -1452,6 +1510,11 @@ export async function prepareResponsesRequest(
|
|
|
1452
1510
|
commitSkillsSnapshot?.();
|
|
1453
1511
|
return {
|
|
1454
1512
|
inboundWire,
|
|
1513
|
+
clientDeclaredEmptyToolCatalog: options.clientDeclaredEmptyToolCatalog === true,
|
|
1514
|
+
// Admission facts later route rewrites must not re-derive: the selector the caller sent and
|
|
1515
|
+
// the key scope as it stood when this request was admitted.
|
|
1516
|
+
inboundSelector,
|
|
1517
|
+
admissionScope,
|
|
1455
1518
|
translatorBudget,
|
|
1456
1519
|
parsed,
|
|
1457
1520
|
toolBridgeMaps,
|
|
@@ -56,6 +56,11 @@ import { stampOAuthAccountLabel, usesApiKeyAccount } from "../../providers/label
|
|
|
56
56
|
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
57
57
|
import { captureRouteStaticPolicy } from "../../router";
|
|
58
58
|
import { CopilotAutoHttpError, resolveCopilotAuto } from "../../providers/github-copilot-auto";
|
|
59
|
+
import {
|
|
60
|
+
AdmissionModelDeniedError,
|
|
61
|
+
admissionModelDeniedResponse,
|
|
62
|
+
assertRouteAllowedByScope,
|
|
63
|
+
} from "../admission-model-scope";
|
|
59
64
|
import { renameRoutedIdentityInContext } from "../../adapters/identity";
|
|
60
65
|
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
61
66
|
import {
|
|
@@ -109,6 +114,8 @@ export async function prepareResponsesTransport(
|
|
|
109
114
|
| "inboundWire"
|
|
110
115
|
| "selectedForwardHeaders"
|
|
111
116
|
| "translatorBudget"
|
|
117
|
+
| "inboundSelector"
|
|
118
|
+
| "admissionScope"
|
|
112
119
|
>,
|
|
113
120
|
) {
|
|
114
121
|
const { config, logCtx, options, req } = requestContext;
|
|
@@ -297,6 +304,7 @@ export async function prepareResponsesTransport(
|
|
|
297
304
|
const copilotRequestedModel = route.modelId;
|
|
298
305
|
/** Project only bounded Copilot refusal metadata; upstream bodies and session tokens never reach the client. */
|
|
299
306
|
const copilotRefusalResponse = (error: unknown): Response | undefined => {
|
|
307
|
+
if (error instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(error);
|
|
300
308
|
if (!(error instanceof CopilotAutoHttpError) || ![401, 403, 429].includes(error.status)) return;
|
|
301
309
|
const response = formatErrorResponse(error.status, error.status === 429 ? "rate_limit_error" : "authentication_error",
|
|
302
310
|
error.message);
|
|
@@ -357,6 +365,13 @@ export async function prepareResponsesTransport(
|
|
|
357
365
|
replayOAuthCredentialSnapshot = { accountId: refreshed.accountId, generation: refreshed.generation };
|
|
358
366
|
selected = await negotiate();
|
|
359
367
|
}
|
|
368
|
+
assertRouteAllowedByScope(
|
|
369
|
+
// The scope captured at admission: a key edited or removed while negotiation was in
|
|
370
|
+
// flight must not widen this request. The refusal names the caller's own selector.
|
|
371
|
+
requestState.admissionScope,
|
|
372
|
+
requestState.inboundSelector,
|
|
373
|
+
{ providerName: route.providerName, modelId: selected.modelId },
|
|
374
|
+
);
|
|
360
375
|
copilotSessionExpiresAt = selected.expiresAt ?? Infinity;
|
|
361
376
|
route.provider = selected.provider;
|
|
362
377
|
route.modelId = selected.modelId;
|
|
@@ -725,6 +740,7 @@ export async function prepareResponsesTransport(
|
|
|
725
740
|
let nextAdapter: ProviderAdapter;
|
|
726
741
|
try { nextAdapter = await refreshDispatchAdapter(requestParsed); }
|
|
727
742
|
catch (error) {
|
|
743
|
+
if (error instanceof AdmissionModelDeniedError) throw error;
|
|
728
744
|
const refusal = copilotRefusalResponse(error);
|
|
729
745
|
if (refusal) return refusal;
|
|
730
746
|
throw error;
|
|
@@ -23,6 +23,7 @@ import { normalizeLogConversationId } from "../request-log-conversation";
|
|
|
23
23
|
import { normalizeDeclaredToolName, type AdapterEvent, type OcxProviderContinuationState } from "../../types";
|
|
24
24
|
import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE, SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE, type OcxErrorPayload } from "../../lib/errors";
|
|
25
25
|
import { SendBudgetExhaustedError, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
26
|
+
import { hasKeyPoolFailover, rotateProviderTransportOn401, rotateProviderTransportOn429 } from "../../providers/key-failover";
|
|
26
27
|
import {
|
|
27
28
|
hasEligibleGenericOAuthFailoverTarget,
|
|
28
29
|
isGenericOAuthFailoverEnabled,
|
|
@@ -224,6 +225,11 @@ export async function executeResponsesRunTurn(
|
|
|
224
225
|
// in-flight; only the latest dispatch may merge adapter-written route
|
|
225
226
|
// state back onto `parsed`, or the superseded attempt would restore the
|
|
226
227
|
// failed account's cursor/continuation over the rotation's rebind.
|
|
228
|
+
type KeyBinding = Extract<NonNullable<ReturnType<typeof adapterBindings.get>>, { kind: "api-key" }>;
|
|
229
|
+
const failedKeyBindings = new WeakMap<Extract<AdapterEvent, { type: "error" }>, KeyBinding>();
|
|
230
|
+
const failedKeys = new Set<string>();
|
|
231
|
+
const keyAttemptLimit = route.provider.apiKeyPool?.length ?? 0;
|
|
232
|
+
let lastKeyFailure: Extract<AdapterEvent, { type: "error" }> | undefined;
|
|
227
233
|
let runTurnAttemptSeq = 0;
|
|
228
234
|
const runTurnAttempt = async (
|
|
229
235
|
targetQueue: AdapterEventQueue,
|
|
@@ -238,7 +244,9 @@ export async function executeResponsesRunTurn(
|
|
|
238
244
|
const attemptSeq = ++runTurnAttemptSeq;
|
|
239
245
|
if (attemptSeq === 1 && options.comboInitialSend) options.comboInitialSend.producerOwned = true;
|
|
240
246
|
let pacingSlot = preacquiredSlot;
|
|
247
|
+
let sentKeyBinding: KeyBinding | undefined;
|
|
241
248
|
const emit = (event: AdapterEvent) => {
|
|
249
|
+
if (event.type === "error" && sentKeyBinding) failedKeyBindings.set(event, sentKeyBinding);
|
|
242
250
|
options.onCompactionRecoveryAdapterEvent?.(event);
|
|
243
251
|
targetQueue.push(event);
|
|
244
252
|
};
|
|
@@ -249,6 +257,12 @@ export async function executeResponsesRunTurn(
|
|
|
249
257
|
pacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
250
258
|
}
|
|
251
259
|
await refreshRunTurnSelection();
|
|
260
|
+
const selectedBinding = adapterBindings.get(transportState.runTurnAdapter);
|
|
261
|
+
if (lastKeyFailure && selectedBinding?.kind === "api-key"
|
|
262
|
+
&& failedKeys.has(selectedBinding.provider.apiKey ?? "")) {
|
|
263
|
+
emit(lastKeyFailure);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
252
266
|
// LOCAL PATCH (runturn-websearch): refreshRunTurnSelection binds route
|
|
253
267
|
// state onto `parsed`; mirror it onto the iteration-local copy the
|
|
254
268
|
// adapter actually receives.
|
|
@@ -293,13 +307,14 @@ export async function executeResponsesRunTurn(
|
|
|
293
307
|
// The only way the request budget reaches a transport the adapter owns. Without it
|
|
294
308
|
// a Cursor turn's inner ladder was three physical sends the cap read as one.
|
|
295
309
|
...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}),
|
|
296
|
-
onPhysicalSend: send =>
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
310
|
+
onPhysicalSend: send => {
|
|
311
|
+
// Capture once at this attempt's send, before later selection or recovery changes it.
|
|
312
|
+
const binding = adapterBindings.get(transportState.runTurnAdapter);
|
|
313
|
+
if (!sentKeyBinding && binding?.kind === "api-key") sentKeyBinding = binding;
|
|
314
|
+
noteAdapterPhysicalSend(logCtx.usageLogInputTokens,
|
|
315
|
+
{ ...send, ...(send.recovery ?? recovery ? { recovery: send.recovery ?? recovery } : {}) },
|
|
316
|
+
{ includeFirst: reportsOwnSends });
|
|
317
|
+
},
|
|
303
318
|
onRecoveryWithheld: noteAdapterRecoveryWithheld,
|
|
304
319
|
},
|
|
305
320
|
emit,
|
|
@@ -361,6 +376,7 @@ export async function executeResponsesRunTurn(
|
|
|
361
376
|
const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true, wsFirstParsed, initialPacingSlot);
|
|
362
377
|
const runTurnFailoverArmed = () =>
|
|
363
378
|
route.provider.authMode === "oauth"
|
|
379
|
+
|| (transportState.runTurnAdapter.name === "qoder" && hasKeyPoolFailover(route.provider))
|
|
364
380
|
|| !!(transportState.genericFailoverAccountId
|
|
365
381
|
&& isGenericOAuthFailoverEnabled(config, route.providerName));
|
|
366
382
|
const dispatchSearchIteration = (iterParsed: PreparedResponsesRequest["parsed"]): AsyncIterable<AdapterEvent> => {
|
|
@@ -464,9 +480,51 @@ export async function executeResponsesRunTurn(
|
|
|
464
480
|
return false;
|
|
465
481
|
}
|
|
466
482
|
};
|
|
483
|
+
const recoverRunTurnKey = async (
|
|
484
|
+
error: Extract<AdapterEvent, { type: "error" }>,
|
|
485
|
+
): Promise<AttemptRecoveryKind | undefined> => {
|
|
486
|
+
const binding = failedKeyBindings.get(error);
|
|
487
|
+
if (error.apiKeyRecovery !== true || (error.status !== 401 && error.status !== 429)
|
|
488
|
+
|| !binding || runTurnAbort.signal.aborted || !hasKeyPoolFailover(route.provider)
|
|
489
|
+
|| failedKeys.size >= keyAttemptLimit) return undefined;
|
|
490
|
+
const failed = binding.provider;
|
|
491
|
+
if (!failed.apiKey || !failed._apiKeyAttempt || failedKeys.has(failed.apiKey)) return undefined;
|
|
492
|
+
const hop = reserveCredentialHop("auth-recovery", `${route.providerName}|${route.modelId}|runturn-key-${error.status}`);
|
|
493
|
+
if (!hop.allowed) return undefined;
|
|
494
|
+
failedKeys.add(failed.apiKey);
|
|
495
|
+
lastKeyFailure = error;
|
|
496
|
+
try {
|
|
497
|
+
const rotate = error.status === 401 ? rotateProviderTransportOn401 : rotateProviderTransportOn429;
|
|
498
|
+
const rotated = rotate(config, route.providerName, failed, {
|
|
499
|
+
attemptedKey: failed.apiKey, attemptedSelection: failed._apiKeyAttempt,
|
|
500
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
501
|
+
});
|
|
502
|
+
if (!rotated || failedKeys.size >= keyAttemptLimit || failedKeys.has(rotated.apiKey ?? "") || runTurnAbort.signal.aborted) {
|
|
503
|
+
hop.permit?.release();
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
route.provider = rotated;
|
|
507
|
+
const adapter = await refreshRunTurnAdapter(parsed);
|
|
508
|
+
if (!adapter.runTurn || failedKeys.has(route.provider.apiKey ?? "") || runTurnAbort.signal.aborted) {
|
|
509
|
+
hop.permit?.release();
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: route.provider,
|
|
513
|
+
adapterName: adapter.name, forwardHeaders: requestState.selectedForwardHeaders });
|
|
514
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
|
|
515
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, adapter.name);
|
|
516
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
517
|
+
return error.status === 401 ? "key-401" : "key-429";
|
|
518
|
+
} catch {
|
|
519
|
+
hop.permit?.release();
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
467
523
|
const recoverRunTurnAdapterOnPreflightError = async (
|
|
468
524
|
error: Extract<AdapterEvent, { type: "error" }>,
|
|
469
525
|
): Promise<AttemptRecoveryKind | undefined> => {
|
|
526
|
+
const keyRecovery = await recoverRunTurnKey(error);
|
|
527
|
+
if (keyRecovery) return keyRecovery;
|
|
470
528
|
if (await recoverRunTurnAdapterOnPreflight401(error)) return "oauth-401";
|
|
471
529
|
if (await rotateRunTurnAdapterOnPreflight429(error)) return "oauth-account-429";
|
|
472
530
|
return undefined;
|
|
@@ -683,7 +741,11 @@ export async function executeResponsesRunTurn(
|
|
|
683
741
|
const stallTimeoutSec = wsPlan?.stallTimeoutSec ?? config.stallTimeoutSec;
|
|
684
742
|
// A disabled watchdog (0) is not a zero preflight: it would commit SSE before Devin's first
|
|
685
743
|
// event and deliver a pre-output 429 in-stream. Fall back to the default finite bound.
|
|
686
|
-
|
|
744
|
+
// A key-pool preflight must not outlast the stall budget, or a silent CLI holds the SSE
|
|
745
|
+
// Response (heartbeats, stall watchdog) hostage; the OAuth preflight wait is unchanged.
|
|
746
|
+
const keyPoolPreflight = transportState.runTurnAdapter.name === "qoder"
|
|
747
|
+
&& route.provider.authMode !== "oauth" && hasKeyPoolFailover(route.provider);
|
|
748
|
+
const preflightDeadlineAt = grokDevinPreflight || keyPoolPreflight
|
|
687
749
|
? Date.now() + (resolveStallTimeoutSec(stallTimeoutSec) || DEFAULT_STALL_TIMEOUT_SEC) * 1_000
|
|
688
750
|
: undefined;
|
|
689
751
|
if (runTurnFailoverArmed()) {
|