1"""Request validation for the relay gateway service.
2
3``validate_gateway_request`` is the boundary check run by
4``RelayGatewayService.handle`` before any dependency runs; malformed
5requests short-circuit with ``Err(RelayGatewayError)`` (400) instead of
6allowing malformed data to fail downstream.
7"""
8
9from __future__ import annotations
10
11from collections.abc import Mapping
12
13from lexigram.contracts.ai.relay import RelayGatewayError, RelayGatewayRequest
14from lexigram.contracts.ai.relay.gateway import RelayGatewayErrorCode
15
16__all__ = ["validate_gateway_request"]
17
18
19def validate_gateway_request(
20 request: RelayGatewayRequest,
21) -> RelayGatewayError | None:
22 """Reject malformed gateway requests before the pipeline runs.
23
24 Args:
25 request: The gateway request to validate.
26
27 Returns:
28 ``None`` when the request is well-formed, otherwise the
29 ``INVALID_REQUEST`` gateway error describing the defect.
30 """
31 if not isinstance(request.request_id, str) or not request.request_id:
32 message = "request_id is required"
33 return RelayGatewayError(
34 code=RelayGatewayErrorCode.INVALID_REQUEST,
35 message=message,
36 status_code=400,
37 request_id=getattr(request, "request_id", None) or "",
38 )
39 if not isinstance(request.tenant_id, str) or not request.tenant_id:
40 message = "tenant_id is required"
41 return RelayGatewayError(
42 code=RelayGatewayErrorCode.INVALID_REQUEST,
43 message=message,
44 status_code=400,
45 request_id=request.request_id,
46 )
47 if not isinstance(request.model, str) or not request.model:
48 message = "model is required"
49 return RelayGatewayError(
50 code=RelayGatewayErrorCode.INVALID_REQUEST,
51 message=message,
52 status_code=400,
53 request_id=request.request_id,
54 )
55 if not isinstance(request.source, str) or not request.source:
56 message = "source is required"
57 return RelayGatewayError(
58 code=RelayGatewayErrorCode.INVALID_REQUEST,
59 message=message,
60 status_code=400,
61 request_id=request.request_id,
62 )
63 if not isinstance(request.payload, Mapping) or not request.payload:
64 message = "payload must be a non-empty object"
65 return RelayGatewayError(
66 code=RelayGatewayErrorCode.INVALID_REQUEST,
67 message=message,
68 status_code=400,
69 request_id=request.request_id,
70 )
71 messages = request.payload.get("messages")
72 if messages is not None:
73 if not isinstance(messages, list):
74 message = "payload messages must be a list"
75 return RelayGatewayError(
76 code=RelayGatewayErrorCode.INVALID_REQUEST,
77 message=message,
78 status_code=400,
79 request_id=request.request_id,
80 )
81 if any(not isinstance(entry, Mapping) for entry in messages):
82 message = "payload messages entries must be objects"
83 return RelayGatewayError(
84 code=RelayGatewayErrorCode.INVALID_REQUEST,
85 message=message,
86 status_code=400,
87 request_id=request.request_id,
88 )
89 if not isinstance(request.headers, Mapping):
90 message = "headers must be an object"
91 return RelayGatewayError(
92 code=RelayGatewayErrorCode.INVALID_REQUEST,
93 message=message,
94 status_code=400,
95 request_id=request.request_id,
96 )
97 return None