Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

285 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Classify LLM invoke failures for investigation and CLI error mapping."""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class LLMInvokeFailure:
"""User-facing investigation failure derived from an LLM invoke exception."""
user_message: str
tracker_message: str
remediation_steps: list[str]
root_cause_category: str = "Configuration Error"
def _timeout_remediation() -> list[str]:
return [
(
"CLI providers: raise the per-provider timeout env "
+ "(e.g. GEMINI_CLI_TIMEOUT_SECONDS, CLAUDE_CODE_TIMEOUT_SECONDS, "
+ "ANTIGRAVITY_CLI_TIMEOUT_SECONDS; clamped 30600 where supported)."
),
(
"API providers (Anthropic, OpenAI, etc.): each ReAct turn is limited to "
+ "~90s per HTTP request; retry or switch to a faster model if turns time out."
),
"Investigation runs many LLM and tool steps — total wall time can be several minutes.",
]
def _looks_like_timeout(exc: BaseException) -> bool:
if isinstance(exc, TimeoutError):
return True
try:
from anthropic import APITimeoutError as AnthropicTimeoutError
except ImportError:
pass
else:
if isinstance(exc, AnthropicTimeoutError):
return True
try:
from openai import APITimeoutError as OpenAITimeoutError
except ImportError:
pass
else:
if isinstance(exc, OpenAITimeoutError):
return True
text = str(exc).lower()
if "timed out" in text or "timeout" in text:
return True
cause: BaseException | None = exc
while cause is not None:
if isinstance(cause, TimeoutError):
return True
next_cause = cause.__cause__ or cause.__context__
if next_cause is cause:
break
cause = next_cause if isinstance(next_cause, BaseException) else None
return False
def _is_llm_cli_error(exc: BaseException, class_name: str) -> bool:
"""True when *exc* is ``integrations.llm_cli.errors.<class_name>`` (matched by name)."""
exc_type = type(exc)
return exc_type.__name__ == class_name and exc_type.__module__.endswith(
"integrations.llm_cli.errors"
)
def is_cli_timeout_error(exc: BaseException) -> bool:
"""Return True when *exc* is a CLI subprocess timeout (expected on slow turns)."""
return _is_llm_cli_error(exc, "CLITimeoutError")
# Turn-error kinds staged when the conversational/action LLM was the intended
# route for the user's input but the provider failed before a normal reply.
# The prompt-log recorder uses this set to report a failed LLM turn (model
# "unknown" plus ``ai_error_kind``) instead of a terminal-action turn
# (``no_conversational_agent``). Terminal-path kinds (investigation failure
# categories, background-task "timeout"/"cli_exit_nonzero", slash outcomes)
# must never appear here.
LLM_PROVIDER_FAILURE_KINDS = frozenset(
{
"llm_unavailable", # reasoning client import/creation failed
"llm_timeout", # conversational stream timed out
"assistant_error", # conversational stream failed mid-turn
"action_agent_error", # action-selection LLM failed for conversational input
}
)
_NOT_CONFIGURED_PATTERNS = (
"_api_key", # env-var style, e.g. "requires ANTHROPIC_API_KEY to be set"
"api key is not set",
"missing api key",
"not available for your account",
"marketplace",
"inference profile",
"not configured",
"no llm provider",
"llm client unavailable",
"billing is not enabled",
)
_QUOTA_PATTERNS = ("429", "quota", "rate limit", "too many requests", "credit")
_AUTH_PATTERNS = (
"authentication",
"unauthorized",
"401",
"403",
"forbidden",
"invalid api key",
"incorrect api key",
"invalid api_key",
"incorrect api_key",
"api_key is invalid",
"x-api-key",
)
def classify_provider_error_kind(message: str) -> str:
"""Bucket an LLM provider failure message for analytics filtering.
Returns one of ``not_configured``, ``quota``, ``auth``, or
``provider_error`` so downstream dashboards can filter provider failures
without regexing over response text.
"""
text = message.lower()
if any(pattern in text for pattern in _AUTH_PATTERNS):
return "auth"
if any(pattern in text for pattern in _QUOTA_PATTERNS):
return "quota"
if any(pattern in text for pattern in _NOT_CONFIGURED_PATTERNS) or (
"model" in text and "not found" in text
):
return "not_configured"
return "provider_error"
def classify_llm_invoke_failure(exc: BaseException) -> LLMInvokeFailure | None:
"""Return a structured failure when *exc* is a known operational LLM error.
Returns ``None`` to signal the caller should re-raise. In particular,
:class:`LLMCreditExhaustedError` is intentionally NOT classified — it
represents a non-recoverable billing condition that callers must halt
on, not wrap into a degraded result.
"""
from core.llm.shared.llm_retry import LLMCreditExhaustedError
# Fatal — propagate to the runner / operator. Do NOT wrap into the
# generic "rate-limited" classification (which the text branch below
# would otherwise match against "credit balance too low" / "quota").
if isinstance(exc, LLMCreditExhaustedError):
return None
if _is_llm_cli_error(exc, "CLIAuthenticationRequired"):
provider = getattr(exc, "provider", None) or "unknown"
return LLMInvokeFailure(
user_message=(
f"The {provider} CLI is not authenticated, so the "
"investigation could not call the model."
),
tracker_message="Failed: CLI not authenticated",
remediation_steps=[
step
for step in (
getattr(exc, "auth_hint", None),
getattr(exc, "detail", None),
"Run `opensre doctor` to verify CLI installation and auth.",
)
if step
],
)
if is_cli_timeout_error(exc):
detail = str(exc).strip() or "The CLI subprocess exceeded its time limit."
return LLMInvokeFailure(
user_message=f"Investigation stopped: {detail}",
tracker_message="Failed: LLM timed out",
remediation_steps=_timeout_remediation(),
root_cause_category="Investigation Error",
)
if _is_llm_cli_error(exc, "CLIInterruptedError"):
return LLMInvokeFailure(
user_message="Investigation was interrupted while waiting for the LLM CLI.",
tracker_message="Failed: LLM interrupted",
remediation_steps=["Retry the investigation when ready."],
root_cause_category="Investigation Error",
)
if not isinstance(exc, RuntimeError):
if _looks_like_timeout(exc):
detail = str(exc).strip() or "The LLM request timed out."
return LLMInvokeFailure(
user_message=f"Investigation stopped: {detail}",
tracker_message="Failed: LLM timed out",
remediation_steps=_timeout_remediation(),
root_cause_category="Investigation Error",
)
return None
err_msg = str(exc).lower()
raw = str(exc)
if ("model" in err_msg and "not found" in err_msg) or "404" in err_msg:
if "anthropic" in err_msg and "was not found" in err_msg:
return LLMInvokeFailure(
user_message=raw.strip()
or "Anthropic model was not found. Check your configured model name.",
tracker_message="Failed: Model not found",
remediation_steps=[
(
"Verify your model name in ANTHROPIC_REASONING_MODEL or "
+ "ANTHROPIC_TOOLCALL_MODEL environment variables."
),
"Confirm the model ID is valid for your Anthropic account.",
],
)
return LLMInvokeFailure(
user_message=(
"The configured AI model was not found (404). "
"If using a local LLM, verify the model name in your .env file."
),
tracker_message="Failed: Model not found",
remediation_steps=[
"Check your .env configuration",
"Verify the model name is correct",
"Ensure the model is downloaded locally",
"Confirm your provider supports this model",
],
)
if "does not support tool" in err_msg or "only supports single tool" in err_msg:
return LLMInvokeFailure(
user_message=(
"The configured model does not support tool calling. "
"The investigation agent requires a model with native tool-calling support."
),
tracker_message="Failed: Model does not support tools",
remediation_steps=[
"Switch to a model that supports tool calling (e.g. claude-opus-4-7, gpt-4o)",
"For Ollama: use llama3.1, qwen2.5, or another tool-call-capable model",
"Check your LLM_MODEL or LLM_PROVIDER setting in .env",
],
)
if "rate limit" in err_msg:
return LLMInvokeFailure(
user_message="The LLM provider rate-limited this investigation request.",
tracker_message="Failed: LLM rate limited",
remediation_steps=[
"Wait a few minutes and retry.",
"Reduce parallel load or switch to a higher quota tier if available.",
],
root_cause_category="Investigation Error",
)
if (
"not authenticated" in err_msg
or "authentication" in err_msg
or ("api key" in err_msg and "invalid" in err_msg)
):
return LLMInvokeFailure(
user_message=f"Investigation stopped: LLM authentication failed. {raw}",
tracker_message="Failed: LLM authentication",
remediation_steps=[
"Verify API keys or CLI login for your LLM_PROVIDER.",
"Run `opensre doctor` to check provider configuration.",
],
)
if _looks_like_timeout(exc):
detail = raw.strip() or "The LLM request timed out."
return LLMInvokeFailure(
user_message=f"Investigation stopped: {detail}",
tracker_message="Failed: LLM timed out",
remediation_steps=_timeout_remediation(),
root_cause_category="Investigation Error",
)
return None