@oh-my-pi/pi-ai 17.1.7 → 17.2.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 (62) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/types/auth-broker/server.d.ts +5 -0
  3. package/dist/types/auth-broker/types.d.ts +4 -0
  4. package/dist/types/auth-storage.d.ts +26 -1
  5. package/dist/types/error/classes.d.ts +5 -1
  6. package/dist/types/error/rate-limit.d.ts +5 -4
  7. package/dist/types/providers/anthropic-client.d.ts +13 -1
  8. package/dist/types/providers/anthropic.d.ts +9 -1
  9. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  10. package/dist/types/providers/cursor-pi-args.d.ts +99 -0
  11. package/dist/types/providers/cursor.d.ts +53 -3
  12. package/dist/types/providers/error-message.d.ts +2 -4
  13. package/dist/types/providers/openai-codex-responses.d.ts +10 -0
  14. package/dist/types/providers/openai-responses-wire.d.ts +8 -0
  15. package/dist/types/providers/openai-shared.d.ts +13 -1
  16. package/dist/types/registry/exa.d.ts +8 -0
  17. package/dist/types/registry/registry.d.ts +7 -1
  18. package/dist/types/registry/xai.d.ts +4 -1
  19. package/dist/types/stream.d.ts +2 -0
  20. package/dist/types/types.d.ts +101 -1
  21. package/dist/types/usage/umans.d.ts +2 -0
  22. package/dist/types/utils/block-symbols.d.ts +11 -0
  23. package/dist/types/utils/harmony-leak.d.ts +15 -7
  24. package/dist/types/utils/schema/normalize.d.ts +1 -0
  25. package/dist/types/utils.d.ts +8 -0
  26. package/package.json +4 -4
  27. package/src/auth-broker/client.ts +5 -1
  28. package/src/auth-broker/server.ts +103 -9
  29. package/src/auth-broker/snapshot-cache.ts +12 -3
  30. package/src/auth-broker/types.ts +6 -0
  31. package/src/auth-storage.ts +447 -28
  32. package/src/error/classes.ts +98 -3
  33. package/src/error/flags.ts +6 -1
  34. package/src/error/rate-limit.ts +18 -12
  35. package/src/providers/amazon-bedrock.ts +3 -3
  36. package/src/providers/anthropic-client.ts +24 -2
  37. package/src/providers/anthropic.ts +31 -6
  38. package/src/providers/cursor/exec-modern.ts +495 -0
  39. package/src/providers/cursor/proto/agent.proto +1007 -0
  40. package/src/providers/cursor-pi-args.ts +135 -0
  41. package/src/providers/cursor.ts +1138 -66
  42. package/src/providers/devin.ts +2 -1
  43. package/src/providers/error-message.ts +3 -1
  44. package/src/providers/google-vertex.ts +3 -5
  45. package/src/providers/openai-codex-responses.ts +109 -27
  46. package/src/providers/openai-completions.ts +19 -4
  47. package/src/providers/openai-responses-wire.ts +8 -0
  48. package/src/providers/openai-responses.ts +12 -1
  49. package/src/providers/openai-shared.ts +83 -14
  50. package/src/registry/api-key-validation.ts +18 -34
  51. package/src/registry/exa.ts +19 -0
  52. package/src/registry/novita.ts +6 -3
  53. package/src/registry/registry.ts +3 -1
  54. package/src/registry/xai.ts +17 -1
  55. package/src/stream.ts +3 -4
  56. package/src/types.ts +115 -0
  57. package/src/usage/umans.ts +192 -0
  58. package/src/utils/block-symbols.ts +12 -0
  59. package/src/utils/harmony-leak.ts +32 -0
  60. package/src/utils/idle-iterator.ts +2 -2
  61. package/src/utils/schema/normalize.ts +140 -38
  62. package/src/utils.ts +173 -3
@@ -1,4 +1,5 @@
1
1
  import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
2
+ import { AbortError } from "./abort";
2
3
 
