Skip to content

guidellm.backends

Backend infrastructure for GuideLLM language model interactions.

Provides abstract base classes, concrete backend implementations, and response handlers for standardized communication with generative AI model providers. The backend system supports distributed execution across worker processes with pluggable response handlers for different API formats. Key components include the abstract Backend base class, OpenAI-compatible HTTP backend, and response handlers for processing streaming and non-streaming API responses.

AudioRequestHandler

Bases: ChatCompletionsRequestHandler

Request handler for audio transcription and translation endpoints.

Processes responses from audio processing APIs that convert speech to text, handling both transcription and translation services. Manages audio-specific usage metrics including audio tokens and processing duration.

Example: :: handler = AudioResponseHandler() response = handler.compile_non_streaming(request, api_response)

Source code in src/guidellm/backends/openai/request_handlers.py
@OpenAIRequestHandlerFactory.register(
    ["/v1/audio/transcriptions", "/v1/audio/translations"]
)
class AudioRequestHandler(ChatCompletionsRequestHandler):
    """
    Request handler for audio transcription and translation endpoints.

    Processes responses from audio processing APIs that convert speech to text,
    handling both transcription and translation services. Manages audio-specific
    usage metrics including audio tokens and processing duration.

    Example:
    ::
        handler = AudioResponseHandler()
        response = handler.compile_non_streaming(request, api_response)
    """

    def __init__(self):
        """
        Initialize the audio response handler.

        Sets up internal state for accumulating streaming response data including
        audio buffers, text chunks, and usage metrics.
        """
        super().__init__()
        self.streaming_buffer: bytearray = bytearray()

    def format(
        self,
        data: GenerationRequest,
        history: HistoryT[GenerationRequest, GenerationResponse] | None = None,
        **kwargs,
    ) -> GenerationRequestArguments:  # noqa: C901
        """
        Format the audio transcription generation request into the
        appropriate structure.

        :param data: The generation request to format
        :param **kwargs: Additional keyword arguments for request formatting
        :return: The formatted request arguments
        """
        if history:
            raise ValueError("AudioRequestHandler does not support multiturn.")

        arguments = GenerationRequestArguments(files={})
        arguments.body = {}

        # Add model
        if kwargs.get("model") is not None:
            arguments.body["model"] = kwargs["model"]

        # Configure streaming
        if kwargs.get("stream"):
            arguments.stream = True
            arguments.body["stream"] = True
            # NOTE: File upload endpoints use flattened stream options
            arguments.body["stream_include_usage"] = True
            arguments.body["stream_continuous_usage_stats"] = True

        # Apply extra arguments
        if kwargs.get("extras"):
            arguments.model_combine(kwargs["extras"])

        # Build audio input
        audio_columns = data.columns.get("audio_column", [])
        if len(audio_columns) != 1:
            raise ValueError(
                f"GenerativeAudioTranscriptionRequestFormatter expects exactly "
                f"one audio column, but got {len(audio_columns)}."
            )

        arguments.files = {
            "file": (
                audio_columns[0].get("file_name") or "audio_input",
                audio_columns[0].get("audio"),
                audio_columns[0].get("mimetype"),
            )
        }

        return arguments

    def compile_non_streaming(
        self,
        request: GenerationRequest,
        arguments: GenerationRequestArguments,
        response: dict,
    ) -> GenerationResponse:
        """
        Process a complete audio transcription or translation response.

        :param request: Original generation request
        :param arguments: The request arguments that were sent
        :param response: Complete API response containing top-level text and usage
        :return: Standardized GenerationResponse with extracted text and metrics
        """
        text = response.get("text")
        input_metrics, output_metrics = self.extract_metrics(
            response.get("usage"), text
        )

        return GenerationResponse(
            request_id=request.request_id,
            request_args=arguments.model_dump_json(),
            response_id=response.get("id"),
            text=text,
            input_metrics=input_metrics,
            output_metrics=output_metrics,
        )

    def post_validation(self, response: GenerationResponse) -> None:
        """Reject audio responses that omit the transcription text field.

        An empty string is a valid transcription for silent or VAD-filtered audio.

        :param response: The compiled audio response to validate.
        :raises ValueError: If the response has no transcription text field.
        """
        if response.text is None:
            raise ValueError(
                "[UNUSABLE_BACKEND_RESPONSE] backend resolved with empty response "
                "payload"
            )

    def extract_metrics(
        self, usage: dict[str, int | dict[str, int]] | None, text: str | None
    ) -> tuple[UsageMetrics, UsageMetrics]:
        """
        Extract input and output usage metrics from audio API response usage data.

        Handles audio-specific metrics including processing duration and audio tokens
        in addition to standard text token counts.

        :param usage: Usage data dictionary from audio API response
        :param text: Generated text for calculating word and character counts.
            None means text is not applicable (metrics will be None);
            empty string means text was applicable but empty (metrics will be 0).
        :return: Tuple of input_metrics and output_metrics as UsageMetrics objects
        """
        if text is None:
            # text not applicable (e.g. tool-call-only) — exclude from aggregation
            text_words = None
            text_chars = None
        else:
            text_words = len(text.split())
            text_chars = len(text)

        if not usage:
            return UsageMetrics(), UsageMetrics(
                text_words=text_words,
                text_characters=text_chars,
            )

        usage_metrics: dict[str, int] = cast("dict[str, int]", usage)

        return UsageMetrics(
            audio_tokens=(usage_metrics.get("prompt_tokens") or 0),
        ), UsageMetrics(
            text_tokens=(usage_metrics.get("completion_tokens") or 0),
            text_words=text_words,
            text_characters=text_chars,
        )

__init__()

Initialize the audio response handler.

Sets up internal state for accumulating streaming response data including audio buffers, text chunks, and usage metrics.

Source code in src/guidellm/backends/openai/request_handlers.py
def __init__(self):
    """
    Initialize the audio response handler.

    Sets up internal state for accumulating streaming response data including
    audio buffers, text chunks, and usage metrics.
    """
    super().__init__()
    self.streaming_buffer: bytearray = bytearray()

compile_non_streaming(request, arguments, response)

Process a complete audio transcription or translation response.

Parameters:

Name Type Description Default
request GenerationRequest

Original generation request

required
arguments GenerationRequestArguments

The request arguments that were sent

required
response dict

Complete API response containing top-level text and usage

required

Returns:

Type Description
GenerationResponse

Standardized GenerationResponse with extracted text and metrics

Source code in src/guidellm/backends/openai/request_handlers.py
def compile_non_streaming(
    self,
    request: GenerationRequest,
    arguments: GenerationRequestArguments,
    response: dict,
) -> GenerationResponse:
    """
    Process a complete audio transcription or translation response.

    :param request: Original generation request
    :param arguments: The request arguments that were sent
    :param response: Complete API response containing top-level text and usage
    :return: Standardized GenerationResponse with extracted text and metrics
    """
    text = response.get("text")
    input_metrics, output_metrics = self.extract_metrics(
        response.get("usage"), text
    )

    return GenerationResponse(
        request_id=request.request_id,
        request_args=arguments.model_dump_json(),
        response_id=response.get("id"),
        text=text,
        input_metrics=input_metrics,
        output_metrics=output_metrics,
    )

extract_metrics(usage, text)

Extract input and output usage metrics from audio API response usage data.

Handles audio-specific metrics including processing duration and audio tokens in addition to standard text token counts.

Parameters:

Name Type Description Default
usage dict[str, int | dict[str, int]] | None

Usage data dictionary from audio API response

required
text str | None

Generated text for calculating word and character counts. None means text is not applicable (metrics will be None); empty string means text was applicable but empty (metrics will be 0).

required

Returns:

Type Description
tuple[UsageMetrics, UsageMetrics]

Tuple of input_metrics and output_metrics as UsageMetrics objects

Source code in src/guidellm/backends/openai/request_handlers.py
def extract_metrics(
    self, usage: dict[str, int | dict[str, int]] | None, text: str | None
) -> tuple[UsageMetrics, UsageMetrics]:
    """
    Extract input and output usage metrics from audio API response usage data.

    Handles audio-specific metrics including processing duration and audio tokens
    in addition to standard text token counts.

    :param usage: Usage data dictionary from audio API response
    :param text: Generated text for calculating word and character counts.
        None means text is not applicable (metrics will be None);
        empty string means text was applicable but empty (metrics will be 0).
    :return: Tuple of input_metrics and output_metrics as UsageMetrics objects
    """
    if text is None:
        # text not applicable (e.g. tool-call-only) — exclude from aggregation
        text_words = None
        text_chars = None
    else:
        text_words = len(text.split())
        text_chars = len(text)

    if not usage:
        return UsageMetrics(), UsageMetrics(
            text_words=text_words,
            text_characters=text_chars,
        )

    usage_metrics: dict[str, int] = cast("dict[str, int]", usage)

    return UsageMetrics(
        audio_tokens=(usage_metrics.get("prompt_tokens") or 0),
    ), UsageMetrics(
        text_tokens=(usage_metrics.get("completion_tokens") or 0),
        text_words=text_words,
        text_characters=text_chars,
    )

format(data, history=None, **kwargs)

Format the audio transcription generation request into the appropriate structure.

Parameters:

Name Type Description Default
data GenerationRequest

The generation request to format

required
**kwargs

Additional keyword arguments for request formatting

{}

Returns:

Type Description
GenerationRequestArguments

The formatted request arguments

Source code in src/guidellm/backends/openai/request_handlers.py
def format(
    self,
    data: GenerationRequest,
    history: HistoryT[GenerationRequest, GenerationResponse] | None = None,
    **kwargs,
) -> GenerationRequestArguments:  # noqa: C901
    """
    Format the audio transcription generation request into the
    appropriate structure.

    :param data: The generation request to format
    :param **kwargs: Additional keyword arguments for request formatting
    :return: The formatted request arguments
    """
    if history:
        raise ValueError("AudioRequestHandler does not support multiturn.")

    arguments = GenerationRequestArguments(files={})
    arguments.body = {}

    # Add model
    if kwargs.get("model") is not None:
        arguments.body["model"] = kwargs["model"]

    # Configure streaming
    if kwargs.get("stream"):
        arguments.stream = True
        arguments.body["stream"] = True
        # NOTE: File upload endpoints use flattened stream options
        arguments.body["stream_include_usage"] = True
        arguments.body["stream_continuous_usage_stats"] = True

    # Apply extra arguments
    if kwargs.get("extras"):
        arguments.model_combine(kwargs["extras"])

    # Build audio input
    audio_columns = data.columns.get("audio_column", [])
    if len(audio_columns) != 1:
        raise ValueError(
            f"GenerativeAudioTranscriptionRequestFormatter expects exactly "
            f"one audio column, but got {len(audio_columns)}."
        )

    arguments.files = {
        "file": (
            audio_columns[0].get("file_name") or "audio_input",
            audio_columns[0].get("audio"),
            audio_columns[0].get("mimetype"),
        )
    }

    return arguments

post_validation(response)

Reject audio responses that omit the transcription text field.

An empty string is a valid transcription for silent or VAD-filtered audio.

Parameters:

Name Type Description Default
response GenerationResponse

The compiled audio response to validate.

required

Raises:

Type Description
ValueError

If the response has no transcription text field.

Source code in src/guidellm/backends/openai/request_handlers.py
def post_validation(self, response: GenerationResponse) -> None:
    """Reject audio responses that omit the transcription text field.

    An empty string is a valid transcription for silent or VAD-filtered audio.

    :param response: The compiled audio response to validate.
    :raises ValueError: If the response has no transcription text field.
    """
    if response.text is None:
        raise ValueError(
            "[UNUSABLE_BACKEND_RESPONSE] backend resolved with empty response "
            "payload"
        )

Backend

Bases: RegistryMixin['type[Backend]'], BackendInterface[GenerationRequest, GenerationResponse]

Base class for generative AI backends with registry and lifecycle management.

Provides a standard interface for backends that communicate with generative AI models. Combines the registry pattern for automatic discovery with a defined lifecycle for process-based distributed execution. Backend state must be pickleable for distributed execution across process boundaries.

Backend lifecycle phases: 1. Creation and configuration 2. Process startup - Initialize resources in worker process 3. Validation - Verify backend readiness 4. Request resolution - Process generation requests 5. Process shutdown - Clean up resources

Example: :: @Backend.register("my_backend") class MyBackend(Backend): def init(self, args: MyBackendArgs): super().init(args) self.api_key = args.api_key

    async def process_startup(self):
        self.client = MyAPIClient(self.api_key)

args = MyBackendArgs(api_key="secret")
backend = Backend.create(args)
Source code in src/guidellm/backends/backend.py
class Backend(
    RegistryMixin["type[Backend]"],
    BackendInterface[GenerationRequest, GenerationResponse],
):
    """
    Base class for generative AI backends with registry and lifecycle management.

    Provides a standard interface for backends that communicate with generative AI
    models. Combines the registry pattern for automatic discovery with a defined
    lifecycle for process-based distributed execution. Backend state must be
    pickleable for distributed execution across process boundaries.

    Backend lifecycle phases:
    1. Creation and configuration
    2. Process startup - Initialize resources in worker process
    3. Validation - Verify backend readiness
    4. Request resolution - Process generation requests
    5. Process shutdown - Clean up resources

    Example:
    ::
        @Backend.register("my_backend")
        class MyBackend(Backend):
            def __init__(self, args: MyBackendArgs):
                super().__init__(args)
                self.api_key = args.api_key

            async def process_startup(self):
                self.client = MyAPIClient(self.api_key)

        args = MyBackendArgs(api_key="secret")
        backend = Backend.create(args)
    """

    _args: BackendArgs

    @classmethod
    def create(cls, args: BackendArgs) -> Backend:
        """
        Create a backend instance based on the backend type.

        :param type_: The type of backend to create
        :param kwargs: Additional arguments for backend initialization
        :return: An instance of a subclass of Backend
        :raises ValueError: If the backend type is not registered
        """
        kind = args.kind

        backend = cls.get_registered_object(kind)

        if backend is None:
            raise ValueError(
                f"Backend type '{kind}' is not registered. "
                f"Available types: {list(cls.registry.keys()) if cls.registry else []}"
            )

        return backend(args)

    def __init__(self, args: BackendArgs):
        """
        Initialize a backend instance.

        :param args: The backend arguments object
        """
        self.kind = args.kind
        self._args = args

    @property
    def info(self) -> dict[str, Any]:
        """
        Return a snapshot of backend configuration for logging or debugging.
        """
        return self._args.model_dump(mode="json") if self._args else {}

    @property
    def processes_limit(self) -> int | None:
        """
        :return: Maximum number of worker processes supported, None if unlimited
        """
        return None

    @property
    def requests_limit(self) -> int | None:
        """
        :return: Maximum number of concurrent requests supported globally,
            None if unlimited
        """
        return None

    @abstractmethod
    async def default_model(self) -> str:
        """
        :return: The default model name or identifier for generation requests,
        """
        ...

info property

Return a snapshot of backend configuration for logging or debugging.

processes_limit property

Returns:

Type Description
int | None

Maximum number of worker processes supported, None if unlimited

requests_limit property

Returns:

Type Description
int | None

Maximum number of concurrent requests supported globally, None if unlimited

__init__(args)

Initialize a backend instance.

Parameters:

Name Type Description Default
args BackendArgs

The backend arguments object

required
Source code in src/guidellm/backends/backend.py
def __init__(self, args: BackendArgs):
    """
    Initialize a backend instance.

    :param args: The backend arguments object
    """
    self.kind = args.kind
    self._args = args

create(args) classmethod

Create a backend instance based on the backend type.

Parameters:

Name Type Description Default
type_

The type of backend to create

required
kwargs

Additional arguments for backend initialization

required

Returns:

Type Description
Backend

An instance of a subclass of Backend

Raises:

Type Description
ValueError

If the backend type is not registered

Source code in src/guidellm/backends/backend.py
@classmethod
def create(cls, args: BackendArgs) -> Backend:
    """
    Create a backend instance based on the backend type.

    :param type_: The type of backend to create
    :param kwargs: Additional arguments for backend initialization
    :return: An instance of a subclass of Backend
    :raises ValueError: If the backend type is not registered
    """
    kind = args.kind

    backend = cls.get_registered_object(kind)

    if backend is None:
        raise ValueError(
            f"Backend type '{kind}' is not registered. "
            f"Available types: {list(cls.registry.keys()) if cls.registry else []}"
        )

    return backend(args)

default_model() abstractmethod async

Returns:

Type Description
str

The default model name or identifier for generation requests,

Source code in src/guidellm/backends/backend.py
@abstractmethod
async def default_model(self) -> str:
    """
    :return: The default model name or identifier for generation requests,
    """
    ...

BackendArgs

Bases: PydanticClassRegistryMixin['BackendArgs'], ABC

Base class for backend creation arguments.

This class serves as a base for defining argument models used in the creation of backend instances. It inherits from PydanticClassRegistryMixin to enable automatic registration of subclasses, allowing for flexible and extensible backend configurations.

Attributes:

Name Type Description
schema_discriminator str

Field name for polymorphic deserialization

Source code in src/guidellm/backends/backend.py
class BackendArgs(PydanticClassRegistryMixin["BackendArgs"], ABC):
    """
    Base class for backend creation arguments.

    This class serves as a base for defining argument models used in the creation
    of backend instances. It inherits from PydanticClassRegistryMixin to enable
    automatic registration of subclasses, allowing for flexible and extensible
    backend configurations.

    :cvar schema_discriminator: Field name for polymorphic deserialization
    """

    model_config = standard_model_config()

    schema_discriminator: ClassVar[str] = "kind"

    @classmethod
    def __pydantic_schema_base_type__(cls) -> type[BackendArgs]:
        """
        Return base type for polymorphic validation hierarchy.

        :return: Base BackendArgs class for schema validation
        """
        if cls.__name__ == "BackendArgs":
            return cls

        return BackendArgs

    kind: str = Field(
        description="Identify the desired backend implementation.",
        examples=["openai_http"],
    )

__pydantic_schema_base_type__() classmethod

Return base type for polymorphic validation hierarchy.

Returns:

Type Description
type[BackendArgs]

Base BackendArgs class for schema validation

Source code in src/guidellm/backends/backend.py
@classmethod
def __pydantic_schema_base_type__(cls) -> type[BackendArgs]:
    """
    Return base type for polymorphic validation hierarchy.

    :return: Base BackendArgs class for schema validation
    """
    if cls.__name__ == "BackendArgs":
        return cls

    return BackendArgs

ChatCompletionsRequestHandler

Bases: TextCompletionsRequestHandler

Request handler for OpenAI-style chat completion endpoints.

Extends TextCompletionsResponseHandler to handle chat completion requests where generated text is nested within message objects in the choices array. Processes both streaming and non-streaming chat completion responses, including tool call responses where the model outputs tool_calls instead of text content.

