Compare commits

...
1 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.5 e469746f75 fix: use StreamingResponse for chat completions SSE
Build and Push / release (push) Successful in 2s
Build and Push / build (push) Successful in 1m22s
sse_starlette's EventSourceResponse added \r\n line endings that
Open WebUI couldn't parse. Switched to plain StreamingResponse with
manual SSE formatting matching OpenAI's exact format.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:29:08 +01:00
3 changed files with 29 additions and 19 deletions
+6
View File
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.0.4] - 2026-02-05
### Fixed
- **Open WebUI streaming compatibility** - Replaced `sse_starlette` `EventSourceResponse` with plain `StreamingResponse` for chat completions; `sse_starlette` added `\r\n` line endings and extra SSE fields that Open WebUI couldn't parse
## [2.0.3] - 2026-02-05 ## [2.0.3] - 2026-02-05
### Fixed ### Fixed
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tatlock" name = "tatlock"
version = "2.0.3" version = "2.0.4"
description = "OpenAI-compatible API with Ollama backend" description = "OpenAI-compatible API with Ollama backend"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [] dependencies = []
+17 -13
View File
@@ -7,7 +7,7 @@ import logging
from typing import AsyncGenerator from typing import AsyncGenerator
from fastapi import APIRouter from fastapi import APIRouter
from sse_starlette.sse import EventSourceResponse from starlette.responses import StreamingResponse
from src.chat import service from src.chat import service
from src.chat.schemas import ( from src.chat.schemas import (
@@ -22,36 +22,33 @@ router = APIRouter(prefix="/chat", tags=["chat"])
async def _stream_response( async def _stream_response(
request: ChatCompletionRequest, request: ChatCompletionRequest,
) -> AsyncGenerator[dict, None]: ) -> AsyncGenerator[str, None]:
""" """
Generate SSE stream for chat completion. Generate SSE stream for chat completion.
EventSourceResponse adds "data: " prefix automatically. Yields raw SSE-formatted strings matching OpenAI's format exactly:
We just yield the dict/string content. data: {json}\n\n
""" """
try: try:
async for chunk in service.create_chat_completion_stream(request): async for chunk in service.create_chat_completion_stream(request):
# Yield dict - EventSourceResponse will format as SSE yield f"data: {chunk.model_dump_json()}\n\n"
yield {"data": chunk.model_dump_json()}
# Send [DONE] message yield "data: [DONE]\n\n"
yield {"data": "[DONE]"}
except Exception as e: except Exception as e:
logger.error(f"Error in streaming response: {e}") logger.error(f"Error in streaming response: {e}")
error_data = {"error": {"message": str(e), "type": "internal_error"}} error_data = json.dumps({"error": {"message": str(e), "type": "internal_error"}})
yield {"data": json.dumps(error_data)} yield f"data: {error_data}\n\n"
@router.post("/completions", response_model=ChatCompletionResponse) @router.post("/completions", response_model=ChatCompletionResponse)
async def create_chat_completion( async def create_chat_completion(
request: ChatCompletionRequest, request: ChatCompletionRequest,
) -> ChatCompletionResponse | EventSourceResponse: ) -> ChatCompletionResponse | StreamingResponse:
""" """
Create chat completion (OpenAI-compatible). Create chat completion (OpenAI-compatible).
Supports both regular and streaming responses. Supports both regular and streaming responses.
Currently returns mock lorem ipsum responses.
Args: Args:
request: Chat completion request request: Chat completion request
@@ -63,6 +60,13 @@ async def create_chat_completion(
if request.stream: if request.stream:
logger.info("Streaming response requested") logger.info("Streaming response requested")
return EventSourceResponse(_stream_response(request)) return StreamingResponse(
_stream_response(request),
media_type="text/event-stream",
headers={
"Cache-Control": "no-store",
"X-Accel-Buffering": "no",
},
)
return await service.create_chat_completion(request) return await service.create_chat_completion(request)