@bitkyc08/opencodex 2.74.0 → 2.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{App-KLz1TAXi.css → App-ia4U0ufY.css} +1 -1
- package/gui/dist/assets/{App-B00Yq11u.js → App-xm0zGNOe.js} +3 -3
- package/gui/dist/assets/{Tray-D3Ij1Cvf.js → Tray-CGAD2AGk.js} +1 -1
- package/gui/dist/assets/{index-BYQQfBGK.js → index-D_RGAT6Q.js} +2 -2
- package/gui/dist/assets/{quota-summary-yPtcEhGI.js → quota-summary-CUFq48t-.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/devin/cloud-direct/chat.ts +38 -6
- package/src/adapters/devin/live-models.ts +28 -8
- package/src/adapters/devin.ts +1 -1
- package/src/claude/devin-output-order.ts +98 -0
- package/src/codex/auth-api/pool-quota-probe.ts +2 -1
- package/src/codex/catalog/retained-sync.ts +9 -0
- package/src/codex/inject/config-toml.ts +179 -248
- package/src/codex/inject/provider-table.ts +5 -2
- package/src/codex/inject/remove.ts +23 -8
- package/src/codex/injected-marker.ts +28 -23
- package/src/codex/quota-auto-refresh.ts +1 -1
- package/src/codex/quota.ts +12 -4
- package/src/codex/toml-source-lines.ts +93 -1
- package/src/config/live-reconcile.ts +18 -0
- package/src/config/rebase-provenance.ts +12 -0
- package/src/config.ts +4 -3
- package/src/generated/compatibility-version.json +50 -38
- package/src/images/loop.ts +2 -0
- package/src/integrations/cursor-local-installer.ts +29 -2
- package/src/lib/redact.ts +54 -8
- package/src/oauth/anthropic-account-refusal.ts +76 -0
- package/src/oauth/anthropic-routing.ts +38 -10
- package/src/providers/new-model-policy-runtime.ts +83 -0
- package/src/providers/new-model-policy.ts +25 -0
- package/src/providers/registry/entries-core.ts +3 -0
- package/src/providers/registry/model-seeds.ts +31 -2
- package/src/providers/xai-transport.ts +2 -2
- package/src/server/claude-messages.ts +33 -10
- package/src/server/management/agent-settings-routes.ts +3 -1
- package/src/server/management/model-rows.ts +34 -4
- package/src/server/management/shared.ts +18 -2
- package/src/server/responses/adapter-continuation.ts +13 -21
- package/src/server/responses/adapter-delivery.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +10 -21
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +2 -1
- package/src/server/responses/request-transport.ts +2 -0
- package/src/server/responses/run-turn-execution.ts +25 -5
- package/src/server/responses/sidecar-execution.ts +9 -14
- package/src/web-search/loop.ts +7 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
|
|
1
2
|
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
2
3
|
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
3
4
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
@@ -57,8 +58,6 @@ import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
|
57
58
|
import {
|
|
58
59
|
AnthropicAccountCooldownError,
|
|
59
60
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
60
|
-
rotateAnthropicAccountOn429,
|
|
61
|
-
recordAnthropicAccount429,
|
|
62
61
|
getAnthropicPoolAccessSnapshot,
|
|
63
62
|
formatAnthropicProviderForLog,
|
|
64
63
|
} from "../../oauth/anthropic-routing";
|
|
@@ -986,27 +985,22 @@ export async function prepareAdapterExchange(
|
|
|
986
985
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
987
986
|
}
|
|
988
987
|
|
|
989
|
-
//
|
|
990
|
-
//
|
|
988
|
+
// Anthropic OAuth: recover a rate limit or proven account entitlement refusal
|
|
989
|
+
// before output, within the shared request and account rotation limits.
|
|
991
990
|
while (
|
|
992
|
-
upstreamResponse.status === 429
|
|
991
|
+
(upstreamResponse.status === 429 || upstreamResponse.status === 403)
|
|
993
992
|
&& transportState.anthropicPoolAccountId
|
|
994
|
-
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
995
993
|
) {
|
|
996
|
-
const nextAccountId =
|
|
997
|
-
config,
|
|
998
|
-
transportState.
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
Date.now(),
|
|
1002
|
-
upstreamResponse.headers,
|
|
1003
|
-
transportState.anthropicRouteDecision,
|
|
1004
|
-
);
|
|
994
|
+
const nextAccountId = await rotateAnthropicAccountOnResponse(upstreamResponse, {
|
|
995
|
+
config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
|
|
996
|
+
decision: transportState.anthropicRouteDecision, signal: upstream.signal,
|
|
997
|
+
canRetry: transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
998
|
+
});
|
|
1005
999
|
if (!nextAccountId) break;
|
|
1006
|
-
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1007
1000
|
try {
|
|
1008
1001
|
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
1009
1002
|
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
1003
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1010
1004
|
transportState.anthropicPoolAccountId = admitted.accountId;
|
|
1011
1005
|
transportState.anthropicPoolFailovers += 1;
|
|
1012
1006
|
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
@@ -1026,11 +1020,6 @@ export async function prepareAdapterExchange(
|
|
|
1026
1020
|
break;
|
|
1027
1021
|
}
|
|
1028
1022
|
}
|
|
1029
|
-
if (upstreamResponse.status === 429 && transportState.anthropicPoolAccountId
|
|
1030
|
-
&& transportState.anthropicPoolFailovers >= ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST) {
|
|
1031
|
-
recordAnthropicAccount429(config, transportState.anthropicPoolAccountId,
|
|
1032
|
-
upstreamResponse.headers.get("retry-after"), Date.now(), upstreamResponse.headers);
|
|
1033
|
-
}
|
|
1034
1023
|
// Generic OAuth account failover (#2568) rotates reactively after a refusal when
|
|
1035
1024
|
// two accounts are stored. Kiro additionally classifies bounded 400/403 refusals;
|
|
1036
1025
|
// all other providers retain the original 429 loop below.
|
|
@@ -1259,7 +1259,8 @@ export async function handleResponsesCompact(
|
|
|
1259
1259
|
upstream.headers,
|
|
1260
1260
|
authCtx.writerGeneration,
|
|
1261
1261
|
authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined,
|
|
1262
|
-
{ modelId: route.modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined
|
|
1262
|
+
{ modelId: route.modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined,
|
|
1263
|
+
poolResponse: authCtx.kind === "pool" },
|
|
1263
1264
|
);
|
|
1264
1265
|
}
|
|
1265
1266
|
recordCompactPoolOutcome(authCtx, upstream.status, {
|
|
@@ -1328,7 +1329,7 @@ export async function handleResponsesCompact(
|
|
|
1328
1329
|
if (outcomeCtx.kind === "pool") {
|
|
1329
1330
|
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/quota");
|
|
1330
1331
|
applyAccountQuotaFromUpstreamHeaders(outcomeCtx.accountId, upstream.headers, outcomeCtx.writerGeneration,
|
|
1331
|
-
undefined, { modelId: route.modelId, poolWriter: outcomeCtx.poolQuotaWriter });
|
|
1332
|
+
undefined, { modelId: route.modelId, poolWriter: outcomeCtx.poolQuotaWriter, poolResponse: true });
|
|
1332
1333
|
}
|
|
1333
1334
|
const retryAfter = upstream.headers.get("retry-after");
|
|
1334
1335
|
const resetAt = [
|
|
@@ -127,7 +127,8 @@ export function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxPro
|
|
|
127
127
|
const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
|
|
128
128
|
return headers => {
|
|
129
129
|
if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return;
|
|
130
|
-
applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined
|
|
130
|
+
applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId, poolWriter: authCtx.kind === "pool" ? authCtx.poolQuotaWriter : undefined,
|
|
131
|
+
poolResponse: authCtx.kind === "pool" });
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -717,7 +718,8 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
717
718
|
firstResponse.headers,
|
|
718
719
|
firstAuthCtx.writerGeneration,
|
|
719
720
|
firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
|
|
720
|
-
{ modelId: route.modelId, poolWriter: firstAuthCtx.kind === "pool" ? firstAuthCtx.poolQuotaWriter : undefined
|
|
721
|
+
{ modelId: route.modelId, poolWriter: firstAuthCtx.kind === "pool" ? firstAuthCtx.poolQuotaWriter : undefined,
|
|
722
|
+
poolResponse: firstAuthCtx.kind === "pool" },
|
|
721
723
|
);
|
|
722
724
|
}
|
|
723
725
|
const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
|
|
@@ -487,7 +487,8 @@ export async function deliverPassthroughResponse(
|
|
|
487
487
|
if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
|
|
488
488
|
applyAccountQuotaFromUpstreamHeaders(admissionState.authCtx.accountId, upstreamResponse.headers,
|
|
489
489
|
admissionState.authCtx.writerGeneration, admissionState.authCtx.kind === "main-pool" ? admissionState.authCtx.mainQuotaWriter : undefined,
|
|
490
|
-
{ modelId: route.modelId, poolWriter: admissionState.authCtx.kind === "pool" ? admissionState.authCtx.poolQuotaWriter : undefined
|
|
490
|
+
{ modelId: route.modelId, poolWriter: admissionState.authCtx.kind === "pool" ? admissionState.authCtx.poolQuotaWriter : undefined,
|
|
491
|
+
poolResponse: admissionState.authCtx.kind === "pool" });
|
|
491
492
|
}
|
|
492
493
|
if (terminalBodyWillRecord) {
|
|
493
494
|
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bindAnthropicRefusalCredential } from "../../oauth/anthropic-account-refusal";
|
|
1
2
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
2
3
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
4
|
import type { OAuthAccessSnapshot } from "../../oauth";
|
|
@@ -521,6 +522,7 @@ export async function prepareResponsesTransport(
|
|
|
521
522
|
try {
|
|
522
523
|
const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
|
|
523
524
|
if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation) {
|
|
525
|
+
bindAnthropicRefusalCredential(response, snapshot);
|
|
524
526
|
recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, response.headers, writerGeneration);
|
|
525
527
|
}
|
|
526
528
|
} catch { /* best-effort observation cannot fail the response */ }
|
|
@@ -50,6 +50,7 @@ import { undeclaredToolCallMessage } from "../responses-undeclared-tool-guard";
|
|
|
50
50
|
import { planWebSearch } from "../../web-search";
|
|
51
51
|
import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
|
|
52
52
|
import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
|
|
53
|
+
import { orderDevinMessagesOutput } from "../../claude/devin-output-order";
|
|
53
54
|
|
|
54
55
|
// LOCAL PATCH (runturn-websearch): top-level fields route binding or the
|
|
55
56
|
// adapter itself may write during a turn. Iteration-local `turnParsed` objects
|
|
@@ -181,6 +182,13 @@ export async function executeResponsesRunTurn(
|
|
|
181
182
|
|
|
182
183
|
const runTurnAbort = new AbortController();
|
|
183
184
|
const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
|
|
185
|
+
const devinProducers = new Set<AbortController>();
|
|
186
|
+
const orderMessagesOutput = (source: AsyncIterable<AdapterEvent>): AsyncIterable<AdapterEvent> =>
|
|
187
|
+
inboundWire === "anthropic" && transportState.runTurnAdapter.name === "devin" && !routedCompaction
|
|
188
|
+
? orderDevinMessagesOutput(source, translatorBudget, runTurnAbort.signal, () => {
|
|
189
|
+
for (const producer of devinProducers) producer.abort();
|
|
190
|
+
})
|
|
191
|
+
: source;
|
|
184
192
|
const queue = createAdapterEventQueue({
|
|
185
193
|
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
186
194
|
});
|
|
@@ -229,6 +237,8 @@ export async function executeResponsesRunTurn(
|
|
|
229
237
|
options.onCompactionRecoveryAdapterEvent?.(event);
|
|
230
238
|
targetQueue.push(event);
|
|
231
239
|
};
|
|
240
|
+
let producerAbort = runTurnAbort;
|
|
241
|
+
let cleanupProducerAbort: (() => void) | undefined;
|
|
232
242
|
try {
|
|
233
243
|
if (!pacingSlotAcquired) {
|
|
234
244
|
pacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
@@ -261,11 +271,16 @@ export async function executeResponsesRunTurn(
|
|
|
261
271
|
turnScopedPacing: true,
|
|
262
272
|
},
|
|
263
273
|
);
|
|
274
|
+
if (inboundWire === "anthropic" && transportState.runTurnAdapter.name === "devin" && !routedCompaction) {
|
|
275
|
+
producerAbort = new AbortController();
|
|
276
|
+
cleanupProducerAbort = linkAbortSignal(producerAbort, runTurnAbort.signal);
|
|
277
|
+
devinProducers.add(producerAbort);
|
|
278
|
+
}
|
|
264
279
|
await transportState.runTurnAdapter.runTurn?.(
|
|
265
280
|
turnParsed,
|
|
266
281
|
{
|
|
267
282
|
headers: requestState.selectedForwardHeaders,
|
|
268
|
-
abortSignal:
|
|
283
|
+
abortSignal: producerAbort.signal,
|
|
269
284
|
comboAttempt: options.comboAttempt === true,
|
|
270
285
|
translatorBudget,
|
|
271
286
|
providerFetch: runTurnProviderFetch,
|
|
@@ -321,6 +336,8 @@ export async function executeResponsesRunTurn(
|
|
|
321
336
|
message: err instanceof Error ? err.message : String(err),
|
|
322
337
|
});
|
|
323
338
|
} finally {
|
|
339
|
+
devinProducers.delete(producerAbort);
|
|
340
|
+
cleanupProducerAbort?.();
|
|
324
341
|
releaseProviderRequestSlot(pacingSlot);
|
|
325
342
|
// Cursor assigns a stable conversation id inside runTurn on the first headerless
|
|
326
343
|
// turn; backfill so Logs can filter/total that opening request (#330 / #522).
|
|
@@ -345,14 +362,14 @@ export async function executeResponsesRunTurn(
|
|
|
345
362
|
});
|
|
346
363
|
void runTurnAttempt(iterQueue, undefined, false, iterParsed);
|
|
347
364
|
const stream = iterQueue.stream();
|
|
348
|
-
if (!runTurnFailoverArmed()) return stream;
|
|
365
|
+
if (!runTurnFailoverArmed()) return orderMessagesOutput(stream);
|
|
349
366
|
// LOCAL PATCH (runturn-websearch): a post-search iteration can open on a
|
|
350
367
|
// 429 too — the search cells already reached the client, so only this
|
|
351
368
|
// answer call rotates. Preflight replays it on the next account with the
|
|
352
369
|
// grown history intact; a mid-stream error still ends the turn as before.
|
|
353
|
-
return (async function* () {
|
|
370
|
+
return orderMessagesOutput((async function* () {
|
|
354
371
|
yield* await preflightRunTurnFailover(stream, iterParsed);
|
|
355
|
-
})();
|
|
372
|
+
})());
|
|
356
373
|
};
|
|
357
374
|
// Rebind the turn to an admitted account. The failed attempt emitted no client-visible bytes,
|
|
358
375
|
// so replay is safe, but a Cursor conversation/checkpoint is credential-scoped: carrying its
|
|
@@ -614,7 +631,7 @@ export async function executeResponsesRunTurn(
|
|
|
614
631
|
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
615
632
|
});
|
|
616
633
|
void runTurnAttempt(retryQueue, "empty-completion");
|
|
617
|
-
return retryQueue.stream();
|
|
634
|
+
return orderMessagesOutput(retryQueue.stream());
|
|
618
635
|
};
|
|
619
636
|
|
|
620
637
|
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
@@ -651,6 +668,7 @@ export async function executeResponsesRunTurn(
|
|
|
651
668
|
retryAfter: resolveClientRetryAfter({ status: httpStatus, message: error.message }),
|
|
652
669
|
});
|
|
653
670
|
};
|
|
671
|
+
// Messages ingress forces internal streaming, including buffered client requests.
|
|
654
672
|
if (parsed.stream) {
|
|
655
673
|
try {
|
|
656
674
|
void runTurn();
|
|
@@ -699,6 +717,8 @@ export async function executeResponsesRunTurn(
|
|
|
699
717
|
}
|
|
700
718
|
eventSource = preflight.stream;
|
|
701
719
|
}
|
|
720
|
+
// Preflight sees raw output before Messages delays text for the late signature.
|
|
721
|
+
eventSource = orderMessagesOutput(eventSource);
|
|
702
722
|
// LOCAL PATCH (runturn-websearch): intercept web_search calls across
|
|
703
723
|
// iterations; terminal output keeps flowing through the same queue/bridge.
|
|
704
724
|
if (wsPlan) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
|
|
1
2
|
import type { ResponsesRequestContext } from "./core-options";
|
|
2
3
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
4
|
import type { ResponsesTransport } from "./request-transport";
|
|
@@ -32,7 +33,6 @@ import { persistKiroAccountState } from "../../providers/kiro-account-state-disk
|
|
|
32
33
|
import { readDisplaySafeErrorText } from "./core-errors";
|
|
33
34
|
import {
|
|
34
35
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
35
|
-
rotateAnthropicAccountOn429,
|
|
36
36
|
getAnthropicPoolAccessSnapshot,
|
|
37
37
|
formatAnthropicProviderForLog,
|
|
38
38
|
} from "../../oauth/anthropic-routing";
|
|
@@ -183,7 +183,7 @@ export async function executeResponsesSidecars(
|
|
|
183
183
|
retryParsed?: OcxParsedRequest,
|
|
184
184
|
originalResponse?: Response,
|
|
185
185
|
): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
|
|
186
|
-
if (route.providerName !== "kiro" && originalResponse && originalResponse.status !== 429) return null;
|
|
186
|
+
if (route.providerName !== "kiro" && !(route.providerName === "anthropic" && originalResponse?.status === 403) && originalResponse && originalResponse.status !== 429) return null;
|
|
187
187
|
const refusal = route.providerName === "kiro" && originalResponse
|
|
188
188
|
? classifyKiroRefusal(originalResponse.status,
|
|
189
189
|
await readDisplaySafeErrorText(originalResponse.clone(), options.abortSignal ?? new AbortController().signal, "")).kind
|
|
@@ -200,7 +200,7 @@ export async function executeResponsesSidecars(
|
|
|
200
200
|
// sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
|
|
201
201
|
// rotation in the attempt row and in the Logs UI.
|
|
202
202
|
let recoveryKind: AttemptRecoveryKind = "key-429";
|
|
203
|
-
const rotated =
|
|
203
|
+
const rotated = !originalResponse || originalResponse.status === 429
|
|
204
204
|
? rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
205
205
|
retryAfter,
|
|
206
206
|
now: Date.now(),
|
|
@@ -260,7 +260,6 @@ export async function executeResponsesSidecars(
|
|
|
260
260
|
// web-search or image-bridge turn was terminal even with the pool fully enabled -- while
|
|
261
261
|
// the very same 429 on the main response path rotated.
|
|
262
262
|
transportState.anthropicPoolAccountId
|
|
263
|
-
&& transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST
|
|
264
263
|
) {
|
|
265
264
|
// Same intersection for the Anthropic roster: its own per-request bound still applies,
|
|
266
265
|
// and the shared budget decides whether this request may spend another send at all.
|
|
@@ -268,16 +267,12 @@ export async function executeResponsesSidecars(
|
|
|
268
267
|
"auth-recovery",
|
|
269
268
|
`${route.providerName}|${route.modelId}|sidecar-anthropic-429`,
|
|
270
269
|
);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
Date.now(),
|
|
278
|
-
responseHeaders,
|
|
279
|
-
transportState.anthropicRouteDecision,
|
|
280
|
-
);
|
|
270
|
+
const nextAccountId = await rotateAnthropicAccountOnResponse(
|
|
271
|
+
originalResponse ?? new Response(null, { status: 429, headers: responseHeaders ?? (retryAfter ? { "retry-after": retryAfter } : undefined) }), {
|
|
272
|
+
config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
|
|
273
|
+
decision: transportState.anthropicRouteDecision, signal: options.abortSignal,
|
|
274
|
+
canRetry: hop.allowed && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
275
|
+
});
|
|
281
276
|
if (!nextAccountId) {
|
|
282
277
|
hop.permit?.release();
|
|
283
278
|
return null;
|
package/src/web-search/loop.ts
CHANGED
|
@@ -325,7 +325,7 @@ export interface WebSearchLoopDeps {
|
|
|
325
325
|
/** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
|
|
326
326
|
onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
|
|
327
327
|
/**
|
|
328
|
-
*
|
|
328
|
+
* Account/key failover hook (429, plus provider-classified pre-output refusals): rotate the provider's active credential and return a rebuilt adapter,
|
|
329
329
|
* or null when the pool is exhausted. Async hooks support OAuth refresh; existing synchronous
|
|
330
330
|
* key-pool hooks remain valid.
|
|
331
331
|
*
|
|
@@ -346,6 +346,7 @@ export interface WebSearchLoopDeps {
|
|
|
346
346
|
retryAfterHeader: string | null,
|
|
347
347
|
responseHeaders?: Headers,
|
|
348
348
|
retryParsed?: OcxParsedRequest,
|
|
349
|
+
// A 403 requires this complete response; status-only callbacks retain 429 semantics.
|
|
349
350
|
originalResponse?: Response,
|
|
350
351
|
) =>
|
|
351
352
|
| { adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind }
|
|
@@ -405,6 +406,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
405
406
|
// One iteration beyond the forced answer is reserved for its empty-answer recovery below.
|
|
406
407
|
const HARD_CAP = maxSearches + 3;
|
|
407
408
|
let emptyAnswerRetries = 0;
|
|
409
|
+
let accountRefusalOutputStarted = false;
|
|
408
410
|
const connectTimeoutMs = deps.connectTimeoutMs ?? 200_000;
|
|
409
411
|
const routedModelStallTimeoutMs = deps.routedModelStallTimeoutMs ?? 200_000;
|
|
410
412
|
|
|
@@ -571,6 +573,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
571
573
|
// 429 key-failover parity with the normal routed path: rotate pool keys until one responds
|
|
572
574
|
// or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
|
|
573
575
|
while ((prepared.response.status === 429
|
|
576
|
+
|| (prepared.response.status === 403 && deps.incomingMeta?.providerName === "anthropic" && !accountRefusalOutputStarted)
|
|
574
577
|
|| (iterParsed._kiroAuthContext && (prepared.response.status === 400 || prepared.response.status === 403))) && deps.on429) {
|
|
575
578
|
const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
|
|
576
579
|
iterParsed, prepared.response);
|
|
@@ -663,10 +666,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
663
666
|
// Codex show only `Working` until both Kiro attempts had finished (often 30-40 seconds).
|
|
664
667
|
// Tool events remain buffered below, so the decision to invoke the hosted sidecar is still
|
|
665
668
|
// atomic and no search call can escape before its stream has validated successfully.
|
|
666
|
-
else if (event.type === "text_delta" && event.phase === "commentary") yield event;
|
|
669
|
+
else if (event.type === "text_delta" && event.phase === "commentary") { accountRefusalOutputStarted = true; yield event; }
|
|
667
670
|
else if (liveWindowOpen && LIVE_STREAMABLE.has(event.type)) {
|
|
668
671
|
// Live events are ALSO buffered: the scanner still needs them for thinking extraction
|
|
669
672
|
// and the forced-answer output check; only the terminal replay skips them (by count).
|
|
673
|
+
accountRefusalOutputStarted = true;
|
|
670
674
|
yield event;
|
|
671
675
|
streamedPassthroughCount++;
|
|
672
676
|
events.push(event);
|
|
@@ -937,6 +941,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
937
941
|
// The thinking that led to the search belongs to the FIRST call's assistant replay turn.
|
|
938
942
|
const iterationThinking = extractIterationThinking(split.passthrough);
|
|
939
943
|
for (const [callIndex, call] of split.calls.entries()) {
|
|
944
|
+
accountRefusalOutputStarted = true;
|
|
940
945
|
yield* runSearchCall(call, callIndex === 0 ? iterationThinking : []);
|
|
941
946
|
}
|
|
942
947
|
} catch (e) {
|