Source code in src/guidellm/backends/openai/request_handlers.py
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
@OpenAIRequestHandlerFactory.register("/v1/chat/completions")
class ChatCompletionsRequestHandler(TextCompletionsRequestHandler):
    """
    Request handler for OpenAI-style chat completion endpoints.

    Extends TextCompletionsResponseHandler to handle chat completion requests where
    generated text is nested within message objects in the choices array. Processes
    both streaming and non-streaming chat completion responses, including tool call
    responses where the model outputs ``tool_calls`` instead of text content.
    """

    def __init__(self):
        super().__init__()
        # Full tool call payloads accumulated across streaming deltas,
        # keyed by the delta ``index`` field.  Needed for multi-turn tool
        # calling so the response carries the id/name/arguments of each call.
        self.streaming_tool_calls: dict[int, ToolCall] = {}
        self.streaming_reasoning_texts: list[str] = []
        self._last_iteration_had_content: bool = False

    @property
    def last_iteration_had_content(self) -> bool:
        """
        :return: True if the last chunk carried output (text/tool-call) tokens,
            not solely reasoning tokens.
        """
        return self._last_iteration_had_content

    @staticmethod
    def _ensure_tool_format(tool: dict[str, Any]) -> dict[str, Any]:
        """Normalise a single tool definition to Chat Completions format.

        Chat Completions expects
        ``{"type": "function", "function": {"name": ..., ...}}``.
        If the tool is already in that format it is returned as-is.  If it is in the
        flat Responses API format (top-level ``name``, no ``function`` key) the detail
        fields are wrapped into a nested ``function`` dict.

        :param tool: A single tool definition dict in either format.
        :return: The tool in Chat Completions format.
        """
        if "name" in tool and "function" not in tool:
            fn = {k: tool[k] for k in _FUNCTION_DETAIL_KEYS if k in tool}
            return {"type": tool.get("type", "function"), "function": fn}
        return tool

    def _format_prompts(
        self,
        column_data: list,
        column_type: str,
        content_extras: dict[str, Any] | None = None,
    ) -> list[dict[str, Any]]:
        """
        Helper method to format different types of data columns
        into the appropriate structure for chat messages.
        """
        formatted_data = []
        for item in column_data:
            if column_type == "text_column":
                content = {"type": "text", "text": item}
                if content_extras:
                    content.update(content_extras)
                formatted_data.append(content)
            elif column_type == "image_column":
                formatted_data.append(
                    {
                        "type": "image_url",
                        "image_url": {"url": item.get("image")},
                    }
                )
            elif column_type == "video_column":
                formatted_data.append(
                    {
                        "type": "video_url",
                        "video_url": {"url": item.get("video")},
                    }
                )
            elif column_type == "audio_column":
                formatted_data.append(
                    {
                        "type": "input_audio",
                        "input_audio": {
                            "data": base64.b64encode(item.get("audio", b"")).decode(
                                "utf-8"
                            ),
                            "format": item.get("format"),
                        },
                    }
                )
            else:
                raise ValueError(f"Unsupported column type: {column_type}")

        return formatted_data

    @staticmethod
    def _build_tool_response_messages(
        tool_calls: list[ToolCall],
        tool_response_columns: list[Any],
    ) -> list[dict[str, Any]]:
        """Build synthetic ``role: "tool"`` messages for each tool call.

        Uses per-request tool response content from ``tool_response_columns``
        when available, falling back to
        :attr:`settings.default_synthetic_tool_response`.

        :param tool_calls: The tool call objects from the prior assistant response.
        :param tool_response_columns: Per-tool-call response content from the
            dataset, which may be ``str`` or ``bytes`` (orjson).
        :return: List of tool-role message dicts ready to append to messages.
        """
        messages: list[dict[str, Any]] = []
        for idx, tc in enumerate(tool_calls):
            # The OpenAI spec allows the server to do multiple tool calls.
            # If the quantity of calls exceeds those in the dataset, inject the
            # default synthetic tool response.
            raw_content = (
                tool_response_columns[idx]
                if idx < len(tool_response_columns)
                else settings.default_synthetic_tool_response
            )
            # The project JSON utils can return bytes or string; ensure string.
            content = (
                raw_content.decode("utf-8")
                if isinstance(raw_content, bytes)
                else raw_content
            )
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": content,
                }
            )
        return messages

    @staticmethod
    def _apply_tool_call_overrides(
        body: dict[str, Any],
        data: GenerationRequest,
    ) -> None:
        """Inject tool definitions and constrain the request body for tool calling.

        Handles three concerns:

        1. Deserializes and injects tool definitions from dataset columns.
        2. Sets ``tool_choice`` to ``"required"`` or ``"none"`` depending on
           whether the current turn expects a tool call.
        3. Removes body keys that are incompatible with tool calling
           (``ignore_eos``, ``stop``, and token-limit keys on tool-call turns).

        :param body: The mutable request body dict being built.
        :param data: The current generation request.
        """
        tools_column = data.columns.get("tools_column", [])
        if tools_column:
            tools_value = tools_column[0]
            # JSON-serialized tool definitions (e.g. from synthetic data
            # generators that store tools as strings for HuggingFace Features
            # compatibility). The project JSON utils can return bytes or str.
            if isinstance(tools_value, str | bytes):
                tools_value = json.loads(tools_value)
            if isinstance(tools_value, list):
                body["tools"] = [
                    ChatCompletionsRequestHandler._ensure_tool_format(t)
                    for t in tools_value
                ]
                body.setdefault("tool_choice", "required")

        if "tools" not in body:
            body.pop("tool_choice", None)
            return

        # Standard and injection turns should not produce tool calls even
        # when tool definitions are in the body (e.g. from extras).
        if data.turn_type in ("standard", "tool_response_injection"):
            body["tool_choice"] = "none"

        # Tool calling requires the model to stop naturally after producing
        # valid JSON; ignore_eos would force generation past that point and
        # break the server's constrained decoding grammar.
        # max_completion_tokens would truncate output mid-JSON and corrupt
        # the arguments sent in conversation history on follow-up turns.
        if data.turn_type == "client_tool_call":
            body.pop("ignore_eos", None)
            body.pop("stop", None)

    def _build_history_messages(
        self,
        history: HistoryT[GenerationRequest, GenerationResponse],
        **kwargs,
    ) -> list[dict[str, Any]]:
        """Build the messages array from completed conversation turns.

        :param history: Completed (request, response) pairs.
        :param kwargs: Forwarded config (``multiturn_reasoning``, etc.).
        :return: Flat list of message dicts ready to extend into the body.
        """
        messages: list[dict[str, Any]] = []
        for idx, (req, res) in enumerate(history):
            # Passes in the prior response so that past data can be included,
            # like for tool calling.
            prior_response = history[idx - 1][1] if idx > 0 else None
            messages.extend(
                self._build_turn_messages(req, res, prior_response, **kwargs)
            )
        return messages

    def _build_turn_messages(  # noqa: C901
        self,
        req: GenerationRequest,
        res: GenerationResponse | None,
        prior_response: GenerationResponse | None,
        **kwargs,
    ) -> list[dict[str, Any]]:
        """Build messages for a single history turn.

        Dispatches on ``req.turn_type``:

        * ``"client_tool_call"``: user content + assistant tool_calls (no tool
          response messages — those come from the following injection turn).
        * ``"tool_response_injection"``: tool-role messages built from
          ``prior_response.tool_calls`` IDs + this turn's
          ``tool_response_column``, then the assistant text response.
        * ``"standard"``: user content + assistant text response.

        :param req: The request for this history turn.
        :param res: The response the server gave for this turn.
        :param prior_response: The response from the immediately preceding
            history turn (used by injection turns for ``tool_call_id``s).
        :param kwargs: Forwarded config (``multiturn_reasoning``, etc.).
        :return: List of message dicts for this turn.
        """
        messages: list[dict[str, Any]] = []
        multiturn_reasoning = kwargs.get("multiturn_reasoning", False)

        if req.turn_type == "tool_response_injection":
            # Injection turn: tool-role messages then assistant text.
            if prior_response and prior_response.tool_calls:
                tool_response_columns = req.columns.get("tool_response_column", [])
                messages.extend(
                    self._build_tool_response_messages(
                        prior_response.tool_calls, tool_response_columns
                    )
                )
            if res is not None and res.text is not None:
                wrapped = _wrap_reasoning(res.reasoning_text, multiturn_reasoning)
                content = res.text
                if wrapped:
                    content = wrapped + content
                messages.append({"role": "assistant", "content": content})
        else:
            # Standard or tool_call turn: system + user content.
            prefix = " ".join(req.columns.get("prefix_column", []))
            if prefix:
                messages.append({"role": "system", "content": prefix})

            extras = kwargs.get("extras")
            content_extras = extras.content if extras is not None else None
            prompts = [
                self._format_prompts(
                    req.columns.get(col, []),
                    col,
                    content_extras,
                )
                for col in (
                    "text_column",
                    "image_column",
                    "video_column",
                    "audio_column",
                )
            ]
            user_content = list(roundrobin(*prompts))
            if user_content:
                messages.append({"role": "user", "content": user_content})

            # Assistant response for history replay.
            wrapped = _wrap_reasoning(
                res.reasoning_text if res else None, multiturn_reasoning
            )
            if res is not None:
                if res.tool_calls:
                    # Tool-call turn: assistant with tool_calls only.
                    # Tool response messages come from the injection turn.
                    assistant_content = res.text
                    if wrapped:
                        assistant_content = wrapped + (assistant_content or "")
                    messages.append(
                        {
                            "role": "assistant",
                            "content": assistant_content,
                            "tool_calls": [tc.model_dump() for tc in res.tool_calls],
                        }
                    )
                elif res.text is not None or wrapped:
                    content = res.text or ""
                    if wrapped:
                        content = wrapped + content
                    messages.append({"role": "assistant", "content": content})

        return messages

    def format(  # noqa: C901, PLR0912, PLR0915
        self,
        data: GenerationRequest,
        history: HistoryT[GenerationRequest, GenerationResponse] | None = None,
        **kwargs,
    ) -> GenerationRequestArguments:
        """
        Format the chat completion generation request into the appropriate structure.

        :param data: The generation request to format
        :param history: Prior (request, response) pairs in the conversation
        :param **kwargs: Additional keyword arguments for request formatting
        :return: The formatted request arguments
        """
        arguments = GenerationRequestArguments()
        arguments.body = {}  # The type checker works best with body assigned here

        # Add model
        if kwargs.get("model") is not None:
            arguments.body["model"] = kwargs["model"]

        # Configure streaming
        if kwargs.get("stream"):
            arguments.stream = True
            arguments.body["stream"] = True
            arguments.body["stream_options"] = {
                "include_usage": True,
                "continuous_usage_stats": True,
            }

        # Handle output tokens
        if data.output_metrics.text_tokens:
            arguments.body.update(
                {
                    "max_completion_tokens": data.output_metrics.text_tokens,
                    "stop": None,
                    "ignore_eos": True,
                }
            )
        elif kwargs.get("max_tokens") is not None:
            arguments.body["max_completion_tokens"] = kwargs["max_tokens"]

        # Apply extra arguments
        if kwargs.get("extras"):
            arguments.model_combine(kwargs["extras"])

        # Build messages from history
        arguments.body["messages"] = (
            self._build_history_messages(history, **kwargs) if history else []
        )

        # Build the current turn's messages
        if data.turn_type == "tool_response_injection":
            # Injection turn: send tool results back to the server.
            # tool_call_ids come from the last history entry's response.
            prior_response = history[-1][1] if history else None
            if prior_response and prior_response.tool_calls:
                tool_response_columns = data.columns.get("tool_response_column", [])
                arguments.body["messages"].extend(
                    self._build_tool_response_messages(
                        prior_response.tool_calls, tool_response_columns
                    )
                )
        else:
            # Standard or tool_call turn: system prompt + user content.
            prefix = " ".join(data.columns.get("prefix_column", []))
            if prefix:
                arguments.body["messages"].append({"role": "system", "content": prefix})

            extras = kwargs.get("extras")
            content_extras = extras.content if extras is not None else None
            prompts = [
                self._format_prompts(
                    data.columns.get(col, []),
                    col,
                    content_extras,
                )
                for col in (
                    "text_column",
                    "image_column",
                    "video_column",
                    "audio_column",
                )
            ]
            user_content = list(roundrobin(*prompts))
            if user_content:
                arguments.body["messages"].append(
                    {"role": "user", "content": user_content}
                )

        # Inject tool definitions and apply tool-call-specific overrides.
        self._apply_tool_call_overrides(arguments.body, data)

        return arguments

    def compile_non_streaming(
        self,
        request: GenerationRequest,
        arguments: GenerationRequestArguments,
        response: dict,
    ) -> GenerationResponse:
        """
        Process a complete chat completion response.

        Extracts content from the message object within choices, handling the nested
        structure specific to chat completion endpoints.

        :param request: Original generation request
        :param arguments: The request arguments that were sent
        :param response: Complete API response containing choices and usage data
        :return: Standardized GenerationResponse with extracted content and metrics
        """
        choices, usage = self.extract_choices_and_usage(response)
        choice: dict[str, dict] = choices[0] if choices else {}
        message = choice.get("message", {})
        text = message.get("content")
        reasoning_text = (
            message.get("reasoning") or message.get("reasoning_content") or None
        )
        raw_tool_calls = message.get("tool_calls")
        if text is None and not raw_tool_calls:
            text = ""  # Edge case: null content and no tools
        input_metrics, output_metrics = self.extract_metrics(usage, text)

        tool_calls: list[ToolCall] | None = None
        if raw_tool_calls:
            tool_calls = [ToolCall.model_validate(tc) for tc in raw_tool_calls]
        _apply_tool_call_metrics(
            output_metrics, len(tool_calls) if tool_calls else 0, text
        )

        return GenerationResponse(
            request_id=request.request_id,
            request_args=arguments.model_dump_json(),
            response_id=response.get("id"),  # use vLLM ID if available
            text=text,
            reasoning_text=reasoning_text,
            tool_calls=tool_calls,
            input_metrics=input_metrics,
            output_metrics=output_metrics,
        )

    def add_streaming_line(self, line: str) -> int | None:
        """
        Process a single line from a chat completion streaming response.

        Handles the chat completion specific delta structure where content is nested
        within delta objects in the streaming response chunks. Also accumulates
        ``tool_calls`` deltas when the model streams function call output.

        :param line: Raw SSE line from the streaming response
        :return: 1 if content was extracted, 0 if line ignored, None if done
        """
        if not (data := self.extract_line_data(line)):
            return None if data is None else 0

        if "id" in data and self.streaming_response_id is None:
            self.streaming_response_id = data["id"]

        updated = False
        # Tracks whether this iteration produced user-visible content (not
        # reasoning-only). Used by the HTTP layer to set TTFOT timing.
        had_content = False
        choices, usage = self.extract_choices_and_usage(data)
        choice: dict[str, dict] = choices[0] if choices else {}
        delta = choice.get("delta", {}) if choices else {}

        # Reasoning tokens trigger TTFT (updated=True) but are not
        # considered "content" for the TTFOT metric.
        if reasoning := (delta.get("reasoning") or delta.get("reasoning_content")):
            self.streaming_reasoning_texts.append(reasoning)
            updated = True
        if content := delta.get("content"):
            self.streaming_texts.append(content)
            updated = True
            had_content = True

        # Accumulate streamed tool_calls deltas.  Each tool call may be split
        # across multiple chunks; we reassemble by ``index``.
        # ``tool_calls`` could be either missing or ``null`` in the delta
        # (some OpenAI-compatible servers emit this), handle both cases
        for tc_delta in delta.get("tool_calls") or []:
            self._accumulate_tool_call_delta(tc_delta)
            updated = True
            had_content = True

        if usage:
            self.streaming_usage = usage

        # Only update the flag when we processed a token-bearing iteration;
        # non-updating lines should not reset the flag.
        if updated:
            self._last_iteration_had_content = had_content
        return 1 if updated else 0

    def _accumulate_tool_call_delta(self, tc_delta: dict[str, Any]) -> None:
        """Merge a single streaming tool_call delta into accumulated state.

        Each tool call is split across multiple SSE chunks.  This method
        creates or updates the :class:`ToolCall` entry keyed by the
        delta's ``index`` field.

        :param tc_delta: A single element from the ``tool_calls`` array in a
            streaming chat completion delta.
        """
        idx = tc_delta["index"]

        if idx not in self.streaming_tool_calls:
            self.streaming_tool_calls[idx] = ToolCall(
                id=tc_delta.get("id", ""),
                type=tc_delta.get("type", "function"),
            )

        tc = self.streaming_tool_calls[idx]
        fn_delta = tc_delta.get("function", {})

        if fn_id := tc_delta.get("id"):
            tc.id = fn_id
        if fn_name := fn_delta.get("name"):
            tc.function.name += fn_name
        if fn_args := fn_delta.get("arguments"):
            tc.function.arguments += fn_args

    def compile_streaming(
        self, request: GenerationRequest, arguments: GenerationRequestArguments
    ) -> GenerationResponse:
        """
        Compile accumulated streaming chat completion content into a final response.

        :param request: Original generation request
        :return: Standardized GenerationResponse with concatenated content and metrics
        """
        return _compile_streaming_response(
            request,
            arguments,
            self.streaming_texts,
            self.streaming_tool_calls,
            self.streaming_usage,
            self.streaming_response_id,
            self.extract_metrics,
            streaming_reasoning_texts=self.streaming_reasoning_texts,
        )

last_iteration_had_content property

Returns:

Type Description
bool

True if the last chunk carried output (text/tool-call) tokens, not solely reasoning tokens.

add_streaming_line(line)

Process a single line from a chat completion streaming response.

Handles the chat completion specific delta structure where content is nested within delta objects in the streaming response chunks. Also accumulates tool_calls deltas when the model streams function call output.

Parameters:

Name Type Description Default
line str

Raw SSE line from the streaming response

required

Returns:

Type Description
int | None

1 if content was extracted, 0 if line ignored, None if done

Source code in src/guidellm/backends/openai/request_handlers.py
def add_streaming_line(self, line: str) -> int | None:
    """
    Process a single line from a chat completion streaming response.

    Handles the chat completion specific delta structure where content is nested
    within delta objects in the streaming response chunks. Also accumulates
    ``tool_calls`` deltas when the model streams function call output.

    :param line: Raw SSE line from the streaming response
    :return: 1 if content was extracted, 0 if line ignored, None if done
    """
    if not (data := self.extract_line_data(line)):
        return None if data is None else 0

    if "id" in data and self.streaming_response_id is None:
        self.streaming_response_id = data["id"]

    updated = False
    # Tracks whether this iteration produced user-visible content (not
    # reasoning-only). Used by the HTTP layer to set TTFOT timing.
    had_content = False
    choices, usage = self.extract_choices_and_usage(data)
    choice: dict[str, dict] = choices[0] if choices else {}
    delta = choice.get("delta", {}) if choices else {}

    # Reasoning tokens trigger TTFT (updated=True) but are not
    # considered "content" for the TTFOT metric.
    if reasoning := (delta.get("reasoning") or delta.get("reasoning_content")):
        self.streaming_reasoning_texts.append(reasoning)
        updated = True
    if content := delta.get("content"):
        self.streaming_texts.append(content)
        updated = True
        had_content = True

    # Accumulate streamed tool_calls deltas.  Each tool call may be split
    # across multiple chunks; we reassemble by ``index``.
    # ``tool_calls`` could be either missing or ``null`` in the delta
    # (some OpenAI-compatible servers emit this), handle both cases
    for tc_delta in delta.get("tool_calls") or []:
        self._accumulate_tool_call_delta(tc_delta)
        updated = True
        had_content = True

    if usage:
        self.streaming_usage = usage

    # Only update the flag when we processed a token-bearing iteration;
    # non-updating lines should not reset the flag.
    if updated:
        self._last_iteration_had_content = had_content
    return 1 if updated else 0

