fabro/lib/crates
Scott Werner 23d3644787
feat(llm): catalog-estimated completion cost on Response (#494)
Standalone pre-OpenRouter step, pulled forward from the #438 triage (the
gateway-refactor plan's "additive feature PR alongside the redo"):
completion responses carry a USD cost with provenance.

## What's here

**`Response.cost_usd` + `Response.cost_source`** — new optional fields
(`skip_serializing_if` keeps the wire shape byte-identical when unset).
`CostSource` (`authoritative` | `estimated`) lives in fabro-model's
billing vocabulary next to `UsdMicros`/`TokenCounts`, since the API
layer reuses it.

**`fabro-llm/src/cost.rs`** — `estimate_cost_usd`, a thin wrapper over
the existing `Catalog::price_tokens` billing machinery (billing-policy-
and speed-aware), ported from #438's prototype with attribution. One fix
over the prototype: model aliases and provider names are canonicalized
before building the `ModelRef` — `ModelPricing::bill` rejects
non-canonical refs, so the original would silently skip cost on alias
requests (caught by a new test).

**Client-level stamping** — one generic post-decode site instead of
#438's ~8 per-adapter sites (which predate the codec refactor):
`Client::complete` stamps blocking responses and `Client::stream` stamps
`Finish` events, beneath the middleware chain so middleware observes
final responses. Codecs stay wire-translation-only — zero wire-snapshot
churn — and every registered adapter (including custom
`register_provider` ones) gets the same treatment. Stamping never
overwrites an existing cost, so future authoritative in-band costs
(OpenRouter) take precedence by construction.

**API surface** — `cost_usd`/`cost_source` on `CompletionResponse`
(OpenAPI spec + handler + regenerated TS client). The streaming endpoint
already carries cost implicitly since `Finish` events serialize the
`Response` verbatim; this makes the blocking surface match. `CostSource`
reuses the canonical fabro-model type via `with_replacement`, with the
standard round-trip test pinning type identity and JSON parity.

## Deliberately not here (stays with the OpenRouter redo per the plan's
hard rule)

- Authoritative `usage.cost` parsing in the `openai_compatible` codec
wire structs
- Cached-token usage parsing (changes observable usage values)
- Per-model `billing_policy` schema field

## Verification

- `cargo nextest run --workspace --no-fail-fast`: 6701 passed; only the
known 5 pre-existing environment-dependent fabro-workflow failures
(identical on main)
- All fabro-llm wire snapshots unmodified; new pins: cost estimation
unit tests (incl. alias canonicalization), Client stamping tests
(blocking, streaming, beneath middleware, no-catalog), fabro-api
`CostSource` round-trip
- clippy `-D warnings` + pinned-nightly fmt clean; `bun run typecheck`
clean in fabro-web

