open_router_usage_tracker 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +252 -31
- data/app/models/concerns/open_router_usage_tracker/trackable.rb +21 -17
- data/app/models/open_router_usage_tracker/daily_summary.rb +15 -0
- data/app/models/open_router_usage_tracker/usage_log.rb +6 -7
- data/lib/generators/open_router_usage_tracker/install/templates/migration.rb +6 -5
- data/lib/generators/open_router_usage_tracker/summary_install/summary_install_generator.rb +19 -0
- data/lib/generators/open_router_usage_tracker/summary_install/templates/migration.rb +17 -0
- data/lib/open_router_usage_tracker/adapter/base.rb +65 -0
- data/lib/open_router_usage_tracker/parsers/anthropic.rb +21 -0
- data/lib/open_router_usage_tracker/parsers/google.rb +17 -0
- data/lib/open_router_usage_tracker/parsers/open_ai.rb +17 -0
- data/lib/open_router_usage_tracker/parsers/open_router.rb +17 -0
- data/lib/open_router_usage_tracker/parsers/x_ai.rb +17 -0
- data/lib/open_router_usage_tracker/version.rb +1 -1
- data/lib/open_router_usage_tracker.rb +2 -15
- metadata +14 -6
- data/lib/open_router_usage_tracker/railtie.rb +0 -4
- data/lib/tasks/open_router_usage_tracker_tasks.rake +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7253067ecf8149856b410df301488651e176e01e7479da73b2a028c97ee65b41
|
4
|
+
data.tar.gz: 8a971201f93f46f4c7b8865c0bf1ae758dbc4b2788e250a6db642cc1f6925f36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a411a62b04519c2a03cf4b81259378a6dce1739f218e432b7cf8656a58b16c511540804a72ac125a2da9606f35cd61b8dbf0db8de74470450acd611d2c8aa2e
|
7
|
+
data.tar.gz: dd5079f38075e63e85761172ac12f97c09f7ce8c19c975f699b93375236da078aeec2ef5885deeb5b2637236c996986701cadafbc1ff36bdc4beb8a9fff6ed4b
|
data/README.md
CHANGED
@@ -1,42 +1,80 @@
|
|
1
1
|
# OpenRouterUsageTracker
|
2
|
-
[](https://badge.fury.io/rb/open_router_usage_tracker)
|
3
2
|
[](https://github.com/mclpio/open_router_usage_tracker/actions)
|
3
|
+
[](https://badge.fury.io/rb/open_router_usage_tracker)
|
4
4
|
|
5
|
-
An effortless Rails engine to track API token usage and cost from
|
5
|
+
An effortless Rails engine to track API token usage and cost from multiple LLM providers, including OpenRouter, OpenAI, Google, Anthropic, and xAI. It enables easy rate-limiting and monitoring for your users.
|
6
6
|
|
7
7
|
## Motivation
|
8
|
-
Managing Large Language Model (LLM) API costs is crucial for any application that provides AI features to users. This gem provides simple, out-of-the-box tools to log every
|
8
|
+
Managing Large Language Model (LLM) API costs is crucial for any application that provides AI features to users. This gem provides simple, out-of-the-box tools to log every API call, associate it with a user, and query their usage over time. This allows you to easily implement spending caps, rate limits, or usage-based billing tiers across different providers.
|
9
|
+
|
10
|
+
## Quick Start
|
11
|
+
|
12
|
+
1. **Add the gem to your Gemfile:**
|
13
|
+
```ruby
|
14
|
+
gem 'open_router_usage_tracker', '~> 1.0.0'
|
15
|
+
```
|
16
|
+
|
17
|
+
2. **Install and run migrations:**
|
18
|
+
```bash
|
19
|
+
bundle install
|
20
|
+
bin/rails g open_router_usage_tracker:install
|
21
|
+
bin/rails g open_router_usage_tracker:summary_install
|
22
|
+
bin/rails db:migrate
|
23
|
+
```
|
24
|
+
|
25
|
+
3. **Add the concern to your User model:**
|
26
|
+
```ruby
|
27
|
+
# app/models/user.rb
|
28
|
+
class User < ApplicationRecord
|
29
|
+
include OpenRouterUsageTracker::Trackable
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
4. **Log a request:**
|
34
|
+
```ruby
|
35
|
+
# In your controller or service
|
36
|
+
OpenRouterUsageTracker.log(
|
37
|
+
response: a_parsed_json_response_from_your_llm_provider,
|
38
|
+
user: current_user,
|
39
|
+
provider: "open_ai"
|
40
|
+
)
|
41
|
+
```
|
9
42
|
|
10
43
|
## Installation
|
11
44
|
Add this line to your application's Gemfile:
|
12
45
|
|
13
46
|
```ruby
|
14
|
-
gem
|
47
|
+
gem 'open_router_usage_tracker', '~> 1.0.0'
|
15
48
|
```
|
16
49
|
|
17
50
|
And then execute:
|
18
51
|
```bash
|
19
|
-
|
52
|
+
bundle install
|
20
53
|
```
|
21
54
|
|
22
55
|
Or install it yourself as:
|
23
56
|
```bash
|
24
|
-
|
57
|
+
gem install open_router_usage_tracker
|
25
58
|
```
|
26
59
|
|
27
60
|
## Setup
|
28
61
|
|
29
|
-
1.
|
62
|
+
1. **Run the Install Generator**: This will create a migration file in your application to add the `open_router_usage_logs` table.
|
30
63
|
```bash
|
31
64
|
bin/rails g open_router_usage_tracker:install
|
32
65
|
```
|
33
66
|
|
34
|
-
|
67
|
+
2. **Run the Summary Table Generator (Required)**: To enable performant daily rate-limiting, generate the migration for the summary table.
|
68
|
+
```bash
|
69
|
+
bin/rails g open_router_usage_tracker:summary_install
|
70
|
+
```
|
71
|
+
|
72
|
+
3. **Run the Database Migrations**:
|
35
73
|
```bash
|
36
74
|
bin/rails db:migrate
|
37
75
|
```
|
38
76
|
|
39
|
-
|
77
|
+
4. **Include the `Trackable` Concern**: To add the usage tracking methods to your user model, include the concern. This works with any user-like model (e.g., `User`, `Account`).
|
40
78
|
```ruby
|
41
79
|
# app/models/user.rb
|
42
80
|
class User < ApplicationRecord
|
@@ -46,57 +84,240 @@ $ gem install open_router_usage_tracker
|
|
46
84
|
end
|
47
85
|
```
|
48
86
|
|
87
|
+
5. **(IMPORTANT) Configure Data Retention**: The `Trackable` concern intentionally does not set a `dependent` option on the `usage_logs` and `daily_summaries` associations. This is a critical design choice to prevent accidental data loss. You must decide what happens to a user's usage data when their account is deleted.
|
88
|
+
|
89
|
+
**To delete all usage data with the user:**
|
90
|
+
```ruby
|
91
|
+
# app/models/user.rb
|
92
|
+
class User < ApplicationRecord
|
93
|
+
include OpenRouterUsageTracker::Trackable
|
94
|
+
|
95
|
+
has_many :usage_logs, as: :user, class_name: "OpenRouterUsageTracker::UsageLog", dependent: :destroy
|
96
|
+
has_many :daily_summaries, as: :user, class_name: "OpenRouterUsageTracker::DailySummary", dependent: :destroy
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
**To keep all usage data:**
|
101
|
+
```ruby
|
102
|
+
# app/models/user.rb
|
103
|
+
class User < ApplicationRecord
|
104
|
+
include OpenRouterUsageTracker::Trackable
|
105
|
+
# No `dependent` option needed. The records will remain.
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
49
109
|
## Usage
|
50
110
|
Using the gem involves two parts: logging new requests and tracking existing usage.
|
51
111
|
|
52
112
|
### Logging a Request
|
53
|
-
In your application where you receive a successful response from
|
113
|
+
In your application where you receive a successful response from an LLM API, call the `log` method. It's designed to be simple and fail loudly if the data is invalid.
|
54
114
|
|
55
115
|
```ruby
|
56
|
-
# Assume `api_response` is the parsed JSON hash from
|
116
|
+
# Assume `api_response` is the parsed JSON hash from the provider
|
57
117
|
# and `current_user` is your authenticated user object.
|
58
118
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
119
|
+
# For OpenRouter (the default provider)
|
120
|
+
OpenRouterUsageTracker.log(response: open_router_response, user: current_user)
|
121
|
+
|
122
|
+
# For OpenAI
|
123
|
+
OpenRouterUsageTracker.log(response: openai_response, user: current_user, provider: "open_ai")
|
124
|
+
|
125
|
+
# For Google
|
126
|
+
OpenRouterUsageTracker.log(response: google_response, user: current_user, provider: "google")
|
127
|
+
|
128
|
+
# You can also prevent storing the raw API response for privacy or storage reasons.
|
129
|
+
OpenRouterUsageTracker.log(response: api_response, user: current_user, provider: "anthropic", store_raw_response: false)
|
66
130
|
```
|
67
131
|
|
68
|
-
###
|
69
|
-
The
|
132
|
+
### Supported Providers
|
133
|
+
The gem currently supports the following providers out-of-the-box:
|
134
|
+
- `open_router` (default)
|
135
|
+
- `open_ai`
|
136
|
+
- `google`
|
137
|
+
- `anthropic`
|
138
|
+
- `x_ai`
|
70
139
|
|
71
|
-
The
|
140
|
+
The gem will automatically parse the response from each provider to extract the relevant usage data. If a provider does not return a specific field (like `cost`), it will be saved as `0`.
|
72
141
|
|
73
|
-
|
142
|
+
#### Providing Your Own Cost
|
74
143
|
|
75
|
-
|
144
|
+
For providers that do not return cost information, you can calculate it yourself and add it to the response hash before logging. The gem will automatically detect and use a `cost` key inside the `usage` hash.
|
76
145
|
|
77
146
|
```ruby
|
78
|
-
#
|
147
|
+
# Calculate your own cost for an OpenAI call
|
148
|
+
openai_response["usage"]["cost"] = your_calculated_cost # e.g., 0.0123
|
79
149
|
|
80
|
-
|
81
|
-
|
150
|
+
# The log method will now use your provided cost
|
151
|
+
OpenRouterUsageTracker.log(response: openai_response, user: current_user, provider: "open_ai")
|
152
|
+
```
|
153
|
+
|
154
|
+
### Required Keys
|
155
|
+
For the `log` method to parse the API response correctly, the `model`, `id`, and `usage` (or similarly named) keys must be present in the response hash. Do not filter them out before logging.
|
156
|
+
|
157
|
+
### Daily Usage Tracking and Rate-Limiting
|
158
|
+
For high-performance rate-limiting, the gem provides methods to query the daily summary table. This avoids slow `SUM` queries on the main log table.
|
159
|
+
|
160
|
+
The `daily_usage_summary_for(day:, provider:, model:)` method provides a near-instantaneous check of a user's usage for a specific model on a given day.
|
161
|
+
|
162
|
+
**Example: Implementing a daily token limit for a specific model**
|
163
|
+
|
164
|
+
Imagine you want to prevent users from using more than 100,000 tokens per day for a specific model.
|
82
165
|
|
83
|
-
|
84
|
-
|
166
|
+
```ruby
|
167
|
+
# somewhere in a controller or before_action
|
168
|
+
def enforce_daily_limit
|
169
|
+
# Be sure to handle timezones correctly for your users.
|
170
|
+
today = Time.zone.today # => Wed, 30 Jul 2025
|
171
|
+
|
172
|
+
# This check is extremely fast as it queries the small summary table.
|
173
|
+
summary = current_user.daily_usage_summary_for(
|
174
|
+
day: today,
|
175
|
+
provider: "open_ai",
|
176
|
+
model: "gpt-4o"
|
177
|
+
)
|
178
|
+
|
179
|
+
if summary && summary.total_tokens > 100_000
|
180
|
+
render json: { error: "You have exceeded your daily token limit for this model." }, status: :too_many_requests
|
85
181
|
return
|
86
182
|
end
|
87
183
|
end
|
88
184
|
```
|
89
185
|
|
90
|
-
|
186
|
+
### Querying Costs
|
187
|
+
|
188
|
+
The `Trackable` concern also provides a powerful method to calculate total costs over a date range by querying the performant `daily_summaries` table.
|
189
|
+
|
190
|
+
The `total_cost_in_range(range, provider:, model: nil)` method allows you to easily calculate costs for a specific provider or a specific model over any period.
|
191
|
+
|
192
|
+
**Example: Analyzing costs over a period**
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
# In a reporting or analytics service
|
196
|
+
def generate_cost_report(user)
|
197
|
+
# Get the date range for the last 30 days.
|
198
|
+
last_30_days = (30.days.ago.to_date)..Date.current
|
199
|
+
|
200
|
+
# Calculate the total cost for all OpenRouter models in the last 30 days.
|
201
|
+
# This is meaningful because OpenRouter provides direct cost data.
|
202
|
+
open_router_cost = user.total_cost_in_range(last_30_days, provider: "open_router")
|
203
|
+
|
204
|
+
# For providers that don't return cost, this will correctly be 0.
|
205
|
+
openai_cost = user.total_cost_in_range(last_30_days, provider: "open_ai")
|
206
|
+
|
207
|
+
puts "Total OpenRouter cost in the last 30 days: $#{open_router_cost.round(2)}"
|
208
|
+
puts "Total OpenAI cost in the last 30 days: $#{openai_cost.round(2)}" # Will be $0.00
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
Note that different LLM models have different costs, so it often makes sense to enforce limits on a per-provider and per-model basis. You can always run your own queries on the `OpenRouterUsageTracker::DailySummary` table for more complex logic.
|
91
213
|
|
92
214
|
## Contributing
|
93
|
-
Open an issue first.
|
215
|
+
Open an issue first to discuss.
|
94
216
|
|
95
217
|
1. Fork the repository.
|
96
218
|
1. Create your feature branch (git checkout -b my-new-feature).
|
97
219
|
1. Commit your changes (git commit -am 'Add some feature').
|
98
220
|
1. Push to the branch (git push origin my-new-feature).
|
99
|
-
1. Create a new Pull Request
|
221
|
+
1. Create a new Pull Request using your fork
|
100
222
|
|
101
223
|
## License
|
102
224
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
225
|
+
|
226
|
+
## Architecture Diagrams
|
227
|
+
|
228
|
+
### Gem Components
|
229
|
+
|
230
|
+
```mermaid
|
231
|
+
graph TD
|
232
|
+
subgraph A[Rails Host App]
|
233
|
+
U[User Model]
|
234
|
+
C[Controller/Service]
|
235
|
+
end
|
236
|
+
|
237
|
+
subgraph B[OpenRouterUsageTracker Gem]
|
238
|
+
T{Trackable Concern}
|
239
|
+
L[Log Method]
|
240
|
+
AD(Adapter)
|
241
|
+
P[Parsers]
|
242
|
+
UL[UsageLog Model]
|
243
|
+
DS[DailySummary Model]
|
244
|
+
end
|
245
|
+
|
246
|
+
subgraph D[Database]
|
247
|
+
T1(open_router_usage_logs Table)
|
248
|
+
T2(open_router_daily_summaries Table)
|
249
|
+
end
|
250
|
+
|
251
|
+
C -- Calls --> L
|
252
|
+
U -- Includes --> T
|
253
|
+
T -- has_many --> UL
|
254
|
+
T -- has_many --> DS
|
255
|
+
|
256
|
+
L -- Uses --> AD
|
257
|
+
AD -- Selects --> P
|
258
|
+
P -- Creates --> UL
|
259
|
+
AD -- Updates --> DS
|
260
|
+
|
261
|
+
UL -- Persisted in --> T1
|
262
|
+
DS -- Persisted in --> T2
|
263
|
+
|
264
|
+
style B fill:#f9f,stroke:#333,stroke-width:2px
|
265
|
+
style A fill:#ccf,stroke:#333,stroke-width:2px
|
266
|
+
```
|
267
|
+
|
268
|
+
### Logging Sequence
|
269
|
+
|
270
|
+
```mermaid
|
271
|
+
sequenceDiagram
|
272
|
+
participant App as Rails Host App
|
273
|
+
participant Gem as OpenRouterUsageTracker
|
274
|
+
participant DB as Database
|
275
|
+
|
276
|
+
App->>+Gem: OpenRouterUsageTracker.log(response: ..., user: ..., provider: 'open_ai')
|
277
|
+
Gem->>Gem: Select 'OpenAi' Parser
|
278
|
+
Gem->>+DB: BEGIN TRANSACTION
|
279
|
+
Gem->>DB: CREATE UsageLog record
|
280
|
+
Gem->>DB: FIND OR INITIALIZE DailySummary for today
|
281
|
+
Gem->>DB: INCREMENT and SAVE DailySummary record
|
282
|
+
DB-->>-Gem: COMMIT TRANSACTION
|
283
|
+
Gem-->>-App: return UsageLog object
|
284
|
+
```
|
285
|
+
|
286
|
+
### Database Schema (ERD)
|
287
|
+
|
288
|
+
```mermaid
|
289
|
+
erDiagram
|
290
|
+
USER ||--o{ USAGE_LOG : "has_many"
|
291
|
+
USER ||--o{ DAILY_SUMMARY : "has_many"
|
292
|
+
|
293
|
+
USER {
|
294
|
+
string name
|
295
|
+
string email
|
296
|
+
}
|
297
|
+
|
298
|
+
USAGE_LOG {
|
299
|
+
string user_type
|
300
|
+
bigint user_id
|
301
|
+
string provider
|
302
|
+
string model
|
303
|
+
string request_id
|
304
|
+
integer prompt_tokens
|
305
|
+
integer completion_tokens
|
306
|
+
integer total_tokens
|
307
|
+
decimal cost
|
308
|
+
json raw_usage_response
|
309
|
+
}
|
310
|
+
|
311
|
+
DAILY_SUMMARY {
|
312
|
+
string user_type
|
313
|
+
bigint user_id
|
314
|
+
date day
|
315
|
+
string provider
|
316
|
+
string model
|
317
|
+
integer total_tokens
|
318
|
+
integer prompt_tokens
|
319
|
+
integer completion_tokens
|
320
|
+
decimal cost
|
321
|
+
}
|
322
|
+
|
323
|
+
```
|
@@ -5,28 +5,32 @@ module OpenRouterUsageTracker
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
has_many :usage_logs, as: :user, class_name: "OpenRouterUsageTracker::UsageLog"
|
8
|
+
has_many :usage_logs, as: :user, class_name: "OpenRouterUsageTracker::UsageLog"
|
9
|
+
has_many :daily_summaries, as: :user, class_name: "OpenRouterUsageTracker::DailySummary"
|
9
10
|
end
|
10
11
|
|
11
|
-
#
|
12
|
+
# Finds a specific daily summary for a given day, provider, and model.
|
13
|
+
# This is the primary, high-performance method for usage checks.
|
12
14
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
# Use .to_i and .to_d to handle cases where there are no logs (sum returns nil)
|
21
|
-
total_tokens = logs_in_range.sum(:total_tokens).to_i
|
22
|
-
total_cost = logs_in_range.sum(:cost).to_d
|
23
|
-
|
24
|
-
{ tokens: total_tokens, cost: total_cost }
|
15
|
+
# @param day [Date] The date to check. Pass `Time.zone.today` to be timezone-aware.
|
16
|
+
# @param provider [String] The provider name (e.g., 'open_router').
|
17
|
+
# @param model [String] The model name (e.g., 'openai/gpt-4o').
|
18
|
+
# @return [OpenRouterUsageTracker::DailySummary, nil]
|
19
|
+
def daily_usage_summary_for(day:, provider:, model:)
|
20
|
+
daily_summaries.find_by(day: day, provider: provider, model: model)
|
25
21
|
end
|
26
22
|
|
27
|
-
#
|
28
|
-
|
29
|
-
|
23
|
+
# Calculates the total cost from the daily summaries within a given date range.
|
24
|
+
# It can be filtered by provider and, optionally, by model.
|
25
|
+
#
|
26
|
+
# @param range [Range<Date>] The date range to query (e.g., 1.month.ago.to_date..Date.current).
|
27
|
+
# @param provider [String] The provider name (e.g., 'open_ai').
|
28
|
+
# @param model [String, nil] The optional model name.
|
29
|
+
# @return [BigDecimal] The total cost.
|
30
|
+
def total_cost_in_range(range, provider:, model: nil)
|
31
|
+
summaries = daily_summaries.where(day: range, provider: provider)
|
32
|
+
summaries = summaries.where(model: model) if model
|
33
|
+
summaries.sum(:cost)
|
30
34
|
end
|
31
35
|
end
|
32
36
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module OpenRouterUsageTracker
|
2
|
+
class DailySummary < ApplicationRecord
|
3
|
+
self.table_name = "open_router_daily_summaries"
|
4
|
+
|
5
|
+
belongs_to :user, polymorphic: true
|
6
|
+
|
7
|
+
validates :day, presence: true
|
8
|
+
validates :total_tokens, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
9
|
+
validates :prompt_tokens, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
10
|
+
validates :completion_tokens, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
11
|
+
validates :cost, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
12
|
+
validates :model, presence: true
|
13
|
+
validates :provider, presence: true
|
14
|
+
end
|
15
|
+
end
|
@@ -6,12 +6,11 @@ module OpenRouterUsageTracker
|
|
6
6
|
belongs_to :user, polymorphic: true
|
7
7
|
|
8
8
|
validates :model, presence: true
|
9
|
-
validates :prompt_tokens,
|
10
|
-
validates :completion_tokens,
|
11
|
-
validates :total_tokens,
|
12
|
-
validates :cost,
|
13
|
-
validates :
|
14
|
-
|
15
|
-
validates :request_id, presence: true, uniqueness: true
|
9
|
+
validates :prompt_tokens, numericality: { greater_than_or_equal_to: 0 }
|
10
|
+
validates :completion_tokens, numericality: { greater_than_or_equal_to: 0 }
|
11
|
+
validates :total_tokens, numericality: { greater_than_or_equal_to: 0 }
|
12
|
+
validates :cost, numericality: { greater_than_or_equal_to: 0 }
|
13
|
+
validates :provider, presence: true
|
14
|
+
validates :request_id, presence: true, uniqueness: { scope: :provider }
|
16
15
|
end
|
17
16
|
end
|
@@ -2,12 +2,13 @@ class CreateOpenRouterUsageLogs < ActiveRecord::Migration[8.0]
|
|
2
2
|
def change
|
3
3
|
create_table :open_router_usage_logs do |t|
|
4
4
|
t.string :model, null: false
|
5
|
-
t.integer :prompt_tokens, null: false
|
6
|
-
t.integer :completion_tokens, null: false
|
7
|
-
t.integer :total_tokens, null: false
|
8
|
-
t.decimal :cost, precision: 10, scale: 5, null: false
|
5
|
+
t.integer :prompt_tokens, null: false, default: 0
|
6
|
+
t.integer :completion_tokens, null: false, default: 0
|
7
|
+
t.integer :total_tokens, null: false, default: 0
|
8
|
+
t.decimal :cost, precision: 10, scale: 5, null: false, default: 0.0
|
9
9
|
t.references :user, null: false, polymorphic: true
|
10
10
|
t.string :request_id, null: false
|
11
|
+
t.string :provider, null: false, default: "open_router"
|
11
12
|
|
12
13
|
# Storing the raw API response is recommended for auditing and future-proofing.
|
13
14
|
# If you are using PostgreSQL, you can change `t.json` to `t.jsonb` for
|
@@ -17,6 +18,6 @@ class CreateOpenRouterUsageLogs < ActiveRecord::Migration[8.0]
|
|
17
18
|
t.timestamps
|
18
19
|
end
|
19
20
|
|
20
|
-
add_index :open_router_usage_logs, :request_id, unique: true
|
21
|
+
add_index :open_router_usage_logs, [ :provider, :request_id ], unique: true
|
21
22
|
end
|
22
23
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "rails/generators/base"
|
2
|
+
|
3
|
+
module OpenRouterUsageTracker
|
4
|
+
module Generators
|
5
|
+
class SummaryInstallGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def self.next_migration_number(dir)
|
11
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_template "migration.rb", "db/migrate/create_open_router_daily_summaries.rb"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateOpenRouterDailySummaries < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :open_router_daily_summaries do |t|
|
4
|
+
t.references :user, null: false, polymorphic: true
|
5
|
+
t.date :day, null: false
|
6
|
+
t.integer :total_tokens, null: false, default: 0
|
7
|
+
t.integer :prompt_tokens, null: false, default: 0
|
8
|
+
t.integer :completion_tokens, null: false, default: 0
|
9
|
+
t.decimal :cost, precision: 10, scale: 5, null: false, default: 0.0
|
10
|
+
t.string :provider, null: false, default: "open_router"
|
11
|
+
t.string :model, null: false
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :open_router_daily_summaries, [ :user_type, :user_id, :day, :provider, :model ], unique: true, name: "index_daily_summaries_on_user_and_day_and_provider_and_model"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module OpenRouterUsageTracker
|
2
|
+
require "open_router_usage_tracker/parsers/open_ai"
|
3
|
+
require "open_router_usage_tracker/parsers/open_router"
|
4
|
+
require "open_router_usage_tracker/parsers/google"
|
5
|
+
require "open_router_usage_tracker/parsers/anthropic"
|
6
|
+
require "open_router_usage_tracker/parsers/x_ai"
|
7
|
+
|
8
|
+
module Adapter
|
9
|
+
module Base
|
10
|
+
SUPPORTED_PROVIDERS = [ "open_ai", "open_router", "google", "anthropic", "x_ai" ].freeze
|
11
|
+
|
12
|
+
# Logs an API usage event, creating a UsageLog and updating the DailySummary.
|
13
|
+
# This is the primary method for recording usage data.
|
14
|
+
#
|
15
|
+
# @param response [Hash] The raw response hash from the API provider.
|
16
|
+
# @param user [ApplicationRecord] The user object (e.g., User, Account) associated with the API call.
|
17
|
+
# @param provider [String] The name of the API provider (e.g., 'open_router', 'open_ai').
|
18
|
+
# Defaults to 'open_router'.
|
19
|
+
# @param store_raw_response [Boolean] If false, an empty hash will be stored in the
|
20
|
+
# raw_usage_response column. Defaults to true.
|
21
|
+
#
|
22
|
+
# @return [OpenRouterUsageTracker::UsageLog] The newly created usage log record.
|
23
|
+
#
|
24
|
+
# @example Log a call and store the raw response
|
25
|
+
# OpenRouterUsageTracker.log(response: api_response, user: current_user)
|
26
|
+
#
|
27
|
+
# @example Log a call without storing the raw response
|
28
|
+
# OpenRouterUsageTracker.log(response: api_response, user: current_user, store_raw_response: false)
|
29
|
+
#
|
30
|
+
def log(response:, user:, provider: "open_router", store_raw_response: true)
|
31
|
+
unless SUPPORTED_PROVIDERS.include?(provider)
|
32
|
+
raise ArgumentError.new("Unsupported provider: #{provider}. Supported providers are: #{SUPPORTED_PROVIDERS.join(', ')}")
|
33
|
+
end
|
34
|
+
|
35
|
+
parser_class = "OpenRouterUsageTracker::Parsers::#{provider.camelize}".constantize
|
36
|
+
attributes = parser_class.parse(response)
|
37
|
+
attributes[:user] = user
|
38
|
+
attributes[:raw_usage_response] = {} unless store_raw_response
|
39
|
+
attributes[:provider] = provider
|
40
|
+
|
41
|
+
ApplicationRecord.transaction do
|
42
|
+
usage_log = OpenRouterUsageTracker::UsageLog.create!(attributes)
|
43
|
+
update_daily_summary(usage_log)
|
44
|
+
usage_log
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def update_daily_summary(usage_log)
|
51
|
+
summary = OpenRouterUsageTracker::DailySummary.find_or_initialize_by(
|
52
|
+
user: usage_log.user,
|
53
|
+
day: Date.current,
|
54
|
+
provider: usage_log.provider,
|
55
|
+
model: usage_log.model
|
56
|
+
)
|
57
|
+
summary.total_tokens += usage_log.total_tokens
|
58
|
+
summary.cost += usage_log.cost
|
59
|
+
summary.prompt_tokens += usage_log.prompt_tokens
|
60
|
+
summary.completion_tokens += usage_log.completion_tokens
|
61
|
+
summary.save!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module OpenRouterUsageTracker
|
2
|
+
module Parsers
|
3
|
+
class Anthropic
|
4
|
+
def self.parse(response)
|
5
|
+
input_tokens = response.dig("usage", "input_tokens").to_i
|
6
|
+
output_tokens = response.dig("usage", "output_tokens").to_i
|
7
|
+
total_tokens = input_tokens + output_tokens
|
8
|
+
|
9
|
+
{
|
10
|
+
model: response.dig("model"),
|
11
|
+
prompt_tokens: input_tokens,
|
12
|
+
completion_tokens: output_tokens,
|
13
|
+
total_tokens: total_tokens,
|
14
|
+
cost: response.dig("usage", "cost").to_f,
|
15
|
+
request_id: response["id"],
|
16
|
+
raw_usage_response: response
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module OpenRouterUsageTracker
|
2
|
+
module Parsers
|
3
|
+
class Google
|
4
|
+
def self.parse(response)
|
5
|
+
{
|
6
|
+
model: response.dig("model"),
|
7
|
+
prompt_tokens: response.dig("usageMetadata", "promptTokenCount").to_i,
|
8
|
+
completion_tokens: response.dig("usageMetadata", "candidatesTokenCount").to_i,
|
9
|
+
total_tokens: response.dig("usageMetadata", "totalTokenCount").to_i,
|
10
|
+
cost: response.dig("usage", "cost").to_f,
|
11
|
+
request_id: response["responseId"],
|
12
|
+
raw_usage_response: response
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module OpenRouterUsageTracker
|
2
|
+
module Parsers
|
3
|
+
class OpenAi
|
4
|
+
def self.parse(response)
|
5
|
+
{
|
6
|
+
model: response.dig("model"),
|
7
|
+
prompt_tokens: response.dig("usage", "input_tokens").to_i,
|
8
|
+
completion_tokens: response.dig("usage", "output_tokens").to_i,
|
9
|
+
total_tokens: response.dig("usage", "total_tokens").to_i,
|
10
|
+
cost: response.dig("usage", "cost").to_f,
|
11
|
+
request_id: response["id"],
|
12
|
+
raw_usage_response: response
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module OpenRouterUsageTracker
|
2
|
+
module Parsers
|
3
|
+
class OpenRouter
|
4
|
+
def self.parse(response)
|
5
|
+
{
|
6
|
+
model: response.dig("model"),
|
7
|
+
prompt_tokens: response.dig("usage", "prompt_tokens").to_i,
|
8
|
+
completion_tokens: response.dig("usage", "completion_tokens").to_i,
|
9
|
+
total_tokens: response.dig("usage", "total_tokens").to_i,
|
10
|
+
cost: response.dig("usage", "cost").to_f,
|
11
|
+
request_id: response.dig("id"),
|
12
|
+
raw_usage_response: response
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module OpenRouterUsageTracker
|
2
|
+
module Parsers
|
3
|
+
class XAi
|
4
|
+
def self.parse(response)
|
5
|
+
{
|
6
|
+
model: response.dig("model"),
|
7
|
+
prompt_tokens: response.dig("usage", "prompt_tokens").to_i,
|
8
|
+
completion_tokens: response.dig("usage", "completion_tokens").to_i,
|
9
|
+
total_tokens: response.dig("usage", "total_tokens").to_i,
|
10
|
+
cost: response.dig("usage", "cost").to_f,
|
11
|
+
request_id: response["id"],
|
12
|
+
raw_usage_response: response
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,24 +1,11 @@
|
|
1
1
|
require "open_router_usage_tracker/version"
|
2
|
-
require "open_router_usage_tracker/railtie"
|
3
2
|
require "open_router_usage_tracker/engine"
|
3
|
+
require "open_router_usage_tracker/adapter/base"
|
4
4
|
|
5
5
|
module OpenRouterUsageTracker
|
6
6
|
class << self
|
7
7
|
attr_writer :configuration
|
8
8
|
|
9
|
-
|
10
|
-
attributes = {
|
11
|
-
model: response["model"],
|
12
|
-
prompt_tokens: response.dig("usage", "prompt_tokens"),
|
13
|
-
completion_tokens: response.dig("usage", "completion_tokens"),
|
14
|
-
total_tokens: response.dig("usage", "total_tokens"),
|
15
|
-
cost: response.dig("usage", "cost"),
|
16
|
-
request_id: response["id"],
|
17
|
-
raw_usage_response: response,
|
18
|
-
user: user
|
19
|
-
}
|
20
|
-
|
21
|
-
OpenRouterUsageTracker::UsageLog.create!(attributes)
|
22
|
-
end
|
9
|
+
include Adapter::Base
|
23
10
|
end
|
24
11
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: open_router_usage_tracker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- MclPio
|
@@ -23,8 +23,8 @@ dependencies:
|
|
23
23
|
- - ">="
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: 8.0.2
|
26
|
-
description: A simple Rails engine to log
|
27
|
-
for tracking user consumption over time, enabling easy rate-limiting.
|
26
|
+
description: A simple Rails engine to log API usage from multiple LLM providers and
|
27
|
+
provide methods for tracking user consumption over time, enabling easy rate-limiting.
|
28
28
|
email:
|
29
29
|
- mclpious@gmail.com
|
30
30
|
executables: []
|
@@ -35,14 +35,21 @@ files:
|
|
35
35
|
- README.md
|
36
36
|
- Rakefile
|
37
37
|
- app/models/concerns/open_router_usage_tracker/trackable.rb
|
38
|
+
- app/models/open_router_usage_tracker/daily_summary.rb
|
38
39
|
- app/models/open_router_usage_tracker/usage_log.rb
|
39
40
|
- lib/generators/open_router_usage_tracker/install/install_generator.rb
|
40
41
|
- lib/generators/open_router_usage_tracker/install/templates/migration.rb
|
42
|
+
- lib/generators/open_router_usage_tracker/summary_install/summary_install_generator.rb
|
43
|
+
- lib/generators/open_router_usage_tracker/summary_install/templates/migration.rb
|
41
44
|
- lib/open_router_usage_tracker.rb
|
45
|
+
- lib/open_router_usage_tracker/adapter/base.rb
|
42
46
|
- lib/open_router_usage_tracker/engine.rb
|
43
|
-
- lib/open_router_usage_tracker/
|
47
|
+
- lib/open_router_usage_tracker/parsers/anthropic.rb
|
48
|
+
- lib/open_router_usage_tracker/parsers/google.rb
|
49
|
+
- lib/open_router_usage_tracker/parsers/open_ai.rb
|
50
|
+
- lib/open_router_usage_tracker/parsers/open_router.rb
|
51
|
+
- lib/open_router_usage_tracker/parsers/x_ai.rb
|
44
52
|
- lib/open_router_usage_tracker/version.rb
|
45
|
-
- lib/tasks/open_router_usage_tracker_tasks.rake
|
46
53
|
homepage: https://github.com/MclPio/open_router_usage_tracker
|
47
54
|
licenses:
|
48
55
|
- MIT
|
@@ -66,5 +73,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
73
|
requirements: []
|
67
74
|
rubygems_version: 3.6.9
|
68
75
|
specification_version: 4
|
69
|
-
summary: Track API token usage and cost from OpenRouter
|
76
|
+
summary: Track API token usage and cost from multiple LLM providers like OpenRouter,
|
77
|
+
OpenAI, Google, and more.
|
70
78
|
test_files: []
|