compile_non_streaming(request, arguments, response)

Process a complete chat completion response.

Extracts content from the message object within choices, handling the nested structure specific to chat completion endpoints.

Parameters:

Name Type Description Default
request GenerationRequest

Original generation request

required
arguments GenerationRequestArguments

The request arguments that were sent

required
response dict

Complete API response containing choices and usage data

required

Returns:

Type Description
GenerationResponse

Standardized GenerationResponse with extracted content and metrics

Source code in src/guidellm/backends/openai/request_handlers.py
def compile_non_streaming(
    self,
    request: GenerationRequest,
    arguments: GenerationRequestArguments,
    response: dict,
) -> GenerationResponse:
    """
    Process a complete chat completion response.

    Extracts content from the message object within choices, handling the nested
    structure specific to chat completion endpoints.

    :param request: Original generation request
    :param arguments: The request arguments that were sent
    :param response: Complete API response containing choices and usage data
    :return: Standardized GenerationResponse with extracted content and metrics
    """
    choices, usage = self.extract_choices_and_usage(response)
    choice: dict[str, dict] = choices[0] if choices else {}
    message = choice.get("message", {})
    text = message.get("content")
    reasoning_text = (
        message.get("reasoning") or message.get("reasoning_content") or None
    )
    raw_tool_calls = message.get("tool_calls")
    if text is None and not raw_tool_calls:
        text = ""  # Edge case: null content and no tools
    input_metrics, output_metrics = self.extract_metrics(usage, text)

    tool_calls: list[ToolCall] | None = None
    if raw_tool_calls:
        tool_calls = [ToolCall.model_validate(tc) for tc in raw_tool_calls]
    _apply_tool_call_metrics(
        output_metrics, len(tool_calls) if tool_calls else 0, text
    )

    return GenerationResponse(
        request_id=request.request_id,
        request_args=arguments.model_dump_json(),
        response_id=response.get("id"),  # use vLLM ID if available
        text=text,
        reasoning_text=reasoning_text,
        tool_calls=tool_calls,
        input_metrics=input_metrics,
        output_metrics=output_metrics,
    )

compile_streaming(request, arguments)

Compile accumulated streaming chat completion content into a final response.

Parameters:

Name Type Description Default
request GenerationRequest

Original generation request

required

Returns:

Type Description
GenerationResponse

Standardized GenerationResponse with concatenated content and metrics

Source code in src/guidellm/backends/openai/request_handlers.py
def compile_streaming(
    self, request: GenerationRequest, arguments: GenerationRequestArguments
) -> GenerationResponse:
    """
    Compile accumulated streaming chat completion content into a final response.

    :param request: Original generation request
    :return: Standardized GenerationResponse with concatenated content and metrics
    """
    return _compile_streaming_response(
        request,
        arguments,
        self.streaming_texts,
        self.streaming_tool_calls,
        self.streaming_usage,
        self.streaming_response_id,
        self.extract_metrics,
        streaming_reasoning_texts=self.streaming_reasoning_texts,
    )

format(data, history=None, **kwargs)

Format the chat completion generation request into the appropriate structure.

Parameters:

Name Type Description Default
data GenerationRequest

The generation request to format

required
history HistoryT[GenerationRequest, GenerationResponse] | None

Prior (request, response) pairs in the conversation

None
**kwargs

Additional keyword arguments for request formatting

{}

Returns:

Type Description
GenerationRequestArguments

The formatted request arguments

Source code in src/guidellm/backends/openai/request_handlers.py
def format(  # noqa: C901, PLR0912, PLR0915
    self,
    data: GenerationRequest,
    history: HistoryT[GenerationRequest, GenerationResponse] | None = None,
    **kwargs,
) -> GenerationRequestArguments:
    """
    Format the chat completion generation request into the appropriate structure.

    :param data: The generation request to format
    :param history: Prior (request, response) pairs in the conversation
    :param **kwargs: Additional keyword arguments for request formatting
    :return: The formatted request arguments
    """
    arguments = GenerationRequestArguments()
    arguments.body = {}  # The type checker works best with body assigned here

    # Add model
    if kwargs.get("model") is not None:
        arguments.body["model"] = kwargs["model"]

    # Configure streaming
    if kwargs.get("stream"):
        arguments.stream = True
        arguments.body["stream"] = True
        arguments.body["stream_options"] = {
            "include_usage": True,
            "continuous_usage_stats": True,
        }

    # Handle output tokens
    if data.output_metrics.text_tokens:
        arguments.body.update(
            {
                "max_completion_tokens": data.output_metrics.text_tokens,
                "stop": None,
                "ignore_eos": True,
            }
        )
    elif kwargs.get("max_tokens") is not None:
        arguments.body["max_completion_tokens"] = kwargs["max_tokens"]

    # Apply extra arguments
    if kwargs.get("extras"):
        arguments.model_combine(kwargs["extras"])

    # Build messages from history
    arguments.body["messages"] = (
        self._build_history_messages(history, **kwargs) if history else []
    )

    # Build the current turn's messages
    if data.turn_type == "tool_response_injection":
        # Injection turn: send tool results back to the server.
        # tool_call_ids come from the last history entry's response.
        prior_response = history[-1][1] if history else None
        if prior_response and prior_response.tool_calls:
            tool_response_columns = data.columns.get("tool_response_column", [])
            arguments.body["messages"].extend(
                self._build_tool_response_messages(
                    prior_response.tool_calls, tool_response_columns
                )
            )
    else:
        # Standard or tool_call turn: system prompt + user content.
        prefix = " ".join(data.columns.get("prefix_column", []))
        if prefix:
            arguments.body["messages"].append({"role": "system", "content": prefix})

        extras = kwargs.get("extras")
        content_extras = extras.content if extras is not None else None
        prompts = [
            self._format_prompts(
                data.columns.get(col, []),
                col,
                content_extras,
            )
            for col in (
                "text_column",
                "image_column",
                "video_column",
                "audio_column",
            )
        ]
        user_content = list(roundrobin(*prompts))
        if user_content:
            arguments.body["messages"].append(
                {"role": "user", "content": user_content}
            )

    # Inject tool definitions and apply tool-call-specific overrides.
    self._apply_tool_call_overrides(arguments.body, data)

    return arguments

OpenAIHTTPBackend

Bases: Backend

HTTP backend for OpenAI-compatible servers.

Supports OpenAI API, vLLM servers, and other compatible endpoints with text/chat completions, streaming, authentication, and multimodal inputs. Handles request formatting, response parsing, error handling, and token usage tracking with flexible parameter customization.

Example: :: backend_args = OpenAIHTTPBackendArgs( target="http://localhost:8000", model="gpt-3.5-turbo", api_key="your-api-key", ) backend = OpenAIHTTPBackend(backend_args)

await backend.process_startup()
async for response, request_info in backend.resolve(request, info):
    process_response(response)
await backend.process_shutdown()
Source code in src/guidellm/backends/openai/http.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
@Backend.register("openai_http")
class OpenAIHTTPBackend(Backend):
    """
    HTTP backend for OpenAI-compatible servers.

    Supports OpenAI API, vLLM servers, and other compatible endpoints with
    text/chat completions, streaming, authentication, and multimodal inputs.
    Handles request formatting, response parsing, error handling, and token
    usage tracking with flexible parameter customization.

    Example:
    ::
        backend_args = OpenAIHTTPBackendArgs(
            target="http://localhost:8000",
            model="gpt-3.5-turbo",
            api_key="your-api-key",
        )
        backend = OpenAIHTTPBackend(backend_args)

        await backend.process_startup()
        async for response, request_info in backend.resolve(request, info):
            process_response(response)
        await backend.process_shutdown()
    """

    _args: OpenAIHTTPBackendArgs

    def __init__(
        self,
        arguments: OpenAIHTTPBackendArgs,
    ):
        """
        Initialize OpenAI HTTP backend with server configuration.
        """
        super().__init__(arguments)

        # Runtime state
        self._in_process = False
        self._async_client: httpx.AsyncClient | None = None

    async def process_startup(self):
        """
        Initialize HTTP client and backend resources.

        :raises RuntimeError: If backend is already initialized
        :raises httpx.RequestError: If HTTP client cannot be created
        """
        if self._in_process:
            raise RuntimeError("Backend already started up for process.")

        self._async_client = httpx.AsyncClient(
            http2=self._args.http2,
            timeout=httpx.Timeout(
                FALLBACK_TIMEOUT,
                read=self._args.timeout,
                connect=self._args.timeout_connect,
            ),
            follow_redirects=self._args.follow_redirects,
            verify=self._args.verify,
            # Allow unlimited connections
            limits=httpx.Limits(
                max_connections=None,
                max_keepalive_connections=None,
                keepalive_expiry=5.0,  # default
            ),
        )
        self._in_process = True

    async def process_shutdown(self):
        """
        Clean up HTTP client and backend resources.

        :raises RuntimeError: If backend was not properly initialized
        :raises httpx.RequestError: If HTTP client cannot be closed
        """
        if not self._in_process:
            raise RuntimeError("Backend not started up for process.")

        await self._async_client.aclose()  # type: ignore [union-attr]
        self._async_client = None
        self._in_process = False

    async def validate(self):
        """
        Validate backend connectivity and configuration.

        :raises RuntimeError: If backend cannot connect or validate configuration
        """
        if self._async_client is None:
            raise RuntimeError("Backend not started up for process.")

        if not self._args.validate_backend:
            return

        try:
            validate_kwargs: dict[str, Any] = {
                "method": "GET",
                "url": f"{self._args.target}/{self._args.api_routes['/health']}",
            }
            existing_headers = validate_kwargs.get("headers")
            built_headers = self._build_headers(existing_headers)
            validate_kwargs["headers"] = built_headers
            response = await self._async_client.request(**validate_kwargs)
            response.raise_for_status()
        except Exception as exc:
            raise RuntimeError(
                "Backend validation request failed. Could not connect to the server "
                "or validate the backend configuration."
            ) from exc

    async def available_models(self) -> list[str]:
        """
        Get available models from the target server.

        :return: List of model identifiers
        :raises httpx.HTTPError: If models endpoint returns an error
        :raises RuntimeError: If backend is not initialized
        """
        if self._async_client is None:
            raise RuntimeError("Backend not started up for process.")

        target = f"{self._args.target}/{self._args.api_routes['/v1/models']}"
        response = await self._async_client.get(target, headers=self._build_headers())
        response.raise_for_status()

        return [item["id"] for item in response.json()["data"]]

    async def default_model(self) -> str:
        """
        Get the default model for this backend.

        :return: Model name or None if no model is available
        """
        if self._args.model or not self._in_process:
            return self._args.model

        models = await self.available_models()
        self._args.model = models[0] if models else ""
        return self._args.model

    async def resolve(  # type: ignore[override, misc]
        self,
        request: GenerationRequest,
        request_info: RequestInfo,
        history: list[tuple[GenerationRequest, GenerationResponse | None]]
        | None = None,
    ) -> AsyncIterator[tuple[GenerationResponse | None, RequestInfo]]:
        """
        Process generation request and yield progressive responses.

        Handles request formatting, timing tracking, API communication, and
        response parsing with streaming support.

        :param request: Generation request with content and parameters
        :param request_info: Request tracking info updated with timing metadata
        :param history: Conversation history (currently not supported)
        :raises NotImplementedError: If history is provided
        :raises RuntimeError: If backend is not initialized
        :raises ValueError: If request type is unsupported
        :yields: Tuples of (response, updated_request_info) as generation progresses
        """
        if self._async_client is None:
            raise RuntimeError("Backend not started up for process.")

        (
            request_handler,
            arguments,
            request_kwargs,
        ) = await self._prepare_resolve_request(request, history)

        if not arguments.stream:
            async for item in self._resolve_non_streaming(
                request, request_info, request_handler, arguments, request_kwargs
            ):
                yield item
            return

        async for item in self._resolve_streaming(
            request, request_info, request_handler, arguments, request_kwargs
        ):
            yield item

    async def _prepare_resolve_request(
        self,
        request: GenerationRequest,
        history: list[tuple[GenerationRequest, GenerationResponse | None]]
        | None = None,
    ) -> tuple[
        OpenAIRequestHandler,
        GenerationRequestArguments,
        dict[str, Any],
    ]:
        """
        Build the request handler, format arguments, and prepare HTTP kwargs.

        :param request: Generation request with content and parameters
        :param history: Optional conversation history for multi-turn requests
        :return: Tuple of (request_handler, formatted_arguments, http_kwargs)
        :raises ValueError: If request format is unsupported
        """
        if (
            request_path := self._args.api_routes.get(self._args.request_format)
        ) is None:
            raise ValueError(
                f"Unsupported request format '{self._args.request_format}'"
            )

        request_handler = OpenAIRequestHandlerFactory.create(
            self._args.request_format,
        )
        arguments: GenerationRequestArguments = request_handler.format(
            data=request,
            history=history,
            model=(await self.default_model()),
            stream=self._args.stream,
            extras=self._args.extras,
            max_tokens=self._args.max_tokens,
            server_history=self._args.server_history,
            multiturn_reasoning=self._args.multiturn_reasoning,
        )

        request_url = f"{self._args.target}/{request_path}"
        request_files = (
            {
                key: tuple(value) if isinstance(value, list) else value
                for key, value in arguments.files.items()
            }
            if arguments.files
            else None
        )
        # Omit `None` from output JSON
        deep_filter(arguments.body or {}, lambda _, v: v is not None)
        request_json = arguments.body if not request_files else None
        request_data = arguments.body if request_files else None

        request_kwargs: dict[str, Any] = {
            "url": request_url,
            "method": arguments.method or "POST",
            "params": arguments.params,
            "headers": self._build_headers(arguments.headers),
            "json": request_json,
            "data": request_data,
            "files": request_files,
        }

        return request_handler, arguments, request_kwargs

    async def _resolve_non_streaming(
        self,
        request: GenerationRequest,
        request_info: RequestInfo,
        request_handler: OpenAIRequestHandler,
        arguments: GenerationRequestArguments,
        request_kwargs: dict[str, Any],
    ) -> AsyncIterator[tuple[GenerationResponse | None, RequestInfo]]:
        """
        Handle a non-streaming generation request.

        :param request: The original generation request
        :param request_info: Request tracking info updated with timing metadata
        :param request_handler: Handler for compiling the response
        :param arguments: Formatted request arguments
        :param request_kwargs: Prepared HTTP request keyword arguments
        :yields: Single (response, request_info) tuple
        """
        if self._async_client is None:
            raise RuntimeError("Backend not started up for process.")

        request_info.timings.request_start = time.time()
        response = await self._async_client.request(**request_kwargs)
        request_info.timings.request_end = time.time()
        response.raise_for_status()
        data = response.json()
        gen_response = request_handler.compile_non_streaming(request, arguments, data)
        request_handler.post_validation(gen_response)
        try:
            self._check_tool_call_expectations(request, gen_response)
        except asyncio.CancelledError:
            yield gen_response, request_info
            raise
        yield gen_response, request_info

    async def _resolve_streaming(
        self,
        request: GenerationRequest,
        request_info: RequestInfo,
        request_handler: OpenAIRequestHandler,
        arguments: GenerationRequestArguments,
        request_kwargs: dict[str, Any],
    ) -> AsyncIterator[tuple[GenerationResponse | None, RequestInfo]]:
        """
        Handle a streaming generation request with progressive timing updates.

        :param request: The original generation request
        :param request_info: Request tracking info updated with timing metadata
        :param request_handler: Handler for processing stream lines and compiling
        :param arguments: Formatted request arguments
        :param request_kwargs: Prepared HTTP request keyword arguments
        :yields: Tuples of (response, request_info) as generation progresses
        """
        if self._async_client is None:
            raise RuntimeError("Backend not started up for process.")

        try:
            request_info.timings.request_start = time.time()

            async with self._async_client.stream(**request_kwargs) as stream:
                stream.raise_for_status()
                end_reached = False

                async for chunk in self._aiter_lines(stream):
                    stream.raise_for_status()
                    iter_time = time.time()

                    if request_info.timings.first_request_iteration is None:
                        request_info.timings.first_request_iteration = iter_time
                    request_info.timings.last_request_iteration = iter_time
                    request_info.timings.request_iterations += 1

                    iterations = request_handler.add_streaming_line(chunk)
                    if iterations is None or iterations <= 0 or end_reached:
                        end_reached = end_reached or iterations is None
                        if end_reached:
                            # Break eagerly once the handler signals completion
                            # (e.g. "data: [DONE]" or "response.completed").
                            # Using continue instead would hang on servers that
                            # keep the HTTP/2 stream open after the last event.
                            break
                        continue

                    if request_info.timings.first_token_iteration is None:
                        request_info.timings.first_token_iteration = iter_time
                        request_info.timings.token_iterations = 0
                        yield None, request_info

                    # TTFOT: record the first content (non-reasoning) token.
                    # For non-reasoning models this fires on the same iteration
                    # as first_token_iteration, making TTFOT == TTFT.
                    if (
                        request_info.timings.first_output_token_iteration is None
                        and request_handler.last_iteration_had_content
                    ):
                        request_info.timings.first_output_token_iteration = iter_time

                    request_info.timings.last_token_iteration = iter_time
                    request_info.timings.token_iterations += iterations

            request_info.timings.request_end = time.time()
            gen_response = request_handler.compile_streaming(request, arguments)
            request_handler.post_validation(gen_response)
            self._check_tool_call_expectations(request, gen_response)
            yield gen_response, request_info
        except asyncio.CancelledError as err:
            # Yield current result to store iterative results before propagating
            yield request_handler.compile_streaming(request, arguments), request_info
            raise err

    async def _aiter_lines(self, stream: httpx.Response) -> AsyncIterator[str]:
        """
        Asynchronously iterate over lines in an HTTP response stream.

        :param stream: HTTP response object with streaming content
        :yield: Lines of text from the response stream
        """
        async for line in stream.aiter_lines():
            if not line.strip():
                continue  # Skip blank lines
            yield line

    def _build_headers(
        self, existing_headers: dict[str, str] | None = None
    ) -> dict[str, str] | None:
        """
        Build headers dictionary with bearer token authentication.

        Merges the Authorization bearer token header (if api_key is set) with any
        existing headers. User-provided headers take precedence over the bearer token.

        :param existing_headers: Optional existing headers to merge with
        :return: Dictionary of headers with bearer token included if api_key is set
        """
        headers: dict[str, str] = {}

        # Add bearer token if api_key is set
        if self._args.api_key:
            token = self._args.api_key.get_secret_value()
            headers["Authorization"] = f"Bearer {token}"

        # Merge with existing headers (user headers take precedence)
        if existing_headers:
            headers = {**headers, **existing_headers}

        return headers or None

    def _check_tool_call_expectations(
        self,
        request: GenerationRequest,
        response: GenerationResponse,
    ) -> None:
        """Validate that a tool-call turn actually produced tool calls.

        Called before the final yield in ``resolve`` so that any raised
        exception prevents the normal yield and is instead handled by the
        ``except`` block (which yields the response once before propagating).
        When the request expected a tool call but the model didn't produce one,
        raises an exception according to ``tool_call_missing_behavior``:

        * ``ignore_continue`` -- no-op; the conversation proceeds normally.
        * ``ignore_stop`` -- raises :class:`asyncio.CancelledError` so the
          worker cancels remaining turns.
        * ``error_stop`` -- raises :class:`ValueError` so the worker marks
          the current turn as errored and cancels remaining turns.

        :param request: The generation request that was resolved.
        :param response: The compiled response from the model.
        """
        if request.turn_type != "client_tool_call" or response.tool_calls:
            return

        behavior = self._args.tool_call_missing_behavior
        if behavior == "ignore_continue":
            pass
        elif behavior == "ignore_stop":
            raise asyncio.CancelledError("Expected tool call but model produced none")
        elif behavior == "error_stop":
            raise ValueError("Expected tool call but model produced none")

