llm_cost_tracker 0.4.0 → 0.5.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +195 -109
  4. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +46 -55
  5. data/app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb +81 -0
  6. data/lib/llm_cost_tracker/budget.rb +34 -37
  7. data/lib/llm_cost_tracker/configuration/instrumentation.rb +37 -0
  8. data/lib/llm_cost_tracker/configuration.rb +10 -5
  9. data/lib/llm_cost_tracker/doctor.rb +166 -0
  10. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +33 -0
  11. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +12 -6
  12. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +38 -8
  13. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +1 -2
  14. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +53 -21
  15. data/lib/llm_cost_tracker/integrations/anthropic.rb +75 -0
  16. data/lib/llm_cost_tracker/integrations/base.rb +72 -0
  17. data/lib/llm_cost_tracker/integrations/object_reader.rb +56 -0
  18. data/lib/llm_cost_tracker/integrations/openai.rb +95 -0
  19. data/lib/llm_cost_tracker/integrations/registry.rb +41 -0
  20. data/lib/llm_cost_tracker/middleware/faraday.rb +4 -3
  21. data/lib/llm_cost_tracker/parsed_usage.rb +8 -1
  22. data/lib/llm_cost_tracker/parsers/anthropic.rb +17 -49
  23. data/lib/llm_cost_tracker/parsers/base.rb +80 -0
  24. data/lib/llm_cost_tracker/parsers/gemini.rb +12 -35
  25. data/lib/llm_cost_tracker/parsers/openai.rb +1 -6
  26. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +6 -15
  27. data/lib/llm_cost_tracker/parsers/openai_usage.rb +8 -30
  28. data/lib/llm_cost_tracker/parsers/registry.rb +17 -2
  29. data/lib/llm_cost_tracker/price_freshness.rb +38 -0
  30. data/lib/llm_cost_tracker/price_registry.rb +14 -0
  31. data/lib/llm_cost_tracker/price_sync/fetcher.rb +2 -1
  32. data/lib/llm_cost_tracker/price_sync/refresh_plan_builder.rb +4 -2
  33. data/lib/llm_cost_tracker/price_sync.rb +10 -0
  34. data/lib/llm_cost_tracker/prices.json +394 -41
  35. data/lib/llm_cost_tracker/pricing.rb +8 -1
  36. data/lib/llm_cost_tracker/request_url.rb +20 -0
  37. data/lib/llm_cost_tracker/storage/active_record_rollups.rb +47 -27
  38. data/lib/llm_cost_tracker/storage/active_record_store.rb +4 -0
  39. data/lib/llm_cost_tracker/stream_collector.rb +3 -3
  40. data/lib/llm_cost_tracker/tag_context.rb +52 -0
  41. data/lib/llm_cost_tracker/tags_column.rb +62 -24
  42. data/lib/llm_cost_tracker/tracker.rb +5 -2
  43. data/lib/llm_cost_tracker/version.rb +1 -1
  44. data/lib/llm_cost_tracker.rb +14 -4
  45. data/lib/tasks/llm_cost_tracker.rake +21 -3
  46. metadata +13 -3
  47. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +0 -51
