ai: keep slow-CPU chat streams alive

On the FX-8320 the first token took over a minute, and Sanic's default
60-second RESPONSE_TIMEOUT killed the stream before anything was sent.
Raise the timeout above the chat loop's own 600s hard ceiling, and emit
a status event before every model call so the stream is never silent —
for proxies and for the user staring at a slow machine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Roland 2026-08-27 16:28:04 -06:00
parent f64406c730
commit c6f707058d
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
3 changed files with 9 additions and 2 deletions

View file

@ -152,6 +152,9 @@ async def post_chat(request: Request, body: ChatRequest):
return
service.record_llm_activity()
# Always send something before the model call: on slow CPUs the first token can be
# minutes away, and a silent stream looks dead to the UI and to proxies.
await _send_event(response, 'status', dict(message='Thinking…'))
tool_calls = []
final_answer = []
async for event in llama_chat_stream(messages, tools):

View file

@ -92,7 +92,8 @@ async def test_chat_tool_call_then_answer(async_client, ai_enabled, video_factor
assert response.status_code == HTTPStatus.OK
events = _parse_sse(response.text)
names = [e for e, _ in events]
# A status event precedes every model call so slow CPUs never leave the stream silent.
names = [e for e, _ in events if e != 'status']
assert names == ['tool_call', 'tool_result', 'token', 'token', 'done']
tool_call = dict(events)['tool_call']
assert tool_call == dict(tool='search_videos', args=dict(search_str='canning'))
@ -116,7 +117,7 @@ async def test_chat_plain_answer(async_client, ai_enabled):
body = dict(mode='help', messages=[dict(role='user', content='hi')])
with patch:
request, response = await async_client.post('/api/chat/', content=json.dumps(body))
events = _parse_sse(response.text)
events = [e for e in _parse_sse(response.text) if e[0] != 'status']
assert events == [('token', dict(content='Hello!')), ('done', dict(content='Hello!'))]

View file

@ -51,6 +51,9 @@ api_app.config.FALLBACK_ERROR_FORMAT = 'json'
# Caddy is the single proxy in front of the API; trust its X-Forwarded-For so request.remote_addr
# is the browser's address (used by the speed test).
api_app.config.PROXIES_COUNT = 1
# The AI chat streams for as long as CHAT_TIMEOUT_SECONDS (its own hard ceiling); Sanic's 60s
# default killed the stream before a slow CPU produced its first token.
api_app.config.RESPONSE_TIMEOUT = 660
api_bp = Blueprint('RootAPI', url_prefix='/api')