"""Abstract LLMProvider + LLMSession interfaces.""" from __future__ import annotations from abc import ABC, abstractmethod from typing import Protocol from backend.services.llm.types import ( Completion, Message, ToolChoice, ToolSchema, ) class LLMSession(ABC): """A multi-turn conversation with provider-specific cache lifecycle. Lifecycle:: session = await provider.create_session(model=..., system=...) try: messages = [Message("user", [ PdfBlock(path, cacheable=True), TextBlock(context, cacheable=True), ])] for turn in range(N): completion = await session.complete( messages=messages, tools=..., tool_choice=..., ) # process tool_calls, append to messages, repeat finally: await session.close() Caching: blocks with ``cacheable=True`` participate in provider caching. Anthropic stamps ``cache_control: ephemeral`` on each cacheable block on every call. Gemini collects all cacheable blocks (plus the system prompt) on the first ``complete()`` call into a ``CachedContent`` object and references it on subsequent calls. The system prompt is always cached. """ provider_name: str """Provider identifier ("anthropic", "gemini") — used for api_logs.""" model: str @abstractmethod async def complete( self, *, messages: list[Message], tools: list[ToolSchema] | None = None, tool_choice: ToolChoice = "auto", ) -> Completion: """Run one inference turn.""" @abstractmethod async def close(self) -> None: """Release any provider-side resources (e.g. delete a cache object). Safe to call multiple times.""" class LLMProvider(Protocol): """Top-level provider interface.""" name: str async def create_session( self, *, model: str, system: str, max_tokens: int = 4096, temperature: float | None = None, ) -> LLMSession: """Construct a session. ``system`` is always cached by the session. ``temperature`` — if not None, applied to every ``complete()`` call on this session. ``None`` means use the provider's default. Set to 0.0 for deterministic-as-possible behavior in agentic loops where the same inputs should produce the same outputs.""" ... async def run_skill( self, *, skill_name: str, model: str, system: str, user_text: str, pdf_path: str | None, output_tool: ToolSchema, ) -> tuple[dict, "Completion"]: """Execute a managed Skill and return (forced-tool input, Completion). DeepSeek and Gemini inline ``skills//SKILL.md`` and run ``validate.py`` locally. Anthropic uses Console Skills when a skill_id is configured, otherwise the same local path.""" ...