@@ -1,51 +1,404 @@
1
1
  {
2
2
  "metadata": {
3
- "updated_at": "2026-04-18",
3
+ "updated_at": "2026-04-25",
4
4
  "currency": "USD",
5
5
  "unit": "1M tokens",
6
6
  "source_urls": [
7
- "https://openai.com/api/pricing",
8
- "https://www.anthropic.com/pricing",
9
- "https://ai.google.dev/gemini-api/docs/pricing"
7
+ "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
8
+ "https://openrouter.ai/api/v1/models"
10
9
  ]
11
10
  },
12
11
  "models": {
13
- "gpt-5.2": { "input": 1.75, "cache_read_input": 0.175, "output": 14.0 },
14
- "gpt-5.1": { "input": 1.25, "cache_read_input": 0.125, "output": 10.0 },
15
- "gpt-5": { "input": 1.25, "cache_read_input": 0.125, "output": 10.0 },
16
- "gpt-5-mini": { "input": 0.25, "cache_read_input": 0.025, "output": 2.0 },
17
- "gpt-5-nano": { "input": 0.05, "cache_read_input": 0.005, "output": 0.4 },
18
- "gpt-4.1": { "input": 2.0, "cache_read_input": 0.5, "output": 8.0 },
19
- "gpt-4.1-mini": { "input": 0.4, "cache_read_input": 0.1, "output": 1.6 },
20
- "gpt-4.1-nano": { "input": 0.1, "cache_read_input": 0.025, "output": 0.4 },
21
- "gpt-4o-2024-05-13": { "input": 5.0, "output": 15.0 },
22
- "gpt-4o": { "input": 2.5, "cache_read_input": 1.25, "output": 10.0 },
23
- "gpt-4o-mini": { "input": 0.15, "cache_read_input": 0.075, "output": 0.6 },
24
- "gpt-4-turbo": { "input": 10.0, "output": 30.0 },
25
- "gpt-4": { "input": 30.0, "output": 60.0 },
26
- "gpt-3.5-turbo": { "input": 0.5, "output": 1.5 },
27
- "o1": { "input": 15.0, "cache_read_input": 7.5, "output": 60.0 },
28
- "o1-mini": { "input": 1.1, "cache_read_input": 0.55, "output": 4.4 },
29
- "o3": { "input": 2.0, "cache_read_input": 0.5, "output": 8.0 },
30
- "o3-mini": { "input": 1.1, "cache_read_input": 0.55, "output": 4.4 },
31
- "o4-mini": { "input": 1.1, "cache_read_input": 0.275, "output": 4.4 },
32
- "claude-sonnet-4-6": { "input": 3.0, "output": 15.0, "cache_read_input": 0.3, "cache_write_input": 3.75 },
33
- "claude-opus-4-6": { "input": 5.0, "output": 25.0, "cache_read_input": 0.5, "cache_write_input": 6.25 },
34
- "claude-opus-4-1": { "input": 15.0, "output": 75.0, "cache_read_input": 1.5, "cache_write_input": 18.75 },
35
- "claude-opus-4": { "input": 15.0, "output": 75.0, "cache_read_input": 1.5, "cache_write_input": 18.75 },
36
- "claude-sonnet-4-5": { "input": 3.0, "output": 15.0, "cache_read_input": 0.3, "cache_write_input": 3.75 },
37
- "claude-sonnet-4": { "input": 3.0, "output": 15.0, "cache_read_input": 0.3, "cache_write_input": 3.75 },
38
- "claude-haiku-4-5": { "input": 1.0, "output": 5.0, "cache_read_input": 0.1, "cache_write_input": 1.25 },
39
- "claude-3-7-sonnet": { "input": 3.0, "output": 15.0, "cache_read_input": 0.3, "cache_write_input": 3.75 },
40
- "claude-3-5-sonnet": { "input": 3.0, "output": 15.0, "cache_read_input": 0.3, "cache_write_input": 3.75 },
41
- "claude-3-5-haiku": { "input": 0.8, "output": 4.0, "cache_read_input": 0.08, "cache_write_input": 1.0 },
42
- "claude-3-opus": { "input": 15.0, "output": 75.0, "cache_read_input": 1.5, "cache_write_input": 18.75 },
43
- "gemini-2.5-pro": { "input": 1.25, "cache_read_input": 0.125, "output": 10.0 },
44
- "gemini-2.5-flash": { "input": 0.3, "cache_read_input": 0.03, "output": 2.5 },
45
- "gemini-2.5-flash-lite": { "input": 0.1, "cache_read_input": 0.01, "output": 0.4 },
46
- "gemini-2.0-flash": { "input": 0.1, "cache_read_input": 0.025, "output": 0.4 },
47
- "gemini-2.0-flash-lite": { "input": 0.075, "output": 0.3 },
48
- "gemini-1.5-pro": { "input": 1.25, "output": 5.0 },
49
- "gemini-1.5-flash": { "input": 0.075, "output": 0.3 }
12
+ "claude-haiku-4-5": {
13
+ "_source": "litellm",
14
+ "input": 1.0,
15
+ "output": 5.0,
16
+ "cache_read_input": 0.09999999999999999,
17
+ "cache_write_input": 1.25,
18
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
19
+ "_fetched_at": "2026-04-25T10:28:30Z"
20
+ },
21
+ "claude-opus-4": {
22
+ "_source": "openrouter",
23
+ "input": 15.0,
24
+ "output": 75.0,
25
+ "cache_read_input": 1.5,
26
+ "cache_write_input": 18.75,
27
+ "_source_version": "e5f9f0125b69f8b3dbc2ff76f6f6caddc18896623ecd99d3beb5733c88149d45",
28
+ "_fetched_at": "2026-04-25T10:28:30Z"
29
+ },
30
+ "claude-opus-4-1": {
31
+ "_source": "litellm",
32
+ "input": 15.0,
33
+ "output": 75.0,
34
+ "cache_read_input": 1.5,
35
+ "cache_write_input": 18.75,
36
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
37
+ "_fetched_at": "2026-04-25T10:28:30Z"
38
+ },
39
+ "claude-opus-4-5": {
40
+ "_source": "litellm",
41
+ "input": 5.0,
42
+ "output": 25.0,
43
+ "cache_read_input": 0.5,
44
+ "cache_write_input": 6.25,
45
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
46
+ "_fetched_at": "2026-04-25T10:28:30Z"
47
+ },
48
+ "claude-opus-4-6": {
49
+ "_source": "litellm",
50
+ "input": 5.0,
51
+ "output": 25.0,
52
+ "cache_read_input": 0.5,
53
+ "cache_write_input": 6.25,
54
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
55
+ "_fetched_at": "2026-04-25T10:28:30Z"
56
+ },
57
+ "claude-opus-4-7": {
58
+ "_source": "litellm",
59
+ "input": 5.0,
60
+ "output": 25.0,
61
+ "cache_read_input": 0.5,
62
+ "cache_write_input": 6.25,
63
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
64
+ "_fetched_at": "2026-04-25T10:28:30Z"
65
+ },
66
+ "claude-sonnet-4": {
67
+ "_source": "openrouter",
68
+ "input": 3.0,
69
+ "output": 15.0,
70
+ "cache_read_input": 0.3,
71
+ "cache_write_input": 3.75,
72
+ "_source_version": "e5f9f0125b69f8b3dbc2ff76f6f6caddc18896623ecd99d3beb5733c88149d45",
73
+ "_fetched_at": "2026-04-25T10:28:30Z"
74
+ },
75
+ "claude-sonnet-4-5": {
76
+ "_source": "litellm",
77
+ "input": 3.0,
78
+ "output": 15.0,
79
+ "cache_read_input": 0.3,
80
+ "cache_write_input": 3.75,
81
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
82
+ "_fetched_at": "2026-04-25T10:28:30Z"
83
+ },
84
+ "claude-sonnet-4-6": {
85
+ "_source": "litellm",
86
+ "input": 3.0,
87
+ "output": 15.0,
88
+ "cache_read_input": 0.3,
89
+ "cache_write_input": 3.75,
90
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
91
+ "_fetched_at": "2026-04-25T10:28:30Z"
92
+ },
93
+ "gemini-2.0-flash": {
94
+ "input": 0.1,
95
+ "cache_read_input": 0.025,
96
+ "output": 0.4,
97
+ "_source": "seed"
98
+ },
99
+ "gemini-2.0-flash-lite": {
100
+ "input": 0.075,
101
+ "output": 0.3,
102
+ "_source": "seed"
103
+ },
104
+ "gemini-2.5-flash": {
105
+ "_source": "openrouter",
106
+ "input": 0.3,
107
+ "output": 2.5,
108
+ "cache_read_input": 0.03,
109
+ "cache_write_input": 0.08333333333333334,
110
+ "_source_version": "e5f9f0125b69f8b3dbc2ff76f6f6caddc18896623ecd99d3beb5733c88149d45",
111
+ "_fetched_at": "2026-04-25T10:28:30Z"
112
+ },
113
+ "gemini-2.5-flash-lite": {
114
+ "_source": "openrouter",
115
+ "input": 0.09999999999999999,
116
+ "output": 0.39999999999999997,
117
+ "cache_read_input": 0.01,
118
+ "cache_write_input": 0.08333333333333334,
119
+ "_source_version": "e5f9f0125b69f8b3dbc2ff76f6f6caddc18896623ecd99d3beb5733c88149d45",
120
+ "_fetched_at": "2026-04-25T10:28:30Z"
121
+ },
122
+ "gemini-2.5-pro": {
123
+ "_source": "openrouter",
124
+ "input": 1.25,
125
+ "output": 10.0,
126
+ "cache_read_input": 0.125,
127
+ "cache_write_input": 0.375,
128
+ "_source_version": "e5f9f0125b69f8b3dbc2ff76f6f6caddc18896623ecd99d3beb5733c88149d45",
129
+ "_fetched_at": "2026-04-25T10:28:30Z"
130
+ },
131
+ "gpt-3.5-turbo": {
132
+ "_source": "litellm",
133
+ "input": 0.5,
134
+ "output": 1.5,
135
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
136
+ "_fetched_at": "2026-04-25T10:28:30Z"
137
+ },
138
+ "gpt-4": {
139
+ "_source": "litellm",
140
+ "input": 30.0,
141
+ "output": 60.0,
142
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
143
+ "_fetched_at": "2026-04-25T10:28:30Z"
144
+ },
145
+ "gpt-4-turbo": {
146
+ "_source": "litellm",
147
+ "input": 10.0,
148
+ "output": 30.0,
149
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
150
+ "_fetched_at": "2026-04-25T10:28:30Z"
151
+ },
152
+ "gpt-4.1": {
153
+ "_source": "litellm",
154
+ "input": 2.0,
155
+ "output": 8.0,
156
+ "cache_read_input": 0.5,
157
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
158
+ "_fetched_at": "2026-04-25T10:28:30Z"
159
+ },
160
+ "gpt-4.1-mini": {
161
+ "_source": "litellm",
162
+ "input": 0.39999999999999997,
163
+ "output": 1.5999999999999999,
164
+ "cache_read_input": 0.09999999999999999,
165
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
166
+ "_fetched_at": "2026-04-25T10:28:30Z"
167
+ },
168
+ "gpt-4.1-nano": {
169
+ "_source": "litellm",
170
+ "input": 0.09999999999999999,
171
+ "output": 0.39999999999999997,
172
+ "cache_read_input": 0.024999999999999998,
173
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
174
+ "_fetched_at": "2026-04-25T10:28:30Z"
175
+ },
176
+ "gpt-4o": {
177
+ "_source": "litellm",
178
+ "input": 2.5,
179
+ "output": 10.0,
180
+ "cache_read_input": 1.25,
181
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
182
+ "_fetched_at": "2026-04-25T10:28:30Z"
183
+ },
184
+ "gpt-4o-2024-05-13": {
185
+ "_source": "litellm",
186
+ "input": 5.0,
187
+ "output": 15.0,
188
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
189
+ "_fetched_at": "2026-04-25T10:28:30Z"
190
+ },
191
+ "gpt-4o-mini": {
192
+ "_source": "litellm",
193
+ "input": 0.15,
194
+ "output": 0.6,
195
+ "cache_read_input": 0.075,
196
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
197
+ "_fetched_at": "2026-04-25T10:28:30Z"
198
+ },
199
+ "gpt-5": {
200
+ "_source": "litellm",
201
+ "input": 1.25,
202
+ "output": 10.0,
203
+ "cache_read_input": 0.125,
204
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
205
+ "_fetched_at": "2026-04-25T10:28:30Z"
206
+ },
207
+ "gpt-5-chat-latest": {
208
+ "_source": "litellm",
209
+ "input": 1.25,
210
+ "output": 10.0,
211
+ "cache_read_input": 0.125,
212
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
213
+ "_fetched_at": "2026-04-25T10:28:30Z"
214
+ },
215
+ "gpt-5-codex": {
216
+ "_source": "litellm",
217
+ "input": 1.25,
218
+ "output": 10.0,
219
+ "cache_read_input": 0.125,
220
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
221
+ "_fetched_at": "2026-04-25T10:28:30Z"
222
+ },
223
+ "gpt-5-mini": {
224
+ "_source": "litellm",
225
+ "input": 0.25,
226
+ "output": 2.0,
227
+ "cache_read_input": 0.024999999999999998,
228
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
229
+ "_fetched_at": "2026-04-25T10:28:30Z"
230
+ },
231
+ "gpt-5-nano": {
232
+ "_source": "litellm",
233
+ "input": 0.049999999999999996,
234
+ "output": 0.39999999999999997,
235
+ "cache_read_input": 0.005,
236
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
237
+ "_fetched_at": "2026-04-25T10:28:30Z"
238
+ },
239
+ "gpt-5-pro": {
240
+ "_source": "litellm",
241
+ "input": 15.0,
242
+ "output": 120.0,
243
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
244
+ "_fetched_at": "2026-04-25T10:28:30Z"
245
+ },
246
+ "gpt-5.1": {
247
+ "_source": "litellm",
248
+ "input": 1.25,
249
+ "output": 10.0,
250
+ "cache_read_input": 0.125,
251
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
252
+ "_fetched_at": "2026-04-25T10:28:30Z"
253
+ },
254
+ "gpt-5.1-chat-latest": {
255
+ "_source": "litellm",
256
+ "input": 1.25,
257
+ "output": 10.0,
258
+ "cache_read_input": 0.125,
259
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
260
+ "_fetched_at": "2026-04-25T10:28:30Z"
261
+ },
262
+ "gpt-5.1-codex": {
263
+ "_source": "litellm",
264
+ "input": 1.25,
265
+ "output": 10.0,
266
+ "cache_read_input": 0.125,
267
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
268
+ "_fetched_at": "2026-04-25T10:28:30Z"
269
+ },
270
+ "gpt-5.1-codex-max": {
271
+ "_source": "litellm",
272
+ "input": 1.25,
273
+ "output": 10.0,
274
+ "cache_read_input": 0.125,
275
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
276
+ "_fetched_at": "2026-04-25T10:28:30Z"
277
+ },
278
+ "gpt-5.1-codex-mini": {
279
+ "_source": "litellm",
280
+ "input": 0.25,
281
+ "output": 2.0,
282
+ "cache_read_input": 0.024999999999999998,
283
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
284
+ "_fetched_at": "2026-04-25T10:28:30Z"
285
+ },
286
+ "gpt-5.2": {
287
+ "_source": "litellm",
288
+ "input": 1.75,
289
+ "output": 14.0,
290
+ "cache_read_input": 0.175,
291
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
292
+ "_fetched_at": "2026-04-25T10:28:30Z"
293
+ },
294
+ "gpt-5.2-chat-latest": {
295
+ "_source": "litellm",
296
+ "input": 1.75,
297
+ "output": 14.0,
298
+ "cache_read_input": 0.175,
299
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
300
+ "_fetched_at": "2026-04-25T10:28:30Z"
301
+ },
302
+ "gpt-5.2-codex": {
303
+ "_source": "litellm",
304
+ "input": 1.75,
305
+ "output": 14.0,
306
+ "cache_read_input": 0.175,
307
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
308
+ "_fetched_at": "2026-04-25T10:28:30Z"
309
+ },
310
+ "gpt-5.2-pro": {
311
+ "_source": "litellm",
312
+ "input": 21.0,
313
+ "output": 168.0,
314
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
315
+ "_fetched_at": "2026-04-25T10:28:30Z"
316
+ },
317
+ "gpt-5.4": {
318
+ "_source": "litellm",
319
+ "input": 2.5,
320
+ "output": 15.0,
321
+ "cache_read_input": 0.25,
322
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
323
+ "_fetched_at": "2026-04-25T10:28:30Z"
324
+ },
325
+ "gpt-5.4-mini": {
326
+ "_source": "litellm",
327
+ "input": 0.75,
328
+ "output": 4.5,
329
+ "cache_read_input": 0.075,
330
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
331
+ "_fetched_at": "2026-04-25T10:28:30Z"
332
+ },
333
+ "gpt-5.4-nano": {
334
+ "_source": "litellm",
335
+ "input": 0.19999999999999998,
336
+ "output": 1.25,
337
+ "cache_read_input": 0.02,
338
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
339
+ "_fetched_at": "2026-04-25T10:28:30Z"
340
+ },
341
+ "gpt-5.4-pro": {
342
+ "_source": "litellm",
343
+ "input": 30.0,
344
+ "output": 180.0,
345
+ "cache_read_input": 3.0,
346
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
347
+ "_fetched_at": "2026-04-25T10:28:30Z"
348
+ },
349
+ "gpt-5.5": {
350
+ "_source": "litellm",
351
+ "input": 5.0,
352
+ "output": 30.0,
353
+ "cache_read_input": 0.5,
354
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
355
+ "_fetched_at": "2026-04-25T10:28:30Z"
356
+ },
357
+ "gpt-5.5-pro": {
358
+ "_source": "litellm",
359
+ "input": 60.0,
360
+ "output": 360.0,
361
+ "cache_read_input": 6.0,
362
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
363
+ "_fetched_at": "2026-04-25T10:28:30Z"
364
+ },
365
+ "o1": {
366
+ "_source": "litellm",
367
+ "input": 15.0,
368
+ "output": 60.0,
369
+ "cache_read_input": 7.5,
370
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
371
+ "_fetched_at": "2026-04-25T10:28:30Z"
372
+ },
373
+ "o1-mini": {
374
+ "input": 1.1,
375
+ "cache_read_input": 0.55,
376
+ "output": 4.4,
377
+ "_source": "seed"
378
+ },
379
+ "o3": {
380
+ "_source": "litellm",
381
+ "input": 2.0,
382
+ "output": 8.0,
383
+ "cache_read_input": 0.5,
384
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
385
+ "_fetched_at": "2026-04-25T10:28:30Z"
386
+ },
387
+ "o3-mini": {
388
+ "_source": "litellm",
389
+ "input": 1.1,
390
+ "output": 4.4,
391
+ "cache_read_input": 0.55,
392
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
393
+ "_fetched_at": "2026-04-25T10:28:30Z"
394
+ },
395
+ "o4-mini": {
396
+ "_source": "litellm",
397
+ "input": 1.1,
398
+ "output": 4.4,
399
+ "cache_read_input": 0.275,
400
+ "_source_version": "W/\"94f52f337ca0b4f45b6b9c56dcb43317895294a30d00ce62cf9376ab3bfddccf\"",
401
+ "_fetched_at": "2026-04-25T10:28:30Z"
402
+ }
50
403
  }
51
404
  }
