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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7a0b0d377d1ceb1cefd29fc39a4c0c59b62abd1693991887df3a3a0d5ee1098
4
- data.tar.gz: d3b49c32c40b830a0eb6440555060d3d396d4090452a03f418e6b093e277b46f
3
+ metadata.gz: 7253067ecf8149856b410df301488651e176e01e7479da73b2a028c97ee65b41
4
+ data.tar.gz: 8a971201f93f46f4c7b8865c0bf1ae758dbc4b2788e250a6db642cc1f6925f36
5
5
  SHA512:
6
- metadata.gz: fe1055299e94060dac77b0ef1771f7297efad1ac915b1ec8a28f42a051530d9b5d7104f22bbf4e4b1ce51932d75db47e90220405b27ee221fd203e345fe19110
7
- data.tar.gz: eba805cd5aa35646a8df1f44161d6077be70cbcf6fc7c84c5c1919e81280428fc7cfa16b420a3662dc27f4c49f242b264015986ec9dfe157c2e63525461df043
6
+ metadata.gz: 9a411a62b04519c2a03cf4b81259378a6dce1739f218e432b7cf8656a58b16c511540804a72ac125a2da9606f35cd61b8dbf0db8de74470450acd611d2c8aa2e
7
+ data.tar.gz: dd5079f38075e63e85761172ac12f97c09f7ce8c19c975f699b93375236da078aeec2ef5885deeb5b2637236c996986701cadafbc1ff36bdc4beb8a9fff6ed4b
data/README.md CHANGED
@@ -1,42 +1,80 @@
1
1
  # OpenRouterUsageTracker
2
- [![Gem Version](https://badge.fury.io/rb/open_router_usage_tracker.svg)](https://badge.fury.io/rb/open_router_usage_tracker)
3
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
+ [![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.io](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:
23
56
  ```bash
24
- $ gem install open_router_usage_tracker
57
+ gem install open_router_usage_tracker
25
58
  ```
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
- 1. **Run the Database Migration**:
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
- 1. **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`).
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 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.
54
114
 
55
115
  ```ruby
56
- # Assume `api_response` is the parsed JSON hash from OpenRouter
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
- begin
60
- OpenRouterUsageTracker.log(response: api_response, user: current_user)
61
- rescue ActiveRecord::RecordInvalid => e
62
- # This can happen if the response hash is missing required keys
63
- # (e.g., 'id', 'model', 'usage').
64
- logger.error "Failed to log OpenRouter usage: #{e.message}"
65
- 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)
66
130
  ```
67
131
 
68
- ### Tracking Usage
69
- The `Trackable` concern adds powerful querying methods to your user model.
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 main method is `usage_in_period(range)`, which returns a hash containing the total tokens and cost for a given time range.
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
- **Example: Implementing a rate limit**
142
+ #### Providing Your Own Cost
74
143
 
75
- Imagine you want to prevent users from using more than 100,000 tokens in a 24-hour period.
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
- # somewhere in a controller or before_action
147
+ # Calculate your own cost for an OpenAI call
148
+ openai_response["usage"]["cost"] = your_calculated_cost # e.g., 0.0123
79
149
 
80
- def check_usage_limit
81
- usage = current_user.usage_in_last_24_hours
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
- if usage[:tokens] > 100_000
84
- render json: { error: "You have exceeded your daily usage limit." }, status: :too_many_requests
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
- The gem also provides `usage_in_last_24_hours` as a convenience method and you can always get all the data using `usage_logs`.
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", dependent: :destroy
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
- # 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.
12
14
  #
13
- # Example:
14
- # user.usage_in_period(24.hours.ago..Time.current)
15
- # => { tokens: 1500, cost: 0.025 }
16
- #
17
- def usage_in_period(range)
18
- logs_in_range = self.usage_logs.where(created_at: range)
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
- # A convenience method for checking the last 24 hours.
28
- def usage_in_last_24_hours
29
- 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)
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, 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
@@ -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,3 +1,3 @@
1
1
  module OpenRouterUsageTracker
2
- VERSION = "0.1.1"
2
+ VERSION = "1.0.0"
3
3
  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
- def log(response:, user:)
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.1.1
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: []
@@ -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/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
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: []
@@ -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