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

1"""Map liter-llm failures to ``ProviderError`` by exception type.""" 

2 

3from __future__ import annotations 

4 

5from lilbee.providers.base import ProviderError, ProviderErrorKind 

6 

7_PROVIDER_NAME = "remote" 

8 

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} 

42 

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} 

65 

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" 

70 

71 

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) 

75 

76 

77def _provider_error(fallback: str, exc: Exception, model: str) -> ProviderError: 

78 """Wrap a liter-llm failure as a ``ProviderError`` classified by type. 

79 

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)