@@ -115,12 +115,19 @@ module LlmCostTracker
115
115
 
116
116
  def fuzzy_match(model, normalized_model, table)
117
117
  sorted_price_keys(table).each do |key|
118
- return table[key] if model.start_with?(key) || normalized_model.start_with?(key)
118
+ return table[key] if snapshot_variant?(model, key) || snapshot_variant?(normalized_model, key)
119
119
  end
120
120
 
121
121
  nil
122
122
  end
123
123
 
124
+ def snapshot_variant?(model, key)
125
+ suffix = model.delete_prefix("#{key}-")
126
+ return false if suffix == model
127
+
128
+ suffix.match?(/\A(?:\d{4}-\d{2}-\d{2}|\d{8})\z/)
129
+ end
130
+
124
131
  def sorted_price_keys(table)
125
132
  cached = @sorted_price_keys_cache
126
133
  return cached[:keys] if cached && cached[:table].equal?(table)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module LlmCostTracker
6
+ module RequestUrl
7
+ class << self
8
+ def label(value)
9
+ uri = URI.parse(value.to_s)
10
+ uri.query = nil
11
+ uri.fragment = nil
12
+ uri.user = nil if uri.respond_to?(:user=)
13
+ uri.password = nil if uri.respond_to?(:password=)
14
+ uri.to_s
15
+ rescue URI::InvalidURIError
16
+ value.to_s.split("?", 2).first
17
+ end
18
+ end
19
+ end
20
+ end
@@ -17,47 +17,67 @@ module LlmCostTracker
17
17
  return unless event.cost&.total_cost
