




















@@ -5,6 +5,7 @@ import {
55extractLeadingHttpStatus,
66formatRawAssistantErrorForUi,
77isGenericProviderInternalError,
8+parseApiErrorInfo,
89} from "../../shared/assistant-error-format.js";
910import {
1011normalizeLowercaseStringOrEmpty,
@@ -675,11 +676,10 @@ function classifyFailoverClassificationFromHttpStatus(
675676return toReasonClassification(classify402Message(message));
676677}
677678if (status === 429) {
678-if (
679-message &&
680-(isProvider(provider, "moonshot") || isProvider(provider, "kimi")) &&
681-isBillingErrorMessage(message)
682-) {
679+if (messageReason === "billing" && !isAmbiguousGeneric429BalanceMessage(message ?? "")) {
680+return toReasonClassification("billing");
681+}
682+if (message && isBilling429MessageForProvider(message, provider)) {
683683return toReasonClassification("billing");
684684}
685685return toReasonClassification("rate_limit");
@@ -797,6 +797,39 @@ function isProvider(provider: string | undefined, match: string): boolean {
797797return Boolean(normalized && normalized.includes(match));
798798}
799799800+function hasProviderBilling429Override(provider: string | undefined): boolean {
801+return (
802+isProvider(provider, "xai") || isProvider(provider, "moonshot") || isProvider(provider, "kimi")
803+);
804+}
805+806+function hasStructuredBilling429Signal(raw: string): boolean {
807+if (hasBillingApiErrorType(raw)) {
808+return true;
809+}
810+const leadingStatus = extractLeadingHttpStatus(raw.trim());
811+return Boolean(leadingStatus?.rest && hasBillingApiErrorType(leadingStatus.rest));
812+}
813+814+function hasBillingApiErrorType(raw: string): boolean {
815+const type = normalizeOptionalLowercaseString(parseApiErrorInfo(raw)?.type);
816+if (!type) {
817+return false;
818+}
819+return isBillingErrorMessage(type) || isBillingErrorMessage(type.replaceAll("_", " "));
820+}
821+822+function isAmbiguousGeneric429BalanceMessage(raw: string): boolean {
823+return /\binsufficient\s+account\s+balance\b/i.test(raw) && !hasStructuredBilling429Signal(raw);
824+}
825+826+function isBilling429MessageForProvider(raw: string, provider: string | undefined): boolean {
827+if (!isBillingErrorMessage(raw)) {
828+return false;
829+}
830+return hasProviderBilling429Override(provider) || !isAmbiguousGeneric429BalanceMessage(raw);
831+}
832+800833// pi-ai providers throw `Error("An unknown error occurred")` provider-agnostically
801834// (anthropic, google, vertex, openai-completions, mistral, bedrock, etc.) when a
802835// stream ends with stopReason === "aborted" | "error" without specific info. Treat
@@ -861,6 +894,10 @@ function classifyFailoverClassificationFromMessage(
861894) {
862895return toReasonClassification("billing");
863896}
897+const leadingStatus = extractLeadingHttpStatus(raw.trim());
898+if (leadingStatus?.code !== 429 && isBillingErrorMessage(raw)) {
899+return toReasonClassification("billing");
900+}
864901if (isPeriodicUsageLimitErrorMessage(raw)) {
865902return toReasonClassification(isBillingErrorMessage(raw) ? "billing" : "rate_limit");
866903}
@@ -888,12 +925,9 @@ function classifyFailoverClassificationFromMessage(
888925if (isGenericProviderInternalError(raw)) {
889926return toReasonClassification("timeout");
890927}
891-// Billing and auth classifiers run before the broad isJsonApiInternalServerError
892-// check so that provider errors like {"type":"api_error","message":"insufficient
893-// balance"} are correctly classified as "billing"/"auth" rather than "timeout".
894-if (isBillingErrorMessage(raw)) {
895-return toReasonClassification("billing");
896-}
928+// Auth classifiers run before the broad isJsonApiInternalServerError check so that
929+// provider errors like {"type":"api_error","message":"invalid api key"} are
930+// correctly classified as "auth" rather than "timeout".
897931const oauthRefreshFailure = classifyOAuthRefreshFailure(raw);
898932if (oauthRefreshFailure?.reason) {
899933return toReasonClassification("auth_permanent");
@@ -1181,6 +1215,16 @@ export function formatAssistantErrorText(
11811215return `LLM request rejected: ${invalidRequest[1]}`;
11821216}
118312171218+if (
1219+isOpenRouterKeyLimitExceededError(raw, opts?.provider) ||
1220+isOpenRouterKeyBudgetLimitExceededError(raw, opts?.provider)
1221+) {
1222+return formatBillingErrorMessage(opts?.provider, opts?.model ?? msg.model);
1223+}
1224+if (isBilling429MessageForProvider(raw, opts?.provider)) {
1225+return formatBillingErrorMessage(opts?.provider, opts?.model ?? msg.model);
1226+}
1227+11841228const transientCopy = formatRateLimitOrOverloadedErrorCopy(raw);
11851229if (transientCopy) {
11861230return transientCopy;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。