3
4
  /** Prefix on errors raised when an Anthropic SSE stream envelope is malformed. */
4
5
  export const STREAM_ENVELOPE_ERROR_PREFIX = "Anthropic stream envelope error:";
@@ -68,6 +69,22 @@ export class OpenAIHttpError extends ProviderHttpError {
68
69
  }
69
70
  }
70
71
 
72
+ /** Non-2xx response from the Anthropic API. */
73
+ const DEFAULT_ANTHROPIC_ERROR_BODY_READ_TIMEOUT_MS = 5_000;
74
+ const MAX_ANTHROPIC_ERROR_BODY_BYTES = 64 * 1024;
75
+ const ANTHROPIC_ERROR_BODY_TRUNCATION_MARKER = "\n[Response body truncated after 64 KiB]";
76
+ let anthropicErrorBodyReadTimeoutMs = DEFAULT_ANTHROPIC_ERROR_BODY_READ_TIMEOUT_MS;
77
+
78
+ /** Test-only control for the otherwise bounded Anthropic error-body drain. */
79
+ export const __anthropicApiErrorForTesting = {
80
+ setBodyReadTimeoutMs(timeoutMs: number | undefined): void {
81
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs < 0)) {
82
+ throw new RangeError("Anthropic error-body timeout must be a non-negative finite number.");
83
+ }
84
+ anthropicErrorBodyReadTimeoutMs = timeoutMs ?? DEFAULT_ANTHROPIC_ERROR_BODY_READ_TIMEOUT_MS;
85
+ },
86
+ };
87
+
71
88
  /** Non-2xx response from the Anthropic API. */
