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 src .endpoints .consumer import (
1718 InferencePartialPayload ,
1819 KServeData ,
1920 KServeInferenceRequest ,
2021 KServeInferenceResponse ,
2122)
23+ from src .endpoints .consumer .gzip_utils import decompress_if_gzip
2224from src .exceptions import ReconciliationError
2325from src .service .data .datasources .data_source import DataSource
2426
@@ -456,23 +458,24 @@ def process_payload(
456458 return np .array (kserve_data .data ), column_names
457459
458460
459- @router .post ("/" )
460- async def consume_cloud_event (
461+ _kserve_payload_adapter = TypeAdapter (KServeInferenceRequest | KServeInferenceResponse )
462+
463+
464+ async def process_cloud_event (
461465 payload : KServeInferenceRequest | KServeInferenceResponse ,
462- ce_id : Annotated [ str | None , Header ()] = None ,
466+ ce_id : str | None = None ,
463467 tag : str | None = None ,
464468) -> dict [str , str ]:
465- """Consume KServe v2 payloads from cloud events .
469+ """Process a KServe payload from a cloud event or internal call .
466470
467- This endpoint accepts both input (request) and output (response) payloads
468- from ModelMesh-served models and stores them for reconciliation .
471+ This is the core logic shared by the HTTP endpoint and the upload
472+ endpoint's internal forwarding path .
469473
470- :param payload: KServe inference request or response
471- :param ce_id: Cloud event ID from header
474+ :param payload: Parsed KServe inference request or response
475+ :param ce_id: Cloud event ID from header (overrides payload.id)
472476 :param tag: Optional tag to associate with the data
473477 :raises HTTPException: If payload processing fails
474478 """
475- # set payload id from cloud event header if present
476479 if ce_id is not None :
477480 payload .id = ce_id
478481
@@ -482,7 +485,6 @@ async def consume_cloud_event(
482485 detail = "Payload requires 'id' field or 'ce-id' header" ,
483486 )
484487
485- # get global storage interface
486488 storage_interface = get_global_storage_interface ()
487489
488490 try :
@@ -494,13 +496,11 @@ async def consume_cloud_event(
494496 )
495497 raise HTTPException (status_code = HTTPStatus .BAD_REQUEST , detail = msg )
496498 logger .info ("KServe Inference Input %s received." , payload .id )
497- # if a match is found, the payload is auto-deleted from data
498499 partial_output = await storage_interface .get_partial_payload (
499500 payload .id , is_input = False , is_modelmesh = False
500501 )
501502 if partial_output is not None :
502503 if not isinstance (partial_output , KServeInferenceResponse ):
503- # This should never happen - indicates storage interface error
504504 raise HTTPException (
505505 status_code = HTTPStatus .INTERNAL_SERVER_ERROR ,
506506 detail = "Invalid payload type from storage" ,
@@ -532,7 +532,6 @@ async def consume_cloud_event(
532532 )
533533 if partial_input is not None :
534534 if not isinstance (partial_input , KServeInferenceRequest ):
535- # This should never happen - indicates storage interface error
536535 raise HTTPException (
537536 status_code = HTTPStatus .INTERNAL_SERVER_ERROR ,
538537 detail = "Invalid payload type from storage" ,
@@ -548,8 +547,6 @@ async def consume_cloud_event(
548547 "message" : f"Output payload { payload .id } processed successfully" ,
549548 }
550549
551- # Defensive programming: this should never happen due to type annotation
552- # but adding explicit fallback for type safety
553550 raise HTTPException (
554551 status_code = HTTPStatus .BAD_REQUEST ,
555552 detail = "Payload must be either KServeInferenceRequest or KServeInferenceResponse" ,
@@ -558,3 +555,34 @@ async def consume_cloud_event(
558555 except ReconciliationError as e :
559556 logger .exception ("Reconciliation failed for payload %s" , payload .id )
560557 raise HTTPException (status_code = HTTPStatus .BAD_REQUEST , detail = str (e )) from e
558+
559+
560+ @router .post ("/" )
561+ async def consume_cloud_event (
562+ http_request : Request ,
563+ ce_id : Annotated [str | None , Header ()] = None ,
564+ tag : str | None = None ,
565+ ) -> dict [str , str ]:
566+ """Consume KServe v2 payloads from cloud events.
567+
568+ Knative Eventing may strip the Content-Encoding header while leaving the
569+ body gzip-compressed, so this endpoint detects gzip by magic bytes and
570+ decompresses before JSON parsing.
571+
572+ :param http_request: Raw HTTP request (body may be gzip-compressed without header)
573+ :param ce_id: Cloud event ID from header
574+ :param tag: Optional tag to associate with the data
575+ :raises HTTPException: If payload processing fails
576+ """
577+ raw_body = await http_request .body ()
578+ body = decompress_if_gzip (raw_body )
579+
580+ try :
581+ payload = _kserve_payload_adapter .validate_json (body )
582+ except ValidationError as e :
583+ raise HTTPException (
584+ status_code = HTTPStatus .BAD_REQUEST ,
585+ detail = f"Invalid payload: { e } " ,
586+ ) from e
587+
588+ return await process_cloud_event (payload , ce_id = ce_id , tag = tag )
0 commit comments