(err: ApiError)
| 1766 | * wording would be classified as permanent here. Document this if it occurs. |
| 1767 | */ |
| 1768 | export function isTransientRateLimit(err: ApiError): boolean { |
| 1769 | // Fix 4 (hardening): insufficient-credits is ALWAYS permanent, regardless of |
| 1770 | // any Retry-After header the response may carry. Check this SHORT-CIRCUIT first |
| 1771 | // so a credits-429 with a stray Retry-After header is never retried. |
| 1772 | if (/insufficient credits/i.test(err.message)) return false; |
| 1773 | |
| 1774 | // Primary transient signal: Retry-After header was present and parsed (set on |
| 1775 | // the error by HttpClient when retryOnRateLimit: false is used and the HTTP |
| 1776 | // layer throws after a single 429). |
| 1777 | if (err.retryAfterMs !== undefined) return true; |
| 1778 | // Secondary: the per-minute rate-limit message wording. |
| 1779 | if (/run trigger rate limit exceeded/i.test(err.message)) return true; |
| 1780 | // Details presence (retryAfterSeconds in body) also indicates the throttle path. |
| 1781 | const retryAfterSec = err.getDetail<number>( |
| 1782 | 'retryAfterSeconds', |
| 1783 | (v): v is number => typeof v === 'number' && v > 0, |
| 1784 | ); |
| 1785 | if (retryAfterSec !== undefined) return true; |
| 1786 | // Absent all signals → treat as permanent (credit depletion or unknown). |
| 1787 | return false; |
| 1788 | } |
| 1789 | |
| 1790 | interface CreateFromPlanOptions extends CommonOptions { |
| 1791 | /** Path to the JSON file containing one `CliPlanInput`. */ |
no test coverage detected