mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 15:51:02 +00:00
fix(failover): classify Moonshot balance 429 as billing (#83079)
Merged via squash.
Prepared head SHA: 9f70bf5935
Co-authored-by: leno23 <39647285+leno23@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
@@ -109,6 +109,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- CLI/status: render extra gateway-like service diagnostics as warning/info output instead of error output. Fixes #46930. (#82922) thanks @giodl73-repo.
|
||||
- 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.
|
||||
|
||||
## 2026.5.17
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ const GEMINI_RESOURCE_EXHAUSTED_MESSAGE =
|
||||
"RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. check quota).";
|
||||
// OpenRouter 402 billing example: https://openrouter.ai/docs/api-reference/errors
|
||||
const OPENROUTER_CREDITS_MESSAGE = "Payment Required: insufficient credits";
|
||||
// Issue-backed Moonshot/Kimi exhausted-balance shape surfaced under HTTP 429 (#43447).
|
||||
const MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD =
|
||||
'{"error":{"type":"rate_limit_reached","message":"Insufficient account balance. Please recharge your Moonshot account."}}';
|
||||
const OPENROUTER_MODEL_NOT_FOUND_PAYLOAD =
|
||||
'{"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"}';
|
||||
const TOGETHER_MONTHLY_SPEND_CAP_MESSAGE =
|
||||
@@ -293,6 +296,46 @@ describe("failover-error", () => {
|
||||
).toBe("overloaded");
|
||||
});
|
||||
|
||||
it("lets Moonshot/Kimi billing-shaped 429 payloads win over generic rate limit status", () => {
|
||||
expect(
|
||||
resolveFailoverReasonFromError({
|
||||
provider: "moonshot",
|
||||
status: 429,
|
||||
message: MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD,
|
||||
}),
|
||||
).toBe("billing");
|
||||
expect(
|
||||
resolveFailoverReasonFromError(
|
||||
{
|
||||
status: 429,
|
||||
message: MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD,
|
||||
},
|
||||
"kimi-claw",
|
||||
),
|
||||
).toBe("billing");
|
||||
expect(
|
||||
resolveFailoverReasonFromError({
|
||||
provider: "moonshot",
|
||||
status: 429,
|
||||
message: OPENAI_RATE_LIMIT_MESSAGE,
|
||||
}),
|
||||
).toBe("rate_limit");
|
||||
expect(
|
||||
resolveFailoverReasonFromError({
|
||||
provider: "openai",
|
||||
status: 429,
|
||||
message: MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD,
|
||||
}),
|
||||
).toBe("rate_limit");
|
||||
expect(
|
||||
classifyFailoverSignal({
|
||||
provider: "moonshot",
|
||||
status: 429,
|
||||
message: MOONSHOT_INSUFFICIENT_BALANCE_429_PAYLOAD,
|
||||
}),
|
||||
).toEqual({ kind: "reason", reason: "billing" });
|
||||
});
|
||||
|
||||
it("classifies OpenRouter no-endpoints 404s as model_not_found", () => {
|
||||
expect(
|
||||
resolveFailoverReasonFromError({
|
||||
|
||||
@@ -612,7 +612,13 @@ export function classifyFailoverReasonFromHttpStatus(
|
||||
? classifyFailoverClassificationFromMessage(message, opts?.provider)
|
||||
: null;
|
||||
return failoverReasonFromClassification(
|
||||
classifyFailoverClassificationFromHttpStatus(status, message, messageClassification, status),
|
||||
classifyFailoverClassificationFromHttpStatus(
|
||||
status,
|
||||
message,
|
||||
messageClassification,
|
||||
status,
|
||||
opts?.provider,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -621,6 +627,7 @@ function classifyFailoverClassificationFromHttpStatus(
|
||||
message: string | undefined,
|
||||
messageClassification: FailoverClassification | null,
|
||||
explicitStatus: number | undefined,
|
||||
provider?: string,
|
||||
): FailoverClassification | null {
|
||||
const messageReason = failoverReasonFromClassification(messageClassification);
|
||||
if (typeof status !== "number" || !Number.isFinite(status)) {
|
||||
@@ -644,6 +651,13 @@ function classifyFailoverClassificationFromHttpStatus(
|
||||
return toReasonClassification(classify402Message(message));
|
||||
}
|
||||
if (status === 429) {
|
||||
if (
|
||||
message &&
|
||||
(isProvider(provider, "moonshot") || isProvider(provider, "kimi")) &&
|
||||
isBillingErrorMessage(message)
|
||||
) {
|
||||
return toReasonClassification("billing");
|
||||
}
|
||||
return toReasonClassification("rate_limit");
|
||||
}
|
||||
if (status === 401 || status === 403) {
|
||||
@@ -910,6 +924,7 @@ export function classifyFailoverSignal(signal: FailoverSignal): FailoverClassifi
|
||||
signal.message,
|
||||
messageClassification,
|
||||
signal.status,
|
||||
signal.provider,
|
||||
);
|
||||
if (statusClassification) {
|
||||
return statusClassification;
|
||||
|
||||
Reference in New Issue
Block a user