@bitkyc08/opencodex 2.67.0-preview.20260926 → 2.68.0-preview.20260927
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-B3VRRYSj.js +0 -51
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/Tray-TYAA0EQY.js +0 -1
- package/gui/dist/assets/index-9Ju7nHsu.js +0 -86
- package/gui/dist/assets/tray-data-xKfS2gSG.js +0 -1
|
@@ -19,8 +19,10 @@ import {
|
|
|
19
19
|
readResponseStreamWithInactivity,
|
|
20
20
|
ResponseBodyInactivityError,
|
|
21
21
|
} from "../../lib/response-body-inactivity";
|
|
22
|
-
import {
|
|
22
|
+
import { resolveStallTimeoutMs } from "../../stall-timeout";
|
|
23
23
|
import { clientEncoderForDelivery, deliverClientEncodedResponse } from "../inference/client-encoder-delivery";
|
|
24
|
+
import { noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
25
|
+
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
24
26
|
|
|
25
27
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
26
28
|
export async function deliverAdapterResponse(
|
|
@@ -33,7 +35,7 @@ export async function deliverAdapterResponse(
|
|
|
33
35
|
| "rememberKiroDeliveredFinalAnswer"
|
|
34
36
|
| "responseStateOptions"
|
|
35
37
|
>,
|
|
36
|
-
transportState: Pick<ResponsesTransport, "activeAdapter" | "bindKeyUsageFromBridge">,
|
|
38
|
+
transportState: Pick<ResponsesTransport, "activeAdapter" | "bindKeyUsageFromBridge" | "sentOAuthSnapshot">,
|
|
37
39
|
sidecarState: Pick<ResponsesSidecarAuth, "routedCompaction">,
|
|
38
40
|
responseEffects: Pick<
|
|
39
41
|
ResponsesEffects,
|
|
@@ -43,7 +45,7 @@ export async function deliverAdapterResponse(
|
|
|
43
45
|
| "notifyResponseComplete"
|
|
44
46
|
>,
|
|
45
47
|
completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
|
|
46
|
-
adapterExchange: Pick<AdapterExchange, "upstreamResponse" | "upstream" | "cleanupUpstreamAbort">,
|
|
48
|
+
adapterExchange: Pick<AdapterExchange, "upstreamResponse" | "upstream" | "cleanupUpstreamAbort" | "localUpstream">,
|
|
47
49
|
continuationState: Pick<AdapterContinuations, "terminalGuardEnabled" | "fetchTerminalGuardContinuation" | "fetchGuardedEmptyCompletionRetry">,
|
|
48
50
|
): Promise<Response> {
|
|
49
51
|
const { logCtx, options, config } = requestContext;
|
|
@@ -54,7 +56,7 @@ export async function deliverAdapterResponse(
|
|
|
54
56
|
rememberKiroDeliveredFinalAnswer,
|
|
55
57
|
responseStateOptions,
|
|
56
58
|
} = requestState;
|
|
57
|
-
const { upstreamResponse, upstream, cleanupUpstreamAbort } = adapterExchange;
|
|
59
|
+
const { upstreamResponse, upstream, cleanupUpstreamAbort, localUpstream } = adapterExchange;
|
|
58
60
|
const {
|
|
59
61
|
terminalGuardEnabled,
|
|
60
62
|
fetchTerminalGuardContinuation,
|
|
@@ -68,7 +70,7 @@ export async function deliverAdapterResponse(
|
|
|
68
70
|
notifyResponseComplete,
|
|
69
71
|
} = responseEffects;
|
|
70
72
|
const { routedCompaction } = sidecarState;
|
|
71
|
-
const bodyInactivityMs =
|
|
73
|
+
const bodyInactivityMs = resolveStallTimeoutMs(config.stallTimeoutSec, { localUpstream });
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
if (parsed.stream) {
|
|
@@ -76,12 +78,15 @@ export async function deliverAdapterResponse(
|
|
|
76
78
|
// same mapping or the bridge catch reports this upstream timeout as a 500 proxy_error.
|
|
77
79
|
const initialEventStream = (async function* (): AsyncGenerator<AdapterEvent> {
|
|
78
80
|
try {
|
|
79
|
-
|
|
81
|
+
for await (const event of readResponseStreamWithInactivity(
|
|
80
82
|
upstreamResponse,
|
|
81
83
|
upstream.signal,
|
|
82
84
|
bodyInactivityMs,
|
|
83
85
|
response => transportState.activeAdapter.parseStream(response, translatorBudget, logCtx.activeTierMetadata),
|
|
84
|
-
)
|
|
86
|
+
)) {
|
|
87
|
+
options.onCompactionRecoveryAdapterEvent?.(event);
|
|
88
|
+
yield event;
|
|
89
|
+
}
|
|
85
90
|
} catch (error) {
|
|
86
91
|
if (error instanceof ResponseBodyInactivityError) {
|
|
87
92
|
yield {
|
|
@@ -118,6 +123,9 @@ export async function deliverAdapterResponse(
|
|
|
118
123
|
// One completion owner for both deliveries: the bridge calls it from its terminal, the
|
|
119
124
|
// direct client encoder from the fold of the same events.
|
|
120
125
|
const onCompletedResponse = (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
126
|
+
const served = transportState.sentOAuthSnapshot;
|
|
127
|
+
if (transportState.activeAdapter.name === "kiro" && response.status === "completed" && served
|
|
128
|
+
&& noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
|
|
121
129
|
commitReasoningReplayServingRoute();
|
|
122
130
|
rememberKiroDeliveredFinalAnswer(transportState.activeAdapter.name, response);
|
|
123
131
|
// Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
|
|
@@ -148,6 +156,7 @@ export async function deliverAdapterResponse(
|
|
|
148
156
|
toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames,
|
|
149
157
|
},
|
|
150
158
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
159
|
+
localUpstream,
|
|
151
160
|
turnAdmissionLease: options.turnAdmissionLease,
|
|
152
161
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
153
162
|
stopUpstream: () => { cancelResponseCompletion(); upstream.abort(); },
|
|
@@ -164,6 +173,7 @@ export async function deliverAdapterResponse(
|
|
|
164
173
|
replayCacheScope: parsed._reasoningReplayScope,
|
|
165
174
|
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
166
175
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
176
|
+
localUpstream,
|
|
167
177
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
168
178
|
declaredToolNames,
|
|
169
179
|
enforceDeclaredToolNames: options.inboundWire !== "chat" && options.inboundWire !== "anthropic",
|
|
@@ -195,6 +205,7 @@ export async function deliverAdapterResponse(
|
|
|
195
205
|
bodyInactivityMs,
|
|
196
206
|
response => transportState.activeAdapter.parseResponse!(response, translatorBudget, logCtx.activeTierMetadata),
|
|
197
207
|
);
|
|
208
|
+
for (const event of initialEvents) options.onCompactionRecoveryAdapterEvent?.(event);
|
|
198
209
|
let guardedEvents: AdapterEvent[];
|
|
199
210
|
if (terminalGuardEnabled) {
|
|
200
211
|
guardedEvents = [];
|
|
@@ -256,6 +267,9 @@ export async function deliverAdapterResponse(
|
|
|
256
267
|
// #1926 gap 2: same buffered-path durability bound as the primary branch.
|
|
257
268
|
await awaitThoughtSignatureDurability();
|
|
258
269
|
if (adapterResponseReachedServingTerminal(events, json)) {
|
|
270
|
+
const served = transportState.sentOAuthSnapshot;
|
|
271
|
+
if (transportState.activeAdapter.name === "kiro" && json.status === "completed" && served
|
|
272
|
+
&& noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
|
|
259
273
|
commitReasoningReplayServingRoute();
|
|
260
274
|
}
|
|
261
275
|
notifyResponseComplete(json);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
2
|
+
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
2
3
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
3
4
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
4
5
|
import type { ResponsesTransport } from "./request-transport";
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
import { clientCancelledResponse, readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
|
|
19
20
|
import { redactSecretString } from "../../lib/redact";
|
|
20
21
|
import { rewriteUpstreamPolicyRefusal } from "./policy-refusal";
|
|
21
|
-
import {
|
|
22
|
+
import { withProviderRequestSlot } from "../../providers/request-pacing";
|
|
22
23
|
import { providerFetch, fetchWithHeaderTimeout, safeHostLabel } from "./fetch-helpers";
|
|
23
24
|
import {
|
|
24
25
|
transientRetryPolicyFor,
|
|
@@ -41,7 +42,12 @@ import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
|
41
42
|
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
42
43
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
43
44
|
import type { OAuthAccessSnapshot } from "../../oauth";
|
|
44
|
-
import { publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
45
|
+
import { OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
46
|
+
import { tryKiroAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
|
|
47
|
+
import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
|
|
48
|
+
import { normalizeFinalKiroHttpError } from "../../adapters/kiro-retry";
|
|
49
|
+
import { noteKiroMonthlyRefusal } from "../../providers/kiro-usage";
|
|
50
|
+
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
45
51
|
import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
|
|
46
52
|
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
47
53
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
@@ -53,9 +59,10 @@ import {
|
|
|
53
59
|
formatAnthropicProviderForLog,
|
|
54
60
|
} from "../../oauth/anthropic-routing";
|
|
55
61
|
import {
|
|
56
|
-
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
57
62
|
isGenericOAuthFailoverEnabled,
|
|
58
63
|
rotateGenericOAuthAccountOn429,
|
|
64
|
+
rotateGenericOAuthAccountOnRefusal,
|
|
65
|
+
quarantineKiroSuspendedAccount,
|
|
59
66
|
failoverAccountSnapshot,
|
|
60
67
|
} from "../../oauth/generic-account-failover";
|
|
61
68
|
import {
|
|
@@ -123,6 +130,7 @@ export async function prepareAdapterExchange(
|
|
|
123
130
|
| "commitResolvedOAuthSelection"
|
|
124
131
|
| "genericFailoverAccountId"
|
|
125
132
|
| "genericFailovers"
|
|
133
|
+
| "genericFailoverLimit"
|
|
126
134
|
| "applyFailoverSnapshot"
|
|
127
135
|
| "noteRoutedAttemptSend"
|
|
128
136
|
>,
|
|
@@ -184,6 +192,10 @@ export async function prepareAdapterExchange(
|
|
|
184
192
|
const stallTimeoutMs = typeof config.stallTimeoutSec === "number" && Number.isFinite(config.stallTimeoutSec) && config.stallTimeoutSec > 0
|
|
185
193
|
? Math.floor(config.stallTimeoutSec * 1000)
|
|
186
194
|
: 300_000;
|
|
195
|
+
// Where this exchange dials. Local upstreams (loopback / private / `.local` / `.lan`) are
|
|
196
|
+
// operator-trusted and often CPU-bound, so an unset stall budget resolves to disabled for them.
|
|
197
|
+
// Key/OAuth rotation re-resolves credentials, never the origin, so the routed baseUrl is stable.
|
|
198
|
+
const localUpstream = isLocalUpstream(route.provider.baseUrl);
|
|
187
199
|
transportState.activeAdapter = transportState.adapter;
|
|
188
200
|
|
|
189
201
|
// One immutable, body-safe outbound request per same-target sequence (URL, serialized body,
|
|
@@ -291,21 +303,23 @@ export async function prepareAdapterExchange(
|
|
|
291
303
|
try {
|
|
292
304
|
if (transportState.activeAdapter.fetchResponse) {
|
|
293
305
|
transportState.noteRoutedAttemptSend(inputTokenEstimate);
|
|
294
|
-
await
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
upstreamResponse = await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot =>
|
|
307
|
+
transportState.activeAdapter.fetchResponse!(builtInitialRequest, {
|
|
308
|
+
kiroPreferAccountFailover: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro"),
|
|
309
|
+
abortSignal: upstream.signal,
|
|
310
|
+
timeoutMs: connectMs,
|
|
311
|
+
sendBudget: adapterDispatchBudget,
|
|
312
|
+
onPhysicalSend: send => noteAdapterPhysicalSend(inputTokenEstimate, send),
|
|
313
|
+
onRecoveryWithheld: noteAdapterRecoveryWithheld,
|
|
314
|
+
stream: parsed.stream,
|
|
315
|
+
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
316
|
+
pacingSlotAcquired: true,
|
|
317
|
+
pacingSlot,
|
|
318
|
+
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
319
|
+
providerName: route.providerName,
|
|
320
|
+
modelId: route.modelId,
|
|
321
|
+
}),
|
|
322
|
+
}));
|
|
309
323
|
} else {
|
|
310
324
|
// #1851 scope guard: transient-5xx retry on this generic adapter path is opt-in for
|
|
311
325
|
// direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other
|
|
@@ -315,11 +329,18 @@ export async function prepareAdapterExchange(
|
|
|
315
329
|
// legacy direct-Google exception is preserved exactly; every other adapter still keeps
|
|
316
330
|
// reset-only semantics so combo failover hops on the first 5xx.
|
|
317
331
|
const transientPolicy = transientRetryPolicyFor(route.provider);
|
|
332
|
+
const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
|
|
333
|
+
if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
|
|
334
|
+
let compactPrepaidUsed = false;
|
|
318
335
|
const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
|
|
319
336
|
? fetchWithTransientRetry
|
|
320
337
|
: fetchWithResetRetry;
|
|
321
338
|
upstreamResponse = await fetchWithRetryPolicy(
|
|
322
339
|
recovery => {
|
|
340
|
+
if (compactPrepaid && !compactPrepaidUsed) {
|
|
341
|
+
if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
|
|
342
|
+
compactPrepaidUsed = true;
|
|
343
|
+
}
|
|
323
344
|
transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery);
|
|
324
345
|
return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
|
|
325
346
|
method: builtInitialRequest.method,
|
|
@@ -335,12 +356,15 @@ export async function prepareAdapterExchange(
|
|
|
335
356
|
{
|
|
336
357
|
abortSignal: upstream.signal,
|
|
337
358
|
label: safeHostLabel(builtInitialRequest.url),
|
|
338
|
-
...(transientPolicy
|
|
359
|
+
...(transientPolicy || compactPrepaid
|
|
339
360
|
// Draws the remainder, not the raw policy. A combo child inherits the parent's
|
|
340
361
|
// holder but used to take a fresh full allowance on its own first send, so the
|
|
341
362
|
// shared counter was inherited without ever being read as a limit.
|
|
342
363
|
? {
|
|
343
|
-
|
|
364
|
+
// The first emergency send is already paid for. Only retries consume the
|
|
365
|
+
// remaining allowance; treating the booking as unavailable blocks a cap of two.
|
|
366
|
+
attempts: Math.min(transientPolicy?.attempts ?? 1,
|
|
367
|
+
remainingTransientSendBudget(transientPolicy?.attempts ?? 1) + (compactPrepaid ? 1 : 0)),
|
|
344
368
|
onSendsConsumed: noteTransientSends,
|
|
345
369
|
}
|
|
346
370
|
: {}),
|
|
@@ -405,7 +429,9 @@ export async function prepareAdapterExchange(
|
|
|
405
429
|
* a permit confirmed earlier would keep the charge for a send that never happened.
|
|
406
430
|
*/
|
|
407
431
|
onDispatch?: () => void,
|
|
432
|
+
preserveFailureResponse?: Response,
|
|
408
433
|
): Promise<Response | { failed: Response }> => {
|
|
434
|
+
let replacementAdmitted = false;
|
|
409
435
|
// The repair permit books the request ledger, but only the transient helper reports
|
|
410
436
|
// that send to the root workflow. The other two dispatch paths confirm it here, at
|
|
411
437
|
// their physical-send boundary, without booking the request ledger again.
|
|
@@ -430,6 +456,7 @@ export async function prepareAdapterExchange(
|
|
|
430
456
|
recordAdapterReasoning(logCtx, retryRequest);
|
|
431
457
|
recordAdapterTier(logCtx, retryRequest);
|
|
432
458
|
} catch (err) {
|
|
459
|
+
if (preserveFailureResponse && !options.abortSignal?.aborted) return { failed: preserveFailureResponse };
|
|
433
460
|
// A rotated/rebuilt adapter build failure is a request-shaping error, not an
|
|
434
461
|
// upstream connect failure: tear the abort link down and map it as 400 (no 413
|
|
435
462
|
// translator-budget mapping here — that stays with parseRequest/buildToolBridgeMaps).
|
|
@@ -455,28 +482,32 @@ export async function prepareAdapterExchange(
|
|
|
455
482
|
try {
|
|
456
483
|
if (transportState.activeAdapter.fetchResponse) {
|
|
457
484
|
transportState.noteRoutedAttemptSend(retryEstimate, recovery);
|
|
458
|
-
await
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
485
|
+
return await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot => {
|
|
486
|
+
// The dispatch boundary is HERE, not before the pacing wait: that wait can reject for
|
|
487
|
+
// an abort, a saturated queue, an expired slot or a removed provider, and none of
|
|
488
|
+
// those reach the wire. Confirming earlier would hold the charge for a send that the
|
|
489
|
+
// pacer refused.
|
|
490
|
+
onDispatch?.();
|
|
491
|
+
return transportState.activeAdapter.fetchResponse!(retryRequest, {
|
|
492
|
+
kiroPreferAccountFailover: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro"),
|
|
493
|
+
abortSignal: upstream.signal,
|
|
494
|
+
timeoutMs: connectMs,
|
|
495
|
+
sendBudget: adapterDispatchBudget,
|
|
496
|
+
onPhysicalSend: send => {
|
|
497
|
+
if (preserveFailureResponse) replacementAdmitted = true;
|
|
498
|
+
noteAdapterPhysicalSend(retryEstimate, send);
|
|
499
|
+
chargeFastDowngradeWorkflowSend();
|
|
500
|
+
},
|
|
501
|
+
onRecoveryWithheld: noteAdapterRecoveryWithheld,
|
|
502
|
+
stream: parsed.stream,
|
|
503
|
+
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
504
|
+
pacingSlotAcquired: true,
|
|
505
|
+
pacingSlot,
|
|
506
|
+
dispatchOverride: oauthDispatch(retryRequest),
|
|
507
|
+
providerName: route.providerName,
|
|
508
|
+
modelId: route.modelId,
|
|
509
|
+
}),
|
|
510
|
+
});
|
|
480
511
|
});
|
|
481
512
|
}
|
|
482
513
|
// #2643 review: this leg used to call fetchWithHeaderTimeout directly, so an
|
|
@@ -509,6 +540,7 @@ export async function prepareAdapterExchange(
|
|
|
509
540
|
// Same boundary on the helper path: the thunk is what reaches the wire, and it
|
|
510
541
|
// can be refused above before it does. use() past the first attempt is a no-op.
|
|
511
542
|
onDispatch?.();
|
|
543
|
+
if (preserveFailureResponse) replacementAdmitted = true;
|
|
512
544
|
if (!refetchTransientPolicy) chargeFastDowngradeWorkflowSend();
|
|
513
545
|
return fetchWithHeaderTimeout(retryRequest.url,
|
|
514
546
|
applyUpstreamRecoveryInit({
|
|
@@ -540,6 +572,8 @@ export async function prepareAdapterExchange(
|
|
|
540
572
|
retryRequest.releaseBodyObservation?.();
|
|
541
573
|
}
|
|
542
574
|
} catch (err) {
|
|
575
|
+
if (preserveFailureResponse && !replacementAdmitted && !options.abortSignal?.aborted)
|
|
576
|
+
return { failed: preserveFailureResponse };
|
|
543
577
|
cleanupUpstreamAbort();
|
|
544
578
|
upstream.abort();
|
|
545
579
|
if (options.abortSignal?.aborted) {
|
|
@@ -575,6 +609,46 @@ export async function prepareAdapterExchange(
|
|
|
575
609
|
try {
|
|
576
610
|
refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
|
|
577
611
|
} catch (err) {
|
|
612
|
+
const failed = transportState.sentOAuthSnapshot;
|
|
613
|
+
if (route.providerName === "kiro" && err instanceof OAuthLoginRequiredError && failed
|
|
614
|
+
&& transportState.genericFailovers < transportState.genericFailoverLimit) {
|
|
615
|
+
const alternate = await tryKiroAlternateAfterTerminalRefresh(config, failed.accountId, failed.generation);
|
|
616
|
+
if (alternate) {
|
|
617
|
+
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
618
|
+
const hop = reserveCredentialHop("auth-recovery",
|
|
619
|
+
`${route.providerName}|${route.modelId}|terminal-refresh-account`,
|
|
620
|
+
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null);
|
|
621
|
+
if (hop.allowed) {
|
|
622
|
+
try {
|
|
623
|
+
const admitted = await applyFailoverSnapshot(alternate);
|
|
624
|
+
if (admitted?.accountId === alternate.accountId) {
|
|
625
|
+
transportState.genericFailovers += 1;
|
|
626
|
+
invalidateSameTargetRequest();
|
|
627
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
628
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
629
|
+
config.cacheRetention);
|
|
630
|
+
bindRouteReasoningReplayScope({
|
|
631
|
+
parsed, providerName: route.providerName, provider: route.provider,
|
|
632
|
+
adapterName: transportState.activeAdapter.name,
|
|
633
|
+
oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot,
|
|
634
|
+
});
|
|
635
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider,
|
|
636
|
+
transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
637
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName,
|
|
638
|
+
route.provider, transportState.activeAdapter.name);
|
|
639
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
640
|
+
const result = await rebuildAndRefetch("oauth-account-429", () => {
|
|
641
|
+
if (!adapterOwnsDispatch) hop.permit?.use();
|
|
642
|
+
});
|
|
643
|
+
if ("failed" in result) return result.failed;
|
|
644
|
+
upstreamResponse = result;
|
|
645
|
+
continue recovery;
|
|
646
|
+
}
|
|
647
|
+
} catch { /* Keep the original public authentication error. */ }
|
|
648
|
+
finally { sendBudgetState.pendingHopPermit = undefined; hop.permit?.release(); }
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
578
652
|
cleanupUpstreamAbort();
|
|
579
653
|
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
|
|
580
654
|
}
|
|
@@ -840,18 +914,123 @@ export async function prepareAdapterExchange(
|
|
|
840
914
|
break;
|
|
841
915
|
}
|
|
842
916
|
}
|
|
843
|
-
// Generic OAuth account failover (#2568)
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
917
|
+
// Generic OAuth account failover (#2568) rotates reactively after a refusal when
|
|
918
|
+
// two accounts are stored. Kiro additionally classifies bounded 400/403 refusals;
|
|
919
|
+
// all other providers retain the original 429 loop below.
|
|
920
|
+
if (route.providerName === "kiro") {
|
|
921
|
+
while (
|
|
922
|
+
(upstreamResponse.status === 429 || upstreamResponse.status === 400 || upstreamResponse.status === 403)
|
|
923
|
+
&& transportState.genericFailoverAccountId
|
|
924
|
+
) {
|
|
925
|
+
const refusal = classifyKiroRefusal(upstreamResponse.status,
|
|
926
|
+
await readDisplaySafeErrorText(upstreamResponse.clone(), upstream.signal, ""));
|
|
927
|
+
if (refusal.kind === "other") break;
|
|
928
|
+
const sent = transportState.sentOAuthSnapshot;
|
|
929
|
+
const monthlyCooldownMs = refusal.kind === "monthly_quota" && sent
|
|
930
|
+
? noteKiroMonthlyRefusal(sent.accountId, sent.generation, Date.now()) : undefined;
|
|
931
|
+
if (monthlyCooldownMs !== undefined) persistKiroAccountState();
|
|
932
|
+
if (refusal.kind === "suspended")
|
|
933
|
+
quarantineKiroSuspendedAccount(transportState.genericFailoverAccountId, sent?.generation);
|
|
934
|
+
if (transportState.genericFailovers >= transportState.genericFailoverLimit
|
|
935
|
+
|| !isGenericOAuthFailoverEnabled(config, "kiro")) break;
|
|
936
|
+
// Intersection with the shared request budget. This arm re-sends through
|
|
937
|
+
// rebuildAndRefetch, so the roster cap alone would let one request walk the roster on
|
|
938
|
+
// an allowance the rest of the request cannot see. A refusal ends the ladder with the
|
|
939
|
+
// original HTTP response already in hand.
|
|
940
|
+
//
|
|
941
|
+
// Who settles this reservation depends on who dispatches the replay (#4709). An
|
|
942
|
+
// adapter that owns its ladder -- Kiro's reset loop, Cursor's transport loop --
|
|
943
|
+
// reserves once per physical send and would charge the same replay again; the helper
|
|
944
|
+
// path reports it again through `onSendsConsumed`. Both turned one physical send into
|
|
945
|
+
// two charges, and once the allowance was spent, into a synthetic error in place of
|
|
946
|
+
// the refusal this hop was recovering from. The wire protocol is resolved from the
|
|
947
|
+
// provider and model, not from the account, so an account rotation cannot move the
|
|
948
|
+
// replay between these two shapes.
|
|
949
|
+
const adapterOwnsDispatch = transportState.activeAdapter.fetchResponse !== undefined;
|
|
950
|
+
const hop = reserveCredentialHop(
|
|
951
|
+
"auth-recovery",
|
|
952
|
+
`${route.providerName}|${route.modelId}|adapter-recovery-oauth-429`,
|
|
953
|
+
// Only a helper-routed replay reports this send back. A reset-only refetch reports
|
|
954
|
+
// nothing and an adapter ladder settles the booking itself, so promising an external
|
|
955
|
+
// report on either would leave a booking pending until it swallowed a later charge.
|
|
956
|
+
!adapterOwnsDispatch && transientRetryPolicyFor(route.provider) !== null,
|
|
957
|
+
);
|
|
958
|
+
if (!hop.allowed) break;
|
|
959
|
+
const nextAccountId = rotateGenericOAuthAccountOnRefusal(
|
|
960
|
+
config,
|
|
961
|
+
route.providerName,
|
|
962
|
+
transportState.genericFailoverAccountId,
|
|
963
|
+
refusal.kind,
|
|
964
|
+
upstreamResponse.headers.get("retry-after"),
|
|
965
|
+
Date.now(),
|
|
966
|
+
route.modelId,
|
|
967
|
+
monthlyCooldownMs,
|
|
968
|
+
);
|
|
969
|
+
if (!nextAccountId) {
|
|
970
|
+
hop.permit?.release();
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
// The FULL snapshot, not just the bearer: Antigravity pairs an account-matched
|
|
975
|
+
// projectId with its token and Kiro carries routing metadata, so a token-only swap
|
|
976
|
+
// would mix one account's credential with another's routing data.
|
|
977
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
978
|
+
if (!await applyFailoverSnapshot(snapshot)) {
|
|
979
|
+
hop.permit?.release();
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
transportState.genericFailovers += 1;
|
|
983
|
+
invalidateSameTargetRequest();
|
|
984
|
+
transportState.activeAdapter = resolveSelectionAdapter(
|
|
985
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
986
|
+
config.cacheRetention,
|
|
987
|
+
);
|
|
988
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
989
|
+
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
990
|
+
// The replay IS this hop's send, so hand the reservation down and let the layer that
|
|
991
|
+
// dispatches settle it: `adapterDispatchBudget` spends it on the adapter's first
|
|
992
|
+
// reservation, and the retry helper's reporter settles the external booking.
|
|
993
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
994
|
+
let result: Response | { failed: Response };
|
|
995
|
+
try {
|
|
996
|
+
// Confirm at the dispatch boundary, not here: a rebuild can fail while shaping the
|
|
997
|
+
// request and return `{ failed }` without reaching the wire, and a permit confirmed
|
|
998
|
+
// before that would hold the charge for a send that never happened. An
|
|
999
|
+
// adapter-owned ladder is the exception -- its own reservation is the confirmation,
|
|
1000
|
+
// and settling here first would hand it a dead permit, which it reads as an
|
|
1001
|
+
// exhausted request and stops sending on.
|
|
1002
|
+
result = await rebuildAndRefetch("oauth-account-429", () => {
|
|
1003
|
+
if (!adapterOwnsDispatch) hop.permit?.use();
|
|
1004
|
+
}, upstreamResponse);
|
|
1005
|
+
} finally {
|
|
1006
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
1007
|
+
}
|
|
1008
|
+
if ("failed" in result) {
|
|
1009
|
+
// A no-op if the boundary was reached; a refund if the rebuild died before it.
|
|
1010
|
+
hop.permit?.release();
|
|
1011
|
+
if (result.failed !== upstreamResponse) return result.failed;
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1015
|
+
upstreamResponse = result;
|
|
1016
|
+
// The hop's permit is already settled by the dispatch boundary above; continuing
|
|
1017
|
+
// only skips the remaining arms, it does not abandon a reservation.
|
|
1018
|
+
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
1019
|
+
} catch {
|
|
1020
|
+
// A throw before the send — snapshot fetch, credential application, adapter
|
|
1021
|
+
// resolution — must hand the reservation back. Without this the ladder charges the
|
|
1022
|
+
// request for a send it never made, and a later recovery in the same request is
|
|
1023
|
+
// refused on an allowance nothing spent. release() is idempotent and a no-op once
|
|
1024
|
+
// used, so a throw from the rebuild keeps its charge.
|
|
1025
|
+
hop.permit?.release();
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
851
1030
|
while (
|
|
852
1031
|
upstreamResponse.status === 429
|
|
853
1032
|
&& transportState.genericFailoverAccountId
|
|
854
|
-
&& transportState.genericFailovers <
|
|
1033
|
+
&& transportState.genericFailovers < transportState.genericFailoverLimit
|
|
855
1034
|
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
856
1035
|
) {
|
|
857
1036
|
// Intersection with the shared request budget. This arm re-sends through
|
|
@@ -944,6 +1123,7 @@ export async function prepareAdapterExchange(
|
|
|
944
1123
|
break;
|
|
945
1124
|
}
|
|
946
1125
|
}
|
|
1126
|
+
}
|
|
947
1127
|
// Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
|
|
948
1128
|
// or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
|
|
949
1129
|
// the missing authoritative signal. Rebuild once through the same sanitation path used by a
|
|
@@ -1042,6 +1222,8 @@ export async function prepareAdapterExchange(
|
|
|
1042
1222
|
break;
|
|
1043
1223
|
}
|
|
1044
1224
|
if (!upstreamResponse.ok) {
|
|
1225
|
+
if (route.providerName === "kiro")
|
|
1226
|
+
upstreamResponse = await normalizeFinalKiroHttpError(upstreamResponse, upstream.signal);
|
|
1045
1227
|
if (options.comboAttempt) {
|
|
1046
1228
|
// No pre-read guard: `consumeComboFailure` -> `readBoundedResponseBody` reads
|
|
1047
1229
|
// `response.body` itself with the abort signal threaded through, and the combo
|
|
@@ -1097,6 +1279,11 @@ export async function prepareAdapterExchange(
|
|
|
1097
1279
|
// material before it reaches the client-facing error surface.
|
|
1098
1280
|
const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
|
|
1099
1281
|
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
1282
|
+
options.onCompactionRecoveryAdapterEvent?.({
|
|
1283
|
+
type: "error", status: upstreamResponse.status,
|
|
1284
|
+
errorType: normalized.type, code: normalized.code,
|
|
1285
|
+
message: "Structured upstream failure observed before client formatting",
|
|
1286
|
+
});
|
|
1100
1287
|
const message = normalized.cyberPolicy
|
|
1101
1288
|
? normalized.message
|
|
1102
1289
|
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
@@ -1141,6 +1328,7 @@ export async function prepareAdapterExchange(
|
|
|
1141
1328
|
cleanupUpstreamAbort,
|
|
1142
1329
|
connectMs,
|
|
1143
1330
|
stallTimeoutMs,
|
|
1331
|
+
localUpstream,
|
|
1144
1332
|
upstreamResponse,
|
|
1145
1333
|
rateLimitPolicy,
|
|
1146
1334
|
get rateLimitRetries(): typeof rateLimitRetries {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHmac, randomBytes } from "node:crypto";
|
|
2
2
|
import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
|
|
3
|
-
import { CODEX_RESPONSES_HTTP_URL } from "./codex-ws-request";
|
|
3
|
+
import { CODEX_RESPONSES_HTTP_URL, CODEX_WS_FRAME_HEADERS } from "./codex-ws-request";
|
|
4
4
|
import { CODEX_WS_ID_MAX_BYTES } from "./codex-ws-correlation";
|
|
5
5
|
import { CodexWsSession } from "./codex-ws-session";
|
|
6
6
|
|
|
7
7
|
export const CODEX_WS_POOL_MAX_SESSIONS = 32;
|
|
8
8
|
export const CODEX_WS_POOL_IDLE_MS = 30_000;
|
|
9
9
|
export const CODEX_WS_POOL_MAX_AGE_MS = 5 * 60_000;
|
|
10
|
-
const MUTABLE_HEADERS = new Set(
|
|
10
|
+
const MUTABLE_HEADERS = new Set<string>(CODEX_WS_FRAME_HEADERS);
|
|
11
11
|
let processKey: Buffer | undefined;
|
|
12
12
|
let poolSequence = 0;
|
|
13
13
|
|
|
@@ -25,7 +25,7 @@ function digest(input: unknown): string {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Identity comes from the selected outgoing request, never a model label or caller hint. */
|
|
28
|
-
export function codexWsReuseIdentity(url: string, headers: Record<string, string>, frameText: string, proxy?: string): CodexWsReuseIdentity | null {
|
|
28
|
+
export function codexWsReuseIdentity(url: string, headers: Record<string, string>, frameText: string, proxy?: string, dialUrl?: string): CodexWsReuseIdentity | null {
|
|
29
29
|
if (url !== CODEX_RESPONSES_HTTP_URL) return null;
|
|
30
30
|
let body: unknown;
|
|
31
31
|
try { body = JSON.parse(frameText); } catch { return null; }
|
|
@@ -52,7 +52,8 @@ export function codexWsReuseIdentity(url: string, headers: Record<string, string
|
|
|
52
52
|
const scope = digest([url, account, thread, turn]);
|
|
53
53
|
const lite = metadata.ws_request_header_x_openai_internal_codex_responses_lite;
|
|
54
54
|
if (lite !== undefined && lite !== "true" && lite !== "false") return null;
|
|
55
|
-
|
|
55
|
+
// `dialUrl` is the socket destination after plugin rewrites; absent means the canonical one.
|
|
56
|
+
return { scope, key: digest([scope, authorization, body.model, body.service_tier ?? null, lite ?? null, immutable, proxy ?? null, dialUrl ?? null]) };
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
interface Entry { identity: CodexWsReuseIdentity; session: CodexWsSession; createdAt: number; idleAt: number; retired: boolean }
|
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
CODEX_RESPONSES_LITE_METADATA_KEY,
|
|
5
5
|
} from "../../codex/forward-transport-headers";
|
|
6
6
|
|
|
7
|
+
/** Per-turn headers the WebSocket carries in `client_metadata` of each frame, not in the upgrade. */
|
|
8
|
+
export const CODEX_WS_FRAME_HEADERS = ["x-codex-turn-state", "x-codex-turn-metadata"] as const;
|
|
9
|
+
|
|
7
10
|
export const CODEX_RESPONSES_HTTP_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
8
11
|
export const CODEX_RESPONSES_WS_URL = "wss://chatgpt.com/backend-api/codex/responses";
|
|
9
12
|
export const WS_BETA = "responses_websockets=2026-02-06";
|
|
@@ -32,7 +35,7 @@ function applyLiteMetadata(body: Record<string, unknown>, headers: Headers): boo
|
|
|
32
35
|
body.client_metadata = { ...(metadata as Record<string, string> | undefined),
|
|
33
36
|
[CODEX_RESPONSES_LITE_METADATA_KEY]: lite };
|
|
34
37
|
}
|
|
35
|
-
for (const name of
|
|
38
|
+
for (const name of CODEX_WS_FRAME_HEADERS) {
|
|
36
39
|
const value = headers.get(name);
|
|
37
40
|
const current = body.client_metadata as Record<string, string> | undefined;
|
|
38
41
|
if (value !== null && !Object.hasOwn(current ?? {}, name)) {
|
|
@@ -154,7 +154,8 @@ import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregi
|
|
|
154
154
|
import type { AdmissionLease } from "../../lib/admission";
|
|
155
155
|
import { redactSecretString } from "../../lib/redact";
|
|
156
156
|
import { readBoundedResponseBytes } from "../../lib/bounded-body";
|
|
157
|
-
import {
|
|
157
|
+
import { resolveStallTimeoutMs } from "../../stall-timeout";
|
|
158
|
+
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
158
159
|
import { isRateLimitOrQuotaFailureMessage } from "../../lib/errors";
|
|
159
160
|
import { supportedLadderFor } from "../effort-policy";
|
|
160
161
|
import {
|
|
@@ -562,6 +563,7 @@ export async function bufferCompactResponse(
|
|
|
562
563
|
upstream: Response,
|
|
563
564
|
signal: AbortSignal,
|
|
564
565
|
stallTimeoutSec?: number,
|
|
566
|
+
localUpstream?: boolean,
|
|
565
567
|
): Promise<Response> {
|
|
566
568
|
const headers = compactResponseHeaders(upstream);
|
|
567
569
|
try {
|
|
@@ -581,7 +583,9 @@ export async function bufferCompactResponse(
|
|
|
581
583
|
const result = await readBoundedResponseBytes(upstream, {
|
|
582
584
|
signal,
|
|
583
585
|
maxBytes: COMPACT_RESPONSE_MAX_BYTES,
|
|
584
|
-
|
|
586
|
+
// Compaction buffers the complete body while holding an active-turn lease. Keep its
|
|
587
|
+
// default bounded even for local destinations so silent bodies cannot exhaust that gate.
|
|
588
|
+
inactivityTimeoutMs: resolveStallTimeoutMs(stallTimeoutSec),
|
|
585
589
|
});
|
|
586
590
|
if (signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
587
591
|
if (result.oversized) return compactResponseTooLargeError();
|
|
@@ -1332,7 +1336,7 @@ export async function handleResponsesCompact(
|
|
|
1332
1336
|
upstream.headers.get("x-codex-secondary-reset-at"),
|
|
1333
1337
|
upstream.headers.get("x-codex-tertiary-reset-at"),
|
|
1334
1338
|
].filter(Boolean);
|
|
1335
|
-
const buffered = await bufferCompactResponse(upstream, req.signal, config.stallTimeoutSec);
|
|
1339
|
+
const buffered = await bufferCompactResponse(upstream, req.signal, config.stallTimeoutSec, isLocalUpstream(compactUrl));
|
|
1336
1340
|
const bufferedErrorText = buffered.ok
|
|
1337
1341
|
? ""
|
|
1338
1342
|
: await buffered.clone().text().catch(() => "");
|
|
@@ -1431,7 +1435,7 @@ export async function handleResponsesCompact(
|
|
|
1431
1435
|
// The routed compaction turn is a handoff inside the same logical request, so it draws the
|
|
1432
1436
|
// REMAINDER. Minting here is what let a native attempt spend three sends and the routed
|
|
1433
1437
|
// fallback spend four more.
|
|
1434
|
-
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRoutingOverride: options.compactionRoutingOverride, ...(admission ? { admission } : {}) });
|
|
1438
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRecoveryKind: "compaction-v1", compactionRoutingOverride: options.compactionRoutingOverride, ...(admission ? { admission } : {}) });
|
|
1435
1439
|
if (!response.ok) return response;
|
|
1436
1440
|
let json: { output?: unknown[]; status?: unknown; error?: unknown };
|
|
1437
1441
|
if (response.headers.get("content-type")?.includes("text/event-stream")) {
|