fix(characters): retry empty character-sheet extraction with thinking disabled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a280ddacde
commit
a9b4a493d8
@ -5,6 +5,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Character-sheet extraction returned nothing with a reasoning model**: the blocking `/api/character-sheets` call could spend its whole token budget thinking and answer with empty content. It now retries once with thinking switched off (and once more without the switch if the backend rejects it).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.34.0] — 2026-09-19
|
## [1.34.0] — 2026-09-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -1271,6 +1271,22 @@ async def character_sheets(request: Request):
|
|||||||
_attribution_llm_lock.release()
|
_attribution_llm_lock.release()
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
_msg = resp.json()["choices"][0]["message"]
|
_msg = resp.json()["choices"][0]["message"]
|
||||||
|
raw = (_msg.get("content") or "").strip()
|
||||||
|
if not raw:
|
||||||
|
# A reasoning model can spend the whole token budget thinking and return no answer at all
|
||||||
|
# (the prompt's /no-think is ignored). Ask once more with thinking switched off.
|
||||||
|
retry_payload = dict(prep["payload"], chat_template_kwargs={"enable_thinking": False})
|
||||||
|
headers = {"Authorization": f"Bearer {prep['api_key']}"}
|
||||||
|
if not await asyncio.to_thread(_attribution_llm_lock.acquire, True, prep["timeout_seconds"]):
|
||||||
|
raise HTTPException(503, "Attribution engine busy")
|
||||||
|
try:
|
||||||
|
resp = await asyncio.to_thread(_post_llm_chat_completion, prep["llm_url"], retry_payload, headers, prep["timeout_seconds"])
|
||||||
|
if resp.status_code in (400, 422): # a backend that rejects the switch
|
||||||
|
resp = await asyncio.to_thread(_post_llm_chat_completion, prep["llm_url"], prep["payload"], headers, prep["timeout_seconds"])
|
||||||
|
finally:
|
||||||
|
_attribution_llm_lock.release()
|
||||||
|
resp.raise_for_status()
|
||||||
|
_msg = resp.json()["choices"][0]["message"]
|
||||||
raw = (_msg.get("content") or _reasoning_text(_msg) or "").strip()
|
raw = (_msg.get("content") or _reasoning_text(_msg) or "").strip()
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@ -254,3 +254,39 @@ class AttributionParserTests(unittest.TestCase):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterSheetsEmptyAnswerTests(unittest.TestCase):
|
||||||
|
"""A reasoning model that spends its budget thinking returns no content; the endpoint retries with thinking off."""
|
||||||
|
|
||||||
|
def _client(self):
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
import routes.conversation as conv
|
||||||
|
app = FastAPI(); app.include_router(conv.router)
|
||||||
|
return TestClient(app), conv
|
||||||
|
|
||||||
|
def _resp(self, content, status=200):
|
||||||
|
from unittest import mock
|
||||||
|
r = mock.Mock(); r.status_code = status; r.raise_for_status = lambda: None
|
||||||
|
r.json.return_value = {"choices": [{"message": {"content": content, "reasoning_content": "thinking..."}}]}
|
||||||
|
return r
|
||||||
|
|
||||||
|
def test_retries_once_with_thinking_disabled_when_the_answer_is_empty(self):
|
||||||
|
from unittest import mock
|
||||||
|
client, conv = self._client()
|
||||||
|
good = '{"sheets":[{"name":"Linosch","physical":"alter Zwerg"}],"characters":["Linosch"]}'
|
||||||
|
with mock.patch.object(conv, "_post_llm_chat_completion", side_effect=[self._resp(""), self._resp(good)]) as post:
|
||||||
|
r = client.post("/api/character-sheets", json={"text": "Linosch trat ein.", "known_characters": ["Linosch"]})
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertEqual(r.json()["sheets"][0]["physical"], "alter Zwerg")
|
||||||
|
self.assertEqual(post.call_count, 2)
|
||||||
|
self.assertEqual(post.call_args_list[1].args[1].get("chat_template_kwargs"), {"enable_thinking": False})
|
||||||
|
|
||||||
|
def test_no_retry_when_the_first_answer_has_content(self):
|
||||||
|
from unittest import mock
|
||||||
|
client, conv = self._client()
|
||||||
|
good = '{"sheets":[{"name":"A","physical":"x"}],"characters":["A"]}'
|
||||||
|
with mock.patch.object(conv, "_post_llm_chat_completion", return_value=self._resp(good)) as post:
|
||||||
|
client.post("/api/character-sheets", json={"text": "A kam.", "known_characters": ["A"]})
|
||||||
|
self.assertEqual(post.call_count, 1)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user