__init__(arguments)

Initialize OpenAI HTTP backend with server configuration.

Source code in src/guidellm/backends/openai/http.py
def __init__(
    self,
    arguments: OpenAIHTTPBackendArgs,
):
    """
    Initialize OpenAI HTTP backend with server configuration.
    """
    super().__init__(arguments)

    # Runtime state
    self._in_process = False
    self._async_client: httpx.AsyncClient | None = None

available_models() async

Get available models from the target server.

Returns:

Type Description
list[str]

List of model identifiers

Raises:

Type Description
httpx.HTTPError

If models endpoint returns an error

RuntimeError

If backend is not initialized

Source code in src/guidellm/backends/openai/http.py
async def available_models(self) -> list[str]:
    """
    Get available models from the target server.

    :return: List of model identifiers
    :raises httpx.HTTPError: If models endpoint returns an error
    :raises RuntimeError: If backend is not initialized
    """
    if self._async_client is None:
        raise RuntimeError("Backend not started up for process.")

    target = f"{self._args.target}/{self._args.api_routes['/v1/models']}"
    response = await self._async_client.get(target, headers=self._build_headers())
    response.raise_for_status()

    return [item["id"] for item in response.json()["data"]]

default_model() async

Get the default model for this backend.

Returns:

Type Description
str

Model name or None if no model is available

Source code in src/guidellm/backends/openai/http.py
async def default_model(self) -> str:
    """
    Get the default model for this backend.

    :return: Model name or None if no model is available
    """
    if self._args.model or not self._in_process:
        return self._args.model

    models = await self.available_models()
    self._args.model = models[0] if models else ""
    return self._args.model

process_shutdown() async

Clean up HTTP client and backend resources.

Raises:

Type Description
RuntimeError

If backend was not properly initialized

httpx.RequestError

If HTTP client cannot be closed

Source code in src/guidellm/backends/openai/http.py
async def process_shutdown(self):
    """
    Clean up HTTP client and backend resources.

    :raises RuntimeError: If backend was not properly initialized
    :raises httpx.RequestError: If HTTP client cannot be closed
    """
    if not self._in_process:
        raise RuntimeError("Backend not started up for process.")

    await self._async_client.aclose()  # type: ignore [union-attr]
    self._async_client = None
    self._in_process = False

process_startup() async

Initialize HTTP client and backend resources.

Raises:

Type Description
RuntimeError

If backend is already initialized

httpx.RequestError

If HTTP client cannot be created

Source code in src/guidellm/backends/openai/http.py
async def process_startup(self):
    """
    Initialize HTTP client and backend resources.

    :raises RuntimeError: If backend is already initialized
    :raises httpx.RequestError: If HTTP client cannot be created
    """
    if self._in_process:
        raise RuntimeError("Backend already started up for process.")

    self._async_client = httpx.AsyncClient(
        http2=self._args.http2,
        timeout=httpx.Timeout(
            FALLBACK_TIMEOUT,
            read=self._args.timeout,
            connect=self._args.timeout_connect,
        ),
        follow_redirects=self._args.follow_redirects,
        verify=self._args.verify,
        # Allow unlimited connections
        limits=httpx.Limits(
            max_connections=None,
            max_keepalive_connections=None,
            keepalive_expiry=5.0,  # default
        ),
    )
    self._in_process = True

resolve(request, request_info, history=None) async

Process generation request and yield progressive responses.

Handles request formatting, timing tracking, API communication, and response parsing with streaming support.

:yields: Tuples of (response, updated_request_info) as generation progresses

Parameters:

Name Type Description Default
request GenerationRequest

Generation request with content and parameters

required
request_info RequestInfo

Request tracking info updated with timing metadata

required
history list[tuple[GenerationRequest, GenerationResponse | None]] | None

Conversation history (currently not supported)

None

Raises:

Type Description
NotImplementedError

If history is provided

RuntimeError

If backend is not initialized

ValueError

If request type is unsupported

Source code in src/guidellm/backends/openai/http.py
async def resolve(  # type: ignore[override, misc]
    self,
    request: GenerationRequest,
    request_info: RequestInfo,
    history: list[tuple[GenerationRequest, GenerationResponse | None]]
    | None = None,
) -> AsyncIterator[tuple[GenerationResponse | None, RequestInfo]]:
    """
    Process generation request and yield progressive responses.

    Handles request formatting, timing tracking, API communication, and
    response parsing with streaming support.

    :param request: Generation request with content and parameters
    :param request_info: Request tracking info updated with timing metadata
    :param history: Conversation history (currently not supported)
    :raises NotImplementedError: If history is provided
    :raises RuntimeError: If backend is not initialized
    :raises ValueError: If request type is unsupported
    :yields: Tuples of (response, updated_request_info) as generation progresses
    """
    if self._async_client is None:
        raise RuntimeError("Backend not started up for process.")

    (
        request_handler,
        arguments,
        request_kwargs,
    ) = await self._prepare_resolve_request(request, history)

    if not arguments.stream:
        async for item in self._resolve_non_streaming(
            request, request_info, request_handler, arguments, request_kwargs
        ):
            yield item
        return

    async for item in self._resolve_streaming(
        request, request_info, request_handler, arguments, request_kwargs
    ):
        yield item

validate() async

Validate backend connectivity and configuration.

Raises:

Type Description
RuntimeError

If backend cannot connect or validate configuration

Source code in src/guidellm/backends/openai/http.py
async def validate(self):
    """
    Validate backend connectivity and configuration.

    :raises RuntimeError: If backend cannot connect or validate configuration
    """
    if self._async_client is None:
        raise RuntimeError("Backend not started up for process.")

    if not self._args.validate_backend:
        return

    try:
        validate_kwargs: dict[str, Any] = {
            "method": "GET",
            "url": f"{self._args.target}/{self._args.api_routes['/health']}",
        }
        existing_headers = validate_kwargs.get("headers")
        built_headers = self._build_headers(existing_headers)
        validate_kwargs["headers"] = built_headers
        response = await self._async_client.request(**validate_kwargs)
        response.raise_for_status()
    except Exception as exc:
        raise RuntimeError(
            "Backend validation request failed. Could not connect to the server "
            "or validate the backend configuration."
        ) from exc

OpenAIRequestHandler

Bases: Protocol

Protocol for handling OpenAI request endpoint

Defines the interface to format the request for a given endpoint and to process both streaming and non-streaming responses from backend APIs, converting them into standardized GenerationResponse objects with consistent metrics extraction.

Source code in src/guidellm/backends/openai/request_handlers.py
class OpenAIRequestHandler(Protocol):
    """
    Protocol for handling OpenAI request endpoint

    Defines the interface to format the request for a given endpoint and to
    process both streaming and non-streaming responses from backend APIs,
    converting them into standardized GenerationResponse objects
    with consistent metrics extraction.
    """

    def format(
        self,
        data: GenerationRequest,
        history: HistoryT[GenerationRequest, GenerationResponse] | None = None,
        **kwargs,
    ) -> GenerationRequestArguments:
        """
        Format the generation request into the appropriate structure for
        the backend API.

        :param data: The generation request to format
        :param history: Prior (request, response) pairs in the conversation
        :param **kwargs: Additional keyword arguments for request formatting
        :return: The formatted request arguments
        """
        ...

    def compile_non_streaming(
        self,
        request: GenerationRequest,
        arguments: GenerationRequestArguments,
        response: Any,
    ) -> GenerationResponse:
        """
        Process a complete non-streaming API response.

        :param request: Original generation request
        :param response: Raw API response data from the backend
        :return: Standardized GenerationResponse with extracted metrics
        """
        ...

    @property
    def last_iteration_had_content(self) -> bool:
        """
        Whether the last chunk carried output (text/tool-call) tokens,
        not solely reasoning tokens.

        Used by the HTTP streaming loop to detect the first output token
        for TTFOT measurement.

        :return: True if the last chunk carried output tokens, not solely
            reasoning tokens.
        """
        ...

    def add_streaming_line(self, line: str) -> int | None:
        """
        Process a single line from a streaming response.

        :param line: Raw line from the streaming response
        :return: 1 if content was updated, 0 if line was ignored, None if done
        """
        ...

    def compile_streaming(
        self, request: GenerationRequest, arguments: GenerationRequestArguments
    ) -> GenerationResponse:
        """
        Compile accumulated streaming data into a final response.

        :param request: Original generation request
        :return: Standardized GenerationResponse with extracted metrics
        """
        ...

    def post_validation(self, response: GenerationResponse) -> None:
        """Validate a compiled response before returning it.

        Default implementation is permissive (no-op). Handlers override
        this to reject responses that lack usable output for their
        endpoint type.

        :param response: The compiled generation response to validate.
        :raises ValueError: If the response is unusable.
        """
        ...

last_iteration_had_content property

Whether the last chunk carried output (text/tool-call) tokens, not solely reasoning tokens.

Used by the HTTP streaming loop to detect the first output token for TTFOT measurement.

Returns:

Type Description
bool

True if the last chunk carried output tokens, not solely reasoning tokens.

add_streaming_line(line)

Process a single line from a streaming response.

Parameters:

Name Type Description Default
line str

Raw line from the streaming response

required

Returns:

Type Description
int | None

1 if content was updated, 0 if line was ignored, None if done

Source code in src/guidellm/backends/openai/request_handlers.py
def add_streaming_line(self, line: str) -> int | None:
    """
    Process a single line from a streaming response.

    :param line: Raw line from the streaming response
    :return: 1 if content was updated, 0 if line was ignored, None if done
    """
    ...

compile_non_streaming(request, arguments, response)

Process a complete non-streaming API response.

Parameters:

Name Type Description Default
request GenerationRequest

Original generation request

required
response Any

Raw API response data from the backend

required

Returns:

Type Description
GenerationResponse

Standardized GenerationResponse with extracted metrics

Source code in src/guidellm/backends/openai/request_handlers.py
def compile_non_streaming(
    self,
    request: GenerationRequest,
    arguments: GenerationRequestArguments,
    response: Any,
) -> GenerationResponse:
    """
    Process a complete non-streaming API response.

    :param request: Original generation request
    :param response: Raw API response data from the backend
    :return: Standardized GenerationResponse with extracted metrics
    """
    ...

compile_streaming(request, arguments)

Compile accumulated streaming data into a final response.

Parameters:

Name Type Description Default
request GenerationRequest

Original generation request

required

Returns:

Type Description
GenerationResponse

Standardized GenerationResponse with extracted metrics

Source code in src/guidellm/backends/openai/request_handlers.py
def compile_streaming(
    self, request: GenerationRequest, arguments: GenerationRequestArguments
) -> GenerationResponse:
    """
    Compile accumulated streaming data into a final response.

    :param request: Original generation request
    :return: Standardized GenerationResponse with extracted metrics
    """
    ...

format(data, history=None, **kwargs)

Format the generation request into the appropriate structure for the backend API.

Parameters:

Name Type Description Default
data GenerationRequest

The generation request to format

required
history HistoryT[GenerationRequest, GenerationResponse] | None

Prior (request, response) pairs in the conversation

None
**kwargs

Additional keyword arguments for request formatting

{}

Returns:

Type Description
GenerationRequestArguments

The formatted request arguments

Source code in src/guidellm/backends/openai/request_handlers.py
def format(
    self,
    data: GenerationRequest,
    history: HistoryT[GenerationRequest, GenerationResponse] | None = None,
    **kwargs,
) -> GenerationRequestArguments:
    """
    Format the generation request into the appropriate structure for
    the backend API.

    :param data: The generation request to format
    :param history: Prior (request, response) pairs in the conversation
    :param **kwargs: Additional keyword arguments for request formatting
    :return: The formatted request arguments
    """
    ...

post_validation(response)

Validate a compiled response before returning it.

Default implementation is permissive (no-op). Handlers override this to reject responses that lack usable output for their endpoint type.

Parameters:

Name Type Description Default
response GenerationResponse

The compiled generation response to validate.

required

Raises:

Type Description
ValueError

If the response is unusable.

Source code in src/guidellm/backends/openai/request_handlers.py
def post_validation(self, response: GenerationResponse) -> None:
    """Validate a compiled response before returning it.

    Default implementation is permissive (no-op). Handlers override
    this to reject responses that lack usable output for their
    endpoint type.

    :param response: The compiled generation response to validate.
    :raises ValueError: If the response is unusable.
    """
    ...

OpenAIRequestHandlerFactory

Bases: RegistryMixin[type[OpenAIRequestHandler]]

Factory for registering and creating OpenAI request handlers by request type.

Registry-based system for associating handler classes with specific API types, enabling automatic selection of the appropriate handler for processing responses from different generation services.

Source code in src/guidellm/backends/openai/request_handlers.py
class OpenAIRequestHandlerFactory(RegistryMixin[type[OpenAIRequestHandler]]):
    """
    Factory for registering and creating OpenAI request handlers by request type.

    Registry-based system for associating handler classes with specific API
    types, enabling automatic selection of the appropriate handler for processing
    responses from different generation services.
    """

    @classmethod
    def create(
        cls,
        request_type: str,
        handler_overrides: dict[str, type[OpenAIRequestHandler]] | None = None,
    ) -> OpenAIRequestHandler:
        """
        Create a request handler class for the given request type.

        :param request_type: The type of generation request (e.g., "/chat/completions")
        :param handler_overrides: Optional mapping of request types to handler classes
            to override the default registry by checking first and then falling back
            to the registered handlers.
        :return: The corresponding instantiated GenerationResponseHandler
        :raises ValueError: When no handler is registered for the request type
        """
        if handler_overrides and request_type in handler_overrides:
            return handler_overrides[request_type]()

        handler_cls = cls.get_registered_object(request_type)
        if not handler_cls:
            raise ValueError(
                f"No response handler registered for type '{request_type}'."
            )

        return handler_cls()

create(request_type, handler_overrides=None) classmethod

Create a request handler class for the given request type.

Parameters:

Name Type Description Default
request_type str

The type of generation request (e.g., "/chat/completions")

required
handler_overrides dict[str, type[OpenAIRequestHandler]] | None

Optional mapping of request types to handler classes to override the default registry by checking first and then falling back to the registered handlers.

None

Returns:

Type Description
OpenAIRequestHandler

The corresponding instantiated GenerationResponseHandler

Raises:

Type Description
ValueError

When no handler is registered for the request type

Source code in src/guidellm/backends/openai/request_handlers.py
@classmethod
def create(
    cls,
    request_type: str,
    handler_overrides: dict[str, type[OpenAIRequestHandler]] | None = None,
) -> OpenAIRequestHandler:
    """
    Create a request handler class for the given request type.

    :param request_type: The type of generation request (e.g., "/chat/completions")
    :param handler_overrides: Optional mapping of request types to handler classes
        to override the default registry by checking first and then falling back
        to the registered handlers.
    :return: The corresponding instantiated GenerationResponseHandler
    :raises ValueError: When no handler is registered for the request type
    """
    if handler_overrides and request_type in handler_overrides:
        return handler_overrides[request_type]()

    handler_cls = cls.get_registered_object(request_type)
    if not handler_cls:
        raise ValueError(
            f"No response handler registered for type '{request_type}'."
        )

    return handler_cls()

OpenAIWebSocketBackend

Bases: Backend

WebSocket client for realtime (streaming) audio transcription.

Connects to a vLLM-style /v1/realtime WebSocket, streams PCM16 audio chunks, and maps transcription.* events into GenerationResponse with timings.

Example: :: args = OpenAIWebSocketBackendArgs( target="http://localhost:8000", model="my-model", ) backend = OpenAIWebSocketBackend(args)

await backend.process_startup()
async for response, request_info in backend.resolve(request, info):
    ...
