retry.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import type { NamedError } from "@opencode-ai/util/error"
  2. import { MessageV2 } from "./message-v2"
  3. export namespace SessionRetry {
  4. export const RETRY_INITIAL_DELAY = 2000
  5. export const RETRY_BACKOFF_FACTOR = 2
  6. export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
  7. export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
  8. export async function sleep(ms: number, signal: AbortSignal): Promise<void> {
  9. return new Promise((resolve, reject) => {
  10. const abortHandler = () => {
  11. clearTimeout(timeout)
  12. reject(new DOMException("Aborted", "AbortError"))
  13. }
  14. const timeout = setTimeout(
  15. () => {
  16. signal.removeEventListener("abort", abortHandler)
  17. resolve()
  18. },
  19. Math.min(ms, RETRY_MAX_DELAY),
  20. )
  21. signal.addEventListener("abort", abortHandler, { once: true })
  22. })
  23. }
  24. export function delay(attempt: number, error?: MessageV2.APIError) {
  25. if (error) {
  26. const headers = error.data.responseHeaders
  27. if (headers) {
  28. const retryAfterMs = headers["retry-after-ms"]
  29. if (retryAfterMs) {
  30. const parsedMs = Number.parseFloat(retryAfterMs)
  31. if (!Number.isNaN(parsedMs)) {
  32. return parsedMs
  33. }
  34. }
  35. const retryAfter = headers["retry-after"]
  36. if (retryAfter) {
  37. const parsedSeconds = Number.parseFloat(retryAfter)
  38. if (!Number.isNaN(parsedSeconds)) {
  39. // convert seconds to milliseconds
  40. return Math.ceil(parsedSeconds * 1000)
  41. }
  42. // Try parsing as HTTP date format
  43. const parsed = Date.parse(retryAfter) - Date.now()
  44. if (!Number.isNaN(parsed) && parsed > 0) {
  45. return Math.ceil(parsed)
  46. }
  47. }
  48. return RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)
  49. }
  50. }
  51. return Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS)
  52. }
  53. export function retryable(error: ReturnType<NamedError["toObject"]>) {
  54. if (MessageV2.APIError.isInstance(error)) {
  55. if (!error.data.isRetryable) return undefined
  56. return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
  57. }
  58. if (typeof error.data?.message === "string") {
  59. try {
  60. const json = JSON.parse(error.data.message)
  61. if (json.type === "error" && json.error?.type === "too_many_requests") {
  62. return "Too Many Requests"
  63. }
  64. if (json.code.includes("exhausted") || json.code.includes("unavailable")) {
  65. return "Provider is overloaded"
  66. }
  67. if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
  68. return "Rate Limited"
  69. }
  70. if (
  71. json.error?.message?.includes("no_kv_space") ||
  72. (json.type === "error" && json.error?.type === "server_error") ||
  73. !!json.error
  74. ) {
  75. return "Provider Server Error"
  76. }
  77. } catch {}
  78. }
  79. return undefined
  80. }
  81. }