@oh-my-pi/pi-ai 16.2.1 → 16.2.3

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 (170) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/auth-gateway/server.d.ts +0 -19
  3. package/dist/types/auth-retry.d.ts +2 -10
  4. package/dist/types/auth-storage.d.ts +1 -2
  5. package/dist/types/dialect/demotion.d.ts +22 -0
  6. package/dist/types/dialect/index.d.ts +2 -0
  7. package/dist/types/error/abort.d.ts +14 -0
  8. package/dist/types/error/auth-classify.d.ts +16 -0
  9. package/dist/types/error/auth.d.ts +27 -0
  10. package/dist/types/error/aws.d.ts +23 -0
  11. package/dist/types/error/classes.d.ts +102 -0
  12. package/dist/types/error/finalize.d.ts +39 -0
  13. package/dist/types/error/flags.d.ts +70 -0
  14. package/dist/types/error/format.d.ts +20 -0
  15. package/dist/types/error/gateway.d.ts +20 -0
  16. package/dist/types/error/index.d.ts +13 -0
  17. package/dist/types/error/oauth.d.ts +43 -0
  18. package/dist/types/error/provider.d.ts +42 -0
  19. package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
  20. package/dist/types/error/retryable.d.ts +27 -0
  21. package/dist/types/error/validation.d.ts +32 -0
  22. package/dist/types/index.d.ts +1 -3
  23. package/dist/types/providers/amazon-bedrock.d.ts +0 -5
  24. package/dist/types/providers/anthropic-client.d.ts +2 -16
  25. package/dist/types/providers/anthropic.d.ts +6 -1
  26. package/dist/types/providers/aws-eventstream.d.ts +2 -1
  27. package/dist/types/providers/cursor.d.ts +8 -7
  28. package/dist/types/providers/google-gemini-cli.d.ts +0 -5
  29. package/dist/types/providers/google-shared.d.ts +0 -5
  30. package/dist/types/providers/ollama.d.ts +0 -5
  31. package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
  32. package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
  33. package/dist/types/providers/openai-codex-responses.d.ts +2 -2
  34. package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
  35. package/dist/types/providers/openai-responses-wire.d.ts +1 -1
  36. package/dist/types/providers/openai-responses.d.ts +3 -2
  37. package/dist/types/providers/openai-shared.d.ts +8 -6
  38. package/dist/types/providers/pi-native-client.d.ts +0 -9
  39. package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
  40. package/dist/types/types.d.ts +6 -0
  41. package/dist/types/usage/openai-codex-base-url.d.ts +17 -0
  42. package/dist/types/utils/block-symbols.d.ts +20 -0
  43. package/dist/types/utils/openai-http.d.ts +4 -8
  44. package/dist/types/utils/retry.d.ts +2 -14
  45. package/dist/types/utils/stream-markup-healing.d.ts +14 -5
  46. package/dist/types/utils/thinking-loop.d.ts +3 -1
  47. package/package.json +8 -4
  48. package/src/api-registry.ts +3 -1
  49. package/src/auth-broker/discover.ts +7 -2
  50. package/src/auth-broker/remote-store.ts +8 -7
  51. package/src/auth-gateway/server.ts +27 -115
  52. package/src/auth-retry.ts +9 -21
  53. package/src/auth-storage.ts +34 -49
  54. package/src/dialect/demotion.ts +31 -0
  55. package/src/dialect/factory.ts +0 -2
  56. package/src/dialect/index.ts +2 -0
  57. package/src/dialect/owned-stream.ts +0 -1
  58. package/src/dialect/thinking.ts +22 -10
  59. package/src/error/abort.ts +18 -0
  60. package/src/error/auth-classify.ts +30 -0
  61. package/src/error/auth.ts +48 -0
  62. package/src/error/aws.ts +31 -0
  63. package/src/error/classes.ts +186 -0
  64. package/src/error/finalize.ts +69 -0
  65. package/src/error/flags.ts +486 -0
  66. package/src/error/format.ts +36 -0
  67. package/src/error/gateway.ts +96 -0
  68. package/src/error/index.ts +13 -0
  69. package/src/error/oauth.ts +58 -0
  70. package/src/error/provider.ts +56 -0
  71. package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
  72. package/src/error/retryable.ts +70 -0
  73. package/src/error/validation.ts +44 -0
  74. package/src/index.ts +1 -3
  75. package/src/providers/amazon-bedrock.ts +61 -57
  76. package/src/providers/anthropic-client.ts +13 -41
  77. package/src/providers/anthropic-messages-server.ts +9 -2
  78. package/src/providers/anthropic.ts +84 -147
  79. package/src/providers/aws-credentials.ts +41 -11
  80. package/src/providers/aws-eventstream.ts +12 -21
  81. package/src/providers/azure-openai-responses.ts +27 -17
  82. package/src/providers/cursor.ts +59 -53
  83. package/src/providers/devin.ts +28 -20
  84. package/src/providers/gitlab-duo-workflow.ts +30 -10
  85. package/src/providers/gitlab-duo.ts +22 -6
  86. package/src/providers/google-auth.ts +15 -5
  87. package/src/providers/google-gemini-cli.ts +46 -60
  88. package/src/providers/google-shared.ts +40 -38
  89. package/src/providers/google-vertex.ts +3 -2
  90. package/src/providers/google.ts +5 -1
  91. package/src/providers/mock.ts +10 -7
  92. package/src/providers/ollama.ts +40 -30
  93. package/src/providers/openai-chat-server.ts +2 -1
  94. package/src/providers/openai-codex/request-transformer.ts +13 -12
  95. package/src/providers/openai-codex/response-handler.ts +1 -1
  96. package/src/providers/openai-codex-responses.ts +931 -1012
  97. package/src/providers/openai-completions.ts +46 -36
  98. package/src/providers/openai-responses-server-schema.ts +3 -0
  99. package/src/providers/openai-responses-server.ts +82 -30
  100. package/src/providers/openai-responses-wire.ts +1 -1
  101. package/src/providers/openai-responses.ts +50 -24
  102. package/src/providers/openai-shared.ts +118 -51
  103. package/src/providers/pi-native-client.ts +12 -18
  104. package/src/providers/pi-native-server.ts +9 -6
  105. package/src/providers/register-builtins.ts +5 -2
  106. package/src/providers/transform-messages.ts +31 -63
  107. package/src/registry/alibaba-coding-plan.ts +6 -5
  108. package/src/registry/api-key-login.ts +4 -3
  109. package/src/registry/api-key-validation.ts +4 -3
  110. package/src/registry/cloudflare-ai-gateway.ts +4 -3
  111. package/src/registry/coreweave.ts +2 -1
  112. package/src/registry/deepseek.ts +2 -1
  113. package/src/registry/google-antigravity.ts +2 -1
  114. package/src/registry/google-gemini-cli.ts +2 -1
  115. package/src/registry/kagi.ts +4 -3
  116. package/src/registry/kilo.ts +32 -10
  117. package/src/registry/litellm.ts +4 -3
  118. package/src/registry/llama-cpp.ts +3 -2
  119. package/src/registry/lm-studio.ts +3 -2
  120. package/src/registry/nvidia.ts +7 -7
  121. package/src/registry/oauth/anthropic.ts +23 -6
  122. package/src/registry/oauth/callback-server.ts +5 -3
  123. package/src/registry/oauth/cursor.ts +18 -4
  124. package/src/registry/oauth/devin.ts +14 -3
  125. package/src/registry/oauth/github-copilot.ts +21 -10
  126. package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
  127. package/src/registry/oauth/gitlab-duo.ts +30 -6
  128. package/src/registry/oauth/google-antigravity.ts +14 -5
  129. package/src/registry/oauth/google-gemini-cli.ts +24 -7
  130. package/src/registry/oauth/google-oauth-shared.ts +6 -3
  131. package/src/registry/oauth/index.ts +19 -8
  132. package/src/registry/oauth/kimi.ts +40 -10
  133. package/src/registry/oauth/openai-codex.ts +26 -11
  134. package/src/registry/oauth/opencode.ts +4 -3
  135. package/src/registry/oauth/perplexity.ts +33 -11
  136. package/src/registry/oauth/xai-oauth.ts +62 -18
  137. package/src/registry/oauth/xiaomi.ts +21 -9
  138. package/src/registry/ollama-cloud.ts +5 -4
  139. package/src/registry/ollama.ts +3 -2
  140. package/src/registry/parallel.ts +4 -3
  141. package/src/registry/qwen-portal.ts +4 -3
  142. package/src/registry/tavily.ts +4 -3
  143. package/src/registry/vercel-ai-gateway.ts +4 -3
  144. package/src/registry/vllm.ts +3 -2
  145. package/src/stream.ts +79 -24
  146. package/src/types.ts +6 -0
  147. package/src/usage/claude.ts +2 -1
  148. package/src/usage/github-copilot.ts +4 -3
  149. package/src/usage/google-antigravity.ts +2 -1
  150. package/src/usage/openai-codex-base-url.ts +30 -10
  151. package/src/utils/abort.ts +4 -2
  152. package/src/utils/block-symbols.ts +32 -0
  153. package/src/utils/event-stream.ts +15 -3
  154. package/src/utils/http-inspector.ts +4 -4
  155. package/src/utils/idle-iterator.ts +6 -5
  156. package/src/utils/openai-http.ts +11 -46
  157. package/src/utils/parse-bind.ts +7 -5
  158. package/src/utils/proxy.ts +2 -1
  159. package/src/utils/retry.ts +7 -23
  160. package/src/utils/schema/normalize.ts +3 -2
  161. package/src/utils/stream-markup-healing.ts +41 -18
  162. package/src/utils/thinking-loop.ts +15 -11
  163. package/src/utils/validation.ts +4 -3
  164. package/dist/types/dialect/pi.d.ts +0 -9
  165. package/dist/types/errors.d.ts +0 -24
  166. package/dist/types/utils/overflow.d.ts +0 -55
  167. package/src/dialect/pi.md +0 -55
  168. package/src/dialect/pi.ts +0 -600
  169. package/src/errors.ts +0 -32
  170. 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";