claude_usage 0.1.2
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 +7 -0
- data/.rubocop.yml +74 -0
- data/CHANGELOG.md +53 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +212 -0
- data/Rakefile +8 -0
- data/app/controllers/claude_usage/application_controller.rb +11 -0
- data/app/controllers/claude_usage/usage_controller.rb +55 -0
- data/app/helpers/claude_usage/usage_helper.rb +20 -0
- data/app/views/claude_usage/usage/index.html.erb +298 -0
- data/config/routes.rb +6 -0
- data/lib/claude_usage/cost_calculator.rb +57 -0
- data/lib/claude_usage/engine.rb +23 -0
- data/lib/claude_usage/file_reader.rb +112 -0
- data/lib/claude_usage/formatters.rb +32 -0
- data/lib/claude_usage/litellm_pricing_fetcher.rb +148 -0
- data/lib/claude_usage/project_name_resolver.rb +23 -0
- data/lib/claude_usage/usage_aggregator.rb +55 -0
- data/lib/claude_usage/version.rb +5 -0
- data/lib/claude_usage.rb +52 -0
- data/lib/generators/claude_usage/install_generator.rb +30 -0
- data/lib/generators/claude_usage/templates/initializer.rb +34 -0
- data/lib/tasks/claude_usage_tasks.rake +313 -0
- data/sig/claude_usage.rbs +4 -0
- metadata +139 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeUsage
|
|
4
|
+
class UsageAggregator
|
|
5
|
+
def initialize(project_name: nil)
|
|
6
|
+
@project_name = project_name || ClaudeUsage.configuration.resolved_project_name
|
|
7
|
+
@file_reader = FileReader.new(project_name: @project_name)
|
|
8
|
+
@cost_calculator = CostCalculator.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def daily_usage
|
|
12
|
+
entries = @file_reader.read_usage_data
|
|
13
|
+
grouped = entries.group_by { |e| e[:timestamp].to_date }
|
|
14
|
+
|
|
15
|
+
grouped.map do |date, day_entries|
|
|
16
|
+
aggregate_entries(date, day_entries)
|
|
17
|
+
end.sort_by { |d| d[:date] }.reverse
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def total_usage
|
|
21
|
+
daily = daily_usage
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
total_input_tokens: daily.sum { |d| d[:input_tokens] },
|
|
25
|
+
total_output_tokens: daily.sum { |d| d[:output_tokens] },
|
|
26
|
+
total_cache_creation_tokens: daily.sum { |d| d[:cache_creation_tokens] },
|
|
27
|
+
total_cache_read_tokens: daily.sum { |d| d[:cache_read_tokens] },
|
|
28
|
+
total_tokens: daily.sum { |d| d[:total_tokens] },
|
|
29
|
+
total_cost: daily.sum { |d| d[:cost] },
|
|
30
|
+
models_used: daily.flat_map { |d| d[:models_used] }.uniq,
|
|
31
|
+
first_usage: daily.last&.dig(:date),
|
|
32
|
+
last_usage: daily.first&.dig(:date),
|
|
33
|
+
days_active: daily.size
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def aggregate_entries(date, entries)
|
|
40
|
+
{
|
|
41
|
+
date: date,
|
|
42
|
+
input_tokens: entries.sum { |e| e[:input_tokens] },
|
|
43
|
+
output_tokens: entries.sum { |e| e[:output_tokens] },
|
|
44
|
+
cache_creation_tokens: entries.sum { |e| e[:cache_creation_tokens] },
|
|
45
|
+
cache_read_tokens: entries.sum { |e| e[:cache_read_tokens] },
|
|
46
|
+
total_tokens: entries.sum do |e|
|
|
47
|
+
e[:input_tokens] + e[:output_tokens] + e[:cache_creation_tokens] + e[:cache_read_tokens]
|
|
48
|
+
end,
|
|
49
|
+
cost: entries.sum { |e| @cost_calculator.calculate_cost(e) },
|
|
50
|
+
models_used: entries.map { |e| e[:model] }.compact.uniq,
|
|
51
|
+
request_count: entries.size
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/claude_usage.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'claude_usage/version'
|
|
4
|
+
require_relative 'claude_usage/project_name_resolver'
|
|
5
|
+
|
|
6
|
+
# Only require engine if Rails is defined
|
|
7
|
+
require_relative 'claude_usage/engine' if defined?(Rails)
|
|
8
|
+
|
|
9
|
+
module ClaudeUsage
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Configuration
|
|
13
|
+
class Configuration
|
|
14
|
+
attr_accessor :project_name, :claude_paths, :cache_expiry, :offline_mode, :verify_ssl
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@project_name = nil # Will auto-detect from Rails.root if not set
|
|
18
|
+
@claude_paths = nil # Will use defaults if not set
|
|
19
|
+
@cache_expiry = 3600 # 1 hour in seconds
|
|
20
|
+
@offline_mode = false
|
|
21
|
+
@verify_ssl = false # Disabled by default for macOS compatibility
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Auto-detect project name from Rails.root
|
|
25
|
+
# Converts: /Users/name/projects/myapp → -Users-name-projects-myapp
|
|
26
|
+
def resolved_project_name
|
|
27
|
+
ProjectNameResolver.resolve(@project_name, fallback: nil)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
attr_writer :configuration
|
|
33
|
+
|
|
34
|
+
def configuration
|
|
35
|
+
@configuration ||= Configuration.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def configure
|
|
39
|
+
yield(configuration)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset_configuration!
|
|
43
|
+
@configuration = Configuration.new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Load service objects
|
|
49
|
+
require_relative 'claude_usage/file_reader'
|
|
50
|
+
require_relative 'claude_usage/litellm_pricing_fetcher'
|
|
51
|
+
require_relative 'claude_usage/cost_calculator'
|
|
52
|
+
require_relative 'claude_usage/usage_aggregator'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module ClaudeUsage
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
|
9
|
+
|
|
10
|
+
desc 'Creates ClaudeUsage initializer and shows setup instructions'
|
|
11
|
+
|
|
12
|
+
def copy_initializer
|
|
13
|
+
@detected_project_name = detect_project_name
|
|
14
|
+
template 'initializer.rb', 'config/initializers/claude_usage.rb'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show_readme
|
|
18
|
+
readme 'README' if behavior == :invoke
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def detect_project_name
|
|
24
|
+
# Use shared resolver to avoid duplication
|
|
25
|
+
require_relative '../../claude_usage/project_name_resolver'
|
|
26
|
+
ClaudeUsage::ProjectNameResolver.resolve(nil, fallback: 'your-project-name')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ClaudeUsage.configure do |config|
|
|
4
|
+
# Project name - AUTO-DETECTED from Rails.root by default
|
|
5
|
+
#
|
|
6
|
+
# Claude Code stores projects using the full absolute path with dashes, like:
|
|
7
|
+
# /Users/name/projects/myapp → -Users-name-projects-myapp
|
|
8
|
+
#
|
|
9
|
+
# The gem automatically detects this, so you usually don't need to set it manually.
|
|
10
|
+
#
|
|
11
|
+
# Only uncomment if auto-detection fails or you need a custom name:
|
|
12
|
+
# config.project_name = "<%= @detected_project_name %>"
|
|
13
|
+
#
|
|
14
|
+
# To see what's auto-detected, run: rake claude_usage:show_project
|
|
15
|
+
|
|
16
|
+
# Custom Claude paths (optional)
|
|
17
|
+
# Default paths are already configured:
|
|
18
|
+
# - ~/.config/claude/projects
|
|
19
|
+
# - ~/.claude/projects
|
|
20
|
+
# config.claude_paths = [
|
|
21
|
+
# File.expand_path("~/.config/claude/projects"),
|
|
22
|
+
# File.expand_path("~/.claude/projects")
|
|
23
|
+
# ]
|
|
24
|
+
|
|
25
|
+
# Pricing cache expiry in seconds (default: 3600 = 1 hour)
|
|
26
|
+
# config.cache_expiry = 3600
|
|
27
|
+
|
|
28
|
+
# Offline mode (use cached pricing only, recommended for production)
|
|
29
|
+
# config.offline_mode = false
|
|
30
|
+
|
|
31
|
+
# SSL verification (disabled by default for macOS compatibility)
|
|
32
|
+
# Enable in production if your certificates are up-to-date
|
|
33
|
+
# config.verify_ssl = false
|
|
34
|
+
end
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../claude_usage/formatters'
|
|
4
|
+
|
|
5
|
+
namespace :claude_usage do
|
|
6
|
+
desc 'Show daily usage report'
|
|
7
|
+
task daily: :environment do
|
|
8
|
+
project_name = ClaudeUsage.configuration.resolved_project_name
|
|
9
|
+
aggregator = ClaudeUsage::UsageAggregator.new
|
|
10
|
+
daily_usage = aggregator.daily_usage
|
|
11
|
+
|
|
12
|
+
puts "\n🤖 Claude Code - Daily Usage Report\n"
|
|
13
|
+
puts '=' * 80
|
|
14
|
+
puts "📁 Project: #{project_name}"
|
|
15
|
+
puts '=' * 80
|
|
16
|
+
|
|
17
|
+
if daily_usage.empty?
|
|
18
|
+
puts "\n❌ No usage data found!"
|
|
19
|
+
puts "\nTroubleshooting:"
|
|
20
|
+
puts ' 1. Run: rake claude_usage:show_project'
|
|
21
|
+
puts ' 2. Check that Claude Code has been used with this project'
|
|
22
|
+
puts " 3. Verify project name matches Claude Code's folder name"
|
|
23
|
+
puts "\n"
|
|
24
|
+
else
|
|
25
|
+
daily_usage.each do |day|
|
|
26
|
+
puts "\n📅 #{day[:date].strftime('%Y-%m-%d')}"
|
|
27
|
+
puts " Input Tokens: #{day[:input_tokens].to_s.rjust(10)}"
|
|
28
|
+
puts " Output Tokens: #{day[:output_tokens].to_s.rjust(10)}"
|
|
29
|
+
puts " Cache Creation: #{day[:cache_creation_tokens].to_s.rjust(10)}"
|
|
30
|
+
puts " Cache Read: #{day[:cache_read_tokens].to_s.rjust(10)}"
|
|
31
|
+
puts " Total Tokens: #{day[:total_tokens].to_s.rjust(10)}"
|
|
32
|
+
puts " Cost: $#{format('%.4f', day[:cost]).rjust(9)}"
|
|
33
|
+
puts " Requests: #{day[:request_count].to_s.rjust(10)}"
|
|
34
|
+
puts " Models: #{day[:models_used].join(', ')}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
puts "\n#{'=' * 80}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc 'Show total usage'
|
|
42
|
+
task total: :environment do
|
|
43
|
+
project_name = ClaudeUsage.configuration.resolved_project_name
|
|
44
|
+
aggregator = ClaudeUsage::UsageAggregator.new
|
|
45
|
+
totals = aggregator.total_usage
|
|
46
|
+
|
|
47
|
+
puts "\n🤖 Claude Code - Total Usage Report\n"
|
|
48
|
+
puts '=' * 80
|
|
49
|
+
puts "📁 Project: #{project_name}"
|
|
50
|
+
puts '=' * 80
|
|
51
|
+
|
|
52
|
+
if totals[:days_active].zero?
|
|
53
|
+
puts "\n❌ No usage data found!"
|
|
54
|
+
puts "\nTroubleshooting:"
|
|
55
|
+
puts ' 1. Run: rake claude_usage:show_project'
|
|
56
|
+
puts ' 2. Check that Claude Code has been used with this project'
|
|
57
|
+
puts " 3. Verify project name matches Claude Code's folder name"
|
|
58
|
+
puts "\n"
|
|
59
|
+
else
|
|
60
|
+
puts "\n💰 Cost: $#{format('%.4f', totals[:total_cost])}"
|
|
61
|
+
|
|
62
|
+
# Show warning if cost is 0 but tokens exist
|
|
63
|
+
if totals[:total_cost].zero? && totals[:total_tokens].positive?
|
|
64
|
+
puts ' ⚠️ Cost is $0 - model pricing may not be available'
|
|
65
|
+
puts ' Run: rake claude_usage:debug_pricing'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts "📊 Total Tokens: #{ClaudeUsage::Formatters.format_number(totals[:total_tokens])}"
|
|
69
|
+
puts " - Input: #{ClaudeUsage::Formatters.format_number(totals[:total_input_tokens])}"
|
|
70
|
+
puts " - Output: #{ClaudeUsage::Formatters.format_number(totals[:total_output_tokens])}"
|
|
71
|
+
puts " - Cache Creation: #{ClaudeUsage::Formatters.format_number(totals[:total_cache_creation_tokens])}"
|
|
72
|
+
puts " - Cache Read: #{ClaudeUsage::Formatters.format_number(totals[:total_cache_read_tokens])}"
|
|
73
|
+
puts "📅 Days Active: #{totals[:days_active]}"
|
|
74
|
+
puts "📅 First Usage: #{totals[:first_usage]}"
|
|
75
|
+
puts "📅 Last Usage: #{totals[:last_usage]}"
|
|
76
|
+
puts "🤖 Models Used: #{totals[:models_used].join(', ')}"
|
|
77
|
+
puts "\n#{'=' * 80}\n"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
desc 'Clear pricing cache'
|
|
82
|
+
task clear_cache: :environment do
|
|
83
|
+
Rails.cache.delete('claude_usage/litellm_pricing_data')
|
|
84
|
+
puts '✅ Pricing cache cleared!'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
desc 'Show project name being tracked'
|
|
88
|
+
task show_project: :environment do
|
|
89
|
+
manual_config = ClaudeUsage.configuration.project_name
|
|
90
|
+
auto_detected = ClaudeUsage.configuration.resolved_project_name
|
|
91
|
+
|
|
92
|
+
puts "\n📁 Project Detection:"
|
|
93
|
+
puts " Rails.root: #{Rails.root}"
|
|
94
|
+
|
|
95
|
+
if manual_config
|
|
96
|
+
puts " Manual config: #{manual_config} (overriding auto-detection)"
|
|
97
|
+
project_name = manual_config
|
|
98
|
+
else
|
|
99
|
+
puts " Auto-detected: #{auto_detected}"
|
|
100
|
+
puts ' Status: ✅ Automatic (no config needed)'
|
|
101
|
+
project_name = auto_detected
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
claude_paths = ClaudeUsage.configuration.claude_paths || ClaudeUsage::FileReader::CLAUDE_PATHS
|
|
105
|
+
puts "\n📂 Searching in directories:"
|
|
106
|
+
found_any = false
|
|
107
|
+
|
|
108
|
+
claude_paths.each do |path|
|
|
109
|
+
if Dir.exist?(path)
|
|
110
|
+
puts " ✅ #{path} (exists)"
|
|
111
|
+
|
|
112
|
+
# List all projects in this directory
|
|
113
|
+
projects = Dir.glob(File.join(path, '*')).select { |f| File.directory?(f) }
|
|
114
|
+
if projects.any?
|
|
115
|
+
puts ' Available projects:'
|
|
116
|
+
projects.each do |p|
|
|
117
|
+
project_folder = File.basename(p)
|
|
118
|
+
jsonl_count = Dir.glob(File.join(p, '**', '*.jsonl')).size
|
|
119
|
+
marker = project_folder == project_name ? '👉' : ' '
|
|
120
|
+
puts " #{marker} #{project_folder} (#{jsonl_count} JSONL files)"
|
|
121
|
+
found_any = true if project_folder == project_name && jsonl_count.positive?
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
puts ' (no projects found)'
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
puts " ❌ #{path} (not found)"
|
|
128
|
+
end
|
|
129
|
+
puts ''
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if found_any
|
|
133
|
+
puts "\n✅ Found data for project '#{project_name}'!"
|
|
134
|
+
puts ' Run: rake claude_usage:total'
|
|
135
|
+
else
|
|
136
|
+
puts "\n⚠️ No data found for project '#{project_name}'"
|
|
137
|
+
puts "\n💡 Solutions:"
|
|
138
|
+
puts ' 1. If your project name differs, set it in config/initializers/claude_usage.rb:'
|
|
139
|
+
puts ' ClaudeUsage.configure do |config|'
|
|
140
|
+
puts " config.project_name = 'actual-project-name'"
|
|
141
|
+
puts ' end'
|
|
142
|
+
puts "\n 2. Check available project names above (marked with 👉 when matching)"
|
|
143
|
+
puts "\n 3. Ensure you've used Claude Code with this project\n"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
desc 'Debug pricing for current project models'
|
|
148
|
+
task debug_pricing: :environment do
|
|
149
|
+
puts "\n🔬 Debugging Model Pricing\n"
|
|
150
|
+
puts '=' * 80
|
|
151
|
+
|
|
152
|
+
ClaudeUsage.configuration.project_name || Rails.root.basename.to_s
|
|
153
|
+
aggregator = ClaudeUsage::UsageAggregator.new
|
|
154
|
+
totals = aggregator.total_usage
|
|
155
|
+
|
|
156
|
+
if totals[:models_used].empty?
|
|
157
|
+
puts '❌ No models found in usage data'
|
|
158
|
+
puts "\n"
|
|
159
|
+
next
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
puts "\n📋 Models in your usage data:"
|
|
163
|
+
totals[:models_used].each { |model| puts " - #{model}" }
|
|
164
|
+
puts ''
|
|
165
|
+
|
|
166
|
+
fetcher = ClaudeUsage::LiteLLMPricingFetcher.new
|
|
167
|
+
|
|
168
|
+
# First, let's check if we can fetch pricing at all
|
|
169
|
+
puts '🔍 Testing pricing fetch...'
|
|
170
|
+
pricing_data = fetcher.fetch_pricing_data
|
|
171
|
+
|
|
172
|
+
if pricing_data.empty?
|
|
173
|
+
puts '❌ Failed to fetch pricing data from LiteLLM!'
|
|
174
|
+
puts ' This might be a network issue or the API is down.'
|
|
175
|
+
puts ' Check your internet connection and try again.'
|
|
176
|
+
puts "\n"
|
|
177
|
+
next
|
|
178
|
+
else
|
|
179
|
+
puts "✅ Successfully fetched pricing for #{pricing_data.keys.size} models"
|
|
180
|
+
|
|
181
|
+
# Check if our specific models are in there
|
|
182
|
+
test_models = ['claude-haiku-4-5-20251001', 'claude-sonnet-4-5-20250929']
|
|
183
|
+
test_models.each do |test_model|
|
|
184
|
+
if pricing_data.key?(test_model)
|
|
185
|
+
puts " ✅ #{test_model} found in database"
|
|
186
|
+
else
|
|
187
|
+
puts " ❌ #{test_model} NOT in database"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
puts ''
|
|
192
|
+
|
|
193
|
+
totals[:models_used].each do |model|
|
|
194
|
+
puts "\n🤖 Model: #{model}"
|
|
195
|
+
puts " #{'-' * 76}"
|
|
196
|
+
|
|
197
|
+
# Skip synthetic models
|
|
198
|
+
if model == '<synthetic>'
|
|
199
|
+
puts ' ℹ️ System/internal messages (not billable)'
|
|
200
|
+
next
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
pricing, debug_info = fetcher.get_model_pricing(model, debug: true)
|
|
204
|
+
|
|
205
|
+
if pricing
|
|
206
|
+
puts ' ✅ Pricing found!'
|
|
207
|
+
puts " Input: $#{pricing['input_cost_per_token'] || pricing[:input_cost_per_token]} per token"
|
|
208
|
+
puts " Output: $#{pricing['output_cost_per_token'] || pricing[:output_cost_per_token]} per token"
|
|
209
|
+
cache_creation = pricing['cache_creation_input_token_cost'] ||
|
|
210
|
+
pricing[:cache_creation_input_token_cost] || 'N/A'
|
|
211
|
+
puts " Cache Creation: $#{cache_creation} per token"
|
|
212
|
+
cache_read = pricing['cache_read_input_token_cost'] ||
|
|
213
|
+
pricing[:cache_read_input_token_cost] || 'N/A'
|
|
214
|
+
puts " Cache Read: $#{cache_read} per token"
|
|
215
|
+
|
|
216
|
+
# Show how it was found
|
|
217
|
+
if debug_info[:fuzzy_match]
|
|
218
|
+
puts " (Found via fuzzy match: #{debug_info[:fuzzy_match]})"
|
|
219
|
+
elsif debug_info[:fallbacks]&.any? { |f| f[:found] }
|
|
220
|
+
found = debug_info[:fallbacks].find { |f| f[:found] }
|
|
221
|
+
puts " (Found via fallback: #{found[:candidate]})"
|
|
222
|
+
end
|
|
223
|
+
else
|
|
224
|
+
puts ' ❌ No pricing found!'
|
|
225
|
+
puts ' This model will contribute $0 to cost calculations'
|
|
226
|
+
puts ''
|
|
227
|
+
puts ' Debugging info:'
|
|
228
|
+
puts " - Original model name: #{model}"
|
|
229
|
+
|
|
230
|
+
if debug_info[:tried].any?
|
|
231
|
+
puts ' - Provider prefixes tried:'
|
|
232
|
+
debug_info[:tried].each do |attempt|
|
|
233
|
+
puts " * #{attempt[:candidate]}"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
if debug_info[:fallbacks].any?
|
|
238
|
+
puts ' - Fallback models tried:'
|
|
239
|
+
debug_info[:fallbacks].each do |attempt|
|
|
240
|
+
puts " * #{attempt[:candidate]}"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
puts ''
|
|
245
|
+
puts ' 💡 Possible solutions:'
|
|
246
|
+
puts ' 1. Model may be too new for LiteLLM pricing database'
|
|
247
|
+
puts ' 2. Model name format may differ (check LiteLLM docs)'
|
|
248
|
+
puts ' 3. Clear cache and retry: rake claude_usage:clear_cache'
|
|
249
|
+
puts ' 4. Check LiteLLM database:'
|
|
250
|
+
puts ' https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
puts "\n#{'=' * 80}\n"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
desc 'Test LiteLLM pricing fetch'
|
|
258
|
+
task test_fetch: :environment do
|
|
259
|
+
puts "\n🧪 Testing LiteLLM Pricing Fetch\n"
|
|
260
|
+
puts '=' * 80
|
|
261
|
+
puts ''
|
|
262
|
+
|
|
263
|
+
fetcher = ClaudeUsage::LiteLLMPricingFetcher.new
|
|
264
|
+
|
|
265
|
+
puts 'Fetching pricing data...'
|
|
266
|
+
pricing_data = fetcher.fetch_pricing_data
|
|
267
|
+
|
|
268
|
+
if pricing_data.empty?
|
|
269
|
+
puts '❌ FAILED - No pricing data received'
|
|
270
|
+
puts ''
|
|
271
|
+
puts 'Possible causes:'
|
|
272
|
+
puts ' 1. Network connectivity issue'
|
|
273
|
+
puts ' 2. LiteLLM API is down'
|
|
274
|
+
puts ' 3. HTTP gem not working correctly'
|
|
275
|
+
puts ''
|
|
276
|
+
puts 'Try:'
|
|
277
|
+
puts " curl #{ClaudeUsage::LiteLLMPricingFetcher::LITELLM_PRICING_URL}"
|
|
278
|
+
puts ''
|
|
279
|
+
else
|
|
280
|
+
puts "✅ SUCCESS - Fetched #{pricing_data.keys.size} models"
|
|
281
|
+
puts ''
|
|
282
|
+
|
|
283
|
+
# Test specific models
|
|
284
|
+
test_models = {
|
|
285
|
+
'claude-haiku-4-5-20251001' => 'Claude 4.5 Haiku',
|
|
286
|
+
'claude-sonnet-4-5-20250929' => 'Claude 4.5 Sonnet',
|
|
287
|
+
'claude-3-5-haiku-20241022' => 'Claude 3.5 Haiku',
|
|
288
|
+
'claude-3-5-sonnet-20241022' => 'Claude 3.5 Sonnet'
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
puts 'Testing specific models:'
|
|
292
|
+
test_models.each do |model_key, model_name|
|
|
293
|
+
if pricing_data.key?(model_key)
|
|
294
|
+
pricing = pricing_data[model_key]
|
|
295
|
+
puts " ✅ #{model_name} (#{model_key})"
|
|
296
|
+
puts " Input: $#{pricing[:input_cost_per_token]}"
|
|
297
|
+
puts " Output: $#{pricing[:output_cost_per_token]}"
|
|
298
|
+
else
|
|
299
|
+
puts " ❌ #{model_name} (#{model_key}) - NOT FOUND"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
puts ''
|
|
303
|
+
|
|
304
|
+
# Show sample of available models
|
|
305
|
+
puts 'Sample of available models:'
|
|
306
|
+
pricing_data.keys.select { |k| k.include?('claude') }.first(10).each do |key|
|
|
307
|
+
puts " - #{key}"
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
puts "\n#{'=' * 80}\n"
|
|
312
|
+
end
|
|
313
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: claude_usage
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Shoeb
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: httparty
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.21'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.21'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec-rails
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '6.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '6.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: webmock
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
description: A Rails engine for monitoring Claude Code usage, calculating costs using
|
|
83
|
+
LiteLLM pricing, and displaying usage analytics in your Rails app
|
|
84
|
+
email:
|
|
85
|
+
- shoebtamboli98@gmail.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- ".rubocop.yml"
|
|
91
|
+
- CHANGELOG.md
|
|
92
|
+
- CODE_OF_CONDUCT.md
|
|
93
|
+
- LICENSE.txt
|
|
94
|
+
- README.md
|
|
95
|
+
- Rakefile
|
|
96
|
+
- app/controllers/claude_usage/application_controller.rb
|
|
97
|
+
- app/controllers/claude_usage/usage_controller.rb
|
|
98
|
+
- app/helpers/claude_usage/usage_helper.rb
|
|
99
|
+
- app/views/claude_usage/usage/index.html.erb
|
|
100
|
+
- config/routes.rb
|
|
101
|
+
- lib/claude_usage.rb
|
|
102
|
+
- lib/claude_usage/cost_calculator.rb
|
|
103
|
+
- lib/claude_usage/engine.rb
|
|
104
|
+
- lib/claude_usage/file_reader.rb
|
|
105
|
+
- lib/claude_usage/formatters.rb
|
|
106
|
+
- lib/claude_usage/litellm_pricing_fetcher.rb
|
|
107
|
+
- lib/claude_usage/project_name_resolver.rb
|
|
108
|
+
- lib/claude_usage/usage_aggregator.rb
|
|
109
|
+
- lib/claude_usage/version.rb
|
|
110
|
+
- lib/generators/claude_usage/install_generator.rb
|
|
111
|
+
- lib/generators/claude_usage/templates/initializer.rb
|
|
112
|
+
- lib/tasks/claude_usage_tasks.rake
|
|
113
|
+
- sig/claude_usage.rbs
|
|
114
|
+
homepage: https://github.com/Shoebtamboli/claude_usage
|
|
115
|
+
licenses:
|
|
116
|
+
- MIT
|
|
117
|
+
metadata:
|
|
118
|
+
homepage_uri: https://github.com/Shoebtamboli/claude_usage
|
|
119
|
+
source_code_uri: https://github.com/Shoebtamboli/claude_usage
|
|
120
|
+
changelog_uri: https://github.com/Shoebtamboli/claude_usage/blob/main/CHANGELOG.md
|
|
121
|
+
rubygems_mfa_required: 'true'
|
|
122
|
+
rdoc_options: []
|
|
123
|
+
require_paths:
|
|
124
|
+
- lib
|
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: 3.2.0
|
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '0'
|
|
135
|
+
requirements: []
|
|
136
|
+
rubygems_version: 3.6.9
|
|
137
|
+
specification_version: 4
|
|
138
|
+
summary: Track Claude Code token usage and costs in Rails applications
|
|
139
|
+
test_files: []
|