mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
fix(auto-reply): surface periodic usage-limit failures in group chats
This commit is contained in:
parent
34d257713e
commit
f32c36cdc8
4 changed files with 116 additions and 8 deletions
|
|
@ -168,6 +168,14 @@ describe("sanitizeUserFacingText", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("preserves rate limit reset details that use resets wording", () => {
|
||||
expect(
|
||||
sanitizeUserFacingText("Error: Rate limit reached, resets 6pm (UTC)", {
|
||||
errorContext: true,
|
||||
}),
|
||||
).toBe("⚠️ Rate limit reached, resets 6pm (UTC)");
|
||||
});
|
||||
|
||||
it("returns a model-switch hint for OpenAI model capacity errors", () => {
|
||||
expect(
|
||||
sanitizeUserFacingText(
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const HTTP_ERROR_HINTS = [
|
|||
"permission",
|
||||
];
|
||||
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;
|
||||
/\bmin(ute)?s?\b|\bhours?\b|\bseconds?\b|\btry again in\b|\bresets?\b|\bplan\b|\bquota\b/i;
|
||||
const MODEL_CAPACITY_ERROR_RE = /\b(?:selected\s+)?model\s+(?:is\s+)?at capacity\b/i;
|
||||
const NON_ERROR_PROVIDER_PAYLOAD_MAX_LENGTH = 16_384;
|
||||
const NON_ERROR_PROVIDER_PAYLOAD_PREFIX_RE = /^codex\s*error(?:\s+\d{3})?[:\s-]+/i;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
|||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import {
|
||||
buildEmptyInteractiveReplyPayload,
|
||||
buildKnownAgentRunFailureReplyPayload,
|
||||
buildContextOverflowRecoveryText,
|
||||
computeContextAwareReserveTokensFloor,
|
||||
MAX_LIVE_SWITCH_RETRIES,
|
||||
|
|
@ -7119,6 +7120,58 @@ describe("runAgentTurnWithFallback", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each(NON_DIRECT_FAILURE_SURFACE_CASES)(
|
||||
"surfaces typed periodic rate-limit details in $label chats",
|
||||
async (testCase) => {
|
||||
const periodicLimitMessage = "You've hit your weekly limit · resets 6pm (UTC)";
|
||||
state.runEmbeddedAgentMock.mockRejectedValueOnce(
|
||||
new FailoverError(periodicLimitMessage, {
|
||||
reason: "rate_limit",
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-1",
|
||||
rawError: periodicLimitMessage,
|
||||
}),
|
||||
);
|
||||
|
||||
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
|
||||
const result = await runAgentTurnWithFallback(
|
||||
createMinimalRunAgentTurnParams({
|
||||
sessionCtx: createNonDirectFailureSessionCtx(testCase),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.kind).toBe("final");
|
||||
if (result.kind === "final") {
|
||||
expect(result.payload.isError).toBe(true);
|
||||
expect(result.payload.text).not.toBe(SILENT_REPLY_TOKEN);
|
||||
expect(result.payload.text).toContain("weekly limit");
|
||||
expect(result.payload.text).toContain("resets 6pm");
|
||||
expect(result.payload.text).not.toContain("few minutes");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("surfaces typed periodic rate-limit details through known failure payloads in group chats", () => {
|
||||
const periodicLimitMessage = "You've hit your weekly limit · resets 6pm (UTC)";
|
||||
const payload = buildKnownAgentRunFailureReplyPayload({
|
||||
err: new FailoverError(periodicLimitMessage, {
|
||||
reason: "rate_limit",
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-1",
|
||||
rawError: periodicLimitMessage,
|
||||
}),
|
||||
sessionCtx: createNonDirectFailureSessionCtx(NON_DIRECT_FAILURE_SURFACE_CASES[0]),
|
||||
resolvedVerboseLevel: "off",
|
||||
});
|
||||
|
||||
expect(payload).toBeDefined();
|
||||
expect(payload?.isError).toBe(true);
|
||||
expect(payload?.text).not.toBe(SILENT_REPLY_TOKEN);
|
||||
expect(payload?.text).toContain("weekly limit");
|
||||
expect(payload?.text).toContain("resets 6pm");
|
||||
expect(payload?.text).not.toContain("few minutes");
|
||||
});
|
||||
|
||||
it.each(NON_DIRECT_FAILURE_SURFACE_CASES)(
|
||||
"surfaces overloaded fallback copy in $label chats",
|
||||
async (testCase) => {
|
||||
|
|
@ -7140,6 +7193,33 @@ describe("runAgentTurnWithFallback", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it("surfaces typed overloaded failures without rate-limit cooldown copy", async () => {
|
||||
state.runEmbeddedAgentMock.mockRejectedValueOnce(
|
||||
new FailoverError("529 Please try again", {
|
||||
reason: "overloaded",
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-1",
|
||||
status: 529,
|
||||
}),
|
||||
);
|
||||
|
||||
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
|
||||
const result = await runAgentTurnWithFallback(
|
||||
createMinimalRunAgentTurnParams({
|
||||
sessionCtx: createNonDirectFailureSessionCtx(NON_DIRECT_FAILURE_SURFACE_CASES[0]),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.kind).toBe("final");
|
||||
if (result.kind === "final") {
|
||||
expect(result.payload.isError).toBe(true);
|
||||
expect(result.payload.text).not.toBe(SILENT_REPLY_TOKEN);
|
||||
expect(result.payload.text).toContain("overloaded");
|
||||
expect(result.payload.text).not.toContain("rate-limited");
|
||||
expect(result.payload.text).not.toContain("few minutes");
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces rate-limit fallback copy in Discord group chats when silentReply.group is disallow", async () => {
|
||||
state.runEmbeddedAgentMock.mockRejectedValueOnce(new Error("429 rate limit exceeded"));
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import {
|
|||
isRateLimitErrorMessage,
|
||||
isTransientHttpError,
|
||||
} from "../../agents/embedded-agent-helpers.js";
|
||||
import { isPeriodicUsageLimitErrorMessage } from "../../agents/embedded-agent-helpers/failover-matches.js";
|
||||
import { sanitizeUserFacingText } from "../../agents/embedded-agent-helpers/sanitize-user-facing-text.js";
|
||||
import { isMessagingToolSendAction } from "../../agents/embedded-agent-messaging.js";
|
||||
import { mergeEmbeddedAgentRunResultForModelFallbackExhaustion } from "../../agents/embedded-agent-runner/result-fallback-classifier.js";
|
||||
|
|
@ -685,6 +686,10 @@ function buildRateLimitCooldownMessage(err: unknown): string {
|
|||
return BILLING_ERROR_USER_MESSAGE;
|
||||
}
|
||||
if (!isFallbackSummaryError(err)) {
|
||||
if (isPeriodicUsageLimitErrorMessage(message)) {
|
||||
const providerMessage = sanitizeUserFacingText(message, { errorContext: true });
|
||||
return providerMessage.startsWith("⚠️") ? providerMessage : `⚠️ ${providerMessage}`;
|
||||
}
|
||||
return "⚠️ All models are temporarily rate-limited. Please try again in a few minutes.";
|
||||
}
|
||||
const expiry = err.soonestCooldownExpiry;
|
||||
|
|
@ -1150,13 +1155,22 @@ export function buildKnownAgentRunFailureReplyPayload(params: {
|
|||
const isPureTransientSummary = isFallbackSummary
|
||||
? isPureTransientRateLimitSummary(params.err)
|
||||
: false;
|
||||
const isRateLimit = isFallbackSummary ? isPureTransientSummary : isRateLimitErrorMessage(message);
|
||||
const failoverReason =
|
||||
!isFallbackSummary && isFailoverError(params.err) ? params.err.reason : undefined;
|
||||
const isOverloaded = failoverReason === "overloaded" || isOverloadedErrorMessage(message);
|
||||
const isRateLimit = isFallbackSummary
|
||||
? isPureTransientSummary
|
||||
: failoverReason
|
||||
? failoverReason === "rate_limit" || failoverReason === "overloaded"
|
||||
: isRateLimitErrorMessage(message);
|
||||
const rateLimitOrOverloadedCopy =
|
||||
!isFallbackSummary || isPureTransientSummary
|
||||
? formatRateLimitOrOverloadedErrorCopy(message)
|
||||
? formatRateLimitOrOverloadedErrorCopy(
|
||||
failoverReason === "overloaded" ? "overloaded" : message,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (isRateLimit && !isOverloadedErrorMessage(message)) {
|
||||
if (isRateLimit && !isOverloaded) {
|
||||
return markAgentRunFailureReplyPayload({
|
||||
text: resolveExternalRunFailureTextForConversation({
|
||||
text: buildRateLimitCooldownMessage(params.err),
|
||||
|
|
@ -3444,12 +3458,18 @@ async function runAgentTurnWithFallbackInternal(
|
|||
const isPureTransientSummary = isFallbackSummary
|
||||
? isPureTransientRateLimitSummary(err)
|
||||
: false;
|
||||
const failoverReason = !isFallbackSummary && isFailoverError(err) ? err.reason : undefined;
|
||||
const isOverloaded = failoverReason === "overloaded" || isOverloadedErrorMessage(message);
|
||||
const isRateLimit = isFallbackSummary
|
||||
? isPureTransientSummary
|
||||
: isRateLimitErrorMessage(message);
|
||||
: failoverReason
|
||||
? failoverReason === "rate_limit" || failoverReason === "overloaded"
|
||||
: isRateLimitErrorMessage(message);
|
||||
const rateLimitOrOverloadedCopy =
|
||||
!isFallbackSummary || isPureTransientSummary
|
||||
? formatRateLimitOrOverloadedErrorCopy(message)
|
||||
? formatRateLimitOrOverloadedErrorCopy(
|
||||
failoverReason === "overloaded" ? "overloaded" : message,
|
||||
)
|
||||
: undefined;
|
||||
const safeMessage = isTransientHttp
|
||||
? sanitizeUserFacingText(message, { errorContext: true })
|
||||
|
|
@ -3457,7 +3477,7 @@ async function runAgentTurnWithFallbackInternal(
|
|||
const trimmedMessage = safeMessage.replace(/\.\s*$/, "");
|
||||
const externalRunFailureReply =
|
||||
!isBilling &&
|
||||
!(isRateLimit && !isOverloadedErrorMessage(message)) &&
|
||||
!(isRateLimit && !isOverloaded) &&
|
||||
!rateLimitOrOverloadedCopy &&
|
||||
!isContextOverflow &&
|
||||
!shouldSurfaceToControlUi
|
||||
|
|
@ -3475,7 +3495,7 @@ async function runAgentTurnWithFallbackInternal(
|
|||
: GENERIC_EXTERNAL_RUN_FAILURE_TEXT;
|
||||
const fallbackText = isBilling
|
||||
? resolveBillingFailureReplyText(err)
|
||||
: isRateLimit && !isOverloadedErrorMessage(message)
|
||||
: isRateLimit && !isOverloaded
|
||||
? buildRateLimitCooldownMessage(err)
|
||||
: rateLimitOrOverloadedCopy
|
||||
? rateLimitOrOverloadedCopy
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue