diff --git a/calliope-backend/src/calliope/routers/prompt_tools.py b/calliope-backend/src/calliope/routers/prompt_tools.py index f77e0911..775579d8 100644 --- a/calliope-backend/src/calliope/routers/prompt_tools.py +++ b/calliope-backend/src/calliope/routers/prompt_tools.py @@ -19,6 +19,10 @@ from calliope.config import settings router = APIRouter() TOTAL_TIMEOUT = 120 CHUNK_TIMEOUT = 45 +# Verified against the assigned translate/polish LM Studio service: sending +# reasoning_effort "none" removes reasoning tokens on both the streaming and +# blocking paths, while chat_template_kwargs was silently ignored there. +NO_THINKING_BODY = {"reasoning_effort": "none"} # Ordered, lossless syntax tokens. Ordinary words and bare numbers stay editable. # Tags/dialogue/parameters are opaque before looking for brackets inside them. _TECH = re.compile( @@ -115,8 +119,14 @@ async def _disconnected(request: Request) -> None: await asyncio.sleep(0.05) -async def _run(request: Request, client: LLMClient, messages: list[dict], temp: float) -> str: - upstream = asyncio.create_task(client.chat(messages, temperature=temp)) +async def _run( + request: Request, + client: LLMClient, + messages: list[dict], + temp: float, + extra_body: dict | None = None, +) -> str: + upstream = asyncio.create_task(client.chat(messages, temperature=temp, extra_body=extra_body)) disconnect = asyncio.create_task(_disconnected(request)) try: done, _ = await asyncio.wait({upstream, disconnect}, return_when=asyncio.FIRST_COMPLETED) @@ -192,7 +202,13 @@ async def transform(body: Transform, request: Request) -> dict: timeout=CHUNK_TIMEOUT, ) result = await asyncio.wait_for( - _run(request, client, messages, 0.2 if direction else 0.6), + _run( + request, + client, + messages, + 0.2 if direction else 0.6, + NO_THINKING_BODY, + ), max(0.001, TOTAL_TIMEOUT - (time.monotonic() - started)), ) if not result.strip(): diff --git a/calliope-backend/tests/test_prompt_tools.py b/calliope-backend/tests/test_prompt_tools.py index e7d75176..da16bb60 100644 --- a/calliope-backend/tests/test_prompt_tools.py +++ b/calliope-backend/tests/test_prompt_tools.py @@ -502,3 +502,48 @@ def test_polish_can_rephrase_within_group(client, provider, text, reply): response = call(client, text=text, action="polish") assert response.status_code == 200, response.text assert response.json()["result"] == reply.strip() + + +def test_thinking_off_on_first_and_correction_requests(client, provider): + provider.replies = [ + "broken", + lambda payload: payload["messages"][1]["content"].replace("霓虹灯", "neon lights"), + ] + response = call(client, text="(霓虹灯:1.2)") + assert response.status_code == 200, response.text + assert len(provider.calls) == 2 + assert [call_["reasoning_effort"] for call_ in provider.calls] == ["none", "none"] + assert [call_["temperature"] for call_ in provider.calls] == [0.2, 0.2] + + +def test_polish_thinking_off_on_first_and_correction_requests(client, provider): + provider.replies = ["broken", lambda payload: payload["messages"][1]["content"]] + + response = client.post( + "/api/prompt-tools/transform", json={"action": "polish", "text": "a cat (neon:1.2)"} + ) + assert response.status_code == 200, response.text + assert len(provider.calls) == 2 + assert [call_["reasoning_effort"] for call_ in provider.calls] == ["none", "none"] + assert [call_["temperature"] for call_ in provider.calls] == [0.6, 0.6] + + +def test_blocking_fallback_preserves_thinking_off(client, provider): + provider.blocking = True + provider.reply = lambda payload: payload["messages"][1]["content"].replace("猫", "cat") + assert call(client).status_code == 200 + assert len(provider.calls) == 2 and not provider.calls[1].get("stream") + assert [call_["reasoning_effort"] for call_ in provider.calls] == ["none", "none"] + + +@pytest.mark.asyncio +async def test_plain_llm_call_not_forced_thinking_off(provider): + from calliope.agent.llm import LLMClient + + client = LLMClient(base_url=f"http://127.0.0.1:{provider.server_port}/v1", model="translate") + try: + assert await client.chat([{"role": "user", "content": "hi"}], 0.7) == "a cat" + finally: + await client.close() + assert provider.calls + assert all("reasoning_effort" not in call_ for call_ in provider.calls)