72
89
  export class AnthropicApiError extends ProviderHttpError {
73
90
  declare readonly headers: Headers;
@@ -79,9 +96,87 @@ export class AnthropicApiError extends ProviderHttpError {
79
96
  this.requestId = headers.get("request-id");
80
97
  }
81
98
 
82
- static async fromResponse(response: Response): Promise<AnthropicApiError> {
83
- const body = await response.text().catch(() => "");
84
- const detail = body.trim() || "status code (no body)";
99
+ static async fromResponse(response: Response, signal?: AbortSignal): Promise<AnthropicApiError> {
100
+ // Avoid getReader() throwing when another consumer already owns the body.
101
+ const reader = response.body?.locked ? undefined : response.body?.getReader();
102
+
103
+ if (!reader) {
104
+ if (signal?.aborted) throw new AbortError("Request was aborted.");
105
+ const detail = "status code (no body)";
106
+ return new AnthropicApiError(response.status, `${response.status} ${detail}`, response.headers);
107
+ }
108
+
109
+ let aborted = false;
110
+ let timedOut = false;
111
+ let readerCancelled = false;
112
+ const cancelReader = () => {
113
+ if (readerCancelled) return;
114
+ readerCancelled = true;
115
+ void reader.cancel().catch(() => {});
116
+ };
117
+ const onAbort = () => {
118
+ if (aborted) return;
119
+ aborted = true;
120
+ cancelReader();
121
+ };
122
+ if (signal?.aborted) onAbort();
123
+ else signal?.addEventListener("abort", onAbort, { once: true });
124
+
125
+ const deadline = performance.now() + anthropicErrorBodyReadTimeoutMs;
126
+ let timeout: Timer | undefined;
127
+ timeout = setTimeout(() => {
128
+ timedOut = true;
129
+ cancelReader();
130
+ }, anthropicErrorBodyReadTimeoutMs);
131
+
132
+ let capturedBytes = 0;
133
+ let truncated = false;
134
+ const bodyChunks: string[] = [];
135
+ try {
136
+ const decoder = new TextDecoder();
137
+ let cleanEof = false;
138
+ while (!aborted && !timedOut) {
139
+ if (performance.now() >= deadline) {
140
+ timedOut = true;
141
+ cancelReader();
142
+ break;
143
+ }
144
+
145
+ let result: { readonly done: boolean; readonly value?: Uint8Array };
146
+ try {
147
+ result = await reader.read();
148
+ } catch {
149
+ break;
150
+ }
151
+ if (aborted || timedOut) break;
152
+ if (result.done) {
153
+ cleanEof = true;
154
+ break;
155
+ }
156
+
157
+ const chunk = result.value;
158
+ if (!chunk) break;
159
+ const bytesToCapture = Math.min(MAX_ANTHROPIC_ERROR_BODY_BYTES - capturedBytes, chunk.byteLength);
160
+ if (bytesToCapture > 0) {
161
+ bodyChunks.push(decoder.decode(chunk.subarray(0, bytesToCapture), { stream: true }));
162
+ capturedBytes += bytesToCapture;
163
+ }
164
+ if (bytesToCapture < chunk.byteLength) {
165
+ truncated = true;
166
+ cancelReader();
167
+ break;
168
+ }
169
+ }
170
+ if (aborted || signal?.aborted) throw new AbortError("Request was aborted.");
171
+ if (cleanEof) bodyChunks.push(decoder.decode());
172
+ if (truncated) bodyChunks.push(ANTHROPIC_ERROR_BODY_TRUNCATION_MARKER);
173
+ } finally {
174
+ if (timeout !== undefined) clearTimeout(timeout);
175
+ signal?.removeEventListener("abort", onAbort);
176
+ reader.releaseLock();
177
+ }
178
+
179
+ const detail = bodyChunks.join("").trim() || "status code (no body)";
85
180
  return new AnthropicApiError(response.status, `${response.status} ${detail}`, response.headers);
86
181
  }
87
182
  }
@@ -123,6 +123,8 @@ const SCHEMA_COMPILE_PATTERN = /compil/i;
123
123
  const INVALID_REQUEST_PATTERN = /invalid_request_error/i;
124
124
  const STRUCTURED_OUTPUTS_PATTERN = /structured[_ -]?outputs?/i;
125
125
  const FEATURE_NOT_SUPPORTED_PATTERN = /not (?:supported|available|enabled)|unsupported|does(?: not|n'?t) support/i;
126
+ const ANTHROPIC_STRICT_FIELD_PATTERN = /\btools\.\d+\.custom\.strict\b/i;
127
+ const EXTRA_INPUTS_NOT_PERMITTED_PATTERN = /extra inputs? (?:are|is) not permitted/i;
126
128
  // Anthropic fast-mode unsupported: 400 rejecting `speed`, or 429 rate_limit_error
127
129
  // because the account lacks the extra-usage entitlement fast mode requires.
128
130
  const FAST_MODE_SPEED_PARAM_PATTERN = /\bspeed\b/i;
@@ -138,6 +140,9 @@ const OAUTH_HTTP_AUTH_PATTERN = /\b401\b/;
138
140
 
139
141
  function matchesStrictToolsRejection(message: string, errorStatus: number | undefined): boolean {
140
142
  if (errorStatus !== 400) return false;
143
+ if (ANTHROPIC_STRICT_FIELD_PATTERN.test(message) && EXTRA_INPUTS_NOT_PERMITTED_PATTERN.test(message)) {
144
+ return true;
145
+ }
141
146
  if (STRUCTURED_OUTPUTS_PATTERN.test(message) && FEATURE_NOT_SUPPORTED_PATTERN.test(message)) return true;
142
147
  if (!INVALID_REQUEST_PATTERN.test(message)) return false;
143
148
  const grammarTooLarge = GRAMMAR_TOO_LARGE_PATTERN.test(message) && GRAMMAR_TOO_LARGE_DETAIL_PATTERN.test(message);
@@ -205,8 +210,8 @@ export function is(id: number | undefined, flag: Flag): boolean {
205
210
 
206
211
  export function retriable(id: number | undefined, opts?: { replayUnsafe?: boolean }): boolean {
207
212
  if (is(id, Flag.ContentBlocked)) return false;
208
- if (is(id, Flag.MalformedFunctionCall)) return true;
209
213
  if (opts?.replayUnsafe) return false;
214
+ if (is(id, Flag.MalformedFunctionCall)) return true;
210
215
  return ((id ?? 0) & RETRIABLE_KINDS) !== 0;
211
216
  }
212
217
 
@@ -21,17 +21,25 @@ const ACCOUNT_RATE_LIMIT_PATTERN =
21
21
  const INSUFFICIENT_BALANCE_PATTERN = /insufficient.?balance/i;
22
22
  const SPEND_LIMIT_PATTERN = /spend.?limit/i;
23
23
  const OPENROUTER_DAILY_FREE_LIMIT_PATTERN = /\bfree[-_ ]models[-_ ]per[-_ ]day\b/i;
24
+ // gRPC/Connect end-streams carry the status as its name (`resource_exhausted`),
25
+ // while HTTP bodies use the phrase ("resource exhausted"). Strip either form
26
+ // before classifying explicit details; an otherwise opaque status is transient
27
+ // model capacity, while quota/rate-limit/server wording remains authoritative.
28
+ const RESOURCE_EXHAUSTED_PATTERN = /resource.?exhausted/gi;
24
29
 
25
30
  /**
26
31
  * Classify a rate-limit error message into a reason category.
27
- * Priority order: QUOTA (Antigravity "quota will reset") > MODEL_CAPACITY > QUOTA (account) >
28
- * RATE_LIMIT > QUOTA (generic) > SERVER_ERROR > UNKNOWN.
32
+ * Priority order: explicit details in a resource-exhausted error > QUOTA
33
+ * (Antigravity "quota will reset") > MODEL_CAPACITY > QUOTA (account) >
34
+ * RATE_LIMIT > QUOTA (generic) > SERVER_ERROR > bare resource-exhausted > UNKNOWN.
29
35
  *
30
- * "resource exhausted" maps to MODEL_CAPACITY (transient, short wait)
31
- * "quota exceeded" / "quota will reset" maps to QUOTA_EXHAUSTED (long wait, switch account)
36
+ * Bare "resource exhausted" / "resource_exhausted" maps to MODEL_CAPACITY (transient, short wait).
37
+ * Explicit details such as "quota exceeded" retain their normal classification.
32
38
  */
33
39
  export function parseRateLimitReason(errorMessage: string): RateLimitReason {
34
- const lower = errorMessage.toLowerCase();
40
+ const lowerWithStatus = errorMessage.toLowerCase();
41
+ const lower = lowerWithStatus.replace(RESOURCE_EXHAUSTED_PATTERN, "");
42
+ const hasResourceExhaustedStatus = lower !== lowerWithStatus;
35
43
 
36
44
  // Antigravity / Cloud Code Assist surface multi-hour daily-quota exhaustion as
37
45
  // "You have exhausted your capacity on this model. Your quota will reset after …".
@@ -42,13 +50,7 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
42
50
  return "QUOTA_EXHAUSTED";
43
51
  }
44
52
 
45
- if (
46
- lower.includes("capacity") ||
47
- lower.includes("overloaded") ||
48
- lower.includes("529") ||
49
- lower.includes("503") ||
50
- lower.includes("resource exhausted")
51
- ) {
53
+ if (lower.includes("capacity") || lower.includes("overloaded") || lower.includes("529") || lower.includes("503")) {
52
54
  return "MODEL_CAPACITY_EXHAUSTED";
53
55
  }
54
56
 
@@ -92,6 +94,10 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
92
94
  return "SERVER_ERROR";
93
95
  }
94
96
 
97
+ if (hasResourceExhaustedStatus) {
98
+ return "MODEL_CAPACITY_EXHAUSTED";
99
+ }
100
+
95
101
  return "UNKNOWN";
96
102
  }
97
103
 
@@ -29,7 +29,7 @@ import type {
29
29
  ToolCall,
30
30
  ToolResultMessage,
31
31
  } from "../types";
32
- import { normalizeToolCallId, resolveCacheRetention } from "../utils";
32
+ import { normalizeSystemPrompts, normalizeToolCallId, resolveCacheRetention } from "../utils";
33
33
  import {
34
34
  clearStreamingPartialJson,
35
35
  kStreamingBlockIndex,
@@ -752,10 +752,10 @@ function supportsThinkingSignature(model: Model<"bedrock-converse-stream">): boo
752
752
  }
753
753
 
754
754
  function buildSystemPrompt(
755
- systemPrompt: readonly string[] | undefined,
755
+ systemPrompt: readonly string[] | string | undefined,
756
756
  promptCachePolicy: BedrockPromptCachePolicy,
757
757
  ): SystemContent[] | undefined {
758
- const prompts = systemPrompt?.map(prompt => prompt.toWellFormed()).filter(prompt => prompt.length > 0) ?? [];
758
+ const prompts = normalizeSystemPrompts(systemPrompt);
759
759
  if (prompts.length === 0) return undefined;
760
760
 
761
761
  const blocks: SystemContent[] = prompts.map(prompt => ({ text: prompt }));
@@ -43,6 +43,12 @@ export interface AnthropicRequestOptions {
43
43
  timeout?: number;
44
44
  /** Per-request retry budget override. */
45
45
  maxRetries?: number;
46
+ /**
47
+ * Maximum delay in milliseconds to wait for a server-directed retry. If the
48
+ * server's `retry-after` hint exceeds this value, the retry is declined and
49
+ * the original error is surfaced. Non-positive values disable the cap. Defaults to 60000.
50
+ */
51
+ maxRetryDelayMs?: number;
46
52
  /** Per-request headers merged after client defaults. */
47
53
  headers?: Record<string, string>;
48
54
  }
@@ -74,6 +80,12 @@ export interface AnthropicClientOptions {
74
80
  authToken?: string | null;
75
81
  baseURL?: string | null;
76
82
  maxRetries?: number;
83
+ /**
84
+ * Maximum delay in milliseconds to wait for a server-directed retry. If the
85
+ * server's `retry-after` hint exceeds this value, the retry is declined and
86
+ * the original error is surfaced. Non-positive values disable the cap. Defaults to 60000.
87
+ */
88
+ maxRetryDelayMs?: number;
77
89
  /** Pre-response timeout in milliseconds. Defaults to 10 minutes. */
78
90
  timeout?: number;
79
91
  defaultHeaders?: Record<string, string>;
@@ -97,7 +109,7 @@ function shouldRetryResponse(response: Response): boolean {
97
109
  }
98
110
 
99
111
  /** Server-suggested delay (`retry-after-ms`, then `retry-after` seconds or HTTP date). */
100
- export function retryDelayFromHeaders(headers: Headers | undefined): number | undefined {
112
+ export function retryDelayFromHeaders(headers: Pick<Headers, "get"> | undefined): number | undefined {
101
113
  if (!headers) return undefined;
102
114
  const retryAfterMs = headers.get("retry-after-ms");
103
115
  if (retryAfterMs) {
@@ -216,6 +228,7 @@ export class AnthropicMessagesClient implements AnthropicMessagesClientLike {
216
228
  const callerSignal = options?.signal;
217
229
  const timeoutMs = options?.timeout ?? opts.timeout ?? DEFAULT_TIMEOUT_MS;
218
230
  const maxRetries = Math.max(0, options?.maxRetries ?? opts.maxRetries ?? DEFAULT_MAX_RETRIES);
231
+ const maxRetryDelayMs = options?.maxRetryDelayMs ?? opts.maxRetryDelayMs ?? 60_000;
219
232
  const url = `${opts.baseURL ?? "https://api.anthropic.com"}${path}`;
220
233
  const headers = this.#buildHeaders(options?.headers);
221
234
  const body = JSON.stringify(params);
@@ -239,11 +252,20 @@ export class AnthropicMessagesClient implements AnthropicMessagesClientLike {
239
252
  if (response.ok) return response;
240
253
 
241
254
  if (attempt < maxRetries && shouldRetryResponse(response)) {
255
+ // Bound the server-directed wait: an over-cap `retry-after` declines
256
+ // the retry and surfaces the original error (status/body/headers
257
+ // intact) so higher-level recovery can run. A non-positive cap disables enforcement.
258
+ // Checked before draining the body so `fromResponse` can still read it.
259
+ const headerDelayMs = retryDelayFromHeaders(response.headers);
260
+ if (headerDelayMs !== undefined && maxRetryDelayMs > 0 && headerDelayMs > maxRetryDelayMs) {
261
+ throw await AIError.AnthropicApiError.fromResponse(response, callerSignal);
262
+ }
242
263
  await response.body?.cancel().catch(() => {});
243
264
  await this.#backoff(attempt, response.headers, callerSignal);
244
265
  continue;
245
266
  }
246
- throw await AIError.AnthropicApiError.fromResponse(response);
267
+
268
+ throw await AIError.AnthropicApiError.fromResponse(response, callerSignal);
247
269
  }
248
270
  }
249
271
 
@@ -60,19 +60,18 @@ import { isFoundryEnabled } from "../utils/foundry";
60
60
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
61
61
  import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
62
62
  import { notifyProviderResponse } from "../utils/provider-response";
63
+ import { getHeadersFromError, getRetryAfterMsFromHeaders } from "../utils/retry-after";
63
64
  import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
64
65
  import { spillToDescription } from "../utils/schema/spill";
65
66
  import { createSdkStreamRequestOptions } from "../utils/sdk-stream-timeout";
66
67
  import { notifyRawSseEvent } from "../utils/sse-debug";
67
68
  import { isForcedToolChoice } from "../utils/tool-choice";
68
69
  import {
69
- AnthropicApiError,
70
70
  AnthropicConnectionTimeoutError,
71
71
  type AnthropicFetchOptions,
72
72
  AnthropicMessagesClient,
73
73
  type AnthropicMessagesClientLike,
74
74
  calculateAnthropicRetryDelayMs,
75
- retryDelayFromHeaders,
76
75
  } from "./anthropic-client";
77
76
  import {
78
77
  type ToolInputSchema as AnthropicToolInputSchema,
@@ -450,6 +449,20 @@ export function clearAnthropicFastModeFallback(
450
449
  (value as AnthropicProviderSessionState).fastModeDisabled = false;
451
450
  }
452
451
  }
452
+ /**
453
+ * Whether the direct Anthropic model's endpoint-scoped fast-mode fallback is
454
+ * currently active. Reading the map directly is intentional: inspection must
455
+ * not materialize a state entry for a model that has never streamed.
456
+ */
457
+ export function isAnthropicFastModeFallbackDisabled(
458
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
459
+ model: Model<Api>,
460
+ ): boolean {
461
+ if (!providerSessionState || model.provider !== "anthropic" || model.api !== "anthropic-messages") return false;
462
+ const baseUrl = resolveAnthropicBaseUrl(model as Model<"anthropic-messages">) ?? "https://api.anthropic.com";
463
+ const key = anthropicProviderSessionStateKey(baseUrl, model.id);
464
+ return (providerSessionState.get(key) as AnthropicProviderSessionState | undefined)?.fastModeDisabled ?? false;
465
+ }
453
466
 
454
467
  function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
455
468
  return params.tools?.some(tool => tool.strict === true) ?? false;
@@ -1155,6 +1168,7 @@ export type AnthropicClientOptionsArgs = {
1155
1168
  thinkingDisplay?: AnthropicThinkingDisplay;
1156
1169
  disableStrictTools?: boolean;
1157
1170
  fetch?: FetchImpl;
1171
+ maxRetryDelayMs?: number;
1158
1172
  claudeCodeSessionId?: string;
1159
1173
  };
1160
1174
 
@@ -1164,6 +1178,7 @@ export type AnthropicClientOptionsResult = {
1164
1178
  authToken?: string | null;
1165
1179
  baseURL?: string;
1166
1180
  maxRetries: number;
1181
+ maxRetryDelayMs?: number;
1167
1182
  defaultHeaders: Record<string, string>;
1168
1183
  fetch?: FetchImpl;
1169
1184
  fetchOptions?: AnthropicFetchOptions;
@@ -1952,6 +1967,7 @@ const streamAnthropicOnce = (
1952
1967
  thinkingEnabled: options?.thinkingEnabled,
1953
1968
  thinkingDisplay: options?.thinkingDisplay,
1954
1969
  fetch: options?.fetch,
1970
+ maxRetryDelayMs: options?.maxRetryDelayMs,
1955
1971
  claudeCodeSessionId: options?.sessionId ?? extractClaudeMetadataSessionId(options?.metadata?.user_id),
1956
1972
  disableStrictTools,
1957
1973
  });
@@ -2699,10 +2715,14 @@ const streamAnthropicOnce = (
2699
2715
  // Honor the server's retry hint (`retry-after-ms`/`retry-after`) on
2700
2716
  // 429/529-style failures: retrying sooner than the server asked is a
2701
2717
  // guaranteed failure that just burns the retry budget.
2702
- const headerDelayMs =
2703
- streamFailure instanceof Error && streamFailure instanceof AnthropicApiError
2704
- ? retryDelayFromHeaders(streamFailure.headers)
2705
- : undefined;
2718
+ const headerDelayMs = getRetryAfterMsFromHeaders(getHeadersFromError(streamFailure));
2719
+ // Bound the server-directed wait so a multi-hour `retry-after` cannot
2720
+ // park the provider stream before higher-level recovery runs. A non-positive cap
2721
+ // disables the bound; an over-cap hint surfaces the original error immediately.
2722
+ const maxRetryDelayMs = options?.maxRetryDelayMs ?? 60_000;
2723
+ if (headerDelayMs !== undefined && maxRetryDelayMs > 0 && headerDelayMs > maxRetryDelayMs) {
2724
+ throw streamFailure;
2725
+ }
2706
2726
  const delayMs = headerDelayMs !== undefined ? Math.max(headerDelayMs, backoffDelayMs) : backoffDelayMs;
2707
2727
  if (options?.providerRetryWait) {
2708
2728
  await options.providerRetryWait(delayMs, options.signal);
@@ -2847,6 +2867,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
2847
2867
  thinkingEnabled = false,
2848
2868
  thinkingDisplay,
2849
2869
  isOAuth,
2870
+ maxRetryDelayMs,
2850
2871
  claudeCodeSessionId,
2851
2872
  disableStrictTools: disableStrictToolsOverride,
2852
2873
  } = args;
@@ -2917,6 +2938,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
2917
2938
  authToken: copilotApiKey,
2918
2939
  baseURL: baseUrl,
2919
2940
  maxRetries: 5,
2941
+ maxRetryDelayMs,
2920
2942
  defaultHeaders,
2921
2943
  fetch: cchFetch,
2922
2944
  fetchOptions,
@@ -2964,6 +2986,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
2964
2986
  authToken: null,
2965
2987
  baseURL: baseUrl,
2966
2988
  maxRetries: 5,
2989
+ maxRetryDelayMs,
2967
2990
  defaultHeaders,
2968
2991
  fetch: cchFetch,
2969
2992
  fetchOptions,
@@ -2982,6 +3005,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
2982
3005
  authToken: null,
2983
3006
  baseURL: baseUrl,
2984
3007
  maxRetries: 5,
3008
+ maxRetryDelayMs,
2985
3009
  defaultHeaders,
2986
3010
  fetch: cchFetch,
2987
3011
  fetchOptions,
@@ -3004,6 +3028,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
3004
3028
  authToken: oauthToken ? apiKey : undefined,
3005
3029
  baseURL: baseUrl,
3006
3030
  maxRetries: 5,
3031
+ maxRetryDelayMs,
3007
3032
  defaultHeaders,
3008
3033
  fetch: cchFetch,
3009
3034
  fetchOptions,