@oh-my-pi/pi-ai 16.2.1 → 16.2.2
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 +27 -0
- package/dist/types/auth-gateway/server.d.ts +0 -19
- package/dist/types/auth-retry.d.ts +2 -10
- package/dist/types/auth-storage.d.ts +1 -2
- package/dist/types/dialect/demotion.d.ts +22 -0
- package/dist/types/dialect/index.d.ts +2 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +70 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +13 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +1 -3
- package/dist/types/providers/amazon-bedrock.d.ts +0 -5
- package/dist/types/providers/anthropic-client.d.ts +2 -16
- package/dist/types/providers/anthropic.d.ts +6 -1
- package/dist/types/providers/aws-eventstream.d.ts +2 -1
- package/dist/types/providers/cursor.d.ts +8 -7
- package/dist/types/providers/google-gemini-cli.d.ts +0 -5
- package/dist/types/providers/google-shared.d.ts +0 -5
- package/dist/types/providers/ollama.d.ts +0 -5
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
- package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -2
- package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
- package/dist/types/providers/openai-responses-wire.d.ts +1 -1
- package/dist/types/providers/openai-responses.d.ts +1 -0
- package/dist/types/providers/openai-shared.d.ts +8 -6
- package/dist/types/providers/pi-native-client.d.ts +0 -9
- package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
- package/dist/types/types.d.ts +6 -0
- package/dist/types/utils/block-symbols.d.ts +20 -0
- package/dist/types/utils/openai-http.d.ts +4 -8
- package/dist/types/utils/retry.d.ts +2 -14
- package/dist/types/utils/thinking-loop.d.ts +3 -1
- package/package.json +8 -4
- package/src/api-registry.ts +3 -1
- package/src/auth-broker/discover.ts +7 -2
- package/src/auth-broker/remote-store.ts +8 -7
- package/src/auth-gateway/server.ts +27 -115
- package/src/auth-retry.ts +9 -21
- package/src/auth-storage.ts +34 -49
- package/src/dialect/demotion.ts +31 -0
- package/src/dialect/factory.ts +0 -2
- package/src/dialect/index.ts +2 -0
- package/src/dialect/owned-stream.ts +0 -1
- package/src/dialect/thinking.ts +22 -10
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +486 -0
- package/src/error/format.ts +36 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +56 -0
- package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +1 -3
- package/src/providers/amazon-bedrock.ts +61 -57
- package/src/providers/anthropic-client.ts +13 -41
- package/src/providers/anthropic-messages-server.ts +9 -2
- package/src/providers/anthropic.ts +84 -147
- package/src/providers/aws-credentials.ts +41 -11
- package/src/providers/aws-eventstream.ts +12 -21
- package/src/providers/azure-openai-responses.ts +27 -17
- package/src/providers/cursor.ts +59 -53
- package/src/providers/devin.ts +28 -20
- package/src/providers/gitlab-duo-workflow.ts +30 -10
- package/src/providers/gitlab-duo.ts +22 -6
- package/src/providers/google-auth.ts +15 -5
- package/src/providers/google-gemini-cli.ts +46 -60
- package/src/providers/google-shared.ts +40 -38
- package/src/providers/google-vertex.ts +3 -2
- package/src/providers/google.ts +5 -1
- package/src/providers/mock.ts +10 -7
- package/src/providers/ollama.ts +34 -29
- package/src/providers/openai-chat-server.ts +2 -1
- package/src/providers/openai-codex/request-transformer.ts +13 -12
- package/src/providers/openai-codex/response-handler.ts +1 -1
- package/src/providers/openai-codex-responses.ts +931 -1012
- package/src/providers/openai-completions.ts +46 -36
- package/src/providers/openai-responses-server-schema.ts +3 -0
- package/src/providers/openai-responses-server.ts +82 -30
- package/src/providers/openai-responses-wire.ts +1 -1
- package/src/providers/openai-responses.ts +46 -22
- package/src/providers/openai-shared.ts +118 -51
- package/src/providers/pi-native-client.ts +12 -18
- package/src/providers/pi-native-server.ts +9 -6
- package/src/providers/register-builtins.ts +5 -2
- package/src/providers/transform-messages.ts +31 -63
- package/src/registry/alibaba-coding-plan.ts +6 -5
- package/src/registry/api-key-login.ts +4 -3
- package/src/registry/api-key-validation.ts +4 -3
- package/src/registry/cloudflare-ai-gateway.ts +4 -3
- package/src/registry/coreweave.ts +2 -1
- package/src/registry/deepseek.ts +2 -1
- package/src/registry/google-antigravity.ts +2 -1
- package/src/registry/google-gemini-cli.ts +2 -1
- package/src/registry/kagi.ts +4 -3
- package/src/registry/kilo.ts +32 -10
- package/src/registry/litellm.ts +4 -3
- package/src/registry/llama-cpp.ts +3 -2
- package/src/registry/lm-studio.ts +3 -2
- package/src/registry/nvidia.ts +7 -7
- package/src/registry/oauth/anthropic.ts +23 -6
- package/src/registry/oauth/callback-server.ts +5 -3
- package/src/registry/oauth/cursor.ts +18 -4
- package/src/registry/oauth/devin.ts +14 -3
- package/src/registry/oauth/github-copilot.ts +21 -10
- package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
- package/src/registry/oauth/gitlab-duo.ts +30 -6
- package/src/registry/oauth/google-antigravity.ts +14 -5
- package/src/registry/oauth/google-gemini-cli.ts +24 -7
- package/src/registry/oauth/google-oauth-shared.ts +6 -3
- package/src/registry/oauth/index.ts +19 -8
- package/src/registry/oauth/kimi.ts +40 -10
- package/src/registry/oauth/openai-codex.ts +26 -11
- package/src/registry/oauth/opencode.ts +4 -3
- package/src/registry/oauth/perplexity.ts +33 -11
- package/src/registry/oauth/xai-oauth.ts +62 -18
- package/src/registry/oauth/xiaomi.ts +21 -9
- package/src/registry/ollama-cloud.ts +5 -4
- package/src/registry/ollama.ts +3 -2
- package/src/registry/parallel.ts +4 -3
- package/src/registry/qwen-portal.ts +4 -3
- package/src/registry/tavily.ts +4 -3
- package/src/registry/vercel-ai-gateway.ts +4 -3
- package/src/registry/vllm.ts +3 -2
- package/src/stream.ts +79 -24
- package/src/types.ts +6 -0
- package/src/usage/claude.ts +2 -1
- package/src/usage/github-copilot.ts +4 -3
- package/src/usage/google-antigravity.ts +2 -1
- package/src/utils/abort.ts +4 -2
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/event-stream.ts +15 -3
- package/src/utils/http-inspector.ts +4 -4
- package/src/utils/idle-iterator.ts +6 -5
- package/src/utils/openai-http.ts +11 -46
- package/src/utils/parse-bind.ts +7 -5
- package/src/utils/proxy.ts +2 -1
- package/src/utils/retry.ts +7 -23
- package/src/utils/schema/normalize.ts +3 -2
- package/src/utils/thinking-loop.ts +15 -11
- package/src/utils/validation.ts +4 -3
- package/dist/types/dialect/pi.d.ts +0 -9
- package/dist/types/errors.d.ts +0 -24
- package/dist/types/utils/overflow.d.ts +0 -55
- package/src/dialect/pi.md +0 -55
- package/src/dialect/pi.ts +0 -600
- package/src/errors.ts +0 -32
- package/src/utils/overflow.ts +0 -140
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { isUnexpectedSocketCloseMessage } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import type { Api, AssistantMessage } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
AnthropicConnectionError,
|
|
5
|
+
AnthropicConnectionTimeoutError,
|
|
6
|
+
ProviderHttpError,
|
|
7
|
+
STREAM_ENVELOPE_ERROR_PREFIX,
|
|
8
|
+
} from "./classes";
|
|
9
|
+
import { isOpaqueStatusBody, matchesUsageLimitText, parseRateLimitReason } from "./rate-limit";
|
|
10
|
+
|
|
11
|
+
export const Flag = {
|
|
12
|
+
Class: 0x1000,
|
|
13
|
+
ThinkingLoop: 0x0001_0000,
|
|
14
|
+
Transient: 0x0002_0000,
|
|
15
|
+
Timeout: 0x0004_0000,
|
|
16
|
+
UsageLimit: 0x0008_0000,
|
|
17
|
+
StaleResponsesItem: 0x0010_0000,
|
|
18
|
+
MalformedFunctionCall: 0x0020_0000,
|
|
19
|
+
ProviderFinishError: 0x0040_0000,
|
|
20
|
+
ContextOverflow: 0x0080_0000,
|
|
21
|
+
AuthFailed: 0x0100_0000,
|
|
22
|
+
SilentAbort: 0x0200_0000,
|
|
23
|
+
UserInterrupt: 0x0400_0000,
|
|
24
|
+
Abort: 0x0800_0000,
|
|
25
|
+
/** Anthropic strict-tool grammar too large / schema too complex to compile (400). */
|
|
26
|
+
Grammar: 0x1000_0000,
|
|
27
|
+
/** Anthropic model/account does not support fast mode / the `speed` parameter. */
|
|
28
|
+
FastModeUnsupported: 0x2000_0000,
|
|
29
|
+
/** OAuth refresh failed definitively — the stored grant is dead, re-login required. */
|
|
30
|
+
OAuthExpiry: 0x4000_0000,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export type Flag = (typeof Flag)[keyof typeof Flag];
|
|
34
|
+
|
|
35
|
+
const KIND_MASK =
|
|
36
|
+
Flag.ThinkingLoop |
|
|
37
|
+
Flag.Transient |
|
|
38
|
+
Flag.Timeout |
|
|
39
|
+
Flag.UsageLimit |
|
|
40
|
+
Flag.StaleResponsesItem |
|
|
41
|
+
Flag.MalformedFunctionCall |
|
|
42
|
+
Flag.ProviderFinishError |
|
|
43
|
+
Flag.ContextOverflow |
|
|
44
|
+
Flag.AuthFailed |
|
|
45
|
+
Flag.SilentAbort |
|
|
46
|
+
Flag.UserInterrupt |
|
|
47
|
+
Flag.Abort |
|
|
48
|
+
Flag.Grammar |
|
|
49
|
+
Flag.FastModeUnsupported |
|
|
50
|
+
Flag.OAuthExpiry;
|
|
51
|
+
|
|
52
|
+
const RETRIABLE_KINDS =
|
|
53
|
+
Flag.Transient | Flag.UsageLimit | Flag.ThinkingLoop | Flag.StaleResponsesItem | Flag.ProviderFinishError;
|
|
54
|
+
|
|
55
|
+
const OVERFLOW_PATTERNS = [
|
|
56
|
+
/prompt is too long/i, // Anthropic
|
|
57
|
+
/input is too long for requested model/i, // Amazon Bedrock
|
|
58
|
+
/exceeds the context window/i, // OpenAI (Completions & Responses API)
|
|
59
|
+
/input token count.*exceeds the maximum/i, // Google (Gemini)
|
|
60
|
+
/maximum prompt length is \d+/i, // xAI (Grok)
|
|
61
|
+
/reduce the length of the messages/i, // Groq
|
|
62
|
+
/maximum context length is \d+ tokens/i, // OpenRouter (all backends)
|
|
63
|
+
/exceeds the limit of \d+/i, // GitHub Copilot
|
|
64
|
+
/exceeds the available context size/i, // llama.cpp server
|
|
65
|
+
/requested tokens?.*exceed.*context (window|length|size)/i, // llama.cpp / OpenAI-compatible local servers
|
|
66
|
+
/context (window|length|size).*(exceeded|overflow|too small)/i, // Generic local server variants
|
|
67
|
+
/(prompt|input).*(too long|too large).*(context|n_ctx)/i, // llama.cpp phrasing variants
|
|
68
|
+
/requested tokens?.*(exceeds?|greater than).*(n_ctx|context)/i, // llama.cpp n_ctx variants
|
|
69
|
+
/greater than the context length/i, // LM Studio
|
|
70
|
+
/context window exceeds limit/i, // MiniMax
|
|
71
|
+
/exceeded model token limit/i, // Kimi For Coding
|
|
72
|
+
/context[_ ]length[_ ]exceeded/i, // Generic fallback
|
|
73
|
+
/too many tokens/i, // Generic fallback
|
|
74
|
+
/token limit exceeded/i, // Generic fallback
|
|
75
|
+
/request_too_large/i, // Anthropic 413 (request body too large)
|
|
76
|
+
/request exceeds the maximum size/i, // Anthropic 413 variant
|
|
77
|
+
/payload too large/i, // Generic HTTP 413 variant
|
|
78
|
+
/entity too large/i, // Generic HTTP 413 variant
|
|
79
|
+
/\b413\b.*\b(request|payload|entity)\b.*\btoo large\b/i, // "413 Request Entity Too Large" variants
|
|
80
|
+
/model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text
|
|
81
|
+
/prompt filled the context window/i, // Ollama OpenAI-compatible empty length completion
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const OVERFLOW_NO_BODY_PATTERN = /\b4(00|13)\s*(status code)?\s*\(no body\)/i;
|
|
85
|
+
const TIMEOUT_PATTERN = /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream stall\b/i;
|
|
86
|
+
const TRANSIENT_ENVELOPE_PATTERN = /anthropic stream envelope error:/i;
|
|
87
|
+
const TRANSIENT_ENVELOPE_BEFORE_START_PATTERN = /before message_start/i;
|
|
88
|
+
export const TRANSIENT_TRANSPORT_PATTERN =
|
|
89
|
+
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|malformed.?function.?call/i;
|
|
90
|
+
const AUTH_FAILURE_PATTERN =
|
|
91
|
+
/\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i;
|
|
92
|
+
const MALFORMED_FUNCTION_CALL_PATTERN = /\bmalformed.?function.?call\b/i;
|
|
93
|
+
const PROVIDER_FINISH_ERROR_PATTERN = /\bProvider (?:returned error finish_reason|finish_reason:\s*error)\b/i;
|
|
94
|
+
const STALE_RESPONSE_ITEM_PATTERNS = [/\bItem with id ['"][^'"]+['"] not found\.?/i, /previous[ _]?response/i] as const;
|
|
95
|
+
const STALE_RESPONSE_ITEM_DETAIL_PATTERN = /not[ _]?found|invalid|expired|stale|zero[ _-]?data[ _-]?retention/i;
|
|
96
|
+
|
|
97
|
+
// Copilot routing flap: HTTP 400 `model_not_supported` (structural code on the
|
|
98
|
+
// error, also surfaced in text). Treated as transient — a retry usually lands
|
|
99
|
+
// on a backend that has the model.
|
|
100
|
+
const COPILOT_MODEL_NOT_SUPPORTED_PATTERN = /model_not_supported/i;
|
|
101
|
+
// Anthropic strict-tool grammar too large / schema too complex (400 invalid_request_error).
|
|
102
|
+
const GRAMMAR_TOO_LARGE_PATTERN = /compiled grammar/i;
|
|
103
|
+
const GRAMMAR_TOO_LARGE_DETAIL_PATTERN = /too large/i;
|
|
104
|
+
const SCHEMA_TOO_COMPLEX_PATTERN = /schema/i;
|
|
105
|
+
const SCHEMA_TOO_COMPLEX_DETAIL_PATTERN = /too complex/i;
|
|
106
|
+
const SCHEMA_COMPILE_PATTERN = /compil/i;
|
|
107
|
+
const INVALID_REQUEST_PATTERN = /invalid_request_error/i;
|
|
108
|
+
// Anthropic fast-mode unsupported: 400 rejecting `speed`, or 429 rate_limit_error
|
|
109
|
+
// because the account lacks the extra-usage entitlement fast mode requires.
|
|
110
|
+
const FAST_MODE_SPEED_PARAM_PATTERN = /\bspeed\b/i;
|
|
111
|
+
const FAST_MODE_NOT_SUPPORTED_PATTERN = /not support/i;
|
|
112
|
+
const FAST_MODE_RATE_LIMIT_PATTERN = /rate_limit_error/i;
|
|
113
|
+
const FAST_MODE_ENTITLEMENT_PATTERN = /fast mode/i;
|
|
114
|
+
// Definitive OAuth refresh failure — the stored grant/client is dead.
|
|
115
|
+
const OAUTH_DEFINITIVE_FAILURE_PATTERN =
|
|
116
|
+
/invalid_grant|invalid_token|unauthorized_client|\brevoked\b|refresh[\s_]?token.*expired/i;
|
|
117
|
+
const OAUTH_TRANSIENT_FAILURE_PATTERN =
|
|
118
|
+
/timeout|network|fetch failed|ECONN(?:REFUSED|RESET)|ETIMEDOUT|EAI_AGAIN|socket hang up|\b(?:408|425|429|5\d{2})\b|rate.?limit|too many requests|temporar|unavailable|forbidden|permission_denied|cloudflare|captcha/i;
|
|
119
|
+
const OAUTH_HTTP_AUTH_PATTERN = /\b401\b/;
|
|
120
|
+
|
|
121
|
+
function matchesGrammarTooLarge(message: string, errorStatus: number | undefined): boolean {
|
|
122
|
+
if (errorStatus !== 400) return false;
|
|
123
|
+
if (!INVALID_REQUEST_PATTERN.test(message)) return false;
|
|
124
|
+
const grammarTooLarge = GRAMMAR_TOO_LARGE_PATTERN.test(message) && GRAMMAR_TOO_LARGE_DETAIL_PATTERN.test(message);
|
|
125
|
+
const schemaTooComplex =
|
|
126
|
+
SCHEMA_TOO_COMPLEX_PATTERN.test(message) &&
|
|
127
|
+
SCHEMA_TOO_COMPLEX_DETAIL_PATTERN.test(message) &&
|
|
128
|
+
SCHEMA_COMPILE_PATTERN.test(message);
|
|
129
|
+
return grammarTooLarge || schemaTooComplex;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function matchesFastModeUnsupported(message: string, errorStatus: number | undefined): boolean {
|
|
133
|
+
if (errorStatus !== 400 && errorStatus !== 429) return false;
|
|
134
|
+
if (
|
|
135
|
+
errorStatus === 400 &&
|
|
136
|
+
INVALID_REQUEST_PATTERN.test(message) &&
|
|
137
|
+
FAST_MODE_SPEED_PARAM_PATTERN.test(message) &&
|
|
138
|
+
FAST_MODE_NOT_SUPPORTED_PATTERN.test(message)
|
|
139
|
+
) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return (
|
|
143
|
+
errorStatus === 429 && FAST_MODE_RATE_LIMIT_PATTERN.test(message) && FAST_MODE_ENTITLEMENT_PATTERN.test(message)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Whether an OAuth refresh error message means the grant is definitively dead. */
|
|
148
|
+
export function isOAuthExpiry(errorMessage: string): boolean {
|
|
149
|
+
if (OAUTH_DEFINITIVE_FAILURE_PATTERN.test(errorMessage)) return true;
|
|
150
|
+
return OAUTH_HTTP_AUTH_PATTERN.test(errorMessage) && !OAUTH_TRANSIENT_FAILURE_PATTERN.test(errorMessage);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const ERROR_KIND_LABELS: readonly [Flag, string][] = [
|
|
154
|
+
[Flag.ThinkingLoop, "thinking-loop"],
|
|
155
|
+
[Flag.Transient, "transient"],
|
|
156
|
+
[Flag.Timeout, "timeout"],
|
|
157
|
+
[Flag.UsageLimit, "usage-limit"],
|
|
158
|
+
[Flag.StaleResponsesItem, "stale-responses-item"],
|
|
159
|
+
[Flag.MalformedFunctionCall, "malformed-function-call"],
|
|
160
|
+
[Flag.ProviderFinishError, "provider-finish-error"],
|
|
161
|
+
[Flag.ContextOverflow, "context-overflow"],
|
|
162
|
+
[Flag.AuthFailed, "auth-failed"],
|
|
163
|
+
[Flag.SilentAbort, "silent-abort"],
|
|
164
|
+
[Flag.UserInterrupt, "user-interrupt"],
|
|
165
|
+
[Flag.Abort, "abort"],
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const STATUS_MESSAGE_PATTERNS = [
|
|
169
|
+
/\bstatus(?:_code)?[:=]\s*(\d{3})\b/i,
|
|
170
|
+
/\bstatus\s+(\d{3})\b/i,
|
|
171
|
+
/\bHTTP\s+(\d{3})\b/i,
|
|
172
|
+
/\b(?:error|failed)\s*[:=]?\s*(\d{3})\b/i,
|
|
173
|
+
/(?:^|\s)(\d{3})\s+(?:[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/,
|
|
174
|
+
] as const;
|
|
175
|
+
|
|
176
|
+
export function create(...flags: number[]): number {
|
|
177
|
+
let bits = 0;
|
|
178
|
+
for (const f of flags) bits |= f;
|
|
179
|
+
return bits | Flag.Class;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function is(id: number | undefined, flag: Flag): boolean {
|
|
183
|
+
return ((id ?? 0) & flag) !== 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function retriable(id: number | undefined, opts?: { replayUnsafe?: boolean }): boolean {
|
|
187
|
+
if (is(id, Flag.MalformedFunctionCall)) return true;
|
|
188
|
+
if (opts?.replayUnsafe) return false;
|
|
189
|
+
return ((id ?? 0) & RETRIABLE_KINDS) !== 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isClassified(id: number | undefined): boolean {
|
|
193
|
+
return ((id ?? 0) & Flag.Class) !== 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function statusFromId(id: number | undefined): number | undefined {
|
|
197
|
+
return id && !isClassified(id) ? id : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function status(error: unknown): number | undefined {
|
|
201
|
+
return statusInternal(error, 0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function statusInternal(error: unknown, depth: number): number | undefined {
|
|
205
|
+
if (depth > 2 || error === undefined || error === null) return undefined;
|
|
206
|
+
if (typeof error === "object") {
|
|
207
|
+
const errObj = error as Record<string, unknown>;
|
|
208
|
+
|
|
209
|
+
if (typeof errObj.status === "number" && errObj.status >= 100 && errObj.status <= 599) {
|
|
210
|
+
return errObj.status;
|
|
211
|
+
}
|
|
212
|
+
if (typeof errObj.statusCode === "number" && errObj.statusCode >= 100 && errObj.statusCode <= 599) {
|
|
213
|
+
return errObj.statusCode;
|
|
214
|
+
}
|
|
215
|
+
if (typeof errObj.response === "object" && errObj.response !== null) {
|
|
216
|
+
const resp = errObj.response as Record<string, unknown>;
|
|
217
|
+
if (typeof resp.status === "number" && resp.status >= 100 && resp.status <= 599) {
|
|
218
|
+
return resp.status;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if ("cause" in errObj) {
|
|
223
|
+
const nested = statusInternal(errObj.cause, depth + 1);
|
|
224
|
+
if (nested !== undefined) return nested;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (error instanceof Error || (typeof error === "object" && error !== null && "message" in error)) {
|
|
229
|
+
const message = (error as { message: string }).message;
|
|
230
|
+
if (typeof message === "string") {
|
|
231
|
+
for (const pattern of STATUS_MESSAGE_PATTERNS) {
|
|
232
|
+
const match = pattern.exec(message);
|
|
233
|
+
if (match) {
|
|
234
|
+
const code = parseInt(match[1], 10);
|
|
235
|
+
if (code >= 100 && code <= 599) return code;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isTransientErrorText(text: string): boolean {
|
|
244
|
+
return (
|
|
245
|
+
isUnexpectedSocketCloseMessage(text) ||
|
|
246
|
+
(TRANSIENT_ENVELOPE_PATTERN.test(text) && TRANSIENT_ENVELOPE_BEFORE_START_PATTERN.test(text)) ||
|
|
247
|
+
TRANSIENT_TRANSPORT_PATTERN.test(text)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isTimeoutText(text: string): boolean {
|
|
252
|
+
return TIMEOUT_PATTERN.test(text);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isAuthFailureText(text: string): boolean {
|
|
256
|
+
return AUTH_FAILURE_PATTERN.test(text);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isStaleResponsesText(text: string): boolean {
|
|
260
|
+
return (
|
|
261
|
+
STALE_RESPONSE_ITEM_PATTERNS[0].test(text) ||
|
|
262
|
+
(STALE_RESPONSE_ITEM_PATTERNS[1].test(text) && STALE_RESPONSE_ITEM_DETAIL_PATTERN.test(text))
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isMalformedFunctionCallText(text: string): boolean {
|
|
267
|
+
return MALFORMED_FUNCTION_CALL_PATTERN.test(text);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function isProviderFinishErrorText(text: string): boolean {
|
|
271
|
+
return PROVIDER_FINISH_ERROR_PATTERN.test(text);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function matchesOverflowText(text: string): boolean {
|
|
275
|
+
return OVERFLOW_PATTERNS.some(p => p.test(text)) || OVERFLOW_NO_BODY_PATTERN.test(text);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function classifyText(errorMessage: string | undefined, errorStatus: number | undefined, api?: Api): number {
|
|
279
|
+
let kinds = 0;
|
|
280
|
+
if (errorMessage) {
|
|
281
|
+
if (matchesOverflowText(errorMessage)) kinds |= Flag.ContextOverflow;
|
|
282
|
+
if (isMalformedFunctionCallText(errorMessage)) kinds |= Flag.MalformedFunctionCall;
|
|
283
|
+
if (isProviderFinishErrorText(errorMessage)) kinds |= Flag.ProviderFinishError;
|
|
284
|
+
if (isAuthFailureText(errorMessage)) kinds |= Flag.AuthFailed;
|
|
285
|
+
|
|
286
|
+
const statusClean = errorStatus ? errorStatus : (status({ message: errorMessage }) ?? undefined);
|
|
287
|
+
const cleanMessage = errorMessage;
|
|
288
|
+
const isOpaque = isOpaqueStatusBody(cleanMessage);
|
|
289
|
+
|
|
290
|
+
const isLimitStatus = statusClean === 429;
|
|
291
|
+
if (
|
|
292
|
+
matchesUsageLimitText(cleanMessage) ||
|
|
293
|
+
(isLimitStatus && (isOpaque || parseRateLimitReason(cleanMessage) === "QUOTA_EXHAUSTED"))
|
|
294
|
+
) {
|
|
295
|
+
kinds |= Flag.UsageLimit;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (isTimeoutText(errorMessage)) kinds |= Flag.Transient | Flag.Timeout;
|
|
299
|
+
else if (isTransientErrorText(errorMessage)) kinds |= Flag.Transient;
|
|
300
|
+
if ((api === "openai-responses" || api === "openai-codex-responses") && isStaleResponsesText(errorMessage)) {
|
|
301
|
+
kinds |= Flag.StaleResponsesItem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Copilot per-client routing flap is transient.
|
|
305
|
+
if (statusClean === 400 && COPILOT_MODEL_NOT_SUPPORTED_PATTERN.test(cleanMessage)) kinds |= Flag.Transient;
|
|
306
|
+
if (matchesGrammarTooLarge(cleanMessage, statusClean)) kinds |= Flag.Grammar;
|
|
307
|
+
if (matchesFastModeUnsupported(cleanMessage, statusClean)) kinds |= Flag.FastModeUnsupported;
|
|
308
|
+
}
|
|
309
|
+
if (kinds !== 0) return create(kinds);
|
|
310
|
+
const fallbackStatus = errorStatus ?? (errorMessage ? status({ message: errorMessage }) : undefined);
|
|
311
|
+
if (fallbackStatus === 401 || fallbackStatus === 403) return create(Flag.AuthFailed);
|
|
312
|
+
return fallbackStatus ?? 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function classify(error: unknown, api?: Api): number {
|
|
316
|
+
let kinds = 0;
|
|
317
|
+
const seen = new Set<object>();
|
|
318
|
+
let link: unknown = error;
|
|
319
|
+
while (link !== undefined && link !== null) {
|
|
320
|
+
if (typeof link === "object") {
|
|
321
|
+
if (seen.has(link)) break;
|
|
322
|
+
seen.add(link);
|
|
323
|
+
|
|
324
|
+
if ("errorId" in link && typeof (link as { errorId: unknown }).errorId === "number") {
|
|
325
|
+
kinds |= (link as { errorId: number }).errorId & KIND_MASK;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (link instanceof AnthropicConnectionTimeoutError) {
|
|
330
|
+
kinds |= Flag.Timeout | Flag.Transient;
|
|
331
|
+
} else if (link instanceof AnthropicConnectionError) {
|
|
332
|
+
kinds |= Flag.Transient;
|
|
333
|
+
} else if (
|
|
334
|
+
typeof link === "object" &&
|
|
335
|
+
"name" in link &&
|
|
336
|
+
(link as { name: string }).name === "CodexWebSocketTransportError"
|
|
337
|
+
) {
|
|
338
|
+
kinds |= Flag.Transient;
|
|
339
|
+
} else if (
|
|
340
|
+
link instanceof Error &&
|
|
341
|
+
link.name === "CodexProviderStreamError" &&
|
|
342
|
+
"retryable" in link &&
|
|
343
|
+
(link as { retryable: unknown }).retryable === true
|
|
344
|
+
) {
|
|
345
|
+
kinds |= Flag.Transient;
|
|
346
|
+
} else if (link instanceof ProviderHttpError) {
|
|
347
|
+
let linkKinds = 0;
|
|
348
|
+
const { status: codeStatus, code } = link;
|
|
349
|
+
if (code === "usage_limit_reached" || code === "insufficient_quota") {
|
|
350
|
+
linkKinds |= Flag.UsageLimit;
|
|
351
|
+
}
|
|
352
|
+
if (code === "overloaded_error" || code === "rate_limit_error") {
|
|
353
|
+
linkKinds |= Flag.Transient;
|
|
354
|
+
}
|
|
355
|
+
if (codeStatus === 401 || codeStatus === 403) {
|
|
356
|
+
linkKinds |= Flag.AuthFailed;
|
|
357
|
+
} else if (codeStatus === 429) {
|
|
358
|
+
if ((linkKinds & Flag.UsageLimit) === 0) {
|
|
359
|
+
linkKinds |= Flag.Transient;
|
|
360
|
+
}
|
|
361
|
+
} else if (codeStatus >= 500) {
|
|
362
|
+
linkKinds |= Flag.Transient;
|
|
363
|
+
}
|
|
364
|
+
kinds |= linkKinds;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let linkMessage: string | undefined;
|
|
368
|
+
if (link instanceof Error) {
|
|
369
|
+
linkMessage = link.message;
|
|
370
|
+
} else if (typeof link === "string") {
|
|
371
|
+
linkMessage = link;
|
|
372
|
+
} else if (
|
|
373
|
+
typeof link === "object" &&
|
|
374
|
+
"message" in link &&
|
|
375
|
+
typeof (link as { message: unknown }).message === "string"
|
|
376
|
+
) {
|
|
377
|
+
linkMessage = (link as { message: string }).message;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const textId = classifyText(linkMessage, status(link), api);
|
|
381
|
+
kinds |= textId & KIND_MASK;
|
|
382
|
+
|
|
383
|
+
link = typeof link === "object" && "cause" in link ? (link as { cause: unknown }).cause : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return kinds !== 0 ? create(kinds) : (status(error) ?? 0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Whether an error (or message string) classifies as an account usage/quota
|
|
391
|
+
* limit — the persistent, credential-rotation-worthy kind. This is the public
|
|
392
|
+
* accessor for {@link Flag.UsageLimit}; prefer it over re-running message
|
|
393
|
+
* regexes at call sites.
|
|
394
|
+
*/
|
|
395
|
+
export function isUsageLimit(error: unknown, api?: Api): boolean {
|
|
396
|
+
return is(classify(error, api), Flag.UsageLimit);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Anthropic strict-tool grammar too large / schema too complex to compile.
|
|
401
|
+
* Accessor for {@link Flag.Grammar}.
|
|
402
|
+
*/
|
|
403
|
+
export function isGrammarError(error: unknown): boolean {
|
|
404
|
+
return is(classify(error), Flag.Grammar);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Anthropic model/account does not support fast mode / the `speed` parameter.
|
|
409
|
+
* Accessor for {@link Flag.FastModeUnsupported}.
|
|
410
|
+
*/
|
|
411
|
+
export function isFastModeUnsupported(error: unknown): boolean {
|
|
412
|
+
return is(classify(error), Flag.FastModeUnsupported);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* GitHub Copilot 400 `model_not_supported` routing flap — transient. Reads the
|
|
417
|
+
* structural `code` (and falls back to {@link Flag.Transient} text classification).
|
|
418
|
+
*/
|
|
419
|
+
export function isCopilotTransientModelError(error: unknown): boolean {
|
|
420
|
+
if (status(error) === 400 && error && typeof error === "object") {
|
|
421
|
+
const info = error as { code?: unknown; error?: { code?: unknown } | null };
|
|
422
|
+
const code = typeof info.code === "string" ? info.code : info.error?.code;
|
|
423
|
+
if (code === "model_not_supported") return true;
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function classifyMessage(message: {
|
|
429
|
+
api?: Api;
|
|
430
|
+
errorId?: number;
|
|
431
|
+
errorMessage?: string;
|
|
432
|
+
errorStatus?: number;
|
|
433
|
+
}): number {
|
|
434
|
+
const existingId = message.errorId;
|
|
435
|
+
const currentStatus = message.errorStatus ?? statusFromId(existingId);
|
|
436
|
+
const textId = classifyText(message.errorMessage, currentStatus, message.api);
|
|
437
|
+
|
|
438
|
+
const kinds = ((existingId ?? 0) | textId) & KIND_MASK;
|
|
439
|
+
const id = kinds !== 0 ? create(kinds) : (statusFromId(textId) ?? statusFromId(existingId) ?? currentStatus ?? 0);
|
|
440
|
+
|
|
441
|
+
message.errorId = id;
|
|
442
|
+
return id;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function attach<E extends object>(error: E, id: number): E {
|
|
446
|
+
Object.defineProperty(error, "errorId", { value: id, enumerable: false, configurable: true });
|
|
447
|
+
return error;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean {
|
|
451
|
+
if (is(message.errorId, Flag.ContextOverflow)) return true;
|
|
452
|
+
if (contextWindow) {
|
|
453
|
+
const inputTokens = message.usage.input + message.usage.cacheRead + message.usage.cacheWrite;
|
|
454
|
+
if (inputTokens > contextWindow) return true;
|
|
455
|
+
}
|
|
456
|
+
return message.stopReason === "error" && !!message.errorMessage && matchesOverflowText(message.errorMessage);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function stringify(id: number | undefined): string {
|
|
460
|
+
if (!id) return "none";
|
|
461
|
+
if (!isClassified(id)) return `status:${id}`;
|
|
462
|
+
const labels = ERROR_KIND_LABELS.filter(([kind]) => is(id, kind)).map(([, label]) => label);
|
|
463
|
+
return labels.length > 0 ? labels.join("|") : `classified:0x${id.toString(16)}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const STREAM_PARSE_TRUNCATION_PATTERN =
|
|
467
|
+
/unterminated string|unexpected end of json input|unexpected end of data|unexpected eof|end of file|eof while parsing|truncated/i;
|
|
468
|
+
const STREAM_EVENT_ORDER_PATTERN = /stream event order|before message_start/i;
|
|
469
|
+
|
|
470
|
+
/** Transient stream corruption where the response was truncated mid-JSON. */
|
|
471
|
+
export function isTransientStreamParseError(error: unknown): boolean {
|
|
472
|
+
return error instanceof Error && STREAM_PARSE_TRUNCATION_PATTERN.test(error.message);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Any malformed stream-envelope error (prefix-tagged or out-of-order events). */
|
|
476
|
+
export function isStreamEnvelopeError(error: unknown): boolean {
|
|
477
|
+
return (
|
|
478
|
+
error instanceof Error &&
|
|
479
|
+
(error.message.includes(STREAM_ENVELOPE_ERROR_PREFIX) || STREAM_EVENT_ORDER_PATTERN.test(error.message))
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Stream-envelope errors safe to retry against the provider (event ordering only). */
|
|
484
|
+
export function isRetryableStreamEnvelopeError(error: unknown): boolean {
|
|
485
|
+
return error instanceof Error && STREAM_EVENT_ORDER_PATTERN.test(error.message);
|
|
486
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CapturedHttpErrorResponse,
|
|
3
|
+
finalizeErrorMessage,
|
|
4
|
+
type RawHttpRequestDump,
|
|
5
|
+
rewriteCopilotError,
|
|
6
|
+
} from "../utils/http-inspector";
|
|
7
|
+
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
8
|
+
|
|
9
|
+
/** Inputs that steer {@link formatMessage}'s formatter selection. */
|
|
10
|
+
export interface FormatMessageOptions {
|
|
11
|
+
/** When present, the raw request is dumped into the message for 400-class failures. */
|
|
12
|
+
rawRequestDump?: RawHttpRequestDump;
|
|
13
|
+
/** Captured non-2xx response body, appended to the message when available. */
|
|
14
|
+
capturedErrorResponse?: CapturedHttpErrorResponse;
|
|
15
|
+
/** Provider id; `"github-copilot"` triggers the copilot message rewrite. */
|
|
16
|
+
provider?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format a provider error into a user-facing message, unifying the three
|
|
21
|
+
* formatters: lightweight retry-after extraction, the raw-dump finalizer, and
|
|
22
|
+
* the copilot rewrite.
|
|
23
|
+
*
|
|
24
|
+
* Selection is driven by inputs, not a mode flag: a `rawRequestDump` routes
|
|
25
|
+
* through {@link finalizeErrorMessage} (retry-after + raw dump + captured body),
|
|
26
|
+
* otherwise the lightweight {@link formatErrorMessageWithRetryAfter} is used.
|
|
27
|
+
*/
|
|
28
|
+
export async function formatMessage(error: unknown, opts: FormatMessageOptions = {}): Promise<string> {
|
|
29
|
+
let message = opts.rawRequestDump
|
|
30
|
+
? await finalizeErrorMessage(error, opts.rawRequestDump, opts.capturedErrorResponse)
|
|
31
|
+
: formatErrorMessageWithRetryAfter(error);
|
|
32
|
+
if (opts.provider === "github-copilot") {
|
|
33
|
+
message = rewriteCopilotError(message, error, opts.provider);
|
|
34
|
+
}
|
|
35
|
+
return message;
|
|
36
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isUsageLimit } from "./flags";
|
|
2
|
+
|
|
3
|
+
/** A gateway-facing classification of an arbitrary upstream/internal error. */
|
|
4
|
+
export interface GatewayErrorClassification {
|
|
5
|
+
status: number;
|
|
6
|
+
type: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Classify an upstream / gateway-internal error into a status code and a
|
|
12
|
+
* format-neutral type. The order is intentional:
|
|
13
|
+
*
|
|
14
|
+
* 1. Honour an explicit numeric `status` property on the thrown error.
|
|
15
|
+
* 2. Parse a status code embedded in the message string. Provider errors
|
|
16
|
+
* virtually always carry one (`Google API error (400): …`, `HTTP 429`,
|
|
17
|
+
* `status=503`) and the embedded value is authoritative.
|
|
18
|
+
* 3. Fall through to **word-boundaried** substring heuristics. The old
|
|
19
|
+
* `lower.includes("rate")` test famously matched `GenerateContentRequest`,
|
|
20
|
+
* surfacing every Google 400 as a 429 `rate_limit_error`. The patterns here
|
|
21
|
+
* all require boundaries so they don't collide with provider field names.
|
|
22
|
+
*/
|
|
23
|
+
export function classifyGatewayError(err: unknown): GatewayErrorClassification {
|
|
24
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25
|
+
|
|
26
|
+
// 1. Custom pi-ai errors may attach a numeric `status` property.
|
|
27
|
+
const statusProp =
|
|
28
|
+
typeof err === "object" && err !== null && typeof (err as { status?: unknown }).status === "number"
|
|
29
|
+
? (err as { status: number }).status | 0
|
|
30
|
+
: undefined;
|
|
31
|
+
if (statusProp !== undefined) return bucketStatus(statusProp, message);
|
|
32
|
+
|
|
33
|
+
if (err instanceof Error && err.name === "AbortError") return { status: 499, type: "request_aborted", message };
|
|
34
|
+
|
|
35
|
+
// 2. Status code embedded in the message. Requires a contextual keyword
|
|
36
|
+
// (`HTTP`, `API error`, `status`, …) or a leading `(NNN)` token so we
|
|
37
|
+
// don't trip on incidental three-digit numbers ("took 200ms").
|
|
38
|
+
const embedded = extractEmbeddedStatus(message);
|
|
39
|
+
if (embedded !== undefined) return bucketStatus(embedded, message);
|
|
40
|
+
|
|
41
|
+
// 3. Word-boundaried substring heuristics.
|
|
42
|
+
if (/\baborted\b|\babort signal\b/i.test(message)) {
|
|
43
|
+
return { status: 499, type: "request_aborted", message };
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
// Match rate-limit phrasings before auth wording: some providers
|
|
47
|
+
// describe throttling as "unauthorized due to rate limit".
|
|
48
|
+
// Keep boundaries so this does not collide with
|
|
49
|
+
// `GenerateContentRequest`, `accelerate`, `iterate`, `deprecated`, etc.
|
|
50
|
+
/\brate[- _]?limit(?:s|ed|ing)?\b|\bquota(?:_exceeded| exceeded)?\b|\btoo[- _]many[- _]requests\b/i.test(
|
|
51
|
+
message,
|
|
52
|
+
) ||
|
|
53
|
+
// Usage-limit phrasings emit no embedded status. Codex friendly text
|
|
54
|
+
// reads "You have hit your ChatGPT usage limit … Try again in ~158
|
|
55
|
+
// min."; the central usage-limit classifier already encodes every known
|
|
56
|
+
// provider variant, so reuse it instead of forking the regex. Without
|
|
57
|
+
// this branch the classifier falls through to the default
|
|
58
|
+
// 502/upstream_error, which is what callers saw when their account
|
|
59
|
+
// hit its cap.
|
|
60
|
+
isUsageLimit(message)
|
|
61
|
+
) {
|
|
62
|
+
return { status: 429, type: "rate_limit_error", message };
|
|
63
|
+
}
|
|
64
|
+
if (/\b(?:unauthorized|forbidden)\b/i.test(message)) {
|
|
65
|
+
return { status: 401, type: "authentication_error", message };
|
|
66
|
+
}
|
|
67
|
+
if (/\b(?:unsupported|invalid_request|invalid request|bad request|malformed)\b/i.test(message)) {
|
|
68
|
+
return { status: 400, type: "invalid_request_error", message };
|
|
69
|
+
}
|
|
70
|
+
return { status: 502, type: "upstream_error", message };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function bucketStatus(status: number, message: string): GatewayErrorClassification {
|
|
74
|
+
if (status === 401 || status === 403) return { status, type: "authentication_error", message };
|
|
75
|
+
if (status === 429) return { status, type: "rate_limit_error", message };
|
|
76
|
+
if (status >= 400 && status < 500) return { status, type: "invalid_request_error", message };
|
|
77
|
+
if (status >= 500) return { status, type: "upstream_error", message };
|
|
78
|
+
return { status: 502, type: "upstream_error", message };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pull a status code from common error-message shapes. Returns undefined when
|
|
83
|
+
* no contextual keyword is present, so we never guess at incidental numbers.
|
|
84
|
+
*/
|
|
85
|
+
function extractEmbeddedStatus(message: string): number | undefined {
|
|
86
|
+
// `Google API error (400)`, `OpenAI API error (429): …`, `(503)`
|
|
87
|
+
// `HTTP 429: too many requests`
|
|
88
|
+
// `status: 503`, `status_code=429`, `status=400`
|
|
89
|
+
const re = /(?:\bHTTP\b|\bAPI error\b|\bstatus(?:[- _]?code)?\b)\s*[:=]?\s*\(?\s*(\d{3})\b|\((\d{3})\)/i;
|
|
90
|
+
const m = message.match(re);
|
|
91
|
+
if (!m) return undefined;
|
|
92
|
+
const raw = m[1] ?? m[2];
|
|
93
|
+
if (!raw) return undefined;
|
|
94
|
+
const code = Number.parseInt(raw, 10);
|
|
95
|
+
return Number.isFinite(code) && code >= 100 && code < 600 ? code : undefined;
|
|
96
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./abort";
|
|
2
|
+
export * from "./auth";
|
|
3
|
+
export * from "./auth-classify";
|
|
4
|
+
export * from "./aws";
|
|
5
|
+
export * from "./classes";
|
|
6
|
+
export * from "./finalize";
|
|
7
|
+
export * from "./flags";
|
|
8
|
+
export * from "./format";
|
|
9
|
+
export * from "./gateway";
|
|
10
|
+
export * from "./oauth";
|
|
11
|
+
export * from "./provider";
|
|
12
|
+
export * from "./retryable";
|
|
13
|
+
export * from "./validation";
|