18
18
  return unless period_totals_enabled?
19
19
 
20
- PERIODS.each_key { |period| increment_period_total(period, event) }
20
+ model = period_total_model
21
+ model.upsert_all(
22
+ period_rows(event),
23
+ on_duplicate: total_upsert_sql(model),
24
+ record_timestamps: true,
25
+ unique_by: unique_by(model, %i[period period_start])
26
+ )
21
27
  end
22
28
 
23
29
  def monthly_total(time: Time.now.utc)
24
- period_total(:monthly, time)
30
+ period_totals(%i[monthly], time: time).fetch(:monthly)
25
31
  end
26
32
 
27
33
  def daily_total(time: Time.now.utc)
28
- period_total(:daily, time)
34
+ period_totals(%i[daily], time: time).fetch(:daily)
29
35
  end
30
36
 
31
- private
37
+ def period_totals(periods, time: Time.now.utc)
38
+ periods = periods.map(&:to_sym).select { |period| PERIODS.key?(period) }
39
+ return {} if periods.empty?
32
40
 
33
- def period_total(period, time)
34
41
  if period_totals_enabled?
35
- period_total_model
36
- .where(period: PERIODS.fetch(period), period_start: bucket_for(period, time))
37
- .pick(:total_cost)
38
- .to_f
42
+ rollup_period_totals(periods, time)
39
43
  else