await backend.process_shutdown()
Source code in src/guidellm/backends/openai/websocket.py
@Backend.register("openai_websocket")
class OpenAIWebSocketBackend(Backend):
    """
    WebSocket client for realtime (streaming) audio transcription.

    Connects to a vLLM-style ``/v1/realtime`` WebSocket, streams PCM16 audio chunks,
    and maps ``transcription.*`` events into ``GenerationResponse`` with timings.

    Example:
    ::
        args = OpenAIWebSocketBackendArgs(
            target="http://localhost:8000",
            model="my-model",
        )
        backend = OpenAIWebSocketBackend(args)

        await backend.process_startup()
        async for response, request_info in backend.resolve(request, info):
            ...
        await backend.process_shutdown()
    """

    _args: OpenAIWebSocketBackendArgs

    def __init__(self, arguments: OpenAIWebSocketBackendArgs):
        """
        Initialize the WebSocket backend from validated args.

        :param arguments: Typed configuration including target, model, and paths.
        """
        super().__init__(arguments)
        self._resolved_model = (arguments.model or "").strip()
        self.validate_backend: dict[str, Any] | None = resolve_validate_kwargs(
            arguments.validate_backend,
            self._args.target,
            _WS_API_ROUTES,
        )
        self._in_process = False
        self._async_client: httpx.AsyncClient | None = None

    @property
    def websocket_path(self) -> str:
        """
        HTTP path segment on the host used for the WebSocket URL.

        :return: Resolved path from ``request_format``.
        """
        return self._args.request_format

    @property
    def info(self) -> dict[str, Any]:
        """
        Return a snapshot of backend configuration for logging or debugging.

        :return: Dict of target, model, WebSocket path, timeouts, and validation opts.
        """
        return {
            "target": self._args.target,
            "model": self._resolved_model or self._args.model,
            "websocket_path": self.websocket_path,
            "chunk_samples": self._args.chunk_samples,
            "timeout": self._args.timeout,
            "timeout_connect": self._args.timeout_connect,
            "verify": self._args.verify,
            "validate_backend": self.validate_backend,
        }

    def _parsed_target(self) -> ParseResult:
        """Parse ``target`` into a URL structure for scheme and host lookup."""
        raw = (
            self._args.target
            if "://" in self._args.target
            else f"http://{self._args.target}"
        )
        return urlparse(raw)

    def _ws_url(self) -> str:
        """Build ``ws://`` or ``wss://`` URL including :attr:`websocket_path`."""
        parsed = self._parsed_target()
        if not parsed.netloc:
            raise ValueError(f"Invalid target URL for WebSocket: {self._args.target!r}")
        ws_scheme = "wss" if parsed.scheme in ("https", "wss") else "ws"
        path = self.websocket_path
        if not path.startswith("/"):
            path = f"/{path}"
        return f"{ws_scheme}://{parsed.netloc}{path}"

    def _ssl_context(self) -> ssl.SSLContext | None:
        """TLS context for secure WebSockets; ``None`` when using plain ``ws``."""
        if self._parsed_target().scheme in ("http", "ws"):
            return None
        ctx = ssl.create_default_context()
        if not self._args.verify:
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE
        return ctx

    def _build_headers(
        self, existing_headers: dict[str, str] | None = None
    ) -> dict[str, str] | None:
        """Merge bearer auth and optional headers for HTTP and WebSocket handshakes."""
        return build_headers(self._args.api_key, existing_headers)

    async def process_startup(self) -> None:
        """
        Create the shared :class:`httpx.AsyncClient` used for health and ``/v1/models``.

        :raises RuntimeError: If the backend was already started in this process.
        """
        if self._in_process:
            raise RuntimeError("Backend already started up for process.")
        self._async_client = httpx.AsyncClient(
            timeout=httpx.Timeout(
                FALLBACK_TIMEOUT,
                read=self._args.timeout,
                connect=self._args.timeout_connect,
            ),
            verify=self._args.verify,
            limits=httpx.Limits(
                max_connections=None,
                max_keepalive_connections=None,
                keepalive_expiry=5.0,
            ),
        )
        self._in_process = True

    async def process_shutdown(self) -> None:
        """
        Close the HTTP client and reset process-local state.

        :raises RuntimeError: If the backend was not started.
        """
        if not self._in_process:
            raise RuntimeError("Backend not started up for process.")
        client = self._async_client
        if client is None:
            raise RuntimeError("Backend not started up for process.")
        await client.aclose()
        self._async_client = None
        self._in_process = False

    async def validate(self) -> None:
        """
        Run the configured HTTP probe (same semantics as ``openai_http``).

        :raises RuntimeError: If the client is not started or the probe fails.
        """
        if self._async_client is None:
            raise RuntimeError("Backend not started up for process.")
        if not self.validate_backend:
            return
        validate_kwargs = {**self.validate_backend}
        existing_headers = validate_kwargs.get("headers")
        validate_kwargs["headers"] = build_headers(self._args.api_key, existing_headers)
        try:
            response = await self._async_client.request(**validate_kwargs)
            response.raise_for_status()
        except Exception as exc:
            raise RuntimeError(
                "Backend validation request failed. Could not connect to the server "
                "or validate the backend configuration."
            ) from exc

    async def available_models(self) -> list[str]:
        """
        List model IDs from ``GET /v1/models`` on the HTTP target.

        :return: Model identifiers from the OpenAI-style payload.
        :raises RuntimeError: If the client is not started or the response is invalid.
        """
        if self._async_client is None:
            raise RuntimeError("Backend not started up for process.")
        target = f"{self._args.target}/v1/models"
        response = await self._async_client.get(
            target, headers=build_headers(self._args.api_key)
        )
        response.raise_for_status()
        return [item["id"] for item in response.json()["data"]]

    async def default_model(self) -> str:
        """
        Return the configured model, or the first from ``available_models`` if empty.

        :return: Non-empty model name when discoverable; otherwise ``""``.
        """
        if self._resolved_model:
            return self._resolved_model
        if not self._in_process:
            return ""
        models = await self.available_models()
        self._resolved_model = models[0] if models else ""
        return self._resolved_model

    async def resolve(  # type: ignore[override, misc]  # noqa: C901, PLR0912, PLR0915
        self,
        request: GenerationRequest,
        request_info: RequestInfo,
        history: list[tuple[GenerationRequest, GenerationResponse | None]]
        | None = None,
    ) -> AsyncIterator[tuple[GenerationResponse | None, RequestInfo]]:
        """
        Stream one realtime transcription over WebSocket for a single audio column.

        Delegates event interpretation to the registered
        :class:`~guidellm.backends.openai.request_handlers.OpenAIWSRequestHandler`
        via ``add_streaming_event`` / ``compile_streaming``, while this method handles
        only I/O and timing.

        :param request: Must contain exactly one ``audio_column`` entry.
        :param request_info: Timings updated as events arrive.
        :param history: Not supported; raises ``NotImplementedError`` if non-empty.
        :raises NotImplementedError: If ``history`` is provided.
        :raises RuntimeError: If the client is not started, model is missing, or the
            peer returns an error event.
        :yields: ``(response_or_none, request_info)`` until stream completion.
        """
        if self._async_client is None:
            raise RuntimeError("Backend not started up for process.")
        if history:
            raise NotImplementedError(
                "openai_websocket does not support multiturn/history yet."
            )

        model_name = await self.default_model()
        if not str(model_name).strip():
            raise RuntimeError(
                "No model configured for openai_websocket and /v1/models returned "
                "none. Pass --model or ensure the server lists at least one model."
            )

        handler = OpenAIWSRequestHandlerFactory.create(self.websocket_path)
        arguments = handler.format(
            request,
            model=model_name,
            websocket_path=self.websocket_path,
            chunk_samples=self._args.chunk_samples,
        )
        body = arguments.body or {}
        chunks = body.get(WS_AUDIO_CHUNKS_BODY_KEY)
        if not isinstance(chunks, list):
            raise RuntimeError(
                "Realtime WebSocket handler format() did not provide "
                f"{WS_AUDIO_CHUNKS_BODY_KEY!r}."
            )

        session_update: dict[str, Any] = {"type": "session.update"}
        extras = self._args.extras or {}
        if extras:
            for key, val in extras.items():
                if key not in ("type", "model"):
                    session_update[key] = val
        session_update["model"] = model_name

        ssl_ctx = self._ssl_context()
        ws_headers = build_headers(self._args.api_key)

        try:
            request_info.timings.request_start = time.time()
            connect_kw: dict[str, Any] = {
                "ssl": ssl_ctx,
                "open_timeout": self._args.timeout_connect,
            }
            if ws_headers:
                connect_kw["additional_headers"] = ws_headers
            async with ws_connect(self._ws_url(), **connect_kw) as ws:
                raw_first = await self._recv_ws(ws)
                first_event = _load_ws_event(raw_first)
                if first_event.get("type") == "error":
                    raise RuntimeError(format_ws_error(first_event.get("error")))
                if first_event.get("type") != "session.created":
                    raise RuntimeError(
                        f"Expected session.created, got {first_event.get('type')!r}"
                    )
                await ws.send(_json_text(session_update))
                for b64_chunk in chunks:
                    await ws.send(
                        _json_text(
                            {"type": "input_audio_buffer.append", "audio": b64_chunk}
                        )
                    )
                await ws.send(
                    _json_text({"type": "input_audio_buffer.commit", "final": False})
                )
                await ws.send(
                    _json_text({"type": "input_audio_buffer.commit", "final": True})
                )

                ignored_events = 0
                while True:
                    raw = await self._recv_ws(ws)
                    event = _load_ws_event(raw)
                    update = handler.add_streaming_event(event)

                    if update.kind is WSEventResult.STREAM_END:
                        iter_time = time.time()
                        request_info.timings.request_end = iter_time
                        # Done-only path: first-token timing when text exists.
                        if (
                            request_info.timings.first_token_iteration is None
                            and _record_content_tokens(
                                request_info,
                                content_tokens=1 if handler.streaming_text else 0,
                                record_request_iteration=True,
                            )
                        ):
                            yield None, request_info
                        break

                    if update.kind in (
                        WSEventResult.CONTENT,
                        WSEventResult.REQUEST_ITERATION,
                    ):
                        if _record_content_tokens(
                            request_info,
                            content_tokens=update.content_tokens,
                            record_request_iteration=True,
                        ):
                            yield None, request_info
                    elif update.kind is WSEventResult.IGNORED:
                        ignored_events += 1
                        if ignored_events > _MAX_IGNORED_WS_EVENT_TYPES:
                            raise RuntimeError(
                                "Exceeded maximum ignored realtime WebSocket events "
                                f"(last type={event.get('type')!r})."
                            )

                compiled = handler.compile_streaming(request, arguments)
                handler.post_validation(compiled)
                yield compiled, request_info

        except asyncio.CancelledError as err:
            yield handler.compile_streaming(request, arguments), request_info
            raise err
        finally:
            if (
                request_info.timings.request_start is not None
                and request_info.timings.request_end is None
            ):
                request_info.timings.request_end = time.time()

    async def _recv_ws(self, ws: ClientConnection) -> str:
        """
        Receive one text frame from the WebSocket, honoring per-message timeout.

        :param ws: Active realtime connection.
        :return: Decoded UTF-8 text from the server.
        """
        if self._args.timeout is None:
            msg = await ws.recv()
        else:
            msg = await asyncio.wait_for(ws.recv(), timeout=self._args.timeout)
        if isinstance(msg, bytes):
            return msg.decode()
        return str(msg)

info property

Return a snapshot of backend configuration for logging or debugging.

Returns:

Type Description
dict[str, Any]

Dict of target, model, WebSocket path, timeouts, and validation opts.

websocket_path property

HTTP path segment on the host used for the WebSocket URL.

Returns:

Type Description
str

Resolved path from request_format.

__init__(arguments)

Initialize the WebSocket backend from validated args.

Parameters:

Name Type Description Default
arguments OpenAIWebSocketBackendArgs

Typed configuration including target, model, and paths.

required
Source code in src/guidellm/backends/openai/websocket.py
def __init__(self, arguments: OpenAIWebSocketBackendArgs):
    """
    Initialize the WebSocket backend from validated args.

    :param arguments: Typed configuration including target, model, and paths.
    """
    super().__init__(arguments)
    self._resolved_model = (arguments.model or "").strip()
    self.validate_backend: dict[str, Any] | None = resolve_validate_kwargs(
        arguments.validate_backend,
        self._args.target,
        _WS_API_ROUTES,
    )
    self._in_process = False
    self._async_client: httpx.AsyncClient | None = None

available_models() async

List model IDs from GET /v1/models on the HTTP target.

Returns:

Type Description
list[str]

Model identifiers from the OpenAI-style payload.

Raises:

Type Description
RuntimeError

If the client is not started or the response is invalid.

Source code in src/guidellm/backends/openai/websocket.py
async def available_models(self) -> list[str]:
    """
    List model IDs from ``GET /v1/models`` on the HTTP target.

    :return: Model identifiers from the OpenAI-style payload.
    :raises RuntimeError: If the client is not started or the response is invalid.
    """
    if self._async_client is None:
        raise RuntimeError("Backend not started up for process.")
    target = f"{self._args.target}/v1/models"
    response = await self._async_client.get(
        target, headers=build_headers(self._args.api_key)
    )
    response.raise_for_status()
    return [item["id"] for item in response.json()["data"]]

default_model() async

Return the configured model, or the first from available_models if empty.

Returns:

Type Description
str

Non-empty model name when discoverable; otherwise "".

Source code in src/guidellm/backends/openai/websocket.py
async def default_model(self) -> str:
    """
    Return the configured model, or the first from ``available_models`` if empty.

    :return: Non-empty model name when discoverable; otherwise ``""``.
    """
    if self._resolved_model:
        return self._resolved_model
    if not self._in_process:
        return ""
    models = await self.available_models()
    self._resolved_model = models[0] if models else ""
    return self._resolved_model

process_shutdown() async

Close the HTTP client and reset process-local state.

Raises:

Type Description
RuntimeError

If the backend was not started.

Source code in src/guidellm/backends/openai/websocket.py
async def process_shutdown(self) -> None:
    """
    Close the HTTP client and reset process-local state.

    :raises RuntimeError: If the backend was not started.
    """
    if not self._in_process:
        raise RuntimeError("Backend not started up for process.")
    client = self._async_client
    if client is None:
        raise RuntimeError("Backend not started up for process.")
    await client.aclose()
    self._async_client = None
    self._in_process = False

process_startup() async

Create the shared :class:httpx.AsyncClient used for health and /v1/models.

Raises:

Type Description
RuntimeError

If the backend was already started in this process.

Source code in src/guidellm/backends/openai/websocket.py
async def process_startup(self) -> None:
    """
    Create the shared :class:`httpx.AsyncClient` used for health and ``/v1/models``.

    :raises RuntimeError: If the backend was already started in this process.
    """
    if self._in_process:
        raise RuntimeError("Backend already started up for process.")
    self._async_client = httpx.AsyncClient(
        timeout=httpx.Timeout(
            FALLBACK_TIMEOUT,
            read=self._args.timeout,
            connect=self._args.timeout_connect,
        ),
        verify=self._args.verify,
        limits=httpx.Limits(
            max_connections=None,
            max_keepalive_connections=None,
            keepalive_expiry=5.0,
        ),
    )
    self._in_process = True

resolve(request, request_info, history=None) async

Stream one realtime transcription over WebSocket for a single audio column.

Delegates event interpretation to the registered :class:~guidellm.backends.openai.request_handlers.OpenAIWSRequestHandler via add_streaming_event / compile_streaming, while this method handles only I/O and timing.

:yields: (response_or_none, request_info) until stream completion.

Parameters:

Name Type Description Default
request GenerationRequest

Must contain exactly one audio_column entry.

required
request_info RequestInfo

Timings updated as events arrive.

required
history list[tuple[GenerationRequest, GenerationResponse | None]] | None

Not supported; raises NotImplementedError if non-empty.

None

Raises:

Type Description
NotImplementedError

If history is provided.

RuntimeError

If the client is not started, model is missing, or the peer returns an error event.

Source code in src/guidellm/backends/openai/websocket.py
async def resolve(  # type: ignore[override, misc]  # noqa: C901, PLR0912, PLR0915
    self,
    request: GenerationRequest,
    request_info: RequestInfo,
    history: list[tuple[GenerationRequest, GenerationResponse | None]]
    | None = None,
) -> AsyncIterator[tuple[GenerationResponse | None, RequestInfo]]:
    """
    Stream one realtime transcription over WebSocket for a single audio column.

    Delegates event interpretation to the registered
    :class:`~guidellm.backends.openai.request_handlers.OpenAIWSRequestHandler`
    via ``add_streaming_event`` / ``compile_streaming``, while this method handles
    only I/O and timing.

    :param request: Must contain exactly one ``audio_column`` entry.
    :param request_info: Timings updated as events arrive.
    :param history: Not supported; raises ``NotImplementedError`` if non-empty.
    :raises NotImplementedError: If ``history`` is provided.
    :raises RuntimeError: If the client is not started, model is missing, or the
        peer returns an error event.
    :yields: ``(response_or_none, request_info)`` until stream completion.
    """
    if self._async_client is None:
        raise RuntimeError("Backend not started up for process.")
    if history:
        raise NotImplementedError(
            "openai_websocket does not support multiturn/history yet."
        )

    model_name = await self.default_model()
    if not str(model_name).strip():
        raise RuntimeError(
            "No model configured for openai_websocket and /v1/models returned "
            "none. Pass --model or ensure the server lists at least one model."
        )

    handler = OpenAIWSRequestHandlerFactory.create(self.websocket_path)
    arguments = handler.format(
        request,
        model=model_name,
        websocket_path=self.websocket_path,
        chunk_samples=self._args.chunk_samples,
    )
    body = arguments.body or {}
    chunks = body.get(WS_AUDIO_CHUNKS_BODY_KEY)
    if not isinstance(chunks, list):
        raise RuntimeError(
            "Realtime WebSocket handler format() did not provide "
            f"{WS_AUDIO_CHUNKS_BODY_KEY!r}."
        )

    session_update: dict[str, Any] = {"type": "session.update"}
    extras = self._args.extras or {}
    if extras:
        for key, val in extras.items():
            if key not in ("type", "model"):
                session_update[key] = val
    session_update["model"] = model_name

    ssl_ctx = self._ssl_context()
    ws_headers = build_headers(self._args.api_key)

    try:
        request_info.timings.request_start = time.time()
        connect_kw: dict[str, Any] = {
            "ssl": ssl_ctx,
            "open_timeout": self._args.timeout_connect,
        }
        if ws_headers:
            connect_kw["additional_headers"] = ws_headers
        async with ws_connect(self._ws_url(), **connect_kw) as ws:
            raw_first = await self._recv_ws(ws)
            first_event = _load_ws_event(raw_first)
            if first_event.get("type") == "error":
                raise RuntimeError(format_ws_error(first_event.get("error")))
            if first_event.get("type") != "session.created":
                raise RuntimeError(
                    f"Expected session.created, got {first_event.get('type')!r}"
                )
            await ws.send(_json_text(session_update))
            for b64_chunk in chunks:
                await ws.send(
                    _json_text(
                        {"type": "input_audio_buffer.append", "audio": b64_chunk}
                    )
                )
            await ws.send(
                _json_text({"type": "input_audio_buffer.commit", "final": False})
            )
            await ws.send(
                _json_text({"type": "input_audio_buffer.commit", "final": True})
            )

            ignored_events = 0
            while True:
                raw = await self._recv_ws(ws)
                event = _load_ws_event(raw)
                update = handler.add_streaming_event(event)

                if update.kind is WSEventResult.STREAM_END:
                    iter_time = time.time()
                    request_info.timings.request_end = iter_time
                    # Done-only path: first-token timing when text exists.
                    if (
                        request_info.timings.first_token_iteration is None
                        and _record_content_tokens(
                            request_info,
                            content_tokens=1 if handler.streaming_text else 0,
                            record_request_iteration=True,
                        )
                    ):
                        yield None, request_info
                    break

                if update.kind in (
                    WSEventResult.CONTENT,
                    WSEventResult.REQUEST_ITERATION,
                ):
                    if _record_content_tokens(
                        request_info,
                        content_tokens=update.content_tokens,
                        record_request_iteration=True,
                    ):
                        yield None, request_info
                elif update.kind is WSEventResult.IGNORED:
                    ignored_events += 1
                    if ignored_events > _MAX_IGNORED_WS_EVENT_TYPES:
                        raise RuntimeError(
                            "Exceeded maximum ignored realtime WebSocket events "
                            f"(last type={event.get('type')!r})."
                        )

            compiled = handler.compile_streaming(request, arguments)
            handler.post_validation(compiled)
            yield compiled, request_info

    except asyncio.CancelledError as err:
        yield handler.compile_streaming(request, arguments), request_info
        raise err
    finally:
        if (
            request_info.timings.request_start is not None
            and request_info.timings.request_end is None
        ):
            request_info.timings.request_end = time.time()

validate() async

Run the configured HTTP probe (same semantics as openai_http).

Raises:

Type Description
RuntimeError

If the client is not started or the probe fails.

Source code in src/guidellm/backends/openai/websocket.py
async def validate(self) -> None:
    """
    Run the configured HTTP probe (same semantics as ``openai_http``).

    :raises RuntimeError: If the client is not started or the probe fails.
    """
    if self._async_client is None:
        raise RuntimeError("Backend not started up for process.")
    if not self.validate_backend:
        return
    validate_kwargs = {**self.validate_backend}
    existing_headers = validate_kwargs.get("headers")
    validate_kwargs["headers"] = build_headers(self._args.api_key, existing_headers)
    try:
        response = await self._async_client.request(**validate_kwargs)
        response.raise_for_status()
    except Exception as exc:
        raise RuntimeError(
            "Backend validation request failed. Could not connect to the server "
            "or validate the backend configuration."
        ) from exc

OpenAIWebSocketBackendArgs

Bases: BackendArgs

Typed configuration for :class:OpenAIWebSocketBackend.

