f33cdc88f4
PRZYCZYNA PUSTYCH ODPOWIEDZI NA ANTHROPICU (potwierdzona w dokumentacji API): domyslnym modelem byl `claude-sonnet-5`, ktory przy POMINIETYM parametrze `thinking` wlacza myslenie adaptacyjne, a `thinking.display` domyslnie jest "omitted". Tokeny myslenia licza sie do max_tokens, wiec przy LLM_MAX_TOKENS=2000 cala tura wychodzila jako bloki `thinking` z pustym tekstem — parser filtrowal type=="text" i zwracal pusty string. Opus 4.8 bez `thinking` nie mysli, wiec tam objaw by nie wystapil. Gwarancja niepustej odpowiedzi (wszyscy trzej dostawcy): - generate() to teraz PETLA, nie pojedynczy strzal: tura -> jesli urwana na limicie, dopisz ture „kontynuuj" w tej samej rozmowie i sklej tekst, - tura zlozona z samego myslenia traktowana jak urwana (nie jak pustka), - pusta i NIE urwana -> jedna proba z podpowiedzia, dopiero potem blad, - kontynuacja konczy sie tura UZYTKOWNIKA — Claude odrzuca prefill asystenta (400), - `thinking` konfigurowany JAWNIE (adaptive + effort=high; ANTHROPIC_THINKING=off). Okna kontekstu i rezerwa na odpowiedz (app/llm/limits.py): - tabela okien/limitow wyjscia per model + nadpisanie z ENV, - plan() liczy okno odpowiedzi jako okno - prompt - margines i NIGDY nie oddaje calego kontekstu promptowi, - Anthropic liczy tokeny DOKLADNIE (/v1/messages/count_tokens), reszta szacuje, - >90 tys. tokenow promptu -> ostrzezenie, ale wyslanie NADAL mozliwe i z pelnym oknem odpowiedzi. UI: suwak budzetu rozszerzony o „bardzo obszerny" i „maksymalny kontekst modelu" (liczony z okna wybranego modelu po odjeciu rezerwy); przy wyniku widac plan tokenow, liczbe tur i ostrzezenia. Domyslny model Anthropic: claude-opus-4-8. Testy: 170 passed / 1 skipped (logika) + 15 (prezentacja). Nowe testy pokrywaja sklejanie kontynuacji, brak prefillu asystenta, ture z samego myslenia, rezerwe na odpowiedz i prog ostrzezenia. Zweryfikowane e2e na atrapie Anthropica odtwarzajacej zgloszony objaw: 3 tury, obie czesci tekstu obecne. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
3.6 KiB
Python
92 lines
3.6 KiB
Python
"""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
|