Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/_experimental/mcp_server/faults/types.py: 100%

36 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1"""Fault taxonomy for upstream OAuth token and DCR registration failures. 

2 

3Each fault is a frozen model on a ``tag`` literal. The tag alone decides the HTTP status, the wire 

4error code, and whose prose the caller sees, so those three facts can never disagree the way they can 

5when an upstream's status and error code are relayed independently. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import Final, Literal, TypeAlias 

11 

12from pydantic import BaseModel, ConfigDict 

13 

14MAX_WIRE_FIELD_CHARS: Final = 500 

15"""Bound on every upstream-derived string that crosses to a caller or into a log line.""" 

16 

17CredentialSource: TypeAlias = Literal["gateway_stored", "caller_supplied"] 

18"""Whose client credentials the gateway presented upstream: the MCP server's stored configuration or 

19credentials the caller supplied on the request. Decides whether a credential rejection is the 

20caller's problem to fix or the gateway operator's.""" 

21 

22GATEWAY_CREDENTIAL_CODES: Final[frozenset[str]] = frozenset({"invalid_client", "unauthorized_client"}) 

23"""RFC 6749 error codes that indict the OAuth client's credentials or grant authorization. When the 

24gateway presented its own stored credentials, these are gateway-side faults the caller cannot act on; 

25when the caller supplied the credentials, they are the caller's to fix.""" 

26 

27GATEWAY_CAPABILITY_CODES: Final[frozenset[str]] = frozenset({"invalid_target"}) 

28"""Codes that indict gateway configuration regardless of whose credentials were presented: 

29``invalid_target`` means the upstream did not accept the RFC 8707 resource indicator the server 

30sent, or requires one it was not configured to send (``upstream_resource``). Never the caller's 

31fault.""" 

32 

33UPSTREAM_FAULT_CODES: Final[frozenset[str]] = frozenset({"server_error", "temporarily_unavailable"}) 

34"""Codes by which the upstream blames itself. Relaying them as caller faults would invert blame, so 

35they classify as upstream-reported faults and render on the 5xx their meaning implies.""" 

36 

37 

38class CallerRejected(BaseModel): 

39 """The upstream spoke the OAuth error contract and the failure is actionable by our caller 

40 (e.g. ``invalid_grant``: re-run authorization). The code and its bounded prose relay on the 

41 4xx status the code itself implies.""" 

42 

43 model_config = ConfigDict(frozen=True) 

44 tag: Literal["caller_rejected"] = "caller_rejected" 

45 code: str 

46 description: str | None = None 

47 error_uri: str | None = None 

48 

49 

50class GatewayRejected(BaseModel): 

51 """The upstream rejected the request for a cause only the gateway operator can address: the 

52 server's stored client credentials or a gateway capability gap. Not actionable by the caller: 

53 rendered as 502 with gateway-authored prose naming the code; the upstream's prose goes to 

54 server logs only.""" 

55 

56 model_config = ConfigDict(frozen=True) 

57 tag: Literal["gateway_rejected"] = "gateway_rejected" 

58 code: str 

59 

60 

61class UpstreamReportedFault(BaseModel): 

62 """The upstream blamed itself in the OAuth vocabulary. Rendered on the 5xx the code implies 

63 (``server_error`` 502, ``temporarily_unavailable`` 503) so blame and status agree.""" 

64 

65 model_config = ConfigDict(frozen=True) 

66 tag: Literal["upstream_reported_fault"] = "upstream_reported_fault" 

67 code: Literal["server_error", "temporarily_unavailable"] 

68 

69 

70class UpstreamProtocolFault(BaseModel): 

71 """The upstream broke the error contract: no JSON ``error`` field, an undecodable body, or a 

72 success response without a usable token. Rendered as 502 with a gateway-authored note; the 

73 upstream body never crosses to the caller.""" 

74 

75 model_config = ConfigDict(frozen=True) 

76 tag: Literal["upstream_protocol_fault"] = "upstream_protocol_fault" 

77 note: str 

78 

79 

80class UpstreamRegistrationRefused(BaseModel): 

81 model_config = ConfigDict(frozen=True) 

82 tag: Literal["upstream_registration_refused"] = "upstream_registration_refused" 

83 status_code: Literal[401, 403] 

84 

85 

86UpstreamOAuthFault: TypeAlias = ( 

87 CallerRejected | GatewayRejected | UpstreamReportedFault | UpstreamProtocolFault | UpstreamRegistrationRefused 

88)