error.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { APICallError } from "ai"
  2. import { STATUS_CODES } from "http"
  3. import { iife } from "@/util/iife"
  4. export namespace ProviderError {
  5. // Adapted from overflow detection patterns in:
  6. // https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
  7. const OVERFLOW_PATTERNS = [
  8. /prompt is too long/i, // Anthropic
  9. /input is too long for requested model/i, // Amazon Bedrock
  10. /exceeds the context window/i, // OpenAI (Completions + Responses API message text)
  11. /input token count.*exceeds the maximum/i, // Google (Gemini)
  12. /maximum prompt length is \d+/i, // xAI (Grok)
  13. /reduce the length of the messages/i, // Groq
  14. /maximum context length is \d+ tokens/i, // OpenRouter, DeepSeek
  15. /exceeds the limit of \d+/i, // GitHub Copilot
  16. /exceeds the available context size/i, // llama.cpp server
  17. /greater than the context length/i, // LM Studio
  18. /context window exceeds limit/i, // MiniMax
  19. /exceeded model token limit/i, // Kimi For Coding, Moonshot
  20. /context[_ ]length[_ ]exceeded/i, // Generic fallback
  21. ]
  22. function isOpenAiErrorRetryable(e: APICallError) {
  23. const status = e.statusCode
  24. if (!status) return e.isRetryable
  25. // openai sometimes returns 404 for models that are actually available
  26. return status === 404 || e.isRetryable
  27. }
  28. // Providers not reliably handled in this function:
  29. // - z.ai: can accept overflow silently (needs token-count/context-window checks)
  30. function isOverflow(message: string) {
  31. if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
  32. // Providers/status patterns handled outside of regex list:
  33. // - Cerebras: often returns "400 (no body)" / "413 (no body)"
  34. // - Mistral: often returns "400 (no body)" / "413 (no body)"
  35. return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
  36. }
  37. function error(providerID: string, error: APICallError) {
  38. if (providerID.includes("github-copilot") && error.statusCode === 403) {
  39. return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
  40. }
  41. return error.message
  42. }
  43. function message(providerID: string, e: APICallError) {
  44. return iife(() => {
  45. const msg = e.message
  46. if (msg === "") {
  47. if (e.responseBody) return e.responseBody
  48. if (e.statusCode) {
  49. const err = STATUS_CODES[e.statusCode]
  50. if (err) return err
  51. }
  52. return "Unknown error"
  53. }
  54. const transformed = error(providerID, e)
  55. if (transformed !== msg) {
  56. return transformed
  57. }
  58. if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
  59. return msg
  60. }
  61. try {
  62. const body = JSON.parse(e.responseBody)
  63. // try to extract common error message fields
  64. const errMsg = body.message || body.error || body.error?.message
  65. if (errMsg && typeof errMsg === "string") {
  66. return `${msg}: ${errMsg}`
  67. }
  68. } catch {}
  69. return `${msg}: ${e.responseBody}`
  70. }).trim()
  71. }
  72. function json(input: unknown) {
  73. if (typeof input === "string") {
  74. try {
  75. const result = JSON.parse(input)
  76. if (result && typeof result === "object") return result
  77. return undefined
  78. } catch {
  79. return undefined
  80. }
  81. }
  82. if (typeof input === "object" && input !== null) {
  83. return input
  84. }
  85. return undefined
  86. }
  87. export type ParsedStreamError =
  88. | {
  89. type: "context_overflow"
  90. message: string
  91. responseBody: string
  92. }
  93. | {
  94. type: "api_error"
  95. message: string
  96. isRetryable: false
  97. responseBody: string
  98. }
  99. export function parseStreamError(input: unknown): ParsedStreamError | undefined {
  100. const body = json(input)
  101. if (!body) return
  102. const responseBody = JSON.stringify(body)
  103. if (body.type !== "error") return
  104. switch (body?.error?.code) {
  105. case "context_length_exceeded":
  106. return {
  107. type: "context_overflow",
  108. message: "Input exceeds context window of this model",
  109. responseBody,
  110. }
  111. case "insufficient_quota":
  112. return {
  113. type: "api_error",
  114. message: "Quota exceeded. Check your plan and billing details.",
  115. isRetryable: false,
  116. responseBody,
  117. }
  118. case "usage_not_included":
  119. return {
  120. type: "api_error",
  121. message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
  122. isRetryable: false,
  123. responseBody,
  124. }
  125. case "invalid_prompt":
  126. return {
  127. type: "api_error",
  128. message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
  129. isRetryable: false,
  130. responseBody,
  131. }
  132. }
  133. }
  134. export type ParsedAPICallError =
  135. | {
  136. type: "context_overflow"
  137. message: string
  138. responseBody?: string
  139. }
  140. | {
  141. type: "api_error"
  142. message: string
  143. statusCode?: number
  144. isRetryable: boolean
  145. responseHeaders?: Record<string, string>
  146. responseBody?: string
  147. metadata?: Record<string, string>
  148. }
  149. export function parseAPICallError(input: { providerID: string; error: APICallError }): ParsedAPICallError {
  150. const m = message(input.providerID, input.error)
  151. if (isOverflow(m)) {
  152. return {
  153. type: "context_overflow",
  154. message: m,
  155. responseBody: input.error.responseBody,
  156. }
  157. }
  158. const metadata = input.error.url ? { url: input.error.url } : undefined
  159. return {
  160. type: "api_error",
  161. message: m,
  162. statusCode: input.error.statusCode,
  163. isRetryable: input.providerID.startsWith("openai")
  164. ? isOpenAiErrorRetryable(input.error)
  165. : input.error.isRetryable,
  166. responseHeaders: input.error.responseHeaders,
  167. responseBody: input.error.responseBody,
  168. metadata,
  169. }
  170. }
  171. }