fix: antwortendes Modell erfassen; 0.040, LaTeX-Dezimalkomma und Teilfragen-Überschriften

- CallRecord.responded_model / LLMUsage.responded_models: tatsächlich antwortendes
  Modell (beim Alias ~deepseek/deepseek-flash-latest) in Log, Report und Benchmark
- normalize_number: führende Null ist nie Tausender (0.040 wurde als 40 gelesen)
- numbers_in: LaTeX-Dezimalkomma 0{,}08 als eine Zahl
- question_ids_in: '### Teil N' als Teilfrage; ausdrückliche Marker haben Vorrang
  vor nummerierten Listen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-17 19:30:19 +02:00
commit 8bd3c2e7e1
2 changed files with 88 additions and 10 deletions

View file

@ -146,6 +146,13 @@ class LLMSession:
provider = getattr(completion, "provider", None) # OpenRouter: tatsächlich antwortender Anbieter
if provider:
self.usage.providers[provider] = self.usage.providers.get(provider, 0) + 1
# Beim Alias (~deepseek/deepseek-flash-latest) steht erst in der Antwort, welches Modell gerechnet hat;
# ohne diese Angabe wären Läufe vor und nach einem Modellwechsel des Alias nicht zu unterscheiden.
antwortend = getattr(completion, "model", None) or getattr(completion, "model_version", None)
if not isinstance(antwortend, str):
antwortend = None
if antwortend:
self.usage.responded_models[antwortend] = self.usage.responded_models.get(antwortend, 0) + 1
usage = getattr(completion, "usage", None)
if usage is not None: # OpenAI-Format (OpenRouter)
self.usage.prompt_tokens += usage.prompt_tokens or 0
@ -157,7 +164,7 @@ class LLMSession:
cost = getattr(usage, "cost", None)
if cost is not None:
self.usage.cost_usd = (self.usage.cost_usd or 0.0) + cost
self._log_call(step, seconds, before, getattr(completion, "provider", None), model)
self._log_call(step, seconds, before, getattr(completion, "provider", None), model, antwortend)
return
meta = getattr(completion, "usage_metadata", None)
if meta is not None: # Gemini-Format, keine Kostenangabe
@ -165,11 +172,11 @@ class LLMSession:
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)
self._log_call(step, seconds, before, getattr(completion, "provider", None), model, antwortend)
def _log_call(self, step, seconds, before, provider, model=None) -> None:
def _log_call(self, step, seconds, before, provider, model=None, responded_model=None) -> None:
self.usage.call_records.append(CallRecord(
step=step, seconds=round(seconds, 1), provider=provider, model=model,
step=step, seconds=round(seconds, 1), provider=provider, model=model, responded_model=responded_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],

View file

@ -427,7 +427,9 @@ class CallRecord(BaseModel):
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)")
model: Optional[str] = Field(default=None, description="Angefragtes Modell (kann je Stufe abweichen)")
responded_model: Optional[str] = Field(
default=None, description="Modell laut Antwort – bei Aliasen wie ~deepseek/deepseek-flash-latest das tatsächliche")
class LLMUsage(BaseModel):
@ -440,6 +442,8 @@ class LLMUsage(BaseModel):
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)")
responded_models: Dict[str, int] = Field(
default_factory=dict, description="Tatsächlich antwortendes Modell -> Anzahl Aufrufe (bei Aliasen vergleichsentscheidend)")
call_records: List[CallRecord] = Field(default_factory=list)
def by_step(self) -> Dict[str, dict]: