feat: Prompt-Cache nutzbar und messbar; Grundsatzfrage Einzelaufruf gegen Stufenkette
OpenRouter-Aufrufe eines Laufs tragen eine gemeinsame session_id (Sticky Routing, damit der Anbieter-Cache greifen kann); Cache-Treffer werden je Aufruf und Stufe erfasst und im Log als Anteil ausgewiesen. docs/Konzeptionelle_Fragen_und_Antworten.md hält Pipeline-Stufen und die Abwägung ein Aufruf vs. Kette mit Messdaten und Quellen fest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6af1d34842
commit
eadf0608ce
2 changed files with 221 additions and 6 deletions
15
core/llm.py
15
core/llm.py
|
|
@ -8,6 +8,7 @@ import os
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import instructor
|
||||
|
||||
|
|
@ -98,6 +99,10 @@ class LLMSession:
|
|||
self.client = self._clients[self.provider]
|
||||
self.usage = LLMUsage(model=model, provider=self.provider)
|
||||
self._lock = threading.Lock()
|
||||
# Eine Kennung je Pipeline-Lauf: OpenRouter leitet alle Aufrufe dann an denselben Anbieter (Sticky
|
||||
# Routing), dessen Prompt-Cache wiederholte Präfixe billiger liest. Ohne sie wechselten die Anbieter
|
||||
# je Aufruf (beobachtet: Modal, Together, Venice in einem Lauf) und kein Cache konnte greifen.
|
||||
self.session_id = uuid.uuid4().hex
|
||||
|
||||
def overrides(self) -> dict:
|
||||
"""Stufen mit abweichendem Modell – für die Fortschrittsausgabe."""
|
||||
|
|
@ -113,7 +118,7 @@ class LLMSession:
|
|||
kwargs["extra_body"] = {"provider": {
|
||||
"require_parameters": True,
|
||||
"preferred_min_throughput": {"p50": OPENROUTER_MIN_THROUGHPUT},
|
||||
}}
|
||||
}, "session_id": self.session_id}
|
||||
if REASONING_EFFORT:
|
||||
kwargs["extra_body"]["reasoning"] = {"effort": REASONING_EFFORT}
|
||||
|
||||
|
|
@ -130,7 +135,8 @@ class LLMSession:
|
|||
time.sleep(wait)
|
||||
|
||||
def _record(self, completion, step: str = "unbenannt", seconds: float = 0.0, model: str = None) -> None:
|
||||
before = (self.usage.completion_tokens, self.usage.reasoning_tokens)
|
||||
before = (self.usage.completion_tokens, self.usage.reasoning_tokens, self.usage.prompt_tokens,
|
||||
self.usage.cached_tokens)
|
||||
with self._lock:
|
||||
self.usage.calls += 1
|
||||
provider = getattr(completion, "provider", None) # OpenRouter: tatsächlich antwortender Anbieter
|
||||
|
|
@ -142,6 +148,8 @@ class LLMSession:
|
|||
self.usage.completion_tokens += usage.completion_tokens or 0
|
||||
details = getattr(usage, "completion_tokens_details", None)
|
||||
self.usage.reasoning_tokens += getattr(details, "reasoning_tokens", 0) or 0
|
||||
prompt_details = getattr(usage, "prompt_tokens_details", None)
|
||||
self.usage.cached_tokens += getattr(prompt_details, "cached_tokens", 0) or 0
|
||||
cost = getattr(usage, "cost", None)
|
||||
if cost is not None:
|
||||
self.usage.cost_usd = (self.usage.cost_usd or 0.0) + cost
|
||||
|
|
@ -152,6 +160,7 @@ class LLMSession:
|
|||
self.usage.prompt_tokens += meta.prompt_token_count or 0
|
||||
self.usage.completion_tokens += meta.candidates_token_count or 0
|
||||
self.usage.reasoning_tokens += meta.thoughts_token_count or 0
|
||||
self.usage.cached_tokens += getattr(meta, "cached_content_token_count", 0) or 0
|
||||
self._log_call(step, seconds, before, getattr(completion, "provider", None), model)
|
||||
|
||||
def _log_call(self, step, seconds, before, provider, model=None) -> None:
|
||||
|
|
@ -159,4 +168,6 @@ class LLMSession:
|
|||
step=step, seconds=round(seconds, 1), provider=provider, model=model,
|
||||
completion_tokens=self.usage.completion_tokens - before[0],
|
||||
reasoning_tokens=self.usage.reasoning_tokens - before[1],
|
||||
prompt_tokens=self.usage.prompt_tokens - before[2],
|
||||
cached_tokens=self.usage.cached_tokens - before[3],
|
||||
))
|
||||
|
|
|
|||
|
|
@ -395,6 +395,8 @@ class CallRecord(BaseModel):
|
|||
seconds: float
|
||||
completion_tokens: int = 0
|
||||
reasoning_tokens: int = 0
|
||||
prompt_tokens: int = 0
|
||||
cached_tokens: int = Field(default=0, description="Input-Tokens aus dem Anbieter-Cache (billiger)")
|
||||
provider: Optional[str] = None
|
||||
model: Optional[str] = Field(default=None, description="Tatsächlich benutztes Modell (kann je Stufe abweichen)")
|
||||
|
||||
|
|
@ -406,6 +408,7 @@ class LLMUsage(BaseModel):
|
|||
prompt_tokens: int = 0
|
||||
completion_tokens: int = Field(default=0, description="inkl. Reasoning-Tokens")
|
||||
reasoning_tokens: int = 0
|
||||
cached_tokens: int = Field(default=0, description="davon Input-Tokens aus dem Anbieter-Cache")
|
||||
cost_usd: Optional[float] = Field(default=None, description="nur OpenRouter liefert Kosten")
|
||||
providers: Dict[str, int] = Field(default_factory=dict, description="Antwortender Anbieter -> Anzahl Aufrufe (OpenRouter)")
|
||||
call_records: List[CallRecord] = Field(default_factory=list)
|
||||
|
|
@ -414,11 +417,14 @@ class LLMUsage(BaseModel):
|
|||
"""Sekunden und Tokens je Pipeline-Stufe."""
|
||||
summary: Dict[str, dict] = {}
|
||||
for record in self.call_records:
|
||||
entry = summary.setdefault(record.step, {"calls": 0, "seconds": 0.0, "completion_tokens": 0, "reasoning_tokens": 0})
|
||||
entry = summary.setdefault(record.step, {"calls": 0, "seconds": 0.0, "completion_tokens": 0,
|
||||
"reasoning_tokens": 0, "prompt_tokens": 0, "cached_tokens": 0})
|
||||
entry["calls"] += 1
|
||||
entry["seconds"] += record.seconds
|
||||
entry["completion_tokens"] += record.completion_tokens
|
||||
entry["reasoning_tokens"] += record.reasoning_tokens
|
||||
entry["prompt_tokens"] += record.prompt_tokens
|
||||
entry["cached_tokens"] += record.cached_tokens
|
||||
return summary
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue