Add Responses API with streaming, history, and advanced features

Implements Phases 2, 3, and 6: Complete Responses API implementation

Core API (Phase 2):
- OpenAI Responses API format with structured output items
- Streaming and non-streaming support via SSE-Starlette
- Reasoning items (thinking summaries)
- Function call items (tool execution)
- Message items (assistant responses)
- Router, schemas, service, and streaming coordinator

Conversation History (Phase 3):
- Hybrid client/server approach
- Auto-generated deterministic conversation IDs
- Configurable max turns with automatic trimming
- Context window management with token counting
- Token usage statistics
- Placeholder for future vector memory integration

Advanced Features (Phase 6):
- Parameter validation with Pydantic field validators:
  - Temperature: 0.0-2.0 range enforcement
  - Reasoning effort: 6 levels (none to xhigh)
  - Max output tokens: positive integer enforcement
  - Stop sequences: up to 4, non-empty strings
- Real-time stop sequence detection during streaming
- Real-time max tokens enforcement with token counting
- Graceful error handling and OpenAI-compatible error format

Testing:
- 9 unit tests for API endpoints and streaming
- 11 unit tests for error handling
- 13 unit tests for conversation history and context
- 12 unit tests for advanced features and validation
- Total: 45 tests with comprehensive coverage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-06 19:38:00 +01:00
co-authored by Claude
parent 4e6ca4466b
commit ff6c3cf1b5
12 changed files with 2680 additions and 0 deletions
+178
View File
@@ -0,0 +1,178 @@
"""
Context window management and token counting.
Handles:
- Token counting (approximate for now, exact in future)
- Context trimming to fit model limits
- Reserve tokens for output generation
"""
from typing import Any
class ContextWindow:
"""
Manage token limits and context trimming.
Real production context window management:
- Approximate token counting (character-based)
- Context trimming (keep most recent within limits)
- Reserve tokens for model output
- Future: Integration with tiktoken for exact counts
"""
def __init__(self, max_tokens: int = 4096):
"""
Initialize context window manager.
Args:
max_tokens: Maximum context window size in tokens
"""
self.max_tokens = max_tokens
async def count_tokens(self, items: list[Any]) -> int:
"""
Count tokens in items.
Current implementation: Approximate via character count
Future: Use tiktoken or similar for exact token counts
Approximation: ~4 characters per token (common for English text)
Args:
items: List of items to count (messages, strings, dicts, etc.)
Returns:
int: Approximate token count
"""
total_chars = 0
for item in items:
if isinstance(item, str):
total_chars += len(item)
elif isinstance(item, dict):
# Count all values in dict
total_chars += len(str(item))
else:
# Fallback: convert to string
total_chars += len(str(item))
# Approximate: 4 characters per token
return total_chars // 4
async def trim_to_fit(
self,
items: list[Any],
reserve_tokens: int = 512
) -> list[Any]:
"""
Trim items to fit within context window.
Strategy:
1. Reserve tokens for output generation
2. Keep most recent items that fit
3. Drop oldest items first
Args:
items: List of items to trim
reserve_tokens: Tokens to reserve for output (default 512)
Returns:
list: Trimmed list of items that fit in context
"""
available_tokens = self.max_tokens - reserve_tokens
if available_tokens <= 0:
# Edge case: reserve too large
return []
# 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_tokens:
# Fits - keep this item
kept_items.insert(0, item)
current_tokens += item_tokens
else:
# Doesn't fit - stop here
break
return kept_items
async def fits_in_context(
self,
items: list[Any],
reserve_tokens: int = 512
) -> bool:
"""
Check if items fit within context window.
Args:
items: List of items to check
reserve_tokens: Tokens to reserve for output
Returns:
bool: True if items fit in context
"""
total_tokens = await self.count_tokens(items)
available_tokens = self.max_tokens - reserve_tokens
return total_tokens <= available_tokens
async def get_usage_stats(
self,
items: list[Any],
reserve_tokens: int = 512
) -> dict:
"""
Get context window usage statistics.
Args:
items: List of items to analyze
reserve_tokens: Tokens reserved for output
Returns:
dict: Usage statistics with keys:
- total_tokens: Total tokens in items
- max_tokens: Maximum context window
- reserved_tokens: Tokens reserved for output
- available_tokens: Tokens available for input
- usage_percent: Percentage of context used
- fits: Whether items fit in context
"""
total_tokens = await self.count_tokens(items)
available_tokens = self.max_tokens - reserve_tokens
usage_percent = (total_tokens / available_tokens * 100) if available_tokens > 0 else 100.0
return {
"total_tokens": total_tokens,
"max_tokens": self.max_tokens,
"reserved_tokens": reserve_tokens,
"available_tokens": available_tokens,
"usage_percent": round(usage_percent, 2),
"fits": total_tokens <= available_tokens
}
# ========================================================================
# Future: Exact Token Counting
# ========================================================================
async def count_tokens_exact(self, text: str, model: str = "gpt-3.5-turbo") -> int:
"""
Count exact tokens using tiktoken.
TODO: Integrate tiktoken library for exact token counting
TODO: Support multiple model encodings (GPT-4, Claude, etc.)
Args:
text: Text to count
model: Model name for encoding
Returns:
int: Exact token count (placeholder - returns approximate for now)
"""
# Placeholder - would use tiktoken here
return await self.count_tokens([text])