@gajae-code/ai 0.7.9 → 0.7.10
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.
|
@@ -134,6 +134,7 @@ export type AnthropicClientOptionsArgs = {
|
|
|
134
134
|
onSseEvent?: AnthropicOptions["onSseEvent"];
|
|
135
135
|
fetch?: FetchImpl;
|
|
136
136
|
requestMaxRetries?: number;
|
|
137
|
+
maxRetryDelayMs?: number;
|
|
137
138
|
};
|
|
138
139
|
export type AnthropicClientOptionsResult = {
|
|
139
140
|
isOAuthToken: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.10",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@gajae-code/utils": "0.7.
|
|
46
|
+
"@gajae-code/utils": "0.7.10",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "@gajae-code/utils";
|
|
22
22
|
import { hasOpus47ApiRestrictions, mapEffortToAnthropicAdaptiveEffort } from "../model-thinking";
|
|
23
23
|
import { calculateCost } from "../models";
|
|
24
|
+
import { isUsageLimitError } from "../rate-limit-utils";
|
|
24
25
|
import { getEnvApiKey, OUTPUT_FALLBACK_BUFFER } from "../stream";
|
|
25
26
|
import type {
|
|
26
27
|
Api,
|
|
@@ -62,6 +63,7 @@ import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
|
|
|
62
63
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
63
64
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
64
65
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
66
|
+
import { getRetryAfterMsFromHeaders } from "../utils/retry-after";
|
|
65
67
|
import { resolveRetryBudget } from "../utils/retry-budget";
|
|
66
68
|
import {
|
|
67
69
|
COMBINATOR_KEYS,
|
|
@@ -724,6 +726,7 @@ export type AnthropicClientOptionsArgs = {
|
|
|
724
726
|
onSseEvent?: AnthropicOptions["onSseEvent"];
|
|
725
727
|
fetch?: FetchImpl;
|
|
726
728
|
requestMaxRetries?: number;
|
|
729
|
+
maxRetryDelayMs?: number;
|
|
727
730
|
};
|
|
728
731
|
|
|
729
732
|
export type AnthropicClientOptionsResult = {
|
|
@@ -877,6 +880,101 @@ function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]):
|
|
|
877
880
|
return merged;
|
|
878
881
|
}
|
|
879
882
|
|
|
883
|
+
const ANTHROPIC_RETRY_DELAY_CAP_MS = 60_000;
|
|
884
|
+
const ANTHROPIC_RATE_LIMIT_HEADER_PREFIX = "anthropic-ratelimit-";
|
|
885
|
+
|
|
886
|
+
function getSafeAnthropicHeaderEvidence(headers: Headers): string[] {
|
|
887
|
+
const evidence: string[] = [];
|
|
888
|
+
for (const [name, value] of headers) {
|
|
889
|
+
const lowerName = name.toLowerCase();
|
|
890
|
+
if (
|
|
891
|
+
lowerName === "retry-after" ||
|
|
892
|
+
lowerName === "retry-after-ms" ||
|
|
893
|
+
lowerName.startsWith(ANTHROPIC_RATE_LIMIT_HEADER_PREFIX)
|
|
894
|
+
) {
|
|
895
|
+
evidence.push(`${lowerName}=${value}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return evidence.sort();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function getStringProperty(source: unknown, key: string): string | undefined {
|
|
902
|
+
if (!isRecord(source)) return undefined;
|
|
903
|
+
const value = source[key];
|
|
904
|
+
return typeof value === "string" ? value : undefined;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function appendAnthropicRateLimitEvidence(bodyText: string, headers: Headers): string {
|
|
908
|
+
const evidence = getSafeAnthropicHeaderEvidence(headers);
|
|
909
|
+
if (evidence.length === 0) return bodyText;
|
|
910
|
+
|
|
911
|
+
const suffix = ` Anthropic rate-limit evidence: ${evidence.join(", ")}`;
|
|
912
|
+
try {
|
|
913
|
+
const parsed = JSON.parse(bodyText) as unknown;
|
|
914
|
+
if (isRecord(parsed)) {
|
|
915
|
+
const error = parsed.error;
|
|
916
|
+
if (isRecord(error) && typeof error.message === "string" && !error.message.includes(suffix)) {
|
|
917
|
+
return JSON.stringify({ ...parsed, error: { ...error, message: `${error.message}${suffix}` } });
|
|
918
|
+
}
|
|
919
|
+
if (typeof parsed.message === "string" && !parsed.message.includes(suffix)) {
|
|
920
|
+
return JSON.stringify({ ...parsed, message: `${parsed.message}${suffix}` });
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
} catch {}
|
|
924
|
+
|
|
925
|
+
return bodyText.includes(suffix) ? bodyText : `${bodyText}${suffix}`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function isAnthropicUsageExhaustionResponse(
|
|
929
|
+
bodyText: string,
|
|
930
|
+
headers: Headers,
|
|
931
|
+
retryAfterMs: number | undefined,
|
|
932
|
+
retryDelayCapMs: number,
|
|
933
|
+
): boolean {
|
|
934
|
+
const overageReason = headers.get("anthropic-ratelimit-unified-overage-disabled-reason")?.toLowerCase();
|
|
935
|
+
if (overageReason === "out_of_credits") return true;
|
|
936
|
+
if (retryAfterMs !== undefined && retryAfterMs > retryDelayCapMs) return true;
|
|
937
|
+
|
|
938
|
+
try {
|
|
939
|
+
const parsed = JSON.parse(bodyText) as unknown;
|
|
940
|
+
const error = isRecord(parsed) ? parsed.error : undefined;
|
|
941
|
+
const type = getStringProperty(error, "type") ?? getStringProperty(parsed, "type");
|
|
942
|
+
const message = getStringProperty(error, "message") ?? getStringProperty(parsed, "message") ?? bodyText;
|
|
943
|
+
return (
|
|
944
|
+
/rate_limit_error/i.test(type ?? "") &&
|
|
945
|
+
(/request would exceed your account.?s rate limit/i.test(message) || /out_of_credits/i.test(message))
|
|
946
|
+
);
|
|
947
|
+
} catch {
|
|
948
|
+
return /request would exceed your account.?s rate limit|out_of_credits/i.test(bodyText);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function wrapAnthropicFetchForBoundedRateLimits(baseFetch: FetchImpl, maxRetryDelayMs: number | undefined): FetchImpl {
|
|
953
|
+
const retryDelayCapMs = maxRetryDelayMs ?? ANTHROPIC_RETRY_DELAY_CAP_MS;
|
|
954
|
+
return Object.assign(
|
|
955
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
956
|
+
const response = await baseFetch(input, init);
|
|
957
|
+
if (response.status !== 429 || retryDelayCapMs === 0) return response;
|
|
958
|
+
|
|
959
|
+
const headers = new Headers(response.headers);
|
|
960
|
+
const retryAfterMs = getRetryAfterMsFromHeaders(headers);
|
|
961
|
+
const bodyText = await response
|
|
962
|
+
.clone()
|
|
963
|
+
.text()
|
|
964
|
+
.catch(() => "");
|
|
965
|
+
if (!isAnthropicUsageExhaustionResponse(bodyText, headers, retryAfterMs, retryDelayCapMs)) return response;
|
|
966
|
+
|
|
967
|
+
headers.set("x-should-retry", "false");
|
|
968
|
+
return new Response(appendAnthropicRateLimitEvidence(bodyText, headers), {
|
|
969
|
+
status: response.status,
|
|
970
|
+
statusText: response.statusText,
|
|
971
|
+
headers,
|
|
972
|
+
});
|
|
973
|
+
},
|
|
974
|
+
baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
880
978
|
// The Anthropic SDK logs malformed SSE frames directly before rethrowing them.
|
|
881
979
|
// We surface the resulting provider error ourselves, so keep the SDK quiet.
|
|
882
980
|
const ANTHROPIC_SDK_LOG_LEVEL = "off" as const;
|
|
@@ -1042,6 +1140,7 @@ export function isProviderRetryableError(error: unknown, provider?: string): boo
|
|
|
1042
1140
|
if (!(error instanceof Error)) return false;
|
|
1043
1141
|
if (provider === "github-copilot" && isCopilotTransientModelError(error)) return true;
|
|
1044
1142
|
const msg = error.message.toLowerCase();
|
|
1143
|
+
if (isUsageLimitError(error.message)) return false;
|
|
1045
1144
|
if (
|
|
1046
1145
|
isUnexpectedSocketCloseMessage(msg) ||
|
|
1047
1146
|
/rate.?limit|too many requests|overloaded|service.?unavailable|internal_error|stream error.*received from peer|1302|timed?\s*out while waiting for the first event|timeout waiting for first/i.test(
|
|
@@ -1166,6 +1265,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1166
1265
|
onSseEvent: options?.onSseEvent,
|
|
1167
1266
|
fetch: options?.fetch,
|
|
1168
1267
|
requestMaxRetries: options?.requestMaxRetries,
|
|
1268
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
1169
1269
|
});
|
|
1170
1270
|
client = created.client;
|
|
1171
1271
|
isOAuthToken = created.isOAuthToken;
|
|
@@ -1723,11 +1823,8 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1723
1823
|
const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
|
|
1724
1824
|
const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
|
|
1725
1825
|
const baseFetch = args.fetch ?? fetch;
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
: args.fetch
|
|
1729
|
-
? baseFetch
|
|
1730
|
-
: undefined;
|
|
1826
|
+
const boundedFetch = wrapAnthropicFetchForBoundedRateLimits(baseFetch, args.maxRetryDelayMs);
|
|
1827
|
+
const debugFetch = onSseEvent ? wrapFetchForSseDebug(boundedFetch, event => onSseEvent(event, model)) : boundedFetch;
|
|
1731
1828
|
if (model.provider === "github-copilot") {
|
|
1732
1829
|
const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
|
|
1733
1830
|
const betaFeatures = [...extraBetas];
|
|
@@ -1755,7 +1852,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1755
1852
|
dangerouslyAllowBrowser: true,
|
|
1756
1853
|
defaultHeaders,
|
|
1757
1854
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
1758
|
-
|
|
1855
|
+
fetch: debugFetch,
|
|
1759
1856
|
...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
|
|
1760
1857
|
};
|
|
1761
1858
|
}
|
|
@@ -1789,7 +1886,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1789
1886
|
dangerouslyAllowBrowser: true,
|
|
1790
1887
|
defaultHeaders,
|
|
1791
1888
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
1792
|
-
|
|
1889
|
+
fetch: debugFetch,
|
|
1793
1890
|
};
|
|
1794
1891
|
}
|
|
1795
1892
|
|
|
@@ -1802,7 +1899,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
1802
1899
|
dangerouslyAllowBrowser: true,
|
|
1803
1900
|
defaultHeaders,
|
|
1804
1901
|
logLevel: ANTHROPIC_SDK_LOG_LEVEL,
|
|
1805
|
-
|
|
1902
|
+
fetch: debugFetch,
|
|
1806
1903
|
...(tlsFetchOptions ? { fetchOptions: tlsFetchOptions } : {}),
|
|
1807
1904
|
};
|
|
1808
1905
|
}
|
package/src/rate-limit-utils.ts
CHANGED
|
@@ -36,6 +36,14 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
|
36
36
|
return "MODEL_CAPACITY_EXHAUSTED";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (
|
|
40
|
+
lower.includes("out_of_credits") ||
|
|
41
|
+
lower.includes("request would exceed your account's rate limit") ||
|
|
42
|
+
lower.includes("request would exceed your accounts rate limit")
|
|
43
|
+
) {
|
|
44
|
+
return "QUOTA_EXHAUSTED";
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
if (
|
|
40
48
|
lower.includes("per minute") ||
|
|
41
49
|
lower.includes("rate limit") ||
|
|
@@ -86,8 +94,7 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
|
86
94
|
|
|
87
95
|
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
88
96
|
const USAGE_LIMIT_PATTERN =
|
|
89
|
-
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|model.?limit|model_limit_reached|message.?limit|message_limit_reached|limit for this model|quota.?exceeded|resource has been exhausted[^\n]*(?:quota|limit)/i;
|
|
90
|
-
|
|
97
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|model.?limit|model_limit_reached|message.?limit|message_limit_reached|limit for this model|quota.?exceeded|out_of_credits|request would exceed your account.?s rate limit|resource has been exhausted[^\n]*(?:quota|limit)/i;
|
|
91
98
|
export function isUsageLimitError(errorMessage: string): boolean {
|
|
92
99
|
return USAGE_LIMIT_PATTERN.test(errorMessage);
|
|
93
100
|
}
|