40
- LlmCostTracker::LlmApiCall
41
- .where(tracked_at: range_start_for(period, time)..time)
42
- .sum(:total_cost)
43
- .to_f
44
+ periods.to_h { |period| [period, fallback_period_total(period, time)] }
44
45
  end
45
46
  end
46
47
 
47
- def increment_period_total(period, event)
48
- model = period_total_model
49
- model.upsert_all(
50
- [
51
- {
52
- period: PERIODS.fetch(period),
53
- period_start: bucket_for(period, event.tracked_at),
54
- total_cost: event.cost.total_cost
55
- }
56
- ],
57
- on_duplicate: total_upsert_sql(model),
58
- record_timestamps: true,
59
- unique_by: unique_by(model, %i[period period_start])
60
- )
48
+ private
49
+
50
+ def period_rows(event)
51
+ PERIODS.map do |period, name|
52
+ {
53
+ period: name,
54
+ period_start: bucket_for(period, event.tracked_at),
55
+ total_cost: event.cost.total_cost
56
+ }
57
+ end
58
+ end
59
+
60
+ def rollup_period_totals(periods, time)
61
+ buckets = periods.to_h { |period| [period, bucket_for(period, time)] }
62
+ index = buckets.to_h { |period, bucket| [[PERIODS.fetch(period), bucket], period] }
63
+ totals = periods.to_h { |period| [period, 0.0] }
64
+
65
+ period_total_model
66
+ .where(period: periods.map { |period| PERIODS.fetch(period) }, period_start: buckets.values)
67
+ .pluck(:period, :period_start, :total_cost)
68
+ .each do |name, start, total|
69
+ period = index[[name, start.to_date]]
70
+ totals[period] = total.to_f if period
71
+ end
72
+
73
+ totals
74
+ end
75
+
76
+ def fallback_period_total(period, time)
77
+ LlmCostTracker::LlmApiCall
78
+ .where(tracked_at: range_start_for(period, time)..time)
79
+ .sum(:total_cost)
80
+ .to_f
61
81
  end
