provider-error.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { Option, Schema } from "effect"
  2. import {
  3. AuthenticationReason,
  4. ContentPolicyReason,
  5. InvalidRequestReason,
  6. LLMError,
  7. ProviderErrorEvent,
  8. ProviderInternalReason,
  9. QuotaExceededReason,
  10. RateLimitReason,
  11. UnknownProviderReason,
  12. type HttpContext,
  13. type HttpRateLimitDetails,
  14. type ProviderMetadata,
  15. } from "./schema"
  16. const patterns = [
  17. /prompt is too long/i,
  18. /input is too long for requested model/i,
  19. /exceeds the context window/i,
  20. /input token count.*exceeds the maximum/i,
  21. /tokens in request more than max tokens allowed/i,
  22. /maximum prompt length is \d+/i,
  23. /reduce the length of the messages/i,
  24. /maximum context length is \d+ tokens/i,
  25. /exceeds the limit of \d+/i,
  26. /exceeds the available context size/i,
  27. /greater than the context length/i,
  28. /context window exceeds limit/i,
  29. /exceeded model token limit/i,
  30. /context[_ ]length[_ ]exceeded/i,
  31. /request entity too large/i,
  32. /context length is only \d+ tokens/i,
  33. /input length.*exceeds.*context length/i,
  34. /prompt too long; exceeded (?:max )?context length/i,
  35. /too large for model with \d+ maximum context length/i,
  36. /model_context_window_exceeded/i,
  37. ]
  38. export const isContextOverflow = (message: string) =>
  39. patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
  40. export const isContextOverflowFailure = (failure: unknown) =>
  41. failure instanceof LLMError
  42. ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
  43. : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow"
  44. const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
  45. const QUOTA_CODES = new Set(["insufficient_quota", "usage_not_included", "billing_error"])
  46. const SERVER_CODES = new Set([
  47. "api_error",
  48. "internal_error",
  49. "internalserverexception",
  50. "modelstreamerrorexception",
  51. "overloaded_error",
  52. "server_error",
  53. "server_is_overloaded",
  54. "serviceunavailableexception",
  55. ])
  56. const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error", "validationexception"])
  57. const RATE_LIMIT_TEXT = /rate increased too quickly|rate[-_\s]?limit|too[_\s]?many[_\s]?requests/i
  58. const QUOTA_TEXT = /insufficient[-_\s]?quota|quota[-_\s]?exceeded/i
  59. const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i
  60. export interface ProviderFailure {
  61. readonly message: string
  62. readonly status?: number | undefined
  63. readonly code?: string | undefined
  64. readonly retryAfterMs?: number | undefined
  65. readonly rateLimit?: HttpRateLimitDetails | undefined
  66. readonly http?: HttpContext | undefined
  67. readonly providerMetadata?: ProviderMetadata | undefined
  68. }
  69. // Keep HTTP failures and provider-reported stream failures on one typed path so
  70. // session retry policy never needs provider-specific string matching.
  71. export function classifyProviderFailure(input: ProviderFailure): LLMError["reason"] {
  72. const body = input.http?.body ?? ""
  73. const codes = [input.code, ...providerCodes(body), ...providerCodes(input.message)]
  74. .filter((code): code is string => code !== undefined)
  75. .map((code) => code.toLowerCase())
  76. const text = body || input.message
  77. const common = { message: input.message, providerMetadata: input.providerMetadata, http: input.http }
  78. const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500)
  79. if (
  80. clientScoped &&
  81. (codes.includes("context_length_exceeded") ||
  82. codes.includes("model_context_window_exceeded") ||
  83. isContextOverflow(text))
  84. )
  85. return new InvalidRequestReason({ ...common, classification: "context-overflow" })
  86. if (CONTENT_POLICY_TEXT.test(text)) return new ContentPolicyReason(common)
  87. if (codes.some((code) => QUOTA_CODES.has(code)) || (input.status === 429 && QUOTA_TEXT.test(text)))
  88. return new QuotaExceededReason(common)
  89. if (input.status === 401) return new AuthenticationReason({ ...common, kind: "invalid" })
  90. if (input.status === 403) return new AuthenticationReason({ ...common, kind: "insufficient-permissions" })
  91. if (codes.includes("authentication_error")) return new AuthenticationReason({ ...common, kind: "invalid" })
  92. if (codes.includes("permission_error"))
  93. return new AuthenticationReason({ ...common, kind: "insufficient-permissions" })
  94. if (
  95. codes.some((code) => code.includes("rate_limit") || code === "too_many_requests" || code === "throttlingexception")
  96. )
  97. return new RateLimitReason({
  98. ...common,
  99. retryAfterMs: input.retryAfterMs,
  100. rateLimit: input.rateLimit,
  101. })
  102. if (RATE_LIMIT_TEXT.test(text))
  103. return new RateLimitReason({
  104. ...common,
  105. retryAfterMs: input.retryAfterMs,
  106. rateLimit: input.rateLimit,
  107. })
  108. if (codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable")))
  109. return new ProviderInternalReason({
  110. ...common,
  111. status: input.status,
  112. retryAfterMs: input.retryAfterMs,
  113. })
  114. if (input.status === 429) {
  115. return new RateLimitReason({
  116. ...common,
  117. retryAfterMs: input.retryAfterMs,
  118. rateLimit: input.rateLimit,
  119. })
  120. }
  121. if (input.status !== undefined && input.status >= 500)
  122. return new ProviderInternalReason({
  123. ...common,
  124. status: input.status,
  125. retryAfterMs: input.retryAfterMs,
  126. })
  127. if (codes.some((code) => INVALID_REQUEST_CODES.has(code))) return new InvalidRequestReason(common)
  128. if (
  129. input.status === 400 ||
  130. input.status === 404 ||
  131. input.status === 409 ||
  132. input.status === 413 ||
  133. input.status === 422
  134. )
  135. return new InvalidRequestReason(common)
  136. return new UnknownProviderReason({ ...common, status: input.status })
  137. }
  138. function providerCodes(value: string) {
  139. const decoded = Option.getOrUndefined(decodeJson(value))
  140. if (!isRecord(decoded)) return []
  141. const error = isRecord(decoded.error) ? decoded.error : undefined
  142. return [decoded.code, error?.code, error?.type].filter((value): value is string => typeof value === "string")
  143. }
  144. function isRecord(value: unknown): value is Record<string, unknown> {
  145. return typeof value === "object" && value !== null
  146. }