agentbill-sdk 1.0.1 → 1.0.3

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: 2a5d6d3184f1e30d721acd50e8b51ac947d086352bdd4f3f26702adc127b021c
4
- data.tar.gz: 57eb001ae9a6ba61c002855ad1abbe17fe14fcc3c8fc269b55903b780da669d2
3
+ metadata.gz: eaa02ddbb2ec18543984f6db7fa7e579b30f94e4505469f6e1eb72cfb13b4048
4
+ data.tar.gz: 9ef5bdf26669e2b5f2d5594960c9b8f946a6e47875c7461dec37b4f3c477a79c
5
5
  SHA512:
6
- metadata.gz: 686aef33e7ff8e70994ffe7ad034beddea13b5adf6fc9cd072288898c253f64c49aae92a28c7068f3de4252304b5b6ed5a86698886a87353ce3912afabb870b2
7
- data.tar.gz: 2a86b18d8ea9a824fc2de991ae25f7f83473eda13f9ffaa96e6e451d1cca0729fa6cc3d3331e6a292a94e31fb604643b651e23697bbeec96dde6de6cae2d3c1f
6
+ metadata.gz: 4401d68f0d95b0d4df7f19c69c90c30ff497fcde8a9aa5177c24a2c52664610de3a1a5eff3407c6d61950d788265eca3c5d51eb05e1a2cde6a2c94337869fbd0
7
+ data.tar.gz: 65202696532413e194c27a8aafeb1e3f0874b58863408a13455fbe07a9e115c73687c29df8a9b58b98b987dff329fb000def1514bfa66454a2abbe03f513b4cb
@@ -0,0 +1,56 @@
1
+ name: Publish to RubyGems
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'ruby-v*'
7
+ workflow_dispatch:
8
+ inputs:
9
+ version:
10
+ description: 'Version to publish'
11
+ required: true
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ id-token: write
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Set up Ruby
24
+ uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: '3.2'
27
+ bundler-cache: true
28
+
29
+ - name: Install dependencies
30
+ run: bundle install
31
+
32
+ - name: Run tests
33
+ run: bundle exec rake test
34
+
35
+ - name: Build gem
36
+ run: gem build agentbill.gemspec
37
+
38
+ - name: Publish to RubyGems
39
+ run: |
40
+ mkdir -p $HOME/.gem
41
+ touch $HOME/.gem/credentials
42
+ chmod 0600 $HOME/.gem/credentials
43
+ printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials
44
+ gem push *.gem
45
+ env:
46
+ RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
47
+
48
+ - name: Create GitHub Release
49
+ uses: actions/create-release@v1
50
+ env:
51
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52
+ with:
53
+ tag_name: ${{ github.ref }}
54
+ release_name: Ruby SDK ${{ github.ref }}
55
+ draft: false
56
+ prerelease: false
data/CHANGELOG.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # Changelog
2
+ # Force sync - Updated to 1.0.2
2
3
 
3
4
  All notable changes to this project will be documented in this file.
4
5
 
5
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
8
 
9
+ ## [1.0.2] - 2025-10-27
10
+
11
+ ### Changed
12
+ - Version bump for consistency across SDKs
13
+
8
14
  ## [1.0.0] - 2025-10-21
9
15
 
10
16
  ### Added
data/README.md CHANGED
@@ -4,10 +4,10 @@ OpenTelemetry-based SDK for automatically tracking and billing AI agent usage.
4
4
 
5
5
  ## Installation
6
6
 
7
- ### From GitHub (Recommended)
7
+ ### From RubyGems (Recommended)
8
8
  ```ruby
9
9
  # In your Gemfile
10
- gem 'agentbill', git: 'https://github.com/Agent-Bill/Ruby.git'
10
+ gem 'agentbill-sdk'
11
11
  ```
12
12
 
