--- id: LLM-WP-0007 type: workplan title: "Kimi K3 default via OpenRouter + EUR spend reporting" domain: agents repo: llm-connect status: finished owner: codex topic_slug: kimi-k3-default-spend-reporting planning_priority: high planning_order: 7 created: "2026-08-03" updated: "2026-08-03" depends_on_workplans: - LLM-WP-0005 related_workplans: - LLM-WP-0006 state_hub_workstream_id: "7878d7d1-b56f-5ac7-9203-e2ce44ac7c2e" --- # LLM-WP-0007 — Kimi K3 default via OpenRouter + EUR spend reporting **status:** finished **owner:** codex ## Purpose 1. Make **MoonshotAI Kimi K3** (`moonshotai/kimi-k3` on OpenRouter) a first-class basemodel option, and **promote it to the library default** once a live smoke call succeeds (rate table, adapter default, config default, docs). 2. Ensure every path that uses **llm-connect from the command line** reports **token usage and cost in euros** for the work it handled. 3. Make weekly spend easy to inspect: - **Last week** Monday 00:00 → Sunday 24:00 (local operator timezone, default Europe/Berlin) - **Current week** Monday 00:00 → now This workplan does **not** change activity-core runtime profiles or K8s deploy defaults unless those profiles currently inherit the global OpenRouter default without an explicit model — those stay explicit where they already are. ## Demand signal Operator request (2026-08-03): no open workplans; next priority is evaluating Kimi K3 as basemodel/default via OpenRouter, plus CLI cost transparency in tokens and €, plus mon–sun / mon–now weekly spend views. ## Current repo state (as of 2026-08-03) | Area | State | |---|---| | Active workplans | **None** — WP-0001…0006 + ADHOC-2026-06-02 all finished/completed | | OpenRouter default model | `anthropic/claude-sonnet-4` (`openrouter.py`, `config.LLMConfig`) | | Rate table | USD-only seed of nine models; **no** `moonshotai/kimi-k3` entry | | Cost model | `estimate_cost` → `CostEstimate` with `cost_usd` only (`costs.py`) | | CLI (`llm-connect`) | Registry tools only: `rates show`, `classes show`, `classes fit` — **no** prompt execute, **no** spend report | | Usage persistence | Server optional audit JSON under `LLM_CONNECT_AUDIT_DIR`; quality ledger is for adaptive routing, not operator spend | | BudgetTracker | Token-cap only; no EUR accounting | OpenRouter live listing (checked 2026-08-03): - Model id: `moonshotai/kimi-k3` - Context: 1_048_576 - List price (USD per token → per 1k): **prompt $0.003 / 1k**, **completion $0.015 / 1k** ## Architecture sketch ``` llm_connect/ rates.py # + moonshotai/kimi-k3 rate; currency remains list-price USD costs.py # CostEstimate gains EUR fields via FxRate fx.py # NEW: USD→EUR conversion (config/env/snapshot; never invent FX) usage.py # NEW: UsageEvent + UsageLedger (append-only JSONL) openrouter.py # default model → moonshotai/kimi-k3 after T01 smoke config.py # LLMConfig.model default aligned cli.py # run | cost report | spend week ``` ### Defaults (after smoke gate) | Surface | New default | |---|---| | `OpenRouterAdapter` `_DEFAULT_MODEL` | `moonshotai/kimi-k3` | | `LLMConfig.model` | `moonshotai/kimi-k3` | | `create_adapter("openrouter")` with no model | inherits adapter default | | Explicit profiles / activity-core triage model | **unchanged** (already pin a model) | If T01 smoke **fails** (auth, model unavailable, empty content), keep Claude Sonnet as default, ship Kimi as an **opt-in** model id in rates + docs, and mark T02 `wait` with a short note — do not force a broken default. ### Cost + EUR - Keep rate table denominated in **USD** (OpenRouter list prices). - Add `FxRate(usd_per_eur or eur_per_usd, source, captured_at)` with resolution: 1. Explicit arg / config 2. Env `LLM_CONNECT_EUR_PER_USD` (float: euros per one USD) 3. Bundled snapshot rate with `captured_at` (operator can refresh) - Extend `CostEstimate` with optional `cost_eur`, `prompt_cost_eur`, `completion_cost_eur`, `fx_source` — missing FX yields `cost_eur=None` with explicit source (same philosophy as unknown models). - Never treat missing rates or missing FX as zero cost. ### Usage ledger (spend history) Append-only JSONL, separate from `QualityLedger` (quality is adaptive-routing signal; spend is operator accounting). ```python @dataclass(frozen=True) class UsageEvent: recorded_at: datetime # timezone-aware UTC provider: str model_id: str prompt_tokens: int completion_tokens: int total_tokens: int cost_usd: float | None cost_eur: float | None cost_source: str fx_source: str | None source: str # "cli" | "server" | "library" tags: dict[str, Any] # optional request id, profile, etc. ``` Default ledger path resolution: 1. `--ledger` / explicit path 2. `LLM_CONNECT_USAGE_LEDGER` env 3. `~/.local/share/llm-connect/usage.jsonl` (XDG-style fallback) Week windows use **Monday 00:00 local** → exclusive end. Local timezone default `Europe/Berlin` (override: `--tz` / `LLM_CONNECT_TZ`). ### CLI surface ```bash # One-shot prompt (records usage when ledger available) llm-connect run "Summarise X" [--provider openrouter] [--model moonshotai/kimi-k3] # stderr or footer: tokens in/out/total · € cost (and USD if useful) # Weekly spend llm-connect spend week # current week Mon → now llm-connect spend week --last # previous Mon → Sun llm-connect spend week --json # Optional: show cost for a hypothetical token count llm-connect cost estimate --model moonshotai/kimi-k3 --prompt-tokens N --completion-tokens M ``` Per-call reporting for `run`: always print tokens; print € when FX + rate known; print clear `unknown` when not. ## Scope guardrails **In scope** - Kimi K3 rate entry + default flip after smoke - EUR conversion on top of existing USD rate table - Usage ledger + CLI run/spend/cost commands - Unit tests with fixed FX and fake clock; no live network in default CI - Contract doc updates under `contracts/functional/` **Out of scope** - Multi-currency rate tables beyond USD list + EUR display - Billing reconciliation against OpenRouter invoices (estimate-based only) - Changing activity-core K8s model pins or triage profile model ids - Full TUI / web dashboard - Automatic live FX fetch in CI (optional later; snapshot + env is enough) ## Tasks ```task id: LLM-WP-0007-T01 status: done priority: high state_hub_task_id: "ca5aabf6-6444-5710-8437-241c99177152" ``` **Validate Kimi K3 via OpenRouter.** Confirm `moonshotai/kimi-k3` is callable with a real key when available (manual/operator smoke; CI uses mock). Add rate table entry: prompt `$0.003` / completion `$0.015` per 1k USD (captured from OpenRouter 2026-08-03). Document model id and pricing source in rates contract or README providers table. Gate for T02. ```task id: LLM-WP-0007-T02 status: done priority: high state_hub_task_id: "cb0a4fb2-d008-51f8-b222-ee16dfdf16fe" ``` **Promote Kimi K3 to OpenRouter basemodel default** after T01 smoke passes. Update `openrouter._DEFAULT_MODEL`, `LLMConfig.model`, any factory/docs that advertise the default, and tests that hard-code `anthropic/claude-sonnet-4` as the implicit default. Leave explicit profile models untouched. If smoke failed, leave default unchanged and record the blocker in the task notes. ```task id: LLM-WP-0007-T03 status: done priority: high state_hub_task_id: "6fa5a4d7-e694-5a7b-b16b-4652062dd78d" ``` **USD→EUR cost conversion.** Add `fx` helper + extend `CostEstimate` with EUR fields and `fx_source`. Resolution order: explicit → env → bundled snapshot. Update `contracts/functional/costs.md`. Tests: known FX, missing FX, unknown model still yields `cost_usd=None` / `cost_eur=None`. ```task id: LLM-WP-0007-T04 status: done priority: high state_hub_task_id: "0ac06271-5dea-5f4f-8ce6-b5db549a21df" ``` **Usage ledger.** Implement `UsageEvent` + append-only JSONL `UsageLedger` (path locks patterned after `QualityLedger`). Helpers: `record(...)`, `iter_events()`, `sum_range(start, end)` aggregating tokens + cost_usd/eur. Default path via env / XDG. Tests for append, concurrent-safe append (best effort flock), and range aggregation. ```task id: LLM-WP-0007-T05 status: done priority: high state_hub_task_id: "33443864-fdce-5b24-8a7f-00c0697a69a1" ``` **CLI `run` with per-call cost footer.** Extend `llm-connect` with a `run` subcommand that creates an adapter, executes a prompt, prints content, and reports tokens + € (and USD). Record a `UsageEvent` to the ledger. Wire cost estimation from response usage + rate table + FX. Tests with mock adapter. ```task id: LLM-WP-0007-T06 status: done priority: high state_hub_task_id: "23223ec1-5575-589d-a8b0-50df8d38bd3c" ``` **CLI weekly spend views.** `llm-connect spend week` and `spend week --last` using Mon–Sun / Mon–now windows in `Europe/Berlin` (overridable). Human table + `--json`. Empty ledger → zero totals, exit 0. Tests with synthetic events and fixed timezone. ```task id: LLM-WP-0007-T07 status: done priority: medium state_hub_task_id: "79dd15d9-b2ad-547b-a4e8-d80b26d342e4" ``` **Optional library/server recording.** When `LLM_CONNECT_USAGE_LEDGER` is set, record usage from adapter post-call path and/or server `/execute` (source `library` / `server`) so non-CLI traffic contributes to weekly spend. Keep opt-in so library consumers are not surprised by home-dir writes. ```task id: LLM-WP-0007-T08 status: done priority: medium state_hub_task_id: "4b835a23-048b-51ee-837d-48abd108f182" ``` **Docs + acceptance polish.** README: new default model, CLI examples for `run` / `spend week`, env vars (`LLM_CONNECT_USAGE_LEDGER`, `LLM_CONNECT_EUR_PER_USD`, `LLM_CONNECT_TZ`). Update `contracts/functional/` as needed. Smoke script or Makefile target for optional live Kimi check. ## Acceptance - [x] `moonshotai/kimi-k3` is in the default rate registry with documented USD rates - [x] After successful smoke, OpenRouter/library default model is `moonshotai/kimi-k3` - [x] `llm-connect run "..."` prints token counts and € cost (or explicit unknown) - [x] Successful CLI runs append to the usage ledger - [x] `llm-connect spend week` and `spend week --last` show tokens + € for the correct Mon-based windows - [x] Default CI tests pass offline (mock adapter, fixed FX, no live OpenRouter) - [x] Missing rate or FX never reported as €0.00 without an `unknown` marker ## Risks / notes - **Model id spelling:** operator said "kimi-k3"; OpenRouter id is `moonshotai/kimi-k3` (not K2 variants). Stick to that id unless operator renames the requirement. - **FX staleness:** bundled EUR rate will drift; env override is the operator escape hatch. Do not scrape FX APIs in the hot path for v1. - **Default flip blast radius:** any consumer relying on implicit Sonnet will change behaviour — document in README and release notes; profiles that pin models are safe. - **Estimate vs invoice:** ledger costs are list-price estimates, not OpenRouter billed totals (discounts, cache reads, provider routing may differ). ## Implementation order T01 → T02 (gated) in parallel with T03 → T04 → T05 → T06 → T07 → T08. T03/T04 can start before T01; T02 must wait for T01 smoke outcome. ## Implementation notes (2026-08-03) - Live OpenRouter smoke: `moonshotai/kimi-k3` returned content "ok" with usage tokens (Together provider). Note: short `max_tokens` can yield empty `content` while spending tokens on reasoning. - Default OpenRouter / LLMConfig model flipped to `moonshotai/kimi-k3`. - Added `fx.py`, EUR fields on `CostEstimate`, `usage.py` ledger, CLI `run` / `cost estimate` / `spend week`. - Opt-in library/server recording via `LLM_CONNECT_USAGE_LEDGER`; CLI suppresses double-record. - Full unit suite: 224 passed offline.