openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
123 lines (122 loc) • 7.63 kB
JavaScript
import { n as isTransientNetworkError } from "./retryable-network-errors-D2gJmBqw.js";
import { i as extractProviderWrappedHttpStatus, n as extractErrorHttpStatus, r as extractLeadingHttpStatus } from "./assistant-error-format-oP4GVe5A.js";
import { n as classifyFailoverSignal } from "./classify-GlkuT1ur.js";
import { t as INCOMPLETE_ASSISTANT_STREAM_RE } from "./message-patterns-Cg93oCGB.js";
import milliseconds from "ms";
import { parseRetryAfterHttpDateMs } from "@openclaw/ai/internal/retry-after";
//#region packages/llm-core/src/types.ts
/** Stable error codes for provider outcomes that cannot be replayed safely. */
const PROVIDER_POST_DISPATCH_AMBIGUITY_ERROR_CODE = "PROVIDER_POST_DISPATCH_AMBIGUITY";
const PROVIDER_FAILURE_WITH_OUTPUT_ERROR_CODE = "PROVIDER_FAILURE_WITH_OUTPUT";
//#endregion
//#region packages/llm-core/src/utils/diagnostics.ts
/** True when the provider explicitly refused the request payload. */
function isProviderRefusalAssistantError(message) {
return Boolean(message?.diagnostics?.some((diagnostic) => diagnostic.type === "provider_refusal"));
}
//#endregion
//#region src/agents/failover/retry-evidence.ts
const RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([
429,
500,
502,
503,
504,
524
]);
const RATE_LIMIT_RETRY_CONTEXT_RE = /rate.?limit|too many requests|resource[_ -]?exhausted|daily (?:request|usage) limit|requests? per day|tokens? per day|quota[_ -]?exceeded/i;
const TRANSIENT_RETRY_EVIDENCE_RE = /overloaded|rate.?limit|too many requests|service.?unavailable|server.?error|internal.?error|provider.?returned.?error|network.?error|connection.?error|connection.?refused|connection.?lost|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|socket connection was closed|timed? out|timeout|terminated|websocket.?closed|websocket.?error|ended without|http2 request did not get a response|retry delay|you can retry your request|try your request again|please retry your request|resource[_ -]?exhausted/i;
const LONG_WINDOW_RATE_LIMIT_RE = /\b(?:daily|weekly|monthly|tokens per day|requests per day|usage limit|subscription|insufficient[_ -]?quota|current quota|quota[_ -]?exceeded|(?:go|free)usagelimiterror|available balance|out of budget)\b/i;
const SHORT_RATE_LIMIT_UNIT_RE = /\b(?:requests per minute|tokens per minute|per-minute|rpm|tpm)\b/i;
const SHORT_WINDOW_RATE_LIMIT_RE = /\b(?:requests per minute|tokens per minute|per-minute|rpm|tpm|model_cooldown)\b|请求过于频繁|调用频率|频率限制/i;
const RETRY_AFTER_VALUE_RE = /\bretry[- ]after\b\s*:?\s*(?:in\s*)?([^\r\n;]+)/i;
const RETRY_AFTER_NUMBER_RE = /^(\d+(?:\.\d+)?)\s*([a-z]+)?\b/i;
const MAX_SHORT_WINDOW_RETRY_AFTER_SECONDS = 60;
/** Extract guarded HTTP status evidence for retry and diagnostic consumers. */
function extractFailoverHttpStatus(message, options) {
if (!message) return;
return (options?.includeLabeledStatus ? extractErrorHttpStatus(message) : extractLeadingHttpStatus(message.trim()) ?? extractProviderWrappedHttpStatus(message.trim()))?.code;
}
function resolveRetrySignalStatus(signal) {
return signal.status ?? extractFailoverHttpStatus(signal.message);
}
/** Narrow evidence that replaying the same assistant request may succeed within this session. */
function hasTransientRetryEvidence(signal) {
const status = resolveRetrySignalStatus(signal);
return status !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(status) || INCOMPLETE_ASSISTANT_STREAM_RE.test(signal.message ?? "") || TRANSIENT_RETRY_EVIDENCE_RE.test(signal.message ?? "") || isTransientNetworkError({ code: signal.code });
}
function hasRateLimitRetryContext(signal) {
return resolveRetrySignalStatus(signal) === 429 || RATE_LIMIT_RETRY_CONTEXT_RE.test(signal.message ?? "");
}
function parseRetryAfterSeconds(valueText, nowMs) {
const secondsMatch = RETRY_AFTER_NUMBER_RE.exec(valueText);
if (secondsMatch?.[1]) {
const value = Number(secondsMatch[1]);
if (!Number.isFinite(value) || value < 0) return;
const unit = secondsMatch[2]?.toLowerCase();
if (unit && !/^(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)$/.test(unit)) return;
const unitMilliseconds = milliseconds(`1${unit ?? "s"}`);
return unitMilliseconds === 1 ? value / 1e3 : value * (unitMilliseconds / 1e3);
}
const retryAtMs = parseRetryAfterHttpDateMs(valueText, nowMs);
return retryAtMs === void 0 ? void 0 : Math.max(0, (retryAtMs - nowMs) / 1e3);
}
/** Extracts a bounded retry hint from provider error text. */
function resolveRetryAfterMs(message, nowMs = Date.now()) {
const value = message?.trim() ? RETRY_AFTER_VALUE_RE.exec(message)?.[1]?.trim() : void 0;
const seconds = value ? parseRetryAfterSeconds(value, nowMs) : void 0;
return seconds === void 0 ? void 0 : Math.ceil(seconds * 1e3);
}
/** Classify provider rate-limit text without deciding a caller's retry policy. */
function classifyRateLimitWindow(message, nowMs = Date.now()) {
const raw = message?.trim();
if (!raw) return { kind: "unknown" };
const hasShortRateLimitUnit = SHORT_RATE_LIMIT_UNIT_RE.test(raw);
const retryAfterValue = RETRY_AFTER_VALUE_RE.exec(raw)?.[1]?.trim();
const retryAfterSeconds = retryAfterValue ? parseRetryAfterSeconds(retryAfterValue, nowMs) : void 0;
if (retryAfterSeconds !== void 0) return retryAfterSeconds > MAX_SHORT_WINDOW_RETRY_AFTER_SECONDS ? { kind: "long" } : {
kind: "short",
retryAfterSeconds
};
if (retryAfterValue && !hasShortRateLimitUnit) return { kind: "long" };
if (LONG_WINDOW_RATE_LIMIT_RE.test(raw) && !hasShortRateLimitUnit) return { kind: "long" };
if (SHORT_WINDOW_RATE_LIMIT_RE.test(raw) || extractLeadingHttpStatus(raw)?.code === 429) return { kind: "short" };
return { kind: "unknown" };
}
/** Apply the intra-attempt replay policy to one already-classified failover signal. */
function shouldRetryFailoverSignal(params) {
if (!params.hasTransientEvidence) return false;
const reason = params.classification?.kind === "reason" ? params.classification.reason : void 0;
if (classifyRateLimitWindow(params.signal.message).kind === "long" && (reason === "billing" || reason === "rate_limit" || hasRateLimitRetryContext(params.signal))) return false;
return true;
}
//#endregion
//#region src/llm/utils/retry.ts
const REPLAY_UNSAFE_ASSISTANT_ERROR_CODES = /* @__PURE__ */ new Set([PROVIDER_FAILURE_WITH_OUTPUT_ERROR_CODE, PROVIDER_POST_DISPATCH_AMBIGUITY_ERROR_CODE]);
/**
* Preserve structured terminal outcomes before text classification.
* Replay could duplicate unknown output or override the provider's refusal.
*/
function isTerminalAssistantError(message) {
return Boolean(message?.errorCode && REPLAY_UNSAFE_ASSISTANT_ERROR_CODES.has(message.errorCode)) || isProviderRefusalAssistantError(message);
}
/** Classify transient provider/transport failures for outer retry policy. */
function isRetryableAssistantError(message) {
if (message.stopReason !== "error" || !message.errorMessage || isTerminalAssistantError(message)) return false;
const errorMessage = message.errorMessage.trim();
const status = extractFailoverHttpStatus(errorMessage);
const signal = {
message: errorMessage,
provider: message.provider,
code: message.errorCode,
errorType: message.errorType,
...status === void 0 ? {} : { status }
};
return shouldRetryFailoverSignal({
classification: classifyFailoverSignal(signal),
hasTransientEvidence: hasTransientRetryEvidence(signal),
signal
});
}
//#endregion
export { resolveRetryAfterMs as a, extractFailoverHttpStatus as i, isTerminalAssistantError as n, isProviderRefusalAssistantError as o, classifyRateLimitWindow as r, isRetryableAssistantError as t };