llm_cost_tracker 0.4.1 → 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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +182 -100
  4. data/lib/llm_cost_tracker/configuration/instrumentation.rb +37 -0
  5. data/lib/llm_cost_tracker/configuration.rb +10 -5
  6. data/lib/llm_cost_tracker/doctor.rb +166 -0
  7. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +33 -0
  8. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +12 -6
  9. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +53 -21
  10. data/lib/llm_cost_tracker/integrations/anthropic.rb +75 -0
  11. data/lib/llm_cost_tracker/integrations/base.rb +72 -0
  12. data/lib/llm_cost_tracker/integrations/object_reader.rb +56 -0
  13. data/lib/llm_cost_tracker/integrations/openai.rb +95 -0
  14. data/lib/llm_cost_tracker/integrations/registry.rb +41 -0
  15. data/lib/llm_cost_tracker/middleware/faraday.rb +4 -3
  16. data/lib/llm_cost_tracker/parsed_usage.rb +8 -1
  17. data/lib/llm_cost_tracker/parsers/base.rb +1 -1
  18. data/lib/llm_cost_tracker/parsers/openai_usage.rb +1 -1
  19. data/lib/llm_cost_tracker/price_freshness.rb +38 -0
  20. data/lib/llm_cost_tracker/price_registry.rb +14 -0
  21. data/lib/llm_cost_tracker/price_sync/fetcher.rb +2 -1
  22. data/lib/llm_cost_tracker/price_sync/refresh_plan_builder.rb +4 -2
  23. data/lib/llm_cost_tracker/price_sync.rb +10 -0
  24. data/lib/llm_cost_tracker/prices.json +394 -41
  25. data/lib/llm_cost_tracker/pricing.rb +8 -1
  26. data/lib/llm_cost_tracker/request_url.rb +20 -0
  27. data/lib/llm_cost_tracker/stream_collector.rb +3 -3
  28. data/lib/llm_cost_tracker/tag_context.rb +52 -0
  29. data/lib/llm_cost_tracker/tracker.rb +5 -2
  30. data/lib/llm_cost_tracker/version.rb +1 -1
  31. data/lib/llm_cost_tracker.rb +14 -4
  32. data/lib/tasks/llm_cost_tracker.rake +21 -3
  33. metadata +12 -3
  34. 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
@@ -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,
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/isolated_execution_state"
4
+
5
+ require_relative "value_helpers"
6
+
7
+ module LlmCostTracker
8
+ module TagContext
9
+ KEY = :llm_cost_tracker_tags
10
+
11
+ class << self
12
+ def with(tags)
13
+ stack = current_stack
14
+ ActiveSupport::IsolatedExecutionState[KEY] = stack + [normalize(tags)]
15
+ yield
16
+ ensure
17
+ ActiveSupport::IsolatedExecutionState[KEY] = stack
18
+ end
19
+
20
+ def tags
21
+ config_tags.merge(scoped_tags)
22
+ end
23
+
24
+ def clear!
25
+ ActiveSupport::IsolatedExecutionState[KEY] = []
26
+ end
27
+
28
+ private
29
+
30
+ def config_tags
31
+ normalize(resolve_default_tags)
32
+ end
33
+
34
+ def resolve_default_tags
35
+ tags = LlmCostTracker.configuration.default_tags
36
+ tags.respond_to?(:call) ? tags.call : tags
37
+ end
38
+
39
+ def scoped_tags
40
+ current_stack.reduce({}) { |merged, tags| merged.merge(tags) }
41
+ end
42
+
43
+ def current_stack
44
+ ActiveSupport::IsolatedExecutionState[KEY] || []
45
+ end
46
+
47
+ def normalize(tags)
48
+ ValueHelpers.deep_dup(tags || {}).to_h
49
+ end
50
+ end
51
+ end
52
+ end
@@ -6,7 +6,7 @@ module LlmCostTracker
6
6
  class Tracker
7
7
  EVENT_NAME = "llm_request.llm_cost_tracker"
8
8
 
9
- USAGE_SOURCES = %i[response stream_final manual unknown].freeze
9
+ USAGE_SOURCES = %i[response stream_final sdk_response manual unknown].freeze
10
10
 
11
11
  class << self
12
12
  def enforce_budget!
@@ -19,6 +19,7 @@ module LlmCostTracker
19
19
  usage_source: nil, provider_response_id: nil, pricing_mode: nil, metadata: {})
20
20
  return unless LlmCostTracker.configuration.enabled
21
21
 
22
+ model = normalize_model(model)
22
23
  usage = usage_data(input_tokens, output_tokens, metadata, pricing_mode)
23
24
  cost_data = cost_for_usage(provider, model, usage)
24
25
 
@@ -68,6 +69,8 @@ module LlmCostTracker
68
69
  )
69
70
  end
70
71
 
72
+ def normalize_model(value) = value.to_s.strip.then { |model| model.empty? ? ParsedUsage::UNKNOWN_MODEL : model }
73
+
71
74
  def build_event(provider:, model:, usage:, cost_data:, metadata:, latency_ms:, stream:, usage_source:,
72
75
  provider_response_id:)
73
76
  Event.new(
@@ -81,7 +84,7 @@ module LlmCostTracker
81
84
  hidden_output_tokens: usage[:hidden_output_tokens],
82
85
  pricing_mode: usage[:pricing_mode],
83
86
  cost: cost_data,
84
- tags: LlmCostTracker.configuration.default_tags.merge(EventMetadata.tags(metadata)).freeze,
87
+ tags: LlmCostTracker::TagContext.tags.merge(EventMetadata.tags(metadata)).freeze,
85
88
  latency_ms: normalized_latency_ms(latency_ms),
86
89
  stream: stream ? true : false,
87
90
  usage_source: normalized_usage_source(usage_source),
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmCostTracker
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end