From 8dedaf469720118598276f69ab87674c7c1d1fcb Mon Sep 17 00:00:00 2001 From: CodexManvik Date: Mon, 9 Nov 2026 03:44:19 +0530 Subject: [PATCH] Decode the LLM event stream as UTF-8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groq's event stream carries no charset, so requests decoded it as ISO-8859-1 and streamed answers showed mojibake (for example around µmol/kg and narrow spaces). Read bytes and decode each line as UTF-8. Co-Authored-By: Claude Opus 5.5 --- floatchat/llm.py | 5 ++++- tests/unit/test_llm.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/floatchat/llm.py b/floatchat/llm.py index 63631d1..12ff52a 100644 --- a/floatchat/llm.py +++ b/floatchat/llm.py @@ -77,7 +77,10 @@ def chat_stream(prompt: str, system: str | None = None, temperature: float = 0.2 return try: resp = _post(_payload(prompt, system, temperature, max_tokens, json_mode=False, stream=True), stream=True) - for line in resp.iter_lines(decode_unicode=True): + # Read bytes and decode as UTF-8: event streams often have no charset, and requests would then fall back to + # ISO-8859-1 and garble characters such as "µ" or narrow spaces + for raw in resp.iter_lines(): + line = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw if not line or not line.startswith("data: "): continue data = line[len("data: "):] diff --git a/tests/unit/test_llm.py b/tests/unit/test_llm.py index ade26fe..a56227e 100644 --- a/tests/unit/test_llm.py +++ b/tests/unit/test_llm.py @@ -74,6 +74,14 @@ def test_stream_yields_tokens(monkeypatch): assert sent[0]["stream"] is True +def test_stream_decodes_utf8_bytes(monkeypatch): + # Event streams usually carry no charset; the bytes must still be read as UTF-8 + text = "O₂ minimum 0.8 µmol/kg" + line = ("data: " + json.dumps({"choices": [{"delta": {"content": text}}]}, ensure_ascii=False)).encode("utf-8") + install(monkeypatch, [FakeResponse(lines=[line, b"data: [DONE]"])]) + assert "".join(llm.chat_stream("q")) == text + + def test_stream_yields_nothing_without_an_llm(monkeypatch): monkeypatch.setattr(config, "LLM_BASE_URL", "") assert list(llm.chat_stream("q")) == []