"""LLM gateway error taxonomy. ``retryable`` means "worth failing over to the backup provider and counting towards the circuit breaker". A malformed request is *not* retryable — sending it to the backup would just burn money twice. """ from __future__ import annotations class LLMError(Exception): """Base class for every gateway-level failure.""" code = "llm_error" class ProviderError(LLMError): """A single provider call failed.""" code = "llm_provider_error" retryable = True def __init__( self, provider: str, message: str, *, status_code: int | None = None, ) -> None: self.provider = provider self.status_code = status_code super().__init__(f"{provider}: {message}") class ProviderTimeoutError(ProviderError): code = "llm_timeout" retryable = True class ProviderRateLimitedError(ProviderError): code = "llm_rate_limited" retryable = True class ProviderServerError(ProviderError): """5xx from the provider.""" code = "llm_server_error" retryable = True class ProviderAuthError(ProviderError): """401/403 — the key for this provider is wrong; the backup may still work.""" code = "llm_auth_error" retryable = True class ProviderBadRequestError(ProviderError): """4xx that the backup provider would reject too. Do not fail over.""" code = "llm_bad_request" retryable = False class ProviderNotImplementedError(ProviderError, NotImplementedError): """Structural placeholder for a provider slot that has no implementation yet.""" code = "llm_provider_not_implemented" retryable = True class CircuitOpenError(ProviderError): """The breaker for this provider is open; the call was not attempted.""" code = "llm_circuit_open" retryable = True class StructuredOutputError(LLMError): """The model could not produce output matching the requested schema.""" code = "llm_structured_output_failed" def __init__(self, provider: str, schema_name: str, attempts: int, detail: str) -> None: self.provider = provider self.schema_name = schema_name self.attempts = attempts super().__init__( f"{provider}: failed to satisfy schema {schema_name} " f"after {attempts} attempts: {detail}" ) class AllProvidersFailedError(LLMError): """Every provider in the scene's routing chain failed.""" code = "llm_all_providers_failed" def __init__(self, scene: str, failures: list[Exception]) -> None: self.scene = scene self.failures = failures detail = "; ".join(f"{type(e).__name__}: {e}" for e in failures) or "no providers routed" super().__init__(f"scene={scene} all providers failed: {detail}") class CachePrefixViolationError(LLMError): """The cacheable system prefix contains volatile tokens. 架构文档 §7.2 缓存前缀纪律 / TD-04: 系统提示词含时间戳、随机 ID、用户昵称 会让 prompt 缓存永远不命中,输入成本翻数倍。 """ code = "llm_prompt_cache_violation" def __init__(self, prompt_ref: str, violations: list[str]) -> None: self.prompt_ref = prompt_ref self.violations = violations super().__init__(f"{prompt_ref}: volatile tokens in cached prefix: {', '.join(violations)}") class DailyBudgetExceededError(LLMError): """Platform-wide daily AI spend crossed the hard threshold. 架构文档 §13 三道成本闸门. Non-critical scenes are rejected/degraded here rather than merely alerted — an alert does not stop a retry storm. """ code = "llm_daily_budget_exceeded" def __init__(self, spent_cents: str, budget_cents: str, scene: str) -> None: self.spent_cents = spent_cents self.budget_cents = budget_cents self.scene = scene super().__init__( f"daily AI budget exhausted (spent={spent_cents} budget={budget_cents}); " f"non-critical scene '{scene}' rejected" ) class PromptNotFoundError(LLMError): code = "llm_prompt_not_found" class SceneNotRoutedError(LLMError): code = "llm_scene_not_routed"