Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e469746f75 | ||
|
|
31e7884d8f |
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Steward analysis leaking into responses** - Removed internal routing analysis (`DELEGATE: tatlock_core...`) from user-visible reasoning in both streaming and non-streaming paths
|
||||
|
||||
## [2.0.2] - 2026-02-05
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tatlock"
|
||||
version = "2.0.2"
|
||||
version = "2.0.4"
|
||||
description = "OpenAI-compatible API with Ollama backend"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
+22
-18
@@ -7,7 +7,7 @@ import logging
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import APIRouter
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from src.chat import service
|
||||
from src.chat.schemas import (
|
||||
@@ -22,47 +22,51 @@ router = APIRouter(prefix="/chat", tags=["chat"])
|
||||
|
||||
async def _stream_response(
|
||||
request: ChatCompletionRequest,
|
||||
) -> AsyncGenerator[dict, None]:
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
Generate SSE stream for chat completion.
|
||||
|
||||
EventSourceResponse adds "data: " prefix automatically.
|
||||
We just yield the dict/string content.
|
||||
Yields raw SSE-formatted strings matching OpenAI's format exactly:
|
||||
data: {json}\n\n
|
||||
"""
|
||||
try:
|
||||
async for chunk in service.create_chat_completion_stream(request):
|
||||
# Yield dict - EventSourceResponse will format as SSE
|
||||
yield {"data": chunk.model_dump_json()}
|
||||
yield f"data: {chunk.model_dump_json()}\n\n"
|
||||
|
||||
# Send [DONE] message
|
||||
yield {"data": "[DONE]"}
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in streaming response: {e}")
|
||||
error_data = {"error": {"message": str(e), "type": "internal_error"}}
|
||||
yield {"data": json.dumps(error_data)}
|
||||
error_data = json.dumps({"error": {"message": str(e), "type": "internal_error"}})
|
||||
yield f"data: {error_data}\n\n"
|
||||
|
||||
|
||||
@router.post("/completions", response_model=ChatCompletionResponse)
|
||||
async def create_chat_completion(
|
||||
request: ChatCompletionRequest,
|
||||
) -> ChatCompletionResponse | EventSourceResponse:
|
||||
) -> ChatCompletionResponse | StreamingResponse:
|
||||
"""
|
||||
Create chat completion (OpenAI-compatible).
|
||||
|
||||
|
||||
Supports both regular and streaming responses.
|
||||
Currently returns mock lorem ipsum responses.
|
||||
|
||||
|
||||
Args:
|
||||
request: Chat completion request
|
||||
|
||||
|
||||
Returns:
|
||||
Chat completion response or SSE stream
|
||||
"""
|
||||
logger.info(f"Chat completion request for model: {request.model}")
|
||||
|
||||
|
||||
if request.stream:
|
||||
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)
|
||||
|
||||
@@ -651,16 +651,6 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
|
||||
# Build response output items
|
||||
output_items = []
|
||||
|
||||
# Add Steward reasoning as a reasoning output item
|
||||
output_items.append(ReasoningOutputItem(
|
||||
id=f"reasoning_{generate_id()}",
|
||||
summary=[
|
||||
"🎩 Steward's Analysis:",
|
||||
enriched.steward_reasoning,
|
||||
],
|
||||
status="completed"
|
||||
))
|
||||
|
||||
# Add Tatlock's message
|
||||
output_items.append(MessageOutputItem(
|
||||
id=f"msg_{generate_id()}",
|
||||
|
||||
@@ -166,26 +166,6 @@ class StreamingCoordinator:
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
# Stream Steward's analysis as reasoning summary
|
||||
steward_lines = enriched.steward_reasoning.split('\n')
|
||||
for line in steward_lines:
|
||||
if line.strip():
|
||||
yield ReasoningSummaryDelta(delta=line + "\n")
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
yield ReasoningSummaryDone()
|
||||
|
||||
# Add Steward reasoning to output items
|
||||
reasoning_item = ReasoningOutputItem(
|
||||
id=f"reasoning_{generate_id()}",
|
||||
summary=[
|
||||
"🎩 Steward's Analysis:",
|
||||
enriched.steward_reasoning,
|
||||
],
|
||||
status="completed"
|
||||
)
|
||||
output_items.append(reasoning_item)
|
||||
|
||||
# Initialize tool tracker
|
||||
tracker = ToolCallTracker(
|
||||
recommended_capabilities=enriched.recommendation.recommended_capabilities,
|
||||
|
||||
Reference in New Issue
Block a user