@bitkyc08/opencodex 2.67.0 → 2.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/bin/ocx.mjs +3 -0
  2. package/gui/dist/assets/App-DCBismRi.css +1 -0
  3. package/gui/dist/assets/App-mpsibsFj.js +51 -0
  4. package/gui/dist/assets/Tray-Bn8WhKBH.js +1 -0
  5. package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
  6. package/gui/dist/assets/index-kUE8tuAK.js +86 -0
  7. package/gui/dist/assets/quota-summary-DL-bNuNL.js +1 -0
  8. package/gui/dist/index.html +21 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-image-guard.ts +13 -1
  11. package/src/adapters/anthropic-image-normalize.ts +3 -2
  12. package/src/adapters/anthropic-output-schema.ts +36 -0
  13. package/src/adapters/anthropic.ts +10 -2
  14. package/src/adapters/base.ts +7 -0
  15. package/src/adapters/codebuddy/live-models.ts +180 -0
  16. package/src/adapters/coding-agent/protocol.ts +91 -12
  17. package/src/adapters/coding-agent/turn.ts +32 -27
  18. package/src/adapters/devin/cloud-direct/index.ts +1 -0
  19. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
  20. package/src/adapters/devin.ts +9 -6
  21. package/src/adapters/google-http.ts +4 -4
  22. package/src/adapters/google.ts +93 -3
  23. package/src/adapters/kiro/adapter.ts +6 -2
  24. package/src/adapters/kiro/stream.ts +12 -2
  25. package/src/adapters/kiro/usage.ts +3 -1
  26. package/src/adapters/kiro-errors.ts +11 -1
  27. package/src/adapters/kiro-events.ts +27 -1
  28. package/src/adapters/kiro-refusal.ts +30 -0
  29. package/src/adapters/kiro-retry.ts +71 -31
  30. package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
  31. package/src/adapters/openai-chat/messages.ts +31 -4
  32. package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
  33. package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
  34. package/src/adapters/openai-chat/tool-schema.ts +6 -2
  35. package/src/adapters/openai-chat.ts +2 -2
  36. package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
  37. package/src/adapters/openai-responses/passthrough.ts +9 -3
  38. package/src/adapters/opencode-go-additional-tools.ts +19 -10
  39. package/src/adapters/physical-send.ts +10 -3
  40. package/src/adapters/registry.ts +2 -1
  41. package/src/adapters/responses-tool-schema.ts +31 -3
  42. package/src/adapters/run-turn-queue.ts +63 -23
  43. package/src/adapters/unique-tool-call-ids.ts +63 -0
  44. package/src/adapters/xai-web-search.ts +32 -2
  45. package/src/bridge/sse.ts +14 -2
  46. package/src/chat/inbound.ts +20 -6
  47. package/src/claude/agents-inject.ts +4 -0
  48. package/src/claude/desktop-3p.ts +53 -13
  49. package/src/claude/desktop-profile.ts +41 -4
  50. package/src/claude/inbound-model-options.ts +14 -2
  51. package/src/claude/inbound.ts +1 -1
  52. package/src/claude/intercept/connect-proxy.ts +17 -1
  53. package/src/claude/intercept/local-ca.ts +7 -1
  54. package/src/claude/message-threads.ts +28 -0
  55. package/src/cli/account-api.ts +13 -0
  56. package/src/cli/account-auth.ts +49 -2
  57. package/src/cli/account-extended.ts +4 -2
  58. package/src/cli/account.ts +4 -2
  59. package/src/cli/capabilities.ts +17 -0
  60. package/src/cli/claude.ts +3 -1
  61. package/src/cli/dispatch.ts +62 -5
  62. package/src/cli/index.ts +84 -79
  63. package/src/cli/minimax.ts +4 -2
  64. package/src/cli/opencode.ts +6 -1
  65. package/src/cli/restart-handoff.ts +110 -0
  66. package/src/cli/status.ts +51 -0
  67. package/src/client/connect.ts +34 -15
  68. package/src/client/link-ingress.ts +102 -0
  69. package/src/client/link-join.ts +37 -16
  70. package/src/client/link-relay.ts +228 -45
  71. package/src/client/link-state.ts +54 -0
  72. package/src/client/link-status.ts +39 -0
  73. package/src/client/link-teardown.ts +2 -2
  74. package/src/client/link-tunnel.ts +615 -86
  75. package/src/client/machine-api.ts +10 -4
  76. package/src/client/machine-listener.ts +57 -10
  77. package/src/client/runtime.ts +210 -35
  78. package/src/clients/config-export/constants.ts +1 -12
  79. package/src/clients/config-export/contracts.ts +2 -0
  80. package/src/clients/config-export/model-metadata.ts +9 -3
  81. package/src/clients/config-export/omp.ts +1 -1
  82. package/src/clients/config-export/zcode-store.ts +2 -2
  83. package/src/clients/config-export.ts +36 -3
  84. package/src/codex/app-server-processes.ts +32 -0
  85. package/src/codex/app-server-restart-service.ts +20 -3
  86. package/src/codex/catalog/access-programs.ts +50 -0
  87. package/src/codex/catalog/build-entries.ts +13 -0
  88. package/src/codex/catalog/derive-entry.ts +1 -0
  89. package/src/codex/catalog/model-hints.ts +11 -0
  90. package/src/codex/catalog/parsing.ts +6 -2
  91. package/src/codex/catalog/provider-models.ts +81 -0
  92. package/src/codex/catalog/retained-sync.ts +3 -0
  93. package/src/codex/codex-write-lock.ts +2 -2
  94. package/src/codex/convergence.ts +2 -0
  95. package/src/codex/desired-state.ts +14 -4
  96. package/src/codex/home.ts +21 -3
  97. package/src/codex/inject/restore.ts +17 -0
  98. package/src/codex/inject/routing-classify.ts +3 -2
  99. package/src/codex/inject-coordination.ts +4 -1
  100. package/src/codex/inject.ts +8 -1
  101. package/src/codex/journal.ts +6 -2
  102. package/src/codex/management-convergence.ts +9 -0
  103. package/src/codex/model-entitlements.ts +28 -5
  104. package/src/codex/routing/idle-window.ts +58 -0
  105. package/src/codex/routing-drift.ts +134 -0
  106. package/src/codex/routing-healer.ts +419 -0
  107. package/src/codex/routing.ts +10 -0
  108. package/src/codex/runtime.ts +188 -57
  109. package/src/codex/sibling-handoff.ts +71 -0
  110. package/src/codex/sibling-start.ts +139 -0
  111. package/src/codex/sync.ts +5 -3
  112. package/src/combos/failover.ts +32 -5
  113. package/src/combos/request.ts +11 -3
  114. package/src/combos/reset-window.ts +10 -5
  115. package/src/combos/resolve.ts +36 -6
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/load-degrade.ts +2 -0
  118. package/src/config/paths.ts +7 -1
  119. package/src/config/process-state.ts +10 -1
  120. package/src/config/schema/compaction-recovery.ts +15 -0
  121. package/src/config/schema/config-schema.ts +2 -0
  122. package/src/config/schema/leaf-validators.ts +9 -3
  123. package/src/generated/compatibility-version.json +360 -204
  124. package/src/images/loop.ts +47 -32
  125. package/src/integrations/catalog-refresh.ts +4 -0
  126. package/src/integrations/omp-yaml-source.ts +1 -0
  127. package/src/lib/claude-request-projection.ts +101 -0
  128. package/src/lib/local-account-switch-capability.ts +48 -0
  129. package/src/lib/local-upstream.ts +146 -0
  130. package/src/lib/package-tree-integrity.ts +25 -3
  131. package/src/lib/package-tree-retarget.ts +130 -0
  132. package/src/lib/process-control.ts +4 -1
  133. package/src/lib/system-restart-contract.ts +106 -0
  134. package/src/lib/upstream-retry.ts +57 -1
  135. package/src/link/ports.ts +9 -0
  136. package/src/link/ssh-argv.ts +16 -0
  137. package/src/link/ssh-runner.ts +70 -2
  138. package/src/link/tunnel-state.ts +58 -9
  139. package/src/oauth/account-quota-rank.ts +29 -18
  140. package/src/oauth/generic-account-failover.ts +179 -22
  141. package/src/oauth/index.ts +17 -4
  142. package/src/oauth/kiro-account-load.ts +107 -0
  143. package/src/oauth/kiro-device-login.ts +312 -0
  144. package/src/oauth/kiro-terminal-failover.ts +28 -0
  145. package/src/oauth/login-flow-state.ts +7 -1
  146. package/src/oauth/pool-settings-capability.ts +23 -14
  147. package/src/oauth/store.ts +56 -3
  148. package/src/oauth/types.ts +4 -0
  149. package/src/plugins/loader.ts +350 -0
  150. package/src/plugins/upstream-hooks.ts +119 -0
  151. package/src/protocols/encoders/adapter-events.ts +9 -2
  152. package/src/providers/account-quota-disk.ts +42 -4
  153. package/src/providers/codebuddy-models.ts +6 -2
  154. package/src/providers/command-code-efforts.ts +33 -10
  155. package/src/providers/kiro-account-state-disk.ts +44 -0
  156. package/src/providers/kiro-model-catalog.ts +162 -0
  157. package/src/providers/kiro-models.ts +5 -4
  158. package/src/providers/kiro-quota-metrics.ts +35 -0
  159. package/src/providers/kiro-usage.ts +118 -14
  160. package/src/providers/quota/account-cache.ts +43 -7
  161. package/src/providers/quota/antigravity.ts +8 -3
  162. package/src/providers/quota/kiro-account-probe.ts +12 -0
  163. package/src/providers/quota/vendor-probes-key.ts +31 -26
  164. package/src/providers/quota/vendor-probes-oauth.ts +16 -7
  165. package/src/providers/quota-types.ts +14 -0
  166. package/src/providers/quota.ts +21 -21
  167. package/src/providers/registry/entries-extended.ts +5 -3
  168. package/src/providers/request-pacing.ts +410 -12
  169. package/src/remote-control/workspace-codex-runtime.ts +4 -3
  170. package/src/responses/citation-markers.ts +132 -65
  171. package/src/responses/hosted-tool-policy.ts +14 -3
  172. package/src/responses/parser-content.ts +3 -2
  173. package/src/responses/parser.ts +4 -1
  174. package/src/responses/schema.ts +4 -0
  175. package/src/responses/visualization-directives.ts +182 -0
  176. package/src/server/chat-native-sse.ts +24 -12
  177. package/src/server/chat-native.ts +137 -68
  178. package/src/server/claude-messages.ts +147 -12
  179. package/src/server/index/link-listener.ts +7 -0
  180. package/src/server/index/optional-listeners.ts +4 -1
  181. package/src/server/index/package-tree-guard.ts +35 -16
  182. package/src/server/index/serve-options.ts +4 -2
  183. package/src/server/index/startup-warnings.ts +6 -0
  184. package/src/server/index.ts +7 -7
  185. package/src/server/inference/client-encoder-delivery.ts +3 -0
  186. package/src/server/inference/context.ts +30 -2
  187. package/src/server/local-account-switch-auth.ts +79 -0
  188. package/src/server/management/agent-settings-routes.ts +11 -3
  189. package/src/server/management/config-routes.ts +28 -11
  190. package/src/server/management/context.ts +2 -0
  191. package/src/server/management/link-routes.ts +123 -39
  192. package/src/server/management/logs-usage-routes.ts +9 -42
  193. package/src/server/management/model-rows.ts +1 -0
  194. package/src/server/management/oauth-account-routes.ts +67 -12
  195. package/src/server/management/route-registry.ts +5 -5
  196. package/src/server/management/sibling-guard.ts +60 -0
  197. package/src/server/management/storage-log-guard-routes.ts +72 -6
  198. package/src/server/management/system-restart.ts +92 -135
  199. package/src/server/management/system-routes.ts +1 -1
  200. package/src/server/management-api.ts +31 -3
  201. package/src/server/management-auth.ts +3 -0
  202. package/src/server/port-reclaim.ts +111 -1
  203. package/src/server/proxy-liveness.ts +16 -0
  204. package/src/server/relay.ts +16 -15
  205. package/src/server/request-log-filter.ts +28 -27
  206. package/src/server/request-log.ts +7 -3
  207. package/src/server/request-metrics.ts +28 -0
  208. package/src/server/responses/adapter-continuation.ts +151 -21
  209. package/src/server/responses/adapter-delivery.ts +21 -7
  210. package/src/server/responses/adapter-dispatch.ts +239 -51
  211. package/src/server/responses/codex-ws-pool.ts +5 -4
  212. package/src/server/responses/codex-ws-request.ts +4 -1
  213. package/src/server/responses/compact.ts +8 -4
  214. package/src/server/responses/compaction-recovery-policy.ts +116 -0
  215. package/src/server/responses/compaction-recovery.ts +330 -0
  216. package/src/server/responses/core-combo-failure.ts +1 -0
  217. package/src/server/responses/core-combo.ts +5 -1
  218. package/src/server/responses/core-lifetime.ts +22 -0
  219. package/src/server/responses/core-options.ts +16 -1
  220. package/src/server/responses/core.ts +14 -4
  221. package/src/server/responses/empty-completion-guard.ts +2 -0
  222. package/src/server/responses/fetch-helpers.ts +82 -31
  223. package/src/server/responses/passthrough-delivery.ts +3 -2
  224. package/src/server/responses/passthrough-dispatch.ts +12 -2
  225. package/src/server/responses/passthrough-execution.ts +2 -2
  226. package/src/server/responses/request-send-budget.ts +21 -5
  227. package/src/server/responses/request-transport.ts +116 -18
  228. package/src/server/responses/reset-replay.ts +32 -0
  229. package/src/server/responses/run-turn-execution.ts +136 -19
  230. package/src/server/responses/sidecar-execution.ts +41 -8
  231. package/src/server/responses/terminal-guard.ts +2 -0
  232. package/src/server/responses/ws-upstream.ts +44 -4
  233. package/src/server/restart-replacement.ts +382 -0
  234. package/src/server/startup-health-cache.ts +23 -4
  235. package/src/server/stop-teardown.ts +35 -1
  236. package/src/server/system-env.ts +3 -0
  237. package/src/service/windows-taskxml.ts +11 -7
  238. package/src/service/windows-wrapper-exit.ts +8 -0
  239. package/src/stall-timeout.ts +35 -10
  240. package/src/storage/policy-job.ts +4 -0
  241. package/src/storage/scanner.ts +177 -29
  242. package/src/storage/storage-mutation-coordinator.ts +17 -0
  243. package/src/tray/windows-tray.ps1 +50 -23
  244. package/src/types/config.ts +4 -0
  245. package/src/types/provider.ts +5 -1
  246. package/src/types/request.ts +13 -2
  247. package/src/update/index.ts +3 -1
  248. package/src/update/job.ts +6 -2
  249. package/src/update/mise-launcher-target.ts +100 -0
  250. package/src/update/worker-launch.ts +46 -0
  251. package/src/usage/log.ts +2 -0
  252. package/src/web-search/loop.ts +5 -2
  253. package/gui/dist/assets/App-BJsT8Icc.css +0 -1
  254. package/gui/dist/assets/App-D3pNGiN4.js +0 -51
  255. package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
  256. package/gui/dist/assets/index-CouvvtMV.js +0 -86
  257. package/gui/dist/assets/tray-data-CYGdjCJ7.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 { waitForProviderRequestSlot } from "../../providers/request-pacing";
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<void>;
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
- input: Parameters<typeof globalThis.fetch>[0],
155
+ rawInput: Parameters<typeof globalThis.fetch>[0],
147
156
  init?: RequestInit,
148
157
  egress?: ProviderEgressBinding,
149
158
  ): Promise<Response> {
150
- const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
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 = decide ? providerEgressSendInit(egress, physicalFetch, input) : {};
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
- return Promise.resolve();
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
- const timeout = new AbortController();
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
- return await fetchExecutor(url, {
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 { resolveStallTimeoutSec } from "../../stall-timeout";
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: resolveStallTimeoutSec(config.stallTimeoutSec) * 1000,
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 < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
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 { resolveStallTimeoutSec } from "../../stall-timeout";
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
- resolveStallTimeoutSec(requestContext.config.stallTimeoutSec) * 1000,
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
- if (base > 0) return { attempts: base };
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
- * `GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST` and `ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST`
221
- * stay exactly as they are: they bound rotation within one credential roster. What neither
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({ sendClass, targetKey, countedExternally });
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
- const committed = await commitResolvedOAuthSelection(snapshot);
215
- if (!committed) return null;
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 preferredAccountId = isGenericFailoverProvider(route.providerName, route.provider)
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
- resolved = await getValidAccessTokenSnapshot(route.providerName);
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, true);
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;