Files
LLeMbas/tests/test_effort.py
T
Jaroslav Beneš 52770d7ab1 Two selects that never wrote anything, and a queue
The approval card in Auto mode and the missing /effort were one bug. Both
selects hung their hx-patch on an empty sibling form reached by form="…",
and htmx binds a trigger to the annotated element: change fires on the
select and bubbles to its ancestors, which a sibling is not. The live rows
read agent_mode=manual and params_json={} while the browser showed Auto and
Effort: high. policy.py was never involved.

The verb moves onto the control; the empty form stays as value scoping,
which is the half of the CLAUDE.md note that was right. conftest gains
control_named so a test asserts the element carrying the name carries the
verb, rather than asserting the markup that was there throughout.

The composer's highlight was a third instance of the same carelessness in
CSS: .tok-mention is written for the transcript and scoped to nothing, so
the mirror painted its token in accent-coloured monospace over the
textarea's own text. Scoped under .msg; the mirror restates transparency
and font rather than inheriting them, and bleeds by box-shadow.

/effort is now offered before the first prompt and _new_chat reads it.
/index re-walks the project directory on demand, file_write drops the
listing it just invalidated, and the index ladder falls through to SFTP on
a host that refuses exec instead of returning nothing.

A second message during a reply is queued rather than starting a second
concurrent generation: a real Message row with queued set, so it survives a
restart and can be withdrawn. _drain hands one on at the end of a reply,
_inject takes one in at a tool-round boundary so an agent can be steered
mid-task. Stop leaves the queue undelivered. The terminal's Auto toggle
becomes off/copy/send, and send posts straight to the chat without touching
the composer.

@ now offers notes, skills, this chat's attachments and a URL to fetch; a
knowledge base attaches as a reference rather than a copy. copy_document
carries provenance, which was the one attach path that dropped it.

Also fixes an unrelated live bug: the round loop compared against the
global MAX_ROUNDS of 3 while sizing itself from the agent budget of 40, so
agent replies stopped after three rounds and reported forty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:49:34 +02:00

233 lines
8.0 KiB
Python

