"""Pydantic models for API requests and responses.""" import logging from typing import List, Dict, Any, Optional, Union, Literal from pydantic import BaseModel, field_validator, model_validator from config.settings import get_settings logger = logging.getLogger(__name__) # ============================================================================= # Content Block Types # ============================================================================= class ContentBlockText(BaseModel): type: Literal["text"] text: str class ContentBlockImage(BaseModel): type: Literal["image"] source: Dict[str, Any] class ContentBlockToolUse(BaseModel): type: Literal["tool_use"] id: str name: str input: Dict[str, Any] class ContentBlockToolResult(BaseModel): type: Literal["tool_result"] tool_use_id: str content: Union[str, List[Dict[str, Any]], Dict[str, Any], List[Any], Any] class ContentBlockThinking(BaseModel): type: Literal["thinking"] thinking: str class SystemContent(BaseModel): type: Literal["text"] text: str # ============================================================================= # Message Types # ============================================================================= class Message(BaseModel): role: Literal["user", "assistant"] content: Union[ str, List[ Union[ ContentBlockText, ContentBlockImage, ContentBlockToolUse, ContentBlockToolResult, ContentBlockThinking, ] ], ] reasoning_content: Optional[str] = None class Tool(BaseModel): name: str description: Optional[str] = None input_schema: Dict[str, Any] class ThinkingConfig(BaseModel): enabled: bool = True # ============================================================================= # Request/Response Models # ============================================================================= class MessagesRequest(BaseModel): model: str max_tokens: int messages: List[Message] system: Optional[Union[str, List[SystemContent]]] = None stop_sequences: Optional[List[str]] = None stream: Optional[bool] = False temperature: Optional[float] = 1.0 top_p: Optional[float] = None top_k: Optional[int] = None metadata: Optional[Dict[str, Any]] = None tools: Optional[List[Tool]] = None tool_choice: Optional[Dict[str, Any]] = None thinking: Optional[ThinkingConfig] = None extra_body: Optional[Dict[str, Any]] = None original_model: Optional[str] = None @model_validator(mode="after") def map_model(self) -> "MessagesRequest": """Map any Claude model name to the configured model.""" settings = get_settings() if self.original_model is None: self.original_model = self.model # Strip provider prefixes clean_v = self.model for prefix in ["anthropic/", "openai/", "gemini/"]: if clean_v.startswith(prefix): clean_v = clean_v[len(prefix) :] break # Map all Claude models to the single configured model if any( name in clean_v.lower() for name in ["haiku", "sonnet", "opus", "claude"] ): self.model = settings.model if self.model != self.original_model: logger.debug(f"MODEL MAPPING: '{self.original_model}' -> '{self.model}'") return self class TokenCountRequest(BaseModel): model: str messages: List[Message] system: Optional[Union[str, List[SystemContent]]] = None tools: Optional[List[Tool]] = None thinking: Optional[ThinkingConfig] = None tool_choice: Optional[Dict[str, Any]] = None @field_validator("model") @classmethod def validate_model_field(cls, v, info): """Map any Claude model name to the configured model.""" settings = get_settings() clean_v = v for prefix in ["anthropic/", "openai/", "gemini/"]: if clean_v.startswith(prefix): clean_v = clean_v[len(prefix) :] break if any( name in clean_v.lower() for name in ["haiku", "sonnet", "opus", "claude"] ): return settings.model return v class TokenCountResponse(BaseModel): input_tokens: int class Usage(BaseModel): input_tokens: int output_tokens: int cache_creation_input_tokens: int = 0 cache_read_input_tokens: int = 0 class MessagesResponse(BaseModel): id: str model: str role: Literal["assistant"] = "assistant" content: List[ Union[ ContentBlockText, ContentBlockToolUse, ContentBlockThinking, Dict[str, Any] ] ] type: Literal["message"] = "message" stop_reason: Optional[ Literal["end_turn", "max_tokens", "stop_sequence", "tool_use"] ] = None stop_sequence: Optional[str] = None usage: Usage