"""Okna kontekstu i planowanie budżetu tokenów (LOG-30/31). Naczelna zasada, której pilnują te testy: **zawsze zostaje miejsce na odpowiedź**. Prompt nigdy nie może wypełnić całego okna kontekstu, bo wtedy model kończy na `max_tokens` z pustą albo uciętą treścią — to był zgłoszony błąd. """ import pytest from app.llm import limits def test_known_models_have_documented_limits(): ctx, out = limits.limits_for("anthropic", "claude-opus-4-8") assert ctx == 1_000_000 and out == 128_000 ctx, out = limits.limits_for("anthropic", "claude-haiku-4-5") assert ctx == 200_000 and out == 64_000 def test_local_model_tag_is_matched_by_prefix(): """Modele lokalne niosą tag (`llama3.1:8b`) — dopasowanie musi to znieść.""" assert limits.limits_for("local", "llama3.1:8b") == limits.limits_for("local", "llama3.1") def test_unknown_model_falls_back_conservatively(): ctx, out = limits.limits_for("local", "jakis-egzotyczny-model") assert ctx == 8_192 and out == 4_096 def test_env_overrides_win(monkeypatch): """Modele wychodzą szybciej, niż aktualizuje się tabela.""" monkeypatch.setenv("LOCAL_CONTEXT_WINDOW", "131072") monkeypatch.setenv("LOCAL_MAX_OUTPUT", "8192") assert limits.limits_for("local", "llama3.1:8b") == (131_072, 8_192) def test_env_override_ignores_garbage(monkeypatch): monkeypatch.setenv("LOCAL_CONTEXT_WINDOW", "nie-liczba") assert limits.limits_for("local", "llama3.1:8b")[0] == 8_192 # ------------------------------------------------- rezerwa miejsca na odpowiedź def test_prompt_budget_always_reserves_room_for_answer(): ctx, out = limits.limits_for("anthropic", "claude-opus-4-8") budget = limits.prompt_token_budget("anthropic", "claude-opus-4-8") assert budget + out + limits.SAFETY_MARGIN <= ctx assert budget > 0 def test_small_context_model_still_leaves_room(): budget = limits.prompt_token_budget("local", "llama3.1:8b") ctx, out = limits.limits_for("local", "llama3.1:8b") assert budget + out + limits.SAFETY_MARGIN <= ctx def test_plan_shrinks_output_when_prompt_is_huge(): """Duży prompt nie może dostać pełnego okna wyjścia — musi się zmieścić.""" ctx, model_out = limits.limits_for("local", "llama3.1:8b") p = limits.plan("local", "llama3.1:8b", prompt_tokens=6_000) assert p["max_output"] <= ctx - 6_000 - limits.SAFETY_MARGIN assert p["max_output"] < model_out def test_plan_reports_not_fitting_instead_of_failing_silently(): p = limits.plan("local", "llama3.1:8b", prompt_tokens=8_000) assert p["fits"] is False assert any("odpowied" in w for w in p["warnings"]) def test_plan_warns_above_90k_but_still_fits(): """Powyżej 90 tys. tokenów ostrzegamy — ale wysłanie MA być nadal możliwe.""" p = limits.plan("anthropic", "claude-opus-4-8", prompt_tokens=120_000) assert p["fits"] is True, "duży prompt nadal musi dać się wysłać" assert p["max_output"] >= limits.MIN_OUTPUT assert any("dużo" in w for w in p["warnings"]) def test_no_warning_below_threshold(): p = limits.plan("anthropic", "claude-opus-4-8", prompt_tokens=10_000) assert p["warnings"] == [] and p["fits"] is True def test_requested_output_is_capped_by_model_maximum(): p = limits.plan("anthropic", "claude-haiku-4-5", prompt_tokens=1_000, want_output=999_999) assert p["max_output"] == 64_000 @pytest.mark.parametrize("tokens", [0, 1_000, 50_000, 200_000, 900_000]) def test_plan_never_returns_negative_output(tokens): p = limits.plan("anthropic", "claude-opus-4-8", prompt_tokens=tokens) assert p["max_output"] >= 0