From 1b9c08efcbb8f957de89ed381caa1b3f5fec7a6d Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 6 Jul 2026 18:47:50 +0200 Subject: [PATCH] fix(http): force UTF-8 decoding on the SSE chat stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit llama.cpp sends the streaming response as text/event-stream WITHOUT a charset; requests then does not decode as UTF-8, so iter_lines(decode_unicode=True) mangled multibyte characters (German Umlaute) into double-encoded garbage (e.g. "schön" -> "schön"). The non-streaming path via resp.json() was fine. Set resp.encoding = "utf-8" before iter_lines. Verified live: streamed bytes for "schön" are now c3 b6 (correct UTF-8), file detected as UTF-8. Co-Authored-By: Claude Opus 4.8 --- src/llamacppctl/http_ops.py | 4 ++++ tests/test_http_ops.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/llamacppctl/http_ops.py b/src/llamacppctl/http_ops.py index 8fb2abf..f414914 100644 --- a/src/llamacppctl/http_ops.py +++ b/src/llamacppctl/http_ops.py @@ -145,6 +145,10 @@ def stream_chat( if resp.status_code != 200: raise HttpError(f"chat completion returned HTTP {resp.status_code}: {resp.text[:500]}") + # llama.cpp streams text/event-stream WITHOUT a charset; requests would then + # fall back to a non-UTF-8 decode and mangle multibyte chars (Umlaute). + resp.encoding = "utf-8" + finish_reason = "" for line in resp.iter_lines(decode_unicode=True): if not line or not line.startswith("data:"): diff --git a/tests/test_http_ops.py b/tests/test_http_ops.py index 0085d1a..99fac57 100644 --- a/tests/test_http_ops.py +++ b/tests/test_http_ops.py @@ -163,6 +163,8 @@ def test_stream_chat_yields_content_and_finish(): finishes = [v for k, v in events if k == "finish"] assert "".join(contents) == "Hallo" assert finishes == ["stop"] + # Stream must be forced to UTF-8 (llama.cpp sends no charset -> Umlaut-Bug). + assert mock_resp.encoding == "utf-8" def test_wait_until_ready_succeeds_immediately():