| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- import { Option, Schema } from "effect"
- import {
- AuthenticationReason,
- ContentPolicyReason,
- InvalidRequestReason,
- LLMError,
- ProviderErrorEvent,
- ProviderInternalReason,
- QuotaExceededReason,
- RateLimitReason,
- UnknownProviderReason,
- type HttpContext,
- type HttpRateLimitDetails,
- type ProviderMetadata,
- } from "./schema"
- const patterns = [
- /prompt is too long/i,
- /input is too long for requested model/i,
- /exceeds the context window/i,
- /input token count.*exceeds the maximum/i,
- /tokens in request more than max tokens allowed/i,
- /maximum prompt length is \d+/i,
- /reduce the length of the messages/i,
- /maximum context length is \d+ tokens/i,
- /exceeds the limit of \d+/i,
- /exceeds the available context size/i,
- /greater than the context length/i,
- /context window exceeds limit/i,
- /exceeded model token limit/i,
- /context[_ ]length[_ ]exceeded/i,
- /request entity too large/i,
- /context length is only \d+ tokens/i,
- /input length.*exceeds.*context length/i,
- /prompt too long; exceeded (?:max )?context length/i,
- /too large for model with \d+ maximum context length/i,
- /model_context_window_exceeded/i,
- ]
- export const isContextOverflow = (message: string) =>
- patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
- export const isContextOverflowFailure = (failure: unknown) =>
- failure instanceof LLMError
- ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
- : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow"
- const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
- const QUOTA_CODES = new Set(["insufficient_quota", "usage_not_included", "billing_error"])
- const SERVER_CODES = new Set([
- "api_error",
- "internal_error",
- "internalserverexception",
- "modelstreamerrorexception",
- "overloaded_error",
- "server_error",
- "server_is_overloaded",
- "serviceunavailableexception",
- ])
- const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error", "validationexception"])
- const RATE_LIMIT_TEXT = /rate increased too quickly|rate[-_\s]?limit|too[_\s]?many[_\s]?requests/i
- const QUOTA_TEXT = /insufficient[-_\s]?quota|quota[-_\s]?exceeded/i
- const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i
- export interface ProviderFailure {
- readonly message: string
- readonly status?: number | undefined
- readonly code?: string | undefined
- readonly retryAfterMs?: number | undefined
- readonly rateLimit?: HttpRateLimitDetails | undefined
- readonly http?: HttpContext | undefined
- readonly providerMetadata?: ProviderMetadata | undefined
- }
- // Keep HTTP failures and provider-reported stream failures on one typed path so
- // session retry policy never needs provider-specific string matching.
- export function classifyProviderFailure(input: ProviderFailure): LLMError["reason"] {
- const body = input.http?.body ?? ""
- const codes = [input.code, ...providerCodes(body), ...providerCodes(input.message)]
- .filter((code): code is string => code !== undefined)
- .map((code) => code.toLowerCase())
- const text = body || input.message
- const common = { message: input.message, providerMetadata: input.providerMetadata, http: input.http }
- const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500)
- if (
- clientScoped &&
- (codes.includes("context_length_exceeded") ||
- codes.includes("model_context_window_exceeded") ||
- isContextOverflow(text))
- )
- return new InvalidRequestReason({ ...common, classification: "context-overflow" })
- if (CONTENT_POLICY_TEXT.test(text)) return new ContentPolicyReason(common)
- if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text)))
- return new QuotaExceededReason(common)
- if (input.status === 401) return new AuthenticationReason({ ...common, kind: "invalid" })
- if (input.status === 403) return new AuthenticationReason({ ...common, kind: "insufficient-permissions" })
- if (codes.includes("authentication_error")) return new AuthenticationReason({ ...common, kind: "invalid" })
- if (codes.includes("permission_error"))
- return new AuthenticationReason({ ...common, kind: "insufficient-permissions" })
- if (
- codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception")
- )
- return new RateLimitReason({
- ...common,
- retryAfterMs: input.retryAfterMs,
- rateLimit: input.rateLimit,
- })
- if (RATE_LIMIT_TEXT.test(text))
- return new RateLimitReason({
- ...common,
- retryAfterMs: input.retryAfterMs,
- rateLimit: input.rateLimit,
- })
- if (codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable")))
- return new ProviderInternalReason({
- ...common,
- status: input.status,
- retryAfterMs: input.retryAfterMs,
- })
- if (input.status === 429) {
- return new RateLimitReason({
- ...common,
- retryAfterMs: input.retryAfterMs,
- rateLimit: input.rateLimit,
- })
- }
- if (input.status !== undefined && input.status >= 500)
- return new ProviderInternalReason({
- ...common,
- status: input.status,
- retryAfterMs: input.retryAfterMs,
- })
- if (codes.some((code) => INVALID_REQUEST_CODES.has(code))) return new InvalidRequestReason(common)
- if (
- input.status === 400 ||
- input.status === 404 ||
- input.status === 409 ||
- input.status === 413 ||
- input.status === 422
- )
- return new InvalidRequestReason(common)
- return new UnknownProviderReason({ ...common, status: input.status })
- }
- function providerCodes(value: string) {
- const decoded = Option.getOrUndefined(decodeJson(value))
- if (!isRecord(decoded)) return []
- const error = isRecord(decoded.error) ? decoded.error : undefined
- return [decoded.code, error?.code, error?.type].filter((value): value is string => typeof value === "string")
- }
- function isRecord(value: unknown): value is Record<string, unknown> {
- return typeof value === "object" && value !== null
- }
|