Source code in src/guidellm/backends/openai/websocket.py
@BackendArgs.register("openai_websocket")
class OpenAIWebSocketBackendArgs(BackendArgs):
    """Typed configuration for :class:`OpenAIWebSocketBackend`."""

    kind: Literal["openai_websocket"] = Field(
        default="openai_websocket",
        description="Type identifier for the backend configuration.",
    )
    target: str = Field(
        description=(
            "HTTP(S) base URL of the server (WebSocket URL is derived from it)."
        ),
    )
    model: str = Field(
        default_factory=str,
        description="Model identifier for generation requests.",
    )
    request_format: str = Field(
        default="/v1/realtime",
        description=(
            "Realtime WebSocket path (only /v1/realtime is supported today). "
            "Use the same top-level CLI flags as ``openai_http``: "
            "--request-format / --request-type."
        ),
    )
    chunk_samples: int = Field(
        default=3200,
        ge=1,
        description="PCM16 frames per input_audio_buffer.append chunk (16 kHz).",
    )
    api_key: SecretStr | None = Field(
        default=None, description="Bearer token if required."
    )  # noqa: F821
    verify: bool = Field(default=False, description="Verify TLS certificates.")
    timeout: float | None = Field(
        default=None,
        description="Per-message read timeout for WebSocket receives (seconds).",
    )
    timeout_connect: float = Field(
        default=FALLBACK_TIMEOUT,
        description="Timeout for establishing the WebSocket connection.",
    )
    validate_backend: bool | str | dict[str, Any] = Field(
        default=True,
        description=(
            "HTTP health check before benchmarks (same semantics as openai_http)."
        ),
    )
    extras: dict[str, Any] | None = Field(
        default=None,
        description="Extra fields merged into session.update (backend model wins).",
    )

    @field_validator("target", mode="after")
    @classmethod
    def strip_target(cls, value: str) -> str:
        """Strip trailing slashes and ``/v1`` suffix from the target URL."""
        return value.rstrip("/").removesuffix("/v1")

    @field_validator("request_format")
    @classmethod
    def validate_request_format(cls, v: str) -> str:
        """Validate ``request_format`` against allowed WebSocket paths."""
        stripped = v.strip()
        if stripped != "/v1/realtime":
            raise ValueError(f"request_format must be '/v1/realtime', got {stripped!r}")
        return stripped

strip_target(value) classmethod

Strip trailing slashes and /v1 suffix from the target URL.

Source code in src/guidellm/backends/openai/websocket.py
@field_validator("target", mode="after")
@classmethod
def strip_target(cls, value: str) -> str:
    """Strip trailing slashes and ``/v1`` suffix from the target URL."""
    return value.rstrip("/").removesuffix("/v1")

validate_request_format(v) classmethod

Validate request_format against allowed WebSocket paths.

Source code in src/guidellm/backends/openai/websocket.py
@field_validator("request_format")
@classmethod
def validate_request_format(cls, v: str) -> str:
    """Validate ``request_format`` against allowed WebSocket paths."""
    stripped = v.strip()
    if stripped != "/v1/realtime":
        raise ValueError(f"request_format must be '/v1/realtime', got {stripped!r}")
    return stripped

TextCompletionsRequestHandler

Bases: OpenAIRequestHandler

Request handler for OpenAI-style legacy completion endpoints.

Processes responses from text completion APIs that return generated text in the 'choices' array with 'text' fields. Handles both streaming and non-streaming responses, extracting usage metrics for input and output tokens.

Example: :: handler = TextCompletionsResponseHandler() response = handler.compile_non_streaming(request, api_response)

Source code in src/guidellm/backends/openai/request_handlers.py
@OpenAIRequestHandlerFactory.register("/v1/completions")
class TextCompletionsRequestHandler(OpenAIRequestHandler):
    """
    Request handler for OpenAI-style legacy completion endpoints.

    Processes responses from text completion APIs that return generated text in the
    'choices' array with 'text' fields. Handles both streaming and non-streaming
    responses, extracting usage metrics for input and output tokens.

    Example:
    ::
        handler = TextCompletionsResponseHandler()
        response = handler.compile_non_streaming(request, api_response)
    """

    def __init__(self):
        """
        Initialize the text completions response handler.

        Sets up internal state for accumulating streaming response data including
        text chunks and usage metrics.
        """
        self.streaming_texts: list[str] = []
        self.streaming_usage: dict[str, int | dict[str, int]] | None = None
        self.streaming_response_id: str | None = None

    @property
    def last_iteration_had_content(self) -> bool:
        """
        Text completions (``/v1/completions``) have no reasoning concept, so
        every token is content. ChatCompletionsRequestHandler and
        ResponsesRequestHandler override this with a tracked flag that starts
        ``False`` and only becomes ``True`` when a content or tool call delta
        arrives.

        :return: Always True for the text completions base class
        """
        return True

    def format(  # noqa: C901
        self,
        data: GenerationRequest,
        history: HistoryT[GenerationRequest, GenerationResponse] | None = None,
        **kwargs,
    ) -> GenerationRequestArguments:
        """
        Format the text completion generation request into the appropriate structure.

        :param data: The generation request to format
        :param **kwargs: Additional keyword arguments for request formatting
        :return: The formatted request arguments
        """
        arguments: GenerationRequestArguments = GenerationRequestArguments()
        arguments.body = {}  # The type checker works better setting this field here

        # Add model
        if kwargs.get("model") is not None:
            arguments.body["model"] = kwargs["model"]

        # Configure streaming
        if kwargs.get("stream"):
            arguments.stream = True
            arguments.body["stream"] = True
            arguments.body["stream_options"] = {
                "include_usage": True,
                "continuous_usage_stats": True,
            }

        # Handle output tokens
        if data.output_metrics.text_tokens:
            arguments.body["max_tokens"] = data.output_metrics.text_tokens
            arguments.body["stop"] = None
            arguments.body["ignore_eos"] = True
        elif kwargs.get("max_tokens") is not None:
            arguments.body["max_tokens"] = kwargs["max_tokens"]

        # Apply extra arguments
        if kwargs.get("extras"):
            arguments.model_combine(kwargs["extras"])

        ## Build prompt ##
        prompts: list[str] = []

        # Include history: previous prompts and their responses
        if history:
            for req, res in history:
                prompts.extend(req.columns.get("prefix_column", []))
                prompts.extend(req.columns.get("text_column", []))
                if res and res.text:
                    prompts.append(res.text)

        # Include prefix
        prompts.extend(data.columns.get("prefix_column", []))
        # Include text column
        prompts.extend(data.columns.get("text_column", []))

        if prompts:
            arguments.body["prompt"] = " ".join(prompts)

        return arguments

    def compile_non_streaming(
        self,
        request: GenerationRequest,
        arguments: GenerationRequestArguments,
        response: dict,
    ) -> GenerationResponse:
        """
        Process a complete text completion response.

        :param request: Original generation request
        :param response: Complete API response containing choices and usage data
        :return: Standardized GenerationResponse with extracted text and metrics
        """
        choices, usage = self.extract_choices_and_usage(response)
        choice = choices[0] if choices else {}
        text = choice.get("text", "")
        input_metrics, output_metrics = self.extract_metrics(usage, text)

        return GenerationResponse(
            request_id=request.request_id,
            request_args=arguments.model_dump_json(),
            response_id=response.get("id"),  # use vLLM ID if available
            text=text,
            input_metrics=input_metrics,
            output_metrics=output_metrics,
        )

    def add_streaming_line(self, line: str) -> int | None:
        """
        Process a single line from a text completion streaming response.

        Parses Server-Sent Events (SSE) formatted lines, extracting text content
        and usage metrics. Accumulates text chunks for final response compilation.

        :param line: Raw SSE line from the streaming response
        :return: 1 if text content was extracted, 0 if line ignored, None if done
        """
        if not (data := self.extract_line_data(line)):
            return None if data is None else 0

        if "id" in data and self.streaming_response_id is None:
            self.streaming_response_id = data["id"]

        updated = False
        choices, usage = self.extract_choices_and_usage(data)
        choice = choices[0] if choices else {}

        if choices and (text := choice.get("text")):
            self.streaming_texts.append(text)
            updated = True

        if usage:
            self.streaming_usage = usage

        return 1 if updated else 0

    def compile_streaming(
        self, request: GenerationRequest, arguments: GenerationRequestArguments
    ) -> GenerationResponse:
        """
        Compile accumulated streaming text chunks into a final response.

        :param request: Original generation request
        :return: Standardized GenerationResponse with concatenated text and metrics
        """
        text = "".join(self.streaming_texts)
        input_metrics, output_metrics = self.extract_metrics(self.streaming_usage, text)

        return GenerationResponse(
            request_id=request.request_id,
            request_args=arguments.model_dump_json(),
            response_id=self.streaming_response_id,  # use vLLM ID if available
            text=text,
            input_metrics=input_metrics,
            output_metrics=output_metrics,
        )

    def post_validation(self, response: GenerationResponse) -> None:
        """Reject responses with no text, tool calls, or output tokens."""
        _validate_text_response(response)

    def extract_line_data(self, line: str) -> dict[str, Any] | None:
        """
        Extract JSON data from a streaming response line.

        :param line: Raw line from the streaming response
        :return: Parsed JSON data as dictionary, or None if line indicates completion
        """
        if not line or not (line := line.strip()) or not line.startswith("data:"):
            return {}

        line = line[len("data:") :].strip()
        if line == "[DONE]":
            return None

        data = json.loads(line)
        _check_streaming_error(data)
        return data

    def extract_choices_and_usage(
        self, response: dict
    ) -> tuple[list[dict], dict[str, int | dict[str, int]]]:
        """
        Extract choices and usage data from the API response.

        :param response: Complete API response containing choices and usage data
        :return: Tuple of choices list and usage dictionary
        """
        return response.get("choices", []), response.get("usage", {})

    def extract_metrics(
        self, usage: dict[str, int | dict[str, int]] | None, text: str | None
    ) -> tuple[UsageMetrics, UsageMetrics]:
        """
        Extract input and output usage metrics from API response usage data.

        :param usage: Usage data dictionary from API response
        :param text: Generated text for calculating word and character counts.
            None means text is not applicable (metrics will be None);
            empty string means text was applicable but empty (metrics will be 0).
        :return: Tuple of input_metrics and output_metrics as UsageMetrics objects
        """
        if text is None:
            # text not applicable (e.g. tool-call-only) — exclude from aggregation
            text_words = None
            text_chars = None
        else:
            text_words = len(text.split())
            text_chars = len(text)

        if not usage:
            return UsageMetrics(), UsageMetrics(
                text_words=text_words,
                text_characters=text_chars,
            )

        input_details: dict[str, int] = cast(
            "dict[str, int]", usage.get("prompt_tokens_details", {}) or {}
        )
        output_details: dict[str, int] = cast(
            "dict[str, int]", usage.get("completion_tokens_details", {}) or {}
        )
        usage_metrics: dict[str, int] = cast("dict[str, int]", usage)

        return UsageMetrics(
            text_tokens=(
                input_details.get("prompt_tokens")
                or usage_metrics.get("prompt_tokens")
                or 0
            ),
            image_tokens=input_details.get("image_tokens"),
            video_tokens=input_details.get("video_tokens"),
            audio_tokens=input_details.get("audio_tokens"),
            audio_seconds=input_details.get("seconds"),
        ), UsageMetrics(
            text_tokens=(
                output_details.get("completion_tokens")
                or usage_metrics.get("completion_tokens")
                or 0
            ),
            text_words=text_words,
            text_characters=text_chars,
            image_tokens=output_details.get("image_tokens"),
            video_tokens=output_details.get("video_tokens"),
            audio_tokens=output_details.get("audio_tokens"),
            audio_seconds=output_details.get("seconds"),
        )

last_iteration_had_content property

Text completions (/v1/completions) have no reasoning concept, so every token is content. ChatCompletionsRequestHandler and ResponsesRequestHandler override this with a tracked flag that starts False and only becomes True when a content or tool call delta arrives.

Returns:

Type Description
bool

Always True for the text completions base class

__init__()

Initialize the text completions response handler.

Sets up internal state for accumulating streaming response data including text chunks and usage metrics.

Source code in src/guidellm/backends/openai/request_handlers.py
def __init__(self):
    """
    Initialize the text completions response handler.

    Sets up internal state for accumulating streaming response data including
    text chunks and usage metrics.
    """
    self.streaming_texts: list[str] = []
    self.streaming_usage: dict[str, int | dict[str, int]] | None = None
    self.streaming_response_id: str | None = None

add_streaming_line(line)

Process a single line from a text completion streaming response.

Parses Server-Sent Events (SSE) formatted lines, extracting text content and usage metrics. Accumulates text chunks for final response compilation.

Parameters:

Name Type Description Default
line str

Raw SSE line from the streaming response

required

Returns:

Type Description
int | None

1 if text content was extracted, 0 if line ignored, None if done

Source code in src/guidellm/backends/openai/request_handlers.py
def add_streaming_line(self, line: str) -> int | None:
    """
    Process a single line from a text completion streaming response.

    Parses Server-Sent Events (SSE) formatted lines, extracting text content
    and usage metrics. Accumulates text chunks for final response compilation.

    :param line: Raw SSE line from the streaming response
    :return: 1 if text content was extracted, 0 if line ignored, None if done
    """
    if not (data := self.extract_line_data(line)):
        return None if data is None else 0

    if "id" in data and self.streaming_response_id is None:
        self.streaming_response_id = data["id"]

    updated = False
    choices, usage = self.extract_choices_and_usage(data)
    choice = choices[0] if choices else {}

    if choices and (text := choice.get("text")):
        self.streaming_texts.append(text)
        updated = True

    if usage:
        self.streaming_usage = usage

    return 1 if updated else 0

compile_non_streaming(request, arguments, response)

Process a complete text completion response.

Parameters:

Name Type Description Default
request GenerationRequest

Original generation request

required
response dict

Complete API response containing choices and usage data

required

Returns:

Type Description
GenerationResponse

Standardized GenerationResponse with extracted text and metrics

Source code in src/guidellm/backends/openai/request_handlers.py
def compile_non_streaming(
    self,
    request: GenerationRequest,
    arguments: GenerationRequestArguments,
    response: dict,
) -> GenerationResponse:
    """
    Process a complete text completion response.

    :param request: Original generation request
    :param response: Complete API response containing choices and usage data
    :return: Standardized GenerationResponse with extracted text and metrics
    """
    choices, usage = self.extract_choices_and_usage(response)
    choice = choices[0] if choices else {}
    text = choice.get("text", "")
    input_metrics, output_metrics = self.extract_metrics(usage, text)

    return GenerationResponse(
        request_id=request.request_id,
        request_args=arguments.model_dump_json(),
        response_id=response.get("id"),  # use vLLM ID if available
        text=text,
        input_metrics=input_metrics,
        output_metrics=output_metrics,
    )

compile_streaming(request, arguments)

Compile accumulated streaming text chunks into a final response.

Parameters:

Name Type Description Default
request GenerationRequest

Original generation request

required

Returns:

Type Description
GenerationResponse

Standardized GenerationResponse with concatenated text and metrics

Source code in src/guidellm/backends/openai/request_handlers.py
def compile_streaming(
    self, request: GenerationRequest, arguments: GenerationRequestArguments
) -> GenerationResponse:
    """
    Compile accumulated streaming text chunks into a final response.

    :param request: Original generation request
    :return: Standardized GenerationResponse with concatenated text and metrics
    """
    text = "".join(self.streaming_texts)
    input_metrics, output_metrics = self.extract_metrics(self.streaming_usage, text)

    return GenerationResponse(
        request_id=request.request_id,
        request_args=arguments.model_dump_json(),
        response_id=self.streaming_response_id,  # use vLLM ID if available
        text=text,
        input_metrics=input_metrics,
        output_metrics=output_metrics,
    )

extract_choices_and_usage(response)

Extract choices and usage data from the API response.

Parameters:

Name Type Description Default
response dict

Complete API response containing choices and usage data

required

Returns:

Type Description
tuple[list[dict], dict[str, int | dict[str, int]]]

Tuple of choices list and usage dictionary

Source code in src/guidellm/backends/openai/request_handlers.py
def extract_choices_and_usage(
    self, response: dict
) -> tuple[list[dict], dict[str, int | dict[str, int]]]:
    """
    Extract choices and usage data from the API response.

    :param response: Complete API response containing choices and usage data
    :return: Tuple of choices list and usage dictionary
    """
    return response.get("choices", []), response.get("usage", {})

extract_line_data(line)

Extract JSON data from a streaming response line.

Parameters:

Name Type Description Default
line str

Raw line from the streaming response

required

Returns:

Type Description
dict[str, Any] | None

Parsed JSON data as dictionary, or None if line indicates completion

Source code in src/guidellm/backends/openai/request_handlers.py
def extract_line_data(self, line: str) -> dict[str, Any] | None:
    """
    Extract JSON data from a streaming response line.

    :param line: Raw line from the streaming response
    :return: Parsed JSON data as dictionary, or None if line indicates completion
    """
    if not line or not (line := line.strip()) or not line.startswith("data:"):
        return {}

    line = line[len("data:") :].strip()
    if line == "[DONE]":
        return None

    data = json.loads(line)
    _check_streaming_error(data)
    return data

extract_metrics(usage, text)

Extract input and output usage metrics from API response usage data.

Parameters:

Name Type Description Default
usage dict[str, int | dict[str, int]] | None

Usage data dictionary from API response

required
text str | None

Generated text for calculating word and character counts. None means text is not applicable (metrics will be None); empty string means text was applicable but empty (metrics will be 0).

required

Returns:

Type Description
tuple[UsageMetrics, UsageMetrics]

Tuple of input_metrics and output_metrics as UsageMetrics objects

Source code in src/guidellm/backends/openai/request_handlers.py
def extract_metrics(
    self, usage: dict[str, int | dict[str, int]] | None, text: str | None
) -> tuple[UsageMetrics, UsageMetrics]:
    """
    Extract input and output usage metrics from API response usage data.

    :param usage: Usage data dictionary from API response
    :param text: Generated text for calculating word and character counts.
        None means text is not applicable (metrics will be None);
        empty string means text was applicable but empty (metrics will be 0).
    :return: Tuple of input_metrics and output_metrics as UsageMetrics objects
    """
    if text is None:
        # text not applicable (e.g. tool-call-only) — exclude from aggregation
        text_words = None
        text_chars = None
    else:
        text_words = len(text.split())
        text_chars = len(text)

    if not usage:
        return UsageMetrics(), UsageMetrics(
            text_words=text_words,
            text_characters=text_chars,
        )

    input_details: dict[str, int] = cast(
        "dict[str, int]", usage.get("prompt_tokens_details", {}) or {}
    )
    output_details: dict[str, int] = cast(
        "dict[str, int]", usage.get("completion_tokens_details", {}) or {}
    )
    usage_metrics: dict[str, int] = cast("dict[str, int]", usage)

    return UsageMetrics(
        text_tokens=(
            input_details.get("prompt_tokens")
            or usage_metrics.get("prompt_tokens")
            or 0
        ),
        image_tokens=input_details.get("image_tokens"),
        video_tokens=input_details.get("video_tokens"),
        audio_tokens=input_details.get("audio_tokens"),
        audio_seconds=input_details.get("seconds"),
    ), UsageMetrics(
        text_tokens=(
            output_details.get("completion_tokens")
            or usage_metrics.get("completion_tokens")
            or 0
        ),
        text_words=text_words,
        text_characters=text_chars,
        image_tokens=output_details.get("image_tokens"),
        video_tokens=output_details.get("video_tokens"),
        audio_tokens=output_details.get("audio_tokens"),
        audio_seconds=output_details.get("seconds"),
    )

