UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

292 lines (291 loc) 11.6 kB
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; //#region src/agents/failover/message-patterns.ts const STATED_TOKEN_SIZES_RE = /(?:\btpm\b|tokens per minute)[^.\n]*?\blimit\s+([\d,]+)[^.\n]*?\brequested\s+([\d,]+)/i; function readStatedTokenCount(digits) { const parsed = Number(digits?.replaceAll(",", "")); return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0; } /** * Groq denominates a per-request size ceiling per minute: an oversized single request is refused * with a 413 naming TPM that states both `Limit <n>` and `Requested <m>`. A request larger than * the whole limit does not fit even an empty bucket, so waiting can never admit it. Ordinary * throttling states a requested size within the limit and remains a rate limit. * * The ceiling belongs to the request and to the refusing provider's quota, not to the model's * context window, so compaction budgeted against that window cannot satisfy it either. * Embedded recovery surfaces reset guidance without retrying. If a transport-owning harness * bypasses that recovery, model failover may advance to a differently provisioned candidate. */ function isProviderRequestSizeCeilingError(errorMessage) { if (!errorMessage) return false; const stated = STATED_TOKEN_SIZES_RE.exec(errorMessage); const limit = readStatedTokenCount(stated?.[1]); const requested = readStatedTokenCount(stated?.[2]); return limit !== void 0 && requested !== void 0 && requested > limit; } const INCOMPLETE_ASSISTANT_STREAM_RE = /^[\w -]*stream ended (?:before (?:message_?stop|(?:a )?terminal (?:finish reason|response event|event))|without a terminal finish reason)[.!]?$/i; const PERIODIC_USAGE_LIMIT_RE = /\b(?:daily|weekly|monthly)(?:\/(?:daily|weekly|monthly))* (?:usage )?limit(?:s)?(?: (?:exhausted|reached|exceeded))?\b/i; const HIGH_CONFIDENCE_AUTH_PERMANENT_PATTERNS = [ /api[_ ]?key[_ ]?(?:revoked|deactivated|deleted)/i, /deactivated[_ ]workspace/i, "key has been disabled", "key has been revoked", "account has been deactivated", "not allowed for this organization" ]; const AMBIGUOUS_AUTH_ERROR_PATTERNS = [ /(?:invalid[_ ]?api[_ ]?key(?![a-z0-9])|api[_ ]?key(?:[_ ]?(?:is[_ ]?)?(?:invalid(?![a-z0-9])|not[_ ]?valid(?![a-z0-9]))))/i, /could not (?:authenticate|validate).*(?:api[_ ]?key|credentials)/i, "permission_error" ]; const COMMON_AUTH_ERROR_PATTERNS = [ "incorrect api key", "invalid token", "authentication", "re-authenticate", "oauth token refresh failed", "unauthorized", "forbidden", "access denied", "insufficient permissions", "insufficient permission", /missing scopes?:/i, "expired", "token has expired", /\b401\b/, /\b403\b/, "no credentials found", "no api key found", /\bfailed to (?:extract|parse|validate|decode)\b.*\btoken\b/ ]; const CJK_AUTH_ERROR_PATTERNS = [ "无权访问", "认证失败", "鉴权失败", "密钥无效", "apikey 无效", /(?:当前\s*ak|ce-011).*?(?:违规请求|禁止访问)|(?:违规请求|禁止访问).*?(?:当前\s*ak|ce-011)/i, /\bce-011\b/i ]; const ZAI_BILLING_CODE_1311_RE = /"code"\s*:\s*1311\b/; const ZAI_AUTH_CODE_1113_RE = /"code"\s*:\s*1113\b/; const VOLCENGINE_INVALID_SUBSCRIPTION_RE = /"code"\s*:\s*"InvalidSubscription"/i; const STATUS_INTERNAL_SERVER_ERROR_RE = /\bstatus:\s*internal server error\b/i; const STATUS_INTERNAL_SERVER_ERROR_WITH_500_RE = /^(?=[\s\S]*\bstatus:\s*internal server error\b)(?=[\s\S]*\bcode["']?\s*[:=]\s*500\b)/i; const HTTP_5XX_STATUS_RE = /\bHTTP\s+5\d\d\b/i; const BILLING_ERROR_HARD_402_RE = /["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|^\s*402\s+payment/i; const RATE_LIMIT_429_RE = /^\s*429\b|\b(?:https?|status(?:[ _-]?code)?|response(?:[ _-]?code)?|http(?:[ _-]?status)?)\b[\s:=#"'(]{0,6}429\b|["'](?:status|code)["']\s*:\s*429\b|\b429\b[\s:)\].,-]*(?:rate[_ -]?limit(?:ed|ing)?|too many requests|resource has been exhausted|quota(?:\s+(?:exceeded|exhausted|depleted|reached))?)\b/i; const ZAI_AUTH_ERROR_PATTERNS = [ZAI_AUTH_CODE_1113_RE]; const ERROR_PATTERNS = { rateLimit: [ /rate[_ ]limit|too many requests/i, RATE_LIMIT_429_RE, /too many (?:concurrent )?requests/i, /\bthrottl(?:ing[_]?exception|ing|ed)\b/i, /\bconcurrency limit\b.*\b(?:breached|reached)\b/i, "model_cooldown", "exceeded your current quota", /\bresource[_ -]?exhausted\b/i, /\bquota[_ -]?exceeded\b/i, "usage limit", /\btpm\b/i, "tokens per minute", "tokens per day", "请求过于频繁", "调用频率", "频率限制", "配额不足", "配额已用尽", "额度不足", "额度已用尽" ], overloaded: [ /overloaded_error|"type"\s*:\s*"overloaded_error"/i, "overloaded", /\b(?:selected\s+)?model\s+(?:is\s+)?at capacity\b/i, /\bservice(?:[_ ]temporarily)?[_ ]unavailable\b/i, "high demand", "high load", "服务过载", "当前负载过高", "访问量过大" ], serverError: [ "an error occurred while processing", "internal server error", "internal_error", "server_error", "bad gateway", "gateway timeout", "upstream error", "upstream connect error", "connection reset", "内部错误", "服务器错误", "服务器内部错误", "系统错误", "系统繁忙", "系统异常" ], timeout: [ "timeout", "timed out", "deadline exceeded", "context deadline exceeded", /^(?=[\s\S]*\bgot status:\s*internal\b)(?=[\s\S]*\bcode["']?\s*[:=]\s*500\b)/i, /^(?=[\s\S]*["']status["']\s*:\s*["']internal["'])(?=[\s\S]*["']code["']\s*:\s*500\b)/i, "connection error", "network error", "network request failed", "fetch failed", "socket hang up", /^stream disconnected before completion(?::[\s\S]*)?$/i, /^premature close of server response while trying to fetch\b/i, INCOMPLETE_ASSISTANT_STREAM_RE, "网络错误", "网络异常", "服务暂时不可用", "服务繁忙", "请求超时", "连接超时", "连接错误", /\beconn(?:refused|reset|aborted)\b/i, /\benetunreach\b/i, /\behostunreach\b/i, /\behostdown\b/i, /\benetreset\b/i, /\betimedout\b/i, /\besockettimedout\b/i, /\bepipe\b/i, /\benotfound\b/i, /\beai_again\b/i, /without sending (?:any )?chunks?/i, /\bstop reason:\s*(?:abort|malformed_response|network_error)\b/i, /\breason:\s*(?:abort|malformed_response|network_error)\b/i, /\bunhandled stop reason:\s*(?:abort|malformed_response|network_error)\b/i, /\bfinish_reason:\s*(?:abort|malformed_response|network_error)\b/i, /\boperation was aborted\b/i, /\bstream (?:was )?(?:closed|aborted)\b/i, /^terminated$/i, /^stream_read_error$/i, /\bund_err_(?:socket|connect|headers?|body|req_content_length_mismatch|aborted|closed)\b/i, /^request failed$/i, /\brequest failed after repeated internal retries\b/i, /^llm request failed\.$/i ], billing: [ BILLING_ERROR_HARD_402_RE, /\b(?:got|returned|received)\s+(?:a\s+)?402\b(?!\s+records\b)/i, "payment required", "insufficient credits", /used\s+all\s+available\s+credits/i, /(?:monthly\s+)?spend(?:ing)?\s+limit/i, /insufficient[_ ]quota/i, /\b(?:go|free)usagelimiterror\b/i, "available balance", "out of budget", "credit balance", "plans & billing", /insufficient[_ ]balance/i, /\binsufficient\s+\w+\s+balance\b/i, "insufficient usd or diem balance", /requires?\s+more\s+credits/i, /out of extra usage/i, /draw from your extra usage/i, /extra usage is required(?: for long context requests)?/i, "余额不足", "账户余额不足", "欠费", "账户已欠费", VOLCENGINE_INVALID_SUBSCRIPTION_RE, /\bdoes not have a valid coding\s*plan subscription\b/i, ZAI_BILLING_CODE_1311_RE, /\bcurrent\s+subscription\s+plan\b.*\b(?:does\s+not|doesn't|not)\b.*\binclude\s+access\b/i, /\bmodel\b.*\bnot\s+available\b.*\bcurrent\s+plan\b/i ], authPermanent: HIGH_CONFIDENCE_AUTH_PERMANENT_PATTERNS, auth: [ ...AMBIGUOUS_AUTH_ERROR_PATTERNS, ...COMMON_AUTH_ERROR_PATTERNS, ...ZAI_AUTH_ERROR_PATTERNS, ...CJK_AUTH_ERROR_PATTERNS ], format: [ "string should match pattern", "tool_use.id", "tool_use_id", "messages.1.content.1.tool_use.id", "invalid request format", /tool call id was.*must be/i, "does not support assistant message prefill", "conversation must end with a user message", /agent harness .* does not support .*provider is not one of/i ] }; const BILLING_ERROR_HEAD_RE = /^(?:error[:\s-]+)?billing(?:\s+error)?(?:[:\s-]+|$)|^(?:error[:\s-]+)?(?:credit balance|insufficient credits?|payment required|http\s*402\b)/i; function matchesErrorPatterns(raw, patterns) { if (!raw) return false; const value = normalizeLowercaseStringOrEmpty(raw); return patterns.some((pattern) => pattern instanceof RegExp ? pattern.test(value) : value.includes(pattern)); } function matchesErrorPatternGroups(raw, groups) { return groups.some((patterns) => matchesErrorPatterns(raw, patterns)); } function matchesFormatErrorPattern(raw) { return matchesErrorPatterns(raw, ERROR_PATTERNS.format); } function isRateLimitErrorMessage(raw) { return matchesErrorPatterns(raw, ERROR_PATTERNS.rateLimit); } function isTimeoutErrorMessage(raw) { return matchesErrorPatterns(raw, ERROR_PATTERNS.timeout); } /** * Provider stream completed with an explicit error finish/stop reason. * These are not request timeouts: the transport finished quickly with a * provider-side error. Keep them failover-eligible as server_error (#109218). */ const PROVIDER_COMPLETED_ERROR_FINISH_REASON_PATTERNS = [ /\bfinish_reason:\s*error\b/i, /\bstop reason:\s*error\b/i, /\bunhandled stop reason:\s*error\b/i, /\breason:\s*error\b/i ]; function isProviderCompletedErrorFinishReasonMessage(raw) { return matchesErrorPatterns(raw, PROVIDER_COMPLETED_ERROR_FINISH_REASON_PATTERNS); } function isPeriodicUsageLimitErrorMessage(raw) { return PERIODIC_USAGE_LIMIT_RE.test(raw); } function isBillingErrorMessage(raw) { const value = normalizeLowercaseStringOrEmpty(raw); if (!value) return false; if ([...raw.matchAll(/(?:^|\n)##\s+\S/g)].length > 1) return false; if (matchesErrorPatterns(value, ERROR_PATTERNS.billing)) return true; if (!BILLING_ERROR_HEAD_RE.test(raw)) return false; return value.includes("upgrade") || value.includes("credits") || value.includes("payment") || value.includes("purchase") || value.includes("subscription") || value.includes("plan"); } function isAuthPermanentErrorMessage(raw) { return matchesErrorPatternGroups(raw, [HIGH_CONFIDENCE_AUTH_PERMANENT_PATTERNS]); } function isAuthErrorMessage(raw) { return matchesErrorPatternGroups(raw, [ AMBIGUOUS_AUTH_ERROR_PATTERNS, COMMON_AUTH_ERROR_PATTERNS, ZAI_AUTH_ERROR_PATTERNS, CJK_AUTH_ERROR_PATTERNS ]); } function isOverloadedErrorMessage(raw) { return matchesErrorPatterns(raw, ERROR_PATTERNS.overloaded); } function isServerErrorMessage(raw) { const value = normalizeLowercaseStringOrEmpty(raw); if (!value) return false; if (STATUS_INTERNAL_SERVER_ERROR_WITH_500_RE.test(value) || HTTP_5XX_STATUS_RE.test(value)) return true; const scrubbed = value.replace(STATUS_INTERNAL_SERVER_ERROR_RE, "").trim(); if (scrubbed === "") return true; return matchesErrorPatterns(scrubbed, ERROR_PATTERNS.serverError); } //#endregion export { isOverloadedErrorMessage as a, isProviderRequestSizeCeilingError as c, isTimeoutErrorMessage as d, matchesFormatErrorPattern as f, isBillingErrorMessage as i, isRateLimitErrorMessage as l, isAuthErrorMessage as n, isPeriodicUsageLimitErrorMessage as o, isAuthPermanentErrorMessage as r, isProviderCompletedErrorFinishReasonMessage as s, INCOMPLETE_ASSISTANT_STREAM_RE as t, isServerErrorMessage as u };