@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
|
@@ -8,10 +8,16 @@ import {
|
|
|
8
8
|
} from "./ws-upstream";
|
|
9
9
|
import type { OcxProviderConfig } from "../../types";
|
|
10
10
|
import type { WsData } from "../ws-bridge";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
requestPacingMaxConcurrentRequests,
|
|
13
|
+
sendTrackingRequestSlot,
|
|
14
|
+
waitForProviderRequestSlot,
|
|
15
|
+
type ProviderRequestSlot,
|
|
16
|
+
} from "../../providers/request-pacing";
|
|
12
17
|
import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
|
|
13
18
|
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
14
19
|
import { configuredOutboundFetch } from "../../lib/proxy-env";
|
|
20
|
+
import { isLoopbackUrl, rewriteUpstream } from "../../plugins/upstream-hooks";
|
|
15
21
|
import {
|
|
16
22
|
describeProviderEgressForLog,
|
|
17
23
|
markEgressTransparentExecutor,
|
|
@@ -27,6 +33,7 @@ export { withUpstreamHttpVersion };
|
|
|
27
33
|
const egressWebsocketDowngradeWarned = new Set<string>();
|
|
28
34
|
/** A provider name is configuration-controlled, so the notice set is bounded like any cache. */
|
|
29
35
|
const EGRESS_DOWNGRADE_NOTICE_LIMIT = 64;
|
|
36
|
+
const pacingWebsocketDowngradeWarned = new Set<string>();
|
|
30
37
|
/**
|
|
31
38
|
* Marks an init whose provider egress route an outer physical-send boundary already decided.
|
|
32
39
|
*
|
|
@@ -34,6 +41,7 @@ const EGRESS_DOWNGRADE_NOTICE_LIMIT = 64;
|
|
|
34
41
|
* `dispatchOverride` performs, and an unknown symbol on a `RequestInit` is inert at the wire.
|
|
35
42
|
*/
|
|
36
43
|
const EGRESS_DECIDED = Symbol.for("opencodex.provider-egress.decided");
|
|
44
|
+
const UPSTREAM_REWRITTEN = Symbol.for("opencodex.plugins.upstream-rewritten");
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
47
|
* Announce once, per provider, that an explicit egress route moved this provider off the
|
|
@@ -61,6 +69,7 @@ function warnEgressWebsocketDowngradeOnce(providerName: string, egress: string):
|
|
|
61
69
|
/** Test seam: the downgrade notice is once per provider per process, not once per request. */
|
|
62
70
|
export function __resetEgressWebsocketDowngradeNotices(): void {
|
|
63
71
|
egressWebsocketDowngradeWarned.clear();
|
|
72
|
+
pacingWebsocketDowngradeWarned.clear();
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
export function disableResponsesRequestTimeout(req: Request, server: Pick<Server<WsData>, "timeout"> | undefined): boolean {
|
|
@@ -121,7 +130,7 @@ export function wantsFreshConnection(
|
|
|
121
130
|
|
|
122
131
|
|
|
123
132
|
export interface PaceAwareFetch {
|
|
124
|
-
waitForPacing?: (signal?: AbortSignal) => Promise<
|
|
133
|
+
waitForPacing?: (signal?: AbortSignal) => Promise<ProviderRequestSlot | undefined>;
|
|
125
134
|
unpacedFetch?: typeof globalThis.fetch;
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -143,11 +152,28 @@ export type ProviderFetch = typeof globalThis.fetch & PaceAwareFetch;
|
|
|
143
152
|
*/
|
|
144
153
|
export function sendWithConnectionPolicy(
|
|
145
154
|
physicalFetch: typeof globalThis.fetch,
|
|
146
|
-
|
|
155
|
+
rawInput: Parameters<typeof globalThis.fetch>[0],
|
|
147
156
|
init?: RequestInit,
|
|
148
157
|
egress?: ProviderEgressBinding,
|
|
149
158
|
): Promise<Response> {
|
|
150
|
-
|
|
159
|
+
let input = rawInput;
|
|
160
|
+
let headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
161
|
+
// Plugin rewrites (src/plugins/upstream-hooks.ts) run here, after the caller chose between
|
|
162
|
+
// the Codex WebSocket and HTTP, and before the connection and egress decisions below so
|
|
163
|
+
// those follow the rewritten destination. Nested passes rewrite once, like the egress mark.
|
|
164
|
+
// A rewrite onto this machine's loopback dials directly: a proxy chosen for the provider
|
|
165
|
+
// (per-provider or HTTP_PROXY) cannot reach a local sidecar. The WebSocket dial does the same.
|
|
166
|
+
const rewriteDone = (init as Record<symbol, unknown> | undefined)?.[UPSTREAM_REWRITTEN] === true;
|
|
167
|
+
let redirectedToLoopback = false;
|
|
168
|
+
if (!rewriteDone) {
|
|
169
|
+
const original = input instanceof Request ? input.url : String(input);
|
|
170
|
+
const target = rewriteUpstream(original, headers, "http");
|
|
171
|
+
headers = target.headers as Headers;
|
|
172
|
+
if (target.url !== original) {
|
|
173
|
+
redirectedToLoopback = isLoopbackUrl(target.url);
|
|
174
|
+
input = input instanceof Request ? new Request(target.url, input) : target.url;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
151
177
|
const fresh = wantsFreshConnection(input);
|
|
152
178
|
if (fresh) {
|
|
153
179
|
headers.set("Connection", "close");
|
|
@@ -161,15 +187,18 @@ export function sendWithConnectionPolicy(
|
|
|
161
187
|
// the reselected provider and the rebuilt destination, so it decides and marks the init; the
|
|
162
188
|
// inner pass honours that mark rather than recomputing from a stale closure.
|
|
163
189
|
const alreadyDecided = (init as Record<symbol, unknown> | undefined)?.[EGRESS_DECIDED] === true;
|
|
164
|
-
const decide = egress !== undefined && !alreadyDecided;
|
|
165
|
-
const egressInit =
|
|
190
|
+
const decide = egress !== undefined && !alreadyDecided && !redirectedToLoopback;
|
|
191
|
+
const egressInit = redirectedToLoopback
|
|
192
|
+
? { proxy: false as const }
|
|
193
|
+
: decide ? providerEgressSendInit(egress, physicalFetch, input) : {};
|
|
166
194
|
return physicalFetch(input, {
|
|
167
195
|
...init,
|
|
168
196
|
headers,
|
|
169
197
|
redirect: "manual",
|
|
170
198
|
...(fresh ? { keepalive: false } : {}),
|
|
171
199
|
...egressInit,
|
|
172
|
-
...(decide ? { [EGRESS_DECIDED]: true } : {}),
|
|
200
|
+
...(decide || redirectedToLoopback ? { [EGRESS_DECIDED]: true } : {}),
|
|
201
|
+
...{ [UPSTREAM_REWRITTEN]: true },
|
|
173
202
|
});
|
|
174
203
|
}
|
|
175
204
|
|
|
@@ -187,6 +216,9 @@ export interface ProviderFetchOptions {
|
|
|
187
216
|
modelId?: string;
|
|
188
217
|
/** One pacing slot was acquired immediately before this fetch wrapper was created. */
|
|
189
218
|
pacingSlotAcquired?: boolean;
|
|
219
|
+
pacingSlot?: ProviderRequestSlot;
|
|
220
|
+
/** A runTurn transport holds one concurrency lease across overlapping physical sends. */
|
|
221
|
+
turnScopedPacing?: boolean;
|
|
190
222
|
/** Captured selected-account observer, attached before the native WS send. */
|
|
191
223
|
onCodexWsQuota?: CodexWsQuotaObserver;
|
|
192
224
|
/** Synchronous admission at actual credential dispatch, after pacing/backoff. */
|
|
@@ -271,6 +303,14 @@ export function providerFetch(
|
|
|
271
303
|
const upstreamWebsocket = provider.upstreamWebsocket;
|
|
272
304
|
if (!options.httpOnly && typeof input === "string" && init
|
|
273
305
|
&& shouldUseCodexWsUpstream(input, init, runtime, upstreamWebsocket)) {
|
|
306
|
+
if (options.providerName && requestPacingMaxConcurrentRequests(provider, options.modelId) > 0) {
|
|
307
|
+
if (!pacingWebsocketDowngradeWarned.has(providerName)
|
|
308
|
+
&& pacingWebsocketDowngradeWarned.size < EGRESS_DOWNGRADE_NOTICE_LIMIT) {
|
|
309
|
+
pacingWebsocketDowngradeWarned.add(providerName);
|
|
310
|
+
console.warn(`[opencodex] provider ${JSON.stringify(redactSecretString(providerName))} has a request concurrency cap; serving over HTTP/SSE.`);
|
|
311
|
+
}
|
|
312
|
+
return httpFetch(input, init);
|
|
313
|
+
}
|
|
274
314
|
const egress = egressFor(input);
|
|
275
315
|
if (providerEgressIsExplicit(egress)) {
|
|
276
316
|
warnEgressWebsocketDowngradeOnce(providerName, describeProviderEgressForLog(egress));
|
|
@@ -281,23 +321,33 @@ export function providerFetch(
|
|
|
281
321
|
// request over HTTP, and dropping the provider's `upstreamHttpVersion`
|
|
282
322
|
// there would silently negotiate a transport the operator ruled out.
|
|
283
323
|
return codexWsUpstreamFetch(input, init, httpFetch, runtime, options.onCodexWsQuota, options.beforeDispatch, options.nativeControl,
|
|
284
|
-
() => waitForPacing(init.signal ?? undefined));
|
|
324
|
+
async () => { (await waitForPacing(init.signal ?? undefined))?.release(); });
|
|
285
325
|
}
|
|
286
326
|
return httpFetch(input, init);
|
|
287
327
|
};
|
|
288
328
|
let pacingSlotAcquired = options.pacingSlotAcquired === true;
|
|
289
|
-
const waitForPacing = (signal?: AbortSignal) => {
|
|
329
|
+
const waitForPacing = async (signal?: AbortSignal): Promise<ProviderRequestSlot | undefined> => {
|
|
290
330
|
if (pacingSlotAcquired) {
|
|
291
331
|
pacingSlotAcquired = false;
|
|
292
|
-
|
|
332
|
+
if (!options.pacingSlot && requestPacingMaxConcurrentRequests(provider, options.modelId) > 0) {
|
|
333
|
+
throw new Error("A pre-acquired concurrency slot must be passed to providerFetch");
|
|
334
|
+
}
|
|
335
|
+
// Cursor RunSSE and BidiAppend can overlap. The runTurn owner releases this slot when
|
|
336
|
+
// the whole turn finishes; transferring it to RunSSE's body would admit another turn
|
|
337
|
+
// while a BidiAppend from this one is still active.
|
|
338
|
+
return options.turnScopedPacing ? undefined : options.pacingSlot;
|
|
339
|
+
}
|
|
340
|
+
if (options.providerName) {
|
|
341
|
+
return waitForProviderRequestSlot(options.providerName, provider, options.modelId, signal,
|
|
342
|
+
options.turnScopedPacing && options.pacingSlot?.leased && !options.pacingSlot.released
|
|
343
|
+
? { concurrency: false } : undefined);
|
|
344
|
+
}
|
|
345
|
+
if (requestPacingMaxConcurrentRequests(provider, options.modelId) > 0) {
|
|
346
|
+
throw new Error("A provider name is required for request concurrency pacing");
|
|
293
347
|
}
|
|
294
|
-
return options.providerName
|
|
295
|
-
? waitForProviderRequestSlot(options.providerName, provider, options.modelId, signal)
|
|
296
|
-
: Promise.resolve();
|
|
297
348
|
};
|
|
298
349
|
const wrapped = async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
|
|
299
|
-
await waitForPacing(init?.signal ?? undefined);
|
|
300
|
-
return unpaced(input, init);
|
|
350
|
+
return sendTrackingRequestSlot(await waitForPacing(init?.signal ?? undefined), () => unpaced(input, init));
|
|
301
351
|
};
|
|
302
352
|
// The returned wrapper forwards its init down to `dispatch`, which applies the route at the
|
|
303
353
|
// physical send. Adapters that hand this executor back as `provider.fetch` (Cursor does)
|
|
@@ -345,8 +395,7 @@ export function storedPoolReplayDispatchNotifier(
|
|
|
345
395
|
{ preconnect: unpacedSource.preconnect },
|
|
346
396
|
) as ProviderFetch["unpacedFetch"];
|
|
347
397
|
const wrapped = async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
|
|
348
|
-
await executor.waitForPacing?.(init?.signal ?? undefined);
|
|
349
|
-
return unpaced!(input, init);
|
|
398
|
+
return sendTrackingRequestSlot(await executor.waitForPacing?.(init?.signal ?? undefined), () => unpaced!(input, init));
|
|
350
399
|
};
|
|
351
400
|
return Object.assign(wrapped, {
|
|
352
401
|
preconnect: executor.preconnect,
|
|
@@ -370,20 +419,21 @@ export async function fetchWithHeaderTimeout(
|
|
|
370
419
|
_manualRedirect = false,
|
|
371
420
|
): Promise<Response> {
|
|
372
421
|
const pacing = executor as ProviderFetch;
|
|
373
|
-
await pacing.waitForPacing?.(abortSignal);
|
|
422
|
+
const slot = await pacing.waitForPacing?.(abortSignal);
|
|
374
423
|
const fetchExecutor = pacing.unpacedFetch ?? executor;
|
|
375
|
-
|
|
376
|
-
const timer = setTimeout(() => {
|
|
377
|
-
if (!timeout.signal.aborted) timeout.abort(new DOMException("Timeout elapsed", "TimeoutError"));
|
|
378
|
-
}, timeoutMs);
|
|
379
|
-
const headers = new Headers(init.headers);
|
|
380
|
-
// Compressed SSE can be held until the decompressor has a complete block. Streaming calls
|
|
381
|
-
// default to identity for low-latency frame delivery, while an explicit caller choice wins.
|
|
382
|
-
if (preferIdentityEncoding && !headers.has("accept-encoding")) {
|
|
383
|
-
headers.set("accept-encoding", "identity");
|
|
384
|
-
}
|
|
424
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
385
425
|
try {
|
|
386
|
-
|
|
426
|
+
const timeout = new AbortController();
|
|
427
|
+
timer = setTimeout(() => {
|
|
428
|
+
if (!timeout.signal.aborted) timeout.abort(new DOMException("Timeout elapsed", "TimeoutError"));
|
|
429
|
+
}, timeoutMs);
|
|
430
|
+
const headers = new Headers(init.headers);
|
|
431
|
+
// Compressed SSE can be held until the decompressor has a complete block. Streaming calls
|
|
432
|
+
// default to identity for low-latency frame delivery, while an explicit caller choice wins.
|
|
433
|
+
if (preferIdentityEncoding && !headers.has("accept-encoding")) {
|
|
434
|
+
headers.set("accept-encoding", "identity");
|
|
435
|
+
}
|
|
436
|
+
return await sendTrackingRequestSlot(slot, () => fetchExecutor(url, {
|
|
387
437
|
...init,
|
|
388
438
|
headers,
|
|
389
439
|
// Never replay provider credentials or request bodies to a redirect destination.
|
|
@@ -391,8 +441,9 @@ export async function fetchWithHeaderTimeout(
|
|
|
391
441
|
redirect: "manual",
|
|
392
442
|
signal: AbortSignal.any([abortSignal, timeout.signal]),
|
|
393
443
|
timeout: 0,
|
|
394
|
-
});
|
|
444
|
+
}));
|
|
395
445
|
} finally {
|
|
396
|
-
clearTimeout(timer);
|
|
446
|
+
if (timer) clearTimeout(timer);
|
|
447
|
+
if (!slot?.bodyTracked) slot?.release();
|
|
397
448
|
}
|
|
398
449
|
}
|
|
@@ -129,7 +129,7 @@ import { registerTurn, unregisterTurn, trackStreamLifetime } from "../lifecycle"
|
|
|
129
129
|
import { relaySseEagerBounded } from "../relay-eager";
|
|
130
130
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
131
131
|
import { idleDeadline } from "../../lib/abort";
|
|
132
|
-
import {
|
|
132
|
+
import { resolveStallTimeoutMs } from "../../stall-timeout";
|
|
133
133
|
import { formatErrorResponse } from "../../bridge";
|
|
134
134
|
import { inspectResponseLogJson } from "../request-log";
|
|
135
135
|
import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
@@ -316,6 +316,7 @@ export async function deliverPassthroughResponse(
|
|
|
316
316
|
| "rememberPassthroughResponse"
|
|
317
317
|
| "noteInspectedPayload"
|
|
318
318
|
| "normalizeFunctionCompletionJson"
|
|
319
|
+
| "localUpstream"
|
|
319
320
|
>,
|
|
320
321
|
): Promise<Response> {
|
|
321
322
|
const { logCtx, config, options, req } = requestContext;
|
|
@@ -354,7 +355,7 @@ export async function deliverPassthroughResponse(
|
|
|
354
355
|
&& !isCodexWsUpstreamResponse(upstreamResponse)
|
|
355
356
|
&& !(options.nativeControl && isNativeControlResponse(upstreamResponse))) {
|
|
356
357
|
upstreamResponse = await classifyPlaintextV2SseResponse(upstreamResponse, {
|
|
357
|
-
timeoutMs:
|
|
358
|
+
timeoutMs: resolveStallTimeoutMs(config.stallTimeoutSec, { localUpstream: nativeExchange.localUpstream }),
|
|
358
359
|
signal: options.abortSignal ?? req.signal,
|
|
359
360
|
});
|
|
360
361
|
}
|
|
@@ -13,6 +13,7 @@ import type { ResponsesEffects } from "./response-effects";
|
|
|
13
13
|
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
14
14
|
import { transientSendCapFor } from "./request-send-budget";
|
|
15
15
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
16
|
+
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
16
17
|
import { codexSafetyBufferingFilterOptions, terminalStatusFromParsed } from "../relay";
|
|
17
18
|
import { imageGenToolCallAliases } from "../responses-image-gen-repair";
|
|
18
19
|
import { rememberResponseState, isBodyNonPersistable } from "../../responses/state";
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
} from "../../responses/namespace-tool-compat";
|
|
44
45
|
import { restoreRoutedCustomCalls, RoutedCustomToolCompatError } from "../../responses/custom-tool-compat";
|
|
45
46
|
import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
|
|
47
|
+
import { MuseToolChoiceCompatibilityError } from "../../adapters/openai-responses/muse-tool-choice";
|
|
46
48
|
import { formatErrorResponse } from "../../bridge";
|
|
47
49
|
import { redactSecretString } from "../../lib/redact";
|
|
48
50
|
import {
|
|
@@ -137,7 +139,6 @@ import type { OAuthAccessSnapshot } from "../../oauth";
|
|
|
137
139
|
import { publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
138
140
|
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
139
141
|
import {
|
|
140
|
-
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
141
142
|
hasEligibleGenericOAuthFailoverTarget,
|
|
142
143
|
isGenericOAuthFailoverEnabled,
|
|
143
144
|
rotateGenericOAuthAccountOn429,
|
|
@@ -193,6 +194,7 @@ export async function preparePassthroughExchange(
|
|
|
193
194
|
| "refreshResolvedOAuthSelection"
|
|
194
195
|
| "replayOAuthCredentialSnapshot"
|
|
195
196
|
| "genericFailovers"
|
|
197
|
+
| "genericFailoverLimit"
|
|
196
198
|
| "applyFailoverSnapshot"
|
|
197
199
|
| "noteRoutedAttemptSend"
|
|
198
200
|
| "selectionIsCurrent"
|
|
@@ -336,6 +338,7 @@ export async function preparePassthroughExchange(
|
|
|
336
338
|
error instanceof NamespaceToolCollisionError
|
|
337
339
|
|| error instanceof XaiToolSchemaCompatibilityError
|
|
338
340
|
|| error instanceof RoutedCustomToolCompatError
|
|
341
|
+
|| error instanceof MuseToolChoiceCompatibilityError
|
|
339
342
|
) {
|
|
340
343
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
|
|
341
344
|
}
|
|
@@ -1318,7 +1321,7 @@ export async function preparePassthroughExchange(
|
|
|
1318
1321
|
// have run and would cool down an account that refused nothing.
|
|
1319
1322
|
&& !isNonReplayableResponse(upstreamResponse)
|
|
1320
1323
|
&& transportState.genericFailoverAccountId
|
|
1321
|
-
&& transportState.genericFailovers <
|
|
1324
|
+
&& transportState.genericFailovers < transportState.genericFailoverLimit
|
|
1322
1325
|
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
1323
1326
|
) {
|
|
1324
1327
|
// The roster cap above is one half of the bound; the request's shared budget is the
|
|
@@ -1782,6 +1785,12 @@ export async function preparePassthroughExchange(
|
|
|
1782
1785
|
break;
|
|
1783
1786
|
}
|
|
1784
1787
|
|
|
1788
|
+
// Where this relay dials upstream. Local infrastructure (loopback / private / `.local` / `.lan`)
|
|
1789
|
+
// is operator-trusted and its silent phases are normal, so an unset stall budget resolves to
|
|
1790
|
+
// disabled for it. Provider rotation above keeps the same endpoint origin, so the routed
|
|
1791
|
+
// provider's baseUrl is the stable classification source.
|
|
1792
|
+
const localUpstream = isLocalUpstream(route.provider.baseUrl);
|
|
1793
|
+
|
|
1785
1794
|
return {
|
|
1786
1795
|
codexSafetyBufferingOptions,
|
|
1787
1796
|
imageGenCallAliases,
|
|
@@ -1819,6 +1828,7 @@ export async function preparePassthroughExchange(
|
|
|
1819
1828
|
rememberPassthroughResponseChecked,
|
|
1820
1829
|
upstream,
|
|
1821
1830
|
connectMs,
|
|
1831
|
+
localUpstream,
|
|
1822
1832
|
upstreamResponse,
|
|
1823
1833
|
};
|
|
1824
1834
|
}
|
|
@@ -11,7 +11,7 @@ import type { ResponsesSendBudget } from "./request-send-budget";
|
|
|
11
11
|
import { preparePassthroughExchange } from "./passthrough-dispatch";
|
|
12
12
|
import { deliverPassthroughResponse } from "./passthrough-delivery";
|
|
13
13
|
import { guardDirectPassthroughBodyInactivity } from "../../lib/response-body-inactivity";
|
|
14
|
-
import {
|
|
14
|
+
import { resolveStallTimeoutMs } from "../../stall-timeout";
|
|
15
15
|
import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health";
|
|
16
16
|
import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
|
|
17
17
|
|
|
@@ -53,7 +53,7 @@ export async function executePassthroughResponse(
|
|
|
53
53
|
return guardDirectPassthroughBodyInactivity(
|
|
54
54
|
response,
|
|
55
55
|
nativeExchange.upstream.signal,
|
|
56
|
-
|
|
56
|
+
resolveStallTimeoutMs(requestContext.config.stallTimeoutSec, { localUpstream: nativeExchange.localUpstream }),
|
|
57
57
|
);
|
|
58
58
|
} finally {
|
|
59
59
|
if (nativeHostState.lease) {
|
|
@@ -60,6 +60,7 @@ export function createResponsesSendBudget(
|
|
|
60
60
|
const noteTransientSends = (used: number): void => {
|
|
61
61
|
const charged = Math.max(0, used);
|
|
62
62
|
sendBudget.used += charged;
|
|
63
|
+
options.onCompactionRecoverySendsReported?.(charged);
|
|
63
64
|
chargeWorkflowSends(workflowRootId, charged);
|
|
64
65
|
};
|
|
65
66
|
// Refused before any dispatch, and deliberately not by evicting the root's ledger entry:
|
|
@@ -156,7 +157,7 @@ export function createResponsesSendBudget(
|
|
|
156
157
|
* refused and the request would answer with a synthetic 502 in place of the real 429 the hop
|
|
157
158
|
* was recovering from.
|
|
158
159
|
*/
|
|
159
|
-
let pendingHopPermit: SingleUseDispatchPermit | undefined;
|
|
160
|
+
let pendingHopPermit: SingleUseDispatchPermit | undefined = options.compactionRecoveryPermit;
|
|
160
161
|
/**
|
|
161
162
|
* The budget an adapter's OWN dispatch ladder reserves against.
|
|
162
163
|
*
|
|
@@ -200,7 +201,9 @@ export function createResponsesSendBudget(
|
|
|
200
201
|
options: { allowFinalRecoveryReserve?: boolean } = {},
|
|
201
202
|
): { attempts: number; permit?: SingleUseDispatchPermit } => {
|
|
202
203
|
const base = remainingTransientSendBudget(cap);
|
|
203
|
-
|
|
204
|
+
// The hop already paid for this leg's first send. Include it in the helper's total
|
|
205
|
+
// attempts without charging it again, or the final account loses one transient attempt.
|
|
206
|
+
if (base > 0) return { attempts: Math.min(cap, base + (pendingHopPermit ? 1 : 0)) };
|
|
204
207
|
// A provider-configured transient total is an exact physical-send ceiling. Once it is
|
|
205
208
|
// exhausted, the request-wide recovery reserve must not silently widen it. The default stays
|
|
206
209
|
// permissive so unconfigured providers retain the guarded profile's fourth recovery send.
|
|
@@ -217,8 +220,8 @@ export function createResponsesSendBudget(
|
|
|
217
220
|
/**
|
|
218
221
|
* One credential hop of this logical request, admitted by the INTERSECTION of two bounds.
|
|
219
222
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
223
|
+
* The snapshotted generic OAuth roster cap and `ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST`
|
|
224
|
+
* bound rotation within one credential roster. What neither
|
|
222
225
|
* can see is everything else this request already sent, so three hops layered on a spent
|
|
223
226
|
* budget still reached upstream three more times. A hop now happens only when its own layer
|
|
224
227
|
* cap AND the shared budget both permit it, and the smaller of the two wins.
|
|
@@ -245,7 +248,13 @@ export function createResponsesSendBudget(
|
|
|
245
248
|
countedExternally = false,
|
|
246
249
|
): { allowed: boolean; permit?: SingleUseDispatchPermit } => {
|
|
247
250
|
if (!isRequestExecutionBudget(sendBudget)) return { allowed: true };
|
|
248
|
-
const decision = sendBudget.reserveDispatch({
|
|
251
|
+
const decision = sendBudget.reserveDispatch({
|
|
252
|
+
sendClass,
|
|
253
|
+
// A same-provider credential hop is not a model/endpoint transition. Its diagnostic
|
|
254
|
+
// label must not replace the physical target used by the adapter's next retry.
|
|
255
|
+
targetKey: sendClass === "auth-recovery" ? sendBudget.lastTargetKey ?? targetKey : targetKey,
|
|
256
|
+
countedExternally,
|
|
257
|
+
});
|
|
249
258
|
return decision.allowed ? { allowed: true, permit: decision.permit } : { allowed: false };
|
|
250
259
|
};
|
|
251
260
|
/**
|
|
@@ -322,6 +331,13 @@ function adapterDispatchBudgetView(
|
|
|
322
331
|
// already paid does not make an unsafe replay safe, so that check stays with the budget.
|
|
323
332
|
if (intent.replaySafe !== false) {
|
|
324
333
|
const hopPermit = hop.claimHopPermit();
|
|
334
|
+
if (hopPermit && intent.targetKey !== budget.lastTargetKey) {
|
|
335
|
+
// The rotated credential can select a different regional endpoint. Replace the
|
|
336
|
+
// provisional booking synchronously so the actual destination obeys transition
|
|
337
|
+
// limits, while the physical send is still charged only once.
|
|
338
|
+
hopPermit.release();
|
|
339
|
+
return budget.reserveDispatch({ ...intent, sendClass: hopPermit.sendClass });
|
|
340
|
+
}
|
|
325
341
|
// Confirmed here rather than in `use()`: the adapter reserves immediately before it
|
|
326
342
|
// opens the transport, which is the same boundary the hop's own confirmation uses.
|
|
327
343
|
// A permit some other leg already settled returns false, and this falls through to a
|
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
commitOAuthAccountSelection,
|
|
7
7
|
getAccountCredentialWithStatus,
|
|
8
8
|
credentialGeneration,
|
|
9
|
+
getAccountSet,
|
|
9
10
|
} from "../../oauth/store";
|
|
11
|
+
import { refreshKiroAccountModelsDetached } from "../../providers/kiro-model-catalog";
|
|
10
12
|
import type { ProviderAdapter, AdapterRequest } from "../../adapters/base";
|
|
13
|
+
import { releaseProviderRequestSlot, waitForProviderRequestSlot, type ProviderRequestSlot } from "../../providers/request-pacing";
|
|
11
14
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../../types";
|
|
12
15
|
import type { AnthropicAccountSelectionReason } from "../../oauth/anthropic-routing";
|
|
13
16
|
import {
|
|
@@ -24,15 +27,24 @@ import {
|
|
|
24
27
|
getValidAccessSnapshotForAccount,
|
|
25
28
|
forceRefreshOAuthAccessSnapshot,
|
|
26
29
|
getValidAccessTokenSnapshot,
|
|
30
|
+
OAuthLoginRequiredError,
|
|
27
31
|
publicOAuthAuthenticationErrorMessage,
|
|
28
32
|
UnsupportedOAuthProviderError,
|
|
29
33
|
} from "../../oauth";
|
|
30
34
|
import {
|
|
35
|
+
GENERIC_OAUTH_MAX_ACCOUNTS_PER_REQUEST,
|
|
36
|
+
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
37
|
+
eligibleFailoverAccounts,
|
|
31
38
|
forgetGenericFailoverRoster,
|
|
32
39
|
isGenericFailoverProvider,
|
|
40
|
+
isProactivePreferenceEnabled,
|
|
33
41
|
preferredInitialAccount,
|
|
42
|
+
refusalAwareInitialKiroAccount,
|
|
34
43
|
noteGenericPoolSelection,
|
|
35
44
|
} from "../../oauth/generic-account-failover";
|
|
45
|
+
import { tryKiroAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
|
|
46
|
+
import { classifyModelFamilyForQuota } from "../../oauth/account-quota-rank";
|
|
47
|
+
import { expandInferenceOAuthSendBudget } from "../inference/context";
|
|
36
48
|
import { stampOAuthAccountLabel, usesApiKeyAccount } from "../../providers/label";
|
|
37
49
|
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
38
50
|
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
@@ -66,6 +78,8 @@ import {
|
|
|
66
78
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
67
79
|
import { bindAttemptDeliveryRecorder } from "../../usage/attempt-delivery";
|
|
68
80
|
import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
|
|
81
|
+
import { clientCancelledResponse } from "./core-errors";
|
|
82
|
+
import { acquireAccountLease, KIRO_ACCOUNT_WAIT_MS } from "../../oauth/kiro-account-load";
|
|
69
83
|
|
|
70
84
|
/** Owns live credential selection and adapter bindings for one request. */
|
|
71
85
|
export async function prepareResponsesTransport(
|
|
@@ -82,6 +96,17 @@ export async function prepareResponsesTransport(
|
|
|
82
96
|
) {
|
|
83
97
|
const { config, logCtx, options, req } = requestContext;
|
|
84
98
|
const { route, parsed, inboundWire, translatorBudget } = requestState;
|
|
99
|
+
const rawKiroCap = route.providerName === "kiro" && route.provider.authMode === "oauth"
|
|
100
|
+
? config.providers.kiro?.oauthAccountFailover?.maxConcurrentPerAccount : undefined;
|
|
101
|
+
const kiroCap = typeof rawKiroCap === "number" && Number.isInteger(rawKiroCap)
|
|
102
|
+
&& rawKiroCap >= 1 && rawKiroCap <= 100 ? rawKiroCap : undefined;
|
|
103
|
+
const kiroLoadEnabled = route.providerName === "kiro" && route.provider.authMode === "oauth"
|
|
104
|
+
&& (kiroCap !== undefined || (config.pool?.kernel === true
|
|
105
|
+
&& config.providers.kiro?.oauthAccountFailover?.strategy === "least-loaded"
|
|
106
|
+
&& isProactivePreferenceEnabled(config, "kiro", Date.now())));
|
|
107
|
+
const capacityResponse = () => new Response(JSON.stringify({ error: {
|
|
108
|
+
type: "server_error", code: "account_capacity", message: "Kiro account capacity is temporarily full; retry shortly.",
|
|
109
|
+
} }), { status: 503, headers: { "content-type": "application/json", "retry-after": "1" } });
|
|
85
110
|
|
|
86
111
|
|
|
87
112
|
// OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
|
|
@@ -173,6 +198,15 @@ export async function prepareResponsesTransport(
|
|
|
173
198
|
const candidate = unchanged ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
|
|
174
199
|
const admitted = await commitResolvedOAuthSelection(candidate);
|
|
175
200
|
if (!admitted) throw new Error("OAuth selection changed during credential recovery");
|
|
201
|
+
if (kiroLoadEnabled && options.accountLoad?.lease?.accountId !== admitted.accountId) {
|
|
202
|
+
const replayLease = await acquireAccountLease("kiro", admitted.accountId, { maxConcurrentPerAccount: kiroCap });
|
|
203
|
+
if (!replayLease || !options.accountLoad) {
|
|
204
|
+
replayLease?.release();
|
|
205
|
+
throw new Error("Kiro replay account capacity is full");
|
|
206
|
+
}
|
|
207
|
+
options.accountLoad.lease?.release();
|
|
208
|
+
options.accountLoad.lease = replayLease;
|
|
209
|
+
}
|
|
176
210
|
genericFailoverAccountId = admitted.accountId;
|
|
177
211
|
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, admitted.accountId);
|
|
178
212
|
return admitted;
|
|
@@ -211,8 +245,22 @@ export async function prepareResponsesTransport(
|
|
|
211
245
|
retryParsed: OcxParsedRequest = parsed,
|
|
212
246
|
): Promise<OAuthAccessSnapshot | null> => {
|
|
213
247
|
if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return null;
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
let speculative = kiroLoadEnabled && options.accountLoad?.lease?.accountId !== snapshot.accountId
|
|
249
|
+
? await acquireAccountLease("kiro", snapshot.accountId, { maxConcurrentPerAccount: kiroCap }) : null;
|
|
250
|
+
if (kiroLoadEnabled && options.accountLoad?.lease?.accountId !== snapshot.accountId && !speculative) return null;
|
|
251
|
+
let committed: OAuthAccessSnapshot | null;
|
|
252
|
+
try { committed = await commitResolvedOAuthSelection(snapshot); }
|
|
253
|
+
catch (error) { speculative?.release(); throw error; }
|
|
254
|
+
if (!committed) { speculative?.release(); return null; }
|
|
255
|
+
if (kiroLoadEnabled && committed.accountId !== (speculative?.accountId ?? options.accountLoad?.lease?.accountId)) {
|
|
256
|
+
speculative?.release();
|
|
257
|
+
speculative = await acquireAccountLease("kiro", committed.accountId, { maxConcurrentPerAccount: kiroCap });
|
|
258
|
+
if (!speculative) return null;
|
|
259
|
+
}
|
|
260
|
+
if (speculative && options.accountLoad) {
|
|
261
|
+
options.accountLoad.lease?.release();
|
|
262
|
+
options.accountLoad.lease = speculative;
|
|
263
|
+
}
|
|
216
264
|
snapshot = committed;
|
|
217
265
|
let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
|
|
218
266
|
if (route.providerName === "github-copilot") {
|
|
@@ -374,24 +422,32 @@ export async function prepareResponsesTransport(
|
|
|
374
422
|
if (!run) throw new Error("Selected provider no longer supports this turn transport");
|
|
375
423
|
let sent = false;
|
|
376
424
|
let refused = false;
|
|
425
|
+
let attemptSlot: ProviderRequestSlot | undefined;
|
|
377
426
|
// Both main and image-loop callers already acquired the initial pacing slot.
|
|
378
427
|
// Subsequent physical messages retain this adapter/credential and are paced normally.
|
|
379
|
-
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
380
|
-
providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
|
|
381
|
-
beforeDispatch: () => {
|
|
382
|
-
if (sent) return;
|
|
383
|
-
if (!selectionIsCurrent(binding)) {
|
|
384
|
-
refused = true;
|
|
385
|
-
throw new Error("Account selection changed before the first turn dispatch");
|
|
386
|
-
}
|
|
387
|
-
commitKeyAttemptSend();
|
|
388
|
-
sent = true;
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
428
|
try {
|
|
429
|
+
attemptSlot = attempt === 0
|
|
430
|
+
? incoming.pacingSlot ?? await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, incoming.abortSignal)
|
|
431
|
+
: await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, incoming.abortSignal);
|
|
432
|
+
const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
433
|
+
providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
|
|
434
|
+
pacingSlot: attemptSlot,
|
|
435
|
+
turnScopedPacing: true,
|
|
436
|
+
beforeDispatch: () => {
|
|
437
|
+
if (sent) return;
|
|
438
|
+
if (!selectionIsCurrent(binding)) {
|
|
439
|
+
refused = true;
|
|
440
|
+
throw new Error("Account selection changed before the first turn dispatch");
|
|
441
|
+
}
|
|
442
|
+
commitKeyAttemptSend();
|
|
443
|
+
sent = true;
|
|
444
|
+
},
|
|
445
|
+
});
|
|
392
446
|
await run(requestParsed, { ...incoming, providerFetch: fetch }, event => { if (!refused) emit(event); });
|
|
393
447
|
} catch (error) {
|
|
394
448
|
if (!refused) throw error;
|
|
449
|
+
} finally {
|
|
450
|
+
releaseProviderRequestSlot(attemptSlot);
|
|
395
451
|
}
|
|
396
452
|
if (!refused) return;
|
|
397
453
|
// The adapter may map the guard's exception to an error event. Neither that
|
|
@@ -512,9 +568,12 @@ export async function prepareResponsesTransport(
|
|
|
512
568
|
// only reacts to a 429, so a turn could open on an account a previous probe already
|
|
513
569
|
// measured as spent. A null answer means "use the active account", so every provider
|
|
514
570
|
// without quota evidence keeps the resolution it has today.
|
|
515
|
-
const
|
|
571
|
+
const refusalAwareId = route.providerName === "kiro" && oauthSelection?.accountId
|
|
572
|
+
? refusalAwareInitialKiroAccount(config, oauthSelection.accountId, Date.now(), route.modelId) : null;
|
|
573
|
+
const preferredAccountId = refusalAwareId ?? (isGenericFailoverProvider(route.providerName, route.provider)
|
|
516
574
|
? preferredInitialAccount(config, route.providerName, Date.now(), route.modelId)
|
|
517
|
-
: null;
|
|
575
|
+
: null);
|
|
576
|
+
let safetyAlternateId: string | null = refusalAwareId;
|
|
518
577
|
// Resolved account-scoped, NOT through failoverAccountSnapshot: that helper marks a
|
|
519
578
|
// rotation site, and rotation sites must apply their credential through
|
|
520
579
|
// applyFailoverSnapshot's pairing rules. This is initial resolution — the code below
|
|
@@ -541,10 +600,22 @@ export async function prepareResponsesTransport(
|
|
|
541
600
|
// Drop the stale roster so the next request re-reads it, and carry on.
|
|
542
601
|
forgetGenericFailoverRoster(route.providerName);
|
|
543
602
|
usedPreferredAccount = false;
|
|
603
|
+
safetyAlternateId = null;
|
|
544
604
|
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
545
605
|
}
|
|
546
606
|
} else {
|
|
547
|
-
|
|
607
|
+
const failedId = route.providerName === "kiro" ? oauthSelection?.accountId : undefined;
|
|
608
|
+
const failedRow = failedId ? getAccountCredentialWithStatus("kiro", failedId) : null;
|
|
609
|
+
const failedGeneration = failedRow ? credentialGeneration(failedRow.credential) : undefined;
|
|
610
|
+
try { resolved = await getValidAccessTokenSnapshot(route.providerName); }
|
|
611
|
+
catch (error) {
|
|
612
|
+
if (route.providerName !== "kiro" || !(error instanceof OAuthLoginRequiredError)
|
|
613
|
+
|| !failedId || !failedGeneration) throw error;
|
|
614
|
+
const alternate = await tryKiroAlternateAfterTerminalRefresh(config, failedId, failedGeneration);
|
|
615
|
+
if (!alternate) throw error;
|
|
616
|
+
resolved = alternate;
|
|
617
|
+
safetyAlternateId = alternate.accountId;
|
|
618
|
+
}
|
|
548
619
|
}
|
|
549
620
|
// A Cloud Code Assist account needs its own project. Antigravity's refresh path
|
|
550
621
|
// tolerates project discovery failing, so a stored account can legitimately have
|
|
@@ -555,8 +626,23 @@ export async function prepareResponsesTransport(
|
|
|
555
626
|
resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
556
627
|
usedPreferredAccount = false;
|
|
557
628
|
}
|
|
558
|
-
const admitted = await commitResolvedOAuthSelection(resolved,
|
|
629
|
+
const admitted = await commitResolvedOAuthSelection(resolved, safetyAlternateId === null);
|
|
559
630
|
if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
|
|
631
|
+
if (safetyAlternateId && admitted.accountId !== safetyAlternateId)
|
|
632
|
+
return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
|
|
633
|
+
if (kiroLoadEnabled) {
|
|
634
|
+
const lease = await acquireAccountLease("kiro", admitted.accountId, {
|
|
635
|
+
maxConcurrentPerAccount: kiroCap, waitMs: KIRO_ACCOUNT_WAIT_MS, signal: options.abortSignal ?? req.signal,
|
|
636
|
+
});
|
|
637
|
+
if (!lease) return (options.abortSignal ?? req.signal).aborted
|
|
638
|
+
? clientCancelledResponse() : capacityResponse();
|
|
639
|
+
if (options.accountLoad) options.accountLoad.lease = lease;
|
|
640
|
+
else lease.release();
|
|
641
|
+
}
|
|
642
|
+
if (route.providerName === "kiro") {
|
|
643
|
+
const account = getAccountSet("kiro")?.accounts.find(row => row.id === admitted.accountId);
|
|
644
|
+
if (account) refreshKiroAccountModelsDetached(account, route.provider);
|
|
645
|
+
}
|
|
560
646
|
if (admitted.accountId !== resolved.accountId) usedPreferredAccount = true;
|
|
561
647
|
resolved = admitted;
|
|
562
648
|
replayOAuthCredentialSnapshot = {
|
|
@@ -721,7 +807,19 @@ export async function prepareResponsesTransport(
|
|
|
721
807
|
);
|
|
722
808
|
}
|
|
723
809
|
|
|
810
|
+
// Freeze the request ceiling before any 429 writes cooldowns. Selection still reads
|
|
811
|
+
// live eligibility on every hop; cooled accounts cannot shorten this request's allowance.
|
|
812
|
+
// The snapshot is clamped: a larger roster must not raise one request's hops or sends.
|
|
813
|
+
const genericRosterSize = genericFailoverAccountId ? new Set([
|
|
814
|
+
genericFailoverAccountId,
|
|
815
|
+
...eligibleFailoverAccounts(route.providerName, Date.now(), classifyModelFamilyForQuota(route.providerName, route.modelId)),
|
|
816
|
+
]).size : 0;
|
|
817
|
+
const fundedAccounts = Math.min(genericRosterSize, GENERIC_OAUTH_MAX_ACCOUNTS_PER_REQUEST);
|
|
818
|
+
const genericFailoverLimit = Math.max(GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST, fundedAccounts - 1);
|
|
819
|
+
if (genericFailoverAccountId) expandInferenceOAuthSendBudget(options.sendBudget, fundedAccounts);
|
|
820
|
+
|
|
724
821
|
return {
|
|
822
|
+
genericFailoverLimit,
|
|
725
823
|
isOAuth401ReplayProvider,
|
|
726
824
|
get sentOAuthSnapshot(): OAuthAccessSnapshot | undefined {
|
|
727
825
|
return sentOAuthSnapshot;
|