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 "unexpected_error",
22 "with_request_id",
23]
24
25
26def conversion_error_to_gateway(
27 error: RelayError, request_id: str
28) -> RelayGatewayError:
29 """Map a converter engine error to a gateway error.
30
31 Args:
32 error: The ``RelayError`` returned by the converter engine. Its
33 message is a domain message and is safe to propagate.
34 request_id: Caller-supplied request id stamped on the result.
35
36 Returns:
37 A ``RelayGatewayError`` classified from the engine error code:
38 malformed/unsupported payloads map to ``INVALID_REQUEST`` (400);
39 every other engine failure maps to ``CONVERSION_FAILED`` (500).
40 The error is never retryable.
41 """
42 if error.code in {
43 RelayErrorCode.MALFORMED_PAYLOAD.value,
44 RelayErrorCode.UNSUPPORTED_FORMAT.value,
45 RelayErrorCode.UNSUPPORTED_FEATURE.value,
46 }:
47 code = RelayGatewayErrorCode.INVALID_REQUEST
48 status_code = 400
49 else:
50 code = RelayGatewayErrorCode.CONVERSION_FAILED
51 status_code = 500
52 return RelayGatewayError(
53 code=code,
54 message=error.message,
55 status_code=status_code,
56 request_id=request_id,
57 retryable=False,
58 )
59
60
61def auth_denied(request_id: str) -> RelayGatewayError:
62 """Build the gateway error for an authorization denial.
63
64 Args:
65 request_id: Caller-supplied request id stamped on the result.
66
67 Returns:
68 An ``AUTH_DENIED`` gateway error (403, never retryable).
69 """
70 return RelayGatewayError(
71 code=RelayGatewayErrorCode.AUTH_DENIED,
72 message="authorization denied",
73 status_code=403,
74 request_id=request_id,
75 retryable=False,
76 )
77
78
79def billing_error_to_gateway(
80 error: RelayBillingError, request_id: str
81) -> RelayGatewayError:
82 """Map a billing admission error to a gateway error.
83
84 Args:
85 error: The ``RelayBillingError`` returned by the billing
86 pipeline. Its message is redaction-safe and propagated.
87 request_id: Caller-supplied request id stamped on the result.
88
89 Returns:
90 A ``RelayGatewayError`` classified from the billing error code:
91 quota denials map to ``QUOTA_EXCEEDED`` (429, retryable); every
92 other billing failure maps to ``BILLING_FAILED`` (500, never
93 retryable).
94 """
95 if error.code == "quota_exhausted":
96 return RelayGatewayError(
97 code=RelayGatewayErrorCode.QUOTA_EXCEEDED,
98 message=error.message,
99 status_code=429,
100 request_id=request_id,
101 retryable=True,
102 )
103 return RelayGatewayError(
104 code=RelayGatewayErrorCode.BILLING_FAILED,
105 message=error.message,
106 status_code=500,
107 request_id=request_id,
108 retryable=False,
109 )
110
111
112def with_request_id(error: RelayGatewayError, request_id: str) -> RelayGatewayError:
113 """Return *error* with the given request id when it has none.
114
115 Errors produced by the channel registry carry an empty request id;
116 this attaches the caller's id without disturbing errors that already
117 carry one (the adapter always stamps its own).
118
119 Args:
120 error: The gateway error to normalize.
121 request_id: Request id to attach when *error* carries none.
122
123 Returns:
124 *error* unchanged when its request id is non-empty, otherwise a
125 new ``RelayGatewayError`` copying code, message, status code,
126 and retryability with the given request id.
127 """
128 if error.request_id:
129 return error
130 return RelayGatewayError(
131 code=error.code,
132 message=error.message,
133 status_code=error.status_code,
134 request_id=request_id,
135 retryable=error.retryable,
136 )
137
138
139def unexpected_error(request_id: str) -> RelayGatewayError:
140 """Build the generic error for unexpected dependency failures.
141
142 Args:
143 request_id: The gateway request identifier.
144
145 Returns:
146 A non-retryable ``CONVERSION_FAILED`` (500) gateway error.
147 """
148 return RelayGatewayError(
149 code=RelayGatewayErrorCode.CONVERSION_FAILED,
150 message="Unexpected relay gateway failure",
151 status_code=500,
152 request_id=request_id,
153 retryable=False,
154 )