"""Reasoning effort: what goes out, and what does not.
The second half matters as much as the first. There is no field that works
everywhere -- OpenAI and vLLM read `reasoning_effort`, llama.cpp drops it
silently and reads only `chat_template_kwargs` -- so both are sent. That is only
safe because neither is sent at all until somebody chooses an effort, which is
what keeps an endpoint strict about unknown parameters working exactly as it
did.
"""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from lembas.db.models import Chat, Connection, Model, User
from lembas.services import chat as chat_service
from .conftest import control_named
def _model(db, **capabilities) -> Model:
connection = Connection(name="c", base_url="http://127.0.0.1:1", api_key_encrypted="")
db.add(connection)
db.commit()
model = Model(
connection_id=connection.id,
model_id="m",
capabilities_json={"reasoning": True, **capabilities},
)
db.add(model)
db.commit()
return model
def _chat(db, effort: str | None = None) -> Chat:
model = _model(db)
user = db.scalars(select(User)).first()
chat = Chat(
user_id=user.id,
model_id=model.model_id,
connection_id=model.connection_id,
params_json={"reasoning_effort": effort} if effort is not None else {},
)
db.add(chat)
db.commit()
return chat
# --- What reaches the endpoint -----------------------------------------------
def test_an_effort_goes_out_in_both_forms(client: TestClient, db, registered):
"""One value, two fields. Neither endpoint family reads the other's."""
chat = _chat(db, "high")
body = chat_service.build_request(db, chat)
assert body["reasoning_effort"] == "high"
assert body["chat_template_kwargs"] == {"reasoning_effort": "high"}
def test_no_effort_means_neither_field(client: TestClient, db, registered):
"""The whole safety of sending both. A chat nobody has set an effort on is
byte-for-byte the request it was before this existed, so a provider that
refuses unknown parameters is untouched until somebody opts in."""
chat = _chat(db)
body = chat_service.build_request(db, chat)
assert "reasoning_effort" not in body
assert "chat_template_kwargs" not in body
def test_a_cleared_effort_means_neither_field(client: TestClient, db, registered):
"""Cleared is stored as None, like every other parameter here."""
chat = _chat(db, None)
body = chat_service.build_request(db, chat)
assert "reasoning_effort" not in body
@pytest.mark.parametrize("junk", ["sudo", "HIGH ", "maximum", "1"])
def test_a_value_that_is_not_an_effort_is_not_sent(client: TestClient, db, registered, junk):
"""Never trusted from the row: it could predate a change to the list."""
chat = _chat(db, junk)
assert "reasoning_effort" not in chat_service.build_request(db, chat)
def test_existing_chat_template_kwargs_are_kept(client: TestClient, db, registered):
"""Merged rather than replaced, so a future caller setting something else
there does not lose it."""
body: dict = {"chat_template_kwargs": {"enable_thinking": True}}
chat_service.apply_effort(body, "low")
assert body["chat_template_kwargs"] == {"enable_thinking": True, "reasoning_effort": "low"}
# --- Setting it ---------------------------------------------------------------
def test_patching_the_effort_stores_it(client: TestClient, db, registered):
chat = _chat(db)
assert client.patch(
f"/api/chats/{chat.id}", data={"reasoning_effort": "medium"}
).status_code == 204
db.refresh(chat)
assert chat.params_json["reasoning_effort"] == "medium"
def test_an_empty_effort_clears_it(client: TestClient, db, registered):
chat = _chat(db, "high")
client.patch(f"/api/chats/{chat.id}", data={"reasoning_effort": ""})
db.refresh(chat)
assert chat.params_json["reasoning_effort"] is None
def test_an_unknown_effort_leaves_the_old_one(client: TestClient, db, registered):
"""Ignored, not refused: a typo should not cost the setting you had."""
chat = _chat(db, "low")
client.patch(f"/api/chats/{chat.id}", data={"reasoning_effort": "extreme"})
db.refresh(chat)
assert chat.params_json["reasoning_effort"] == "low"
def test_the_control_only_appears_on_a_reasoning_model(client: TestClient, db, registered):
"""The flag has existed with no reader since the beginning; this is its
first job. Offering the control everywhere would offer a setting that does
nothing almost everywhere."""
chat = _chat(db)
assert "data-effort" in client.get(f"/chat/{chat.id}").text
model = db.scalars(select(Model)).one()
model.capabilities_json = {"reasoning": False}
db.commit()
assert "data-effort" not in client.get(f"/chat/{chat.id}").text
# --- The per-model default ----------------------------------------------------
def test_a_new_chat_starts_from_the_models_defaults(client: TestClient, db, registered):
"""`Model.params_json` has claimed to do this since it was added and did it
nowhere. It is empty on every existing row, so honouring it changes nothing
until an administrator sets something."""
model = _model(db)
model.params_json = {"reasoning_effort": "high"}
db.commit()
client.post("/api/chats/start", data={"content": "hello", "model_id": "m"})
chat = db.scalars(select(Chat)).one()
assert chat.params_json["reasoning_effort"] == "high"
def test_a_model_with_no_defaults_starts_a_plain_chat(client: TestClient, db, registered):
_model(db)
client.post("/api/chats/start", data={"content": "hello", "model_id": "m"})
assert db.scalars(select(Chat)).one().params_json == {}
# --- Choosable before the first prompt ---------------------------------------
def test_the_effort_select_carries_its_own_verb(client: TestClient, db, registered):
"""The same invariant the mode select needs, for the same reason.
Both were built on `form="…"` pointing at an empty sibling form holding the
`hx-patch`, and both therefore wrote nothing at all: `form=` scopes the
values a request carries, it does not route the event that starts one.
"""
chat = _chat(db)
select = control_named(client.get(f"/chat/{chat.id}").text, "reasoning_effort")
assert select["hx-patch"] == f"/api/chats/{chat.id}"
assert select["form"] == "chat-params-form"
def test_the_effort_is_offered_before_there_is_a_chat(client: TestClient, db, registered):
"""Otherwise it is a setting you can only reach once it is too late to use.
On the new-chat screen there is nothing to PATCH, so it is an ordinary field
of the composer's form and carries no verb -- `_new_chat` reads it.
"""
_model(db)
select = control_named(client.get("/chat").text, "reasoning_effort")
assert "hx-patch" not in select
assert "form" not in select
def test_starting_a_chat_with_an_effort_stores_it(client: TestClient, db, registered):
_model(db)
client.post(
"/api/chats/start",
data={"content": "hello", "model_id": "m", "reasoning_effort": "low"},
)
assert db.scalars(select(Chat)).one().params_json["reasoning_effort"] == "low"
def test_an_explicit_effort_beats_the_models_default(client: TestClient, db, registered):
"""An inherited value is a starting point, not a ceiling."""
model = _model(db)
model.params_json = {"reasoning_effort": "high"}
db.commit()
client.post(
"/api/chats/start",
data={"content": "hello", "model_id": "m", "reasoning_effort": "low"},
)
assert db.scalars(select(Chat)).one().params_json["reasoning_effort"] == "low"
def test_a_nonsense_effort_at_the_start_falls_back(client: TestClient, db, registered):
model = _model(db)
model.params_json = {"reasoning_effort": "high"}
db.commit()
client.post(
"/api/chats/start",
data={"content": "hello", "model_id": "m", "reasoning_effort": "extreme"},
)
assert db.scalars(select(Chat)).one().params_json["reasoning_effort"] == "high"