- LLMProvider: complete(messages, schema) -> LLMResult (текст, разобранный JSON, токены) - openai_compat: /chat/completions, structured output через response_format json_schema, JSON из ```-обёртки тоже разбирается; ретраи на 429/5xx и сетевые ошибки, 4xx — сразу ошибка - mock: детерминированные правила для тестов и демо без сети - LLMSettings (LLM_PROVIDER/BASE_URL/MODEL/API_KEY…), CLI python -m app.llm.cli Refs #17 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
46 lines
1.2 KiB
Python
46 lines
1.2 KiB
Python
"""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: ...
|