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,283 @@
|
|
|
1
|
+
# AgentBill Orders Resource - CRUD operations with line items
|
|
2
|
+
#
|
|
3
|
+
# IMPORTANT: At least one agent must be associated with an order via
|
|
4
|
+
# agent_ids or agent_external_ids. Orders cannot be created without agents.
|
|
5
|
+
#
|
|
6
|
+
# Example:
|
|
7
|
+
# ab = AgentBill::Client.init(api_key: 'agb_...')
|
|
8
|
+
#
|
|
9
|
+
# # List orders
|
|
10
|
+
# result = ab.orders.list(limit: 10)
|
|
11
|
+
#
|
|
12
|
+
# # Create order (agent_ids or agent_external_ids REQUIRED)
|
|
13
|
+
# order = ab.orders.create(
|
|
14
|
+
# account_id: 'acct-123',
|
|
15
|
+
# name: 'Enterprise Q1',
|
|
16
|
+
# agent_ids: ['agent-uuid-1'] # REQUIRED
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# # Add line item
|
|
20
|
+
# ab.orders.add_line_item(
|
|
21
|
+
# order_id: order['id'],
|
|
22
|
+
# description: 'AI Usage',
|
|
23
|
+
# quantity: 1000,
|
|
24
|
+
# unit_price: 0.05,
|
|
25
|
+
# item_type: 'usage'
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# # Get order by ID
|
|
29
|
+
# order = ab.orders.get('order-123')
|
|
30
|
+
#
|
|
31
|
+
# # Update order
|
|
32
|
+
# order = ab.orders.update('order-123', status: 'active')
|
|
33
|
+
#
|
|
34
|
+
# # Delete order
|
|
35
|
+
# ab.orders.delete('order-123')
|
|
36
|
+
|
|
37
|
+
require 'net/http'
|
|
38
|
+
require 'json'
|
|
39
|
+
|
|
40
|
+
module AgentBill
|
|
41
|
+
# Valid item_type values for add_line_item:
|
|
42
|
+
# - usage: Variable consumption billing (API calls, tokens, requests)
|
|
43
|
+
# - platform_fee: Platform access and service charges
|
|
44
|
+
# - setup_fee: One-time onboarding and implementation costs
|
|
45
|
+
# - base_fee: Fixed recurring monthly/annual charges
|
|
46
|
+
# - overage: Usage exceeding included plan amounts
|
|
47
|
+
# - custom: Flexible billing for unique scenarios
|
|
48
|
+
# - flat_rate: Fixed-price service packages or retainers
|
|
49
|
+
# - product: Physical or digital product sales
|
|
50
|
+
# - service: Professional services and consulting
|
|
51
|
+
# - subscription: Recurring subscription billing
|
|
52
|
+
VALID_ITEM_TYPES = %w[usage platform_fee setup_fee base_fee overage custom product service subscription flat_rate].freeze
|
|
53
|
+
|
|
54
|
+
class OrdersResource
|
|
55
|
+
def initialize(config)
|
|
56
|
+
@config = config
|
|
57
|
+
@base_url = config[:base_url] || 'https://api.agentbill.io'
|
|
58
|
+
@api_key = config[:api_key]
|
|
59
|
+
@debug = config[:debug] || false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# List orders with pagination and filtering
|
|
63
|
+
#
|
|
64
|
+
# @param account_id [String, nil] Filter by account ID
|
|
65
|
+
# @param status [String, nil] Filter by status (draft, active, completed, cancelled)
|
|
66
|
+
# @param limit [Integer] Number of orders to return (default: 50)
|
|
67
|
+
# @param offset [Integer] Offset for pagination (default: 0)
|
|
68
|
+
# @param search [String, nil] Search term to filter by name or external_id
|
|
69
|
+
# @return [Hash] Hash with 'data' (list of orders) and 'pagination' info
|
|
70
|
+
def list(account_id: nil, status: nil, limit: 50, offset: 0, search: nil)
|
|
71
|
+
uri = URI("#{@base_url}/functions/v1/api-orders")
|
|
72
|
+
params = { limit: limit.to_s, offset: offset.to_s }
|
|
73
|
+
params[:search] = search if search
|
|
74
|
+
params[:account_id] = account_id if account_id
|
|
75
|
+
params[:status] = status if status
|
|
76
|
+
uri.query = URI.encode_www_form(params)
|
|
77
|
+
|
|
78
|
+
response = make_request(:get, uri)
|
|
79
|
+
handle_response(response, 'list orders')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Create a new order
|
|
83
|
+
#
|
|
84
|
+
# IMPORTANT: At least one agent must be specified via agent_ids or agent_external_ids.
|
|
85
|
+
# Orders cannot be created without agent association for proper billing attribution.
|
|
86
|
+
#
|
|
87
|
+
# @param account_id [String] Account (customer) UUID (required unless account_external_id provided)
|
|
88
|
+
# @param name [String] Order name (required)
|
|
89
|
+
# @param agent_ids [Array<String>, nil] List of agent UUIDs (required if agent_external_ids not provided)
|
|
90
|
+
# @param agent_external_ids [Array<String>, nil] List of agent external IDs (required if agent_ids not provided)
|
|
91
|
+
# @param external_id [String, nil] Your external ID for this order
|
|
92
|
+
# @param account_external_id [String, nil] External ID of the account
|
|
93
|
+
# @param billing_contact_id [String, nil] Contact UUID for billing
|
|
94
|
+
# @param billing_contact_external_id [String, nil] External ID of billing contact
|
|
95
|
+
# @param start_date [String, nil] Order start date (ISO format)
|
|
96
|
+
# @param end_date [String, nil] Order end date (ISO format)
|
|
97
|
+
# @param status [String, nil] Order status (draft, active, completed, cancelled)
|
|
98
|
+
# @param currency [String, nil] Currency code (default: USD)
|
|
99
|
+
# @param metadata [Hash, nil] Additional metadata
|
|
100
|
+
# @return [Hash] Created order object
|
|
101
|
+
# @raise [ArgumentError] If neither agent_ids nor agent_external_ids provided
|
|
102
|
+
def create(account_id:, name:, agent_ids: nil, agent_external_ids: nil, external_id: nil,
|
|
103
|
+
account_external_id: nil, billing_contact_id: nil, billing_contact_external_id: nil,
|
|
104
|
+
start_date: nil, end_date: nil, status: nil, currency: nil, metadata: nil)
|
|
105
|
+
# Validate agent requirement
|
|
106
|
+
if (agent_ids.nil? || agent_ids.empty?) && (agent_external_ids.nil? || agent_external_ids.empty?)
|
|
107
|
+
raise ArgumentError, 'At least one agent must be specified via agent_ids or agent_external_ids. ' \
|
|
108
|
+
'Orders cannot be created without agent association for billing attribution.'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
uri = URI("#{@base_url}/functions/v1/api-orders")
|
|
112
|
+
|
|
113
|
+
payload = { account_id: account_id, name: name }
|
|
114
|
+
payload[:agent_ids] = agent_ids if agent_ids&.any?
|
|
115
|
+
payload[:agent_external_ids] = agent_external_ids if agent_external_ids&.any?
|
|
116
|
+
payload[:external_id] = external_id if external_id
|
|
117
|
+
payload[:account_external_id] = account_external_id if account_external_id
|
|
118
|
+
payload[:billing_contact_id] = billing_contact_id if billing_contact_id
|
|
119
|
+
payload[:billing_contact_external_id] = billing_contact_external_id if billing_contact_external_id
|
|
120
|
+
payload[:start_date] = start_date if start_date
|
|
121
|
+
payload[:end_date] = end_date if end_date
|
|
122
|
+
payload[:status] = status if status
|
|
123
|
+
payload[:currency] = currency if currency
|
|
124
|
+
payload[:metadata] = metadata if metadata
|
|
125
|
+
|
|
126
|
+
response = make_request(:post, uri, payload)
|
|
127
|
+
handle_response(response, 'create order')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get an order by ID or external_id (includes line items)
|
|
131
|
+
#
|
|
132
|
+
# @param order_id [String] Order UUID or external_id
|
|
133
|
+
# @param by_external_id [Boolean] If true, treat order_id as external_id
|
|
134
|
+
# @return [Hash] Order object with order_line_items
|
|
135
|
+
def get(order_id, by_external_id: false)
|
|
136
|
+
uri = URI("#{@base_url}/functions/v1/api-orders")
|
|
137
|
+
params = by_external_id ? { external_id: order_id } : { id: order_id }
|
|
138
|
+
uri.query = URI.encode_www_form(params)
|
|
139
|
+
|
|
140
|
+
response = make_request(:get, uri)
|
|
141
|
+
handle_response(response, 'get order')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Update an order
|
|
145
|
+
#
|
|
146
|
+
# @param order_id [String] Order UUID
|
|
147
|
+
# @param name [String, nil] New name
|
|
148
|
+
# @param billing_contact_id [String, nil] New billing contact ID
|
|
149
|
+
# @param start_date [String, nil] New start date
|
|
150
|
+
# @param end_date [String, nil] New end date
|
|
151
|
+
# @param status [String, nil] New status
|
|
152
|
+
# @param currency [String, nil] New currency
|
|
153
|
+
# @param metadata [Hash, nil] New metadata (replaces existing)
|
|
154
|
+
# @return [Hash] Updated order object
|
|
155
|
+
def update(order_id, name: nil, billing_contact_id: nil, start_date: nil,
|
|
156
|
+
end_date: nil, status: nil, currency: nil, metadata: nil)
|
|
157
|
+
uri = URI("#{@base_url}/functions/v1/api-orders")
|
|
158
|
+
|
|
159
|
+
payload = { id: order_id }
|
|
160
|
+
payload[:name] = name if name
|
|
161
|
+
payload[:billing_contact_id] = billing_contact_id if billing_contact_id
|
|
162
|
+
payload[:start_date] = start_date if start_date
|
|
163
|
+
payload[:end_date] = end_date if end_date
|
|
164
|
+
payload[:status] = status if status
|
|
165
|
+
payload[:currency] = currency if currency
|
|
166
|
+
payload[:metadata] = metadata if metadata
|
|
167
|
+
|
|
168
|
+
response = make_request(:patch, uri, payload)
|
|
169
|
+
handle_response(response, 'update order')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Delete an order and its line items
|
|
173
|
+
#
|
|
174
|
+
# @param order_id [String] Order UUID to delete
|
|
175
|
+
def delete(order_id)
|
|
176
|
+
uri = URI("#{@base_url}/functions/v1/api-orders")
|
|
177
|
+
payload = { id: order_id }
|
|
178
|
+
|
|
179
|
+
response = make_request(:delete, uri, payload)
|
|
180
|
+
handle_response(response, 'delete order')
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Add a line item to an order
|
|
184
|
+
#
|
|
185
|
+
# @param order_id [String] Order UUID (required)
|
|
186
|
+
# @param description [String] Line item description (required)
|
|
187
|
+
# @param quantity [Numeric] Quantity (required)
|
|
188
|
+
# @param unit_price [Numeric] Price per unit (required)
|
|
189
|
+
# @param product_id [String, nil] Product reference ID
|
|
190
|
+
# @param product_name [String, nil] Product name
|
|
191
|
+
# @param product_sku [String, nil] Product SKU
|
|
192
|
+
# @param usage_metric [String, nil] Usage metric name
|
|
193
|
+
# @param usage_quantity [Numeric, nil] Usage quantity
|
|
194
|
+
# @param discount_type [String, nil] "percentage" or "fixed"
|
|
195
|
+
# @param discount_value [Numeric, nil] Discount amount/percentage
|
|
196
|
+
# @param tax_rate [Numeric, nil] Tax rate percentage
|
|
197
|
+
# @param notes [String, nil] Additional notes
|
|
198
|
+
# @param sort_order [Integer, nil] Display order
|
|
199
|
+
# @param agent_id [String, nil] Associated agent ID
|
|
200
|
+
# @param item_type [String, nil] Type: usage, platform_fee, setup_fee, base_fee, overage, custom, product, service, subscription
|
|
201
|
+
# @param metadata [Hash, nil] Additional metadata
|
|
202
|
+
# @return [Hash] Created line item object
|
|
203
|
+
def add_line_item(order_id:, description:, quantity:, unit_price:, product_id: nil,
|
|
204
|
+
product_name: nil, product_sku: nil, usage_metric: nil, usage_quantity: nil,
|
|
205
|
+
discount_type: nil, discount_value: nil, tax_rate: nil, notes: nil,
|
|
206
|
+
sort_order: nil, agent_id: nil, item_type: nil, metadata: nil)
|
|
207
|
+
uri = URI("#{@base_url}/functions/v1/api-orders?action=line-items")
|
|
208
|
+
|
|
209
|
+
payload = {
|
|
210
|
+
order_id: order_id,
|
|
211
|
+
description: description,
|
|
212
|
+
quantity: quantity,
|
|
213
|
+
unit_price: unit_price
|
|
214
|
+
}
|
|
215
|
+
payload[:product_id] = product_id if product_id
|
|
216
|
+
payload[:product_name] = product_name if product_name
|
|
217
|
+
payload[:product_sku] = product_sku if product_sku
|
|
218
|
+
payload[:usage_metric] = usage_metric if usage_metric
|
|
219
|
+
payload[:usage_quantity] = usage_quantity if usage_quantity
|
|
220
|
+
payload[:discount_type] = discount_type if discount_type
|
|
221
|
+
payload[:discount_value] = discount_value if discount_value
|
|
222
|
+
payload[:tax_rate] = tax_rate if tax_rate
|
|
223
|
+
payload[:notes] = notes if notes
|
|
224
|
+
payload[:sort_order] = sort_order if sort_order
|
|
225
|
+
payload[:agent_id] = agent_id if agent_id
|
|
226
|
+
payload[:item_type] = item_type if item_type
|
|
227
|
+
payload[:metadata] = metadata if metadata
|
|
228
|
+
|
|
229
|
+
response = make_request(:post, uri, payload)
|
|
230
|
+
handle_response(response, 'add line item')
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Remove a line item from an order
|
|
234
|
+
#
|
|
235
|
+
# @param line_item_id [String] Line item UUID to delete
|
|
236
|
+
def remove_line_item(line_item_id)
|
|
237
|
+
uri = URI("#{@base_url}/functions/v1/api-orders?action=line-items")
|
|
238
|
+
payload = { id: line_item_id }
|
|
239
|
+
|
|
240
|
+
response = make_request(:delete, uri, payload)
|
|
241
|
+
handle_response(response, 'remove line item')
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def make_request(method, uri, payload = nil)
|
|
247
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
248
|
+
http.use_ssl = true
|
|
249
|
+
http.read_timeout = 30
|
|
250
|
+
|
|
251
|
+
case method
|
|
252
|
+
when :get
|
|
253
|
+
request = Net::HTTP::Get.new(uri)
|
|
254
|
+
when :post
|
|
255
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
256
|
+
request.body = payload.to_json if payload
|
|
257
|
+
when :patch
|
|
258
|
+
request = Net::HTTP::Patch.new(uri.path)
|
|
259
|
+
request.body = payload.to_json if payload
|
|
260
|
+
when :delete
|
|
261
|
+
request = Net::HTTP::Delete.new(uri.request_uri)
|
|
262
|
+
request.body = payload.to_json if payload
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
request['Content-Type'] = 'application/json'
|
|
266
|
+
request['X-API-Key'] = @api_key
|
|
267
|
+
|
|
268
|
+
http.request(request)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def handle_response(response, operation)
|
|
272
|
+
body = response.body ? JSON.parse(response.body) : {}
|
|
273
|
+
|
|
274
|
+
unless %w[200 201 204].include?(response.code)
|
|
275
|
+
error_msg = body['error'] || "Failed to #{operation}"
|
|
276
|
+
raise error_msg
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
puts "[AgentBill] #{operation} successful" if @debug
|
|
280
|
+
body
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Perplexity AI Wrapper for AgentBill Ruby SDK
|
|
2
|
+
require_relative 'pricing'
|
|
3
|
+
|
|
4
|
+
module AgentBill
|
|
5
|
+
class PerplexityWrapper
|
|
6
|
+
def initialize(client, config, tracer)
|
|
7
|
+
@client = client
|
|
8
|
+
@config = config
|
|
9
|
+
@tracer = tracer
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def wrap
|
|
13
|
+
original_chat_create = @client.method(:chat_create)
|
|
14
|
+
config = @config
|
|
15
|
+
tracer = @tracer
|
|
16
|
+
|
|
17
|
+
@client.define_singleton_method(:chat_create) do |params|
|
|
18
|
+
model = params[:model] || 'llama-3.1-sonar-small-128k-online'
|
|
19
|
+
messages = params[:messages] || []
|
|
20
|
+
|
|
21
|
+
start_time = Time.now
|
|
22
|
+
span = tracer.start_span('perplexity.chat.completions.create', {
|
|
23
|
+
# ✅ OTEL GenAI compliant attributes
|
|
24
|
+
'gen_ai.system' => 'perplexity',
|
|
25
|
+
'gen_ai.request.model' => model,
|
|
26
|
+
'gen_ai.operation.name' => 'chat',
|
|
27
|
+
# ⚠️ Backward compatibility
|
|
28
|
+
'model' => model,
|
|
29
|
+
'provider' => 'perplexity'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
response = original_chat_create.call(params)
|
|
34
|
+
latency = ((Time.now - start_time) * 1000).round
|
|
35
|
+
|
|
36
|
+
input_tokens = response.dig(:usage, :prompt_tokens) || 0
|
|
37
|
+
output_tokens = response.dig(:usage, :completion_tokens) || 0
|
|
38
|
+
cost = Pricing.calculate_cost(model, input_tokens, output_tokens, 'perplexity')
|
|
39
|
+
|
|
40
|
+
track_usage(model, 'perplexity', input_tokens, output_tokens, latency, cost, config)
|
|
41
|
+
|
|
42
|
+
span.set_attributes({
|
|
43
|
+
# ✅ OTEL GenAI compliant attributes
|
|
44
|
+
'gen_ai.usage.input_tokens' => input_tokens,
|
|
45
|
+
'gen_ai.usage.output_tokens' => output_tokens,
|
|
46
|
+
'gen_ai.response.id' => response.dig(:id),
|
|
47
|
+
# ⚠️ Backward compatibility
|
|
48
|
+
'response.prompt_tokens' => input_tokens,
|
|
49
|
+
'response.completion_tokens' => output_tokens,
|
|
50
|
+
'latency_ms' => latency,
|
|
51
|
+
'cost' => cost
|
|
52
|
+
})
|
|
53
|
+
span.set_status(0)
|
|
54
|
+
|
|
55
|
+
puts "[AgentBill] ✓ Perplexity chat tracked: $#{format('%.6f', cost)}" if config[:debug]
|
|
56
|
+
response
|
|
57
|
+
rescue => e
|
|
58
|
+
span.set_status(1, e.message)
|
|
59
|
+
raise
|
|
60
|
+
ensure
|
|
61
|
+
span.finish
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@client
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def track_usage(model, provider, input_tokens, output_tokens, latency, cost, config)
|
|
71
|
+
uri = URI("#{config[:base_url] || 'https://api.agentbill.io'}/functions/v1/track-ai-usage")
|
|
72
|
+
|
|
73
|
+
payload = {
|
|
74
|
+
api_key: config[:api_key],
|
|
75
|
+
customer_id: config[:customer_id],
|
|
76
|
+
agent_id: config[:agent_id],
|
|
77
|
+
event_name: 'ai_request',
|
|
78
|
+
model: model,
|
|
79
|
+
provider: provider,
|
|
80
|
+
prompt_tokens: input_tokens,
|
|
81
|
+
completion_tokens: output_tokens,
|
|
82
|
+
latency_ms: latency,
|
|
83
|
+
cost: cost
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
88
|
+
http.use_ssl = true
|
|
89
|
+
http.read_timeout = 10
|
|
90
|
+
|
|
91
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
92
|
+
request['Content-Type'] = 'application/json'
|
|
93
|
+
request.body = payload.to_json
|
|
94
|
+
|
|
95
|
+
http.request(request)
|
|
96
|
+
rescue => e
|
|
97
|
+
puts "[AgentBill] Tracking failed: #{e.message}" if config[:debug]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# AgentBill SDK Pricing - LOCAL ESTIMATION ONLY
|
|
2
|
+
#
|
|
3
|
+
# IMPORTANT: Cost calculation is now 100% SERVER-SIDE
|
|
4
|
+
# The server uses the model_pricing database table (synced by sync-model-pricing).
|
|
5
|
+
# This module is kept ONLY for local estimation/display purposes.
|
|
6
|
+
# The AUTHORITATIVE cost is always calculated server-side.
|
|
7
|
+
|
|
8
|
+
module AgentBill
|
|
9
|
+
module Pricing
|
|
10
|
+
# Default fallback rates for local estimation only
|
|
11
|
+
DEFAULT_INPUT_PER_1M = 1.0
|
|
12
|
+
DEFAULT_OUTPUT_PER_1M = 2.0
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# LOCAL ESTIMATION ONLY - for display purposes.
|
|
16
|
+
# Actual cost is calculated server-side using model_pricing database.
|
|
17
|
+
def calculate_cost(model, input_tokens, output_tokens, provider = 'openai')
|
|
18
|
+
# Ollama is free
|
|
19
|
+
return 0.0 if provider == 'ollama' || model.to_s.start_with?('ollama/')
|
|
20
|
+
|
|
21
|
+
# Simple default estimate - server calculates actual cost
|
|
22
|
+
input_cost = (input_tokens.to_f / 1_000_000) * DEFAULT_INPUT_PER_1M
|
|
23
|
+
output_cost = (output_tokens.to_f / 1_000_000) * DEFAULT_OUTPUT_PER_1M
|
|
24
|
+
input_cost + output_cost
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# LOCAL ESTIMATION ONLY for image generation
|
|
28
|
+
def calculate_image_cost(model, size, quality = 'standard')
|
|
29
|
+
if model.include?('dall-e-3')
|
|
30
|
+
quality == 'hd' ? 0.08 : 0.04
|
|
31
|
+
else
|
|
32
|
+
0.02
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# LOCAL ESTIMATION ONLY for audio
|
|
37
|
+
def calculate_audio_cost(model, duration_seconds: 0, chars: 0)
|
|
38
|
+
if model == 'whisper-1'
|
|
39
|
+
(duration_seconds.to_f / 60.0) * 0.006
|
|
40
|
+
elsif model.start_with?('tts-')
|
|
41
|
+
(chars.to_f / 1_000_000) * 15.0
|
|
42
|
+
else
|
|
43
|
+
0.0
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def embedding_model?(model)
|
|
48
|
+
model.downcase.include?('embedding') || model.downcase.include?('embed')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module AgentBill
|
|
8
|
+
# Signal Types Resource - CRUD operations for signal type management
|
|
9
|
+
# v7.15.0: New resource for programmatic signal type management
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# client = AgentBill::Client.new(api_key: 'agb_...')
|
|
13
|
+
#
|
|
14
|
+
# # Create signal type
|
|
15
|
+
# st = client.signal_types.create(name: 'ai_completion', category: 'activity')
|
|
16
|
+
#
|
|
17
|
+
# # List all
|
|
18
|
+
# signal_types = client.signal_types.list
|
|
19
|
+
#
|
|
20
|
+
# # Get by ID
|
|
21
|
+
# st = client.signal_types.get(signal_type_id)
|
|
22
|
+
#
|
|
23
|
+
# # Update
|
|
24
|
+
# client.signal_types.update(signal_type_id, description: 'Updated')
|
|
25
|
+
#
|
|
26
|
+
# # Delete
|
|
27
|
+
# client.signal_types.delete(signal_type_id)
|
|
28
|
+
#
|
|
29
|
+
class SignalTypesResource
|
|
30
|
+
def initialize(config)
|
|
31
|
+
@base_url = config[:base_url] || 'https://api.agentbill.io'
|
|
32
|
+
@api_key = config[:api_key]
|
|
33
|
+
@debug = config[:debug] || false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# List signal types with optional filters
|
|
37
|
+
#
|
|
38
|
+
# @param category [String, nil] Filter by category ('activity' or 'outcome')
|
|
39
|
+
# @param include_global [Boolean] Include global signal types (default: true)
|
|
40
|
+
# @return [Hash] Response with :data array and :summary
|
|
41
|
+
def list(category: nil, include_global: true)
|
|
42
|
+
uri = URI("#{@base_url}/functions/v1/api-signal-types")
|
|
43
|
+
params = {}
|
|
44
|
+
params[:category] = category if category
|
|
45
|
+
params[:include_global] = 'false' unless include_global
|
|
46
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
47
|
+
|
|
48
|
+
response = make_request(:get, uri)
|
|
49
|
+
handle_response(response, 'list signal types')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Create a new signal type
|
|
53
|
+
#
|
|
54
|
+
# @param name [String] Signal type name (required)
|
|
55
|
+
# @param category [String] Category - 'activity' or 'outcome' (default: 'activity')
|
|
56
|
+
# @param description [String, nil] Description
|
|
57
|
+
# @param unit [String, nil] Unit of measurement
|
|
58
|
+
# @param default_value [Float, nil] Default value
|
|
59
|
+
# @param external_id [String, nil] Your external ID
|
|
60
|
+
# @param expected_metrics [Hash, nil] Expected metrics configuration
|
|
61
|
+
# @param revenue_formula [String, nil] Revenue formula
|
|
62
|
+
# @param validation_rules [Hash, nil] Validation rules
|
|
63
|
+
# @return [Hash] Created signal type
|
|
64
|
+
def create(name:, category: 'activity', description: nil, unit: nil, default_value: nil,
|
|
65
|
+
external_id: nil, expected_metrics: nil, revenue_formula: nil, validation_rules: nil)
|
|
66
|
+
payload = { name: name, category: category }
|
|
67
|
+
payload[:description] = description if description
|
|
68
|
+
payload[:unit] = unit if unit
|
|
69
|
+
payload[:default_value] = default_value if default_value
|
|
70
|
+
payload[:external_id] = external_id if external_id
|
|
71
|
+
payload[:expected_metrics] = expected_metrics if expected_metrics
|
|
72
|
+
payload[:revenue_formula] = revenue_formula if revenue_formula
|
|
73
|
+
payload[:validation_rules] = validation_rules if validation_rules
|
|
74
|
+
|
|
75
|
+
uri = URI("#{@base_url}/functions/v1/api-signal-types")
|
|
76
|
+
response = make_request(:post, uri, payload)
|
|
77
|
+
handle_response(response, 'create signal type')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get a signal type by ID or external_id
|
|
81
|
+
#
|
|
82
|
+
# @param signal_type_id [String] Signal type UUID or external_id
|
|
83
|
+
# @param by_external_id [Boolean] If true, treat signal_type_id as external_id
|
|
84
|
+
# @return [Hash] Signal type object
|
|
85
|
+
def get(signal_type_id, by_external_id: false)
|
|
86
|
+
uri = URI("#{@base_url}/functions/v1/api-signal-types")
|
|
87
|
+
param = by_external_id ? :external_id : :id
|
|
88
|
+
uri.query = URI.encode_www_form({ param => signal_type_id })
|
|
89
|
+
|
|
90
|
+
response = make_request(:get, uri)
|
|
91
|
+
handle_response(response, 'get signal type')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Update a signal type
|
|
95
|
+
#
|
|
96
|
+
# @param signal_type_id [String] Signal type UUID
|
|
97
|
+
# @param name [String, nil] New name
|
|
98
|
+
# @param description [String, nil] New description
|
|
99
|
+
# @param category [String, nil] New category
|
|
100
|
+
# @param unit [String, nil] New unit
|
|
101
|
+
# @param default_value [Float, nil] New default value
|
|
102
|
+
# @param external_id [String, nil] New external ID
|
|
103
|
+
# @param expected_metrics [Hash, nil] New expected metrics
|
|
104
|
+
# @param revenue_formula [String, nil] New revenue formula
|
|
105
|
+
# @param validation_rules [Hash, nil] New validation rules
|
|
106
|
+
# @param is_active [Boolean, nil] Active status
|
|
107
|
+
# @return [Hash] Updated signal type
|
|
108
|
+
def update(signal_type_id, name: nil, description: nil, category: nil, unit: nil,
|
|
109
|
+
default_value: nil, external_id: nil, expected_metrics: nil,
|
|
110
|
+
revenue_formula: nil, validation_rules: nil, is_active: nil)
|
|
111
|
+
payload = { id: signal_type_id }
|
|
112
|
+
payload[:name] = name if name
|
|
113
|
+
payload[:description] = description if description
|
|
114
|
+
payload[:category] = category if category
|
|
115
|
+
payload[:unit] = unit if unit
|
|
116
|
+
payload[:default_value] = default_value if default_value
|
|
117
|
+
payload[:external_id] = external_id if external_id
|
|
118
|
+
payload[:expected_metrics] = expected_metrics if expected_metrics
|
|
119
|
+
payload[:revenue_formula] = revenue_formula if revenue_formula
|
|
120
|
+
payload[:validation_rules] = validation_rules if validation_rules
|
|
121
|
+
payload[:is_active] = is_active unless is_active.nil?
|
|
122
|
+
|
|
123
|
+
uri = URI("#{@base_url}/functions/v1/api-signal-types")
|
|
124
|
+
response = make_request(:patch, uri, payload)
|
|
125
|
+
handle_response(response, 'update signal type')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Delete a signal type
|
|
129
|
+
#
|
|
130
|
+
# @param signal_type_id [String] Signal type UUID to delete
|
|
131
|
+
# @return [Hash] Deletion result
|
|
132
|
+
def delete(signal_type_id)
|
|
133
|
+
uri = URI("#{@base_url}/functions/v1/api-signal-types")
|
|
134
|
+
response = make_request(:delete, uri, { id: signal_type_id })
|
|
135
|
+
handle_response(response, 'delete signal type')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def make_request(method, uri, payload = nil)
|
|
141
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
142
|
+
http.use_ssl = true
|
|
143
|
+
|
|
144
|
+
request = case method
|
|
145
|
+
when :get
|
|
146
|
+
Net::HTTP::Get.new(uri)
|
|
147
|
+
when :post
|
|
148
|
+
Net::HTTP::Post.new(uri)
|
|
149
|
+
when :patch
|
|
150
|
+
Net::HTTP::Patch.new(uri)
|
|
151
|
+
when :delete
|
|
152
|
+
Net::HTTP::Delete.new(uri)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
request['Content-Type'] = 'application/json'
|
|
156
|
+
request['X-API-Key'] = @api_key
|
|
157
|
+
request.body = payload.to_json if payload
|
|
158
|
+
|
|
159
|
+
puts "[AgentBill] #{method.upcase} #{uri}" if @debug
|
|
160
|
+
|
|
161
|
+
http.request(request)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def handle_response(response, operation)
|
|
165
|
+
body = response.body ? JSON.parse(response.body) : {}
|
|
166
|
+
|
|
167
|
+
case response.code.to_i
|
|
168
|
+
when 200, 201, 204
|
|
169
|
+
body
|
|
170
|
+
when 404
|
|
171
|
+
raise "Signal type not found"
|
|
172
|
+
when 409
|
|
173
|
+
raise "Signal type already exists: #{body['error']}"
|
|
174
|
+
else
|
|
175
|
+
raise "Failed to #{operation}: #{body['error'] || response.body}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|