"""LLM client contract. Anything that can turn a chat into text satisfies `LLMClient` — the app, the exercise generators and the evals only ever depend on this Protocol, never on a vendor SDK (same pattern for ASR / TTS / storage). """ from collections.abc import Sequence from typing import Literal, Protocol, runtime_checkable from pydantic import BaseModel Role = Literal["system", "user", "assistant"] class ChatMessage(BaseModel): role: Role content: str class LLMResponse(BaseModel): text: str model: str input_tokens: int | None = None output_tokens: int | None = None class LLMError(RuntimeError): """A completion failed (network, auth, rate limit, provider error...).""" @runtime_checkable class LLMClient(Protocol): async def complete( self, messages: Sequence[ChatMessage], *, temperature: float = 0.7, max_tokens: int = 1024, ) -> LLMResponse: ...