62
82
 
63
83
  def period_totals_enabled?
@@ -50,6 +50,10 @@ module LlmCostTracker
50
50
  ActiveRecordRollups.daily_total(time: time)
51
51
  end
52
52
 
53
+ def period_totals(periods, time: Time.now.utc)
54
+ ActiveRecordRollups.period_totals(periods, time: time)
55
+ end
56
+
53
57
  private
54
58
 
55
59
  def stringify_tags(tags)
@@ -115,7 +115,7 @@ module LlmCostTracker
115
115
  def finalize(parsed, snapshot)
116
116
  parsed.with(
117
117
  provider: @provider,
118
- model: present_model(parsed.model) || snapshot[:model]
118
+ model: present_model(parsed.model) || present_model(snapshot[:model]) || ParsedUsage::UNKNOWN_MODEL
119
119
  )
120
120
  end
121
121
 
@@ -136,7 +136,7 @@ module LlmCostTracker
136
136
 
137
137
  ParsedUsage.build(
138
138
  provider: @provider,
139
- model: snapshot[:model],
139
+ model: snapshot[:model] || ParsedUsage::UNKNOWN_MODEL,
140
140
  input_tokens: input,
141
141
  output_tokens: output,
142
142
  stream: true,
@@ -148,7 +148,7 @@ module LlmCostTracker
148
148
  def build_unknown_usage(snapshot)
149
149
  ParsedUsage.build(
150
150
  provider: @provider,
151
- model: snapshot[:model],
151
+ model: snapshot[:model] || ParsedUsage::UNKNOWN_MODEL,
152
152
  input_tokens: 0,
153
153
  output_tokens: 0,
154
154
  total_tokens: 0,