Spaces:
Running
Running
| """Provider abstraction: one interface, many model backends. | |
| The whole point: the pipeline depends only on `LLMProvider.complete()`. Swapping | |
| mock ↔ Gemma ↔ Claude ↔ Gemini changes nothing upstream. Each response carries a | |
| full token/cost/cache usage record so every call is measurable. | |
| """ | |
| from __future__ import annotations | |
| import time | |
| from dataclasses import dataclass, field | |
| from typing import Optional | |
| from .pricing import compute_cost | |
| class CacheBlock: | |
| """A prompt segment marked cacheable (placed at the head of the payload).""" | |
| text: str | |
| cacheable: bool = True | |
| class LLMRequest: | |
| """A model-agnostic completion request. | |
| `system_blocks` are ordered; cacheable blocks form the stable, cached prefix | |
| (system prompt + tool defs + schema). `user_content` is the dynamic suffix | |
| (the document) and is never cached. Keeping these separate is what makes | |
| prompt caching actually work. | |
| """ | |
| system_blocks: list[CacheBlock] = field(default_factory=list) | |
| user_content: str = "" | |
| max_tokens: int = 1024 | |
| temperature: float = 0.0 | |
| task: str = "generic" # classify|extract|normalize|validate|agent|... | |
| json_schema: Optional[dict] = None # if set, request structured output | |
| context: dict = field(default_factory=dict) # provider hints (doc_type, etc.) | |
| def full_prompt(self) -> str: | |
| """Concatenation of all blocks + user content (for token estimation/hash).""" | |
| return "\n".join(b.text for b in self.system_blocks) + "\n" + self.user_content | |
| def cacheable_prefix(self) -> str: | |
| return "\n".join(b.text for b in self.system_blocks if b.cacheable) | |
| class Usage: | |
| input_tokens: int = 0 | |
| output_tokens: int = 0 | |
| cache_read_tokens: int = 0 | |
| cache_write_tokens: int = 0 | |
| def total_tokens(self) -> int: | |
| return self.input_tokens + self.output_tokens | |
| class LLMResponse: | |
| text: str | |
| model: str | |
| provider: str | |
| usage: Usage | |
| latency_ms: float | |
| cache_hit: bool = False | |
| cost_usd: float = 0.0 | |
| routing_reason: str = "" | |
| error: Optional[str] = None | |
| def build( | |
| cls, | |
| text: str, | |
| model: str, | |
| provider: str, | |
| usage: Usage, | |
| latency_ms: float, | |
| cache_hit: bool = False, | |
| routing_reason: str = "", | |
| error: Optional[str] = None, | |
| ) -> "LLMResponse": | |
| cost = compute_cost( | |
| model, | |
| usage.input_tokens, | |
| usage.output_tokens, | |
| usage.cache_read_tokens, | |
| usage.cache_write_tokens, | |
| ) | |
| return cls( | |
| text=text, | |
| model=model, | |
| provider=provider, | |
| usage=usage, | |
| latency_ms=round(latency_ms, 1), | |
| cache_hit=cache_hit, | |
| cost_usd=cost, | |
| routing_reason=routing_reason, | |
| error=error, | |
| ) | |
| def estimate_tokens(text: str) -> int: | |
| """Cheap heuristic: ~4 chars/token. Used by mock/local providers and as a | |
| fallback when a real API doesn't return usage. Good enough for cost demos.""" | |
| if not text: | |
| return 0 | |
| return max(1, len(text) // 4) | |
| class LLMProvider: | |
| """Base class. Subclasses implement `complete`.""" | |
| name: str = "base" | |
| tier: str = "base" # offline|local|hosted | |
| def available(self) -> bool: # pragma: no cover - trivial | |
| return True | |
| def complete(self, req: LLMRequest, model: str) -> LLMResponse: # pragma: no cover | |
| raise NotImplementedError | |
| # Helper so subclasses can time themselves consistently. | |
| def _now() -> float: | |
| return time.perf_counter() | |