Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/guides/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,34 @@ guidellm run \

This will include `temperature`, `top_p`, and `top_k` in every request body sent to the server.

## Structured Chat Content Payloads

Some chat templates require metadata alongside the text in each structured content object. Pass these fields through `extras.content` in the `openai_http` backend configuration. GuideLLM adds them to every generated text content object for Chat Completions and Responses API requests.

```bash
guidellm run \
--backend '{
"kind": "openai_http",
"target": "http://localhost:8000",
"model": "google/translategemma-12b-it",
"request_format": "/v1/chat/completions",
"extras": {
"content": {
"source_lang_code": "en",
"target_lang_code": "es"
}
}
}' \
--data kind=synthetic_text,prompt_tokens=1000,output_tokens=1000 \
--constraint kind=max_duration,seconds=60
```

### How It Works

The `--backend` config is parsed into keyword arguments for the backend constructor. The `extras` field within that config maps to a `GenerationRequestArguments` object that supports the following sub-fields:

- `body`: A dictionary of key-value pairs merged into the HTTP request body. Use this for sampling parameters like `temperature`, `top_p`, `top_k`, `repetition_penalty`, etc.
- `content`: A dictionary of fields merged into each generated text content object.
- `headers`: A dictionary of additional HTTP headers to include in requests.
- `params`: A dictionary of query parameters to append to the request URL.

Expand Down
52 changes: 44 additions & 8 deletions src/guidellm/backends/openai/request_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,10 @@ def _ensure_tool_format(tool: dict[str, Any]) -> dict[str, Any]:
return tool

def _format_prompts(
self, column_data: list[dict[str, Any]], column_type: str
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
Expand All @@ -716,7 +719,10 @@ def _format_prompts(
formatted_data = []
for item in column_data:
if column_type == "text_column":
formatted_data.append({"type": "text", "text": item})
content = {"type": "text", "text": item}
if content_extras:
content.update(content_extras)
formatted_data.append(content)
elif column_type == "image_column":
formatted_data.append(
{
Expand Down Expand Up @@ -910,8 +916,14 @@ def _build_turn_messages( # noqa: C901
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)
self._format_prompts(
req.columns.get(col, []),
col,
content_extras,
)
for col in (
"text_column",
"image_column",
Expand Down Expand Up @@ -1018,8 +1030,14 @@ def format( # noqa: C901, PLR0912, PLR0915
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)
self._format_prompts(
data.columns.get(col, []),
col,
content_extras,
)
for col in (
"text_column",
"image_column",
Expand Down Expand Up @@ -1497,12 +1515,18 @@ def _ensure_tool_format(tool: dict[str, Any]) -> dict[str, Any]:
return tool

def _format_prompts(
self, column_data: list, column_type: str
self,
column_data: list,
column_type: str,
content_extras: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
formatted_data: list[dict[str, Any]] = []
for item in column_data:
if column_type == "text_column":
formatted_data.append({"type": "input_text", "text": item})
content = {"type": "input_text", "text": item}
if content_extras:
content.update(content_extras)
formatted_data.append(content)
elif column_type == "image_column":
formatted_data.append(
{
Expand Down Expand Up @@ -1582,8 +1606,14 @@ def _build_turn_input_items( # noqa: C901
items.append({"role": "assistant", "content": content})
else:
# Standard or tool_call turn: user content.
extras = kwargs.get("extras")
content_extras = extras.content if extras is not None else None
prompts = [
self._format_prompts(req.columns.get(col, []), col)
self._format_prompts(
req.columns.get(col, []),
col,
content_extras,
)
for col in (
"text_column",
"image_column",
Expand Down Expand Up @@ -1751,8 +1781,14 @@ def format( # noqa: C901
)
elif data.turn_type != "tool_response_injection":
# Standard or tool_call turn: user content.
extras = kwargs.get("extras")
content_extras = extras.content if extras is not None else None
prompts = [
self._format_prompts(data.columns.get(col, []), col)
self._format_prompts(
data.columns.get(col, []),
col,
content_extras,
)
for col in (
"text_column",
"image_column",
Expand Down
7 changes: 7 additions & 0 deletions src/guidellm/schemas/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ class GenerationRequestArguments(StandardBaseDict):
default=None,
description="Files to include in the request, if applicable.",
)
content: dict[str, Any] | None = Field(
default=None,
description=(
"Additional fields to include in generated text content objects, "
"if applicable."
),
)

def model_combine(
self, additional: GenerationRequestArguments | dict[str, Any]
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/backends/openai/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,34 @@ def test_server_history_with_responses_api(self):
)
assert backend._args.server_history is True

@pytest.mark.asyncio
@pytest.mark.regression
async def test_content_extras_are_forwarded_to_request_handler(
self,
mock_request_handler,
):
"""The HTTP backend forwards content extras through its handler boundary.

## WRITTEN BY AI ##
"""
extras = GenerationRequestArguments(
content={
"metadata": {"category": "support"},
"priority": 1,
}
)
backend = _make_backend(
target="http://localhost:8000",
model="test-model",
extras=extras,
)
mock_handler, handler_patch = mock_request_handler

with handler_patch:
await backend._prepare_resolve_request(GenerationRequest())

assert mock_handler.format.call_args.kwargs["extras"] == extras

@pytest.mark.smoke
def test_factory_registration(self):
"""Test that OpenAIHTTPBackend is registered with Backend factory."""
Expand Down
Loading