(
getClient: () => Promise<Anthropic>,
operation: (
client: Anthropic,
attempt: number,
context: RetryContext,
) => Promise<T>,
options: RetryOptions,
)
| 168 | } |
| 169 | |
| 170 | export async function* withRetry<T>( |
| 171 | getClient: () => Promise<Anthropic>, |
| 172 | operation: ( |
| 173 | client: Anthropic, |
| 174 | attempt: number, |
| 175 | context: RetryContext, |
| 176 | ) => Promise<T>, |
| 177 | options: RetryOptions, |
| 178 | ): AsyncGenerator<SystemAPIErrorMessage, T> { |
| 179 | const maxRetries = getMaxRetries(options) |
| 180 | const retryContext: RetryContext = { |
| 181 | model: options.model, |
| 182 | thinkingConfig: options.thinkingConfig, |
| 183 | ...(isFastModeEnabled() && { fastMode: options.fastMode }), |
| 184 | } |
| 185 | let client: Anthropic | null = null |
| 186 | let consecutive529Errors = options.initialConsecutive529Errors ?? 0 |
| 187 | let lastError: unknown |
| 188 | let persistentAttempt = 0 |
| 189 | for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { |
| 190 | if (options.signal?.aborted) { |
| 191 | throw new APIUserAbortError() |
| 192 | } |
| 193 | |
| 194 | // Capture whether fast mode is active before this attempt |
| 195 | // (fallback may change the state mid-loop) |
| 196 | const wasFastModeActive = isFastModeEnabled() |
| 197 | ? retryContext.fastMode && !isFastModeCooldown() |
| 198 | : false |
| 199 | |
| 200 | try { |
| 201 | // Check for mock rate limits (used by /mock-limits command for Ant employees) |
| 202 | if (process.env.USER_TYPE === 'ant') { |
| 203 | const mockError = checkMockRateLimitError( |
| 204 | retryContext.model, |
| 205 | wasFastModeActive, |
| 206 | ) |
| 207 | if (mockError) { |
| 208 | throw mockError |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | // Get a fresh client instance on first attempt or after authentication errors |
| 213 | // - 401 for first-party API authentication failures |
| 214 | // - 403 "OAuth token has been revoked" (another process refreshed the token) |
| 215 | // - Bedrock-specific auth errors (403 or CredentialsProviderError) |
| 216 | // - Vertex-specific auth errors (credential refresh failures, 401) |
| 217 | // - ECONNRESET/EPIPE: stale keep-alive socket; disable pooling and reconnect |
| 218 | const isStaleConnection = isStaleConnectionError(lastError) |
| 219 | if ( |
| 220 | isStaleConnection && |
| 221 | getFeatureValue_CACHED_MAY_BE_STALE( |
| 222 | 'tengu_disable_keepalive_on_econnreset', |
| 223 | false, |
| 224 | ) |
| 225 | ) { |
| 226 | logForDebugging( |
| 227 | 'Stale connection (ECONNRESET/EPIPE) — disabling keep-alive for retry', |
no test coverage detected