Coverage for src / lexigram / ai / relay / gateway / errors.py: 91%

22 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-08 23:08 +0800

1"""Gateway error mapping helpers for the relay gateway service. 

2 

3Converts engine-level ``RelayError`` values into gateway-level 

4``RelayGatewayError`` values and re-attaches request ids to registry and 

5upstream errors that were produced without one. 

6""" 

7 

8from __future__ import annotations 

9 

10from lexigram.contracts.ai.exceptions import RelayError, RelayErrorCode 

11from lexigram.contracts.ai.governance import RelayBillingError 

12from lexigram.contracts.ai.relay.gateway import ( 

13 RelayGatewayError, 

14 RelayGatewayErrorCode, 

15) 

16 

17__all__ = [ 

18 "auth_denied", 

19 "billing_error_to_gateway", 

20 "conversion_error_to_gateway", 

21 "with_request_id", 

22] 

23 

24 

25def conversion_error_to_gateway( 

26 error: RelayError, request_id: str 

27) -> RelayGatewayError: 

28 """Map a converter engine error to a gateway error. 

29 

30 Args: 

31 error: The ``RelayError`` returned by the converter engine. Its 

32 message is a domain message and is safe to propagate. 

33 request_id: Caller-supplied request id stamped on the result. 

34 

35 Returns: 

36 A ``RelayGatewayError`` classified from the engine error code: 

37 malformed/unsupported payloads map to ``INVALID_REQUEST`` (400); 

38 every other engine failure maps to ``CONVERSION_FAILED`` (500). 

39 The error is never retryable. 

40 """ 

41 if error.code in { 

42 RelayErrorCode.MALFORMED_PAYLOAD.value, 

43 RelayErrorCode.UNSUPPORTED_FORMAT.value, 

44 RelayErrorCode.UNSUPPORTED_FEATURE.value, 

45 }: 

46 code = RelayGatewayErrorCode.INVALID_REQUEST 

47 status_code = 400 

48 else: 

49 code = RelayGatewayErrorCode.CONVERSION_FAILED 

50 status_code = 500 

51 return RelayGatewayError( 

52 code=code, 

53 message=error.message, 

54 status_code=status_code, 

55 request_id=request_id, 

56 retryable=False, 

57 ) 

58 

59 

60def auth_denied(request_id: str) -> RelayGatewayError: 

61 """Build the gateway error for an authorization denial. 

62 

63 Args: 

64 request_id: Caller-supplied request id stamped on the result. 

65 

66 Returns: 

67 An ``AUTH_DENIED`` gateway error (403, never retryable). 

68 """ 

69 return RelayGatewayError( 

70 code=RelayGatewayErrorCode.AUTH_DENIED, 

71 message="authorization denied", 

72 status_code=403, 

73 request_id=request_id, 

74 retryable=False, 

75 ) 

76 

77 

78def billing_error_to_gateway( 

79 error: RelayBillingError, request_id: str 

80) -> RelayGatewayError: 

81 """Map a billing admission error to a gateway error. 

82 

83 Args: 

84 error: The ``RelayBillingError`` returned by the billing 

85 pipeline. Its message is redaction-safe and propagated. 

86 request_id: Caller-supplied request id stamped on the result. 

87 

88 Returns: 

89 A ``RelayGatewayError`` classified from the billing error code: 

90 quota denials map to ``QUOTA_EXCEEDED`` (429, retryable); every 

91 other billing failure maps to ``BILLING_FAILED`` (500, never 

92 retryable). 

93 """ 

94 if error.code == "quota_exhausted": 

95 return RelayGatewayError( 

96 code=RelayGatewayErrorCode.QUOTA_EXCEEDED, 

97 message=error.message, 

98 status_code=429, 

99 request_id=request_id, 

100 retryable=True, 

101 ) 

102 return RelayGatewayError( 

103 code=RelayGatewayErrorCode.BILLING_FAILED, 

104 message=error.message, 

105 status_code=500, 

106 request_id=request_id, 

107 retryable=False, 

108 ) 

109 

110 

111def with_request_id(error: RelayGatewayError, request_id: str) -> RelayGatewayError: 

112 """Return *error* with the given request id when it has none. 

113 

114 Errors produced by the channel registry carry an empty request id; 

115 this attaches the caller's id without disturbing errors that already 

116 carry one (the adapter always stamps its own). 

117 

118 Args: 

119 error: The gateway error to normalize. 

120 request_id: Request id to attach when *error* carries none. 

121 

122 Returns: 

123 *error* unchanged when its request id is non-empty, otherwise a 

124 new ``RelayGatewayError`` copying code, message, status code, 

125 and retryability with the given request id. 

126 """ 

127 if error.request_id: 

128 return error 

129 return RelayGatewayError( 

130 code=error.code, 

131 message=error.message, 

132 status_code=error.status_code, 

133 request_id=request_id, 

134 retryable=error.retryable, 

135 )