fix: pretty-print ChatGPT usage_limit_reached errors with reset time (#1177)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
jnjpng
2026-02-26 17:15:40 -08:00
committed by GitHub
parent 462d14edce
commit 932147b642
4 changed files with 193 additions and 1 deletions

View File

@@ -180,6 +180,93 @@ function getTierUsageLimitMessage(reasons: string[]): string | undefined {
return undefined;
}
const CHATGPT_USAGE_LIMIT_HINT =
"Switch models with /model, or connect your own provider keys with /connect.";
/**
* Check if a string contains a ChatGPT usage_limit_reached error with optional
* reset timing, and return a friendly message.
*
* ChatGPT wraps the error as embedded JSON inside a detail string like:
* RATE_LIMIT_EXCEEDED: ChatGPT rate limit exceeded: {"error":{"type":"usage_limit_reached",...}}
*/
export function checkChatGptUsageLimitError(text: string): string | undefined {
if (!text.includes("usage_limit_reached")) return undefined;
// Try to extract the embedded JSON object
const jsonStart = text.indexOf("{");
if (jsonStart < 0) {
return `ChatGPT usage limit reached. ${CHATGPT_USAGE_LIMIT_HINT}`;
}
try {
const parsed = JSON.parse(text.slice(jsonStart));
const errorObj = parsed.error || parsed;
if (errorObj.type !== "usage_limit_reached") return undefined;
// Extract plan type
const planType = errorObj.plan_type;
const planInfo = planType ? ` (${planType} plan)` : "";
// Extract reset timing — prefer resets_in_seconds, fall back to resets_at
let resetInfo = "Try again later";
if (
typeof errorObj.resets_in_seconds === "number" &&
errorObj.resets_in_seconds > 0
) {
resetInfo = formatResetTime(errorObj.resets_in_seconds * 1000);
} else if (typeof errorObj.resets_at === "number") {
const resetMs = errorObj.resets_at * 1000 - Date.now();
if (resetMs > 0) {
resetInfo = formatResetTime(resetMs);
}
}
return `ChatGPT usage limit reached${planInfo}. ${resetInfo}.\n${CHATGPT_USAGE_LIMIT_HINT}`;
} catch {
// JSON parse failed — return generic message
return `ChatGPT usage limit reached. ${CHATGPT_USAGE_LIMIT_HINT}`;
}
}
/**
* Walk an error object to find a ChatGPT usage_limit_reached detail string
* and format it. Handles APIError, nested run-metadata objects, and strings.
*/
function findAndFormatChatGptUsageLimit(e: unknown): string | undefined {
// Direct string
if (typeof e === "string") return checkChatGptUsageLimitError(e);
if (typeof e !== "object" || e === null) return undefined;
// APIError or Error — check .message
if (e instanceof Error) {
const msg = checkChatGptUsageLimitError(e.message);
if (msg) return msg;
}
const obj = e as Record<string, unknown>;
// Check e.error.error.detail (run-metadata shape)
if (obj.error && typeof obj.error === "object") {
const errObj = obj.error as Record<string, unknown>;
if (errObj.error && typeof errObj.error === "object") {
const inner = errObj.error as Record<string, unknown>;
if (typeof inner.detail === "string") {
const msg = checkChatGptUsageLimitError(inner.detail);
if (msg) return msg;
}
}
// Check e.error.detail
if (typeof errObj.detail === "string") {
const msg = checkChatGptUsageLimitError(errObj.detail);
if (msg) return msg;
}
}
return undefined;
}
const ENCRYPTED_CONTENT_HINT = [
"",
"This occurs when the conversation contains messages with encrypted",
@@ -307,6 +394,12 @@ export function formatErrorDetails(
const encryptedContentMsg = checkEncryptedContentError(e);
if (encryptedContentMsg) return encryptedContentMsg;
// Check for ChatGPT usage limit errors — walk nested error objects like
// checkEncryptedContentError does, since these arrive both as APIError
// and as plain run-metadata objects ({error: {error: {detail: "..."}}})
const chatGptUsageLimitMsg = findAndFormatChatGptUsageLimit(e);
if (chatGptUsageLimitMsg) return chatGptUsageLimitMsg;
// Check for Z.ai provider errors (wrapped in generic "OpenAI" messages)
const errorText =
e instanceof APIError