@oh-my-pi/pi-ai 17.1.8 → 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.
- package/CHANGELOG.md +56 -0
- package/dist/types/auth-broker/server.d.ts +5 -0
- package/dist/types/auth-broker/types.d.ts +4 -0
- package/dist/types/auth-storage.d.ts +26 -1
- package/dist/types/error/classes.d.ts +5 -1
- package/dist/types/error/rate-limit.d.ts +5 -4
- package/dist/types/providers/anthropic-client.d.ts +13 -1
- package/dist/types/providers/anthropic.d.ts +9 -1
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor-pi-args.d.ts +99 -0
- package/dist/types/providers/cursor.d.ts +53 -3
- package/dist/types/providers/error-message.d.ts +2 -4
- package/dist/types/providers/openai-codex-responses.d.ts +10 -0
- package/dist/types/providers/openai-responses-wire.d.ts +8 -0
- package/dist/types/providers/openai-shared.d.ts +13 -1
- package/dist/types/registry/exa.d.ts +8 -0
- package/dist/types/registry/registry.d.ts +7 -1
- package/dist/types/registry/xai.d.ts +4 -1
- package/dist/types/stream.d.ts +2 -0
- package/dist/types/types.d.ts +101 -1
- package/dist/types/usage/umans.d.ts +2 -0
- package/dist/types/utils/block-symbols.d.ts +11 -0
- package/dist/types/utils/harmony-leak.d.ts +15 -7
- package/dist/types/utils/schema/normalize.d.ts +1 -0
- package/package.json +4 -4
- package/src/auth-broker/client.ts +5 -1
- package/src/auth-broker/server.ts +103 -9
- package/src/auth-broker/snapshot-cache.ts +12 -3
- package/src/auth-broker/types.ts +6 -0
- package/src/auth-storage.ts +439 -28
- package/src/error/classes.ts +98 -3
- package/src/error/flags.ts +6 -1
- package/src/error/rate-limit.ts +18 -12
- package/src/providers/amazon-bedrock.ts +3 -3
- package/src/providers/anthropic-client.ts +24 -2
- package/src/providers/anthropic.ts +31 -6
- package/src/providers/cursor/exec-modern.ts +495 -0
- package/src/providers/cursor/proto/agent.proto +1007 -0
- package/src/providers/cursor-pi-args.ts +135 -0
- package/src/providers/cursor.ts +1138 -66
- package/src/providers/devin.ts +2 -1
- package/src/providers/error-message.ts +3 -1
- package/src/providers/openai-codex-responses.ts +106 -26
- package/src/providers/openai-completions.ts +19 -4
- package/src/providers/openai-responses-wire.ts +8 -0
- package/src/providers/openai-responses.ts +12 -1
- package/src/providers/openai-shared.ts +75 -12
- package/src/registry/api-key-validation.ts +18 -34
- package/src/registry/exa.ts +19 -0
- package/src/registry/novita.ts +6 -3
- package/src/registry/registry.ts +3 -1
- package/src/registry/xai.ts +17 -1
- package/src/stream.ts +1 -2
- package/src/types.ts +115 -0
- package/src/usage/umans.ts +192 -0
- package/src/utils/block-symbols.ts +12 -0
- package/src/utils/harmony-leak.ts +32 -0
- package/src/utils/idle-iterator.ts +2 -2
- package/src/utils/schema/normalize.ts +140 -38
package/src/error/classes.ts
CHANGED
|
@@ -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
|
-
|
|
84
|
-
const
|
|
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
|
}
|
package/src/error/flags.ts
CHANGED
|
@@ -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
|
|
package/src/error/rate-limit.ts
CHANGED
|
@@ -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:
|
|
28
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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,
|