feat: Anthropic web server tools, provider metadata, messaging hardening

- Add local web_search/web_fetch SSE handling and optional tool schemas
- Extend HeuristicToolParser for JSON-style WebFetch/WebSearch text
- Consolidate provider defaults, ids, and exception typing; stream contracts
- Messaging: typed options, voice config injection, platform contract cleanup
- Tests for web server tools, converters, parsers, contracts; ignore debug-*.log
This commit is contained in:
Alishahryar1
2026-04-24 23:01:14 -07:00
parent 4b89183ba0
commit b926f60f64
50 changed files with 1658 additions and 439 deletions
+58 -4
View File
@@ -2,8 +2,11 @@
import os
from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI, Request
from fastapi.exception_handlers import request_validation_exception_handler
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from loguru import logger
@@ -11,7 +14,6 @@ from config.logging_config import configure_logging
from config.settings import get_settings
from providers.exceptions import ProviderError
from .dependencies import cleanup_provider
from .routes import router
from .runtime import AppRuntime
@@ -26,9 +28,7 @@ configure_logging(_settings.log_file)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager."""
runtime = AppRuntime.for_app(
app, settings=get_settings(), provider_cleanup=cleanup_provider
)
runtime = AppRuntime.for_app(app, settings=get_settings())
await runtime.startup()
yield
@@ -48,6 +48,60 @@ def create_app() -> FastAPI:
app.include_router(router)
# Exception handlers
@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError):
"""Log request shape for 422 debugging without content values."""
body: Any
try:
body = await request.json()
except Exception as e:
body = {"_json_error": type(e).__name__}
messages = body.get("messages") if isinstance(body, dict) else None
message_summary: list[dict[str, Any]] = []
if isinstance(messages, list):
for msg in messages:
if not isinstance(msg, dict):
message_summary.append({"message_kind": type(msg).__name__})
continue
content = msg.get("content")
item: dict[str, Any] = {
"role": msg.get("role"),
"content_kind": type(content).__name__,
}
if isinstance(content, list):
item["block_types"] = [
block.get("type", "dict")
if isinstance(block, dict)
else type(block).__name__
for block in content[:12]
]
item["block_keys"] = [
sorted(str(key) for key in block)[:12]
for block in content[:5]
if isinstance(block, dict)
]
elif isinstance(content, str):
item["content_length"] = len(content)
message_summary.append(item)
logger.debug(
"Request validation failed: path={} query={} error_locs={} error_types={} message_summary={} tool_names={}",
request.url.path,
str(request.url.query),
[list(error.get("loc", ())) for error in exc.errors()],
[str(error.get("type", "")) for error in exc.errors()],
message_summary,
[
str(tool.get("name", ""))
for tool in body.get("tools", [])
if isinstance(body, dict)
and isinstance(body.get("tools"), list)
and isinstance(tool, dict)
],
)
return await request_validation_exception_handler(request, exc)
@app.exception_handler(ProviderError)
async def provider_error_handler(request: Request, exc: ProviderError):
"""Handle provider-specific errors and return Anthropic format."""