error.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { APICallError } from "ai"
  2. import { STATUS_CODES } from "http"
  3. import { iife } from "@/util/iife"
  4. import type { ProviderV2 } from "@kirincode-ai/core/provider"
  5. import { isContextOverflow } from "@kirincode-ai/llm"
  6. export class HeaderTimeoutError extends Error {
  7. public override readonly name = "ProviderHeaderTimeoutError"
  8. constructor(public readonly ms: number) {
  9. super(`Provider response headers timed out after ${ms}ms`)
  10. }
  11. }
  12. export class ResponseStreamError extends Error {
  13. public override readonly name = "ProviderResponseStreamError"
  14. constructor(message: string, options?: ErrorOptions) {
  15. super(message, options)
  16. }
  17. }
  18. function isOpenAiErrorRetryable(e: APICallError) {
  19. const status = e.statusCode
  20. if (!status) return e.isRetryable
  21. // openai sometimes returns 404 for models that are actually available
  22. return status === 404 || e.isRetryable
  23. }
  24. // Providers not reliably handled in this function:
  25. // - z.ai: can accept overflow silently (needs token-count/context-window checks)
  26. function message(providerID: ProviderV2.ID, e: APICallError) {
  27. return iife(() => {
  28. const msg = e.message
  29. if (msg === "") {
  30. if (e.responseBody) return e.responseBody
  31. if (e.statusCode) {
  32. const err = STATUS_CODES[e.statusCode]
  33. if (err) return err
  34. }
  35. return "Unknown error"
  36. }
  37. if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
  38. return msg
  39. }
  40. try {
  41. const body = JSON.parse(e.responseBody)
  42. // try to extract common error message fields
  43. const errMsg = body.message || body.error || body.error?.message
  44. if (errMsg && typeof errMsg === "string") {
  45. return `${msg}: ${errMsg}`
  46. }
  47. } catch {}
  48. // If responseBody is HTML (e.g. from a gateway or proxy error page),
  49. // provide a human-readable message instead of dumping raw markup
  50. if (/^\s*<!doctype|^\s*<html/i.test(e.responseBody)) {
  51. if (e.statusCode === 401) {
  52. return "Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running `opencode auth login <your provider URL>` to re-authenticate."
  53. }
  54. if (e.statusCode === 403) {
  55. return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings."
  56. }
  57. return msg
  58. }
  59. return `${msg}: ${e.responseBody}`
  60. }).trim()
  61. }
  62. function json(input: unknown) {
  63. if (typeof input === "string") {
  64. try {
  65. const result = JSON.parse(input)
  66. if (result && typeof result === "object") return result
  67. return undefined
  68. } catch {
  69. return undefined
  70. }
  71. }
  72. if (typeof input === "object" && input !== null) {
  73. return input
  74. }
  75. return undefined
  76. }
  77. export type ParsedStreamError =
  78. | {
  79. type: "context_overflow"
  80. message: string
  81. responseBody: string
  82. }
  83. | {
  84. type: "api_error"
  85. message: string
  86. isRetryable: boolean
  87. responseBody: string
  88. }
  89. export function parseStreamError(input: unknown): ParsedStreamError | undefined {
  90. const raw = json(input)
  91. const body = typeof raw?.message === "string" ? (json(raw.message) ?? raw) : raw
  92. if (!body) return
  93. const responseBody = JSON.stringify(body)
  94. if (body.type !== "error") return
  95. switch (body?.error?.code) {
  96. case "context_length_exceeded":
  97. return {
  98. type: "context_overflow",
  99. message: "Input exceeds context window of this model",
  100. responseBody,
  101. }
  102. case "insufficient_quota":
  103. return {
  104. type: "api_error",
  105. message: "Quota exceeded. Check your plan and billing details.",
  106. isRetryable: false,
  107. responseBody,
  108. }
  109. case "usage_not_included":
  110. return {
  111. type: "api_error",
  112. message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
  113. isRetryable: false,
  114. responseBody,
  115. }
  116. case "invalid_prompt":
  117. return {
  118. type: "api_error",
  119. message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
  120. isRetryable: false,
  121. responseBody,
  122. }
  123. case "server_is_overloaded":
  124. case "server_error":
  125. return {
  126. type: "api_error",
  127. message: typeof body?.error?.message === "string" ? body?.error?.message : "Server error.",
  128. isRetryable: true,
  129. responseBody,
  130. }
  131. }
  132. }
  133. export type ParsedAPICallError =
  134. | {
  135. type: "context_overflow"
  136. message: string
  137. responseBody?: string
  138. }
  139. | {
  140. type: "api_error"
  141. message: string
  142. statusCode?: number
  143. isRetryable: boolean
  144. responseHeaders?: Record<string, string>
  145. responseBody?: string
  146. metadata?: Record<string, string>
  147. }
  148. export function parseAPICallError(input: { providerID: ProviderV2.ID; error: APICallError }): ParsedAPICallError {
  149. const m = message(input.providerID, input.error)
  150. const body = json(input.error.responseBody)
  151. if (isContextOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
  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") ? isOpenAiErrorRetryable(input.error) : input.error.isRetryable,
  164. responseHeaders: input.error.responseHeaders,
  165. responseBody: input.error.responseBody,
  166. metadata,
  167. }
  168. }
  169. export * as ProviderError from "./error"