diff --git a/blueprints/conversation/__init__.py b/blueprints/conversation/__init__.py index e2c2752..268f9ea 100644 --- a/blueprints/conversation/__init__.py +++ b/blueprints/conversation/__init__.py @@ -619,7 +619,7 @@ async def stream_query(): _log_summary() yield "data: [DONE]\n\n" - return await make_response( + response = await make_response( event_generator(), 200, { @@ -628,6 +628,10 @@ async def stream_query(): "X-Accel-Buffering": "no", }, ) + # Quart's default 60s response deadline cancels even active SSE streams. + # Silent model calls are bounded separately by MODEL_STALL_TIMEOUT. + response.timeout = None + return response @conversation_blueprint.route("/") diff --git a/tests/unit/test_stream_stall.py b/tests/unit/test_stream_stall.py index 629cf7f..51e14da 100644 --- a/tests/unit/test_stream_stall.py +++ b/tests/unit/test_stream_stall.py @@ -86,7 +86,7 @@ def _final(text): } -async def _run(owner, monkeypatch, script): +async def _run(owner, monkeypatch, script, *, response_timeout=60): user, conversation = owner monkeypatch.setattr(conv, "MODEL_STALL_TIMEOUT", LIMIT) monkeypatch.setattr(conv, "get_agent_for", lambda u: _TimedAgent(script)) @@ -97,6 +97,7 @@ async def _run(owner, monkeypatch, script): monkeypatch.setattr(conv, "build_system_prompt_with_memories", _prompt) app = Quart(__name__) + app.config["RESPONSE_TIMEOUT"] = response_timeout app.config["JWT_SECRET_KEY"] = "test-secret" JWTManager(app) app.register_blueprint(conv.conversation_blueprint) @@ -122,6 +123,22 @@ def test_stall_timeout_only_while_a_model_call_is_in_flight(monkeypatch): assert conv.stall_timeout(100.0, 200.0) == 1.0 # floor +async def test_active_stream_outlasts_response_timeout(owner, monkeypatch): + script = [ + (0, _start("m1")), + (0, _chunk("m1", "")), + (0.15, _chunk("m1", "working")), + (0.15, _chunk("m1", "Answer")), + (0, _end("m1")), + (0, _final("Answer")), + ] + + frames = await _run(owner, monkeypatch, script, response_timeout=0.1) + + assert "error" not in [f["type"] for f in frames] + assert frames[-1] == {"type": "response", "message": "Answer"} + + async def test_long_thinking_is_not_a_stall(owner, monkeypatch): # The whole call takes ~4x the limit, but output never pauses for longer # than half of it; the old time-to-first-content deadline killed this.