"""LLM provider contract (ADR-008): the rest of the bot depends on this, never on a vendor SDK.""" from dataclasses import dataclass, field from typing import Any, Literal, Protocol Role = Literal["system", "user", "assistant"] @dataclass(frozen=True) class ChatMessage: role: Role content: str @dataclass(frozen=True) class JsonSchema: """Ask the model for a JSON object of this shape (structured output).""" name: str schema: dict[str, Any] @dataclass(frozen=True) class LLMResult: text: str # Parsed object when a JSON schema was requested and the reply was valid JSON; None otherwise. # The caller still validates it — a model can return well-formed JSON of the wrong shape. data: dict[str, Any] | None = None prompt_tokens: int = 0 completion_tokens: int = 0 raw: dict[str, Any] = field(default_factory=dict, repr=False) @property def tokens(self) -> int: return self.prompt_tokens + self.completion_tokens class LLMError(Exception): pass class LLMProvider(Protocol): async def complete(self, messages: list[ChatMessage], schema: JsonSchema | None = None) -> LLMResult: ... async def aclose(self) -> None: ...