IMPLEMENTATION_PLAN.md: - Phase-by-phase implementation plan - Success criteria for each phase - Testing requirements - Dependencies and prerequisites CLEANUP_TODO.md: - Architecture decision log - Future considerations and trade-offs - Migration path notes - Technical debt tracking These documents provide context for implementation decisions and serve as a reference for future development. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
32 KiB
Implementation Plan: Lorem Tester Agent with Responses API
Overview
Build production-ready OpenAI Responses API (/v1/responses) with Lorem Tester agent. All infrastructure is real code - only mock at PydanticAI interface boundary.
API Format: OpenAI Responses API (NOT Chat Completions) Timeline: Phased implementation, each phase independently testable with Open WebUI
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Responses Router & Service (REAL implementation) │
│ - Conversation history with optional conversation_id │
│ - Streaming coordination (reasoning, tools, content) │
│ - Output array builder (reasoning/function/message items) │
│ - Thinking block formatting │
│ - Error handling │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌────────────────────────┐
│ Agent Interface │ ◄── Abstraction layer
└────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌──────────┐ ┌─────────────┐
│ Lorem │ │ Tatlock │
│ Tester │ │ Agent │
│ │ │ (future) │
│ (Mock │ │ (Real │
│ PydanticAI) │ PydanticAI) │
└──────────┘ └─────────────┘
Phase 1: Agent Interface & Multi-Model Support
1.1 Create Agent Interface Abstraction
File: src/agents/base.py
from abc import ABC, abstractmethod
from typing import AsyncGenerator
from src.responses.schemas import OutputItem
class AgentInterface(ABC):
"""Abstract interface for all agents (Lorem Tester, Tatlock, etc.)"""
@abstractmethod
async def generate_response(
self,
messages: list[dict],
**kwargs
) -> AsyncGenerator[OutputItem, None]:
"""
Generate streaming response as output items.
Yields: OutputItem objects (reasoning, function_call, message)
"""
pass
@abstractmethod
async def supports_tools(self) -> bool:
"""Whether agent supports function calling"""
pass
@abstractmethod
async def supports_reasoning(self) -> bool:
"""Whether agent provides reasoning summaries"""
pass
@abstractmethod
async def get_capabilities(self) -> dict:
"""Return agent capabilities for model listing"""
pass
File: src/agents/lorem_tester.py
class LoremTesterAgent(AgentInterface):
"""
Mock agent that implements all Responses API features with Lorem Ipsum.
Features:
- Reasoning summaries (mock step-by-step thinking)
- Function calling (mock tool execution)
- Multi-turn conversations
- Error scenarios (triggered by keywords)
This is the ONLY mock part - everything else is real production code.
"""
async def generate_response(
self,
messages: list[dict],
reasoning: dict | None = None,
tools: list[dict] | None = None,
**kwargs
) -> AsyncGenerator[OutputItem, None]:
"""
Mock PydanticAI interface - generates fake reasoning and responses.
Real implementation would call PydanticAI here.
"""
# 1. Yield reasoning item if requested
if reasoning and reasoning.get("summary") == "auto":
yield OutputItem(
type="reasoning",
id=f"rs_{generate_id()}",
summary=self._generate_mock_reasoning(messages)
)
# 2. Randomly yield function calls (30% chance)
if tools and random.random() < 0.3:
yield OutputItem(
type="function_call",
id=f"fc_{generate_id()}",
name=random.choice([t["name"] for t in tools]),
arguments=self._generate_mock_args()
)
# 3. Yield final message
yield OutputItem(
type="message",
id=f"msg_{generate_id()}",
role="assistant",
content=[{
"type": "output_text",
"text": self._generate_lorem_ipsum()
}]
)
def _generate_mock_reasoning(self, messages: list[dict]) -> list[str]:
"""Generate fake reasoning steps"""
return [
"Analyzing the user's request and context...",
"Considering available information and constraints...",
"Formulating a comprehensive response strategy...",
"Selecting appropriate lorem ipsum content..."
]
File: src/agents/tatlock.py
class TatlockAgent(AgentInterface):
"""Placeholder for future real agent with PydanticAI"""
async def generate_response(self, messages, **kwargs):
"""Minimal placeholder implementation"""
yield OutputItem(
type="message",
id=f"msg_{generate_id()}",
role="assistant",
content=[{
"type": "output_text",
"text": "Tatlock agent not yet implemented"
}]
)
async def supports_tools(self) -> bool:
return False # Not yet
async def supports_reasoning(self) -> bool:
return False # Not yet
1.2 Model Registry
File: src/agents/registry.py
from src.agents.lorem_tester import LoremTesterAgent
from src.agents.tatlock import TatlockAgent
class ModelRegistry:
"""Central registry for all available models"""
MODELS = {
"lorem-tester": {
"agent_class": LoremTesterAgent,
"capabilities": {
"streaming": True,
"reasoning": True,
"tools": True,
"vision": False,
"audio": False
},
"description": "Testing agent with mock Responses API features",
"created": 1733529600, # 2025-12-06
},
"tatlock": {
"agent_class": TatlockAgent,
"capabilities": {
"streaming": True,
"reasoning": False, # Not yet
"tools": False, # Not yet
"vision": False,
"audio": False
},
"description": "Tatlock reasoning agent (placeholder)",
"created": 1733529600,
}
}
@classmethod
def get_agent(cls, model_id: str) -> AgentInterface:
"""Instantiate agent for given model"""
if model_id not in cls.MODELS:
raise ValueError(f"Model {model_id} not found")
agent_class = cls.MODELS[model_id]["agent_class"]
return agent_class()
@classmethod
def list_models(cls) -> list[dict]:
"""Return all models in OpenAI format"""
return [
{
"id": model_id,
"object": "model",
"created": config["created"],
"owned_by": "tatlock",
"capabilities": config["capabilities"],
"description": config["description"]
}
for model_id, config in cls.MODELS.items()
]
1.3 Update Models Endpoint
File: src/models/service.py
from src.agents.registry import ModelRegistry
async def list_models() -> dict:
"""List all available models"""
return {
"object": "list",
"data": ModelRegistry.list_models()
}
Tests:
- Model registry returns both models
- Each model has correct capabilities
- GET /v1/models returns proper format
- Invalid model selection returns 404
Phase 2: Responses API Core Structure
2.1 Response Schemas
File: src/responses/schemas.py
from src.core.models import CustomBaseModel
from typing import Literal
class OutputTextContent(CustomBaseModel):
"""Text content in message output"""
type: Literal["output_text"] = "output_text"
text: str
annotations: list[dict] = []
class MessageOutputItem(CustomBaseModel):
"""Message item in output array"""
type: Literal["message"] = "message"
id: str
role: Literal["assistant"] = "assistant"
content: list[OutputTextContent]
status: Literal["completed"] | None = "completed"
class ReasoningOutputItem(CustomBaseModel):
"""Reasoning item in output array"""
type: Literal["reasoning"] = "reasoning"
id: str
summary: list[str]
status: Literal["completed"] | None = "completed"
class FunctionCallOutputItem(CustomBaseModel):
"""Function call item in output array"""
type: Literal["function_call"] = "function_call"
id: str
name: str
arguments: str # JSON string
status: Literal["completed"] | None = "completed"
# Union type for output items
OutputItem = MessageOutputItem | ReasoningOutputItem | FunctionCallOutputItem
class ResponseUsage(CustomBaseModel):
"""Token usage statistics"""
input_tokens: int
output_tokens: int
reasoning_tokens: int = 0
total_tokens: int
class ResponseRequest(CustomBaseModel):
"""Responses API request"""
model: str
input: list[dict] # Previous responses or messages
reasoning: dict | None = None # {"effort": "medium", "summary": "auto"}
tools: list[dict] | None = None
metadata: dict | None = None # Custom field for conversation_id
stream: bool = False
max_output_tokens: int | None = None
temperature: float = 1.0
stop: list[str] | None = None
class Response(CustomBaseModel):
"""Complete response object"""
id: str
object: Literal["response"] = "response"
created_at: int
model: str
status: Literal["completed", "in_progress", "failed"]
output: list[OutputItem]
usage: ResponseUsage
2.2 Streaming Event Schemas
File: src/responses/streaming.py
from enum import Enum
class StreamEventType(str, Enum):
"""Streaming event types"""
REASONING_SUMMARY_DELTA = "response.reasoning_summary_text.delta"
REASONING_SUMMARY_DONE = "response.reasoning_summary_text.done"
OUTPUT_TEXT_DELTA = "response.output_text.delta"
OUTPUT_TEXT_DONE = "response.output_text.done"
FUNCTION_CALL_DELTA = "response.function_call_arguments.delta"
FUNCTION_CALL_DONE = "response.function_call_arguments.done"
RESPONSE_DONE = "response.done"
class StreamEvent(CustomBaseModel):
"""Base streaming event"""
event: StreamEventType
data: dict
# Specific event types
class ReasoningSummaryDelta(CustomBaseModel):
event: Literal[StreamEventType.REASONING_SUMMARY_DELTA]
delta: str
class OutputTextDelta(CustomBaseModel):
event: Literal[StreamEventType.OUTPUT_TEXT_DELTA]
delta: str
class FunctionCallDelta(CustomBaseModel):
event: Literal[StreamEventType.FUNCTION_CALL_DELTA]
delta: str
name: str | None = None # Only in first chunk
class ResponseDone(CustomBaseModel):
event: Literal[StreamEventType.RESPONSE_DONE]
response: Response
2.3 Responses Router
File: src/responses/router.py
from fastapi import APIRouter, HTTPException
from sse_starlette.sse import EventSourceResponse
from src.responses import service
from src.responses.schemas import ResponseRequest, Response
router = APIRouter(prefix="/responses", tags=["responses"])
@router.post("", response_model=Response)
async def create_response(
request: ResponseRequest,
) -> Response | EventSourceResponse:
"""
Create a response using Responses API format.
Supports:
- Reasoning summaries
- Function calling
- Streaming
- Multi-turn conversations
Args:
request: Response request
Returns:
Response object or SSE stream
"""
if request.stream:
return EventSourceResponse(
service.create_response_stream(request)
)
return await service.create_response(request)
2.4 Responses Service
File: src/responses/service.py
import time
from src.agents.registry import ModelRegistry
from src.responses.schemas import Response, OutputItem, ResponseUsage
from src.responses.streaming import StreamingCoordinator
async def create_response(request: ResponseRequest) -> Response:
"""Create non-streaming response"""
# Get agent for model
agent = ModelRegistry.get_agent(request.model)
# Collect all output items from agent
output_items = []
async for item in agent.generate_response(
messages=request.input,
reasoning=request.reasoning,
tools=request.tools,
temperature=request.temperature,
max_tokens=request.max_output_tokens,
):
output_items.append(item)
# Calculate token usage (real counting for production)
usage = _calculate_usage(request.input, output_items)
return Response(
id=f"resp_{generate_id()}",
created_at=int(time.time()),
model=request.model,
status="completed",
output=output_items,
usage=usage
)
async def create_response_stream(request: ResponseRequest):
"""Create streaming response"""
coordinator = StreamingCoordinator()
async for event in coordinator.stream_response(request):
yield {
"event": event.event,
"data": event.model_dump_json()
}
Tests:
- Non-streaming response has correct structure
- Output array contains reasoning, function, message items
- Token usage calculated correctly
- Model selection works
Phase 3: Conversation History with Hybrid Approach
3.1 Conversation Storage
File: src/responses/history.py
from typing import Dict, List
import hashlib
class ConversationHistory:
"""
Real conversation history management.
Supports hybrid approach:
- Client sends full input array (OpenAI compatible)
- Optional conversation_id in metadata for server-side grouping
- Server can augment with vector memories (future)
"""
def __init__(self, max_turns: int = 20):
self._conversations: Dict[str, List[Response]] = {}
self._max_turns = max_turns
async def get_conversation_id(
self,
request: ResponseRequest
) -> str:
"""
Get or generate conversation ID.
Priority:
1. metadata.conversation_id if provided
2. Generate from first message hash
"""
if request.metadata and "conversation_id" in request.metadata:
return request.metadata["conversation_id"]
# Generate deterministic ID from first message
first_msg = str(request.input[0]) if request.input else ""
return hashlib.sha256(first_msg.encode()).hexdigest()[:16]
async def add_response(
self,
conversation_id: str,
response: Response
):
"""Add response to conversation history"""
if conversation_id not in self._conversations:
self._conversations[conversation_id] = []
self._conversations[conversation_id].append(response)
# Trim old turns
await self._trim_history(conversation_id)
async def get_history(
self,
conversation_id: str
) -> List[Response]:
"""Retrieve conversation history"""
return self._conversations.get(conversation_id, [])
async def _trim_history(self, conversation_id: str):
"""Keep only recent turns within limit"""
if len(self._conversations[conversation_id]) > self._max_turns:
self._conversations[conversation_id] = (
self._conversations[conversation_id][-self._max_turns:]
)
# Future: Integration point for Qdrant vector memory
async def get_relevant_memories(
self,
conversation_id: str,
query: str
) -> List[dict]:
"""
Retrieve relevant memories from vector store.
TODO: Integrate Qdrant for semantic search
"""
return [] # Placeholder
3.2 Context Window Management
File: src/responses/context.py
class ContextWindow:
"""Manage token limits and context trimming"""
def __init__(self, max_tokens: int = 4096):
self.max_tokens = max_tokens
async def count_tokens(self, items: list) -> int:
"""
Real token counting.
For now: approximate by character count
Future: Use tiktoken or similar
"""
total_chars = sum(
len(str(item))
for item in items
)
return total_chars // 4 # Rough approximation
async def trim_to_fit(
self,
items: list,
reserve_tokens: int = 512
) -> list:
"""Trim old items to fit context window"""
available = self.max_tokens - reserve_tokens
# Start from most recent, work backwards
kept_items = []
current_tokens = 0
for item in reversed(items):
item_tokens = await self.count_tokens([item])
if current_tokens + item_tokens <= available:
kept_items.insert(0, item)
current_tokens += item_tokens
else:
break
return kept_items
Tests:
- Conversation ID generation (with and without metadata)
- History storage and retrieval
- Context window trimming
- Token counting
Phase 4: Streaming Coordinator
4.1 Streaming Implementation
File: src/responses/streaming.py
class StreamingCoordinator:
"""
Real production streaming logic.
Coordinates complex streaming:
1. Reasoning summary chunks
2. Function call arguments
3. Output text chunks
4. Error handling
5. Final response event
"""
async def stream_response(
self,
request: ResponseRequest
) -> AsyncGenerator[StreamEvent, None]:
"""Coordinate streaming from agent"""
agent = ModelRegistry.get_agent(request.model)
output_items = []
current_reasoning = []
current_function = None
current_message = ""
try:
async for item in agent.generate_response(
messages=request.input,
reasoning=request.reasoning,
tools=request.tools,
**request.model_dump(exclude={"input", "reasoning", "tools"})
):
output_items.append(item)
# Stream based on item type
if item.type == "reasoning":
# Stream reasoning summary
for step in item.summary:
yield ReasoningSummaryDelta(
event=StreamEventType.REASONING_SUMMARY_DELTA,
delta=step
)
yield StreamEvent(
event=StreamEventType.REASONING_SUMMARY_DONE,
data={}
)
elif item.type == "function_call":
# Stream function call arguments
# First chunk includes name
yield FunctionCallDelta(
event=StreamEventType.FUNCTION_CALL_DELTA,
name=item.name,
delta=""
)
# Stream arguments in chunks
args = item.arguments
chunk_size = 20
for i in range(0, len(args), chunk_size):
yield FunctionCallDelta(
event=StreamEventType.FUNCTION_CALL_DELTA,
delta=args[i:i+chunk_size]
)
yield StreamEvent(
event=StreamEventType.FUNCTION_CALL_DONE,
data={}
)
elif item.type == "message":
# Stream output text
text = item.content[0].text
words = text.split()
for word in words:
yield OutputTextDelta(
event=StreamEventType.OUTPUT_TEXT_DELTA,
delta=f"{word} "
)
# Simulate typing delay (for testing)
await asyncio.sleep(0.05)
yield StreamEvent(
event=StreamEventType.OUTPUT_TEXT_DONE,
data={}
)
# Final response.done event with complete response
usage = _calculate_usage(request.input, output_items)
final_response = Response(
id=f"resp_{generate_id()}",
created_at=int(time.time()),
model=request.model,
status="completed",
output=output_items,
usage=usage
)
yield ResponseDone(
event=StreamEventType.RESPONSE_DONE,
response=final_response
)
except Exception as e:
# Stream error
yield self._create_error_event(e)
Tests:
- Reasoning summary streaming
- Function call argument streaming
- Output text streaming word-by-word
- response.done event with complete response
- Error mid-stream handling
Phase 5: Error Handling
5.1 Error Scenarios in Lorem Tester
File: src/agents/lorem_tester.py (additions)
class LoremTesterAgent:
"""
Trigger errors based on keywords in input for testing:
- "trigger_rate_limit" → 429 rate limit error
- "trigger_timeout" → timeout after 121s
- "trigger_invalid_tool" → invalid tool call error
- "trigger_context_overflow" → context length exceeded
- "trigger_partial_failure" → error mid-stream
"""
async def generate_response(self, messages, **kwargs):
# Check for error triggers
last_message = str(messages[-1]) if messages else ""
if "trigger_rate_limit" in last_message:
raise RateLimitError("Rate limit exceeded (mock)")
if "trigger_timeout" in last_message:
await asyncio.sleep(121) # Exceed default timeout
if "trigger_context_overflow" in last_message:
raise ContextLengthError("Context length exceeded (mock)")
if "trigger_partial_failure" in last_message:
# Yield some items then fail
yield OutputItem(...)
yield OutputItem(...)
raise APIError("Simulated mid-stream failure")
# Normal flow...
5.2 Error Responses
File: src/responses/schemas.py (additions)
class ErrorResponse(CustomBaseModel):
"""Error response format"""
error: dict
class ErrorDetail(CustomBaseModel):
type: str
message: str
code: int | None = None
File: src/responses/service.py (additions)
async def create_response_stream(request):
"""Create streaming response with error handling"""
coordinator = StreamingCoordinator()
try:
async for event in coordinator.stream_response(request):
yield {"event": event.event, "data": event.model_dump_json()}
except RateLimitError as e:
yield {
"event": "error",
"data": json.dumps({
"type": "rate_limit_exceeded",
"message": str(e),
"code": 429
})
}
except ContextLengthError as e:
yield {
"event": "error",
"data": json.dumps({
"type": "context_length_exceeded",
"message": str(e),
"code": 400
})
}
except Exception as e:
yield {
"event": "error",
"data": json.dumps({
"type": "internal_error",
"message": str(e),
"code": 500
})
}
Tests:
- All error trigger keywords
- Mid-stream failure handling
- Error event format
- Graceful degradation
Phase 6: Advanced Features
6.1 Stop Sequences
class StreamingCoordinator:
async def _check_stop_sequence(
self,
accumulated_text: str,
stop_sequences: list[str]
) -> bool:
"""Check if any stop sequence encountered"""
return any(seq in accumulated_text for seq in stop_sequences)
6.2 Token Limits
class StreamingCoordinator:
async def _enforce_max_tokens(
self,
token_count: int,
max_tokens: int | None
) -> bool:
"""Stop streaming if max tokens reached"""
if max_tokens and token_count >= max_tokens:
return True
return False
6.3 Parameter Validation
File: src/responses/schemas.py (additions)
from pydantic import Field, field_validator
class ResponseRequest(CustomBaseModel):
# ... existing fields
temperature: float = Field(
default=1.0,
ge=0.0,
le=2.0,
description="Sampling temperature"
)
@field_validator('reasoning')
def validate_reasoning(cls, v):
if v is not None:
if 'effort' in v:
allowed = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh']
if v['effort'] not in allowed:
raise ValueError(f"reasoning.effort must be one of {allowed}")
return v
Tests:
- Stop sequence detection
- Max token enforcement
- Temperature validation
- Reasoning effort validation
Phase 7: Testing & Integration
7.1 Test Suite Structure
tests/
├── responses/
│ ├── test_router.py # API endpoint tests
│ ├── test_service.py # Service layer tests
│ ├── test_streaming.py # Streaming tests
│ └── test_history.py # Conversation history tests
├── agents/
│ ├── test_lorem_tester.py # Lorem Tester agent tests
│ ├── test_registry.py # Model registry tests
│ └── test_interface.py # Agent interface tests
└── integration/
└── test_openwebui.py # Open WebUI integration tests
7.2 Open WebUI Integration Testing
Manual Test Checklist:
- Both models appear in model dropdown
- Lorem Tester shows thinking/reasoning bubbles
- Reasoning displays separately from response
- Tool calls show in visual flow
- Tool execution progress visible
- Final answer clearly separated
- Conversation history maintained across turns
- Error messages display gracefully
- Streaming smooth and responsive
- All trigger keywords work for testing
7.3 Documentation
File: OPENWEBUI_INTEGRATION.md
# Open WebUI Integration Guide
## Connection Setup
1. In Open WebUI, go to Settings → Connections
2. Add new OpenAI-compatible API:
- Base URL: `http://localhost:8000/v1`
- API Key: (leave empty for now)
- API Type: OpenAI
## Available Models
### lorem-tester
Testing model with all Responses API features:
- Reasoning summaries (thinking bubbles)
- Function calling (mock tools)
- Error scenarios (trigger keywords)
**Test Keywords**:
- Include "trigger_rate_limit" to test rate limit errors
- Include "trigger_timeout" to test timeout handling
- Include "trigger_partial_failure" to test mid-stream errors
### tatlock
Placeholder for production agent (minimal implementation)
## Visual Features
Lorem Tester demonstrates:
- **Thinking Bubbles**: Separate reasoning display
- **Tool Progress**: Visual flow for function calls
- **Final Answer**: Clear separation from thinking
- **Error Handling**: Graceful error messages
## Conversation History
The API supports hybrid conversation tracking:
- Send full conversation in `input` array (OpenAI compatible)
- Optional: Include `metadata.conversation_id` for server-side grouping
- Server maintains history for context window management
- Future: Vector memory integration via conversation_id
Phase 8: Cleanup Old API
File: CLEANUP_TODO.md
# Cleanup Todo: Remove Chat Completions API
After Responses API is fully tested and integrated, remove old API:
## Code Removal
- [ ] Delete `src/chat/` directory entirely
- router.py
- service.py
- schemas.py
- constants.py
- dependencies.py
- [ ] Remove chat imports from `src/main.py`
- Remove chat router registration
- Remove `/v1/chat/completions` route
- [ ] Update core router (`src/core/router.py`)
- Remove any chat-specific health checks
## Documentation Updates
- [ ] README.md
- Remove chat completions examples
- Update all examples to use Responses API
- Update architecture diagrams
- Update feature list
- [ ] AGENTS.md
- Remove chat completions references
- Update all code examples to Responses API
- Update streaming patterns
- [ ] CHANGELOG.md
- Add deprecation notice for v0.1.0
- Document Responses API adoption
## Test Cleanup
- [ ] Delete `tests/chat/` directory
- [ ] Remove chat-related fixtures from `conftest.py`
- [ ] Update integration tests
## Configuration Cleanup
- [ ] Review `src/core/config.py` for chat-specific settings
- [ ] Remove unused constants
## Verification
- [ ] All tests pass without chat code
- [ ] Open WebUI integration still works
- [ ] Documentation is consistent
- [ ] No broken imports
Project Structure (Final)
src/
├── agents/ # Agent implementations
│ ├── base.py # AgentInterface abstraction
│ ├── lorem_tester.py # Mock agent with all features
│ ├── tatlock.py # Placeholder real agent
│ └── registry.py # Model registry
├── responses/ # Responses API domain
│ ├── router.py # POST /v1/responses
│ ├── schemas.py # Request/response/output items
│ ├── service.py # Response generation logic
│ ├── streaming.py # Streaming coordinator
│ ├── history.py # Conversation history
│ └── context.py # Context window management
├── models/
│ ├── router.py # GET /v1/models
│ ├── schemas.py # Model schemas
│ └── service.py # Model listing
├── core/
│ ├── config.py # Global configuration
│ ├── models.py # Custom Pydantic base
│ ├── exceptions.py # Custom exceptions
│ └── router.py # Health/root endpoints
└── main.py # Application factory
tests/
├── responses/ # Responses API tests
├── agents/ # Agent tests
├── models/ # Model tests
└── integration/ # Open WebUI integration
docs/
├── OPENWEBUI_INTEGRATION.md # Integration guide
├── CLEANUP_TODO.md # Old API removal checklist
└── IMPLEMENTATION_PLAN.md # This file
Implementation Order
- Phase 1 - Agent interface, registry, model selection
- Phase 2 - Responses API schemas and core service
- Phase 3 - Conversation history with hybrid approach
- Phase 4 - Streaming coordinator with all event types
- Phase 5 - Error handling and trigger scenarios
- Phase 6 - Advanced features (stop, tokens, validation)
- Phase 7 - Testing and Open WebUI integration
- Phase 8 - Clean out old Chat Completions API
Each phase is independently testable with Open WebUI.
Questions Resolved
- ✅ Conversation tracking: Hybrid approach with optional conversation_id
- ✅ Persistence: In-memory for now, Qdrant integration point ready
- ✅ Tool execution: Mock only for Lorem Tester
- ✅ API format: Responses API only (not Chat Completions)
Next Steps
Ready to begin Phase 1: Agent Interface & Multi-Model Support