Skip to content

Commit 019dbcc

Browse files
leno23altaywtf
andauthored
fix(failover): classify Moonshot balance 429 as billing (#83079)
Merged via squash. Prepared head SHA: 9f70bf5 Co-authored-by: leno23 <39647285+leno23@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf
1 parent 72eef85 commit 019dbcc

3 files changed

Lines changed: 60 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Docs: https://docs.openclaw.ai
109109
- Browser plugin: trust managed Chrome CDP diagnostics when launch HTTP probes race cold-start readiness, avoiding false startup failures. Fixes #82904. (#82986) Thanks @kmanan and @hclsys.
110110
- Android: prompt before replacing a changed Gateway TLS thumbprint, showing the old and new SHA-256 fingerprints so users can accept expected certificate rotations instead of hard failing on pin mismatch. (#83077) Thanks @sliekens.
111111
- CLI/status: render extra gateway-like service diagnostics as warning/info output instead of error output. Fixes #46930. (#82922) thanks @giodl73-repo.
112+
- Agents/failover: classify Moonshot/Kimi exhausted-balance HTTP 429 payloads as billing instead of generic rate limits, preserving billing guidance and fallback behavior. Fixes #43447. (#83079) Thanks @leno23.
112113

113114
## 2026.5.17
114115

src/agents/failover-error.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const GEMINI_RESOURCE_EXHAUSTED_MESSAGE =
2121
"RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. check quota).";
2222
// OpenRouter 402 billing example: https://openrouter.ai/docs/api-reference/errors
2323
const OPENROUTER_CREDITS_MESSAGE = "Payment Required: insufficient credits";
24+
// Issue-backed Moonshot/Kimi exhausted-balance shape surfaced under HTTP 429 (#43447).
25+
const MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD =
26+
'{"error":{"type":"rate_limit_reached","message":"Insufficient account balance. Please recharge your Moonshot account."}}';
2427
const OPENROUTER_MODEL_NOT_FOUND_PAYLOAD =
2528
'{"error":{"message":"Healer Alpha was a stealth model revealed on March 18th as an early testing version of MiMo-V2-Omni. Find it here: https://openrouter.ai/xiaomi/mimo-v2-omni","code":404},"user_id":"user_33GTyP8uDSYYbaeBO48AGHXyuMC"}';
2629
const TOGETHER_MONTHLY_SPEND_CAP_MESSAGE =
@@ -293,6 +296,46 @@ describe("failover-error", () => {
293296
).toBe("overloaded");
294297
});
295298

299+
it("lets Moonshot/Kimi billing-shaped 429 payloads win over generic rate limit status", () => {
300+
expect(
301+
resolveFailoverReasonFromError({
302+
provider: "moonshot",
303+
status: 429,
304+
message: MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD,
305+
}),
306+
).toBe("billing");
307+
expect(
308+
resolveFailoverReasonFromError(
309+
{
310+
status: 429,
311+
message: MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD,
312+
},
313+
"kimi-claw",
314+
),
315+
).toBe("billing");
316+
expect(
317+
resolveFailoverReasonFromError({
318+
provider: "moonshot",
319+
status: 429,
320+
message: OPENAI_RATE_LIMIT_MESSAGE,
321+
}),
322+
).toBe("rate_limit");
323+
expect(
324+
resolveFailoverReasonFromError({
325+
provider: "openai",
326+
status: 429,
327+
message: MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD,
328+
}),
329+
).toBe("rate_limit");
330+
expect(
331+
classifyFailoverSignal({
332+
provider: "moonshot",
333+
status: 429,
334+
message: MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD,
335+
}),
336+
).toEqual({ kind: "reason", reason: "billing" });
337+
});
338+
296339
it("classifies OpenRouter no-endpoints 404s as model_not_found", () => {
297340
expect(
298341
resolveFailoverReasonFromError({

src/agents/pi-embedded-helpers/errors.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,13 @@ export function classifyFailoverReasonFromHttpStatus(
612612
? classifyFailoverClassificationFromMessage(message, opts?.provider)
613613
: null;
614614
return failoverReasonFromClassification(
615-
classifyFailoverClassificationFromHttpStatus(status, message, messageClassification, status),
615+
classifyFailoverClassificationFromHttpStatus(
616+
status,
617+
message,
618+
messageClassification,
619+
status,
620+
opts?.provider,
621+
),
616622
);
617623
}
618624

@@ -621,6 +627,7 @@ function classifyFailoverClassificationFromHttpStatus(
621627
message: string | undefined,
622628
messageClassification: FailoverClassification | null,
623629
explicitStatus: number | undefined,
630+
provider?: string,
624631
): FailoverClassification | null {
625632
const messageReason = failoverReasonFromClassification(messageClassification);
626633
if (typeof status !== "number" || !Number.isFinite(status)) {
@@ -644,6 +651,13 @@ function classifyFailoverClassificationFromHttpStatus(
644651
return toReasonClassification(classify402Message(message));
645652
}
646653
if (status === 429) {
654+
if (
655+
message &&
656+
(isProvider(provider, "moonshot") || isProvider(provider, "kimi")) &&
657+
isBillingErrorMessage(message)
658+
) {
659+
return toReasonClassification("billing");
660+
}
647661
return toReasonClassification("rate_limit");
648662
}
649663
if (status === 401 || status === 403) {
@@ -910,6 +924,7 @@ export function classifyFailoverSignal(signal: FailoverSignal): FailoverClassifi
910924
signal.message,
911925
messageClassification,
912926
signal.status,
927+
signal.provider,
913928
);
914929
if (statusClassification) {
915930
return statusClassification;

0 commit comments

Comments
 (0)