strix/tests/test_llm_extra_body.py
Szymon Nastaly 972f5097c2
Some checks failed
CI / python (push) Has been cancelled
CI / tui (push) Has been cancelled
CI / viewer (push) Has been cancelled
CI / package (push) Has been cancelled
CI / workflows (push) Has been cancelled
CI / ci-passed (push) Has been cancelled
feat(llm): extra request-body fields via LLM_EXTRA_BODY (#1495)
* feat(llm): extra request-body fields via LLM_EXTRA_BODY

Some provider parameters, such as OpenAI's service_tier and
safety_identifier, are read only from the request body, so
LLM_EXTRA_HEADERS can't set them. LLM_EXTRA_BODY merges a JSON object
into the body of every request to the configured model, on both routing
paths. A dedicated dedupe model gets its own DEDUPE_LLM_EXTRA_BODY
instead, as with headers.

* fix(config): decode saved JSON-object settings when loading

persist_current() saves env values verbatim, so LLM_EXTRA_HEADERS,
LLM_EXTRA_BODY and their DEDUPE_ variants land in cli-config.json as JSON
text. The loader passed that text unchanged to the dict fields, so a later
launch without the env var failed validation. Decode saved strings for
dict fields, as pydantic-settings does for env vars.

---------

Co-authored-by: Szymon Nastaly <szymon.nastaly@autoform.ch>
2026-10-15 16:37:51 -04:00

97 lines
3.1 KiB
Python

"""Tests for LLM_EXTRA_BODY: extra fields merged into every LLM request body."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Any
import httpx
import litellm
import openai
import pytest
from agents.extensions.models.litellm_model import LitellmModel
from agents.models.interface import ModelTracing
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
from agents.models.openai_responses import OpenAIResponsesModel
from strix.config import loader
from strix.config.loader import load_settings
from strix.core.inputs import make_model_settings
if TYPE_CHECKING:
from collections.abc import Iterator
from agents.models.interface import Model
_EXTRA_BODY = {"service_tier": "flex", "safety_identifier": "user-hash"}
@pytest.fixture(autouse=True)
def _reset(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.delenv("LLM_EXTRA_BODY", raising=False)
monkeypatch.setattr(loader, "_cached", None)
monkeypatch.setattr(loader, "_override", None)
yield
def test_extra_body_parsed_from_json_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"service_tier": "flex", "n": 1}))
assert load_settings().llm.extra_body == {"service_tier": "flex", "n": 1}
async def _send(model: Model) -> None:
await model.get_response(
system_instructions=None,
input="Reply with just 'OK'.",
model_settings=make_model_settings(None, model_name="gpt-5", extra_body=_EXTRA_BODY),
tools=[],
output_schema=None,
handoffs=[],
tracing=ModelTracing.DISABLED,
previous_response_id=None,
conversation_id=None,
prompt=None,
)
def _capturing_client(captured: dict[str, Any]) -> openai.AsyncOpenAI:
def handler(request: httpx.Request) -> httpx.Response:
captured.update(json.loads(request.content))
return httpx.Response(400, json={"error": {"message": "captured"}})
return openai.AsyncOpenAI(
api_key="test",
base_url="https://gateway.example/v1",
max_retries=0,
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
@pytest.mark.asyncio
@pytest.mark.parametrize("model_cls", [OpenAIChatCompletionsModel, OpenAIResponsesModel])
async def test_extra_body_reaches_native_openai_request_body(model_cls: type) -> None:
captured: dict[str, Any] = {}
with pytest.raises(openai.BadRequestError):
await _send(model_cls(model="gpt-5", openai_client=_capturing_client(captured)))
assert {k: captured.get(k) for k in _EXTRA_BODY} == _EXTRA_BODY
@pytest.mark.asyncio
async def test_extra_body_passed_to_litellm(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
async def fake_acompletion(**kwargs: Any) -> Any:
captured.update(kwargs)
raise RuntimeError("captured")
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
with pytest.raises(RuntimeError, match="captured"):
await _send(LitellmModel(model="openai/gpt-5", base_url="https://gateway.example/v1"))
assert captured["extra_body"] == _EXTRA_BODY