Coverage for src/lilbee/providers/backends/liter_llm/errors.py: 100%
15 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Map liter-llm failures to ``ProviderError`` by exception type."""
3from __future__ import annotations
5from lilbee.providers.base import ProviderError, ProviderErrorKind
7_PROVIDER_NAME = "remote"
9# User-facing message per recognised error kind. Each names the problem against
10# {model} and makes clear the cause sits with the user's provider account or
11# network, not with lilbee. UNKNOWN has no entry and falls back to the raw error.
12_KIND_MESSAGES: dict[ProviderErrorKind, str] = {
13 ProviderErrorKind.RATE_LIMIT: (
14 "{model} is rate-limited or out of quota. That's a limit on your provider "
15 "API key, not a lilbee problem. Check your plan and billing with the "
16 "provider, or pick a different model."
17 ),
18 ProviderErrorKind.AUTH: (
19 "{model} rejected your API key. Check that the key is set correctly and has "
20 "access to this model. That's between your key and the provider, not a lilbee problem."
21 ),
22 ProviderErrorKind.NOT_FOUND: (
23 "The provider doesn't offer {model} on your account. "
24 "Pick a different model or check the name."
25 ),
26 ProviderErrorKind.CONTEXT_OVERFLOW: (
27 "This conversation is too long for {model}'s context window. "
28 "Start a new chat or pick a model with a larger context."
29 ),
30 ProviderErrorKind.BAD_REQUEST: (
31 "The provider rejected the request for {model}. Check the model name and your settings."
32 ),
33 ProviderErrorKind.CONNECTION: (
34 "Couldn't reach the provider for {model}, or it timed out. Check your "
35 "connection and base URL, then try again or pick a different model."
36 ),
37 ProviderErrorKind.SERVER: (
38 "The provider for {model} is unavailable right now. That's on the provider's "
39 "side, not a lilbee problem. Try again shortly or pick a different model."
40 ),
41}
43# liter-llm exception class name -> error kind. Every HTTP status maps to one
44# typed class in liter-llm, and the classes are flat (each subclasses
45# ``LiterLlmError`` directly), so this one table covers all providers. Eight classes
46# are not in the table and classify as UNKNOWN (BudgetExceededError, HookRejectedError,
47# IdempotencyConflictError, IdempotencyInFlightError, InternalError,
48# InvalidHeaderError, OutboundForbiddenError, SerializationError). Names, not
49# classes, so the table stays importable without the native module.
50_ERROR_KINDS: dict[str, ProviderErrorKind] = {
51 "AuthenticationError": ProviderErrorKind.AUTH,
52 "NotFoundError": ProviderErrorKind.NOT_FOUND,
53 "RateLimitedError": ProviderErrorKind.RATE_LIMIT,
54 "ProviderQuotaExceededError": ProviderErrorKind.RATE_LIMIT,
55 "ContextWindowExceededError": ProviderErrorKind.CONTEXT_OVERFLOW,
56 "BadRequestError": ProviderErrorKind.BAD_REQUEST,
57 "ContentPolicyError": ProviderErrorKind.BAD_REQUEST,
58 "EndpointNotSupportedError": ProviderErrorKind.BAD_REQUEST,
59 "LiterLlmTimeoutError": ProviderErrorKind.CONNECTION,
60 "NetworkError": ProviderErrorKind.CONNECTION,
61 "StreamingError": ProviderErrorKind.CONNECTION,
62 "ServiceUnavailableError": ProviderErrorKind.SERVER,
63 "ServerError": ProviderErrorKind.SERVER,
64}
66# Operation labels prefixed onto the fallback message for an unrecognised error.
67_CHAT_FAILED = "Chat failed"
68_EMBED_FAILED = "Embedding failed"
69_RERANK_FAILED = "Rerank failed"
72def _classify_error(exc: BaseException) -> ProviderErrorKind:
73 """Map a liter-llm exception to a ``ProviderErrorKind`` by type, never by message."""
74 return _ERROR_KINDS.get(type(exc).__name__, ProviderErrorKind.UNKNOWN)
77def _provider_error(fallback: str, exc: Exception, model: str) -> ProviderError:
78 """Wrap a liter-llm failure as a ``ProviderError`` classified by type.
80 Recognised kinds get a blob-free, user-facing message; unrecognised ones
81 keep the raw ``{fallback}: {exc}`` shape so nothing is lost when debugging.
82 """
83 kind = _classify_error(exc)
84 template = _KIND_MESSAGES.get(kind)
85 message = template.format(model=model) if template is not None else f"{fallback}: {exc}"
86 return ProviderError(message, provider=_PROVIDER_NAME, kind=kind)