Implement LLM-WP-0007: Kimi K3 default and EUR spend reporting
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:
parent
f3121c3f1f
commit
09aa1f3604
21 changed files with 1396 additions and 103 deletions
|
|
@ -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 Mon–Sun",
|
||||
)
|
||||
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 (Mon–Sun)" if which == "last" else "current week (Mon–now)"
|
||||
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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue