From dd7c629b29f28f8fc8bfa8b64988e6532e219aec Mon Sep 17 00:00:00 2001 From: linhongkuan Date: Wed, 24 Jun 2026 23:06:12 +0800 Subject: [PATCH] fix: skip blank stream response lines --- ollama/_client.py | 4 ++++ tests/test_client.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/ollama/_client.py b/ollama/_client.py index 18cb0fb4..76973015 100644 --- a/ollama/_client.py +++ b/ollama/_client.py @@ -189,6 +189,8 @@ def inner(): raise ResponseError(e.response.text, e.response.status_code) from None for line in r.iter_lines(): + if not line: + continue part = json.loads(line) if err := part.get('error'): raise ResponseError(err) @@ -782,6 +784,8 @@ async def inner(): raise ResponseError(e.response.text, e.response.status_code) from None async for line in r.aiter_lines(): + if not line: + continue part = json.loads(line) if err := part.get('error'): raise ResponseError(err) diff --git a/tests/test_client.py b/tests/test_client.py index 34657513..6cb0c438 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -138,6 +138,31 @@ def generate(): assert part['message']['content'] == next(it) +def test_client_chat_stream_skips_blank_lines(httpserver: HTTPServer): + def stream_handler(_: Request): + def generate(): + yield '\n' + yield json.dumps({'model': 'dummy', 'message': {'role': 'assistant', 'content': 'ok'}}) + '\n' + yield '\n' + + return Response(generate()) + + httpserver.expect_ordered_request( + '/api/chat', + method='POST', + json={ + 'model': 'dummy', + 'messages': [{'role': 'user', 'content': 'ping'}], + 'tools': [], + 'stream': True, + }, + ).respond_with_handler(stream_handler) + + client = Client(httpserver.url_for('/')) + response = list(client.chat('dummy', messages=[{'role': 'user', 'content': 'ping'}], stream=True)) + assert [part['message']['content'] for part in response] == ['ok'] + + @pytest.mark.parametrize('message_format', ('dict', 'pydantic_model')) @pytest.mark.parametrize('file_style', ('path', 'bytes')) def test_client_chat_images(httpserver: HTTPServer, message_format: str, file_style: str, tmp_path): @@ -945,6 +970,32 @@ def generate(): assert part['message']['content'] == next(it) +async def test_async_client_chat_stream_skips_blank_lines(httpserver: HTTPServer): + def stream_handler(_: Request): + def generate(): + yield '\n' + yield json.dumps({'model': 'dummy', 'message': {'role': 'assistant', 'content': 'ok'}}) + '\n' + yield '\n' + + return Response(generate()) + + httpserver.expect_ordered_request( + '/api/chat', + method='POST', + json={ + 'model': 'dummy', + 'messages': [{'role': 'user', 'content': 'ping'}], + 'tools': [], + 'stream': True, + }, + ).respond_with_handler(stream_handler) + + client = AsyncClient(httpserver.url_for('/')) + response = await client.chat('dummy', messages=[{'role': 'user', 'content': 'ping'}], stream=True) + parts = [part async for part in response] + assert [part['message']['content'] for part in parts] == ['ok'] + + async def test_async_client_chat_images(httpserver: HTTPServer): httpserver.expect_ordered_request( '/api/chat',