open_router_usage_tracker 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f11a0fd8aa29a664ab72f45aa1516974dde22505c9e8f060ca8a051f0558de92
4
- data.tar.gz: ad7a682ba71c648dba5dd12d6f47df0cd3ec9fc2590579e8a14bae4e1dd71e5c
3
+ metadata.gz: 7253067ecf8149856b410df301488651e176e01e7479da73b2a028c97ee65b41
4
+ data.tar.gz: 8a971201f93f46f4c7b8865c0bf1ae758dbc4b2788e250a6db642cc1f6925f36
5
5
  SHA512:
6
- metadata.gz: '002058380aa56c5dfde8490cf908f019da6cb752774f6eef197c4c5746b4b5ce99fa45e76eefc0e596a5c19a5f3eee3ea811b04cbf85fb095a024db8ff08a365'
7
- data.tar.gz: 9dc836d59bbda52b9974f98aae37da47eafbbe1581e8f260ef5bbe45de5ccb885148865cf0d787c01aa4ae84a0425d6f043aa8d3801a8e95058113c854c5fa64
6
+ metadata.gz: 9a411a62b04519c2a03cf4b81259378a6dce1739f218e432b7cf8656a58b16c511540804a72ac125a2da9606f35cd61b8dbf0db8de74470450acd611d2c8aa2e
7
+ data.tar.gz: dd5079f38075e63e85761172ac12f97c09f7ce8c19c975f699b93375236da078aeec2ef5885deeb5b2637236c996986701cadafbc1ff36bdc4beb8a9fff6ed4b
data/README.md CHANGED
@@ -2,21 +2,54 @@
2
2
  [![Build Status](https://github.com/mclpio/open_router_usage_tracker/actions/workflows/ci.yml/badge.svg)](https://github.com/mclpio/open_router_usage_tracker/actions)
3
3
  [![Gem Version](https://badge.fury.io/rb/open_router_usage_tracker.svg)](https://badge.fury.io/rb/open_router_usage_tracker)
4
4
 
5
- An effortless Rails engine to track API token usage and cost from [OpenRouter](https://openrouter.ai/), enabling easy rate-limiting and monitoring for your users.
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 OpenRouter 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.
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 "open_router_usage_tracker"
47
+ gem 'open_router_usage_tracker', '~> 1.0.0'
15
48
  ```
16
49
 
17
50
  And then execute:
18
51
  ```bash
19
- bundle
52
+ bundle install
20
53
  ```
21
54
 
22
55
  Or install it yourself as:
@@ -26,22 +59,22 @@ gem install open_router_usage_tracker
26
59
 
27
60
  ## Setup
28
61
 
29
- 1. **Run the Install Generator**: This will create a migration file in your application to add the `open_router_usage_logs` table.
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
- 2. **Run the Summary Table Generator (New in v0.2.0)**: To enable performant daily rate-limiting, generate the migration for the summary table.
67
+ 2. **Run the Summary Table Generator (Required)**: To enable performant daily rate-limiting, generate the migration for the summary table.
35
68
  ```bash
36
69
  bin/rails g open_router_usage_tracker:summary_install
37
70
  ```
38
71
 
39
- 3. **Run the Database Migrations**:
72
+ 3. **Run the Database Migrations**:
40
73
  ```bash
41
74
  bin/rails db:migrate
42
75
  ```
43
76
 
44
- 4. **Include the `Trackable` Concern**: To add the usage tracking methods (`usage_in_period`, etc.) to your user model, include the concern. This works with any user-like model (e.g., `User`, `Account`).
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`).
45
78
  ```ruby
46
79
  # app/models/user.rb
47
80
  class User < ApplicationRecord
@@ -51,84 +84,240 @@ gem install open_router_usage_tracker
51
84
  end
52
85
  ```
53
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
+
54
109
  ## Usage
55
110
  Using the gem involves two parts: logging new requests and tracking existing usage.
56
111
 
57
112
  ### Logging a Request
58
- In your application where you receive a successful response from the OpenRouter API, call the `log` method. It's designed to be simple and fail loudly if the data is invalid.
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.
59
114
 
60
115
  ```ruby
61
- # Assume `api_response` is the parsed JSON hash from OpenRouter
116
+ # Assume `api_response` is the parsed JSON hash from the provider
62
117
  # and `current_user` is your authenticated user object.
63
118
 
64
- begin
65
- OpenRouterUsageTracker.log(response: api_response, user: current_user)
66
- rescue ActiveRecord::RecordInvalid => e
67
- # This can happen if the response hash is missing required keys
68
- # (e.g., 'id', 'model', 'usage').
69
- logger.error "Failed to log OpenRouter usage: #{e.message}"
70
- end
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)
130
+ ```
131
+
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`
139
+
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`.
141
+
142
+ #### Providing Your Own Cost
143
+
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.
145
+
146
+ ```ruby
147
+ # Calculate your own cost for an OpenAI call
148
+ openai_response["usage"]["cost"] = your_calculated_cost # e.g., 0.0123
149
+
150
+ # The log method will now use your provided cost
151
+ OpenRouterUsageTracker.log(response: openai_response, user: current_user, provider: "open_ai")
71
152
  ```
72
153
 
73
- ### Daily Usage Tracking and Rate-Limiting (Recommended)
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.
74
156
 
75
- For high-performance rate-limiting, the gem provides helpers that query a daily summary table. This avoids slow `SUM` queries on the main log table.
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.
76
159
 
77
- The primary method is `cost_exceeded?(limit:)`, which provides a near-instantaneous check against a user's daily spending.
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.
78
161
 
79
- **Example: Implementing a daily cost limit**
162
+ **Example: Implementing a daily token limit for a specific model**
80
163
 
81
- Imagine you want to prevent users from spending more than $1.00 per day.
164
+ Imagine you want to prevent users from using more than 100,000 tokens per day for a specific model.
82
165
 
83
166
  ```ruby
84
167
  # somewhere in a controller or before_action
85
-
86
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
+
87
172
  # This check is extremely fast as it queries the small summary table.
88
- if current_user.cost_exceeded?(limit: 1.00)
89
- render json: { error: "You have exceeded your daily spending limit." }, status: :too_many_requests
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
90
181
  return
91
182
  end
92
183
  end
93
184
  ```
94
185
 
95
- You can also retrieve the full summary for the current day (UTC):
186
+ ### Querying Costs
96
187
 
97
- ```ruby
98
- summary = current_user.usage_today
99
- # => <OpenRouterUsageTracker::DailySummary id: 1, user_id: 1, day: "2025-06-28", total_tokens: 1500, cost: 0.025, ...>
188
+ The `Trackable` concern also provides a powerful method to calculate total costs over a date range by querying the performant `daily_summaries` table.
100
189
 
101
- summary.cost
102
- # => 0.025
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.
103
191
 
104
- summary.total_tokens
105
- # => 1500
106
- ```
192
+ **Example: Analyzing costs over a period**
107
193
 
108
- ### Historical Usage Tracking
109
-
110
- The `Trackable` concern also adds methods for querying historical usage from the main log table.
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
111
199
 
112
- The main method is `usage_in_period(range)`, which returns a hash containing the total tokens and cost for a given time range.
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")
113
203
 
114
- **Example: Checking usage for the current month**
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")
115
206
 
116
- ```ruby
117
- range = Time.current.beginning_of_month..Time.current
118
- usage = current_user.usage_in_period(range)
119
- # => { tokens: 50000, cost: 1.25 }
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
120
210
  ```
121
211
 
122
- **DEPRECATED for rate-limiting**: The `usage_in_last_24_hours` method is still available but is **not recommended** for implementing rate limits due to its performance implications. Use `cost_exceeded?` instead for a more robust and scalable solution.
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.
123
213
 
124
214
  ## Contributing
125
- Open an issue first.
215
+ Open an issue first to discuss.
126
216
 
127
217
  1. Fork the repository.
128
218
  1. Create your feature branch (git checkout -b my-new-feature).
129
219
  1. Commit your changes (git commit -am 'Add some feature').
130
220
  1. Push to the branch (git push origin my-new-feature).
131
- 1. Create a new Pull Request.
221
+ 1. Create a new Pull Request using your fork
132
222
 
133
223
  ## License
134
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,42 +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", dependent: :destroy
9
- has_many :daily_summaries, as: :user, class_name: "OpenRouterUsageTracker::DailySummary", dependent: :destroy
8
+ has_many :usage_logs, as: :user, class_name: "OpenRouterUsageTracker::UsageLog"
9
+ has_many :daily_summaries, as: :user, class_name: "OpenRouterUsageTracker::DailySummary"
10
10
  end
11
11
 
12
- def usage_today
13
- daily_summaries.find_by(day: Date.current)
14
- end
15
-
16
- def cost_exceeded?(limit:, period: :daily)
17
- case period
18
- when :daily
19
- usage_today&.cost.to_d > limit
20
- else
21
- raise ArgumentError, "Unsupported period: #{period}"
22
- end
23
- end
24
-
25
- # A flexible method to query usage within any time period.
12
+ # Finds a specific daily summary for a given day, provider, and model.
13
+ # This is the primary, high-performance method for usage checks.
26
14
  #
27
- # Example:
28
- # user.usage_in_period(24.hours.ago..Time.current)
29
- # => { tokens: 1500, cost: 0.025 }
30
- #
31
- def usage_in_period(range)
32
- logs_in_range = self.usage_logs.where(created_at: range)
33
-
34
- # Use .to_i and .to_d to handle cases where there are no logs (sum returns nil)
35
- total_tokens = logs_in_range.sum(:total_tokens).to_i
36
- total_cost = logs_in_range.sum(:cost).to_d
37
-
38
- { 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)
39
21
  end
40
22
 
41
- # A convenience method for checking the last 24 hours.
42
- def usage_in_last_24_hours
43
- usage_in_period(24.hours.ago..Time.current)
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)
44
34
  end
45
35
  end
46
36
  end
@@ -6,6 +6,10 @@ module OpenRouterUsageTracker
6
6
 
7
7
  validates :day, presence: true
8
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 }
9
11
  validates :cost, presence: true, numericality: { greater_than_or_equal_to: 0 }
12
+ validates :model, presence: true
13
+ validates :provider, presence: true
10
14
  end
11
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, presence: true
10
- validates :completion_tokens, presence: true
11
- validates :total_tokens, presence: true
12
- validates :cost, presence: true
13
- validates :raw_usage_response, presence: true
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
@@ -4,10 +4,14 @@ class CreateOpenRouterDailySummaries < ActiveRecord::Migration[8.0]
4
4
  t.references :user, null: false, polymorphic: true
5
5
  t.date :day, null: false
6
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
7
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
8
12
  t.timestamps
9
13
  end
10
14
 
11
- add_index :open_router_daily_summaries, [ :user_type, :user_id, :day ], unique: true, name: "index_daily_summaries_on_user_and_day"
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"
12
16
  end
13
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,3 +1,3 @@
1
1
  module OpenRouterUsageTracker
2
- VERSION = "0.2.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,43 +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
- def log(response:, user:)
10
- ApplicationRecord.transaction do
11
- usage_log = create_usage_log(response, user)
12
- update_daily_summary(usage_log)
13
- usage_log
14
- end
15
- end
16
-
17
- private
18
-
19
- def create_usage_log(response, user)
20
- attributes = {
21
- model: response["model"],
22
- prompt_tokens: response.dig("usage", "prompt_tokens"),
23
- completion_tokens: response.dig("usage", "completion_tokens"),
24
- total_tokens: response.dig("usage", "total_tokens"),
25
- cost: response.dig("usage", "cost"),
26
- request_id: response["id"],
27
- raw_usage_response: response,
28
- user: user
29
- }
30
- OpenRouterUsageTracker::UsageLog.create!(attributes)
31
- end
32
-
33
- def update_daily_summary(usage_log)
34
- summary = OpenRouterUsageTracker::DailySummary.find_or_initialize_by(
35
- user: usage_log.user,
36
- day: Date.current
37
- )
38
- summary.total_tokens += usage_log.total_tokens
39
- summary.cost += usage_log.cost
40
- summary.save!
41
- end
9
+ include Adapter::Base
42
10
  end
43
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.2.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 OpenRouter API usage and provide methods
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: []
@@ -42,10 +42,14 @@ files:
42
42
  - lib/generators/open_router_usage_tracker/summary_install/summary_install_generator.rb
43
43
  - lib/generators/open_router_usage_tracker/summary_install/templates/migration.rb
44
44
  - lib/open_router_usage_tracker.rb
45
+ - lib/open_router_usage_tracker/adapter/base.rb
45
46
  - lib/open_router_usage_tracker/engine.rb
46
- - lib/open_router_usage_tracker/railtie.rb
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
47
52
  - lib/open_router_usage_tracker/version.rb
48
- - lib/tasks/open_router_usage_tracker_tasks.rake
49
53
  homepage: https://github.com/MclPio/open_router_usage_tracker
50
54
  licenses:
51
55
  - MIT
@@ -69,5 +73,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
73
  requirements: []
70
74
  rubygems_version: 3.6.9
71
75
  specification_version: 4
72
- 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.
73
78
  test_files: []
@@ -1,4 +0,0 @@
1
- module OpenRouterUsageTracker
2
- class Railtie < ::Rails::Railtie
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :open_router_usage_tracker do
3
- # # Task goes here
4
- # end