Implement LLM-WP-0007: Kimi K3 default and EUR spend reporting
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled

Add moonshotai/kimi-k3 as OpenRouter basemodel default after live smoke,
USD→EUR cost conversion, append-only usage ledger, and CLI run/cost/spend
week commands with token and euro reporting.
This commit is contained in:
tegwick 2026-08-03 22:00:34 +02:00
parent f3121c3f1f
commit 09aa1f3604
21 changed files with 1396 additions and 103 deletions

View file

@ -1,16 +1,30 @@
"""Command-line helpers for llm-connect registries."""
"""Command-line helpers for llm-connect registries, run, and spend."""
from __future__ import annotations
import argparse
import json
import sys
from collections.abc import Iterable, Mapping
from datetime import datetime
from pathlib import Path
from typing import Any
from llm_connect.costs import estimate_cost
from llm_connect.factory import create_adapter
from llm_connect.fx import resolve_fx_rate
from llm_connect.models import RunConfig
from llm_connect.problem_classes import ProblemClass, ProblemClassRegistry
from llm_connect.quality import QualityLedger
from llm_connect.rates import ModelRateRegistry
from llm_connect.usage import (
UsageLedger,
default_usage_ledger_path,
event_from_response,
format_cost_footer,
suppress_auto_usage_record,
week_window,
)
def main(argv: list[str] | None = None) -> int:
@ -43,6 +57,55 @@ def _build_parser() -> argparse.ArgumentParser:
class_fit.add_argument("--min-observations", type=int, default=3)
class_fit.add_argument("--json", action="store_true", help="Emit JSON")
class_fit.set_defaults(func=_classes_fit)
run = commands.add_parser("run", help="Execute a prompt and report tokens + cost")
run.add_argument("prompt", help="Prompt text to send")
run.add_argument("--provider", default="openrouter", help="Provider key (default openrouter)")
run.add_argument("--model", default=None, help="Model id (provider default if omitted)")
run.add_argument("--temperature", type=float, default=0.7)
run.add_argument("--max-tokens", type=int, default=2000)
run.add_argument("--timeout", type=int, default=300, help="Timeout seconds")
run.add_argument("--ledger", type=Path, default=None, help="Usage ledger JSONL path")
run.add_argument("--no-ledger", action="store_true", help="Do not append to the usage ledger")
run.add_argument("--json", action="store_true", help="Emit JSON response + cost")
run.add_argument(
"--eur-per-usd",
type=float,
default=None,
help="Override FX rate (euros per one USD)",
)
run.set_defaults(func=_run_prompt)
cost = commands.add_parser("cost", help="Cost estimation helpers")
cost_commands = cost.add_subparsers(dest="cost_command", required=True)
cost_estimate = cost_commands.add_parser(
"estimate",
help="Estimate cost for token counts without calling a provider",
)
cost_estimate.add_argument("--model", required=True, help="Model id")
cost_estimate.add_argument("--prompt-tokens", type=int, required=True)
cost_estimate.add_argument("--completion-tokens", type=int, default=0)
cost_estimate.add_argument("--rates", type=Path, help="YAML registry overlay")
cost_estimate.add_argument("--eur-per-usd", type=float, default=None)
cost_estimate.add_argument("--json", action="store_true", help="Emit JSON")
cost_estimate.set_defaults(func=_cost_estimate)
spend = commands.add_parser("spend", help="Inspect recorded spend")
spend_commands = spend.add_subparsers(dest="spend_command", required=True)
spend_week = spend_commands.add_parser(
"week",
help="Weekly spend: current Mon→now, or --last for previous MonSun",
)
spend_week.add_argument(
"--last",
action="store_true",
help="Previous calendar week (Mon 00:00 → Sun end / this Mon 00:00)",
)
spend_week.add_argument("--ledger", type=Path, default=None, help="Usage ledger JSONL path")
spend_week.add_argument("--tz", default=None, help="Timezone (default Europe/Berlin)")
spend_week.add_argument("--json", action="store_true", help="Emit JSON")
spend_week.set_defaults(func=_spend_week)
return parser
@ -123,6 +186,160 @@ def _classes_fit(args: argparse.Namespace) -> int:
return 0
def _run_prompt(args: argparse.Namespace) -> int:
fx = resolve_fx_rate(args.eur_per_usd)
adapter_kwargs: dict[str, Any] = {}
if args.model is not None:
adapter_kwargs["model"] = args.model
adapter = create_adapter(args.provider, **adapter_kwargs)
config = RunConfig(
model_name=args.model or "gpt-4",
temperature=args.temperature,
max_tokens=args.max_tokens,
timeout_seconds=args.timeout,
)
with suppress_auto_usage_record():
response = adapter.execute_prompt(args.prompt, config)
usage = response.usage or {}
prompt_tokens = int(usage.get("prompt_tokens") or 0)
completion_tokens = int(usage.get("completion_tokens") or 0)
total_tokens = int(usage.get("total_tokens") or (prompt_tokens + completion_tokens))
model_id = response.model or args.model or "unknown"
estimate = estimate_cost(
model_id,
prompt_tokens,
completion_tokens,
fx=fx,
)
if not args.no_ledger:
ledger_path = args.ledger or default_usage_ledger_path()
event = event_from_response(
response,
provider=args.provider,
source="cli",
fx=fx,
)
UsageLedger(ledger_path).append(event)
footer = format_cost_footer(
estimate,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=total_tokens,
model_id=model_id,
)
if args.json:
print(
json.dumps(
{
"content": response.content,
"model": model_id,
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens,
},
"cost_usd": estimate.cost_usd,
"cost_eur": estimate.cost_eur,
"cost_source": estimate.cost_source,
"fx_source": estimate.fx_source,
"finish_reason": response.finish_reason,
},
indent=2,
sort_keys=True,
)
)
return 0
print(response.content)
print(footer, file=sys.stderr)
return 0
def _cost_estimate(args: argparse.Namespace) -> int:
if args.prompt_tokens < 0 or args.completion_tokens < 0:
raise SystemExit("token counts must be non-negative")
registry = ModelRateRegistry.default()
if args.rates:
registry = registry.merged_with(ModelRateRegistry.from_yaml(args.rates))
estimate = estimate_cost(
args.model,
args.prompt_tokens,
args.completion_tokens,
registry=registry,
fx=args.eur_per_usd,
)
total = args.prompt_tokens + args.completion_tokens
if args.json:
print(
json.dumps(
{
"model": args.model,
"prompt_tokens": args.prompt_tokens,
"completion_tokens": args.completion_tokens,
"total_tokens": total,
"cost_usd": estimate.cost_usd,
"cost_eur": estimate.cost_eur,
"cost_source": estimate.cost_source,
"fx_source": estimate.fx_source,
},
indent=2,
sort_keys=True,
)
)
return 0
print(
format_cost_footer(
estimate,
prompt_tokens=args.prompt_tokens,
completion_tokens=args.completion_tokens,
total_tokens=total,
model_id=args.model,
)
)
return 0
def _spend_week(args: argparse.Namespace) -> int:
which = "last" if args.last else "current"
start, end = week_window(which, tz=args.tz)
ledger_path = args.ledger or default_usage_ledger_path()
summary = UsageLedger(ledger_path).sum_range(start, end)
if args.json:
payload = summary.to_dict()
payload["which"] = which
payload["tz"] = args.tz or "Europe/Berlin"
payload["ledger"] = str(ledger_path)
print(json.dumps(payload, indent=2, sort_keys=True))
return 0
label = "last week (MonSun)" if which == "last" else "current week (Monnow)"
print(f"spend {label}")
print(f"window: {_fmt_local(start)}{_fmt_local(end)}")
print(f"ledger: {ledger_path}")
print(f"events: {summary.event_count}")
print(
f"tokens: in={summary.prompt_tokens} out={summary.completion_tokens} "
f"total={summary.total_tokens}"
)
usd = "unknown" if summary.cost_usd is None else f"{summary.cost_usd:.6f}"
eur = "unknown" if summary.cost_eur is None else f"{summary.cost_eur:.6f}"
print(f"cost: eur={eur} usd={usd}")
if summary.unknown_cost_events:
print(f"unknown_cost_events: {summary.unknown_cost_events}")
return 0
def _fmt_local(dt: datetime) -> str:
return dt.isoformat()
def _classes_payload(classes: Iterable[ProblemClass]) -> dict[str, dict[str, Any]]:
return {
problem_class.name: {