| 66 | } |
| 67 | |
| 68 | export function retryable(error: Err, provider: string) { |
| 69 | // context overflow errors should not be retried |
| 70 | if (SessionV1.ContextOverflowError.isInstance(error)) return undefined |
| 71 | if (SessionV1.APIError.isInstance(error)) { |
| 72 | const status = error.data.statusCode |
| 73 | // 5xx errors are transient server failures and should always be retried, |
| 74 | // even when the provider SDK doesn't explicitly mark them as retryable. |
| 75 | if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined |
| 76 | if (error.data.responseBody?.includes("FreeUsageLimitError")) { |
| 77 | return { |
| 78 | message: GO_UPSELL_MESSAGE, |
| 79 | action: { |
| 80 | reason: "free_tier_limit", |
| 81 | provider, |
| 82 | title: "Free limit reached", |
| 83 | message: "Subscribe to OpenCode Go for reliable access to the best open-source models, starting at $5/month.", |
| 84 | label: "subscribe", |
| 85 | link: GO_UPSELL_URL, |
| 86 | }, |
| 87 | } |
| 88 | } |
| 89 | if (error.data.responseBody?.includes("GoUsageLimitError")) { |
| 90 | const body = parseJSON(error.data.responseBody) |
| 91 | const workspace = str(body?.metadata?.workspace) |
| 92 | const limitName = str(body?.metadata?.limitName) |
| 93 | const retryAfter = num(error.data.responseHeaders?.["retry-after"]) |
| 94 | const resetIn = iife(() => { |
| 95 | if (retryAfter === undefined) return "" |
| 96 | const seconds = Math.max(0, Math.ceil(retryAfter)) |
| 97 | const days = Math.floor(seconds / 86_400) |
| 98 | const hours = Math.floor((seconds % 86_400) / 3_600) |
| 99 | const minutes = Math.ceil((seconds % 3_600) / 60) |
| 100 | const unit = (value: number, name: string) => `${value} ${name}${value === 1 ? "" : "s"}` |
| 101 | |
| 102 | if (days > 0) return hours > 0 ? `${unit(days, "day")} ${unit(hours, "hour")}` : unit(days, "day") |
| 103 | if (hours > 0) return minutes > 0 ? `${unit(hours, "hour")} ${unit(minutes, "minute")}` : unit(hours, "hour") |
| 104 | return minutes > 0 ? unit(minutes, "minute") : "less than a minute" |
| 105 | }) |
| 106 | |
| 107 | const message = `${limitName ? `${limitName} usage limit` : "Usage limit"} reached. It will reset in ${resetIn}. To continue using this model now, enable usage from your available balance` |
| 108 | |
| 109 | const link = `https://opencode.ai/workspace/${workspace}/go` |
| 110 | return { |
| 111 | message: `${message} - ${link}`, |
| 112 | action: { |
| 113 | reason: "account_rate_limit", |
| 114 | provider, |
| 115 | title: "Go limit reached", |
| 116 | message, |
| 117 | label: "open settings", |
| 118 | link, |
| 119 | }, |
| 120 | } |
| 121 | } |
| 122 | return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message } |
| 123 | } |
| 124 | |
| 125 | // Check for rate limit patterns in plain text error messages |