From b5e44e493aa6873bed483b47c288f6e0dcbc61d2 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 24 Feb 2026 11:47:36 -0800 Subject: [PATCH] fix: skip retry/recovery for non-retryable API errors (#384) --- src/core/bot.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/core/bot.ts b/src/core/bot.ts index 00bc42d..d9208c3 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -67,6 +67,11 @@ function formatApiErrorForUser(error: { message: string; stopReason: string; api const apiMsg = (typeof apiError.message === 'string' ? apiError.message : '').toLowerCase(); const reasons: string[] = Array.isArray(apiError.reasons) ? apiError.reasons : []; + // Billing / credits exhausted + if (msg.includes('out of credits') || apiMsg.includes('out of credits')) { + return '(Out of credits for hosted inference. Add credits or enable auto-recharge at app.letta.com/settings/organization/usage.)'; + } + // Rate limiting / usage exceeded (429) if (msg.includes('rate limit') || msg.includes('429') || msg.includes('usage limit') || apiMsg.includes('rate limit') || apiMsg.includes('usage limit')) { @@ -1726,8 +1731,19 @@ export class LettaBot implements AgentSession { } } + // Non-retryable errors: billing, auth, not-found -- skip recovery/retry + // entirely and surface the error to the user immediately. + const errMsg = lastErrorDetail?.message?.toLowerCase() || ''; + const isNonRetryableError = isTerminalError && ( + errMsg.includes('out of credits') || errMsg.includes('usage limit') || + errMsg.includes('401') || errMsg.includes('403') || + errMsg.includes('unauthorized') || errMsg.includes('forbidden') || + errMsg.includes('not found') || errMsg.includes('404') || + errMsg.includes('rate limit') || errMsg.includes('429') + ); + const shouldRetryForEmptyResult = streamMsg.success && resultText === '' && nothingDelivered; - const shouldRetryForErrorResult = isTerminalError && nothingDelivered && !isConflictError; + const shouldRetryForErrorResult = isTerminalError && nothingDelivered && !isConflictError && !isNonRetryableError; if (shouldRetryForEmptyResult || shouldRetryForErrorResult) { if (shouldRetryForEmptyResult) { log.error(`Warning: Agent returned empty result with no response. stopReason=${streamMsg.stopReason || 'N/A'}, conv=${streamMsg.conversationId || 'N/A'}`);