format(data, history=None, **kwargs)

Format the text completion generation request into the appropriate structure.

Parameters:

Name Type Description Default
data GenerationRequest

The generation request to format

required
**kwargs

Additional keyword arguments for request formatting

{}

Returns:

Type Description
GenerationRequestArguments

The formatted request arguments

Source code in src/guidellm/backends/openai/request_handlers.py
def format(  # noqa: C901
    self,
    data: GenerationRequest,
    history: HistoryT[GenerationRequest, GenerationResponse] | None = None,
    **kwargs,
) -> GenerationRequestArguments:
    """
    Format the text completion generation request into the appropriate structure.

    :param data: The generation request to format
    :param **kwargs: Additional keyword arguments for request formatting
    :return: The formatted request arguments
    """
    arguments: GenerationRequestArguments = GenerationRequestArguments()
    arguments.body = {}  # The type checker works better setting this field here

    # Add model
    if kwargs.get("model") is not None:
        arguments.body["model"] = kwargs["model"]

    # Configure streaming
    if kwargs.get("stream"):
        arguments.stream = True
        arguments.body["stream"] = True
        arguments.body["stream_options"] = {
            "include_usage": True,
            "continuous_usage_stats": True,
        }

    # Handle output tokens
    if data.output_metrics.text_tokens:
        arguments.body["max_tokens"] = data.output_metrics.text_tokens
        arguments.body["stop"] = None
        arguments.body["ignore_eos"] = True
    elif kwargs.get("max_tokens") is not None:
        arguments.body["max_tokens"] = kwargs["max_tokens"]

    # Apply extra arguments
    if kwargs.get("extras"):
        arguments.model_combine(kwargs["extras"])

    ## Build prompt ##
    prompts: list[str] = []

    # Include history: previous prompts and their responses
    if history:
        for req, res in history:
            prompts.extend(req.columns.get("prefix_column", []))
            prompts.extend(req.columns.get("text_column", []))
            if res and res.text:
                prompts.append(res.text)

    # Include prefix
    prompts.extend(data.columns.get("prefix_column", []))
    # Include text column
    prompts.extend(data.columns.get("text_column", []))

    if prompts:
        arguments.body["prompt"] = " ".join(prompts)

    return arguments

post_validation(response)

Reject responses with no text, tool calls, or output tokens.

Source code in src/guidellm/backends/openai/request_handlers.py
def post_validation(self, response: GenerationResponse) -> None:
    """Reject responses with no text, tool calls, or output tokens."""
    _validate_text_response(response)

VLLMPythonBackend

Bases: Backend

Python API backend for VLLM inference engine.

Engine parameters not set in vllm_config use vLLM's AsyncEngineArgs defaults. Example: :: backend = VLLMPythonBackend(model="meta-llama/Llama-2-7b-chat-hf") # Or: vllm_config={"tensor_parallel_size": 1, "gpu_memory_utilization": 0.9}

await backend.process_startup()
async for response, request_info in backend.resolve(request, info):
    process_response(response)
await backend.process_shutdown()
Source code in src/guidellm/backends/vllm_python/vllm.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
@Backend.register("vllm_python")
class VLLMPythonBackend(Backend):
    """
    Python API backend for VLLM inference engine.

    Engine parameters not set in vllm_config use vLLM's AsyncEngineArgs defaults.
    Example:
    ::
        backend = VLLMPythonBackend(model="meta-llama/Llama-2-7b-chat-hf")
        # Or: vllm_config={"tensor_parallel_size": 1, "gpu_memory_utilization": 0.9}

        await backend.process_startup()
        async for response, request_info in backend.resolve(request, info):
            process_response(response)
        await backend.process_shutdown()
    """

    _args: VLLMPythonBackendArgs

    @classmethod
    def backend_args(cls) -> type[BackendArgs]:
        """Return the Pydantic model for this backend's creation arguments."""
        return VLLMPythonBackendArgs

    def __init__(
        self,
        arguments: VLLMPythonBackendArgs,
    ):
        """
        Initialize VLLM Python backend with model and configuration.
        """
        super().__init__(arguments)

        # Runtime state
        self._in_process = False
        self._engine: vllm.AsyncLLMEngine | None = None
        self._resolved_chat_template: str | None | object = _CHAT_TEMPLATE_UNSET

    @property
    def processes_limit(self) -> int | None:
        """
        Limits VLLM Python to a single process, since it starts up an entire
        VLLM engine.
        """
        return 1

    async def process_startup(self):
        """
        Initialize VLLM AsyncLLMEngine instance with configured parameters.

        :raises RuntimeError: If backend is already initialized
        """
        if self._in_process:
            raise RuntimeError("Backend already started up for process.")

        engine_args = vllm.AsyncEngineArgs(**self._args.vllm_config)
        self._engine = vllm.AsyncLLMEngine.from_engine_args(engine_args)
        self._in_process = True

    async def process_shutdown(self):
        """
        Clean up VLLM AsyncLLMEngine instance and resources.

        :raises RuntimeError: If backend was not properly initialized
        """
        if not self._in_process:
            raise RuntimeError("Backend not started up for process.")

        if self._engine is not None:
            self._engine.shutdown()
            self._engine = None
        self._in_process = False

    async def validate(self):
        """
        Validate backend readiness by attempting a test generation.

        :raises RuntimeError: If backend cannot generate or is not initialized
        """
        engine = self._validate_backend_initialized()

        try:
            await engine.check_health()
        except BaseException as exc:
            raise RuntimeError(
                "Backend validation failed. Health check failed."
            ) from exc

    async def available_models(self) -> list[str]:
        """
        Get available models from this backend.

        :return: List containing the configured model identifier
        """
        # VLLM only supports one model per VLLM instance.
        return [self._args.model]

    async def default_model(self) -> str:
        """
        Get the default model for this backend.

        :return: Model name or identifier
        """
        return self._args.model

    def _validate_backend_initialized(self) -> vllm.AsyncLLMEngine:
        """
        Validate that the backend is initialized and return the engine.

        :raises RuntimeError: If backend is not initialized
        :return: The initialized AsyncLLMEngine
        """
        if self._engine is None:
            raise RuntimeError("Backend not started up for process.")
        return self._engine

    def _validate_history(
        self, history: list[tuple[GenerationRequest, GenerationResponse]] | None
    ) -> None:
        """
        Validate that history is not provided (not yet supported).

        :param history: Conversation history
        :raises NotImplementedError: If history is provided
        """
        if history is not None:
            raise NotImplementedError("Multi-turn requests not yet supported")

    def _build_multi_modal_data_from_columns(  # noqa: C901, PLR0912
        self, columns: dict[str, Any]
    ) -> dict[str, Any] | None:
        """
        Build vLLM multi_modal_data dict from image_column, audio_column.

        video_column is not yet supported (no frame extraction); it is skipped.
        """
        multi_modal_data: dict[str, Any] = {}
        # We look specifically for "image_column" and "audio_column" which contain lists
        # of dicts
        image_items = columns.get("image_column", [])
        audio_items = columns.get("audio_column", [])
        # video_column: not yet supported; would require frame extraction
        for item in image_items:
            if not item or not isinstance(item, dict):
                continue
            # Convert raw image dicts into PIL Images as required by vLLM's vision
            # processor
            pil_image = vision.image_dict_to_pil(item)
            if "image" not in multi_modal_data:
                multi_modal_data["image"] = pil_image
            else:
                # If multiple images exist, vLLM expects a list of PIL Images
                existing = multi_modal_data["image"]
                if isinstance(existing, list):
                    existing.append(pil_image)
                else:
                    multi_modal_data["image"] = [existing, pil_image]
        if audio_items:
            if len(audio_items) > 1:
                logger.warning(
                    "Only one audio item per request is supported; "
                    "ignoring {} extra audio item(s).",
                    len(audio_items) - 1,
                )
            first = audio_items[0]
            if not first or not isinstance(first, dict):
                logger.warning("audio_column item is empty or not a dict; skipping.")
            else:
                audio_bytes = first.get("audio")
                if isinstance(audio_bytes, bytes) and len(audio_bytes) > 0:
                    try:
                        # Decode raw audio bytes into an array since vLLM audio models
                        # expect either raw numpy arrays or specific tensor formats
                        audio_data = audio.decode_audio(audio_bytes)
                        multi_modal_data["audio"] = audio_data
                    except (ValueError, TypeError, OSError, RuntimeError) as exc:
                        raise ValueError(
                            f"Failed to decode audio from audio_column for vLLM: {exc}"
                        ) from exc
        return multi_modal_data if multi_modal_data else None

    def _extract_text_from_content(
        self, content: str | list[dict[str, Any]] | Any
    ) -> str:
        """
        Extract text content from message content field.

        Handles both string content and list-based multimodal content blocks.
        For list-based content, extracts text from blocks with type "text" and
        concatenates them together.

        :param content: Content field which can be a string or list of content blocks
        :return: Extracted text string
        """
        if isinstance(content, str):
            return content
        if isinstance(content, list):
            # Extract text from content blocks with type "text"
            text_parts = []
            for block in content:
                if isinstance(block, dict):
                    block_type = block.get("type")
                    if block_type == "text":
                        text = block.get("text")
                        if text:
                            text_parts.append(text)
            return "".join(text_parts)
        # Fallback: convert to string
        return str(content) if content is not None else ""

    def _build_placeholder_prefix(self, multi_modal_data: dict[str, Any]) -> str:
        """
        Build the placeholder prefix string for all modalities in
        multi_modal_data.

        Returns a string like ``"<image>\\n<|audio|>\\n"`` with one
        placeholder per item, or ``""`` if no multimodal items are
        present.  Placeholder tokens default to ``<image>`` and
        ``<|audio|>`` but can be overridden via
        ``image_placeholder`` / ``audio_placeholder`` at construction.
        """
        parts: list[str] = []
        images = multi_modal_data.get("image")
        if images is not None:
            num = len(images) if isinstance(images, list | tuple) else 1
            if num > 0:
                ph = self._args.image_placeholder
                parts.extend([ph] * num)
        audio = multi_modal_data.get("audio")
        if audio is not None:
            # Single audio item (numpy array) — not a list of items.
            num = len(audio) if isinstance(audio, list | tuple) else 1
            if num > 0:
                ph = self._args.audio_placeholder
                parts.extend([ph] * num)
        if not parts:
            return ""
        return "\n".join(parts) + "\n"

    @staticmethod
    def _format_column_blocks(
        column_data: list[Any], column_type: str
    ) -> list[dict[str, Any]]:
        """Format data column items into vLLM-compatible content blocks.

        Analogous to the HTTP backend's ``_format_prompts`` but emitting
        vLLM-specific block types that chat templates can render into the
        correct model-specific placeholder tokens.
        """
        blocks: list[dict[str, Any]] = []
        for item in column_data:
            if not item:
                continue
            if column_type == "text_column":
                blocks.append({"type": "text", "text": str(item)})
            elif column_type == "image_column":
                blocks.append({"type": "image"})
            elif column_type == "audio_column":
                blocks.append({"type": "audio"})
        return blocks

    def _inject_placeholders_into_messages(
        self,
        formatted_messages: list[dict[str, Any]],
        multi_modal_data: dict[str, Any],
    ) -> None:
        """
        Inject multimodal placeholder tokens into the last user message's content.

        vLLM requires one placeholder per multimodal item in the prompt text so its
        processor can apply prompt replacement. This must happen *before* the chat
        template is applied so that placeholders end up inside the correct message
        turn (not prepended to the entire formatted prompt).
        """
        prefix = self._build_placeholder_prefix(multi_modal_data)
        if not prefix:
            return
        for msg in reversed(formatted_messages):
            if msg.get("role") == "user":
                msg["content"] = prefix + (msg.get("content") or "")
                return
        if formatted_messages:
            formatted_messages[-1]["content"] = prefix + (
                formatted_messages[-1].get("content") or ""
            )

    def _extract_prompt_chat_plain(
        self, formatted_messages: list[dict[str, Any]]
    ) -> str:
        """Concatenate message content into a single raw prompt string.

        Equivalent to the HTTP /v1/completions behaviour: prefix + text
        with no role prefixes or trailing generation prompt.
        """
        return " ".join(
            msg["content"] for msg in formatted_messages if msg.get("content")
        )

    def _resolve_chat_template(self) -> str | None:
        """
        Resolve and validate request_format to a template string or None.

        Returns None for default tokenizer template; returns the template string
        when valid. Raises ValueError for invalid input (wrong format, bad path,
        or invalid Jinja2 syntax).
        """
        template = self._args.request_format
        if template in (
            "plain",
            "default-template",
        ):
            # No custom template provided; 'plain' and 'default-template' are handled
            # internally
            return None
        path = Path(template)
        # Treat the request_format string as a file path. If it exists and contains
        # Jinja2 syntax, read the content as the template.
        if path.exists() and path.is_file():
            content = path.read_text()
            if not _has_jinja2_markers(content):
                raise ValueError(
                    "Invalid chat template: path "
                    f"{path.as_posix()!r} exists but file content does not "
                    "contain Jinja2 template syntax ({{, {%}, or {#})."
                )
            try:
                jinja2.Template(content)
            except jinja2.TemplateSyntaxError as e:
                raise ValueError(
                    f"Invalid chat template in file {path.as_posix()!r}: {e}"
                ) from e
            return content
        if _has_jinja2_markers(template):
            try:
                jinja2.Template(template)
            except jinja2.TemplateSyntaxError as e:
                raise ValueError(f"Invalid chat template: {e}") from e
            return template
        raise ValueError(
            "request_format must be 'plain', 'default-template', a path to a "
            "Jinja2 template file, or a string containing Jinja2 template "
            "syntax ({{, {%}, or {#). Got: " + repr(template) + "."
        )

    def _extract_prompt_chat_tokenizer(
        self, formatted_messages: list[dict[str, Any]]
    ) -> str:
        """Apply tokenizer chat template to formatted messages."""
        engine = self._validate_backend_initialized()
        tokenizer = engine.tokenizer
        if tokenizer is None:
            raise RuntimeError("Backend engine has no tokenizer.")

        if self._args.request_format in (
            "plain",
            "default-template",
        ):
            resolved: str | None = None
        else:
            if self._resolved_chat_template is _CHAT_TEMPLATE_UNSET:
                self._resolved_chat_template = self._resolve_chat_template()
            resolved = cast("str | None", self._resolved_chat_template)
        if resolved is not None:
            # Safe to mutate: vLLM runs one model per engine and the resolved
            # template is constant across all requests for this backend instance.
            tokenizer.chat_template = resolved  # type: ignore[attr-defined]
        prompt = tokenizer.apply_chat_template(
            formatted_messages,  # type: ignore[arg-type]
            tokenize=False,
            add_generation_prompt=True,
        )
        if isinstance(prompt, str):
            return prompt
        raise RuntimeError("Backend received unexpected type from tokenizer.")

    def _resolve_request(self, request: GenerationRequest) -> _ResolvedRequest:
        """
        Build a fully resolved request from column-based GenerationRequest.

        Mirrors the HTTP backend's ``ChatCompletionsRequestHandler.format``:
        prefix items are space-joined into one system message and all data
        columns (text, image, audio) are formatted as typed content blocks
        then interleaved via ``roundrobin`` into a single user message.

        When a chat template is active and multimodal data is present, the
        list-of-blocks content is passed directly to the tokenizer so the
        template emits model-specific placeholder tokens.  For plain format
        or text-only requests the content is flattened to strings.

        :param request: Column-based generation request
        :return: Resolved request with formatted prompt and multimodal data
        :raises ValueError: If request has no text or multimodal columns
        """
        columns = request.columns

        messages: list[dict[str, Any]] = []

        prefix = " ".join(str(p) for p in columns.get("prefix_column", []) if p)
        if prefix:
            messages.append({"role": "system", "content": prefix})

        text_blocks = self._format_column_blocks(
            columns.get("text_column", []), "text_column"
        )

        multi_modal_data = self._build_multi_modal_data_from_columns(columns)

        # We use explicit content blocks (e.g. {"type": "image"}) when applying a
        # chat template so that the template itself can generate the correct,
        # model-specific tokens. Otherwise, we flatten to strings and fall back
        # to placeholder-string injection.
        use_content_blocks = (
            multi_modal_data
            and (text_blocks or prefix)
            and self._args.request_format != "plain"
        )

        if use_content_blocks:
            # Interleave text and media blocks into a single content list,
            # matching the HTTP backend's roundrobin approach.
            media_lists = [
                self._format_column_blocks(columns.get(col, []), col)
                for col in ("image_column", "audio_column")
            ]
            user_content: list[dict[str, Any]] = list(
                roundrobin(text_blocks, *media_lists)
            )
        else:
            # Text-only or plain mode: media is handled later via placeholder
            # injection, so only text blocks go into the user message here.
            user_content = list(text_blocks)

        if user_content:
            messages.append({"role": "user", "content": user_content})

        if messages:
            if use_content_blocks:
                prompt = self._extract_prompt_chat_tokenizer(messages)
            else:
                formatted_messages = [
                    {
                        "role": msg["role"],
                        "content": self._extract_text_from_content(
                            msg.get("content", "")
                        ),
                    }
                    for msg in messages
                ]

                if multi_modal_data:
                    # Placeholders must be injected into the message text
                    # *before* the chat template is applied so they end up
                    # inside the correct message turn.
                    self._inject_placeholders_into_messages(
                        formatted_messages, multi_modal_data
                    )

                if self._args.request_format == "plain":
                    prompt = self._extract_prompt_chat_plain(formatted_messages)
                else:
                    prompt = self._extract_prompt_chat_tokenizer(formatted_messages)
        elif multi_modal_data:
            # Multimodal-only (e.g. audio transcription with no text/prefix):
            # no messages to inject into, so use a raw placeholder prompt.
            prompt = self._build_placeholder_prefix(multi_modal_data)
        else:
            raise ValueError("Request must include text_column or multimodal columns.")

        return _ResolvedRequest(
            prompt=prompt,
            stream=self._args.stream,
            multi_modal_data=multi_modal_data,
        )

    def _update_request_timing(
        self, request_info: RequestInfo, iter_time: float
    ) -> None:
        """
        Update request iteration timing information.

        :param request_info: Request tracking info to update
        :param iter_time: Current iteration time
        """
        if request_info.timings.first_request_iteration is None:
            request_info.timings.first_request_iteration = iter_time
        request_info.timings.last_request_iteration = iter_time
        request_info.timings.request_iterations += 1

    def _update_token_timing(
        self, request_info: RequestInfo, iter_time: float, iterations: int = 1
    ) -> None:
        """
        Update token iteration timing information.

        :param request_info: Request tracking info to update
        :param iter_time: Current iteration time
        :param iterations: Number of token iterations (default: 1)
        """
        if request_info.timings.first_token_iteration is None:
            request_info.timings.first_token_iteration = iter_time
        request_info.timings.last_token_iteration = iter_time
        request_info.timings.token_iterations += iterations

    def _text_from_output(self, output: vllm.RequestOutput | None) -> str:
        """
        Extract generated text from VLLM RequestOutput.

        :param output: VLLM request output (may be None)
        :return: output.outputs[0].text if present, else ""
        """
        if output is None or not output.outputs:
            return ""
        return output.outputs[0].text or ""

    def _stream_usage_tokens(
        self,
        output: vllm.RequestOutput,
        request_info: RequestInfo,
    ) -> tuple[int, int]:
        """
        Compute token counts for the streaming path.

        Primary source is output.outputs[0].token_ids (always populated by vLLM).
        Falls back to token_iterations if token_ids is unexpectedly absent.

        :return: (input_tokens, output_tokens)
        """
        input_tokens = len(output.prompt_token_ids or [])
        output_tokens = 0

        if output.outputs:
            out = output.outputs[0]
            if out.token_ids is not None:
                output_tokens = len(out.token_ids)

        if output_tokens == 0 and request_info.timings.token_iterations:
            output_tokens = request_info.timings.token_iterations

        return input_tokens, output_tokens

    def _usage_from_output(
        self,
        output: vllm.RequestOutput | None,
        *,
        request_info: RequestInfo | None = None,
    ) -> dict[str, int] | None:
        """
        Build usage dict (prompt_tokens, completion_tokens, total_tokens).

        When request_info is None (non-stream), uses only output token counts.
        When request_info is set (stream), uses _stream_usage_tokens.
        """
        if output is None:
            return None
        input_tokens = len(output.prompt_token_ids or [])
        output_tokens = 0

        if request_info is None:
            if output.outputs:
                out = output.outputs[0]
                if out.token_ids is not None:
                    output_tokens = len(out.token_ids)
        else:
            input_tokens, output_tokens = self._stream_usage_tokens(
                output, request_info
            )

        return {
            "prompt_tokens": input_tokens,
            "completion_tokens": output_tokens,
            "total_tokens": input_tokens + output_tokens,
        }

    def _build_final_response(
        self,
        request: GenerationRequest,
        request_info: RequestInfo,
        final_output: vllm.RequestOutput | None,
        stream: bool,
        text: str = "",
    ) -> tuple[GenerationResponse, RequestInfo] | None:
        """Build and return the final (response, request_info) to yield, or None."""
        if final_output is None:
            return None
        # Use provided text (e.g. from cancel) or final vLLM output.
        final_text = text or self._text_from_output(final_output)
        # Text and usage from final output only; pass 0 for accumulated tokens.
        usage = self._usage_from_output(
            final_output,
            request_info=request_info if stream else None,
        )
        # We do not send streaming lines during the stream loop, so the handler
        # never receives an id from the stream. Set it from the final vLLM output
        # so the response has a usable id.
        response_id = final_output.request_id if final_output.request_id else None
        # Build response with final text only (no streamed chunks).
        response = VLLMResponseHandler.build_response(
            request, final_text, usage, response_id=response_id
        )
        return response, request_info

    def _create_sampling_params(
        self,
        max_tokens_override: int | None = None,
    ) -> vllm.SamplingParams:
        """
        Create VLLM SamplingParams.

        When max_tokens_override is set (from benchmark output_metrics), it is used
        as max_tokens and EOS is ignored to force generation of exactly that many
        tokens, matching HTTP backend behavior. Otherwise vLLM defaults are used
        (generate until EOS or model max context).

        :param max_tokens_override: Optional max_tokens from request (e.g. benchmark)
        :return: Configured SamplingParams instance
        """
        params: dict[str, Any] = {}

        if max_tokens_override is not None and max_tokens_override > 0:
            params["max_tokens"] = max_tokens_override
            params["ignore_eos"] = True

        return vllm.SamplingParams(**params)

    def _raise_generation_error(self, exc: BaseException) -> None:
        """Re-raise generation failure with context.

        Special-cases audio and engine-death errors.
        """
        error_msg = str(exc)
        # vLLM EngineDeadError: engine core subprocess died
        # (root cause is usually earlier in logs)
        if "EngineCore encountered an issue" in error_msg or (
            exc.__cause__ is not None
            and "EngineCore encountered an issue" in str(exc.__cause__)
        ):
            raise RuntimeError(
                "Generation failed: The vLLM engine core process "
                "has stopped. The failure usually happens during "
                "engine startup or the first request; the root "
                "cause is logged above by the engine core (look "
                "for tracebacks or errors before this message). "
                "Run with a single worker (e.g. --max-workers 1)"
                " and check stderr, or set "
                "GUIDELLM__LOGGING__CONSOLE_LOG_LEVEL=DEBUG. "
                "Common causes: OOM during model load, "
                "unsupported ops (e.g. on CPU/MPS), model load "
                "failure, or model/vLLM compatibility (e.g. "
                "tensor shape errors in rotary_embedding or "
                "attention). For the latter, try the legacy "
                "engine: VLLM_USE_V1=0. Original error: "
                f"{exc}"
            ) from exc
        if "At most 0 audio" in error_msg or "audio(s) may be provided" in error_msg:
            raise RuntimeError(
                f"Generation failed: The model '{self._args.model}' does not "
                f"support audio inputs. Use an audio-capable model "
                f"(e.g. Whisper-based). Original error: {exc}"
            ) from exc
        raise RuntimeError(f"Generation failed: {exc}") from exc

    async def _run_generation(
        self,
        request: GenerationRequest,
        request_info: RequestInfo,
        stream: bool,
        generate_input: str | dict[str, Any],
        sampling_params: vllm.SamplingParams,
        request_id: str,
        state: dict[str, Any],
    ) -> AsyncIterator[tuple[GenerationResponse, RequestInfo]]:
        """Run engine.generate loop and yield final (response, request_info)."""
        state["final_output"] = None
        engine = self._validate_backend_initialized()

        prompt_arg = cast("Any", generate_input)
        try:
            async for request_output in engine.generate(  # type: ignore[misc]
                prompt_arg, sampling_params, request_id
            ):
                iter_time = time.time()
                self._update_request_timing(request_info, iter_time)
                state["final_output"] = request_output

                if (
                    request_output.outputs
                    and request_output.outputs[0].finish_reason is not None
                ):
                    break

                if stream:
                    # Streaming gives more granular timing
                    # information that can be saved here.
                    self._update_token_timing(request_info, iter_time, 1)
        except Exception as e:
            logger.debug(
                "vLLM engine.generate() failed: {}: {}",
                type(e).__name__,
                e,
                exc_info=True,
            )
            raise RuntimeError(
                f"vLLM generation failed: {type(e).__name__}: {e}"
            ) from e

        request_info.timings.request_end = time.time()
        final_output = state["final_output"]
        result = self._build_final_response(
            request,
            request_info,
            final_output,
            stream,
            self._text_from_output(final_output) if final_output else "",
        )
        if result is not None:
            yield result

    async def resolve(  # type: ignore[override, misc]
        self,
        request: GenerationRequest,
        request_info: RequestInfo,
        history: list[tuple[GenerationRequest, GenerationResponse]] | None = None,
    ) -> AsyncIterator[tuple[GenerationResponse, RequestInfo]]:
        """
        Process generation request and yield a single response.

        Resolves the request (chat template, placeholders, multimodal data),
        then runs async generation via AsyncLLMEngine. For streaming, records
        per-token timings. In both cases the caller receives exactly one
        (response, request_info) pair.

        :param request: Generation request with content and parameters
        :param request_info: Request tracking info updated with timing metadata
        :param history: Conversation history (currently not supported)
        :raises NotImplementedError: If history is provided
        :raises RuntimeError: If backend is not initialized
        :yields: Single tuple of (response, updated_request_info)
        """
        self._validate_backend_initialized()
        self._validate_history(history)

        resolved = self._resolve_request(request)
        sampling_params = self._create_sampling_params(
            max_tokens_override=(
                request.output_metrics.text_tokens
                if request.output_metrics.text_tokens
                else None
            ),
        )

        generate_input: str | dict[str, Any]
        if resolved.multi_modal_data:
            generate_input = {
                "prompt": resolved.prompt,
                "multi_modal_data": resolved.multi_modal_data,
            }
        else:
            generate_input = resolved.prompt

        request_id = str(uuid.uuid4())
        request_info.timings.request_start = time.time()

        gen_state: dict[str, Any] = {}
        try:
            async for result in self._run_generation(
                request,
                request_info,
                resolved.stream,
                generate_input,
                sampling_params,
                request_id,
                gen_state,
            ):
                yield result
        except asyncio.CancelledError as err:
            final_output = gen_state.get("final_output")
            cancel_result = self._build_final_response(
                request,
                request_info,
                final_output,
                resolved.stream,
                self._text_from_output(final_output) if final_output else "",
            )
            if cancel_result is not None:
                yield cancel_result
            raise err
        except (RuntimeError, ValueError, TypeError, OSError, KeyError) as exc:
            self._raise_generation_error(exc)

