1010from typing import Annotated , Never
1111
1212import numpy as np
13- from fastapi import APIRouter , Header , HTTPException
13+ from fastapi import APIRouter , Header , HTTPException , Request
1414from numpy import ndarray
15+ from pydantic import TypeAdapter , ValidationError
1516
1617from trustyai_service .endpoints .consumer import (
1718 InferencePartialPayload ,
1819 KServeData ,
1920 KServeInferenceRequest ,
2021 KServeInferenceResponse ,
2122)
23+ from trustyai_service .endpoints .consumer .gzip_utils import decompress_if_gzip
2224from trustyai_service .exceptions import ReconciliationError
2325from trustyai_service .service .data .datasources .data_source import DataSource
2426
@@ -443,23 +445,24 @@ def process_payload(
443445 return np .array (kserve_data .data ), column_names
444446
445447
446- @router .post ("/" )
447- async def consume_cloud_event (
448+ _kserve_payload_adapter = TypeAdapter (KServeInferenceRequest | KServeInferenceResponse )
449+
450+
451+ async def process_cloud_event (
448452 payload : KServeInferenceRequest | KServeInferenceResponse ,
449- ce_id : Annotated [ str | None , Header ()] = None ,
453+ ce_id : str | None = None ,
450454 tag : str | None = None ,
451455) -> dict [str , str ]:
452- """Consume KServe v2 payloads from cloud events .
456+ """Process a KServe payload from a cloud event or internal call .
453457
454- This endpoint accepts both input (request) and output (response) payloads
455- from ModelMesh-served models and stores them for reconciliation .
458+ This is the core logic shared by the HTTP endpoint and the upload
459+ endpoint's internal forwarding path .
456460
457- :param payload: KServe inference request or response
458- :param ce_id: Cloud event ID from header
461+ :param payload: Parsed KServe inference request or response
462+ :param ce_id: Cloud event ID from header (overrides payload.id)
459463 :param tag: Optional tag to associate with the data
460464 :raises HTTPException: If payload processing fails
461465 """
462- # set payload id from cloud event header if present
463466 if ce_id is not None :
464467 payload .id = ce_id
465468
@@ -469,7 +472,6 @@ async def consume_cloud_event(
469472 detail = "Payload requires 'id' field or 'ce-id' header" ,
470473 )
471474
472- # get global storage interface
473475 storage_interface = get_global_storage_interface ()
474476
475477 try :
@@ -481,13 +483,11 @@ async def consume_cloud_event(
481483 )
482484 raise HTTPException (status_code = HTTPStatus .BAD_REQUEST , detail = msg )
483485 logger .info ("KServe Inference Input %s received." , payload .id )
484- # if a match is found, the payload is auto-deleted from data
485486 partial_output = await storage_interface .get_partial_payload (
486487 payload .id , is_input = False , is_modelmesh = False
487488 )
488489 if partial_output is not None :
489490 if not isinstance (partial_output , KServeInferenceResponse ):
490- # This should never happen - indicates storage interface error
491491 raise HTTPException (
492492 status_code = HTTPStatus .INTERNAL_SERVER_ERROR ,
493493 detail = "Invalid payload type from storage" ,
@@ -519,7 +519,6 @@ async def consume_cloud_event(
519519 )
520520 if partial_input is not None :
521521 if not isinstance (partial_input , KServeInferenceRequest ):
522- # This should never happen - indicates storage interface error
523522 raise HTTPException (
524523 status_code = HTTPStatus .INTERNAL_SERVER_ERROR ,
525524 detail = "Invalid payload type from storage" ,
@@ -535,8 +534,6 @@ async def consume_cloud_event(
535534 "message" : f"Output payload { payload .id } processed successfully" ,
536535 }
537536
538- # Defensive programming: this should never happen due to type annotation
539- # but adding explicit fallback for type safety
540537 raise HTTPException (
541538 status_code = HTTPStatus .BAD_REQUEST ,
542539 detail = "Payload must be either KServeInferenceRequest or KServeInferenceResponse" ,
@@ -545,3 +542,34 @@ async def consume_cloud_event(
545542 except ReconciliationError as e :
546543 logger .exception ("Reconciliation failed for payload %s" , payload .id )
547544 raise HTTPException (status_code = HTTPStatus .BAD_REQUEST , detail = str (e )) from e
545+
546+
547+ @router .post ("/" )
548+ async def consume_cloud_event (
549+ http_request : Request ,
550+ ce_id : Annotated [str | None , Header ()] = None ,
551+ tag : str | None = None ,
552+ ) -> dict [str , str ]:
553+ """Consume KServe v2 payloads from cloud events.
554+
555+ Knative Eventing may strip the Content-Encoding header while leaving the
556+ body gzip-compressed, so this endpoint detects gzip by magic bytes and
557+ decompresses before JSON parsing.
558+
559+ :param http_request: Raw HTTP request (body may be gzip-compressed without header)
560+ :param ce_id: Cloud event ID from header
561+ :param tag: Optional tag to associate with the data
562+ :raises HTTPException: If payload processing fails
563+ """
564+ raw_body = await http_request .body ()
565+ body = decompress_if_gzip (raw_body )
566+
567+ try :
568+ payload = _kserve_payload_adapter .validate_json (body )
569+ except ValidationError as e :
570+ raise HTTPException (
571+ status_code = HTTPStatus .BAD_REQUEST ,
572+ detail = f"Invalid payload: { e } " ,
573+ ) from e
574+
575+ return await process_cloud_event (payload , ce_id = ce_id , tag = tag )
0 commit comments