from __future__ import annotations import pytest from pydantic import ValidationError from app.core.config import Settings, supports_reasoning from tests.conftest import build_settings class TestModelFamilyCheck: @pytest.mark.parametrize( "model", ["gpt-5.4-mini", "gpt-5.5", "gpt-5", "gpt-5.6-terra", "gpt-4.1", "o3", "o4-mini"], ) def test_accepts_known_structured_output_families(self, model: str) -> None: assert build_settings(openai_model=model).openai_model == model def test_accepts_a_future_point_release(self) -> None: """Prefix matching exists so a new gpt-5.x is not rejected on arrival.""" assert build_settings(openai_model="gpt-5.9-turbo").openai_model == "gpt-5.9-turbo" def test_rejects_chat_latest_variants(self) -> None: with pytest.raises(ValidationError, match="chat-product variant"): build_settings(openai_model="gpt-5.4-chat-latest") def test_rejects_gpt_4o(self) -> None: """Pre-2024-08-06 snapshots lack structured outputs and aliases hide which is which.""" with pytest.raises(ValidationError): build_settings(openai_model="gpt-4o") def test_rejects_an_unknown_family(self) -> None: with pytest.raises(ValidationError): build_settings(openai_model="llama-3-70b") def test_rejects_empty(self) -> None: with pytest.raises(ValidationError): build_settings(openai_model=" ") class TestReasoningDetection: @pytest.mark.parametrize("model", ["gpt-5.4-mini", "gpt-5", "o3", "o4-mini"]) def test_reasoning_families(self, model: str) -> None: assert supports_reasoning(model) def test_gpt_41_is_not_a_reasoning_model(self) -> None: """Allowed as a model, but sending it a reasoning parameter is a 400.""" assert not supports_reasoning("gpt-4.1") class TestEffort: @pytest.mark.parametrize("effort", ["none", "minimal", "low", "medium", "high", "xhigh"]) def test_accepts_valid_levels(self, effort: str) -> None: assert build_settings(openai_effort=effort).openai_effort == effort def test_normalises_case(self) -> None: assert build_settings(openai_effort="LOW").openai_effort == "low" def test_rejects_unknown_level(self) -> None: with pytest.raises(ValidationError): build_settings(openai_effort="extreme") class TestDefaults: def test_defaults_match_the_documented_baseline(self) -> None: settings = Settings(_env_file=None) assert settings.openai_model == "gpt-5.4-mini" assert settings.openai_max_output_tokens == 4000 assert settings.openai_effort == "low" assert settings.openai_enable_prompt_cache is True def test_has_no_sampling_parameters(self) -> None: """Their presence would be a 400 waiting to happen on a reasoning model.""" fields = set(Settings.model_fields) assert not {"openai_temperature", "openai_top_p"} & fields def test_max_output_tokens_floor_rejects_a_truncating_budget(self) -> None: """Reasoning tokens share this budget, so a small cap truncates the JSON.""" with pytest.raises(ValidationError): build_settings(openai_max_output_tokens=1200) def test_max_output_tokens_floor_is_the_lowest_safe_budget(self) -> None: assert build_settings(openai_max_output_tokens=2048).openai_max_output_tokens == 2048 def test_size_limit_converts_to_bytes(self) -> None: assert build_settings(max_pdf_size_mb=10).max_pdf_size_bytes == 10 * 1024 * 1024