processes_limit property

Limits VLLM Python to a single process, since it starts up an entire VLLM engine.

__init__(arguments)

Initialize VLLM Python backend with model and configuration.

Source code in src/guidellm/backends/vllm_python/vllm.py
def __init__(
    self,
    arguments: VLLMPythonBackendArgs,
):
    """
    Initialize VLLM Python backend with model and configuration.
    """
    super().__init__(arguments)

    # Runtime state
    self._in_process = False
    self._engine: vllm.AsyncLLMEngine | None = None
    self._resolved_chat_template: str | None | object = _CHAT_TEMPLATE_UNSET

available_models() async

Get available models from this backend.

Returns:

Type Description
list[str]

List containing the configured model identifier

Source code in src/guidellm/backends/vllm_python/vllm.py
async def available_models(self) -> list[str]:
    """
    Get available models from this backend.

    :return: List containing the configured model identifier
    """
    # VLLM only supports one model per VLLM instance.
    return [self._args.model]

backend_args() classmethod

Return the Pydantic model for this backend's creation arguments.

Source code in src/guidellm/backends/vllm_python/vllm.py
@classmethod
def backend_args(cls) -> type[BackendArgs]:
    """Return the Pydantic model for this backend's creation arguments."""
    return VLLMPythonBackendArgs

default_model() async

Get the default model for this backend.

Returns:

Type Description
str

Model name or identifier

Source code in src/guidellm/backends/vllm_python/vllm.py
async def default_model(self) -> str:
    """
    Get the default model for this backend.

    :return: Model name or identifier
    """
    return self._args.model

process_shutdown() async

Clean up VLLM AsyncLLMEngine instance and resources.

Raises:

Type Description
RuntimeError

If backend was not properly initialized

Source code in src/guidellm/backends/vllm_python/vllm.py
async def process_shutdown(self):
    """
    Clean up VLLM AsyncLLMEngine instance and resources.

    :raises RuntimeError: If backend was not properly initialized
    """
    if not self._in_process:
        raise RuntimeError("Backend not started up for process.")

    if self._engine is not None:
        self._engine.shutdown()
        self._engine = None
    self._in_process = False

process_startup() async

Initialize VLLM AsyncLLMEngine instance with configured parameters.

Raises:

Type Description
RuntimeError

If backend is already initialized

Source code in src/guidellm/backends/vllm_python/vllm.py
async def process_startup(self):
    """
    Initialize VLLM AsyncLLMEngine instance with configured parameters.

    :raises RuntimeError: If backend is already initialized
    """
    if self._in_process:
        raise RuntimeError("Backend already started up for process.")

    engine_args = vllm.AsyncEngineArgs(**self._args.vllm_config)
    self._engine = vllm.AsyncLLMEngine.from_engine_args(engine_args)
    self._in_process = True

resolve(request, request_info, history=None) async

Process generation request and yield a single response.

Resolves the request (chat template, placeholders, multimodal data), then runs async generation via AsyncLLMEngine. For streaming, records per-token timings. In both cases the caller receives exactly one (response, request_info) pair.

:yields: Single tuple of (response, updated_request_info)

Parameters:

Name Type Description Default
request GenerationRequest

Generation request with content and parameters

required
request_info RequestInfo

Request tracking info updated with timing metadata

required
history list[tuple[GenerationRequest, GenerationResponse]] | None

Conversation history (currently not supported)

None

Raises:

Type Description
NotImplementedError

If history is provided

RuntimeError

If backend is not initialized

Source code in src/guidellm/backends/vllm_python/vllm.py
async def resolve(  # type: ignore[override, misc]
    self,
    request: GenerationRequest,
    request_info: RequestInfo,
    history: list[tuple[GenerationRequest, GenerationResponse]] | None = None,
) -> AsyncIterator[tuple[GenerationResponse, RequestInfo]]:
    """
    Process generation request and yield a single response.

    Resolves the request (chat template, placeholders, multimodal data),
    then runs async generation via AsyncLLMEngine. For streaming, records
    per-token timings. In both cases the caller receives exactly one
    (response, request_info) pair.

    :param request: Generation request with content and parameters
    :param request_info: Request tracking info updated with timing metadata
    :param history: Conversation history (currently not supported)
    :raises NotImplementedError: If history is provided
    :raises RuntimeError: If backend is not initialized
    :yields: Single tuple of (response, updated_request_info)
    """
    self._validate_backend_initialized()
    self._validate_history(history)

    resolved = self._resolve_request(request)
    sampling_params = self._create_sampling_params(
        max_tokens_override=(
            request.output_metrics.text_tokens
            if request.output_metrics.text_tokens
            else None
        ),
    )

    generate_input: str | dict[str, Any]
    if resolved.multi_modal_data:
        generate_input = {
            "prompt": resolved.prompt,
            "multi_modal_data": resolved.multi_modal_data,
        }
    else:
        generate_input = resolved.prompt

    request_id = str(uuid.uuid4())
    request_info.timings.request_start = time.time()

    gen_state: dict[str, Any] = {}
    try:
        async for result in self._run_generation(
            request,
            request_info,
            resolved.stream,
            generate_input,
            sampling_params,
            request_id,
            gen_state,
        ):
            yield result
    except asyncio.CancelledError as err:
        final_output = gen_state.get("final_output")
        cancel_result = self._build_final_response(
            request,
            request_info,
            final_output,
            resolved.stream,
            self._text_from_output(final_output) if final_output else "",
        )
        if cancel_result is not None:
            yield cancel_result
        raise err
    except (RuntimeError, ValueError, TypeError, OSError, KeyError) as exc:
        self._raise_generation_error(exc)

validate() async

Validate backend readiness by attempting a test generation.

Raises:

Type Description
RuntimeError

If backend cannot generate or is not initialized

Source code in src/guidellm/backends/vllm_python/vllm.py
async def validate(self):
    """
    Validate backend readiness by attempting a test generation.

    :raises RuntimeError: If backend cannot generate or is not initialized
    """
    engine = self._validate_backend_initialized()

    try:
        await engine.check_health()
    except BaseException as exc:
        raise RuntimeError(
            "Backend validation failed. Health check failed."
        ) from exc

VLLMResponseHandler

Stateless response builder for the vLLM Python backend.

Converts (text, usage dict) pairs into GenerationResponse with proper UsageMetrics. All methods are static; no per-request accumulation state.

Source code in src/guidellm/backends/vllm_python/vllm_response.py
class VLLMResponseHandler:
    """
    Stateless response builder for the vLLM Python backend.

    Converts (text, usage dict) pairs into GenerationResponse with proper
    UsageMetrics.  All methods are static; no per-request accumulation state.
    """

    @staticmethod
    def build_response(
        request: GenerationRequest,
        text: str,
        usage: dict[str, int] | None,
        response_id: str | None = None,
    ) -> GenerationResponse:
        """
        Build a GenerationResponse from text and usage extracted from vLLM output.

        :param request: Original generation request (provides request_id)
        :param text: Generated text
        :param usage: Token usage dict (prompt_tokens, completion_tokens, total_tokens)
        :param response_id: Optional response/request ID from vLLM
        :return: GenerationResponse with text and metrics
        """
        input_metrics, output_metrics = VLLMResponseHandler._extract_metrics(
            usage, text
        )
        return GenerationResponse(
            request_id=request.request_id,
            response_id=response_id,
            request_args=None,
            text=text or None,
            input_metrics=input_metrics,
            output_metrics=output_metrics,
        )

    @staticmethod
    def _extract_metrics(
        usage: dict[str, Any] | None, text: str | None
    ) -> tuple[UsageMetrics, UsageMetrics]:
        """Build UsageMetrics from usage dict and text (word/char counts).

        text=None means text is not applicable (metrics will be None);
        text="" means text was applicable but empty (metrics will be 0).
        """
        if text is None:
            # text not applicable (e.g. tool-call-only) — exclude from aggregation
            text_words = None
            text_chars = None
        else:
            text_words = len(text.split())
            text_chars = len(text)

        if not usage:
            return UsageMetrics(), UsageMetrics(
                text_words=text_words,
                text_characters=text_chars,
            )

        usage_metrics = cast("dict[str, int]", usage)
        input_details = cast(
            "dict[str, int]", usage.get("prompt_tokens_details", {}) or {}
        )
        output_details = cast(
            "dict[str, int]", usage.get("completion_tokens_details", {}) or {}
        )

        input_metrics = UsageMetrics(
            text_tokens=usage_metrics.get("prompt_tokens", 0),
            image_tokens=input_details.get("image_tokens"),
            video_tokens=input_details.get("video_tokens"),
            audio_tokens=input_details.get("audio_tokens"),
            audio_seconds=input_details.get("seconds"),
        )
        output_metrics = UsageMetrics(
            text_tokens=usage_metrics.get("completion_tokens", 0),
            text_words=text_words,
            text_characters=text_chars,
            image_tokens=output_details.get("image_tokens"),
            video_tokens=output_details.get("video_tokens"),
            audio_tokens=output_details.get("audio_tokens"),
            audio_seconds=output_details.get("seconds"),
        )
        return input_metrics, output_metrics

build_response(request, text, usage, response_id=None) staticmethod

Build a GenerationResponse from text and usage extracted from vLLM output.

Parameters:

Name Type Description Default
request GenerationRequest

Original generation request (provides request_id)

required
text str

Generated text

required
usage dict[str, int] | None

Token usage dict (prompt_tokens, completion_tokens, total_tokens)

required
response_id str | None

Optional response/request ID from vLLM

None

Returns:

Type Description
GenerationResponse

GenerationResponse with text and metrics

Source code in src/guidellm/backends/vllm_python/vllm_response.py
@staticmethod
def build_response(
    request: GenerationRequest,
    text: str,
    usage: dict[str, int] | None,
    response_id: str | None = None,
) -> GenerationResponse:
    """
    Build a GenerationResponse from text and usage extracted from vLLM output.

    :param request: Original generation request (provides request_id)
    :param text: Generated text
    :param usage: Token usage dict (prompt_tokens, completion_tokens, total_tokens)
    :param response_id: Optional response/request ID from vLLM
    :return: GenerationResponse with text and metrics
    """
    input_metrics, output_metrics = VLLMResponseHandler._extract_metrics(
        usage, text
    )
    return GenerationResponse(
        request_id=request.request_id,
        response_id=response_id,
        request_args=None,
        text=text or None,
        input_metrics=input_metrics,
        output_metrics=output_metrics,
    )