@code-yeongyu/senpi-ai 2026.10.31-2 → 2026.11.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.
Files changed (45) hide show
  1. package/dist/api/openai-responses-shared.d.ts +2 -0
  2. package/dist/api/openai-responses-shared.js +19 -3
  3. package/dist/api/openai-responses.js +30 -9
  4. package/dist/auth/credential-store.d.ts +3 -1
  5. package/dist/auth/credential-store.js +78 -0
  6. package/dist/auth/oauth/kimi-coding.js +6 -4
  7. package/dist/auth/oauth/openai-chatgpt-model-catalog.d.ts +36 -0
  8. package/dist/auth/oauth/openai-chatgpt-model-catalog.js +104 -0
  9. package/dist/auth/oauth/openai-chatgpt.js +76 -0
  10. package/dist/auth/oauth/radius.js +1 -2
  11. package/dist/auth/oauth-refresh.d.ts +2 -0
  12. package/dist/auth/oauth-refresh.js +53 -7
  13. package/dist/auth/pool/failover.d.ts +0 -40
  14. package/dist/auth/pool/failover.js +0 -76
  15. package/dist/auth/pool/slots.js +10 -0
  16. package/dist/auth/resolve.js +3 -0
  17. package/dist/auth/types.d.ts +11 -0
  18. package/dist/models.d.ts +4 -5
  19. package/dist/models.js +36 -1
  20. package/dist/provider-diagnostic.d.ts +2 -0
  21. package/dist/provider-diagnostic.js +6 -1
  22. package/dist/providers/data/.manifest.json +1 -1
  23. package/dist/providers/data/opengateway.json +1 -1
  24. package/dist/providers/data/openrouter.json +1 -1
  25. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  26. package/dist/types.d.ts +6 -0
  27. package/dist/utils/empty-response-errors.d.ts +1 -2
  28. package/dist/utils/empty-response-errors.js +1 -2
  29. package/dist/utils/oauth-refresh-error.d.ts +8 -1
  30. package/dist/utils/oauth-refresh-error.js +3 -1
  31. package/dist/utils/provider-diagnostic-carrier.d.ts +10 -1
  32. package/dist/utils/provider-diagnostic-carrier.js +35 -0
  33. package/dist/utils/provider-diagnostic-sources.d.ts +2 -0
  34. package/dist/utils/provider-diagnostic-sources.js +5 -0
  35. package/dist/utils/provider-diagnostic-vocabulary.d.ts +1 -0
  36. package/dist/utils/provider-diagnostic-vocabulary.js +9 -0
  37. package/dist/utils/provider-retry.js +4 -1
  38. package/dist/utils/retry.d.ts +2 -0
  39. package/dist/utils/retry.js +14 -3
  40. package/dist/utils/siwc-errors.d.ts +2 -0
  41. package/dist/utils/siwc-errors.js +2 -0
  42. package/dist/utils/tool-name-match.js +46 -1
  43. package/package.json +2 -2
  44. package/dist/auth/pool/classify.d.ts +0 -16
  45. package/dist/auth/pool/classify.js +0 -75
package/dist/types.d.ts CHANGED
@@ -19,6 +19,12 @@ import type { AssistantMessageEventStream } from "./utils/event-stream.ts";
19
19
  export type { Model } from "./model.ts";
20
20
  export type { SessionAffinityFormat } from "./openai-responses-compat.ts";
