mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 05:30:21 +00:00
Merged via squash.
Prepared head SHA: 755cff833c
Co-authored-by: bugkill3r <2924124+bugkill3r@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
@@ -121,6 +121,58 @@ describe("formatAssistantErrorText", () => {
|
||||
expect(formatAssistantErrorText(msg)).toContain("rate limit reached");
|
||||
});
|
||||
|
||||
it("surfaces provider-specific rate limit message with reset time (#54433)", () => {
|
||||
const msg = makeAssistantError(
|
||||
"You have hit your ChatGPT usage limit (go plan). Try again in ~4381 min.",
|
||||
);
|
||||
const result = formatAssistantErrorText(msg);
|
||||
expect(result).toContain("4381 min");
|
||||
expect(result).toContain("go plan");
|
||||
expect(result).not.toBe("⚠️ API rate limit reached. Please try again later.");
|
||||
});
|
||||
|
||||
it("surfaces provider-specific rate limit message from JSON payload (#54433)", () => {
|
||||
const msg = makeAssistantError(
|
||||
'429 {"type":"error","error":{"type":"rate_limit_error","message":"Rate limit reached. Try again in 30 seconds."}}',
|
||||
);
|
||||
const result = formatAssistantErrorText(msg);
|
||||
expect(result).toContain("30 seconds");
|
||||
expect(result).not.toBe("⚠️ API rate limit reached. Please try again later.");
|
||||
});
|
||||
|
||||
it("returns generic rate limit message when no specific details are present", () => {
|
||||
const msg = makeAssistantError("429 Too Many Requests");
|
||||
expect(formatAssistantErrorText(msg)).toBe(
|
||||
"⚠️ API rate limit reached. Please try again later.",
|
||||
);
|
||||
});
|
||||
|
||||
it("strips leading HTTP status code prefix from non-JSON rate limit messages", () => {
|
||||
const msg = makeAssistantError("429 Your quota has been exhausted, try again in 24 hours");
|
||||
const result = formatAssistantErrorText(msg);
|
||||
expect(result).toContain("try again in 24 hours");
|
||||
expect(result).not.toMatch(/^⚠️ 429\b/);
|
||||
expect(result).toBe("⚠️ Your quota has been exhausted, try again in 24 hours");
|
||||
});
|
||||
|
||||
it("falls back to generic copy for HTML quota pages", () => {
|
||||
const msg = makeAssistantError(
|
||||
"429 <!DOCTYPE html><html><body>Your quota is exhausted</body></html>",
|
||||
);
|
||||
expect(formatAssistantErrorText(msg)).toBe(
|
||||
"⚠️ API rate limit reached. Please try again later.",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to generic copy for prefixed HTML rate-limit pages", () => {
|
||||
const msg = makeAssistantError(
|
||||
"Error: 521 <!DOCTYPE html><html><body>rate limit</body></html>",
|
||||
);
|
||||
expect(formatAssistantErrorText(msg)).toBe(
|
||||
"⚠️ API rate limit reached. Please try again later.",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a friendly message for empty stream chunk errors", () => {
|
||||
const msg = makeAssistantError("request ended without sending any chunks");
|
||||
expect(formatAssistantErrorText(msg)).toBe("LLM request timed out.");
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
extractLeadingHttpStatus,
|
||||
formatRawAssistantErrorForUi,
|
||||
isCloudflareOrHtmlErrorPage,
|
||||
parseApiErrorInfo,
|
||||
parseApiErrorPayload,
|
||||
} from "../../shared/assistant-error-format.js";
|
||||
export {
|
||||
@@ -55,9 +56,51 @@ const RATE_LIMIT_ERROR_USER_MESSAGE = "⚠️ API rate limit reached. Please try
|
||||
const OVERLOADED_ERROR_USER_MESSAGE =
|
||||
"The AI service is temporarily overloaded. Please try again in a moment.";
|
||||
|
||||
/**
|
||||
* Check whether the raw rate-limit error contains provider-specific details
|
||||
* worth surfacing (e.g. reset times, plan names, quota info). Bare status
|
||||
* codes like "429" or generic phrases like "rate limit exceeded" are not
|
||||
* considered specific enough.
|
||||
*/
|
||||
const RATE_LIMIT_SPECIFIC_HINT_RE =
|
||||
/\bmin(ute)?s?\b|\bhours?\b|\bseconds?\b|\btry again in\b|\breset\b|\bplan\b|\bquota\b/i;
|
||||
|
||||
function extractProviderRateLimitMessage(raw: string): string | undefined {
|
||||
const withoutPrefix = raw.replace(ERROR_PREFIX_RE, "").trim();
|
||||
// Try to pull a human-readable message out of a JSON error payload first.
|
||||
const info = parseApiErrorInfo(raw) ?? parseApiErrorInfo(withoutPrefix);
|
||||
// When the raw string is not a JSON payload, strip any leading HTTP status
|
||||
// code (e.g. "429 ") so the surfaced message stays clean.
|
||||
const candidate =
|
||||
info?.message ?? (extractLeadingHttpStatus(withoutPrefix)?.rest || withoutPrefix);
|
||||
|
||||
if (!candidate || !RATE_LIMIT_SPECIFIC_HINT_RE.test(candidate)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Skip HTML/Cloudflare error pages even if the body mentions quota/plan text.
|
||||
if (isCloudflareOrHtmlErrorPage(withoutPrefix)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Avoid surfacing very long or clearly non-human-readable blobs.
|
||||
const trimmed = candidate.trim();
|
||||
if (
|
||||
trimmed.length > 300 ||
|
||||
trimmed.startsWith("{") ||
|
||||
/^(?:<!doctype\s+html\b|<html\b)/i.test(trimmed)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `⚠️ ${trimmed}`;
|
||||
}
|
||||
|
||||
function formatRateLimitOrOverloadedErrorCopy(raw: string): string | undefined {
|
||||
if (isRateLimitErrorMessage(raw)) {
|
||||
return RATE_LIMIT_ERROR_USER_MESSAGE;
|
||||
// Surface the provider's specific message when it contains actionable
|
||||
// details (reset time, plan name, quota info) instead of the generic copy.
|
||||
return extractProviderRateLimitMessage(raw) ?? RATE_LIMIT_ERROR_USER_MESSAGE;
|
||||
}
|
||||
if (isOverloadedErrorMessage(raw)) {
|
||||
return OVERLOADED_ERROR_USER_MESSAGE;
|
||||
|
||||
Reference in New Issue
Block a user