import pytest from pydantic import BaseModel from src.config import ModelConfig from src.exceptions import ValidationException from src.llm.caching import PromptCachePolicy from src.llm.request_builder import execute_completion from tests.llm.conftest import FakeBackend class SampleResponse(BaseModel): answer: str async def test_gemini_explicit_budget_passes_tokens_through_without_adjustment( fake_backend: FakeBackend, ) -> None: config = ModelConfig( model="gemini-2.5-flash", transport="gemini", thinking_budget_tokens=256, ) await execute_completion( fake_backend, config, messages=[{"role": "user", "content": "Hello"}], max_tokens=100, ) call = fake_backend.calls[0] # No auto-adjustment — operators set explicit values assert call["max_output_tokens"] == 100 assert call["max_tokens"] == 100 assert call["thinking_budget_tokens"] == 256 async def test_thinking_params_are_passed_through_without_capability_dropping( fake_backend: FakeBackend, ) -> None: config = ModelConfig( model="claude-haiku-4-5", transport="anthropic", thinking_effort="high", thinking_budget_tokens=1024, ) await execute_completion( fake_backend, config, messages=[{"role": "user", "content": "Hello"}], max_tokens=100, ) call = fake_backend.calls[0] assert call["thinking_effort"] == "high" assert call["thinking_budget_tokens"] == 1024 async def test_cache_policy_is_passed_through_extra_params( fake_backend: FakeBackend, ) -> None: config = ModelConfig(model="gpt-4.1-mini", transport="openai") cache_policy = PromptCachePolicy(mode="prefix", ttl_seconds=300) await execute_completion( fake_backend, config, messages=[{"role": "user", "content": "Hello"}], max_tokens=100, response_format=SampleResponse, cache_policy=cache_policy, ) call = fake_backend.calls[0] assert call["response_format"] is SampleResponse assert call["extra_params"]["cache_policy"] == cache_policy async def test_provider_params_are_merged_into_extra_params( fake_backend: FakeBackend, ) -> None: config = ModelConfig( model="gpt-4.1-mini", transport="openai", top_p=0.9, provider_params={"custom_flag": True}, ) await execute_completion( fake_backend, config, messages=[{"role": "user", "content": "Hello"}], max_tokens=100, ) call = fake_backend.calls[0] assert call["extra_params"]["top_p"] == 0.9 assert call["extra_params"]["custom_flag"] is True async def test_provider_timeout_is_normalized_into_extra_params( fake_backend: FakeBackend, ) -> None: """Numeric-string provider timeout values are normalized before backends.""" config = ModelConfig( model="gpt-4.1-mini", transport="openai", provider_params={"timeout": "42.5"}, ) await execute_completion( fake_backend, config, messages=[{"role": "user", "content": "Hello"}], max_tokens=100, ) call = fake_backend.calls[0] assert call["extra_params"]["timeout"] == 42.5 @pytest.mark.parametrize( "timeout", ["slow", "", 0, -1, True, float("nan"), float("inf"), "nan", "inf"], ) async def test_provider_timeout_rejects_invalid_values( fake_backend: FakeBackend, timeout: object, ) -> None: """Invalid provider timeout values fail before provider SDK calls.""" config = ModelConfig( model="gpt-4.1-mini", transport="openai", provider_params={"timeout": timeout}, ) with pytest.raises( ValidationException, match=r"provider_params\.timeout must be a positive number of seconds", ): await execute_completion( fake_backend, config, messages=[{"role": "user", "content": "Hello"}], max_tokens=100, )