198 lines
6.2 KiB
TypeScript
198 lines
6.2 KiB
TypeScript
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
|
|
import type { ClawdbotConfig } from "../../config/config.js";
|
|
import { formatSandboxToolPolicyBlockedMessage } from "../sandbox.js";
|
|
import type { FailoverReason } from "./types.js";
|
|
|
|
export function isContextOverflowError(errorMessage?: string): boolean {
|
|
if (!errorMessage) return false;
|
|
const lower = errorMessage.toLowerCase();
|
|
return (
|
|
lower.includes("request_too_large") ||
|
|
lower.includes("request exceeds the maximum size") ||
|
|
lower.includes("context length exceeded") ||
|
|
lower.includes("maximum context length") ||
|
|
lower.includes("prompt is too long") ||
|
|
lower.includes("context overflow") ||
|
|
(lower.includes("413") && lower.includes("too large"))
|
|
);
|
|
}
|
|
|
|
export function isCompactionFailureError(errorMessage?: string): boolean {
|
|
if (!errorMessage) return false;
|
|
if (!isContextOverflowError(errorMessage)) return false;
|
|
const lower = errorMessage.toLowerCase();
|
|
return (
|
|
lower.includes("summarization failed") ||
|
|
lower.includes("auto-compaction") ||
|
|
lower.includes("compaction failed") ||
|
|
lower.includes("compaction")
|
|
);
|
|
}
|
|
|
|
export function formatAssistantErrorText(
|
|
msg: AssistantMessage,
|
|
opts?: { cfg?: ClawdbotConfig; sessionKey?: string },
|
|
): string | undefined {
|
|
if (msg.stopReason !== "error") return undefined;
|
|
const raw = (msg.errorMessage ?? "").trim();
|
|
if (!raw) return "LLM request failed with an unknown error.";
|
|
|
|
const unknownTool =
|
|
raw.match(/unknown tool[:\s]+["']?([a-z0-9_-]+)["']?/i) ??
|
|
raw.match(/tool\s+["']?([a-z0-9_-]+)["']?\s+(?:not found|is not available)/i);
|
|
if (unknownTool?.[1]) {
|
|
const rewritten = formatSandboxToolPolicyBlockedMessage({
|
|
cfg: opts?.cfg,
|
|
sessionKey: opts?.sessionKey,
|
|
toolName: unknownTool[1],
|
|
});
|
|
if (rewritten) return rewritten;
|
|
}
|
|
|
|
if (isContextOverflowError(raw)) {
|
|
return (
|
|
"Context overflow: prompt too large for the model. " +
|
|
"Try again with less input or a larger-context model."
|
|
);
|
|
}
|
|
|
|
if (/incorrect role information|roles must alternate/i.test(raw)) {
|
|
return (
|
|
"Message ordering conflict - please try again. " +
|
|
"If this persists, use /new to start a fresh session."
|
|
);
|
|
}
|
|
|
|
const invalidRequest = raw.match(/"type":"invalid_request_error".*?"message":"([^"]+)"/);
|
|
if (invalidRequest?.[1]) {
|
|
return `LLM request rejected: ${invalidRequest[1]}`;
|
|
}
|
|
|
|
if (isOverloadedErrorMessage(raw)) {
|
|
return "The AI service is temporarily overloaded. Please try again in a moment.";
|
|
}
|
|
|
|
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
|
|
}
|
|
|
|
export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean {
|
|
if (!msg || msg.stopReason !== "error") return false;
|
|
return isRateLimitErrorMessage(msg.errorMessage ?? "");
|
|
}
|
|
|
|
type ErrorPattern = RegExp | string;
|
|
|
|
const ERROR_PATTERNS = {
|
|
rateLimit: [
|
|
/rate[_ ]limit|too many requests|429/,
|
|
"exceeded your current quota",
|
|
"resource has been exhausted",
|
|
"quota exceeded",
|
|
"resource_exhausted",
|
|
"usage limit",
|
|
],
|
|
overloaded: [/overloaded_error|"type"\s*:\s*"overloaded_error"/i, "overloaded"],
|
|
timeout: ["timeout", "timed out", "deadline exceeded", "context deadline exceeded"],
|
|
billing: [
|
|
/\b402\b/,
|
|
"payment required",
|
|
"insufficient credits",
|
|
"credit balance",
|
|
"plans & billing",
|
|
],
|
|
auth: [
|
|
/invalid[_ ]?api[_ ]?key/,
|
|
"incorrect api key",
|
|
"invalid token",
|
|
"authentication",
|
|
"unauthorized",
|
|
"forbidden",
|
|
"access denied",
|
|
"expired",
|
|
"token has expired",
|
|
/\b401\b/,
|
|
/\b403\b/,
|
|
"no credentials found",
|
|
"no api key found",
|
|
],
|
|
format: [
|
|
"invalid_request_error",
|
|
"string should match pattern",
|
|
"tool_use.id",
|
|
"tool_use_id",
|
|
"messages.1.content.1.tool_use.id",
|
|
"invalid request format",
|
|
],
|
|
} as const;
|
|
|
|
function matchesErrorPatterns(raw: string, patterns: readonly ErrorPattern[]): boolean {
|
|
if (!raw) return false;
|
|
const value = raw.toLowerCase();
|
|
return patterns.some((pattern) =>
|
|
pattern instanceof RegExp ? pattern.test(value) : value.includes(pattern),
|
|
);
|
|
}
|
|
|
|
export function isRateLimitErrorMessage(raw: string): boolean {
|
|
return matchesErrorPatterns(raw, ERROR_PATTERNS.rateLimit);
|
|
}
|
|
|
|
export function isTimeoutErrorMessage(raw: string): boolean {
|
|
return matchesErrorPatterns(raw, ERROR_PATTERNS.timeout);
|
|
}
|
|
|
|
export function isBillingErrorMessage(raw: string): boolean {
|
|
const value = raw.toLowerCase();
|
|
if (!value) return false;
|
|
if (matchesErrorPatterns(value, ERROR_PATTERNS.billing)) return true;
|
|
return (
|
|
value.includes("billing") &&
|
|
(value.includes("upgrade") ||
|
|
value.includes("credits") ||
|
|
value.includes("payment") ||
|
|
value.includes("plan"))
|
|
);
|
|
}
|
|
|
|
export function isBillingAssistantError(msg: AssistantMessage | undefined): boolean {
|
|
if (!msg || msg.stopReason !== "error") return false;
|
|
return isBillingErrorMessage(msg.errorMessage ?? "");
|
|
}
|
|
|
|
export function isAuthErrorMessage(raw: string): boolean {
|
|
return matchesErrorPatterns(raw, ERROR_PATTERNS.auth);
|
|
}
|
|
|
|
export function isOverloadedErrorMessage(raw: string): boolean {
|
|
return matchesErrorPatterns(raw, ERROR_PATTERNS.overloaded);
|
|
}
|
|
|
|
export function isCloudCodeAssistFormatError(raw: string): boolean {
|
|
return matchesErrorPatterns(raw, ERROR_PATTERNS.format);
|
|
}
|
|
|
|
export function isAuthAssistantError(msg: AssistantMessage | undefined): boolean {
|
|
if (!msg || msg.stopReason !== "error") return false;
|
|
return isAuthErrorMessage(msg.errorMessage ?? "");
|
|
}
|
|
|
|
export function classifyFailoverReason(raw: string): FailoverReason | null {
|
|
if (isRateLimitErrorMessage(raw)) return "rate_limit";
|
|
if (isOverloadedErrorMessage(raw)) return "rate_limit";
|
|
if (isCloudCodeAssistFormatError(raw)) return "format";
|
|
if (isBillingErrorMessage(raw)) return "billing";
|
|
if (isTimeoutErrorMessage(raw)) return "timeout";
|
|
if (isAuthErrorMessage(raw)) return "auth";
|
|
return null;
|
|
}
|
|
|
|
export function isFailoverErrorMessage(raw: string): boolean {
|
|
return classifyFailoverReason(raw) !== null;
|
|
}
|
|
|
|
export function isFailoverAssistantError(msg: AssistantMessage | undefined): boolean {
|
|
if (!msg || msg.stopReason !== "error") return false;
|
|
return isFailoverErrorMessage(msg.errorMessage ?? "");
|
|
}
|