Independent of the route-vocabulary work in #493 — branches directly off
main. After both land, the OpenRouter redo shrinks to config + typed
codec params + authoritative-cost decode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:55:47 -04:00
..
build-support fix(build): refresh embedded git sha on branch commits 2026-05-09 14:10:04 -04:00
fabro-acp fix(workflow): capture configured artifacts once (#337) 2026-05-21 10:52:22 -04:00
fabro-agent feat(llm): catalog-estimated completion cost on Response (#494) 2026-06-11 16:55:47 -04:00
fabro-api feat(llm): catalog-estimated completion cost on Response (#494) 2026-06-11 16:55:47 -04:00
fabro-auth Replace vague expect/panic messages with invariant-explaining messages (#422) 2026-05-27 10:38:20 -04:00
fabro-automation fix(automation): honor schedule trigger enabled state 2026-05-30 02:19:52 -04:00
fabro-checkpoint Interpolation foundation (InterpString v2) (#472) 2026-06-10 12:51:08 -04:00
fabro-cli feat(llm): catalog-estimated completion cost on Response (#494) 2026-06-11 16:55:47 -04:00
fabro-client Add fabro variable CLI namespace for server-managed variables (#434) 2026-05-27 13:57:25 -04:00
fabro-config refactor(model): put the codec on the route (#493) 2026-06-11 16:11:05 -04:00
fabro-core Replace bare unwrap() with documented expect() across production runtim… (#415) 2026-05-26 17:46:39 -04:00
fabro-dev feat(llm): add Claude Fable 5 support (#482) 2026-06-10 14:01:56 -04:00
fabro-dump Model run sandbox lifecycle explicitly (#431) 2026-05-27 12:48:56 -04:00
fabro-environment Interpolation foundation (InterpString v2) (#472) 2026-06-10 12:51:08 -04:00
fabro-github fix(github): refresh installation tokens during workflows 2026-05-06 07:15:18 -04:00
fabro-graphviz Replace vague expect/panic messages with invariant-explaining messages (#422) 2026-05-27 10:38:20 -04:00
fabro-hooks Add event-sourced todo tools for OpenAI and Anthropic profiles (#353) 2026-05-22 13:44:42 -04:00
fabro-http refactor(static): centralize env var names 2026-04-24 12:29:51 -04:00
fabro-install feat(install): enable only allowed sandbox providers 2026-06-02 18:39:31 -04:00
fabro-interview Replace stdin JSONL control pipe with WebSocket worker control bus (#440) 2026-05-27 20:24:25 -04:00
fabro-llm feat(llm): catalog-estimated completion cost on Response (#494) 2026-06-11 16:55:47 -04:00
fabro-macros refactor(dev): simplify generated docs tooling 2026-04-24 18:41:00 -04:00
fabro-manifest Interpolation foundation (InterpString v2) (#472) 2026-06-10 12:51:08 -04:00
fabro-mcp Add legacy SSE MCP transport support (#386) 2026-05-24 15:29:01 -04:00
fabro-mcp-server feat: Add approve/deny run controls to MCP and CLI (#400) 2026-05-25 15:49:57 -04:00
fabro-model feat(llm): catalog-estimated completion cost on Response (#494) 2026-06-11 16:55:47 -04:00
fabro-oauth Replace bare unwrap() with documented expect() across production runtim… (#415) 2026-05-26 17:46:39 -04:00
fabro-options-metadata refactor(dev): simplify generated docs tooling 2026-04-24 18:41:00 -04:00
fabro-proc refactor(static): centralize env var names 2026-04-24 12:29:51 -04:00
fabro-redact refactor(integrations): make chat integrations Slack-only 2026-05-09 11:43:16 -04:00
fabro-sandbox Model run sandbox lifecycle explicitly (#431) 2026-05-27 12:48:56 -04:00
fabro-server feat(llm): catalog-estimated completion cost on Response (#494) 2026-06-11 16:55:47 -04:00
fabro-slack Replace vague expect/panic messages with invariant-explaining messages (#422) 2026-05-27 10:38:20 -04:00
fabro-spa feat(dev): gitignore embedded spa assets 2026-04-26 21:31:11 -04:00
fabro-static refactor: rationalize server secret scopes (vault-only for optional int… (#401) 2026-05-25 17:26:01 -04:00
fabro-store perf(server): cache bare GitHub clones for automation materialization 2026-05-29 08:29:58 -04:00
fabro-telemetry Replace vague expect/panic messages with invariant-explaining messages (#422) 2026-05-27 10:38:20 -04:00
fabro-template feat(template): resolve template error locations (#333) 2026-05-20 20:15:04 -04:00
fabro-test test(llm): pin provider wire behavior with per-dialect snapshot tests (#471) 2026-06-08 16:11:27 -04:00
fabro-tool feat: Add approve/deny run controls to MCP and CLI (#400) 2026-05-25 15:49:57 -04:00
fabro-tracker fix(github): refresh installation tokens during workflows 2026-05-06 07:15:18 -04:00
fabro-types Interpolation foundation (InterpString v2) (#472) 2026-06-10 12:51:08 -04:00
fabro-util Replace vague expect/panic messages with invariant-explaining messages (#422) 2026-05-27 10:38:20 -04:00
fabro-validate fix(graph): support dotted Fabro graph attributes (#324) 2026-05-20 09:31:08 -04:00
fabro-variable feat(server): add variables API (#430) 2026-05-27 11:46:36 -04:00
fabro-vault feat(server): add variables API (#430) 2026-05-27 11:46:36 -04:00
fabro-workflow feat(llm): catalog-estimated completion cost on Response (#494) 2026-06-11 16:55:47 -04:00