@bitkyc08/opencodex 2.45.0 → 2.46.0-preview.20260907
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/{index-J96sug5C.css → index-BFgUC17B.css} +1 -1
- package/gui/dist/assets/{index-CCfD72yq.js → index-NcAVXkST.js} +19 -19
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/raycast.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/openai-responses.ts +7 -0
- package/src/bridge.ts +27 -7
- package/src/claude/inbound.ts +23 -5
- package/src/claude/outbound.ts +87 -19
- package/src/cli/capabilities.ts +4 -1
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +2 -2
- package/src/cli/export-command.ts +22 -10
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +34 -7
- package/src/cli/integrations.ts +34 -1
- package/src/cli/provider.ts +27 -15
- package/src/cli/registry.ts +2 -2
- package/src/cli/version-skew.ts +36 -3
- package/src/clients/aside-profiles.ts +8 -7
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export/raycast.ts +106 -0
- package/src/clients/config-export.ts +36 -0
- package/src/clients/model-presentation.ts +61 -0
- package/src/codex/catalog/sync.ts +44 -2
- package/src/codex/convergence.ts +7 -0
- package/src/generated/compatibility-version.json +59 -43
- package/src/generated/model-metadata.ts +1 -0
- package/src/images/loop.ts +10 -2
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +158 -25
- package/src/integrations/raycast-detect.ts +111 -0
- package/src/integrations/registry.ts +18 -0
- package/src/integrations/state.ts +82 -13
- package/src/integrations/writer.ts +46 -31
- package/src/lib/bounded-body.ts +22 -7
- package/src/oauth/anthropic-routing.ts +59 -14
- package/src/oauth/health.ts +3 -0
- package/src/providers/quota.ts +145 -9
- package/src/providers/registry.ts +31 -0
- package/src/responses/parser.ts +16 -3
- package/src/responses/reasoning-envelope.ts +3 -2
- package/src/server/grok-responses-control-frame.ts +43 -0
- package/src/server/management/config-routes.ts +6 -2
- package/src/server/management/integration-routes.ts +29 -2
- package/src/server/management/model-routes.ts +9 -1
- package/src/server/request-decompress.ts +34 -8
- package/src/server/responses/agent-task-recovery-cache.ts +43 -14
- package/src/server/responses/agent-task-recovery.ts +28 -16
- package/src/server/responses/core.ts +39 -6
- package/src/web-search/loop.ts +10 -2
|
@@ -2,6 +2,20 @@ const MAX_CACHE_BYTES = 8 * 1024 * 1024;
|
|
|
2
2
|
const MAX_CONCURRENT_RECOVERIES = 32;
|
|
3
3
|
const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
4
4
|
|
|
5
|
+
export type AgentTaskRecoveryResolutionFailureReason =
|
|
6
|
+
| "recovery_unavailable"
|
|
7
|
+
| "caller_cancelled"
|
|
8
|
+
| "recovery_http_rejected"
|
|
9
|
+
| "recovery_timeout"
|
|
10
|
+
| "recovery_aborted"
|
|
11
|
+
| "recovery_transport_error"
|
|
12
|
+
| "recovery_invalid_output";
|
|
13
|
+
|
|
14
|
+
/** Shared flights carry bounded failures; only successful plaintext enters the cache. */
|
|
15
|
+
export type AgentTaskRecoveryResolution =
|
|
16
|
+
| { readonly recovered: true; readonly assignment: string }
|
|
17
|
+
| { readonly recovered: false; readonly reason: AgentTaskRecoveryResolutionFailureReason };
|
|
18
|
+
|
|
5
19
|
interface RecoveryCacheEntry {
|
|
6
20
|
assignment: string;
|
|
7
21
|
bytes: number;
|
|
@@ -11,7 +25,7 @@ interface RecoveryCacheEntry {
|
|
|
11
25
|
|
|
12
26
|
interface RecoveryFlight {
|
|
13
27
|
controller: AbortController;
|
|
14
|
-
promise: Promise<
|
|
28
|
+
promise: Promise<AgentTaskRecoveryResolution>;
|
|
15
29
|
waiters: number;
|
|
16
30
|
settled: boolean;
|
|
17
31
|
}
|
|
@@ -63,7 +77,7 @@ function insertRecoveryCacheEntry(key: string, assignment: string, maxEntries: n
|
|
|
63
77
|
function startRecoveryFlight(
|
|
64
78
|
key: string,
|
|
65
79
|
maxEntries: number,
|
|
66
|
-
request: (signal: AbortSignal) => Promise<
|
|
80
|
+
request: (signal: AbortSignal) => Promise<AgentTaskRecoveryResolution>,
|
|
67
81
|
): RecoveryFlight | null {
|
|
68
82
|
const active = RECOVERY_FLIGHTS.get(key);
|
|
69
83
|
if (active) return active;
|
|
@@ -72,15 +86,15 @@ function startRecoveryFlight(
|
|
|
72
86
|
const controller = new AbortController();
|
|
73
87
|
const flight: RecoveryFlight = {
|
|
74
88
|
controller,
|
|
75
|
-
promise: Promise.resolve(
|
|
89
|
+
promise: Promise.resolve({ recovered: false, reason: "recovery_unavailable" }),
|
|
76
90
|
waiters: 0,
|
|
77
91
|
settled: false,
|
|
78
92
|
};
|
|
79
93
|
flight.promise = request(controller.signal)
|
|
80
|
-
.then((
|
|
81
|
-
if (
|
|
82
|
-
insertRecoveryCacheEntry(key, assignment, maxEntries);
|
|
83
|
-
return
|
|
94
|
+
.then((result): AgentTaskRecoveryResolution => {
|
|
95
|
+
if (controller.signal.aborted) return { recovered: false, reason: "recovery_aborted" };
|
|
96
|
+
if (result.recovered) insertRecoveryCacheEntry(key, result.assignment, maxEntries);
|
|
97
|
+
return result;
|
|
84
98
|
})
|
|
85
99
|
.finally(() => {
|
|
86
100
|
flight.settled = true;
|
|
@@ -93,14 +107,14 @@ function startRecoveryFlight(
|
|
|
93
107
|
async function waitForRecoveryFlight(
|
|
94
108
|
flight: RecoveryFlight,
|
|
95
109
|
abortSignal?: AbortSignal,
|
|
96
|
-
): Promise<
|
|
97
|
-
if (abortSignal?.aborted) return
|
|
110
|
+
): Promise<AgentTaskRecoveryResolution> {
|
|
111
|
+
if (abortSignal?.aborted) return { recovered: false, reason: "caller_cancelled" };
|
|
98
112
|
flight.waiters += 1;
|
|
99
113
|
let onAbort: (() => void) | undefined;
|
|
100
114
|
try {
|
|
101
115
|
if (!abortSignal) return await flight.promise;
|
|
102
|
-
const cancelled = new Promise<
|
|
103
|
-
onAbort = () => resolve(
|
|
116
|
+
const cancelled = new Promise<AgentTaskRecoveryResolution>((resolve) => {
|
|
117
|
+
onAbort = () => resolve({ recovered: false, reason: "caller_cancelled" });
|
|
104
118
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
105
119
|
if (abortSignal.aborted) onAbort();
|
|
106
120
|
});
|
|
@@ -120,12 +134,27 @@ export async function resolveCachedAgentTaskRecovery(
|
|
|
120
134
|
request: (signal: AbortSignal) => Promise<string | null>,
|
|
121
135
|
abortSignal?: AbortSignal,
|
|
122
136
|
): Promise<string | null> {
|
|
123
|
-
|
|
137
|
+
const result = await resolveCachedAgentTaskRecoveryWithResult(key, maxEntries, async signal => {
|
|
138
|
+
const assignment = await request(signal);
|
|
139
|
+
return assignment
|
|
140
|
+
? { recovered: true, assignment }
|
|
141
|
+
: { recovered: false, reason: "recovery_unavailable" };
|
|
142
|
+
}, abortSignal);
|
|
143
|
+
return result.recovered ? result.assignment : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function resolveCachedAgentTaskRecoveryWithResult(
|
|
147
|
+
key: string,
|
|
148
|
+
maxEntries: number,
|
|
149
|
+
request: (signal: AbortSignal) => Promise<AgentTaskRecoveryResolution>,
|
|
150
|
+
abortSignal?: AbortSignal,
|
|
151
|
+
): Promise<AgentTaskRecoveryResolution> {
|
|
152
|
+
if (abortSignal?.aborted) return { recovered: false, reason: "caller_cancelled" };
|
|
124
153
|
sweepRecoveryCache(Date.now(), maxEntries);
|
|
125
154
|
const cached = RECOVERY_CACHE.get(key)?.assignment;
|
|
126
|
-
if (cached) return cached;
|
|
155
|
+
if (cached) return { recovered: true, assignment: cached };
|
|
127
156
|
const flight = startRecoveryFlight(key, maxEntries, request);
|
|
128
|
-
return flight ? waitForRecoveryFlight(flight, abortSignal) :
|
|
157
|
+
return flight ? waitForRecoveryFlight(flight, abortSignal) : { recovered: false, reason: "recovery_unavailable" };
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
export function discardCachedAgentTaskRecovery(key: string): void {
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
2
2
|
import { decodeJwtPayload, extractAccountId } from "../../oauth/chatgpt";
|
|
3
3
|
import type { OcxConfig } from "../../types";
|
|
4
|
-
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
4
|
+
import { boundedBodyDecodeFailure, readBoundedResponseBody } from "../../lib/bounded-body";
|
|
5
5
|
import { isApiAuthRequired, isProxyAdmissionSecret } from "../auth-cors";
|
|
6
6
|
import { structurallyValidFernetTokens } from "./encrypted-payload";
|
|
7
7
|
import {
|
|
8
8
|
cachedAgentTaskRecovery,
|
|
9
9
|
discardCachedAgentTaskRecovery,
|
|
10
10
|
resetAgentTaskRecoveryCache,
|
|
11
|
-
|
|
11
|
+
resolveCachedAgentTaskRecoveryWithResult,
|
|
12
|
+
type AgentTaskRecoveryResolution,
|
|
13
|
+
type AgentTaskRecoveryResolutionFailureReason,
|
|
12
14
|
} from "./agent-task-recovery-cache";
|
|
13
15
|
|
|
14
16
|
/** Experimental opt-in normalization through ChatGPT's fixed Codex endpoint. */
|
|
@@ -44,9 +46,8 @@ export interface AgentTaskRecoveryOptions {
|
|
|
44
46
|
export type AgentTaskRecoveryFailureReason =
|
|
45
47
|
| "unsupported_envelope"
|
|
46
48
|
| "admission_denied"
|
|
47
|
-
//
|
|
48
|
-
|
|
|
49
|
-
| "caller_cancelled"
|
|
49
|
+
// recovery_unavailable includes capacity rejection, which does not imply an upstream attempt.
|
|
50
|
+
| AgentTaskRecoveryResolutionFailureReason
|
|
50
51
|
| "input_changed";
|
|
51
52
|
|
|
52
53
|
export type AgentTaskRecoveryResult =
|
|
@@ -436,7 +437,7 @@ async function requestRecovery(
|
|
|
436
437
|
envelope: AgentEnvelope,
|
|
437
438
|
options: AgentTaskRecoveryOptions,
|
|
438
439
|
abortSignal?: AbortSignal,
|
|
439
|
-
): Promise<
|
|
440
|
+
): Promise<AgentTaskRecoveryResolution> {
|
|
440
441
|
const controller = new AbortController();
|
|
441
442
|
const timeout = setTimeout(
|
|
442
443
|
() => controller.abort(new DOMException("Agent task recovery timed out", "TimeoutError")),
|
|
@@ -454,8 +455,11 @@ async function requestRecovery(
|
|
|
454
455
|
redirect: "error",
|
|
455
456
|
});
|
|
456
457
|
if (!response.ok) {
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
// A rejected or never-settling cancellation must not extend the recovery deadline.
|
|
459
|
+
try { void response.body?.cancel().catch(() => undefined); } catch { /* already closed */ }
|
|
460
|
+
if (abortSignal?.aborted) return { recovered: false, reason: "recovery_aborted" };
|
|
461
|
+
if (controller.signal.aborted) return { recovered: false, reason: "recovery_timeout" };
|
|
462
|
+
return { recovered: false, reason: "recovery_http_rejected" };
|
|
459
463
|
}
|
|
460
464
|
const body = await readBoundedResponseBody(response, {
|
|
461
465
|
signal,
|
|
@@ -465,10 +469,18 @@ async function requestRecovery(
|
|
|
465
469
|
inactivityTimeoutMs: options.timeoutMs ?? 45_000,
|
|
466
470
|
firstByteTimeoutMs: options.timeoutMs ?? 45_000,
|
|
467
471
|
});
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
+
if (abortSignal?.aborted) return { recovered: false, reason: "recovery_aborted" };
|
|
473
|
+
if (controller.signal.aborted || body.timedOut) return { recovered: false, reason: "recovery_timeout" };
|
|
474
|
+
if (body.truncated || body.oversized || !body.displaySafe) return { recovered: false, reason: "recovery_invalid_output" };
|
|
475
|
+
const assignment = assignmentFromRecoverySse(body.text, envelope);
|
|
476
|
+
return assignment === null
|
|
477
|
+
? { recovered: false, reason: "recovery_invalid_output" }
|
|
478
|
+
: { recovered: true, assignment };
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (abortSignal?.aborted) return { recovered: false, reason: "recovery_aborted" };
|
|
481
|
+
const decodeFailure = boundedBodyDecodeFailure(error);
|
|
482
|
+
if (controller.signal.aborted || decodeFailure === "timeout") return { recovered: false, reason: "recovery_timeout" };
|
|
483
|
+
return { recovered: false, reason: decodeFailure === "invalid_utf8" ? "recovery_invalid_output" : "recovery_transport_error" };
|
|
472
484
|
} finally {
|
|
473
485
|
clearTimeout(timeout);
|
|
474
486
|
}
|
|
@@ -497,23 +509,23 @@ export async function recoverEncryptedAgentTaskWithResult(
|
|
|
497
509
|
const admitted = admittedRecovery(req, input, config, context.parentThreadId);
|
|
498
510
|
if (!admitted.admitted) return { recovered: false, reason: admitted.reason };
|
|
499
511
|
const { admission, cacheKey, envelope } = admitted.recovery;
|
|
500
|
-
const
|
|
512
|
+
const result = await resolveCachedAgentTaskRecoveryWithResult(
|
|
501
513
|
cacheKey,
|
|
502
514
|
options.cacheEntries ?? 200,
|
|
503
515
|
signal => requestRecovery(admission, envelope, options, signal),
|
|
504
516
|
context.abortSignal,
|
|
505
517
|
);
|
|
506
|
-
if (!
|
|
518
|
+
if (!result.recovered) {
|
|
507
519
|
return {
|
|
508
520
|
recovered: false,
|
|
509
|
-
reason: context.abortSignal?.aborted ? "caller_cancelled" :
|
|
521
|
+
reason: context.abortSignal?.aborted ? "caller_cancelled" : result.reason,
|
|
510
522
|
};
|
|
511
523
|
}
|
|
512
524
|
if (context.abortSignal?.aborted) {
|
|
513
525
|
discardCachedAgentTaskRecovery(cacheKey);
|
|
514
526
|
return { recovered: false, reason: "caller_cancelled" };
|
|
515
527
|
}
|
|
516
|
-
if (!injectAssignment(input, envelope, assignment)) {
|
|
528
|
+
if (!injectAssignment(input, envelope, result.assignment)) {
|
|
517
529
|
discardCachedAgentTaskRecovery(cacheKey);
|
|
518
530
|
return { recovered: false, reason: "input_changed" };
|
|
519
531
|
}
|
|
@@ -230,7 +230,7 @@ import {
|
|
|
230
230
|
} from "../../providers/request-pacing";
|
|
231
231
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
232
232
|
import { isMuseSubscriptionUsagePayload, parseMuseSubscriptionUsage } from "../../providers/muse-subscription-usage";
|
|
233
|
-
import { hasPassiveAccountQuota, recordPassiveAccountQuota } from "../../providers/quota";
|
|
233
|
+
import { hasPassiveAccountQuota, recordAnthropicAccountQuotaFromHeaders, recordPassiveAccountQuota } from "../../providers/quota";
|
|
234
234
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
235
235
|
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
236
236
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
@@ -374,6 +374,7 @@ import {
|
|
|
374
374
|
type UpstreamHostAdmissionLease,
|
|
375
375
|
} from "../../codex/upstream-host-health";
|
|
376
376
|
import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
|
|
377
|
+
import { createGrokResponsesControlFrameBlockRewrite } from "../grok-responses-control-frame";
|
|
377
378
|
import {
|
|
378
379
|
createResponsesSnapshotBlockRewrite,
|
|
379
380
|
hasResponsesSnapshotRepair,
|
|
@@ -3946,7 +3947,27 @@ async function handleResponsesInner(
|
|
|
3946
3947
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
3947
3948
|
if (selectionIsCurrent(requestBindings.get(wireRequest))) {
|
|
3948
3949
|
const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
|
|
3949
|
-
|
|
3950
|
+
const binding = requestBindings.get(wireRequest);
|
|
3951
|
+
const snapshot = route.providerName === "anthropic" && anthropicPoolAccountId && binding?.kind === "oauth"
|
|
3952
|
+
? binding.snapshot : undefined;
|
|
3953
|
+
const writerGeneration = snapshot ? captureConfigGeneration() : 0;
|
|
3954
|
+
const sentHeaders = snapshot ? new Headers(dispatchInit.headers) : undefined;
|
|
3955
|
+
const ownsBearer = snapshot !== undefined
|
|
3956
|
+
&& sentHeaders?.get("authorization") === `Bearer ${snapshot.accessToken}`
|
|
3957
|
+
&& !sentHeaders?.has("x-api-key");
|
|
3958
|
+
const response = await fetchImpl(destination, dispatchInit);
|
|
3959
|
+
// Observe each physical response before retries replace it. The binding belongs to
|
|
3960
|
+
// this dispatch, so a manual switch cannot file A's headers against B. Header
|
|
3961
|
+
// overrides and credential replacement make ownership unprovable: skip those writes.
|
|
3962
|
+
if (ownsBearer && snapshot) {
|
|
3963
|
+
try {
|
|
3964
|
+
const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
|
|
3965
|
+
if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation) {
|
|
3966
|
+
recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, response.headers, writerGeneration);
|
|
3967
|
+
}
|
|
3968
|
+
} catch { /* best-effort observation cannot fail the response */ }
|
|
3969
|
+
}
|
|
3970
|
+
return response;
|
|
3950
3971
|
}
|
|
3951
3972
|
const nextAdapter = await refreshDispatchAdapter(requestParsed);
|
|
3952
3973
|
const rebuilt = await nextAdapter.buildRequest(requestParsed, {
|
|
@@ -5494,9 +5515,9 @@ async function handleResponsesInner(
|
|
|
5494
5515
|
// Grok Build renders deltas live but reconstructs its durable assistant
|
|
5495
5516
|
// turn from the completed response snapshot. Native Responses streams
|
|
5496
5517
|
// may instead carry the complete items in output_item.done, so the
|
|
5497
|
-
// explicit Grok compatibility marker enables strict
|
|
5518
|
+
// explicit Grok compatibility marker enables strict client compatibility rewrites.
|
|
5498
5519
|
// The provider's broader snapshot/lifecycle repair remains opt-in.
|
|
5499
|
-
const
|
|
5520
|
+
const grokClientCompatibilityEnabled = logCtx.surface === "grok";
|
|
5500
5521
|
const snapshotRepairEnabled = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair);
|
|
5501
5522
|
const githubCopilotRepairEnabled = route.providerName === "github-copilot";
|
|
5502
5523
|
const responseModelRewrite = parsed._responseModelId !== undefined
|
|
@@ -5545,7 +5566,10 @@ async function handleResponsesInner(
|
|
|
5545
5566
|
githubCopilotRepairEnabled
|
|
5546
5567
|
? createGithubCopilotResponsesBlockRewrite(translatorBudget)
|
|
5547
5568
|
: undefined,
|
|
5548
|
-
|
|
5569
|
+
grokClientCompatibilityEnabled
|
|
5570
|
+
? createGrokResponsesControlFrameBlockRewrite()
|
|
5571
|
+
: undefined,
|
|
5572
|
+
grokClientCompatibilityEnabled
|
|
5549
5573
|
? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
|
|
5550
5574
|
: undefined,
|
|
5551
5575
|
snapshotRepairEnabled
|
|
@@ -5956,7 +5980,10 @@ async function handleResponsesInner(
|
|
|
5956
5980
|
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
5957
5981
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
5958
5982
|
const canRunWebSearch = !!wsPlan && !adapter.runTurn;
|
|
5959
|
-
const rotateSidecarProviderOn429 = async (
|
|
5983
|
+
const rotateSidecarProviderOn429 = async (
|
|
5984
|
+
retryAfter: string | null,
|
|
5985
|
+
responseHeaders?: Headers,
|
|
5986
|
+
): Promise<ProviderAdapter | null> => {
|
|
5960
5987
|
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
5961
5988
|
retryAfter,
|
|
5962
5989
|
now: Date.now(),
|
|
@@ -6000,6 +6027,8 @@ async function handleResponsesInner(
|
|
|
6000
6027
|
anthropicPoolAccountId,
|
|
6001
6028
|
retryAfter,
|
|
6002
6029
|
anthropicSessionKey,
|
|
6030
|
+
Date.now(),
|
|
6031
|
+
responseHeaders,
|
|
6003
6032
|
);
|
|
6004
6033
|
if (!nextAccountId) return null;
|
|
6005
6034
|
try {
|
|
@@ -7032,6 +7061,8 @@ async function handleResponsesInner(
|
|
|
7032
7061
|
anthropicPoolAccountId,
|
|
7033
7062
|
upstreamResponse.headers.get("retry-after"),
|
|
7034
7063
|
anthropicSessionKey,
|
|
7064
|
+
Date.now(),
|
|
7065
|
+
upstreamResponse.headers,
|
|
7035
7066
|
);
|
|
7036
7067
|
if (!nextAccountId) break;
|
|
7037
7068
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
@@ -7445,6 +7476,8 @@ async function handleResponsesInner(
|
|
|
7445
7476
|
anthropicPoolAccountId,
|
|
7446
7477
|
response.headers.get("retry-after"),
|
|
7447
7478
|
anthropicSessionKey,
|
|
7479
|
+
Date.now(),
|
|
7480
|
+
response.headers,
|
|
7448
7481
|
);
|
|
7449
7482
|
if (nextAccountId) {
|
|
7450
7483
|
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
package/src/web-search/loop.ts
CHANGED
|
@@ -309,8 +309,16 @@ export interface WebSearchLoopDeps {
|
|
|
309
309
|
* 429 failover hook: rotate the provider's active credential and return a rebuilt adapter,
|
|
310
310
|
* or null when the pool is exhausted. Async hooks support OAuth refresh; existing synchronous
|
|
311
311
|
* key-pool hooks remain valid.
|
|
312
|
+
*
|
|
313
|
+
* `responseHeaders` carries the whole refusal, not just Retry-After, because an Anthropic
|
|
314
|
+
* 429 states the window's reset epoch even when it omits Retry-After -- and a rotation that
|
|
315
|
+
* cannot see it cools the drained account for the short default instead of until the window
|
|
316
|
+
* actually reopens. Optional so existing callers keep compiling.
|
|
312
317
|
*/
|
|
313
|
-
on429?: (
|
|
318
|
+
on429?: (
|
|
319
|
+
retryAfterHeader: string | null,
|
|
320
|
+
responseHeaders?: Headers,
|
|
321
|
+
) => ProviderAdapter | null | Promise<ProviderAdapter | null>;
|
|
314
322
|
/** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
|
|
315
323
|
retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
|
|
316
324
|
/** Called only when the final bridged Responses stream reaches completed or incomplete. */
|
|
@@ -521,7 +529,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
521
529
|
// 429 key-failover parity with the normal routed path: rotate pool keys until one responds
|
|
522
530
|
// or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
|
|
523
531
|
while (prepared.response.status === 429 && deps.on429) {
|
|
524
|
-
const rotated = await deps.on429(prepared.response.headers.get("retry-after"));
|
|
532
|
+
const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers);
|
|
525
533
|
if (!rotated) break;
|
|
526
534
|
// Never let a broken body's cancel promise outlive the cumulative header deadline. Observe
|
|
527
535
|
// it, but proceed immediately to the rotated fetch under the SAME deadline signal.
|