Files
hindsight/hindsight-api-slim/tests/test_llm_extra_body.py
T
Nicolò Boschi 6ba4aeaf03 chore: format test files with ruff (enable formatter on tests/) (#2074)
Tests were excluded from both ruff lint and format via the top-level
[tool.ruff].exclude in hindsight-api-slim, hindsight-embed and the shared
ruff.toml. As a result test files drifted from the formatter's style and
every PR that touched a test (or ran format-on-save) carried large
formatting-only churn.

Move the tests exclude into [tool.ruff.lint].exclude (and [lint].exclude in
ruff.toml) so the formatter now covers tests while lint rules — too noisy for
test code (unused imports/vars, import ordering) — stay excluded. Then run
ruff format across all test directories.

Note: lint.exclude is a post-traversal path filter, so it needs the glob form
'tests/**' rather than the directory form 'tests/' used by top-level exclude.
2026-06-09 13:23:11 +02:00

253 lines
9.5 KiB
Python

"""
Tests for the ``llm_extra_body`` knob (env: ``HINDSIGHT_API_LLM_EXTRA_BODY``).
The same JSON dict of extra request-body params is threaded into every API
provider. Each provider merges it in its own native parameter space:
- OpenAI-compatible / Fireworks: OpenAI SDK ``extra_body`` (already covered
elsewhere; the wiring predates this change).
- Anthropic: the Anthropic SDK ``extra_body`` kwarg.
- Gemini / VertexAI: seeded into ``GenerateContentConfig`` (the SDK's native
generation-param space — Gemini nests these in the request body).
- LiteLLM (+ bedrock alias + router): merged as top-level ``acompletion`` kwargs
so LiteLLM normalizes/drops them per-provider.
These are deterministic unit tests: the SDK client is mocked and we assert the
params actually reach the call.
"""
import os
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
EXTRA_BODY = {"temperature": 0.2, "top_p": 0.9}
# ─── config / env parsing ─────────────────────────────────────────────────────
def test_extra_body_parsed_from_env():
"""The JSON env var is parsed into ``HindsightConfig.llm_extra_body``."""
import json
from hindsight_api.config import ENV_LLM_EXTRA_BODY, HindsightConfig, clear_config_cache
with patch.dict(os.environ, {ENV_LLM_EXTRA_BODY: json.dumps(EXTRA_BODY)}, clear=False):
clear_config_cache()
config = HindsightConfig.from_env()
assert config.llm_extra_body == EXTRA_BODY
clear_config_cache()
def test_extra_body_default_is_none():
"""When the env var is unset, ``llm_extra_body`` defaults to None."""
from hindsight_api.config import ENV_LLM_EXTRA_BODY, HindsightConfig, clear_config_cache
env = {k: v for k, v in os.environ.items() if k != ENV_LLM_EXTRA_BODY}
with patch.dict(os.environ, env, clear=True):
clear_config_cache()
config = HindsightConfig.from_env()
assert config.llm_extra_body is None
clear_config_cache()
# ─── Anthropic ────────────────────────────────────────────────────────────────
def _make_anthropic_provider(extra_body=None):
with patch("anthropic.AsyncAnthropic") as mock_client_cls:
mock_client_cls.return_value = MagicMock()
from hindsight_api.engine.providers.anthropic_llm import AnthropicLLM
provider = AnthropicLLM(
provider="anthropic",
api_key="fake-key",
base_url="",
model="claude-sonnet-4-20250514",
extra_body=extra_body,
)
provider._client = MagicMock()
return provider
def _fake_anthropic_response():
block = MagicMock()
block.type = "text"
block.text = "ok"
resp = MagicMock()
resp.content = [block]
resp.usage = MagicMock(input_tokens=5, output_tokens=2, cache_read_input_tokens=0)
resp.stop_reason = "end_turn"
return resp
def test_anthropic_stores_extra_body():
provider = _make_anthropic_provider(extra_body=EXTRA_BODY)
assert provider._extra_body == EXTRA_BODY
def test_anthropic_empty_extra_body_defaults_to_dict():
provider = _make_anthropic_provider(extra_body=None)
assert provider._extra_body == {}
@pytest.mark.asyncio
async def test_anthropic_call_passes_extra_body():
"""``call()`` forwards extra_body via the Anthropic SDK ``extra_body`` kwarg."""
provider = _make_anthropic_provider(extra_body=EXTRA_BODY)
provider._client.messages.create = AsyncMock(return_value=_fake_anthropic_response())
with patch("hindsight_api.engine.providers.anthropic_llm.get_metrics_collector"):
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test", max_retries=0)
kwargs = provider._client.messages.create.call_args.kwargs
assert kwargs.get("extra_body") == EXTRA_BODY
@pytest.mark.asyncio
async def test_anthropic_no_extra_body_omits_key():
"""``call()`` does not pass ``extra_body`` when none is configured."""
provider = _make_anthropic_provider(extra_body=None)
provider._client.messages.create = AsyncMock(return_value=_fake_anthropic_response())
with patch("hindsight_api.engine.providers.anthropic_llm.get_metrics_collector"):
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test", max_retries=0)
assert "extra_body" not in provider._client.messages.create.call_args.kwargs
# ─── Gemini ───────────────────────────────────────────────────────────────────
def _make_gemini_provider(extra_body=None):
pytest.importorskip("google.genai")
with patch("google.genai.Client") as mock_client_cls:
mock_client_cls.return_value = MagicMock()
from hindsight_api.engine.providers.gemini_llm import GeminiLLM
provider = GeminiLLM(
provider="gemini",
api_key="fake-key",
base_url="",
model="gemini-2.5-flash",
extra_body=extra_body,
)
provider._client = MagicMock()
return provider
def _fake_gemini_response():
r = MagicMock()
r.text = "hello"
r.candidates = [MagicMock(finish_reason="STOP")]
r.usage_metadata = MagicMock(prompt_token_count=5, candidates_token_count=2)
return r
def test_gemini_stores_extra_body():
provider = _make_gemini_provider(extra_body=EXTRA_BODY)
assert provider._extra_body == EXTRA_BODY
@pytest.mark.asyncio
async def test_gemini_call_applies_extra_body_to_generation_config():
"""``call()`` seeds extra_body into GenerateContentConfig (temperature/top_p)."""
provider = _make_gemini_provider(extra_body=EXTRA_BODY)
provider._client.aio.models.generate_content = AsyncMock(return_value=_fake_gemini_response())
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test")
config_arg = provider._client.aio.models.generate_content.call_args.kwargs.get("config")
assert config_arg is not None
assert config_arg.temperature == 0.2
assert config_arg.top_p == 0.9
@pytest.mark.asyncio
async def test_gemini_explicit_temperature_overrides_extra_body():
"""An explicit per-call temperature wins over the extra_body default."""
provider = _make_gemini_provider(extra_body={"temperature": 0.2})
provider._client.aio.models.generate_content = AsyncMock(return_value=_fake_gemini_response())
await provider.call(messages=[{"role": "user", "content": "hi"}], temperature=0.9, scope="test")
config_arg = provider._client.aio.models.generate_content.call_args.kwargs.get("config")
assert config_arg.temperature == 0.9
# ─── LiteLLM ──────────────────────────────────────────────────────────────────
def _make_litellm_provider(extra_body=None):
pytest.importorskip("litellm")
from hindsight_api.engine.providers.litellm_llm import LiteLLMLLM
return LiteLLMLLM(
provider="litellm",
api_key="fake-key",
base_url="",
model="gpt-4o",
extra_body=extra_body,
)
def _fake_litellm_response():
msg = MagicMock()
msg.content = "ok"
choice = MagicMock()
choice.message = msg
choice.finish_reason = "stop"
resp = MagicMock()
resp.choices = [choice]
resp.usage = MagicMock(prompt_tokens=5, completion_tokens=2)
return resp
def test_litellm_stores_extra_body():
provider = _make_litellm_provider(extra_body=EXTRA_BODY)
assert provider._extra_body == EXTRA_BODY
@pytest.mark.asyncio
async def test_litellm_call_merges_extra_body_as_top_level_kwargs():
"""``call()`` merges extra_body into the acompletion kwargs."""
provider = _make_litellm_provider(extra_body=EXTRA_BODY)
provider._acompletion = AsyncMock(return_value=_fake_litellm_response())
with patch("hindsight_api.engine.providers.litellm_llm.get_metrics_collector"):
await provider.call(messages=[{"role": "user", "content": "hi"}], scope="test", max_retries=0)
kwargs = provider._acompletion.call_args.kwargs
assert kwargs.get("temperature") == 0.2
assert kwargs.get("top_p") == 0.9
@pytest.mark.asyncio
async def test_litellm_explicit_param_wins_over_extra_body():
"""``setdefault`` semantics: an explicit per-call value is not overwritten."""
provider = _make_litellm_provider(extra_body={"temperature": 0.2})
provider._acompletion = AsyncMock(return_value=_fake_litellm_response())
with patch("hindsight_api.engine.providers.litellm_llm.get_metrics_collector"):
await provider.call(messages=[{"role": "user", "content": "hi"}], temperature=0.9, scope="test", max_retries=0)
assert provider._acompletion.call_args.kwargs.get("temperature") == 0.9
def test_litellm_router_forwards_extra_body():
"""The Router subclass forwards extra_body through to the shared LiteLLM base."""
pytest.importorskip("litellm")
from hindsight_api.engine.providers.litellm_router_llm import LiteLLMRouterLLM
config = {"model_list": [{"model_name": "m", "litellm_params": {"model": "gpt-4o", "api_key": "x"}}]}
provider = LiteLLMRouterLLM(
provider="litellmrouter",
api_key="",
base_url="",
model="m",
config=config,
extra_body=EXTRA_BODY,
)
assert provider._extra_body == EXTRA_BODY