llm_cost_tracker 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +11 -5
- data/app/assets/llm_cost_tracker/application.css +784 -802
- data/app/controllers/llm_cost_tracker/application_controller.rb +14 -2
- data/app/controllers/llm_cost_tracker/calls_controller.rb +28 -21
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -4
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
- data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +19 -16
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
- data/app/models/llm_cost_tracker/call.rb +28 -63
- data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
- data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
- data/app/models/llm_cost_tracker/call_tag.rb +0 -2
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +2 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +64 -43
- data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
- data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +81 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +6 -68
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +20 -12
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +74 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +132 -125
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +120 -159
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +140 -194
- data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
- data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
- data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
- data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
- data/app/views/llm_cost_tracker/tags/show.html.erb +85 -104
- data/config/routes.rb +3 -3
- data/lib/llm_cost_tracker/budget.rb +25 -28
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +2 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +30 -52
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +18 -33
- data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
- data/lib/llm_cost_tracker/charges/cost.rb +27 -0
- data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
- data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +40 -44
- data/lib/llm_cost_tracker/check.rb +5 -0
- data/lib/llm_cost_tracker/configuration.rb +13 -61
- data/lib/llm_cost_tracker/currency.rb +5 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
- data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
- data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
- data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
- data/lib/llm_cost_tracker/doctor.rb +66 -64
- data/lib/llm_cost_tracker/engine.rb +4 -4
- data/lib/llm_cost_tracker/event.rb +12 -20
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +4 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +8 -9
- data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
- data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
- data/lib/llm_cost_tracker/ingestion.rb +24 -36
- data/lib/llm_cost_tracker/integrations/anthropic.rb +94 -116
- data/lib/llm_cost_tracker/integrations/base.rb +39 -57
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +72 -332
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +89 -145
- data/lib/llm_cost_tracker/integrations.rb +32 -25
- data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
- data/lib/llm_cost_tracker/ledger/period.rb +5 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
- data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
- data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
- data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
- data/lib/llm_cost_tracker/ledger/store.rb +18 -42
- data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
- data/lib/llm_cost_tracker/ledger.rb +14 -11
- data/lib/llm_cost_tracker/logging.rb +4 -21
- data/lib/llm_cost_tracker/middleware/faraday.rb +63 -51
- data/lib/llm_cost_tracker/parsers.rb +140 -29
- data/lib/llm_cost_tracker/prices.json +1707 -1
- data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
- data/lib/llm_cost_tracker/pricing/calculation.rb +260 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
- data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +84 -0
- data/lib/llm_cost_tracker/pricing/mode.rb +53 -35
- data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
- data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
- data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
- data/lib/llm_cost_tracker/pricing/source.rb +7 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
- data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
- data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
- data/lib/llm_cost_tracker/pricing.rb +10 -295
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
- data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
- data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +181 -0
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
- data/lib/llm_cost_tracker/providers.rb +35 -0
- data/lib/llm_cost_tracker/railtie.rb +0 -7
- data/lib/llm_cost_tracker/report/data.rb +3 -4
- data/lib/llm_cost_tracker/report/formatter.rb +33 -20
- data/lib/llm_cost_tracker/report.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +6 -19
- data/lib/llm_cost_tracker/tags/context.rb +9 -6
- data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
- data/lib/llm_cost_tracker/timing.rb +2 -4
- data/lib/llm_cost_tracker/tracker.rb +24 -36
- data/lib/llm_cost_tracker/usage/catalog.rb +58 -0
- data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
- data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +24 -46
- data/lib/llm_cost_tracker/usage/source.rb +14 -0
- data/lib/llm_cost_tracker/usage/token_usage.rb +100 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +43 -52
- data/lib/tasks/llm_cost_tracker.rake +14 -73
- metadata +92 -58
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -106
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -183
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
- data/lib/llm_cost_tracker/billing/components.rb +0 -95
- data/lib/llm_cost_tracker/capture/stream.rb +0 -9
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
- data/lib/llm_cost_tracker/doctor/check.rb +0 -7
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -32
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -25
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
- data/lib/llm_cost_tracker/masking.rb +0 -39
- data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -193
- data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
- data/lib/llm_cost_tracker/parsers/base.rb +0 -131
- data/lib/llm_cost_tracker/parsers/gemini.rb +0 -232
- data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -51
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -155
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
- data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
- data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
- data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
- data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -134
- data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
- data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -254
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -172
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -142
- data/lib/llm_cost_tracker/reconciliation.rb +0 -118
- data/lib/llm_cost_tracker/token_usage.rb +0 -93
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3bb624cf9437e2ab972021128ab552b48b16c9b8d209429fb264062837e8547
|
|
4
|
+
data.tar.gz: 8785221213ed888a592b312e5a734193637653930ef9652ece73f650cb920eb5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c223c14dbfe3e2ebf61930175ae7607c2a4a05f502962963312c4ec929965242fccab115eda9d1426d6e331d7fb23ad811f73c9ba8a795cb3262c3d49a60eb45
|
|
7
|
+
data.tar.gz: 6b8e3ef019f41907909bb9f07eb58085dd6355a442699d52440de564c97fb6fb65979ee01271e4741bd9411ce7ef6a3b69102accd764fdf419d42db1bdb2f6e8
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.12.0] - 2026-06-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `bin/rails llm_cost_tracker:rebuild_rollups` rebuilds the `llm_cost_tracker_call_rollups` cache from the calls ledger — populate it after turning on `config.cache_rollups` for an app with existing calls, or resync it if rollup totals ever drift from the calls.
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
|
|
15
|
+
- BREAKING: the experimental `Reconciliation` subsystem (provider invoice import + diff, the `/reconciliation` dashboard page, `bin/rails llm_cost_tracker:reconcile:*` rake tasks, `config.reconciliation_enabled`, `config.reconciliation_importers`, the `llm_cost_tracker:reconciliation` generator, and the `llm_cost_tracker_provider_invoices` / `_provider_invoice_imports` tables) is gone. It was never finished and never billing-accurate. `calls.provider_response_id` (captured on every call) already covers invoice cross-reference; if invoice-vs-ledger reconciliation ships again it lives in a separate gem. Existing installs can drop the two tables — see [docs/upgrading.md](docs/upgrading.md#v011--v012-unreleased).
|
|
16
|
+
- `config.instrument :gemnii` (or any other typo / unknown integration name) no longer raises at config time — it now logs `Logging.warn("Unknown integration: :gemnii. Known: ...")` once when integrations install, and `bin/rails llm_cost_tracker:doctor` shows the unknown name as a `:warn` row so the typo is visible without crashing boot.
|
|
17
|
+
- Pre-call budget enforcement for Azure-hosted OpenAI calls now keys on `"azure_openai"` (matching the recorded `Call.provider`), so `pricing_overrides` for Azure rates actually gate the call. Previously it always keyed on `"openai"` regardless of the SDK client's `base_url`.
|
|
18
|
+
- BREAKING: removed the `batch:` keyword argument from `LlmCostTracker.track`, `LlmCostTracker.track_stream`, and `stream.usage` (inside `track_stream` blocks). Signal a batch-tier call via `pricing_mode: :batch` (or any pricing_mode containing the `batch` token like `:batch_flex`) — that's the single source of truth now. Previously `batch:` and `pricing_mode:` could disagree, especially after request-side pricing_mode merge inside `Tracker.record` overwrote the parser's mode but left the stored `batch` flag stale, so `calls.batch` could read `true` while `calls.pricing_mode` read `flex` (or vice versa) for the same row.
|
|
19
|
+
- The `bin/rails llm_cost_tracker:prices:explain` rake task (and `LlmCostTracker::Pricing.explain`) is removed — the dashboard's Data Quality page surfaces unknown-pricing models and their effective rates instead.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The RubyLLM SDK integration now requires `ruby_llm >= 1.15.0` (was `>= 1.14.1`).
|
|
24
|
+
- Engine no longer adds `tag` / `tag_value` to Rails `filter_parameters` — the Symbol filter was substring-matching unrelated host-app params (`tags`, `meta_tag`, etc.) into `[FILTERED]`. `Tags::Sanitizer` continues redacting secret-shaped tag values at storage.
|
|
25
|
+
- BREAKING: the serialized event `cost` (the `llm_request.llm_cost_tracker` notification payload and the async-ingestion inbox payload) is now `{ components: {...}, total:, currency: }` (was flat with a top-level `total_cost:`). Notification subscribers should read `cost[:total]`; `ingestion: :async` rolling deploys should drain the inbox first — see [docs/upgrading.md](docs/upgrading.md#v011--v012-unreleased).
|
|
26
|
+
- BREAKING: `pricing_mode` in the `llm_request.llm_cost_tracker` notification payload is now a String (e.g. `"batch"`, `"fast_data_residency"`), not a Symbol — subscribers matching it against a Symbol must compare to the String.
|
|
27
|
+
- BREAKING: `LlmCostTracker.track(tokens:)` now takes the same `_tokens`-suffixed keys as `stream.usage` and the stored columns — `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `audio_input_tokens`, etc. (was the short `input`, `output`, `cache_read_input`, …). Update manual `track` calls. Pricing-file / `pricing_overrides` field names are unchanged — they stay `input`, `output`, … (per-component rates, a separate vocabulary).
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- RubyLLM streaming chats to Anthropic and Gemini (`chat.ask { |chunk| }`) are now recorded — previously the streamed response's raw body is the SSE text rather than the parsed hash the integration read, so an internal lookup raised and the call was silently dropped from the ledger. Blocking RubyLLM calls were unaffected.
|
|
32
|
+
- A malformed or very long `pricing_mode` (or a provider `service_tier` / `speed` with many underscore-separated tokens) no longer hangs cost calculation — the call lands `cost_status: unknown` instead of pinning a CPU.
|
|
33
|
+
- Gemini preview models dated with a four-digit year (e.g. `gemini-2.5-flash-preview-09-2025`) now fall back to the stable model's price instead of landing `cost_status: unknown`.
|
|
34
|
+
- A typo'd price-key prefix in `pricing_overrides` or a custom `prices_file` (e.g. `bath_input` for `batch_input`, or any unknown `<mode>_<component>`) now logs an `Unknown price keys` warning and is ignored, instead of being silently accepted so the override quietly never applied at the intended mode/tier.
|
|
35
|
+
- Anthropic responses with `service_tier: "priority"` now keep `:priority` as their pricing_mode instead of being silently billed at standard rates — committed-tier customers get `cost_status: unknown` (signaling to add `priority_input`/`priority_output` to `pricing_overrides`) instead of an over-counted USD figure that ignores their commitment discount.
|
|
36
|
+
- OpenAI's `scale` enterprise tier and `priority` tier are now recognized as pricing modes (no more `Logging.warn` about unknown tokens); calls land as `cost_status: unknown` when negotiated rates are absent so you can add them via `pricing_overrides`.
|
|
37
|
+
- Gemini responses echoing `usageMetadata.serviceTier: "unspecified"` (the default) now resolve to standard pricing instead of warning about an unknown token and landing as `cost_status: unknown`.
|
|
38
|
+
- Anthropic SDK batch results (`client.messages.batches.results_streaming(id).each`) land in the ledger with `pricing_mode: :batch` and the per-result `provider_response_id`, with a same-process best-effort dedup against already-ledgered `provider_response_id`s so re-iterating the stream doesn't duplicate rows (concurrent retrieves from multiple processes can still race; async-mode rows in the inbox aren't checked until they drain).
|
|
39
|
+
- OpenAI SDK batch processing auto-captures: `client.batches.retrieve(id)` on a completed batch downloads the output JSONL and emits one ledger event per response with `pricing_mode: :batch` and the per-response `provider_response_id`, with the same best-effort dedup as Anthropic batches.
|
|
40
|
+
- OpenRouter pricing is now scraped via `openrouter.ai/api/v1/models`, so RubyLLM-routed OpenRouter calls (e.g. `openrouter/openai/gpt-4o`) get a real `total_cost` from the next `prices_file` refresh instead of landing as `cost_status: unknown`. The scrape also captures `image` / `audio` per-token rates so OpenRouter calls with multimodal inputs bill against the correct bucket instead of folding image/audio tokens into the text-input rate.
|
|
41
|
+
- Misspelled `pricing_mode:` values now log a `Logging.warn` listing the unrecognized token (e.g. `:bach` for `:batch`) so the resulting `cost_status: unknown` call surfaces a typo instead of silently absorbing it; the warn fires once per unique token.
|
|
42
|
+
- Whisper-style transcriptions whose response carries `usage.type = "duration"` now emit a `transcription_minute` line item (quantity = `ceil(seconds / 60)`) across both the OpenAI Ruby SDK patch and the Faraday / RubyLLM HTTP path; the call previously recorded with zero tokens and no line item, so audio-minute usage was invisible.
|
|
43
|
+
- OpenAI Responses-API `image_generation_call` and `computer_call` output items now emit line items so per-call hosted-tool usage shows up on the dashboard alongside the existing `web_search_call` / `file_search_call` / `code_interpreter_call` coverage.
|
|
44
|
+
- `LlmCostTracker.track(..., enforce_budget: true)` now actually raises `BudgetExceededError` pre-call when the estimated cost (token cost plus priced service line items) overshoots the budget, even when `budget_exceeded_behavior: :notify` is configured — previously the kwarg silently no-op'd unless policy was already `:block_requests`.
|
|
45
|
+
- `Call#pricing_snapshot.rates` now includes per-charge rates for non-token service line items (web search, MCP calls, TTS character billing, etc.) — previously only token rates were captured, so audit/replay of service-charge pricing had no record of the rate that was actually applied.
|
|
46
|
+
- Tags with invalid keys (e.g. containing whitespace or characters outside `[\w.-]`) are now skipped at write with a `Logging.warn` instead of being silently written and then raising `InvalidFilterError` on dashboard read.
|
|
47
|
+
- A raising `default_tags` proc is now captured by `Logging.warn` and falls back to empty default tags, so a broken user callback doesn't take down every tracked call.
|
|
48
|
+
- `LlmCostTracker::Ingestion::Worker.shutdown!(drain: true)` always attempts the final inbox flush even if waking the worker thread raises, so pending inbox rows aren't left when the host process exits.
|
|
49
|
+
- Gemini preview-dated models (e.g. `gemini-2.5-flash-preview-04-17`) now resolve to the stable entry's pricing — previously the `preview-MM-DD` suffix didn't match the dated-snapshot regex so the call landed as `cost_status: unknown`.
|
|
50
|
+
- Gemini parser now reads `usageMetadata.serviceTier` from the response body in addition to the `x-gemini-service-tier` header, so tier-aware pricing applies when only the body carries the tier signal.
|
|
51
|
+
- Line-item and pricing-snapshot `currency` is now stored uppercase regardless of `prices_file` casing, so a `prices_file` with `currency: "eur"` shows up as `EUR` everywhere and service-line items don't get partitioned out of header totals on a case mismatch with cost-data currency.
|
|
52
|
+
- Async-ingestion inbox rows reaching `MAX_ATTEMPTS_BEFORE_QUARANTINE` now log a `Logging.warn` (with row ids) at the moment they quarantine, so production sees the event in `Rails.logger` instead of needing to run `bin/rails llm_cost_tracker:doctor` to discover it.
|
|
53
|
+
- Dashboard "Setup required" page now flags missing `llm_cost_tracker_ingestion_inbox_entries` and `llm_cost_tracker_ingestion_leases` tables when `ingestion: :async` is configured — previously the drift only surfaced as a worker boot crash.
|
|
54
|
+
- Gemini image-generation models (`gemini-2.5-flash-image`, `gemini-3-pro-image-preview`, `gemini-3.1-flash-image-preview`) and stable preview text models (`gemini-3.1-pro-preview`, `gemini-2.5-flash-lite-preview-09-2025`, etc.) are no longer dropped by the price scraper — they flow into the pricing snapshot on the next refresh cycle.
|
|
55
|
+
- Gemini parser splits `IMAGE`-modality tokens from `promptTokensDetails` / `candidatesTokensDetails` (mirroring the existing AUDIO handling), so image-output usage from Gemini calls routes to `image_output` rates instead of falling into the text-output bucket.
|
|
56
|
+
- RubyLLM SDK integration over-subtracted cache-read tokens from recorded `input_tokens` on chat completions, so the figure landed in the ledger short by the cache-read amount; the gem now passes RubyLLM's net `input_tokens` through unchanged.
|
|
57
|
+
- RubyLLM SDK integration captures `service_tier` from response bodies across Anthropic, OpenAI, and Gemini — previously the field was read from the wrong JSON path so batch and flex modes silently priced against standard rates.
|
|
58
|
+
- RubyLLM SDK integration records the provider's response id in `provider_response_id` (previously always nil), so each ledger row carries the upstream id you can cross-reference against provider invoices and logs.
|
|
59
|
+
- RubyLLM Anthropic chat completions split 1-hour and 5-minute cache writes into separate token buckets so 1h writes bill at the 2x extended rate instead of being lumped into the 5m bucket at 1.25x.
|
|
60
|
+
- Async-inbox `total_cost` now round-trips through the JSON payload without losing precision; previously the payload coerced `BigDecimal` to `Float` and dropped digits past ~15 significant figures, so high-volume aggregate billing under `ingestion: :async` came out systematically short. BREAKING for subscribers to the `llm_request.llm_cost_tracker` `ActiveSupport::Notifications` event: `payload[:cost]` numeric values are now decimal strings (was `Float`) — wrap with `BigDecimal(value)` before arithmetic.
|
|
61
|
+
|
|
62
|
+
## [0.11.0] - 2026-05-21
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- A "Pricing" page under the dashboard sidebar's new "Reference" group lists every model's rates from `pricing_overrides`, your `prices_file`, and the bundled fallback as separate tabs; the active source (first non-empty in priority order) is highlighted, with last-updated date and currency next to the row count.
|
|
67
|
+
- The Overview page now has Provider and Model filter pills next to Date, so cost slices can be scoped without leaving Overview.
|
|
68
|
+
- The Calls page now has a Stream filter pill for narrowing to streaming or non-streaming calls.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- Models, Calls, and Tags-breakdown tables sort by clicking a column header — ▲/▼ shows direction and clicking again reverses it. URLs use `?sort=<column>&dir=asc|desc`. The previous "Recent / Most expensive / Largest input / Slowest" sort buttons on Calls are gone; use `?sort=cost&dir=desc`, `?sort=latency&dir=desc`, etc. instead.
|
|
73
|
+
- `?sort=unknown_pricing` on `/calls` is replaced by the `?cost_status=incomplete` filter — the Data Quality "Incomplete pricing by model" panel's "Calls" button uses the new URL.
|
|
74
|
+
- The dashboard sidebar stays sticky while you scroll, with pages grouped into "Insights" (Overview / Models / Calls / Tags / Data Quality) and "Reference" (Pricing / Reconciliation if enabled).
|
|
75
|
+
- Dashboard pages use native `<details>` filter popovers instead of inline form rows. Only one popover stays open at a time, and Esc closes the open one.
|
|
76
|
+
- The Calls show page tucks the pricing snapshot and metadata JSON behind expandable `<details>` blocks; the redundant per-component "Tokens" and "Cost" lists are dropped — the Token mix / Cost mix bars already carry the breakdown.
|
|
77
|
+
- The Data Quality page groups stat cards under "Volume" and "Issues" headers, hides zero-count issue cards, and replaces the whole "Issues" block with a single "No data-quality issues in this slice" message when nothing is wrong.
|
|
78
|
+
- The daily spend chart text no longer stretches horizontally on wide screens.
|
|
79
|
+
- `bin/rails llm_cost_tracker:doctor` groups checks under Setup / Schema / Data integrity / Operations headers, renders each row with a `[✓]` / `[!]` / `[x]` status icon (green / yellow / red on a TTY), and aligns the columns so the message stays readable.
|
|
80
|
+
- The dashboard "Setup required" screen clears after `bin/rails db:migrate` without a Rails server restart, and the schema-drift details render as a monospaced block instead of indented bullets.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- The `upgrade_call_rollups_provider`, `upgrade_provider_invoice_imports_provider`, and `upgrade_provider_invoices_metadata_index` migrations no-op when their target table doesn't exist (installs that never opted into `cache_rollups` or reconciliation) instead of crashing.
|
|
85
|
+
- `llm_cost_tracker:*` rake tasks ran their body twice on each invocation, so `doctor` and `report` printed every line twice and `prices:refresh` re-scraped on each run.
|
|
86
|
+
|
|
5
87
|
## [0.10.0] - 2026-05-17
|
|
6
88
|
|
|
7
89
|
### Added
|
data/README.md
CHANGED
|
@@ -15,7 +15,10 @@ attribution only.
|
|
|
15
15
|
|
|
16
16
|
Requires Ruby 3.4+, Rails 7.1+, PostgreSQL or MySQL.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
<picture>
|
|
19
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/dashboard-overview-dark.png">
|
|
20
|
+
<img alt="LLM Cost Tracker dashboard" src="docs/dashboard-overview-light.png">
|
|
21
|
+
</picture>
|
|
19
22
|
|
|
20
23
|
## Quickstart
|
|
21
24
|
|
|
@@ -29,16 +32,17 @@ gem "openai"
|
|
|
29
32
|
bin/rails llm_cost_tracker:setup
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
Runs the install generator, drops a price snapshot, migrates the database, and verifies via `llm_cost_tracker:doctor`.
|
|
35
|
+
Runs the install generator, drops a price snapshot, migrates the database, and verifies via `llm_cost_tracker:doctor`. The generated `config/initializers/llm_cost_tracker.rb` looks like:
|
|
33
36
|
|
|
34
37
|
```ruby
|
|
35
|
-
# config/initializers/llm_cost_tracker.rb
|
|
36
38
|
LlmCostTracker.configure do |config|
|
|
37
39
|
config.default_tags = -> { { environment: Rails.env } }
|
|
38
40
|
config.instrument :openai
|
|
39
41
|
end
|
|
40
42
|
```
|
|
41
43
|
|
|
44
|
+
Edit it in place to add tags, switch on async ingestion, etc.
|
|
45
|
+
|
|
42
46
|
Tag your calls to attribute spend:
|
|
43
47
|
|
|
44
48
|
```ruby
|
|
@@ -82,7 +86,9 @@ The engine ships without authentication on purpose.
|
|
|
82
86
|
| Anything else | `LlmCostTracker.track` |
|
|
83
87
|
|
|
84
88
|
Streams capture when the provider emits final usage. OpenAI Faraday streams
|
|
85
|
-
|
|
89
|
+
get `stream_options: { include_usage: true }` auto-injected so the final
|
|
90
|
+
usage chunk lands in the ledger (opt out via
|
|
91
|
+
`config.auto_enable_stream_usage = false`).
|
|
86
92
|
|
|
87
93
|
## What it isn't
|
|
88
94
|
|
|
@@ -99,7 +105,7 @@ For batch jobs, internal gateways, or anything without an SDK/Faraday hook:
|
|
|
99
105
|
LlmCostTracker.track(
|
|
100
106
|
provider: :anthropic,
|
|
101
107
|
model: "claude-sonnet-4-6",
|
|
102
|
-
tokens: {
|
|
108
|
+
tokens: { input_tokens: 1500, output_tokens: 320 },
|
|
103
109
|
tags: { feature: "summarizer", user_id: current_user.id }
|
|
104
110
|
)
|
|
105
111
|
```
|