"""LLM client interface and shared value objects.

The LLM layer is an interface so providers (OpenRouter, Ollama, ...) are
swappable with one config change (build plan Section 2.4). Clients return plain
chat completions; the agent loop is responsible for the structured-output
protocol on top.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol, runtime_checkable


class LLMError(Exception):
    """Raised when a provider call fails (transport, auth, or bad response)."""


@dataclass
class LLMMessage:
    role: str  # "system" | "user" | "assistant"
    content: str

    def as_dict(self) -> dict[str, str]:
        return {"role": self.role, "content": self.content}


@dataclass
class LLMResponse:
    content: str
    model: str
    prompt_tokens: int = 0
    completion_tokens: int = 0

    @property
    def total_tokens(self) -> int:
        return self.prompt_tokens + self.completion_tokens


@runtime_checkable
class LLMClient(Protocol):
    """A provider-agnostic chat completion client."""

    provider: str

    async def complete(
        self,
        messages: list[LLMMessage],
        *,
        model: str,
        temperature: float = 0.0,
        max_tokens: int | None = None,
    ) -> LLMResponse: ...