21
21
  export type OpenAIResponsesCompat = BaseOpenAIResponsesCompat & {
22
+ /**
23
+ * The selected credential uses Sign in with ChatGPT plan usage on the public Responses API.
24
+ * Set per credential route, not by token prefix or provider id. Never set for API keys
25
+ * or the ChatGPT backend/Codex route.
26
+ */
27
+ siwc?: boolean;
22
28
  /**
23
29
  * Exact native search type for a compatible Responses endpoint.
24
30
  * OpenAI supports both names; xAI documents only web_search.
@@ -10,6 +10,5 @@
10
10
  */
11
11
  export declare const EMPTY_RESPONSE_ERROR = "Model returned an empty response twice";
12
12
  export declare const EMPTY_TOOL_USE_ERROR = "Model returned tool_use without a tool call twice";
13
- export declare const FORWARDED_EMPTY_RESPONSE_ERROR = "Model returned an empty response after streaming thinking";
14
- export declare const FORWARDED_EMPTY_TOOL_USE_ERROR = "Model returned tool_use without a tool call after streaming thinking";
13
+ export { FORWARDED_EMPTY_RESPONSE_ERROR, FORWARDED_EMPTY_TOOL_USE_ERROR } from "./retry.ts";
15
14
  //# sourceMappingURL=empty-response-errors.d.ts.map
@@ -10,6 +10,5 @@
10
10
  */
11
11
  export const EMPTY_RESPONSE_ERROR = "Model returned an empty response twice";
12
12
  export const EMPTY_TOOL_USE_ERROR = "Model returned tool_use without a tool call twice";
13
- export const FORWARDED_EMPTY_RESPONSE_ERROR = "Model returned an empty response after streaming thinking";
14
- export const FORWARDED_EMPTY_TOOL_USE_ERROR = "Model returned tool_use without a tool call after streaming thinking";
13
+ export { FORWARDED_EMPTY_RESPONSE_ERROR, FORWARDED_EMPTY_TOOL_USE_ERROR } from "./retry.js";
15
14
  //# sourceMappingURL=empty-response-errors.js.map
@@ -1,7 +1,14 @@
1
1
  import { ModelsError } from "./models-error.ts";
2
2
  export declare class OAuthTokenEndpointError extends Error {
3
3
  readonly status: number;
4
- constructor(message: string, status: number);
4
+ /** The OAuth `error` code of the response body (RFC 6749 section 5.2), when the adapter read one. */
5
+ readonly oauthError?: string;
6
+ /** The adapter itself decided that the endpoint refused this credential, so it needs a new login. */
7
+ readonly credentialRefused: boolean;
8
+ constructor(message: string, status: number, facts?: {
9
+ oauthError?: string;
10
+ credentialRefused?: boolean;
11
+ });
5
12
  }
6
13
  /** Closed, log-safe facts only; never inspect the message or response body. */
7
14
  export declare function oauthRefreshFailureCause(error: unknown): string | undefined;
@@ -1,9 +1,11 @@
1
1
  var _a;
2
2
  import { ModelsError } from "./models-error.js";
3
3
  export class OAuthTokenEndpointError extends Error {
4
- constructor(message, status) {
4
+ constructor(message, status, facts = {}) {
5
5
  super(message);
6
6
  this.status = status;
7
+ this.oauthError = facts.oauthError;
8
+ this.credentialRefused = facts.credentialRefused ?? false;
7
9
  }
8
10
  }
9
11
  const TRANSPORT_CODES = new Set([
@@ -1,5 +1,14 @@
1
- import type { ProviderDiagnostic } from "../provider-diagnostic.ts";
1
+ import type { ProviderDiagnostic, ProviderDiagnosticCategory } from "../provider-diagnostic.ts";
2
+ type SiwcRecovery = "usage_limit" | "account_disabled" | "auth" | "request" | "transient";
3
+ export declare function siwcErrorForCode(code: string): {
4
+ readonly category: ProviderDiagnosticCategory;
5
+ readonly status: number;
6
+ readonly recovery: SiwcRecovery;
7
+ } | undefined;
8
+ /** Shared by HTTP retry, assistant retry, and the credential pool; unknown codes stay unknown. */
9
+ export declare function siwcRecoveryFromDiagnostic(diagnostic: ProviderDiagnostic | undefined): SiwcRecovery | undefined;
2
10
  export declare function attachProviderDiagnostic<E extends object>(error: E, diagnostic: ProviderDiagnostic | undefined): E;
3
11
  /** Raw lookup; callers revalidate before exposing the value. */
4
12
  export declare function peekProviderDiagnostic(error: unknown): ProviderDiagnostic | undefined;
13
+ export {};
5
14
  //# sourceMappingURL=provider-diagnostic-carrier.d.ts.map
@@ -1,3 +1,38 @@
1
+ /** Exact Responses codes, not prose: errors-and-recovery#structured-responses-errors. */
2
+ const SIWC_ERRORS = {
3
+ subscription_sharing_usage_limit_exceeded: { category: "quota", status: 429, recovery: "usage_limit" },
4
+ subscription_sharing_user_not_eligible: { category: "unknown", status: 403, recovery: "account_disabled" },
5
+ subscription_sharing_usage_unavailable: { category: "provider_unavailable", status: 503, recovery: "transient" },
6
+ subscription_sharing_user_unavailable: { category: "provider_unavailable", status: 503, recovery: "transient" },
7
+ subscription_sharing_invalid_user: { category: "auth", status: 401, recovery: "auth" },
8
+ subscription_sharing_unsupported_capability: { category: "invalid_request", status: 400, recovery: "request" },
9
+ subscription_sharing_route_not_supported: { category: "invalid_request", status: 403, recovery: "request" },
10
+ chatpass_v2_scope_not_authorized: { category: "invalid_request", status: 403, recovery: "request" },
11
+ chatpass_v2_invalid_authorization_context: { category: "invalid_request", status: 403, recovery: "request" },
12
+ };
13
+ export function siwcErrorForCode(code) {
14
+ return Object.hasOwn(SIWC_ERRORS, code) ? SIWC_ERRORS[code] : undefined;
15
+ }
16
+ /** Shared by HTTP retry, assistant retry, and the credential pool; unknown codes stay unknown. */
17
+ export function siwcRecoveryFromDiagnostic(diagnostic) {
18
+ if (diagnostic?.evidence === "structured_code" && diagnostic.code !== undefined) {
19
+ return siwcErrorForCode(diagnostic.code)?.recovery;
20
+ }
21
+ // A direct-admission detail body has no code. Route provenance is necessary here:
22
+ // a generic provider's 403 must retain its existing credential policy.
23
+ if (diagnostic?.route !== "siwc" || diagnostic.evidence !== "structured_status")
24
+ return undefined;
25
+ switch (diagnostic.httpStatus) {
26
+ case 401:
27
+ return "auth";
28
+ case 403:
29
+ return "request";
30
+ case 503:
31
+ return "transient";
32
+ default:
33
+ return undefined;
34
+ }
35
+ }
1
36
  /**
2
37
  * Adapter-owned side channel for a diagnostic that rides along a thrown provider
3
38
  * error. The error keeps its exact message and identity; the diagnostic lives in
@@ -16,6 +16,8 @@ export declare function anthropicProviderDiagnosticFromSseData(data: string): Pr
16
16
  * buckets that can contradict the precise code.
17
17
  */
18
18
  export declare function openAICompatibleProviderDiagnosticFromError(error: unknown): ProviderDiagnostic | undefined;
19
+ /** SIWC transport boundary, including direct-admission {"detail": ...} bodies. */
20
+ export declare function siwcProviderDiagnosticFromError(error: unknown): ProviderDiagnostic | undefined;
19
21
  export declare function transportProviderDiagnosticFromError(error: unknown): ProviderDiagnostic | undefined;
20
22
  /**
21
23
  * Await one provider transport call and, if it rejects, attach the classifier's
@@ -97,6 +97,11 @@ export function openAICompatibleProviderDiagnosticFromError(error) {
97
97
  return undefined;
98
98
  }
99
99
  }
100
+ /** SIWC transport boundary, including direct-admission {"detail": ...} bodies. */
101
+ export function siwcProviderDiagnosticFromError(error) {
102
+ const diagnostic = openAICompatibleProviderDiagnosticFromError(error);
103
+ return diagnostic === undefined ? undefined : { ...diagnostic, route: "siwc" };
104
+ }
100
105
  /**
101
106
  * A connection failure carries no HTTP status; it proves only that the request got
102
107
  * no provider answer. Reads the error's own `code`, then `cause.code` (fetch
@@ -31,6 +31,7 @@ export declare function buildProviderDiagnostic(input: {
31
31
  code?: string;
32
32
  evidence: ProviderDiagnosticEvidence;
33
33
  resetsAt?: number;
34
+ route?: "siwc";
34
35
  }): ProviderDiagnostic | undefined;
35
36
  export interface ProviderFailureFacts {
36
37
  /** Status read from the transport error; `undefined` for an in-stream error. */
@@ -1,3 +1,4 @@
1
+ import { siwcErrorForCode } from "./provider-diagnostic-carrier.js";
1
2
  /** Serialized UTF-8 budget for one emitted diagnostic. */
2
3
  export const PROVIDER_DIAGNOSTIC_MAX_BYTES = 512;
3
4
  /**
@@ -8,6 +9,9 @@ export const PROVIDER_DIAGNOSTIC_MAX_BYTES = 512;
8
9
  * `billing_error` proves a billing failure, not quota exhaustion, so it stays `unknown`.
9
10
  */
10
11
  export function categoryForProviderCode(code) {
12
+ const siwc = siwcErrorForCode(code);
13
+ if (siwc !== undefined)
14
+ return siwc.category;
11
15
  switch (code) {
12
16
  case "authentication_error":
13
17
  case "invalid_api_key":
@@ -63,6 +67,9 @@ export function categoryForTransportCode(code) {
63
67
  }
64
68
  /** The HTTP status a recognized token may arrive with; anything else is contradictory evidence. */
65
69
  export function isProviderCodeStatusCompatible(code, status) {
70
+ const siwc = siwcErrorForCode(code);
71
+ if (siwc !== undefined)
72
+ return status === siwc.status;
66
73
  switch (code) {
67
74
  case "authentication_error":
68
75
  case "invalid_api_key":
@@ -142,6 +149,8 @@ export function buildProviderDiagnostic(input) {
142
149
  diagnostic.httpStatus = input.httpStatus;
143
150
  if (input.code !== undefined)
144
151
  diagnostic.code = input.code;
152
+ if (input.route !== undefined)
153
+ diagnostic.route = input.route;
145
154
  if (input.resetsAt !== undefined && (input.category === "quota" || input.category === "rate_limit"))
146
155
  diagnostic.resetsAt = input.resetsAt;
147
156
  const size = new TextEncoder().encode(JSON.stringify(diagnostic)).length;
@@ -1,4 +1,4 @@
1
- import { attachProviderDiagnostic, peekProviderDiagnostic } from "./provider-diagnostic-carrier.js";
1
+ import { attachProviderDiagnostic, peekProviderDiagnostic, siwcRecoveryFromDiagnostic, } from "./provider-diagnostic-carrier.js";
2
2
  import { appendRetryAfterMsMarker, extract429RetryAfterMs } from "./retry-hint.js";
3
3
  const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
4
4
  const DIGITALOCEAN_STREAM_FAILURE_MESSAGE = "Upstream error from DigitalOcean: stream failed";
@@ -14,6 +14,9 @@ function isProviderError(error) {
14
14
  }
15
15
  /** Mirrors the pinned OpenAI/Anthropic SDK retry policy; review when either SDK is upgraded. */
16
16
  function isRetryableProviderError(error) {
17
+ const siwc = siwcRecoveryFromDiagnostic(peekProviderDiagnostic(error));
18
+ if (siwc !== undefined)
19
+ return siwc === "transient";
17
20
  const shouldRetry = error.headers?.get("x-should-retry");
18
21
  if (shouldRetry === "true")
19
22
  return true;
@@ -1,4 +1,6 @@
1
1
  import type { AssistantMessage } from "../types.ts";
2
+ export declare const FORWARDED_EMPTY_RESPONSE_ERROR = "Model returned an empty response after streaming thinking";
3
+ export declare const FORWARDED_EMPTY_TOOL_USE_ERROR = "Model returned tool_use without a tool call after streaming thinking";
2
4
  /**
3
5
  * Diagnostic type on a terminal assistant message whose request failed because an OAuth refresh failed
4
6
  * transiently (senpi#2893). Defined here, beside the classifier that reads it, so the retry entry graph
@@ -1,4 +1,8 @@
1
- import { FORWARDED_EMPTY_RESPONSE_ERROR, FORWARDED_EMPTY_TOOL_USE_ERROR } from "./empty-response-errors.js";
1
+ import { siwcRecoveryFromDiagnostic } from "./provider-diagnostic-carrier.js";
2
+ // Owned beside their classifier so retry stays a two-file entry graph. The stream
3
+ // wrapper's empty-response-errors entry re-exports these same terminal texts.
4
+ export const FORWARDED_EMPTY_RESPONSE_ERROR = "Model returned an empty response after streaming thinking";
5
+ export const FORWARDED_EMPTY_TOOL_USE_ERROR = "Model returned tool_use without a tool call after streaming thinking";
2
6
  /**
3
7
  * Diagnostic type on a terminal assistant message whose request failed because an OAuth refresh failed
4
8
  * transiently (senpi#2893). Defined here, beside the classifier that reads it, so the retry entry graph
@@ -102,8 +106,8 @@ const NON_RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
102
106
  "tools\\.[^ ]*function\\.parameters",
103
107
  "tools\\.\\d+\\.function\\.parameters",
104
108
  "invalid tool schema",
105
- // Sign in with ChatGPT: the subscription's shared usage limit, which resets
106
- // after hours rather than seconds.
109
+ // Legacy text-only SIWC failures. Structured diagnostics take precedence below;
110
+ // this code alone proves no reset time.
107
111
  "subscription_sharing_usage_limit_exceeded",
108
112
  ]);
109
113
  const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
@@ -389,6 +393,13 @@ export async function retryAssistantCall(produce, policy, signal, callbacks) {
389
393
  * before restarting the assistant turn.
390
394
  */
391
395
  export function isRetryableAssistantError(message) {
396
+ const siwc = siwcRecoveryFromDiagnostic(message.providerDiagnostic);
397
+ if (siwc !== undefined &&
398
+ message.stopReason === "error" &&
399
+ message.stopDetails?.type !== "refusal" &&
400
+ message.stopDetails?.type !== "sensitive") {
401
+ return siwc === "transient";
402
+ }
392
403
  // A route that does not serve the model answers the same on every attempt, whatever status wraps it.
393
404
  if (message.errorCause?.type === "model_config")
394
405
  return false;
@@ -0,0 +1,2 @@
1
+ export { siwcRecoveryFromDiagnostic } from "./provider-diagnostic-carrier.ts";
2
+ //# sourceMappingURL=siwc-errors.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { siwcRecoveryFromDiagnostic } from "./provider-diagnostic-carrier.js";
2
+ //# sourceMappingURL=siwc-errors.js.map
@@ -50,6 +50,51 @@ function uniqueMatch(names, matches) {
50
50
  const found = names.filter(matches);
51
51
  return found.length === 1 ? found[0] : undefined;
52
52
  }
53
+ /**
54
+ * Words that turn a stray prefix into an instruction rather than noise: a call
55
+ * named `delete_write` or `force_bash` is refused, never run as `write` or
56
+ * `bash`.
57
+ */
58
+ const DESTRUCTIVE_PREFIX_WORDS = new Set([
59
+ "delete",
60
+ "remove",
61
+ "rm",
62
+ "force",
63
+ "drop",
64
+ "purge",
65
+ "wipe",
66
+ "kill",
67
+ "reset",
68
+ "overwrite",
69
+ "truncate",
70
+ "destroy",
71
+ ]);
72
+ /**
73
+ * Last resort for a plain name carrying a stray word prefix (`trophy_eval` for
74
+ * `eval`, omo#12065). The prefixes models emit there are arbitrary words a
75
+ * namespacing proxy invented (`trophy`, `fringe`, `board`, `column`), so the
76
+ * prefix cannot be allowlisted; instead it is refused when it carries meaning.
77
+ * Resolves to the one registered tool whose exact name is a `_`-separated
78
+ * suffix of the requested name, unless any word of the stripped prefix is a
79
+ * destructive verb (`delete_write`) or itself names a registered tool
80
+ * (`read_write`). Namespaced names are excluded so `mcp__sandbox__run_bash`
81
+ * still never falls through to a local `bash`, and two suffix matches
82
+ * (`x_read_file` against `read_file` and `file`) refuse.
83
+ */
84
+ function strayPrefixMatch(requested, names) {
85
+ if (toolNameForms(requested).length > 1)
86
+ return undefined;
87
+ const match = uniqueMatch(names, (name) => requested.length > name.length + 1 && requested.endsWith(`_${name}`));
88
+ if (match === undefined)
89
+ return undefined;
90
+ const prefix = requested.slice(0, requested.length - match.length - 1);
91
+ const registered = new Set(names.map(foldToolName));
92
+ const meaningful = [prefix, ...prefix.split(/[-_]+/)].some((word) => {
93
+ const key = foldToolName(word);
94
+ return DESTRUCTIVE_PREFIX_WORDS.has(key) || registered.has(key);
95
+ });
96
+ return meaningful ? undefined : match;
97
+ }
53
98
  /** Resolve `requested` to the one available tool it names, or `undefined` when none or several match. */
54
99
  export function resolveToolNameMatch(requested, available) {
55
100
  const names = [...new Set(available)];
@@ -66,6 +111,6 @@ export function resolveToolNameMatch(requested, available) {
66
111
  if (unprefixed !== undefined)
67
112
  return unprefixed;
68
113
  }
69
- return undefined;
114
+ return strayPrefixMatch(requested, names);
70
115
  }
71
116
  //# sourceMappingURL=tool-name-match.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-ai",
3
- "version": "2026.10.31-2",
3
+ "version": "2026.11.2-2",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -88,7 +88,7 @@
88
88
  "dependencies": {
89
89
  "@anthropic-ai/sdk": "0.127.0",
90
90
  "@aws-sdk/client-bedrock-runtime": "3.1136.0",
91
- "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.31-2",
91
+ "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.11.2-2",
92
92
  "@google/genai": "2.23.0",
93
93
  "@smithy/node-http-handler": "4.12.1",
94
94
  "google-auth-library": "10.6.2",
@@ -1,16 +0,0 @@
1
- export type PoolFailureAction = "rotate" | "retry" | "fail";
2
- export type PoolBlockReason = "auth_error" | "rate_limit";
3
- export type PoolFailureClassification = {
4
- action: PoolFailureAction;
5
- blockReason?: PoolBlockReason;
6
- retryAfterMs?: number;
7
- };
8
- /**
9
- * Three-way in-lane failure taxonomy. Rotation is reserved for failures another
10
- * account can plausibly absorb (rate/capacity and per-account auth); transient
11
- * transport failures stay on the same slot for the outer retry policy, and
12
- * everything unrecognized default-denies to `fail` so the model fallback chain
13
- * above this engine keeps owning unknown errors.
14
- */
15
- export declare function classifyPoolFailure(error: unknown): PoolFailureClassification;
16
- //# sourceMappingURL=classify.d.ts.map
@@ -1,75 +0,0 @@
1
- const MAX_NESTING_DEPTH = 3;
2
- function fieldOf(value, key) {
3
- if (typeof value !== "object" || value === null)
4
- return undefined;
5
- const entry = Object.entries(value).find(([candidate]) => candidate === key);
6
- return entry?.[1];
7
- }
8
- function statusOf(error, depth = 0) {
9
- if (depth >= MAX_NESTING_DEPTH)
10
- return undefined;
11
- for (const key of ["status", "statusCode"]) {
12
- const value = fieldOf(error, key);
13
- if (typeof value === "number")
14
- return value;
15
- }
16
- const nested = fieldOf(error, "error");
17
- return nested === undefined ? undefined : statusOf(nested, depth + 1);
18
- }
19
- function messageOf(error, depth = 0) {
20
- if (typeof error === "string")
21
- return error;
22
- if (error instanceof Error)
23
- return error.message;
24
- if (depth >= MAX_NESTING_DEPTH)
25
- return String(error);
26
- const message = fieldOf(error, "message");
27
- if (typeof message === "string")
28
- return message;
29
- const nested = fieldOf(error, "error");
30
- return nested === undefined ? String(error) : messageOf(nested, depth + 1);
31
- }
32
- function retryAfterMsOf(error, text) {
33
- const explicit = fieldOf(error, "retryAfterMs");
34
- if (typeof explicit === "number" && Number.isFinite(explicit) && explicit > 0)
35
- return Math.ceil(explicit);
36
- const milliseconds = text.match(/\bretry[-_ ]?after[-_ ]?ms\s*[:=]\s*(\d+(?:\.\d+)?)/i);
37
- if (milliseconds?.[1] !== undefined)
38
- return Math.ceil(Number(milliseconds[1]));
39
- const seconds = text.match(/\bretry[-_ ]?after\s*[:=]\s*(\d+(?:\.\d+)?)/i);
40
- return seconds?.[1] === undefined ? undefined : Math.ceil(Number(seconds[1]) * 1_000);
41
- }
42
- const RATE_LIMIT_TEXT = /rate.?limit|resource_exhausted|quota|too many requests|overloaded/i;
43
- const AUTH_TEXT = /invalid.?api.?key|unauthorized|authentication|forbidden|token (?:is )?expired|revoked/i;
44
- const TRANSIENT_TEXT = /econnreset|econnrefused|etimedout|socket hang up|fetch failed|network error|internal server error|service unavailable|bad gateway|gateway timeout/i;
45
- function rotateOnRateLimit(error, text) {
46
- const retryAfterMs = retryAfterMsOf(error, text);
47
- return retryAfterMs === undefined
48
- ? { action: "rotate", blockReason: "rate_limit" }
49
- : { action: "rotate", blockReason: "rate_limit", retryAfterMs };
50
- }
51
- /**
52
- * Three-way in-lane failure taxonomy. Rotation is reserved for failures another
53
- * account can plausibly absorb (rate/capacity and per-account auth); transient
54
- * transport failures stay on the same slot for the outer retry policy, and
55
- * everything unrecognized default-denies to `fail` so the model fallback chain
56
- * above this engine keeps owning unknown errors.
57
- */
58
- export function classifyPoolFailure(error) {
59
- const text = messageOf(error);
60
- const status = statusOf(error);
61
- if (status === 429 || status === 529)
62
- return rotateOnRateLimit(error, text);
63
- if (status === 401 || status === 403)
64
- return { action: "rotate", blockReason: "auth_error" };
65
- if (status !== undefined && status >= 500 && status < 600)
66
- return { action: "retry" };
67
- if (RATE_LIMIT_TEXT.test(text))
68
- return rotateOnRateLimit(error, text);
69
- if (AUTH_TEXT.test(text))
70
- return { action: "rotate", blockReason: "auth_error" };
71
- if (TRANSIENT_TEXT.test(text))
72
- return { action: "retry" };
73
- return { action: "fail" };
74
- }
75
- //# sourceMappingURL=classify.js.map