agentbill-sdk 1.0.2 → 2.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/CHANGELOG.md +1 -0
- data/agentbill.gemspec +1 -1
- data/examples/zero_config.rb +69 -0
- data/lib/agentbill/version.rb +1 -1
- data/lib/agentbill.rb +153 -12
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9f3faf536ea2cdd78d8d97f3200ab80bab8889654b924a2f035a35685aa210f
|
|
4
|
+
data.tar.gz: 4397d5f95b5e13bee80a28f7a27165a4f8b5e60c5f478bbfd19b11030651413f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 625bad2690c2dd452d299a5664cca0db3d2e806ab0318ce0a0ce7330b6ce6b873db4213b63e2123286ade6593b13ece94de3b1550e96f5f26b4dd2122c6921ba
|
|
7
|
+
data.tar.gz: 3257333096fda49a819738485c457e69741c556b35281648890533e58a3819d631289e9fc170575a55bf598d41a6d2aafe3c7e42bee8c7ea4c25b95bf9937672
|
data/CHANGELOG.md
CHANGED
data/agentbill.gemspec
CHANGED
|
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
|
|
|
4
4
|
spec.name = "agentbill-sdk"
|
|
5
5
|
spec.version = AgentBill::VERSION
|
|
6
6
|
spec.authors = ["AgentBill"]
|
|
7
|
-
spec.email = ["
|
|
7
|
+
spec.email = ["dominic@agentbill.io"]
|
|
8
8
|
|
|
9
9
|
spec.summary = "OpenTelemetry-based SDK for tracking AI agent usage and billing"
|
|
10
10
|
spec.description = "Automatically track and bill AI agent usage with zero-config instrumentation for OpenAI, Anthropic, and more"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
=begin
|
|
2
|
+
Zero-Config AI Cost Guard Example
|
|
3
|
+
|
|
4
|
+
Just wrap your AI client - that's it!
|
|
5
|
+
|
|
6
|
+
What happens automatically:
|
|
7
|
+
- Provider detection (OpenAI, Anthropic, etc.)
|
|
8
|
+
- Budget validation BEFORE spending (when daily_budget or monthly_budget is set)
|
|
9
|
+
- Automatic blocking if budget would be exceeded
|
|
10
|
+
- Usage tracking AFTER completion
|
|
11
|
+
=end
|
|
12
|
+
|
|
13
|
+
require 'agentbill'
|
|
14
|
+
require 'openai'
|
|
15
|
+
|
|
16
|
+
# 1. Initialize AgentBill with your API key and budget
|
|
17
|
+
agentbill = AgentBill::Client.init({
|
|
18
|
+
api_key: ENV['AGENTBILL_API_KEY'],
|
|
19
|
+
customer_id: 'customer-123',
|
|
20
|
+
daily_budget: 10.00, # Optional: $10/day limit (enables Cost Guard)
|
|
21
|
+
monthly_budget: 200.00, # Optional: $200/month limit (enables Cost Guard)
|
|
22
|
+
debug: true
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
# 2. Wrap your AI client - provider auto-detected!
|
|
26
|
+
openai = agentbill.wrap_openai(OpenAI::Client.new(
|
|
27
|
+
access_token: ENV['OPENAI_API_KEY']
|
|
28
|
+
))
|
|
29
|
+
|
|
30
|
+
# 3. Use normally - Cost Guard protection happens automatically
|
|
31
|
+
begin
|
|
32
|
+
response = openai.chat({
|
|
33
|
+
model: 'gpt-4o-mini',
|
|
34
|
+
messages: [
|
|
35
|
+
{ role: 'user', content: 'What is the capital of France?' }
|
|
36
|
+
]
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
puts '✅ Response: ' + response['choices'][0]['message']['content']
|
|
40
|
+
puts '💰 Cost tracked automatically'
|
|
41
|
+
puts '🛡️ Budget validated BEFORE spending'
|
|
42
|
+
|
|
43
|
+
rescue => e
|
|
44
|
+
if e.respond_to?(:code)
|
|
45
|
+
case e.code
|
|
46
|
+
when 'BUDGET_EXCEEDED'
|
|
47
|
+
puts '❌ Budget limit reached - request blocked by Cost Guard'
|
|
48
|
+
when 'RATE_LIMIT_EXCEEDED'
|
|
49
|
+
puts '❌ Rate limit reached - request blocked by Cost Guard'
|
|
50
|
+
else
|
|
51
|
+
raise e
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
raise e
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
=begin
|
|
59
|
+
That's it! Zero configuration needed.
|
|
60
|
+
|
|
61
|
+
Cost Guard Features:
|
|
62
|
+
- ✅ Hard budget limits (daily/monthly) - request blocked BEFORE spending
|
|
63
|
+
- ✅ Real-time cost estimation and validation
|
|
64
|
+
- ✅ Automatic usage tracking
|
|
65
|
+
- ✅ Works across all providers (OpenAI, Anthropic, Bedrock, Azure, Mistral, Google AI)
|
|
66
|
+
|
|
67
|
+
To disable Cost Guard: Simply remove daily_budget and monthly_budget from init.
|
|
68
|
+
Advanced features (caching, fallback, etc.) coming in Pro tier.
|
|
69
|
+
=end
|
data/lib/agentbill/version.rb
CHANGED
data/lib/agentbill.rb
CHANGED
|
@@ -17,29 +17,142 @@ module AgentBill
|
|
|
17
17
|
new(config)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def estimate_tokens(text)
|
|
23
|
+
# Simple token estimation: ~4 chars per token
|
|
24
|
+
[1, text.to_s.length / 4].max
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def estimate_cost(model, input_tokens, output_tokens)
|
|
28
|
+
# Model pricing (per 1K tokens)
|
|
29
|
+
pricing = {
|
|
30
|
+
'gpt-4' => { input: 0.03, output: 0.06 },
|
|
31
|
+
'gpt-4o' => { input: 0.005, output: 0.015 },
|
|
32
|
+
'gpt-4o-mini' => { input: 0.00015, output: 0.0006 },
|
|
33
|
+
'claude-sonnet-4-5' => { input: 0.003, output: 0.015 },
|
|
34
|
+
'claude-opus-4-1-20250805' => { input: 0.015, output: 0.075 },
|
|
35
|
+
'claude-3-5-sonnet-20241022' => { input: 0.003, output: 0.015 },
|
|
36
|
+
'mistral-large-latest' => { input: 0.004, output: 0.012 },
|
|
37
|
+
'gemini-pro' => { input: 0.00025, output: 0.0005 }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
model_price = pricing[model] || { input: 0.001, output: 0.002 }
|
|
41
|
+
(input_tokens / 1000.0 * model_price[:input]) + (output_tokens / 1000.0 * model_price[:output])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_request(model, messages, estimated_output_tokens = 1000)
|
|
45
|
+
return { 'allowed' => true } unless @config[:daily_budget] || @config[:monthly_budget]
|
|
46
|
+
|
|
47
|
+
uri = URI("#{@config[:base_url] || 'https://bgwyprqxtdreuutzpbgw.supabase.co'}/functions/v1/ai-cost-guard-router")
|
|
48
|
+
|
|
49
|
+
payload = {
|
|
50
|
+
api_key: @config[:api_key],
|
|
51
|
+
customer_id: @config[:customer_id],
|
|
52
|
+
model: model,
|
|
53
|
+
messages: messages
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
58
|
+
http.use_ssl = true
|
|
59
|
+
http.read_timeout = 10
|
|
60
|
+
|
|
61
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
62
|
+
request['Content-Type'] = 'application/json'
|
|
63
|
+
request.body = payload.to_json
|
|
64
|
+
|
|
65
|
+
response = http.request(request)
|
|
66
|
+
result = JSON.parse(response.body)
|
|
67
|
+
|
|
68
|
+
puts "[AgentBill Cost Guard] Router response: #{result}" if @config[:debug]
|
|
69
|
+
puts "[AgentBill] Tier: #{result['tier']}, Mode: #{result['mode']}" if @config[:debug] && result['tier']
|
|
70
|
+
result
|
|
71
|
+
rescue => e
|
|
72
|
+
puts "[AgentBill Cost Guard] Router failed: #{e.message}" if @config[:debug]
|
|
73
|
+
{ 'allowed' => true } # Fail open
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def track_usage(model, provider, input_tokens, output_tokens, latency_ms, cost)
|
|
78
|
+
uri = URI("#{@config[:base_url] || 'https://bgwyprqxtdreuutzpbgw.supabase.co'}/functions/v1/track-ai-usage")
|
|
79
|
+
|
|
80
|
+
payload = {
|
|
81
|
+
api_key: @config[:api_key],
|
|
82
|
+
customer_id: @config[:customer_id],
|
|
83
|
+
model: model,
|
|
84
|
+
provider: provider,
|
|
85
|
+
prompt_tokens: input_tokens,
|
|
86
|
+
completion_tokens: output_tokens,
|
|
87
|
+
latency_ms: latency_ms,
|
|
88
|
+
cost: cost
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
93
|
+
http.use_ssl = true
|
|
94
|
+
http.read_timeout = 10
|
|
95
|
+
|
|
96
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
97
|
+
request['Content-Type'] = 'application/json'
|
|
98
|
+
request.body = payload.to_json
|
|
99
|
+
|
|
100
|
+
http.request(request)
|
|
101
|
+
puts "[AgentBill Cost Guard] Usage tracked: $#{format('%.4f', cost)}" if @config[:debug]
|
|
102
|
+
rescue => e
|
|
103
|
+
puts "[AgentBill Cost Guard] Tracking failed: #{e.message}" if @config[:debug]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
public
|
|
108
|
+
|
|
20
109
|
def wrap_openai(client)
|
|
21
110
|
original_method = client.method(:chat)
|
|
111
|
+
config = @config
|
|
112
|
+
tracer = @tracer
|
|
22
113
|
|
|
23
114
|
client.define_singleton_method(:chat) do |params|
|
|
24
|
-
|
|
115
|
+
model = params[:model] || 'unknown'
|
|
116
|
+
messages = params[:messages] || []
|
|
117
|
+
max_tokens = params[:max_tokens] || params[:max_completion_tokens] || 1000
|
|
118
|
+
|
|
119
|
+
# Phase 1: Validate budget BEFORE API call
|
|
120
|
+
validation = config[:_client].send(:validate_request, model, messages, max_tokens)
|
|
121
|
+
unless validation['allowed']
|
|
122
|
+
error_msg = validation['reason'] || 'Budget limit reached'
|
|
123
|
+
puts "[AgentBill Cost Guard] ❌ Request blocked: #{error_msg}" if config[:debug]
|
|
124
|
+
error = StandardError.new(error_msg)
|
|
125
|
+
error.define_singleton_method(:code) { 'BUDGET_EXCEEDED' }
|
|
126
|
+
raise error
|
|
127
|
+
end
|
|
25
128
|
|
|
26
|
-
|
|
27
|
-
|
|
129
|
+
# Phase 2: Execute AI call
|
|
130
|
+
start_time = Time.now
|
|
131
|
+
span = tracer.start_span('openai.chat.completion', {
|
|
132
|
+
'model' => model,
|
|
28
133
|
'provider' => 'openai'
|
|
29
134
|
})
|
|
30
135
|
|
|
31
136
|
begin
|
|
32
137
|
response = original_method.call(params)
|
|
33
|
-
|
|
34
138
|
latency = ((Time.now - start_time) * 1000).round
|
|
139
|
+
|
|
140
|
+
# Phase 3: Track actual usage
|
|
141
|
+
input_tokens = response.dig(:usage, :prompt_tokens) || 0
|
|
142
|
+
output_tokens = response.dig(:usage, :completion_tokens) || 0
|
|
143
|
+
cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens)
|
|
144
|
+
|
|
145
|
+
config[:_client].send(:track_usage, model, 'openai', input_tokens, output_tokens, latency, cost)
|
|
146
|
+
|
|
35
147
|
span.set_attributes({
|
|
36
|
-
'response.prompt_tokens' =>
|
|
37
|
-
'response.completion_tokens' =>
|
|
148
|
+
'response.prompt_tokens' => input_tokens,
|
|
149
|
+
'response.completion_tokens' => output_tokens,
|
|
38
150
|
'response.total_tokens' => response.dig(:usage, :total_tokens),
|
|
39
151
|
'latency_ms' => latency
|
|
40
152
|
})
|
|
41
153
|
span.set_status(0)
|
|
42
154
|
|
|
155
|
+
puts "[AgentBill Cost Guard] ✓ Protected call completed: $#{format('%.4f', cost)}" if config[:debug]
|
|
43
156
|
response
|
|
44
157
|
rescue => e
|
|
45
158
|
span.set_status(1, e.message)
|
|
@@ -49,31 +162,57 @@ module AgentBill
|
|
|
49
162
|
end
|
|
50
163
|
end
|
|
51
164
|
|
|
165
|
+
# Store reference to self for helper methods
|
|
166
|
+
@config[:_client] = self
|
|
52
167
|
client
|
|
53
168
|
end
|
|
54
169
|
|
|
55
170
|
def wrap_anthropic(client)
|
|
56
171
|
original_method = client.method(:messages)
|
|
172
|
+
config = @config
|
|
173
|
+
tracer = @tracer
|
|
57
174
|
|
|
58
175
|
client.define_singleton_method(:messages) do |params|
|
|
59
|
-
|
|
176
|
+
model = params[:model] || 'unknown'
|
|
177
|
+
messages = params[:messages] || []
|
|
178
|
+
max_tokens = params[:max_tokens] || 1000
|
|
60
179
|
|
|
61
|
-
|
|
62
|
-
|
|
180
|
+
# Phase 1: Validate budget BEFORE API call
|
|
181
|
+
validation = config[:_client].send(:validate_request, model, messages, max_tokens)
|
|
182
|
+
unless validation['allowed']
|
|
183
|
+
error_msg = validation['reason'] || 'Budget limit reached'
|
|
184
|
+
puts "[AgentBill Cost Guard] ❌ Request blocked: #{error_msg}" if config[:debug]
|
|
185
|
+
error = StandardError.new(error_msg)
|
|
186
|
+
error.define_singleton_method(:code) { 'BUDGET_EXCEEDED' }
|
|
187
|
+
raise error
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Phase 2: Execute AI call
|
|
191
|
+
start_time = Time.now
|
|
192
|
+
span = tracer.start_span('anthropic.message', {
|
|
193
|
+
'model' => model,
|
|
63
194
|
'provider' => 'anthropic'
|
|
64
195
|
})
|
|
65
196
|
|
|
66
197
|
begin
|
|
67
198
|
response = original_method.call(params)
|
|
68
|
-
|
|
69
199
|
latency = ((Time.now - start_time) * 1000).round
|
|
200
|
+
|
|
201
|
+
# Phase 3: Track actual usage
|
|
202
|
+
input_tokens = response.dig(:usage, :input_tokens) || 0
|
|
203
|
+
output_tokens = response.dig(:usage, :output_tokens) || 0
|
|
204
|
+
cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens)
|
|
205
|
+
|
|
206
|
+
config[:_client].send(:track_usage, model, 'anthropic', input_tokens, output_tokens, latency, cost)
|
|
207
|
+
|
|
70
208
|
span.set_attributes({
|
|
71
|
-
'response.input_tokens' =>
|
|
72
|
-
'response.output_tokens' =>
|
|
209
|
+
'response.input_tokens' => input_tokens,
|
|
210
|
+
'response.output_tokens' => output_tokens,
|
|
73
211
|
'latency_ms' => latency
|
|
74
212
|
})
|
|
75
213
|
span.set_status(0)
|
|
76
214
|
|
|
215
|
+
puts "[AgentBill Cost Guard] ✓ Protected call completed: $#{format('%.4f', cost)}" if config[:debug]
|
|
77
216
|
response
|
|
78
217
|
rescue => e
|
|
79
218
|
span.set_status(1, e.message)
|
|
@@ -83,6 +222,8 @@ module AgentBill
|
|
|
83
222
|
end
|
|
84
223
|
end
|
|
85
224
|
|
|
225
|
+
# Store reference to self for helper methods
|
|
226
|
+
@config[:_client] = self
|
|
86
227
|
client
|
|
87
228
|
end
|
|
88
229
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agentbill-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AgentBill
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-10-
|
|
11
|
+
date: 2025-10-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -41,7 +41,7 @@ dependencies:
|
|
|
41
41
|
description: Automatically track and bill AI agent usage with zero-config instrumentation
|
|
42
42
|
for OpenAI, Anthropic, and more
|
|
43
43
|
email:
|
|
44
|
-
-
|
|
44
|
+
- dominic@agentbill.io
|
|
45
45
|
executables: []
|
|
46
46
|
extensions: []
|
|
47
47
|
extra_rdoc_files: []
|
|
@@ -66,6 +66,7 @@ files:
|
|
|
66
66
|
- examples/anthropic_basic.rb
|
|
67
67
|
- examples/manual_otel_tracking.rb
|
|
68
68
|
- examples/openai_basic.rb
|
|
69
|
+
- examples/zero_config.rb
|
|
69
70
|
- lib/agentbill.rb
|
|
70
71
|
- lib/agentbill/tracer.rb
|
|
71
72
|
- lib/agentbill/version.rb
|