13
13
  ### From RubyGems
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 = ["support@agentbill.com"]
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,45 @@
1
+ require 'agentbill'
2
+
3
+ # Initialize AgentBill
4
+ agentbill = AgentBill::Client.init({
5
+ api_key: ENV['AGENTBILL_API_KEY'] || 'your-api-key',
6
+ base_url: ENV['AGENTBILL_BASE_URL'],
7
+ customer_id: 'customer-123',
8
+ debug: true
9
+ })
10
+
11
+ # Start a span for a database query
12
+ span = agentbill.tracer.start_span('database_query', {
13
+ 'db.system' => 'postgresql',
14
+ 'db.operation' => 'SELECT',
15
+ 'db.table' => 'users'
16
+ })
17
+
18
+ # Simulate work
19
+ sleep(0.1)
20
+
21
+ # Add more attributes during execution
22
+ span.set_attributes({
23
+ 'db.rows_returned' => 42,
24
+ 'query.duration_ms' => 95
25
+ })
26
+
27
+ # Set status (1 = success, 2 = error)
28
+ span.set_status(1)
29
+ span.end
30
+
31
+ # Start another span for an API call
32
+ api_span = agentbill.tracer.start_span('external_api_call', {
33
+ 'http.method' => 'POST',
34
+ 'http.url' => 'https://api.example.com/endpoint',
35
+ 'http.status_code' => 200
36
+ })
37
+
38
+ sleep(0.2)
39
+ api_span.set_status(1)
40
+ api_span.end
41
+
42
+ # Flush all spans to AgentBill
43
+ agentbill.flush
44
+
45
+ puts "✅ OTEL spans tracked successfully!"
@@ -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
@@ -1,3 +1,3 @@
1
1
  module AgentBill
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.3"
3
3
  end
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
- start_time = Time.now
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
- span = @tracer.start_span('openai.chat.completion', {
27
- 'model' => params[:model] || 'unknown',
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' => response.dig(:usage, :prompt_tokens),
37
- 'response.completion_tokens' => response.dig(:usage, :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
- start_time = Time.now
176
+ model = params[:model] || 'unknown'
177
+ messages = params[:messages] || []
178
+ max_tokens = params[:max_tokens] || 1000
179
+
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
60
189
 
61
- span = @tracer.start_span('anthropic.message', {
62
- 'model' => params[:model] || 'unknown',
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' => response.dig(:usage, :input_tokens),
72
- 'response.output_tokens' => response.dig(:usage, :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,19 +222,49 @@ 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
 
89
- def track_signal(event_name:, revenue: 0, data: {})
230
+ # Track a comprehensive signal with all 68 parameters
231
+ #
232
+ # Supports all parameters:
233
+ # - event_name (required)
234
+ # - data_source, timestamp
235
+ # - agent_external_id, customer_external_id, account_external_id, user_external_id,
236
+ # order_external_id, session_id, conversation_id, thread_id
237
+ # - model, provider, prompt_hash, prompt_sample, response_sample, function_name, tool_name
238
+ # - prompt_tokens, completion_tokens, total_tokens, streaming_tokens, cached_tokens, reasoning_tokens
239
+ # - latency_ms, time_to_first_token, time_to_action_ms, queue_time_ms, processing_time_ms
240
+ # - revenue, cost, conversion_value, revenue_source
241
+ # - experiment_id, experiment_group, variant_id, ab_test_name
242
+ # - conversion_type, conversion_step, funnel_stage, goal_achieved
243
+ # - feedback_score, user_satisfaction, error_type, error_message, retry_count, success_rate
244
+ # - tags, category, priority, severity, compliance_flag, data_classification
245
+ # - product_id, feature_flag, environment, deployment_version, region, tenant_id
246
+ # - parent_span_id, trace_id
247
+ # - custom_dimensions, metadata, data
248
+ #
249
+ # Example:
250
+ # agentbill.track_signal(
251
+ # event_name: "user_conversion",
252
+ # revenue: 99.99,
253
+ # customer_external_id: "cust_123",
254
+ # experiment_id: "exp_abc",
255
+ # conversion_type: "purchase",
256
+ # tags: ["checkout", "success"]
257
+ # )
258
+ def track_signal(**params)
259
+ raise ArgumentError, "event_name is required" unless params[:event_name]
260
+
90
261
  uri = URI("#{@config[:base_url] || 'https://bgwyprqxtdreuutzpbgw.supabase.co'}/functions/v1/record-signals")
91
262
 
92
- payload = {
93
- event_name: event_name,
94
- revenue: revenue,
95
- customer_id: @config[:customer_id],
96
- timestamp: Time.now.to_i,
97
- data: data
98
- }
263
+ # Add timestamp if not provided
264
+ params[:timestamp] ||= Time.now.to_f
265
+
266
+ # Remove nil values
267
+ payload = params.reject { |_, v| v.nil? }
99
268
 
100
269
  begin
101
270
  http = Net::HTTP.new(uri.host, uri.port)
@@ -109,12 +278,15 @@ module AgentBill
109
278
  response = http.request(request)
110
279
 
111
280
  if @config[:debug]
112
- puts "[AgentBill] Signal tracked: #{event_name}, revenue: $#{revenue}"
281
+ puts "[AgentBill] Signal tracked: #{params[:event_name]}"
113
282
  end
283
+
284
+ response.code == '200'
114
285
  rescue => e
115
286
  if @config[:debug]
116
287
  puts "[AgentBill] Failed to track signal: #{e.message}"
117
288
  end
289
+ false
118
290
  end
119
291
  end
120
292
 
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: 1.0.1
4
+ version: 1.0.3
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-24 00:00:00.000000000 Z
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
- - support@agentbill.com
44
+ - dominic@agentbill.io
45
45
  executables: []
46
46
  extensions: []
47
47
  extra_rdoc_files: []
@@ -50,6 +50,7 @@ files:
50
50
  - ".github/ISSUE_TEMPLATE/feature_request.md"
51
51
  - ".github/pull_request_template.md"
52
52
  - ".github/workflows/ci.yml"
53
+ - ".github/workflows/publish.yml"
53
54
  - ".gitignore"
54
55
  - ".rspec"
55
56
  - ".rubocop.yml"
@@ -63,7 +64,9 @@ files:
63
64
  - SECURITY.md
64
65
  - agentbill.gemspec
65
66
  - examples/anthropic_basic.rb
67
+ - examples/manual_otel_tracking.rb
66
68
  - examples/openai_basic.rb
69
+ - examples/zero_config.rb
67
70
  - lib/agentbill.rb
68
71
  - lib/agentbill/tracer.rb
69
72
  - lib/agentbill/version.rb