Python 64.6%
TypeScript 33.7%
CSS 0.8%
1"""SSE parser: keep-alive comments, fragmented tool_calls, usage chunk, finish_reason."""23import json45import httpx6import pytest7import respx89from app.core.config import Settings10from app.llm.openrouter import LLMClient1112CHUNKS = [13 ": OPENROUTER PROCESSING",14 "",15 'data: {"id":"gen-1","model":"anthropic/claude-sonnet-4.6","choices":[{"index":0,"delta":{"content":"Bonjour"},"finish_reason":null}]}',16 'data: {"id":"gen-1","model":"anthropic/claude-sonnet-4.6","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"financial_calc","arguments":"{\\"function\\": \\"fv_l"}}]},"finish_reason":null}]}',17 ": OPENROUTER PROCESSING",18 'data: {"id":"gen-1","model":"anthropic/claude-sonnet-4.6","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ump\\", \\"params\\": {\\"rate\\": 0.05}}"}}]},"finish_reason":null}]}',19 'data: {"id":"gen-1","model":"anthropic/claude-sonnet-4.6","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}',20 'data: {"id":"gen-1","model":"anthropic/claude-sonnet-4.6","choices":[],"usage":{"prompt_tokens":120,"completion_tokens":30,"cost":0.00123}}',21 "data: [DONE]",22]232425@pytest.mark.asyncio26async def test_parse_fragmented_tool_calls() -> None:27 settings = Settings(OPENROUTER_API_KEY="k", OPENROUTER_BASE_URL="https://or.test/api/v1")28 client = LLMClient(settings)29 body = "\n".join(CHUNKS) + "\n"30 with respx.mock(base_url="https://or.test/api/v1") as mock:31 mock.post("/chat/completions").mock(32 return_value=httpx.Response(200, content=body.encode(),33 headers={"content-type": "text/event-stream"}))34 events = [ev async for ev in client.stream_chat(35 [{"role": "user", "content": "hi"}], None, ["anthropic/claude-sonnet-4.6", "openai/gpt-5.5"])]36 sent = json.loads(mock.calls[0].request.content)37 assert sent["models"] == ["anthropic/claude-sonnet-4.6", "openai/gpt-5.5"]38 assert sent["usage"] == {"include": True}39 assert mock.calls[0].request.headers["X-Title"] == "UQO-Chat"40 types = [e.type for e in events]41 assert types[0] == "text_delta" and events[0].data["delta"] == "Bonjour"42 end = next(e for e in events if e.type == "tool_call_end")43 assert end.data["name"] == "financial_calc"44 assert json.loads(end.data["arguments"]) == {"function": "fv_lump", "params": {"rate": 0.05}}45 usage = next(e for e in events if e.type == "usage")46 assert usage.data["input_tokens"] == 120 and usage.data["cost_usd"] == 0.0012347 done = events[-1]48 assert done.type == "done" and done.data["finish_reason"] == "tool_calls"49 await client.aclose()505152@pytest.mark.asyncio53async def test_retry_then_error() -> None:54 settings = Settings(OPENROUTER_API_KEY="k", OPENROUTER_BASE_URL="https://or.test/api/v1")55 client = LLMClient(settings)56 with respx.mock(base_url="https://or.test/api/v1") as mock:57 mock.post("/chat/completions").mock(return_value=httpx.Response(500, text="boom"))58 with pytest.raises(Exception): # noqa: B01759 _ = [ev async for ev in client.stream_chat([{"role": "user", "content": "x"}], None,60 ["m"])]61 assert len(mock.calls) == 362 await client.aclose()63