agentbill-sdk 2.0.1 → 7.17.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/.github/workflows/ci.yml +5 -4
- data/CHANGELOG.md +67 -13
- data/README.md +4 -3
- data/SECURITY.md +2 -2
- data/examples/ollama_basic.rb +81 -0
- data/examples/openai_custom_event.rb +50 -0
- data/examples/perplexity_basic.rb +66 -0
- data/lib/agentbill/agents.rb +226 -0
- data/lib/agentbill/customers.rb +164 -0
- data/lib/agentbill/distributed.rb +109 -0
- data/lib/agentbill/exceptions.rb +84 -0
- data/lib/agentbill/ollama_wrapper.rb +153 -0
- data/lib/agentbill/orders.rb +283 -0
- data/lib/agentbill/perplexity_wrapper.rb +101 -0
- data/lib/agentbill/pricing.rb +52 -0
- data/lib/agentbill/signal_types.rb +179 -0
- data/lib/agentbill/signals.rb +199 -0
- data/lib/agentbill/tracer.rb +69 -12
- data/lib/agentbill/tracing.rb +343 -0
- data/lib/agentbill/version.rb +1 -1
- data/lib/agentbill/wrappers.rb +384 -0
- data/lib/agentbill.rb +288 -52
- metadata +17 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# AgentBill Customers Resource - CRUD operations
|
|
2
|
+
#
|
|
3
|
+
# Example:
|
|
4
|
+
# ab = AgentBill::Client.init(api_key: 'agb_...')
|
|
5
|
+
#
|
|
6
|
+
# # List customers
|
|
7
|
+
# result = ab.customers.list(limit: 10)
|
|
8
|
+
#
|
|
9
|
+
# # Create customer
|
|
10
|
+
# customer = ab.customers.create(name: 'Acme', email: 'a@b.com')
|
|
11
|
+
#
|
|
12
|
+
# # Get customer by ID
|
|
13
|
+
# customer = ab.customers.get('cust-123')
|
|
14
|
+
#
|
|
15
|
+
# # Update customer
|
|
16
|
+
# customer = ab.customers.update('cust-123', name: 'Acme Inc')
|
|
17
|
+
#
|
|
18
|
+
# # Delete customer
|
|
19
|
+
# ab.customers.delete('cust-123')
|
|
20
|
+
|
|
21
|
+
require 'net/http'
|
|
22
|
+
require 'json'
|
|
23
|
+
|
|
24
|
+
module AgentBill
|
|
25
|
+
class CustomersResource
|
|
26
|
+
def initialize(config)
|
|
27
|
+
@config = config
|
|
28
|
+
@base_url = config[:base_url] || 'https://api.agentbill.io'
|
|
29
|
+
@api_key = config[:api_key]
|
|
30
|
+
@debug = config[:debug] || false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# List customers with pagination and search
|
|
34
|
+
#
|
|
35
|
+
# @param limit [Integer] Number of customers to return (default: 50)
|
|
36
|
+
# @param offset [Integer] Offset for pagination (default: 0)
|
|
37
|
+
# @param search [String, nil] Search term to filter by name or email
|
|
38
|
+
# @return [Hash] Hash with 'data' (list of customers) and 'pagination' info
|
|
39
|
+
def list(limit: 50, offset: 0, search: nil)
|
|
40
|
+
uri = URI("#{@base_url}/functions/v1/api-customers")
|
|
41
|
+
params = { limit: limit.to_s, offset: offset.to_s }
|
|
42
|
+
params[:search] = search if search
|
|
43
|
+
uri.query = URI.encode_www_form(params)
|
|
44
|
+
|
|
45
|
+
response = make_request(:get, uri)
|
|
46
|
+
handle_response(response, 'list customers')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Create a new customer
|
|
50
|
+
#
|
|
51
|
+
# @param name [String] Customer name (required)
|
|
52
|
+
# @param email [String] Customer email (required)
|
|
53
|
+
# @param phone [String, nil] Phone number
|
|
54
|
+
# @param website [String, nil] Website URL
|
|
55
|
+
# @param external_id [String, nil] Your external ID for this customer
|
|
56
|
+
# @param metadata [Hash, nil] Additional metadata
|
|
57
|
+
# @return [Hash] Created customer object
|
|
58
|
+
def create(name:, email:, phone: nil, website: nil, external_id: nil, metadata: nil)
|
|
59
|
+
uri = URI("#{@base_url}/functions/v1/api-customers")
|
|
60
|
+
|
|
61
|
+
payload = { name: name, email: email }
|
|
62
|
+
payload[:phone] = phone if phone
|
|
63
|
+
payload[:website] = website if website
|
|
64
|
+
payload[:external_id] = external_id if external_id
|
|
65
|
+
payload[:metadata] = metadata if metadata
|
|
66
|
+
|
|
67
|
+
response = make_request(:post, uri, payload)
|
|
68
|
+
handle_response(response, 'create customer')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get a customer by ID or external_id
|
|
72
|
+
#
|
|
73
|
+
# @param customer_id [String] Customer UUID or external_id
|
|
74
|
+
# @param by_external_id [Boolean] If true, treat customer_id as external_id
|
|
75
|
+
# @return [Hash] Customer object
|
|
76
|
+
def get(customer_id, by_external_id: false)
|
|
77
|
+
uri = URI("#{@base_url}/functions/v1/api-customers")
|
|
78
|
+
params = by_external_id ? { external_id: customer_id } : { id: customer_id }
|
|
79
|
+
uri.query = URI.encode_www_form(params)
|
|
80
|
+
|
|
81
|
+
response = make_request(:get, uri)
|
|
82
|
+
result = handle_response(response, 'get customer')
|
|
83
|
+
|
|
84
|
+
if result['data'].is_a?(Array)
|
|
85
|
+
raise "Customer not found" if result['data'].empty?
|
|
86
|
+
return result['data'][0]
|
|
87
|
+
end
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Update a customer
|
|
92
|
+
#
|
|
93
|
+
# @param customer_id [String] Customer UUID
|
|
94
|
+
# @param name [String, nil] New name
|
|
95
|
+
# @param email [String, nil] New email
|
|
96
|
+
# @param phone [String, nil] New phone
|
|
97
|
+
# @param website [String, nil] New website
|
|
98
|
+
# @param metadata [Hash, nil] New metadata (replaces existing)
|
|
99
|
+
# @return [Hash] Updated customer object
|
|
100
|
+
def update(customer_id, name: nil, email: nil, phone: nil, website: nil, metadata: nil)
|
|
101
|
+
uri = URI("#{@base_url}/functions/v1/api-customers")
|
|
102
|
+
|
|
103
|
+
payload = { id: customer_id }
|
|
104
|
+
payload[:name] = name if name
|
|
105
|
+
payload[:email] = email if email
|
|
106
|
+
payload[:phone] = phone if phone
|
|
107
|
+
payload[:website] = website if website
|
|
108
|
+
payload[:metadata] = metadata if metadata
|
|
109
|
+
|
|
110
|
+
response = make_request(:patch, uri, payload)
|
|
111
|
+
handle_response(response, 'update customer')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Delete a customer
|
|
115
|
+
#
|
|
116
|
+
# @param customer_id [String] Customer UUID to delete
|
|
117
|
+
def delete(customer_id)
|
|
118
|
+
uri = URI("#{@base_url}/functions/v1/api-customers")
|
|
119
|
+
payload = { id: customer_id }
|
|
120
|
+
|
|
121
|
+
response = make_request(:delete, uri, payload)
|
|
122
|
+
handle_response(response, 'delete customer')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def make_request(method, uri, payload = nil)
|
|
128
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
129
|
+
http.use_ssl = true
|
|
130
|
+
http.read_timeout = 30
|
|
131
|
+
|
|
132
|
+
case method
|
|
133
|
+
when :get
|
|
134
|
+
request = Net::HTTP::Get.new(uri)
|
|
135
|
+
when :post
|
|
136
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
137
|
+
request.body = payload.to_json if payload
|
|
138
|
+
when :patch
|
|
139
|
+
request = Net::HTTP::Patch.new(uri.path)
|
|
140
|
+
request.body = payload.to_json if payload
|
|
141
|
+
when :delete
|
|
142
|
+
request = Net::HTTP::Delete.new(uri.path)
|
|
143
|
+
request.body = payload.to_json if payload
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
request['Content-Type'] = 'application/json'
|
|
147
|
+
request['X-API-Key'] = @api_key
|
|
148
|
+
|
|
149
|
+
http.request(request)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def handle_response(response, operation)
|
|
153
|
+
body = response.body ? JSON.parse(response.body) : {}
|
|
154
|
+
|
|
155
|
+
unless %w[200 201 204].include?(response.code)
|
|
156
|
+
error_msg = body['error'] || "Failed to #{operation}"
|
|
157
|
+
raise error_msg
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
puts "[AgentBill] #{operation} successful" if @debug
|
|
161
|
+
body
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# AgentBill Distributed Tracing for Ruby
|
|
2
|
+
#
|
|
3
|
+
# Functions for cross-service trace propagation and correlation.
|
|
4
|
+
# This is the SINGLE SOURCE OF TRUTH for trace context in the Ruby SDK.
|
|
5
|
+
|
|
6
|
+
module AgentBill
|
|
7
|
+
module Distributed
|
|
8
|
+
# Thread-local storage for trace context
|
|
9
|
+
@trace_context = {}
|
|
10
|
+
@tracing_token = nil
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Get the current trace context (trace_id, span_id)
|
|
14
|
+
# This is the SINGLE SOURCE OF TRUTH used by the tracer.
|
|
15
|
+
def get_trace_context
|
|
16
|
+
Thread.current[:agentbill_trace_context]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Set the trace context manually.
|
|
20
|
+
# Called by the tracer after starting a span.
|
|
21
|
+
def set_trace_context(trace_id, span_id)
|
|
22
|
+
Thread.current[:agentbill_trace_context] = {
|
|
23
|
+
trace_id: trace_id,
|
|
24
|
+
span_id: span_id
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Clear the current trace context
|
|
29
|
+
def clear_trace_context
|
|
30
|
+
Thread.current[:agentbill_trace_context] = nil
|
|
31
|
+
Thread.current[:agentbill_tracing_token] = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get the current tracing token
|
|
35
|
+
def get_tracing_token
|
|
36
|
+
Thread.current[:agentbill_tracing_token]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate a new tracing token for distributed trace propagation
|
|
40
|
+
def generate_tracing_token
|
|
41
|
+
ctx = get_trace_context
|
|
42
|
+
random_suffix = SecureRandom.hex(4)
|
|
43
|
+
|
|
44
|
+
if ctx
|
|
45
|
+
token = "agentbill-v1-#{ctx[:trace_id]}-#{ctx[:span_id]}-#{random_suffix}"
|
|
46
|
+
else
|
|
47
|
+
trace_id = SecureRandom.hex(16)
|
|
48
|
+
span_id = SecureRandom.hex(8)
|
|
49
|
+
set_trace_context(trace_id, span_id)
|
|
50
|
+
token = "agentbill-v1-#{trace_id}-#{span_id}-#{random_suffix}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Thread.current[:agentbill_tracing_token] = token
|
|
54
|
+
token
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Set the tracing token received from an upstream service
|
|
58
|
+
def set_tracing_token(token)
|
|
59
|
+
return false unless token&.start_with?('agentbill-v1-')
|
|
60
|
+
|
|
61
|
+
parts = token.split('-')
|
|
62
|
+
return false if parts.length < 5
|
|
63
|
+
|
|
64
|
+
trace_id = parts[2]
|
|
65
|
+
span_id = parts[3]
|
|
66
|
+
|
|
67
|
+
set_trace_context(trace_id, span_id)
|
|
68
|
+
Thread.current[:agentbill_tracing_token] = token
|
|
69
|
+
true
|
|
70
|
+
rescue
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get HTTP headers for trace propagation
|
|
75
|
+
def propagate_trace_headers
|
|
76
|
+
headers = {}
|
|
77
|
+
|
|
78
|
+
token = get_tracing_token
|
|
79
|
+
headers['X-AgentBill-Trace'] = token if token
|
|
80
|
+
|
|
81
|
+
ctx = get_trace_context
|
|
82
|
+
if ctx
|
|
83
|
+
headers['traceparent'] = "00-#{ctx[:trace_id]}-#{ctx[:span_id]}-01"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
headers
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Extract trace context from incoming HTTP headers
|
|
90
|
+
def extract_trace_from_headers(headers)
|
|
91
|
+
# Try AgentBill token first
|
|
92
|
+
token = headers['X-AgentBill-Trace'] || headers['x-agentbill-trace']
|
|
93
|
+
return true if token && set_tracing_token(token)
|
|
94
|
+
|
|
95
|
+
# Try W3C traceparent
|
|
96
|
+
traceparent = headers['traceparent'] || headers['Traceparent']
|
|
97
|
+
if traceparent
|
|
98
|
+
parts = traceparent.split('-')
|
|
99
|
+
if parts.length >= 3
|
|
100
|
+
set_trace_context(parts[1], parts[2])
|
|
101
|
+
return true
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# AgentBill Exceptions
|
|
4
|
+
#
|
|
5
|
+
# Custom exception classes for Cost Guard protection and error handling.
|
|
6
|
+
|
|
7
|
+
module AgentBill
|
|
8
|
+
# Base class for all AgentBill errors
|
|
9
|
+
class AgentBillError < StandardError
|
|
10
|
+
attr_reader :details
|
|
11
|
+
|
|
12
|
+
def initialize(message, details = {})
|
|
13
|
+
@details = details
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Raised when a request would exceed the configured budget limits.
|
|
19
|
+
#
|
|
20
|
+
# This is thrown by the context manager or wrapper when ai-cost-guard-router
|
|
21
|
+
# returns allowed=false due to budget constraints.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# begin
|
|
25
|
+
# AgentBill.tracing(customer_id: 'cust-123', daily_budget: 10.00) do |ctx|
|
|
26
|
+
# response = openai.chat.completions.create(...)
|
|
27
|
+
# end
|
|
28
|
+
# rescue AgentBill::BudgetExceededError => e
|
|
29
|
+
# puts "Budget exceeded: #{e.reason}"
|
|
30
|
+
# # Handle gracefully - queue for later, show user message, etc.
|
|
31
|
+
# end
|
|
32
|
+
class BudgetExceededError < AgentBillError
|
|
33
|
+
attr_reader :reason, :code
|
|
34
|
+
|
|
35
|
+
def initialize(reason, details = {})
|
|
36
|
+
@reason = reason
|
|
37
|
+
@code = 'BUDGET_EXCEEDED'
|
|
38
|
+
super("Budget exceeded: #{reason}", details)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Raised when a request would exceed rate limits.
|
|
43
|
+
class RateLimitExceededError < AgentBillError
|
|
44
|
+
attr_reader :reason, :code
|
|
45
|
+
|
|
46
|
+
def initialize(reason, details = {})
|
|
47
|
+
@reason = reason
|
|
48
|
+
@code = 'RATE_LIMIT_EXCEEDED'
|
|
49
|
+
super("Rate limit exceeded: #{reason}", details)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Raised when a request violates a Cost Guard policy.
|
|
54
|
+
class PolicyViolationError < AgentBillError
|
|
55
|
+
attr_reader :reason, :code
|
|
56
|
+
|
|
57
|
+
def initialize(reason, details = {})
|
|
58
|
+
@reason = reason
|
|
59
|
+
@code = 'POLICY_VIOLATION'
|
|
60
|
+
super("Policy violation: #{reason}", details)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Raised when input validation fails.
|
|
65
|
+
class ValidationError < AgentBillError
|
|
66
|
+
attr_reader :field, :code
|
|
67
|
+
|
|
68
|
+
def initialize(message, field = nil)
|
|
69
|
+
@field = field
|
|
70
|
+
@code = 'VALIDATION_ERROR'
|
|
71
|
+
super("Validation error: #{message}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Raised when tracing operations fail.
|
|
76
|
+
class TracingError < AgentBillError
|
|
77
|
+
attr_reader :code
|
|
78
|
+
|
|
79
|
+
def initialize(message, details = {})
|
|
80
|
+
@code = 'TRACING_ERROR'
|
|
81
|
+
super("Tracing error: #{message}", details)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Ollama Wrapper for AgentBill Ruby SDK (Local, $0 cost)
|
|
2
|
+
require_relative 'pricing'
|
|
3
|
+
|
|
4
|
+
module AgentBill
|
|
5
|
+
class OllamaWrapper
|
|
6
|
+
def initialize(client, config, tracer)
|
|
7
|
+
@client = client
|
|
8
|
+
@config = config
|
|
9
|
+
@tracer = tracer
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def wrap
|
|
13
|
+
wrap_chat
|
|
14
|
+
wrap_generate
|
|
15
|
+
@client
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def wrap_chat
|
|
21
|
+
original_chat = @client.method(:chat)
|
|
22
|
+
config = @config
|
|
23
|
+
tracer = @tracer
|
|
24
|
+
|
|
25
|
+
@client.define_singleton_method(:chat) do |params|
|
|
26
|
+
model = params[:model] || 'llama2'
|
|
27
|
+
|
|
28
|
+
start_time = Time.now
|
|
29
|
+
span = tracer.start_span('ollama.chat', {
|
|
30
|
+
# ✅ OTEL GenAI compliant attributes
|
|
31
|
+
'gen_ai.system' => 'ollama',
|
|
32
|
+
'gen_ai.request.model' => model,
|
|
33
|
+
'gen_ai.operation.name' => 'chat',
|
|
34
|
+
# ⚠️ Backward compatibility
|
|
35
|
+
'model' => model,
|
|
36
|
+
'provider' => 'ollama'
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
response = original_chat.call(params)
|
|
41
|
+
latency = ((Time.now - start_time) * 1000).round
|
|
42
|
+
|
|
43
|
+
input_tokens = response[:prompt_eval_count] || 0
|
|
44
|
+
output_tokens = response[:eval_count] || 0
|
|
45
|
+
|
|
46
|
+
track_usage(model, 'ollama', input_tokens, output_tokens, latency, 0, config)
|
|
47
|
+
|
|
48
|
+
span.set_attributes({
|
|
49
|
+
# ✅ OTEL GenAI compliant attributes
|
|
50
|
+
'gen_ai.usage.input_tokens' => input_tokens,
|
|
51
|
+
'gen_ai.usage.output_tokens' => output_tokens,
|
|
52
|
+
# ⚠️ Backward compatibility
|
|
53
|
+
'response.prompt_eval_count' => input_tokens,
|
|
54
|
+
'response.eval_count' => output_tokens,
|
|
55
|
+
'latency_ms' => latency,
|
|
56
|
+
'cost' => 0
|
|
57
|
+
})
|
|
58
|
+
span.set_status(0)
|
|
59
|
+
|
|
60
|
+
puts "[AgentBill] ✓ Ollama chat tracked (local, $0.00)" if config[:debug]
|
|
61
|
+
response
|
|
62
|
+
rescue => e
|
|
63
|
+
span.set_status(1, e.message)
|
|
64
|
+
raise
|
|
65
|
+
ensure
|
|
66
|
+
span.finish
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def wrap_generate
|
|
72
|
+
original_generate = @client.method(:generate)
|
|
73
|
+
config = @config
|
|
74
|
+
tracer = @tracer
|
|
75
|
+
|
|
76
|
+
@client.define_singleton_method(:generate) do |params|
|
|
77
|
+
model = params[:model] || 'llama2'
|
|
78
|
+
|
|
79
|
+
start_time = Time.now
|
|
80
|
+
span = tracer.start_span('ollama.generate', {
|
|
81
|
+
# ✅ OTEL GenAI compliant attributes
|
|
82
|
+
'gen_ai.system' => 'ollama',
|
|
83
|
+
'gen_ai.request.model' => model,
|
|
84
|
+
'gen_ai.operation.name' => 'text_completion',
|
|
85
|
+
# ⚠️ Backward compatibility
|
|
86
|
+
'model' => model,
|
|
87
|
+
'provider' => 'ollama'
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
response = original_generate.call(params)
|
|
92
|
+
latency = ((Time.now - start_time) * 1000).round
|
|
93
|
+
|
|
94
|
+
input_tokens = response[:prompt_eval_count] || 0
|
|
95
|
+
output_tokens = response[:eval_count] || 0
|
|
96
|
+
|
|
97
|
+
track_usage(model, 'ollama', input_tokens, output_tokens, latency, 0, config)
|
|
98
|
+
|
|
99
|
+
span.set_attributes({
|
|
100
|
+
# ✅ OTEL GenAI compliant attributes
|
|
101
|
+
'gen_ai.usage.input_tokens' => input_tokens,
|
|
102
|
+
'gen_ai.usage.output_tokens' => output_tokens,
|
|
103
|
+
# ⚠️ Backward compatibility
|
|
104
|
+
'response.prompt_eval_count' => input_tokens,
|
|
105
|
+
'response.eval_count' => output_tokens,
|
|
106
|
+
'latency_ms' => latency,
|
|
107
|
+
'cost' => 0
|
|
108
|
+
})
|
|
109
|
+
span.set_status(0)
|
|
110
|
+
|
|
111
|
+
puts "[AgentBill] ✓ Ollama generate tracked (local, $0.00)" if config[:debug]
|
|
112
|
+
response
|
|
113
|
+
rescue => e
|
|
114
|
+
span.set_status(1, e.message)
|
|
115
|
+
raise
|
|
116
|
+
ensure
|
|
117
|
+
span.finish
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def track_usage(model, provider, input_tokens, output_tokens, latency, cost, config)
|
|
123
|
+
uri = URI("#{config[:base_url] || 'https://api.agentbill.io'}/functions/v1/track-ai-usage")
|
|
124
|
+
|
|
125
|
+
payload = {
|
|
126
|
+
api_key: config[:api_key],
|
|
127
|
+
customer_id: config[:customer_id],
|
|
128
|
+
agent_id: config[:agent_id],
|
|
129
|
+
event_name: 'ai_request',
|
|
130
|
+
model: model,
|
|
131
|
+
provider: provider,
|
|
132
|
+
prompt_tokens: input_tokens,
|
|
133
|
+
completion_tokens: output_tokens,
|
|
134
|
+
latency_ms: latency,
|
|
135
|
+
cost: cost
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
140
|
+
http.use_ssl = true
|
|
141
|
+
http.read_timeout = 10
|
|
142
|
+
|
|
143
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
144
|
+
request['Content-Type'] = 'application/json'
|
|
145
|
+
request.body = payload.to_json
|
|
146
|
+
|
|
147
|
+
http.request(request)
|
|
148
|
+
rescue => e
|
|
149
|
+
puts "[AgentBill] Tracking failed: #{e.message}" if config[:debug]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|