From 0aa71636afe8a41a7ce96f4d1841831f509320c0 Mon Sep 17 00:00:00 2001 From: harshitgavita-07 Date: Tue, 6 Oct 2026 22:24:33 +0530 Subject: [PATCH 1/3] fix: ignore empty SSE data frames in Google provider --- src/tau_ai/google.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tau_ai/google.py b/src/tau_ai/google.py index 316304eb9..e0483e283 100644 --- a/src/tau_ai/google.py +++ b/src/tau_ai/google.py @@ -500,7 +500,8 @@ def _parse_sse_line(line: str) -> str | None: line = line.strip() if not line or not line.startswith("data:"): return None - return line.removeprefix("data:").strip() + data = line.removeprefix("data:").strip() + return data or None def _loads_object(value: str) -> dict[str, JSONValue] | None: From c1e4a685803679a7d69e695ffac5a5c8ac4cc615 Mon Sep 17 00:00:00 2001 From: harshitgavita-07 Date: Tue, 6 Oct 2026 22:25:12 +0530 Subject: [PATCH 2/3] fix: ignore empty SSE data frames in OpenAI-compatible provider --- src/tau_ai/openai_compatible.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tau_ai/openai_compatible.py b/src/tau_ai/openai_compatible.py index d0a77fa8b..364468ff4 100644 --- a/src/tau_ai/openai_compatible.py +++ b/src/tau_ai/openai_compatible.py @@ -1259,7 +1259,8 @@ def _parse_sse_line(line: str) -> str | None: line = line.strip() if not line or not line.startswith("data:"): return None - return line.removeprefix("data:").strip() + data = line.removeprefix("data:").strip() + return data or None def _loads_object(value: str) -> dict[str, JSONValue] | None: From e6aaf6e27f4842e30f51866b0abe593dbdd091d6 Mon Sep 17 00:00:00 2001 From: harshitgavita-07 Date: Tue, 6 Oct 2026 22:25:46 +0530 Subject: [PATCH 3/3] test: cover empty SSE data frames for OpenAI-compatible, Google and Mistral providers --- tests/test_sse_empty_data_frames.py | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_sse_empty_data_frames.py diff --git a/tests/test_sse_empty_data_frames.py b/tests/test_sse_empty_data_frames.py new file mode 100644 index 000000000..78c99b8ab --- /dev/null +++ b/tests/test_sse_empty_data_frames.py @@ -0,0 +1,87 @@ +import json +from collections.abc import AsyncIterator + +import httpx +import pytest + +from tau_agent import UserMessage +from tau_ai import AssistantDoneEvent, OpenAICompatibleConfig, OpenAICompatibleProvider +from tau_ai.google import GoogleGenerativeAIProvider +from tau_ai.mistral import MistralConversationsProvider + + +def _sse(*frames: str) -> str: + return "".join(f"{frame}\n\n" for frame in frames) + + +def _chat_chunk(**delta: object) -> str: + return "data: " + json.dumps({"choices": [{"delta": delta}]}) + + +def _google_chunk(text: str, finish: str | None = None) -> str: + candidate: dict[str, object] = {"content": {"parts": [{"text": text}]}} + if finish is not None: + candidate["finishReason"] = finish + return "data: " + json.dumps({"candidates": [candidate]}) + + +def _mistral_chunk(**delta: object) -> str: + return "data: " + json.dumps({"choices": [{"delta": delta}]}) + + +async def _collect(stream: AsyncIterator[object]) -> list[object]: + return [event async for event in stream] + + +_CONFIG = OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1") + +_CASES = { + "openai_compatible": ( + OpenAICompatibleProvider, + _sse( + _chat_chunk(role="assistant"), + "data:", + _chat_chunk(content="ok"), + "data: [DONE]", + ), + "model-x", + ), + "google": ( + GoogleGenerativeAIProvider, + _sse(_google_chunk("o"), "data:", _google_chunk("k", "STOP")), + "gemini-x", + ), + "mistral": ( + MistralConversationsProvider, + _sse( + _mistral_chunk(role="assistant"), + "data:", + _mistral_chunk(content="ok"), + "data: [DONE]", + ), + "mistral-x", + ), +} + + +@pytest.mark.anyio +@pytest.mark.parametrize("name", sorted(_CASES)) +async def test_provider_ignores_empty_data_frames(name: str) -> None: + provider_cls, body, model = _CASES[name] + + def handler(_request: httpx.Request) -> httpx.Response: + return httpx.Response(200, text=body, headers={"content-type": "text/event-stream"}) + + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + provider = provider_cls(_CONFIG, client=client) + events = await _collect( + provider.stream_response( + model=model, + system="You are Tau.", + messages=[UserMessage(content="Say ok")], + tools=[], + ) + ) + + assert isinstance(events[-1], AssistantDoneEvent), events[-1] + assert events[-1].message.text == "ok"