llm_cost_tracker 0.6.0 → 0.6.1
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 +6 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- metadata +1 -18
- data/docs/architecture.md +0 -28
- data/docs/budgets.md +0 -45
- data/docs/configuration.md +0 -65
- data/docs/cookbook.md +0 -185
- data/docs/dashboard-overview.png +0 -0
- data/docs/dashboard.md +0 -38
- data/docs/extending.md +0 -32
- data/docs/operations.md +0 -44
- data/docs/pricing.md +0 -94
- data/docs/querying.md +0 -36
- data/docs/streaming.md +0 -70
- data/docs/technical/README.md +0 -10
- data/docs/technical/data-flow.md +0 -70
- data/docs/technical/extension-points.md +0 -111
- data/docs/technical/module-map.md +0 -197
- data/docs/technical/operational-notes.md +0 -97
- data/docs/upgrading.md +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6942269ea8e3ce5f22bb30a1dc6f6c73ee056b33f4f5d7697d1749ce5742ca93
|
|
4
|
+
data.tar.gz: 1b0712cfcbea56ee0e03dd1cb6c400969fc485c5648f75616140a62f6e82fc02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9793cc211dd08669deecf4a1d6a242ec4f2fff3fc41922472d158002731f4db40d618163d6fb054eeb36cae4e8aa708a0055a0fe1820faf88081b26de407107a
|
|
7
|
+
data.tar.gz: 05ed21f8309b16475b4ca7a0a938c999336f0ebb09baf897225422c69dd8e7e81f73ef01e69ff2d4c80d392f49230c212a87ec5c925c6e242b24942c5369f37f
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.1] - 2026-04-29
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Exclude repository documentation from the published gem package.
|
|
12
|
+
|
|
7
13
|
## [0.6.0] - 2026-04-29
|
|
8
14
|
|
|
9
15
|
### Added
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm_cost_tracker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergii Khomenko
|
|
@@ -283,23 +283,6 @@ files:
|
|
|
283
283
|
- app/views/llm_cost_tracker/tags/index.html.erb
|
|
284
284
|
- app/views/llm_cost_tracker/tags/show.html.erb
|
|
285
285
|
- config/routes.rb
|
|
286
|
-
- docs/architecture.md
|
|
287
|
-
- docs/budgets.md
|
|
288
|
-
- docs/configuration.md
|
|
289
|
-
- docs/cookbook.md
|
|
290
|
-
- docs/dashboard-overview.png
|
|
291
|
-
- docs/dashboard.md
|
|
292
|
-
- docs/extending.md
|
|
293
|
-
- docs/operations.md
|
|
294
|
-
- docs/pricing.md
|
|
295
|
-
- docs/querying.md
|
|
296
|
-
- docs/streaming.md
|
|
297
|
-
- docs/technical/README.md
|
|
298
|
-
- docs/technical/data-flow.md
|
|
299
|
-
- docs/technical/extension-points.md
|
|
300
|
-
- docs/technical/module-map.md
|
|
301
|
-
- docs/technical/operational-notes.md
|
|
302
|
-
- docs/upgrading.md
|
|
303
286
|
- lib/llm_cost_tracker.rb
|
|
304
287
|
- lib/llm_cost_tracker/active_record_adapter.rb
|
|
305
288
|
- lib/llm_cost_tracker/assets.rb
|
data/docs/architecture.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
LLM Cost Tracker is a provider-agnostic billing ledger. Core code should model durable billing concepts, not the naming quirks of one provider or one model family.
|
|
4
|
-
|
|
5
|
-
Core vocabulary belongs in provider-neutral terms:
|
|
6
|
-
|
|
7
|
-
- `input_tokens`
|
|
8
|
-
- `cache_read_input_tokens`
|
|
9
|
-
- `cache_write_input_tokens`
|
|
10
|
-
- `output_tokens`
|
|
11
|
-
- `hidden_output_tokens`
|
|
12
|
-
- `pricing_mode`
|
|
13
|
-
- `provider_response_id`
|
|
14
|
-
|
|
15
|
-
Provider-specific names belong only at ingestion boundaries: parsers and stream adapters. Those adapters translate raw fields into the canonical ledger vocabulary before data reaches `Tracker`, `Pricing`, storage, dashboard services, or reports.
|
|
16
|
-
|
|
17
|
-
Pricing logic should prefer generic mechanisms over provider branches. Use provider/model price entries only for lookup and rate selection. Use `pricing_mode` plus mode-prefixed price keys for alternate billing modes instead of adding model-specific conditionals.
|
|
18
|
-
|
|
19
|
-
Tags remain the extension point for app-specific attribution such as tenant, user, feature, trace, job, workflow, or agent session. Do not promote those dimensions into first-class columns unless the ledger itself needs them for provider-agnostic billing behavior.
|
|
20
|
-
|
|
21
|
-
Hot-path guardrails must not aggregate over the growing call ledger. ActiveRecord period budgets should read maintained rows in `llm_cost_tracker_period_totals`; dashboard analytics may run grouped queries because they are user-initiated reporting paths. Do not add dashboard-only aggregate tables until bounded indexed reads from `llm_api_calls` are no longer enough for the supported date range.
|
|
22
|
-
|
|
23
|
-
## Technical Docs
|
|
24
|
-
|
|
25
|
-
- [Module map](technical/module-map.md)
|
|
26
|
-
- [Data flow](technical/data-flow.md)
|
|
27
|
-
- [Extension points](technical/extension-points.md)
|
|
28
|
-
- [Operational notes](technical/operational-notes.md)
|
data/docs/budgets.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# Budgets and Guardrails
|
|
2
|
-
|
|
3
|
-
Budgets are safety rails for a Rails app using LLMs in production. They are not
|
|
4
|
-
invoice reconciliation and they are not a transactional quota system.
|
|
5
|
-
|
|
6
|
-
The full behavior reference is moving here from the README: monthly, daily, and
|
|
7
|
-
per-call budgets; notification payloads; preflight behavior; and failure modes.
|
|
8
|
-
|
|
9
|
-
## Canonical Sources
|
|
10
|
-
|
|
11
|
-
Until this page is expanded, use:
|
|
12
|
-
|
|
13
|
-
- [Budgets](../README.md#budgets)
|
|
14
|
-
- [Known limitations](../README.md#known-limitations)
|
|
15
|
-
- [Operations](operations.md)
|
|
16
|
-
|
|
17
|
-
## Behaviors
|
|
18
|
-
|
|
19
|
-
- `:notify`: call `on_budget_exceeded` after a priced event crosses a limit.
|
|
20
|
-
- `:raise`: record the event, then raise `BudgetExceededError`.
|
|
21
|
-
- `:block_requests`: preflight future calls when stored period totals are
|
|
22
|
-
already over budget.
|
|
23
|
-
|
|
24
|
-
```ruby
|
|
25
|
-
config.monthly_budget = 500.00
|
|
26
|
-
config.daily_budget = 50.00
|
|
27
|
-
config.per_call_budget = 2.00
|
|
28
|
-
config.budget_exceeded_behavior = :block_requests
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
`:block_requests` needs ActiveRecord storage for shared period totals. Under
|
|
32
|
-
concurrency it stops the next request after overspend is visible; it does not
|
|
33
|
-
make provider spend transactional.
|
|
34
|
-
|
|
35
|
-
## Error Payload
|
|
36
|
-
|
|
37
|
-
`BudgetExceededError` exposes:
|
|
38
|
-
|
|
39
|
-
- `budget_type`
|
|
40
|
-
- `total`
|
|
41
|
-
- `budget`
|
|
42
|
-
- `monthly_total`
|
|
43
|
-
- `daily_total`
|
|
44
|
-
- `call_cost`
|
|
45
|
-
- `last_event`
|
data/docs/configuration.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Configuration
|
|
2
|
-
|
|
3
|
-
Configuration is the contract between the host app and the ledger: where events
|
|
4
|
-
go, which integrations are enabled, how attribution is attached, and how the app
|
|
5
|
-
reacts when storage, pricing, or budgets need attention.
|
|
6
|
-
|
|
7
|
-
The full option reference is moving here from the README. Until that migration is
|
|
8
|
-
complete, the README anchors below remain canonical.
|
|
9
|
-
|
|
10
|
-
## Canonical Sources
|
|
11
|
-
|
|
12
|
-
Until this page is expanded, use:
|
|
13
|
-
|
|
14
|
-
- [Quickstart](../README.md#quickstart)
|
|
15
|
-
- [Capturing calls](../README.md#capturing-calls)
|
|
16
|
-
- [Tags](../README.md#tags-who-burned-this-money)
|
|
17
|
-
- [Pricing](../README.md#pricing)
|
|
18
|
-
- [Budgets](../README.md#budgets)
|
|
19
|
-
|
|
20
|
-
## Scope
|
|
21
|
-
|
|
22
|
-
This page is scoped to:
|
|
23
|
-
|
|
24
|
-
- `storage_backend`: `:log`, `:active_record`, and `:custom`; ActiveRecord capture uses a durable inbox when the ingestion migration is present
|
|
25
|
-
- `default_tags`: static tags and per-request callable tags
|
|
26
|
-
- `instrument`: RubyLLM and official SDK integrations
|
|
27
|
-
- `prices_file` and `pricing_overrides`
|
|
28
|
-
- `monthly_budget`, `daily_budget`, and `per_call_budget`
|
|
29
|
-
- `budget_exceeded_behavior`
|
|
30
|
-
- `storage_error_behavior`
|
|
31
|
-
- `unknown_pricing_behavior`
|
|
32
|
-
- `openai_compatible_providers`
|
|
33
|
-
- `report_tag_breakdowns`
|
|
34
|
-
|
|
35
|
-
## Minimal Production Config
|
|
36
|
-
|
|
37
|
-
```ruby
|
|
38
|
-
LlmCostTracker.configure do |config|
|
|
39
|
-
config.storage_backend = :active_record
|
|
40
|
-
config.default_tags = -> { { environment: Rails.env } }
|
|
41
|
-
config.prices_file = Rails.root.join("config/llm_cost_tracker_prices.yml")
|
|
42
|
-
config.instrument :openai
|
|
43
|
-
end
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Keep configuration at boot. Mutable shared settings are frozen after
|
|
47
|
-
`configure` returns so request-time code cannot silently change global tracking
|
|
48
|
-
behavior.
|
|
49
|
-
|
|
50
|
-
Enabled SDK integrations are fail-fast. The client gem must be loaded, meet the
|
|
51
|
-
minimum supported version, and expose the expected classes and methods before
|
|
52
|
-
LLM Cost Tracker installs its wrapper.
|
|
53
|
-
|
|
54
|
-
## Capture Verification
|
|
55
|
-
|
|
56
|
-
After boot, run:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
bin/rails llm_cost_tracker:verify_capture
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
For ActiveRecord storage, the task records a synthetic manual event inside a
|
|
63
|
-
rollback and checks that notifications and persistence both work. For log and
|
|
64
|
-
custom storage, it reports the configured capture path without invoking external
|
|
65
|
-
sinks.
|
data/docs/cookbook.md
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
# Cookbook
|
|
2
|
-
|
|
3
|
-
Short integration recipes for common Ruby clients. Prefer SDK integrations or middleware. Use `track` and `track_stream` only as fallback helpers for unsupported clients.
|
|
4
|
-
|
|
5
|
-
| Client | Best path | Why |
|
|
6
|
-
|---|---|---|
|
|
7
|
-
| RubyLLM | `config.instrument :ruby_llm` | The integration wraps RubyLLM's provider layer without adding a third-party instrumentation gem. |
|
|
8
|
-
| Official `openai` gem | `config.instrument :openai` | The integration wraps SDK resource methods without changing call sites. |
|
|
9
|
-
| Official `anthropic` gem | `config.instrument :anthropic` | The integration records returned message usage without changing call sites. |
|
|
10
|
-
| `ruby-openai` | Faraday middleware | The client is built on Faraday and accepts middleware via the constructor block. |
|
|
11
|
-
| OpenAI-compatible proxy | Faraday middleware | Use `ruby-openai` or a direct Faraday client against the proxy host. |
|
|
12
|
-
| Custom Faraday client | Faraday middleware | The middleware can parse known provider responses automatically. |
|
|
13
|
-
| Other clients | Adapter first, fallback helpers second | Add a stable integration instead of scattering per-call ledger code. |
|
|
14
|
-
|
|
15
|
-
## RubyLLM
|
|
16
|
-
|
|
17
|
-
Enable the integration once, then keep normal RubyLLM calls unchanged.
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
LlmCostTracker.configure do |config|
|
|
21
|
-
config.instrument :ruby_llm
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
LlmCostTracker.with_tags(feature: "support_chat") do
|
|
25
|
-
RubyLLM.chat.ask("Hello")
|
|
26
|
-
RubyLLM.embed("text to embed")
|
|
27
|
-
end
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
The RubyLLM integration supports `ruby_llm >= 1.14.1` and checks RubyLLM's provider contract at boot. Chat, embedding, and transcription calls are captured. Image generation, moderation, and tool execution are not recorded as separate ledger rows.
|
|
31
|
-
|
|
32
|
-
## Official OpenAI SDK
|
|
33
|
-
|
|
34
|
-
Enable the integration once, then keep normal `openai` gem calls unchanged.
|
|
35
|
-
|
|
36
|
-
```ruby
|
|
37
|
-
LlmCostTracker.configure do |config|
|
|
38
|
-
config.instrument :openai
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
client = OpenAI::Client.new(api_key: ENV["OPENAI_API_KEY"])
|
|
42
|
-
|
|
43
|
-
client.responses.create(model: "gpt-4o", input: "Hello")
|
|
44
|
-
client.chat.completions.create(
|
|
45
|
-
model: "gpt-4o",
|
|
46
|
-
messages: [{ role: "user", content: "Hello" }]
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
client.responses.stream(model: "gpt-4o", input: "Hello").each do |event|
|
|
50
|
-
puts event.type
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
client.responses.stream_raw(model: "gpt-4o", input: "Hello").each do |event|
|
|
54
|
-
puts event.type
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
client.chat.completions.stream_raw(
|
|
58
|
-
model: "gpt-4o",
|
|
59
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
60
|
-
stream_options: { include_usage: true }
|
|
61
|
-
).each do |event|
|
|
62
|
-
puts event
|
|
63
|
-
end
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
The OpenAI SDK integration supports `openai >= 0.59.0`. Streaming calls are recorded after the returned stream is consumed. Chat Completions streams need `stream_options: { include_usage: true }` for final usage.
|
|
67
|
-
|
|
68
|
-
## Official Anthropic SDK
|
|
69
|
-
|
|
70
|
-
Enable the integration once, then keep normal `anthropic` gem calls unchanged.
|
|
71
|
-
|
|
72
|
-
```ruby
|
|
73
|
-
LlmCostTracker.configure do |config|
|
|
74
|
-
config.instrument :anthropic
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
client = Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"])
|
|
78
|
-
|
|
79
|
-
client.messages.create(
|
|
80
|
-
max_tokens: 1024,
|
|
81
|
-
model: "claude-sonnet-4-5-20250929",
|
|
82
|
-
messages: [{ role: "user", content: "Hello" }]
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
client.messages.stream(
|
|
86
|
-
max_tokens: 1024,
|
|
87
|
-
model: "claude-sonnet-4-5-20250929",
|
|
88
|
-
messages: [{ role: "user", content: "Hello" }]
|
|
89
|
-
).each do |event|
|
|
90
|
-
puts event.type
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
client.messages.stream_raw(
|
|
94
|
-
max_tokens: 1024,
|
|
95
|
-
model: "claude-sonnet-4-5-20250929",
|
|
96
|
-
messages: [{ role: "user", content: "Hello" }]
|
|
97
|
-
).each do |event|
|
|
98
|
-
puts event.type
|
|
99
|
-
end
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
The Anthropic SDK integration supports `anthropic >= 1.36.0`. Streaming calls are recorded after the returned stream is consumed.
|
|
103
|
-
|
|
104
|
-
## ruby-openai
|
|
105
|
-
|
|
106
|
-
`ruby-openai` is a community client that occupies the same `OpenAI::Client` constant as the official gem; only one of the two can be loaded. `config.instrument :openai` is for the official gem. For `ruby-openai`, attach the Faraday middleware via the constructor block:
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"]) do |f|
|
|
110
|
-
f.use :llm_cost_tracker, tags: { feature: "chat" }
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
client.chat(
|
|
114
|
-
parameters: {
|
|
115
|
-
model: "gpt-4o",
|
|
116
|
-
messages: [{ role: "user", content: "Hello" }],
|
|
117
|
-
stream: proc { |chunk, _bytesize| puts chunk.dig("choices", 0, "delta", "content") },
|
|
118
|
-
stream_options: { include_usage: true }
|
|
119
|
-
}
|
|
120
|
-
)
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Use the constructor block on every client you build, or wrap client creation in your own factory.
|
|
124
|
-
|
|
125
|
-
## Azure OpenAI
|
|
126
|
-
|
|
127
|
-
Azure's v1 API works with OpenAI-compatible HTTP shapes, but pricing and deployment names are yours. Register the Azure host, use the Faraday middleware path, and keep Azure-specific prices in `prices_file` or `pricing_overrides`.
|
|
128
|
-
|
|
129
|
-
```ruby
|
|
130
|
-
LlmCostTracker.configure do |config|
|
|
131
|
-
config.openai_compatible_providers["my-resource.openai.azure.com"] = "azure_openai"
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
conn = Faraday.new(url: "https://my-resource.openai.azure.com") do |f|
|
|
135
|
-
f.use :llm_cost_tracker, tags: { feature: "chat" }
|
|
136
|
-
f.request :json
|
|
137
|
-
f.response :json
|
|
138
|
-
f.adapter Faraday.default_adapter
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
conn.post(
|
|
142
|
-
"/openai/v1/responses",
|
|
143
|
-
{ model: "gpt-4o-prod", input: "Hello" },
|
|
144
|
-
{ "api-key" => ENV.fetch("AZURE_OPENAI_API_KEY") }
|
|
145
|
-
)
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Gemini API
|
|
149
|
-
|
|
150
|
-
Google's official Gemini SDKs do not include Ruby. Use a Faraday client against the REST API so the Gemini parser can capture usage automatically.
|
|
151
|
-
|
|
152
|
-
```ruby
|
|
153
|
-
conn = Faraday.new(url: "https://generativelanguage.googleapis.com") do |f|
|
|
154
|
-
f.use :llm_cost_tracker, tags: { feature: "chat" }
|
|
155
|
-
f.request :json
|
|
156
|
-
f.response :json
|
|
157
|
-
f.adapter Faraday.default_adapter
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
conn.post(
|
|
161
|
-
"/v1beta/models/gemini-2.5-flash:generateContent?key=#{ENV.fetch("GOOGLE_API_KEY")}",
|
|
162
|
-
{ contents: [{ role: "user", parts: [{ text: "Hello" }] }] }
|
|
163
|
-
)
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## LiteLLM proxy
|
|
167
|
-
|
|
168
|
-
LiteLLM Proxy speaks an OpenAI-compatible HTTP shape, so register the proxy host once and keep using the normal middleware path.
|
|
169
|
-
|
|
170
|
-
```ruby
|
|
171
|
-
LlmCostTracker.configure do |config|
|
|
172
|
-
config.openai_compatible_providers["proxy.internal.example"] = "litellm"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
client = OpenAI::Client.new(
|
|
176
|
-
access_token: ENV["LITELLM_API_KEY"],
|
|
177
|
-
uri_base: "https://proxy.internal.example"
|
|
178
|
-
) do |f|
|
|
179
|
-
f.use :llm_cost_tracker, tags: { gateway: "litellm" }
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
client.chat(parameters: { model: "openai/gpt-5-mini", messages: [{ role: "user", content: "Hello" }] })
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
If your proxy exposes custom model IDs or discounts, add them in `prices_file` or `pricing_overrides`.
|
data/docs/dashboard-overview.png
DELETED
|
Binary file
|
data/docs/dashboard.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Dashboard
|
|
2
|
-
|
|
3
|
-
The dashboard is a Rails Engine for humans reviewing spend, attribution, and data
|
|
4
|
-
quality. It is server-rendered ERB, has no JavaScript bundle, and reads from the
|
|
5
|
-
host app's `llm_api_calls` table.
|
|
6
|
-
|
|
7
|
-
The detailed dashboard guide is moving here from the README: mounting, route
|
|
8
|
-
constraints, authentication examples, page map, and operational notes.
|
|
9
|
-
|
|
10
|
-
## Canonical Sources
|
|
11
|
-
|
|
12
|
-
Until this page is expanded, use:
|
|
13
|
-
|
|
14
|
-
- [Dashboard](../README.md#dashboard)
|
|
15
|
-
- [Privacy](../README.md#privacy)
|
|
16
|
-
- [Operations](operations.md)
|
|
17
|
-
|
|
18
|
-
## Mounting
|
|
19
|
-
|
|
20
|
-
```ruby
|
|
21
|
-
mount LlmCostTracker::Engine => "/llm-costs"
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Use `storage_backend = :active_record` for apps that mount the dashboard.
|
|
25
|
-
|
|
26
|
-
## Pages
|
|
27
|
-
|
|
28
|
-
- Overview: spend trend, budget status, anomaly banner, provider rollup, top models
|
|
29
|
-
- Models: spend and usage by provider and model
|
|
30
|
-
- Calls: filterable call ledger with CSV export
|
|
31
|
-
- Tags: tag keys and tag value breakdowns
|
|
32
|
-
- Data Quality: unknown pricing, untagged calls, missing latency, incomplete streams
|
|
33
|
-
|
|
34
|
-
## Authentication
|
|
35
|
-
|
|
36
|
-
The gem does not ship dashboard auth. Mount the engine behind the host app's
|
|
37
|
-
existing authentication layer: Devise, basic auth, Cloudflare Access, or your own
|
|
38
|
-
constraints.
|
data/docs/extending.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# Extending LLM Cost Tracker
|
|
2
|
-
|
|
3
|
-
Extensions belong at clear boundaries: parsers for response shapes, integrations
|
|
4
|
-
for SDK hooks, pricing files for rates, and custom storage for apps that own
|
|
5
|
-
persistence themselves.
|
|
6
|
-
|
|
7
|
-
The practical extension guide is moving here from the README. The lower-level
|
|
8
|
-
contracts already live in the technical extension reference.
|
|
9
|
-
|
|
10
|
-
## Canonical Sources
|
|
11
|
-
|
|
12
|
-
Until this page is expanded, use:
|
|
13
|
-
|
|
14
|
-
- [Capturing calls](../README.md#capturing-calls)
|
|
15
|
-
- [Pricing](pricing.md)
|
|
16
|
-
- [Technical extension points](technical/extension-points.md)
|
|
17
|
-
|
|
18
|
-
## Extension Points
|
|
19
|
-
|
|
20
|
-
- Custom parser: translate a provider response into `ParsedUsage`.
|
|
21
|
-
- OpenAI-compatible host: register the host-to-provider mapping.
|
|
22
|
-
- Custom storage: receive the canonical `Event` and write it elsewhere.
|
|
23
|
-
- Notifications subscriber: observe `llm_request.llm_cost_tracker`.
|
|
24
|
-
- Local price file: model gateway IDs, contract rates, or unsupported models.
|
|
25
|
-
|
|
26
|
-
## Parser Boundary
|
|
27
|
-
|
|
28
|
-
A parser matches request URLs, translates provider response shapes into
|
|
29
|
-
`ParsedUsage`, and returns `nil` when the response is outside its contract.
|
|
30
|
-
|
|
31
|
-
Do provider-specific translation at this boundary. Keep `Tracker`, storage,
|
|
32
|
-
dashboard, and pricing in canonical ledger terms.
|
data/docs/operations.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Operations
|
|
2
|
-
|
|
3
|
-
Production use is mostly about choosing the right storage backend, keeping the
|
|
4
|
-
database healthy, and understanding where the gem is intentionally best effort.
|
|
5
|
-
|
|
6
|
-
The operational guide is moving here from the README: retention, tag storage,
|
|
7
|
-
thread safety, connection pools, and deployment notes.
|
|
8
|
-
|
|
9
|
-
## Canonical Sources
|
|
10
|
-
|
|
11
|
-
Until this page is expanded, use:
|
|
12
|
-
|
|
13
|
-
- [Privacy](../README.md#privacy)
|
|
14
|
-
- [Known limitations](../README.md#known-limitations)
|
|
15
|
-
- [Technical operational notes](technical/operational-notes.md)
|
|
16
|
-
|
|
17
|
-
## Production Defaults
|
|
18
|
-
|
|
19
|
-
- Use `storage_backend = :active_record` for the shared ledger, dashboard, and
|
|
20
|
-
cross-process budget guardrails.
|
|
21
|
-
- Size the ActiveRecord connection pool for your app plus ledger writes.
|
|
22
|
-
- Keep `storage_error_behavior = :warn` unless losing the LLM response is better
|
|
23
|
-
than losing the ledger event.
|
|
24
|
-
- Treat `:block_requests` as a guardrail, not a hard quota.
|
|
25
|
-
- Keep `default_tags` callables fast and thread-safe.
|
|
26
|
-
|
|
27
|
-
## Retention
|
|
28
|
-
|
|
29
|
-
Retention is explicit. Use the prune task when the ledger should not grow
|
|
30
|
-
forever:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
DAYS=90 bin/rails llm_cost_tracker:prune
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
When ActiveRecord period rollups are installed, pruning decrements the
|
|
37
|
-
affected daily and monthly buckets in the same batch transaction as the ledger
|
|
38
|
-
delete.
|
|
39
|
-
|
|
40
|
-
## Data Shape
|
|
41
|
-
|
|
42
|
-
Tags are JSONB with a GIN index on PostgreSQL and JSON text elsewhere. The
|
|
43
|
-
dashboard and query helpers work across supported adapters, but PostgreSQL is the
|
|
44
|
-
strongest path for large tag-heavy ledgers.
|
data/docs/pricing.md
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# Pricing and Price Refresh
|
|
2
|
-
|
|
3
|
-
LLM Cost Tracker prices calls locally from recorded usage and a versioned price
|
|
4
|
-
registry. Providers usually return token counts, not a stable per-request price,
|
|
5
|
-
so the gem stores the calculated cost with each ledger row.
|
|
6
|
-
|
|
7
|
-
The full pricing reference is moving here from the README: registry shape,
|
|
8
|
-
refresh tasks, precedence, provider-qualified keys, and mode-specific rates.
|
|
9
|
-
|
|
10
|
-
## Canonical Sources
|
|
11
|
-
|
|
12
|
-
Until this page is expanded, use:
|
|
13
|
-
|
|
14
|
-
- [Pricing](../README.md#pricing)
|
|
15
|
-
- [Supported providers](../README.md#supported-providers)
|
|
16
|
-
- [Known limitations](../README.md#known-limitations)
|
|
17
|
-
|
|
18
|
-
## Registry Rules
|
|
19
|
-
|
|
20
|
-
- Built-in prices live in `lib/llm_cost_tracker/prices.json`.
|
|
21
|
-
- Local snapshots live wherever `config.prices_file` points.
|
|
22
|
-
- Precedence is `pricing_overrides`, then `prices_file`, then bundled prices.
|
|
23
|
-
- Provider-qualified keys like `openai/gpt-4o-mini` win over model-only keys.
|
|
24
|
-
- Historical rows keep the cost calculated when the call was recorded.
|
|
25
|
-
|
|
26
|
-
## Refresh Commands
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
bin/rails generate llm_cost_tracker:prices
|
|
30
|
-
bin/rails llm_cost_tracker:prices:refresh
|
|
31
|
-
bin/rails llm_cost_tracker:prices:check
|
|
32
|
-
PROVIDER=openai MODEL=gpt-4o bin/rails llm_cost_tracker:prices:explain
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The refresh task reads the maintained LLM Cost Tracker snapshot and writes to
|
|
36
|
-
`ENV["OUTPUT"]`, then `config.prices_file`, then
|
|
37
|
-
`config/llm_cost_tracker_prices.yml`.
|
|
38
|
-
|
|
39
|
-
## Price Fields
|
|
40
|
-
|
|
41
|
-
Base fields:
|
|
42
|
-
|
|
43
|
-
- `input`
|
|
44
|
-
- `output`
|
|
45
|
-
- `cache_read_input`
|
|
46
|
-
- `cache_write_input`
|
|
47
|
-
|
|
48
|
-
Mode-prefixed fields use the same base terms:
|
|
49
|
-
|
|
50
|
-
- `batch_input`
|
|
51
|
-
- `batch_output`
|
|
52
|
-
- `priority_input`
|
|
53
|
-
- `batch_cache_read_input`
|
|
54
|
-
|
|
55
|
-
## Pricing Modes
|
|
56
|
-
|
|
57
|
-
Pass `pricing_mode: :batch` when usage came from a provider batch job or another
|
|
58
|
-
discounted mode:
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
LlmCostTracker.track(
|
|
62
|
-
provider: "openai",
|
|
63
|
-
model: "gpt-4o",
|
|
64
|
-
input_tokens: 1_000_000,
|
|
65
|
-
output_tokens: 250_000,
|
|
66
|
-
pricing_mode: :batch,
|
|
67
|
-
feature: "offline_eval"
|
|
68
|
-
)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
The calculator uses `batch_input`, `batch_output`, and other matching
|
|
72
|
-
mode-prefixed fields when present, then falls back to the base field for missing
|
|
73
|
-
mode-specific rates.
|
|
74
|
-
|
|
75
|
-
## Price Explain
|
|
76
|
-
|
|
77
|
-
Use `prices:explain` when Data Quality shows unknown pricing or a local override
|
|
78
|
-
does not behave as expected:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
PROVIDER=openai MODEL=gpt-4o PRICING_MODE=batch bin/rails llm_cost_tracker:prices:explain
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Optional token env vars let the command check the exact buckets that a call used:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
PROVIDER=custom MODEL=gateway-model INPUT_TOKENS=1000 OUTPUT_TOKENS=200 CACHE_READ_INPUT_TOKENS=50 bin/rails llm_cost_tracker:prices:explain
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The command reports the matched source, matched key, match strategy, effective
|
|
91
|
-
rates, and any missing rate needed to price the event.
|
|
92
|
-
|
|
93
|
-
Provider-specific pricing pages belong in scrapers and snapshots. Runtime
|
|
94
|
-
pricing should stay in canonical billing terms.
|
data/docs/querying.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Querying and Reports
|
|
2
|
-
|
|
3
|
-
Once calls are in `llm_api_calls`, the host app owns the data. Query it from a
|
|
4
|
-
console, a scheduled job, your admin UI, or the mounted dashboard.
|
|
5
|
-
|
|
6
|
-
The full querying reference is moving here from the README: ActiveRecord scopes,
|
|
7
|
-
reporting helpers, tag breakdowns, and SQL-side grouping patterns.
|
|
8
|
-
|
|
9
|
-
## Canonical Sources
|
|
10
|
-
|
|
11
|
-
Until this page is expanded, use:
|
|
12
|
-
|
|
13
|
-
- [Querying](../README.md#querying)
|
|
14
|
-
- [Dashboard](dashboard.md)
|
|
15
|
-
- [Operations](operations.md)
|
|
16
|
-
|
|
17
|
-
## Common Queries
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
LlmCostTracker::LlmApiCall.today.total_cost
|
|
21
|
-
LlmCostTracker::LlmApiCall.this_month.cost_by_model
|
|
22
|
-
LlmCostTracker::LlmApiCall.this_month.cost_by_provider
|
|
23
|
-
LlmCostTracker::LlmApiCall.this_month.cost_by_tag("feature")
|
|
24
|
-
LlmCostTracker::LlmApiCall.by_tags(user_id: 42, feature: "chat").this_month.total_cost
|
|
25
|
-
LlmCostTracker::LlmApiCall.daily_costs(days: 7)
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Report Task
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
bin/rails llm_cost_tracker:report
|
|
32
|
-
DAYS=7 bin/rails llm_cost_tracker:report
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
This page is scoped to cost scopes, tag grouping, period grouping, latency
|
|
36
|
-
helpers, unknown-pricing queries, and report tag breakdowns.
|
data/docs/streaming.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# Streaming Capture
|
|
2
|
-
|
|
3
|
-
Streaming calls should appear in the ledger instead of disappearing into a live
|
|
4
|
-
callback. LLM Cost Tracker records them when the provider emits final usage or
|
|
5
|
-
when the app supplies explicit totals.
|
|
6
|
-
|
|
7
|
-
The full streaming reference is moving here from the README: Faraday streaming,
|
|
8
|
-
`track_stream`, provider response IDs, final usage events, and data-quality
|
|
9
|
-
states.
|
|
10
|
-
|
|
11
|
-
## Canonical Sources
|
|
12
|
-
|
|
13
|
-
Until this page is expanded, use:
|
|
14
|
-
|
|
15
|
-
- [Capturing calls](../README.md#capturing-calls)
|
|
16
|
-
- [Known limitations](../README.md#known-limitations)
|
|
17
|
-
- [Cookbook](cookbook.md)
|
|
18
|
-
|
|
19
|
-
## Faraday Path
|
|
20
|
-
|
|
21
|
-
The middleware tees Faraday's `on_data` callback, keeps chunks flowing to the
|
|
22
|
-
caller, and records usage when the response completes.
|
|
23
|
-
|
|
24
|
-
OpenAI streams need final usage:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
stream_options: { include_usage: true }
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Anthropic and Gemini are parsed from their provider stream event shapes when
|
|
31
|
-
usage is present.
|
|
32
|
-
|
|
33
|
-
## SDK Path
|
|
34
|
-
|
|
35
|
-
Official OpenAI and Anthropic SDK streams are captured when `config.instrument`
|
|
36
|
-
is enabled for the provider. The returned stream object is preserved, and usage
|
|
37
|
-
is recorded after the stream is consumed.
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
config.instrument :openai
|
|
41
|
-
config.instrument :anthropic
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Captured SDK helpers:
|
|
45
|
-
|
|
46
|
-
- OpenAI `responses.stream`, `responses.stream_raw`, `responses.retrieve_streaming`, and `chat.completions.stream_raw`.
|
|
47
|
-
- Anthropic `messages.stream` and `messages.stream_raw`.
|
|
48
|
-
|
|
49
|
-
OpenAI Chat Completions streams need final usage:
|
|
50
|
-
|
|
51
|
-
```ruby
|
|
52
|
-
stream_options: { include_usage: true }
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Manual Path
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
LlmCostTracker.track_stream(provider: "openai", model: "gpt-4o") do |stream|
|
|
59
|
-
my_client.stream(...) { |event| stream.event(event.to_h) }
|
|
60
|
-
end
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
If the client already knows totals, skip provider event parsing:
|
|
64
|
-
|
|
65
|
-
```ruby
|
|
66
|
-
stream.usage(input_tokens: 120, output_tokens: 45)
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Missing final usage is stored with `usage_source: "unknown"` so the Data Quality
|
|
70
|
-
page can surface it.
|
data/docs/technical/README.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# Technical Documentation
|
|
2
|
-
|
|
3
|
-
These files describe the internal module boundaries for LLM Cost Tracker.
|
|
4
|
-
|
|
5
|
-
- [Module map](module-map.md)
|
|
6
|
-
- [Data flow](data-flow.md)
|
|
7
|
-
- [Extension points](extension-points.md)
|
|
8
|
-
- [Operational notes](operational-notes.md)
|
|
9
|
-
|
|
10
|
-
The main rule is simple: provider-specific API shapes stop at ingestion boundaries. The ledger, storage, budgets, dashboard, and reports work with canonical billing concepts.
|
data/docs/technical/data-flow.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# Data Flow
|
|
2
|
-
|
|
3
|
-
This is the normal path from an application LLM call to stored ledger data.
|
|
4
|
-
|
|
5
|
-
## Faraday Requests
|
|
6
|
-
|
|
7
|
-
1. The host app sends an HTTP request through Faraday.
|
|
8
|
-
2. `LlmCostTracker::Middleware::Faraday` checks whether a parser matches the request URL.
|
|
9
|
-
3. For non-streaming responses, the middleware passes request and response data to the parser.
|
|
10
|
-
4. For streaming responses, the middleware tees `on_data`, collects stream events, and parses final usage when the stream completes.
|
|
11
|
-
5. The parser returns `ParsedUsage` with canonical fields.
|
|
12
|
-
6. `Tracker.record` prices and persists the event.
|
|
13
|
-
|
|
14
|
-
## SDK Integrations
|
|
15
|
-
|
|
16
|
-
1. The host app enables an integration with `config.instrument`.
|
|
17
|
-
2. `LlmCostTracker::Integrations` checks the SDK version, target classes, and target methods once at install time.
|
|
18
|
-
3. `LlmCostTracker::Integrations` prepends a narrow wrapper to supported SDK resource methods.
|
|
19
|
-
4. The host app keeps calling the provider SDK normally.
|
|
20
|
-
5. The wrapper measures latency, extracts usage from the SDK response object, and sends canonical fields to `Tracker.record`.
|
|
21
|
-
6. If an explicitly enabled SDK is not loaded or does not satisfy the install contract, boot raises before the app silently misses usage.
|
|
22
|
-
|
|
23
|
-
## Explicit Tracking
|
|
24
|
-
|
|
25
|
-
1. The host app calls `LlmCostTracker.track` with known usage totals, or `LlmCostTracker.track_stream` with stream events.
|
|
26
|
-
2. `track` sends manual totals directly to `Tracker.record`.
|
|
27
|
-
3. `track_stream` uses `StreamCollector`, then parser lookup by provider when events need parsing.
|
|
28
|
-
4. `Tracker.record` prices and persists the event.
|
|
29
|
-
|
|
30
|
-
## Canonical Event Build
|
|
31
|
-
|
|
32
|
-
`Tracker.record` performs the central normalization step:
|
|
33
|
-
|
|
34
|
-
1. Blank model identifiers become `unknown`.
|
|
35
|
-
2. Input, output, cache-read, cache-write, hidden-output, and pricing-mode values are extracted from metadata.
|
|
36
|
-
3. `Pricing.cost_for` calculates a `Cost` object or returns `nil` for unknown pricing.
|
|
37
|
-
4. Tags are merged from `with_tags`, `default_tags`, middleware tags, and explicit metadata.
|
|
38
|
-
5. An `Event` is created and emitted through `ActiveSupport::Notifications`.
|
|
39
|
-
6. The configured storage backend receives the event.
|
|
40
|
-
7. Budget checks run unless storage explicitly returns `false`.
|
|
41
|
-
|
|
42
|
-
## ActiveRecord Storage
|
|
43
|
-
|
|
44
|
-
1. `Storage::ActiveRecordInbox.save` writes a compact durable event row when the ingestion tables are present.
|
|
45
|
-
2. `Storage::ActiveRecordIngestor` claims retryable inbox rows through a database lease and writes batches into `llm_api_calls`.
|
|
46
|
-
3. `Storage::ActiveRecordStore.insert_many` converts tags for JSON or text storage and writes optional fields only when their columns exist.
|
|
47
|
-
4. The call rows, period rollup updates, and inbox deletes happen in one transaction.
|
|
48
|
-
5. `ActiveRecordRollups.increment_many!` updates daily and monthly totals only for rows inserted by the batch.
|
|
49
|
-
6. Budget reads use period totals plus pending inbox totals when available.
|
|
50
|
-
|
|
51
|
-
The inbox write is the durability boundary. Ledger freshness is eventually consistent unless the caller explicitly waits with `LlmCostTracker.flush!`.
|
|
52
|
-
|
|
53
|
-
## Dashboard Reads
|
|
54
|
-
|
|
55
|
-
1. Controllers build a filtered `LlmApiCall` scope.
|
|
56
|
-
2. Dashboard services run targeted aggregate queries.
|
|
57
|
-
3. Helpers render filters, charts, pagination, CSV links, and numeric formatting.
|
|
58
|
-
4. Views render plain ERB with the engine CSS asset.
|
|
59
|
-
|
|
60
|
-
Dashboard reads do not mutate ledger state. They can be heavier than request-time code, but they still need explicit grouping and indexes.
|
|
61
|
-
|
|
62
|
-
## Pricing Refresh
|
|
63
|
-
|
|
64
|
-
1. `llm_cost_tracker:prices:refresh` chooses `ENV["OUTPUT"]`, then `config.prices_file`, then `config/llm_cost_tracker_prices.yml`.
|
|
65
|
-
2. `PriceSync::Fetcher` fetches the maintained LLM Cost Tracker price snapshot.
|
|
66
|
-
3. `PriceSync` validates schema compatibility, gem-version compatibility, and model price shape.
|
|
67
|
-
4. `RegistryWriter` writes a local JSON or YAML registry.
|
|
68
|
-
5. Runtime pricing reloads the local file when its mtime changes.
|
|
69
|
-
|
|
70
|
-
The gem never fetches pricing from the network during normal request tracking.
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# Extension Points
|
|
2
|
-
|
|
3
|
-
Extensions should plug into existing provider-agnostic boundaries. If a new feature needs a provider-specific branch outside ingestion code, revisit the design first.
|
|
4
|
-
|
|
5
|
-
## Custom Parsers
|
|
6
|
-
|
|
7
|
-
Use parser registration when a provider or gateway has a response shape the built-ins do not cover.
|
|
8
|
-
|
|
9
|
-
Expected parser contract:
|
|
10
|
-
|
|
11
|
-
- `match?(url)` detects supported request URLs.
|
|
12
|
-
- `parse(request_url, request_body, response_status, response_body)` returns `ParsedUsage` or `nil`.
|
|
13
|
-
- `parse_stream(request_url, request_body, response_status, events)` returns `ParsedUsage` or `nil`.
|
|
14
|
-
- `streaming_request?(request_url, request_body)` detects streaming requests when the provider does not use a simple `stream: true` field.
|
|
15
|
-
- `provider_names` returns provider names that can be used by `track_stream(provider: ...)`.
|
|
16
|
-
|
|
17
|
-
Use `Parsers::Base` helpers for URL matching and stream-event extraction. Use `Parsers::OpenaiUsage` only for OpenAI-shaped usage hashes.
|
|
18
|
-
|
|
19
|
-
## SDK Integrations
|
|
20
|
-
|
|
21
|
-
Use SDK integrations when a popular Ruby client does not expose a Faraday middleware stack but returns stable usage objects. RubyLLM and the official `openai` and `anthropic` gems qualify. Faraday-based clients that expose a middleware hook, such as `ruby-openai`'s constructor block, are covered by the Faraday middleware instead. Clients with no stable hook must use the explicit `track` / `track_stream` fallback until an integration exists.
|
|
22
|
-
|
|
23
|
-
Expected integration contract:
|
|
24
|
-
|
|
25
|
-
- no hard dependency on the provider SDK
|
|
26
|
-
- fail-fast boot when an explicitly enabled SDK is missing or below the minimum supported version
|
|
27
|
-
- install-time checks for the target classes and methods
|
|
28
|
-
- idempotent `Module#prepend` around narrow resource methods
|
|
29
|
-
- no tracking when the integration is not enabled in configuration
|
|
30
|
-
- canonical usage fields passed to `Tracker.record`
|
|
31
|
-
|
|
32
|
-
SDK integrations belong under `LlmCostTracker::Integrations`. Do not put SDK object-shape handling in parsers, storage, or pricing.
|
|
33
|
-
|
|
34
|
-
External integrations can register an adapter with
|
|
35
|
-
`LlmCostTracker::Integrations.register(:name, adapter)`. The adapter must
|
|
36
|
-
respond to `install` and `status`, and enabled names are still selected through
|
|
37
|
-
`config.instrument`.
|
|
38
|
-
|
|
39
|
-
## OpenAI-Compatible Gateways
|
|
40
|
-
|
|
41
|
-
Use `config.openai_compatible_providers` when a gateway speaks the OpenAI request and response shape.
|
|
42
|
-
|
|
43
|
-
This is for shape compatibility, not pricing. Gateway-specific model IDs or discounts belong in `prices_file` or `pricing_overrides`.
|
|
44
|
-
|
|
45
|
-
## Prices
|
|
46
|
-
|
|
47
|
-
Use `config.prices_file` for the app's source-controlled price snapshot.
|
|
48
|
-
|
|
49
|
-
Use `config.pricing_overrides` for urgent or environment-specific overrides that are easier to keep in Ruby.
|
|
50
|
-
|
|
51
|
-
Supported canonical keys:
|
|
52
|
-
|
|
53
|
-
- `input`
|
|
54
|
-
- `output`
|
|
55
|
-
- `cache_read_input`
|
|
56
|
-
- `cache_write_input`
|
|
57
|
-
- `batch_input`
|
|
58
|
-
- `batch_output`
|
|
59
|
-
- mode-prefixed keys such as `priority_input` or `batch_cache_read_input`
|
|
60
|
-
|
|
61
|
-
Provider-specific pricing details must be translated before they reach runtime pricing.
|
|
62
|
-
|
|
63
|
-
## Tags
|
|
64
|
-
|
|
65
|
-
Tags are the extension point for application attribution:
|
|
66
|
-
|
|
67
|
-
- tenant
|
|
68
|
-
- user
|
|
69
|
-
- feature
|
|
70
|
-
- trace
|
|
71
|
-
- job
|
|
72
|
-
- workflow
|
|
73
|
-
- agent session
|
|
74
|
-
|
|
75
|
-
Use `config.default_tags`, middleware `tags:`, explicit metadata, and `LlmCostTracker.with_tags`. Do not add first-class columns for app dimensions unless the ledger needs that field for provider-agnostic billing behavior.
|
|
76
|
-
|
|
77
|
-
## Storage
|
|
78
|
-
|
|
79
|
-
Use `storage_backend = :custom` only when the host app needs to own persistence completely.
|
|
80
|
-
|
|
81
|
-
Custom storage receives a canonical `Event`. Returning `false` tells the tracker not to run budget checks for that event.
|
|
82
|
-
|
|
83
|
-
ActiveRecord storage is the production path for dashboards and cross-process budgets.
|
|
84
|
-
|
|
85
|
-
Storage adapters can register with
|
|
86
|
-
`LlmCostTracker::Storage.register(:name, backend)`. A backend must respond to
|
|
87
|
-
`save(event)` and may expose `verify` for capture diagnostics.
|
|
88
|
-
|
|
89
|
-
## Dashboard
|
|
90
|
-
|
|
91
|
-
Dashboard additions should be read-only services under `app/services/llm_cost_tracker/dashboard`.
|
|
92
|
-
|
|
93
|
-
Keep controller actions thin:
|
|
94
|
-
|
|
95
|
-
- parse params
|
|
96
|
-
- build filtered scope
|
|
97
|
-
- call services
|
|
98
|
-
- render views
|
|
99
|
-
|
|
100
|
-
Keep view logic in helpers when it is reused across pages. Do not add JavaScript for dashboard behavior.
|
|
101
|
-
|
|
102
|
-
## Generators
|
|
103
|
-
|
|
104
|
-
Generators are installation contracts. New generator behavior should be:
|
|
105
|
-
|
|
106
|
-
- additive when possible
|
|
107
|
-
- idempotent where Rails generator APIs allow it
|
|
108
|
-
- explicit about destructive or table-rewriting operations
|
|
109
|
-
- covered by generator template specs
|
|
110
|
-
|
|
111
|
-
Fresh install templates and upgrade generators should stay aligned. If a fresh install gains a column or index, the upgrade path needs a generator unless the next release intentionally makes a breaking install path.
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# Module Map
|
|
2
|
-
|
|
3
|
-
LLM Cost Tracker is organized around a small set of durable responsibilities. File layout does not need to mirror these modules perfectly, but new code should fit one of these boundaries.
|
|
4
|
-
|
|
5
|
-
## Public API and Configuration
|
|
6
|
-
|
|
7
|
-
Primary files:
|
|
8
|
-
|
|
9
|
-
- `lib/llm_cost_tracker.rb`
|
|
10
|
-
- `lib/llm_cost_tracker/configuration.rb`
|
|
11
|
-
- `lib/llm_cost_tracker/tag_context.rb`
|
|
12
|
-
- `lib/llm_cost_tracker/doctor.rb`
|
|
13
|
-
- `lib/llm_cost_tracker/logging.rb`
|
|
14
|
-
- `lib/llm_cost_tracker/errors.rb`
|
|
15
|
-
|
|
16
|
-
Responsibilities:
|
|
17
|
-
|
|
18
|
-
- Expose `configure`, `track`, `track_stream`, `with_tags`, and `enforce_budget!`.
|
|
19
|
-
- Keep configuration immutable after `configure` returns.
|
|
20
|
-
- Merge scoped tags and default tags without leaking state across threads.
|
|
21
|
-
- Report installation and pricing health through `llm_cost_tracker:doctor`.
|
|
22
|
-
|
|
23
|
-
This module should stay small. It can orchestrate other modules, but it should not contain provider parsing, SQL details, dashboard aggregation, or pricing-source logic.
|
|
24
|
-
|
|
25
|
-
## SDK Integrations
|
|
26
|
-
|
|
27
|
-
Primary files:
|
|
28
|
-
|
|
29
|
-
- `lib/llm_cost_tracker/integrations/*`
|
|
30
|
-
|
|
31
|
-
Responsibilities:
|
|
32
|
-
|
|
33
|
-
- Add optional instrumentation for Ruby SDKs without adding provider SDK dependencies.
|
|
34
|
-
- Install narrow, idempotent `Module#prepend` wrappers around stable SDK resource methods.
|
|
35
|
-
- Extract SDK response objects into canonical usage fields.
|
|
36
|
-
- Keep SDK-specific object handling out of `Tracker` and storage.
|
|
37
|
-
|
|
38
|
-
Integrations are for Ruby SDK object shapes. Parsers are for HTTP and stream payload shapes.
|
|
39
|
-
|
|
40
|
-
## Ingestion
|
|
41
|
-
|
|
42
|
-
Primary files:
|
|
43
|
-
|
|
44
|
-
- `lib/llm_cost_tracker/middleware/faraday.rb`
|
|
45
|
-
- `lib/llm_cost_tracker/stream_collector.rb`
|
|
46
|
-
- `lib/llm_cost_tracker/parsed_usage.rb`
|
|
47
|
-
- `lib/llm_cost_tracker/request_url.rb`
|
|
48
|
-
- `lib/llm_cost_tracker/parsers/*`
|
|
49
|
-
|
|
50
|
-
Responsibilities:
|
|
51
|
-
|
|
52
|
-
- Detect supported LLM HTTP requests.
|
|
53
|
-
- Parse provider responses and stream events into `ParsedUsage`.
|
|
54
|
-
- Translate provider-specific fields into canonical usage fields.
|
|
55
|
-
- Preserve app streaming behavior while teeing events for tracking.
|
|
56
|
-
|
|
57
|
-
Provider-specific code belongs here. The output boundary is `ParsedUsage`, not raw provider JSON.
|
|
58
|
-
|
|
59
|
-
## Canonical Ledger
|
|
60
|
-
|
|
61
|
-
Primary files:
|
|
62
|
-
|
|
63
|
-
- `lib/llm_cost_tracker/tracker.rb`
|
|
64
|
-
- `lib/llm_cost_tracker/event.rb`
|
|
65
|
-
- `lib/llm_cost_tracker/event_metadata.rb`
|
|
66
|
-
- `lib/llm_cost_tracker/usage_breakdown.rb`
|
|
67
|
-
- `lib/llm_cost_tracker/cost.rb`
|
|
68
|
-
- `lib/llm_cost_tracker/unknown_pricing.rb`
|
|
69
|
-
|
|
70
|
-
Responsibilities:
|
|
71
|
-
|
|
72
|
-
- Normalize provider, model, usage, tags, latency, streaming flags, and response IDs.
|
|
73
|
-
- Price canonical usage through `Pricing`.
|
|
74
|
-
- Emit `ActiveSupport::Notifications`.
|
|
75
|
-
- Persist the event through the configured storage backend.
|
|
76
|
-
- Run budget checks after successful storage.
|
|
77
|
-
|
|
78
|
-
This module must remain provider-agnostic. It should never branch on a specific provider model family.
|
|
79
|
-
|
|
80
|
-
## Pricing
|
|
81
|
-
|
|
82
|
-
Primary files:
|
|
83
|
-
|
|
84
|
-
- `lib/llm_cost_tracker/pricing.rb`
|
|
85
|
-
- `lib/llm_cost_tracker/price_registry.rb`
|
|
86
|
-
- `lib/llm_cost_tracker/price_freshness.rb`
|
|
87
|
-
- `lib/llm_cost_tracker/prices.json`
|
|
88
|
-
- `lib/llm_cost_tracker/price_sync/*`
|
|
89
|
-
- `lib/tasks/llm_cost_tracker.rake`
|
|
90
|
-
|
|
91
|
-
Responsibilities:
|
|
92
|
-
|
|
93
|
-
- Load bundled prices, local price snapshots, and Ruby overrides.
|
|
94
|
-
- Apply pricing precedence: `pricing_overrides`, `prices_file`, bundled prices.
|
|
95
|
-
- Calculate costs from canonical usage fields.
|
|
96
|
-
- Update local snapshots from the maintained LLM Cost Tracker price registry.
|
|
97
|
-
- Validate snapshot schema compatibility, gem-version compatibility, and price entry shape.
|
|
98
|
-
|
|
99
|
-
Pricing refresh must not perform boot-time or request-time network work. Runtime pricing uses bundled prices, local files, and in-memory caches.
|
|
100
|
-
|
|
101
|
-
## Storage
|
|
102
|
-
|
|
103
|
-
Primary files:
|
|
104
|
-
|
|
105
|
-
- `lib/llm_cost_tracker/llm_api_call.rb`
|
|
106
|
-
- `lib/llm_cost_tracker/period_total.rb`
|
|
107
|
-
- `lib/llm_cost_tracker/llm_api_call_metrics.rb`
|
|
108
|
-
- `lib/llm_cost_tracker/storage/active_record_store.rb`
|
|
109
|
-
- `lib/llm_cost_tracker/storage/active_record_rollups.rb`
|
|
110
|
-
- `lib/llm_cost_tracker/storage/registry.rb`
|
|
111
|
-
- `lib/llm_cost_tracker/tags_column.rb`
|
|
112
|
-
- `lib/llm_cost_tracker/tag_key.rb`
|
|
113
|
-
- `lib/llm_cost_tracker/tag_sql.rb`
|
|
114
|
-
- `lib/llm_cost_tracker/tag_query.rb`
|
|
115
|
-
- `lib/llm_cost_tracker/tag_accessors.rb`
|
|
116
|
-
- `lib/llm_cost_tracker/period_grouping.rb`
|
|
117
|
-
|
|
118
|
-
Responsibilities:
|
|
119
|
-
|
|
120
|
-
- Persist canonical events into ActiveRecord.
|
|
121
|
-
- Hide database-specific tag storage differences.
|
|
122
|
-
- Maintain period rollups for hot-path budget reads.
|
|
123
|
-
- Provide safe scopes for filters, periods, tags, unknown pricing, and reports.
|
|
124
|
-
|
|
125
|
-
Storage can know about database adapters and optional columns. It should not parse provider responses or fetch price data.
|
|
126
|
-
|
|
127
|
-
## Budgets and Retention
|
|
128
|
-
|
|
129
|
-
Primary files:
|
|
130
|
-
|
|
131
|
-
- `lib/llm_cost_tracker/budget.rb`
|
|
132
|
-
- `lib/llm_cost_tracker/retention.rb`
|
|
133
|
-
- `lib/llm_cost_tracker/storage/active_record_rollups.rb`
|
|
134
|
-
|
|
135
|
-
Responsibilities:
|
|
136
|
-
|
|
137
|
-
- Enforce monthly, daily, and per-call guardrails.
|
|
138
|
-
- Support preflight blocking where ActiveRecord rollups are available.
|
|
139
|
-
- Prune old ledger rows in batches.
|
|
140
|
-
- Keep budget checks bounded by maintained aggregates, not by full ledger scans.
|
|
141
|
-
|
|
142
|
-
Budget behavior is part of the hot path. Any change here must be measured against per-request overhead.
|
|
143
|
-
|
|
144
|
-
## Dashboard and Reporting
|
|
145
|
-
|
|
146
|
-
Primary files:
|
|
147
|
-
|
|
148
|
-
- `lib/llm_cost_tracker/report*.rb`
|
|
149
|
-
- `app/controllers/llm_cost_tracker/*`
|
|
150
|
-
- `app/services/llm_cost_tracker/dashboard/*`
|
|
151
|
-
- `app/helpers/llm_cost_tracker/*`
|
|
152
|
-
- `app/views/llm_cost_tracker/*`
|
|
153
|
-
- `app/assets/llm_cost_tracker/application.css`
|
|
154
|
-
|
|
155
|
-
Responsibilities:
|
|
156
|
-
|
|
157
|
-
- Render server-side dashboard pages.
|
|
158
|
-
- Aggregate spend, calls, providers, models, tags, latency, and data quality.
|
|
159
|
-
- Export filtered calls as CSV.
|
|
160
|
-
- Keep dashboard queries explicit and indexed.
|
|
161
|
-
|
|
162
|
-
Dashboard code may run grouped SQL because it is user-initiated reporting. It must stay server-rendered and must not introduce a JavaScript bundle.
|
|
163
|
-
|
|
164
|
-
## Rails Integration and Generators
|
|
165
|
-
|
|
166
|
-
Primary files:
|
|
167
|
-
|
|
168
|
-
- `lib/llm_cost_tracker/railtie.rb`
|
|
169
|
-
- `lib/llm_cost_tracker/engine.rb`
|
|
170
|
-
- `lib/llm_cost_tracker/assets.rb`
|
|
171
|
-
- `lib/llm_cost_tracker/generators/llm_cost_tracker/*`
|
|
172
|
-
- `config/routes.rb`
|
|
173
|
-
|
|
174
|
-
Responsibilities:
|
|
175
|
-
|
|
176
|
-
- Register rake tasks and Faraday middleware.
|
|
177
|
-
- Mount the isolated Rails engine.
|
|
178
|
-
- Generate migrations, initializer, dashboard route, and local price snapshots.
|
|
179
|
-
- Serve dashboard CSS as a fingerprinted engine asset.
|
|
180
|
-
|
|
181
|
-
Generator templates are public installation contracts. Treat them like API.
|
|
182
|
-
|
|
183
|
-
## Test Suites
|
|
184
|
-
|
|
185
|
-
Primary files:
|
|
186
|
-
|
|
187
|
-
- `spec/llm_cost_tracker/*`
|
|
188
|
-
- `spec/llm_cost_tracker/engine/*`
|
|
189
|
-
- `spec/llm_cost_tracker/dashboard/*`
|
|
190
|
-
- `spec/fixtures/pricing/*`
|
|
191
|
-
- `spec/support/*`
|
|
192
|
-
|
|
193
|
-
Responsibilities:
|
|
194
|
-
|
|
195
|
-
- Cover canonical behavior, parser boundaries, pricing precedence, storage rollups, dashboard rendering, generators, and concurrency.
|
|
196
|
-
- Keep request specs plain and stable.
|
|
197
|
-
- Run through `bin/check` before release work or commits that touch code.
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# Operational Notes
|
|
2
|
-
|
|
3
|
-
This file describes runtime constraints that should shape implementation decisions.
|
|
4
|
-
|
|
5
|
-
## Hot Paths
|
|
6
|
-
|
|
7
|
-
Hot-path code includes:
|
|
8
|
-
|
|
9
|
-
- Faraday middleware request and response handling
|
|
10
|
-
- stream collection
|
|
11
|
-
- `Tracker.record`
|
|
12
|
-
- `Pricing.cost_for`
|
|
13
|
-
- ActiveRecord event persistence
|
|
14
|
-
- budget checks
|
|
15
|
-
|
|
16
|
-
Hot-path code must avoid:
|
|
17
|
-
|
|
18
|
-
- network calls
|
|
19
|
-
- per-event schema discovery beyond memoized checks
|
|
20
|
-
- full ledger aggregation
|
|
21
|
-
- unbounded stream buffers
|
|
22
|
-
- N+1 queries
|
|
23
|
-
- price-refresh work
|
|
24
|
-
|
|
25
|
-
## Pricing Freshness
|
|
26
|
-
|
|
27
|
-
Runtime pricing is local:
|
|
28
|
-
|
|
29
|
-
1. Ruby overrides
|
|
30
|
-
2. configured local price snapshot
|
|
31
|
-
3. bundled prices
|
|
32
|
-
|
|
33
|
-
Price update tasks are operational tooling. They can fetch the maintained LLM Cost Tracker price snapshot because the operator runs them intentionally. Request tracking must never depend on live provider pricing pages.
|
|
34
|
-
|
|
35
|
-
## Budget Reads
|
|
36
|
-
|
|
37
|
-
Monthly and daily budgets should read `llm_cost_tracker_period_totals` when the table exists and add pending `llm_cost_tracker_inbox_events` totals while durable ingestion is enabled. Falling back to summing `llm_api_calls` is an upgrade compatibility path, not the preferred production path.
|
|
38
|
-
|
|
39
|
-
The stored period total and pending inbox total should be read in one database statement so request-time budget checks do not undercount during the inbox-to-ledger handoff.
|
|
40
|
-
|
|
41
|
-
Per-call budgets are checked from the current event only.
|
|
42
|
-
|
|
43
|
-
## Durable Ingestion
|
|
44
|
-
|
|
45
|
-
Inbox writes inside an open caller transaction need a separate database connection to survive caller rollbacks. If the pool cannot provide one, storage should fail honestly through `storage_error_behavior` instead of writing into the caller transaction and pretending the event is durable.
|
|
46
|
-
|
|
47
|
-
The ingestor is database-leased and database-polled, with an opportunistic local wake after a successful inbox insert. The wake only reduces freshness latency in the process that wrote the row; correctness still comes from the shared database lease, retryable row locks, and adaptive polling across Puma, Sidekiq, Unicorn, deploy restarts, and multi-process hosts.
|
|
48
|
-
|
|
49
|
-
Freshness and durability are separate concerns. If the writing process exits before its local ingestor drains the row, another process can pick it up on a later poll; budget reads include pending inbox totals and operators can call `LlmCostTracker.flush!` when they need the ledger drained before continuing.
|
|
50
|
-
|
|
51
|
-
The ingestor should check for claimable rows before acquiring the leader lease. Empty queues should not create steady lease-table writes across an idle fleet.
|
|
52
|
-
|
|
53
|
-
Batch size is a conservative internal constant. Do not expose it as a configuration knob until production measurements show that a supported workload needs tuning; more knobs make installations harder to reason about.
|
|
54
|
-
|
|
55
|
-
Ingestors should claim only retryable rows. Rows that keep failing after the retry cap stay in `llm_cost_tracker_inbox_events` with `last_error` for operator inspection and must not block healthy rows behind them.
|
|
56
|
-
|
|
57
|
-
Process shutdown should stop the local ingestor thread without forcing every exiting process to drain the shared inbox. Operators can call `LlmCostTracker.flush!` when they intentionally want to wait for the durable inbox to drain.
|
|
58
|
-
|
|
59
|
-
## Retention
|
|
60
|
-
|
|
61
|
-
Retention may delete old `llm_api_calls`. Period rollups are the durable budget aggregate. Any migration or refactor that changes rollups must preserve the meaning of retained totals or clearly document a breaking change.
|
|
62
|
-
|
|
63
|
-
## Optional Columns
|
|
64
|
-
|
|
65
|
-
The gem supports upgrade paths where older apps may not have every column yet. Optional column checks must be memoized and refreshed when ActiveRecord column information is reset.
|
|
66
|
-
|
|
67
|
-
Do not put table or column checks directly in loops that run for every event without caching.
|
|
68
|
-
|
|
69
|
-
## Dashboard Queries
|
|
70
|
-
|
|
71
|
-
Dashboard queries can aggregate because they are user-initiated. They should still use:
|
|
72
|
-
|
|
73
|
-
- filtered scopes
|
|
74
|
-
- bounded pagination
|
|
75
|
-
- database-side grouping
|
|
76
|
-
- indexes that match common filters
|
|
77
|
-
- single aggregate queries for related counters
|
|
78
|
-
|
|
79
|
-
Avoid loading ledger rows into Ruby just to count, sum, group, or sort.
|
|
80
|
-
|
|
81
|
-
The dashboard is not the center of the storage design. Prefer bounded ranges, existing ledger indexes, pagination, and database-side aggregates over new dashboard-specific tables. Add a summary table only when a measured supported dashboard query cannot be made acceptable with the existing ledger and period totals.
|
|
82
|
-
|
|
83
|
-
## Streaming
|
|
84
|
-
|
|
85
|
-
Streaming capture must keep the host app's stream behavior intact.
|
|
86
|
-
|
|
87
|
-
The middleware should collect enough data to parse final usage while bounding memory. When usage never arrives or capture overflows, record an unknown-usage event so Data Quality can surface the gap.
|
|
88
|
-
|
|
89
|
-
## Release Checks
|
|
90
|
-
|
|
91
|
-
Run `bin/check` before committing code changes intended for release. It includes full RuboCop, full RSpec, project coverage, and patch coverage for the current diff.
|
|
92
|
-
|
|
93
|
-
Project coverage defaults to the Codecov target. Patch coverage defaults to 95% so local checks stay stricter than Codecov parser differences. Thresholds can be adjusted locally with `PROJECT_COVERAGE_MIN`, `PATCH_COVERAGE_MIN`, or `COVERAGE_BASE`.
|
|
94
|
-
|
|
95
|
-
For the closest match to the Codecov upload job, run `BUNDLE_GEMFILE=gemfiles/rails_8_1.gemfile bin/check`.
|
|
96
|
-
|
|
97
|
-
Docs-only changes do not require the full suite, but any code, generator, migration, parser, pricing, dashboard, or storage change does.
|
data/docs/upgrading.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# Upgrading
|
|
2
|
-
|
|
3
|
-
LLM Cost Tracker is still moving quickly, so upgrades should be explicit:
|
|
4
|
-
inspect the changelog, run doctor, and apply only the generators your schema is
|
|
5
|
-
missing.
|
|
6
|
-
|
|
7
|
-
The version-by-version upgrade guide is moving here from the README.
|
|
8
|
-
|
|
9
|
-
## Canonical Sources
|
|
10
|
-
|
|
11
|
-
Until this page is expanded, use:
|
|
12
|
-
|
|
13
|
-
- [Changelog](../CHANGELOG.md)
|
|
14
|
-
- [Quickstart](../README.md#quickstart)
|
|
15
|
-
- [Operations](operations.md)
|
|
16
|
-
|
|
17
|
-
## Schema Generators
|
|
18
|
-
|
|
19
|
-
Existing installs can add newer optional columns through focused generators:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
bin/rails generate llm_cost_tracker:add_period_totals
|
|
23
|
-
bin/rails generate llm_cost_tracker:add_ingestion
|
|
24
|
-
bin/rails generate llm_cost_tracker:add_streaming
|
|
25
|
-
bin/rails generate llm_cost_tracker:add_provider_response_id
|
|
26
|
-
bin/rails generate llm_cost_tracker:add_usage_breakdown
|
|
27
|
-
bin/rails generate llm_cost_tracker:upgrade_tags_to_jsonb
|
|
28
|
-
bin/rails generate llm_cost_tracker:upgrade_cost_precision
|
|
29
|
-
bin/rails generate llm_cost_tracker:add_latency_ms
|
|
30
|
-
bin/rails db:migrate
|
|
31
|
-
bin/rails llm_cost_tracker:doctor
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
On PostgreSQL, `upgrade_tags_to_jsonb` rewrites `llm_api_calls`. For large
|
|
35
|
-
tables, run it during a maintenance window or replace it with a two-phase
|
|
36
|
-
backfill.
|
|
37
|
-
|
|
38
|
-
## Upgrade Habit
|
|
39
|
-
|
|
40
|
-
Run:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
bin/rails llm_cost_tracker:doctor
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Doctor tells you which optional columns and production-hardening pieces are still
|
|
47
|
-
missing.
|