agentbill-sdk 7.17.0 → 9.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55eed0a2b3bf82cdd116d56d4fdd3f41c8ab60166be0d456d1a5791a2bf12dbd
4
- data.tar.gz: aae409ee8bd40d1ba774364ce570c6d5e4da4ad81847dc466433e755a2dc0e7d
3
+ metadata.gz: 0ee9e7fe3afb95e290b137974dadcb40147cc4b3855985688bf5ece9c037930b
4
+ data.tar.gz: 52fdd0c0634edc3bd36425244a0fc923e2b820bfced8fe8a14ace7538e55225d
5
5
  SHA512:
6
- metadata.gz: 5ca68a5403e625c73793d0d26b68795b56660f348e70d49b977981dc78f46429804cc25a736e1146763566472c78e8749263c1e42ba36d36670b18bbc709ead7
7
- data.tar.gz: 831a7b5f9170db9a40b52411771a992a07d683c73fc31a350c69e1070d37f6007d3e44902b5c503dd254aa08e21f87f68ea7ac69965025900f9de73cabeb490b
6
+ metadata.gz: c567fa89b467b00d36f1df21ac752ea772d6c333af73df20f4ea986739acc2490e57d52568d1604a8b4067588f1796d7e94ae2e7cf3ee3ecfca71c6cdfe4c5c2
7
+ data.tar.gz: 3a18a47c140ed7eb1d6a46f5ee8ddc04ab91ce822bae23f7f8dcecbadf444b9b80f95f83f5006e56ddaa6cd468ddd7bad3b3940ffc030fc92116e8341b14a99b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.19.3] - 2026-02-24
9
+
10
+ ### Fixed
11
+ - **OTLP Normalizer**: `company.id` → `agentbill.company_id` resource attribute — fixes collector rejection
12
+
13
+ ## [7.19.2] - 2026-02-24
14
+
15
+ ### Fixed
16
+ - **M365 Seeder v1.1.0** — 3 critical fixes under Shared OTLP Normalizer topic:
17
+ 1. **Identity Resolution (0% → 100%)**: Auto-provisions customers before seeding
18
+ 2. **Valid Trace/Span IDs**: Uses `generateDedupFromSource()` — eliminates normalizer repair warnings
19
+ 3. **Batched Collector Calls**: 50 spans/batch with 500ms delay — fixes 504 timeouts
20
+
21
+ ## [7.19.1] - 2026-02-24
22
+
23
+ ### Added
24
+ - **Version Alignment**: Aligned with Python SDK v7.19.1 — M365 Copilot test data seeder for pipeline validation without enterprise account
25
+
26
+ ## [7.19.0] - 2026-02-24
27
+
28
+ ### Changed
29
+ - **Version Alignment**: Aligned with Python SDK v7.19.0 shared OTLP normalizer and connector migration
30
+
8
31
  ## [7.6.3] - 2025-12-17
9
32
 
10
33
  ### Changed
@@ -60,7 +83,7 @@ agentbill.track_signal(
60
83
  - **CRITICAL**: Fixed authentication header to use `X-API-Key` instead of `Authorization: Bearer` format
61
84
  - Updated `track_signal()` method in agentbill.rb to use correct authentication
62
85
  - Updated `flush()` method in tracer.rb to use correct authentication
63
- - Signals now properly authenticate with record-signals and otel-collector edge functions
86
+ - Signals now properly authenticate with the unified OTEL pipeline (otel-collector)
64
87
 
65
88
  ## [2.0.1] - 2025-10-25
66
89
 
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Canonical attribute constants for OTEL span attributes.
4
+ #
5
+ # Single source of truth for all attribute keys used across the SDK.
6
+ # Prevents string drift between signals.rb, tracer.rb, and wrapper code.
7
+ #
8
+ # v9.5.5: Added multi-modality attributes (R3 alignment with Python/TS/Go).
9
+
10
+ module AgentBill
11
+ module Attributes
12
+ # Core signal attributes
13
+ ATTR_EVENT_NAME = 'agentbill.event_name'
14
+ ATTR_IS_BUSINESS_EVENT = 'agentbill.is_business_event'
15
+ ATTR_IDEMPOTENCY_KEY = 'agentbill.idempotency_key'
16
+ ATTR_DATA_SOURCE = 'agentbill.data_source'
17
+
18
+ # Identity
19
+ ATTR_CUSTOMER_ID = 'agentbill.customer_id'
20
+ ATTR_AGENT_ID = 'agentbill.agent_id'
21
+
22
+ # Business data
23
+ ATTR_REVENUE = 'agentbill.revenue'
24
+ ATTR_CURRENCY = 'agentbill.currency'
25
+ ATTR_EVENT_TYPE = 'agentbill.event_type'
26
+ ATTR_EVENT_VALUE = 'agentbill.event_value'
27
+
28
+ # Linking
29
+ ATTR_SESSION_ID = 'agentbill.session_id'
30
+ ATTR_ORDER_ID = 'agentbill.order_id'
31
+ ATTR_ORDER_EXTERNAL_ID = 'agentbill.order_external_id'
32
+ ATTR_PARENT_SPAN_ID = 'agentbill.parent_span_id'
33
+ ATTR_METADATA = 'agentbill.metadata'
34
+
35
+ # AI telemetry (OpenTelemetry Semantic Conventions)
36
+ ATTR_MODEL = 'gen_ai.request.model'
37
+ ATTR_PROVIDER = 'gen_ai.system'
38
+ ATTR_PROMPT_TOKENS = 'gen_ai.usage.prompt_tokens'
39
+ ATTR_COMPLETION_TOKENS = 'gen_ai.usage.completion_tokens'
40
+ ATTR_TOTAL_TOKENS = 'gen_ai.usage.total_tokens'
41
+ ATTR_LATENCY_MS = 'agentbill.latency_ms'
42
+ ATTR_PROMPT_HASH = 'agentbill.prompt_hash'
43
+
44
+ # Modality / operation name (OTEL Semantic Convention: gen_ai.operation.name)
45
+ # Official values: chat, embeddings, text_completion, create_image, audio_speech, audio_transcription
46
+ ATTR_OPERATION_NAME = 'gen_ai.operation.name'
47
+
48
+ # Image generation — request params (pricing tier) vs usage (quantity billed)
49
+ ATTR_IMAGE_SIZE = 'gen_ai.request.image_size' # e.g. "1024x1024" — determines price tier
50
+ ATTR_IMAGE_QUALITY = 'gen_ai.request.image_quality' # e.g. "hd", "standard" — determines price tier
51
+ ATTR_IMAGE_COUNT = 'gen_ai.usage.image_count' # actual images generated — quantity billed
52
+
53
+ # Audio — request params vs usage
54
+ ATTR_AUDIO_INPUT_FORMAT = 'gen_ai.request.audio_input_format'
55
+ ATTR_AUDIO_OUTPUT_FORMAT = 'gen_ai.request.audio_output_format'
56
+ ATTR_AUDIO_DURATION_SECONDS = 'gen_ai.usage.audio_duration_seconds'
57
+
58
+ # TTS — request params vs usage
59
+ ATTR_TTS_VOICE = 'gen_ai.request.tts_voice'
60
+ ATTR_TTS_CHARACTERS = 'gen_ai.usage.characters'
61
+
62
+ # Embedding — request params vs usage
63
+ ATTR_EMBEDDING_DIMENSIONS = 'gen_ai.request.embedding_dimensions'
64
+ ATTR_EMBEDDING_INPUT_COUNT = 'gen_ai.usage.embedding_input_count'
65
+
66
+ # Cache attributes
67
+ ATTR_CACHE_HIT = 'agentbill.cache_hit'
68
+ ATTR_FROM_CACHE = 'agentbill.from_cache'
69
+ ATTR_TOKENS_SAVED = 'agentbill.tokens_saved'
70
+ ATTR_COST_SAVED = 'agentbill.cost_saved'
71
+
72
+ # Cost tracing (Phase 1: enable_cost_tracing flag)
73
+ ATTR_COST_TRACING_ENABLED = 'agentbill.cost_tracing.enabled'
74
+
75
+ # Manual cost (Phase 3 – two-variant model)
76
+ ATTR_COST_PROVIDED = 'agentbill.cost.provided' # bool: server skips model_pricing
77
+ ATTR_COST_PROVIDER = 'agentbill.cost.provider' # str: maps from CostData.vendor
78
+ ATTR_COST_AMOUNT = 'agentbill.cost.amount' # float: pre-calculated cost
79
+ ATTR_COST_CURRENCY = 'agentbill.cost.currency' # str: currency code
80
+
81
+ # Business grouping (whitelist)
82
+ GROUPING_KEYS = %w[session_id workflow_id batch_id correlation_id].freeze
83
+
84
+ # Bulk
85
+ ATTR_BULK_INDEX = 'agentbill.bulk_index'
86
+ ATTR_BULK_REQUEST = 'agentbill.bulk_request'
87
+
88
+ # Resource / SDK constants
89
+ RESOURCE_SERVICE_NAME = 'agentbill-ruby-sdk'
90
+ SDK_VERSION = '9.5.5'
91
+ SCOPE_NAME = 'agentbill.signals'
92
+ SCOPE_NAME_BULK = 'agentbill.signals.bulk'
93
+ SIGNAL_SPAN_NAME = 'agentbill.trace.signal'
94
+ end
95
+ end
@@ -1,5 +1,6 @@
1
1
  # Ollama Wrapper for AgentBill Ruby SDK (Local, $0 cost)
2
- require_relative 'pricing'
2
+ # v7.17.3: Fixed to use OTEL spans instead of deleted track-ai-usage endpoint
3
+
3
4
 
4
5
  module AgentBill
5
6
  class OllamaWrapper
@@ -43,12 +44,14 @@ module AgentBill
43
44
  input_tokens = response[:prompt_eval_count] || 0
44
45
  output_tokens = response[:eval_count] || 0
45
46
 
46
- track_usage(model, 'ollama', input_tokens, output_tokens, latency, 0, config)
47
-
47
+ # v7.17.3: Track via OTEL span attributes (no track_usage call)
48
48
  span.set_attributes({
49
49
  # ✅ OTEL GenAI compliant attributes
50
50
  'gen_ai.usage.input_tokens' => input_tokens,
51
51
  'gen_ai.usage.output_tokens' => output_tokens,
52
+ 'gen_ai.usage.total_tokens' => input_tokens + output_tokens,
53
+ 'agentbill.event_name' => 'ollama_local',
54
+ 'agentbill.latency_ms' => latency,
52
55
  # ⚠️ Backward compatibility
53
56
  'response.prompt_eval_count' => input_tokens,
54
57
  'response.eval_count' => output_tokens,
@@ -64,6 +67,8 @@ module AgentBill
64
67
  raise
65
68
  ensure
66
69
  span.finish
70
+ # v7.17.3: Auto-flush span to OTEL collector
71
+ tracer.flush_sync rescue nil
67
72
  end
68
73
  end
69
74
  end
@@ -94,12 +99,14 @@ module AgentBill
94
99
  input_tokens = response[:prompt_eval_count] || 0
95
100
  output_tokens = response[:eval_count] || 0
96
101
 
97
- track_usage(model, 'ollama', input_tokens, output_tokens, latency, 0, config)
98
-
102
+ # v7.17.3: Track via OTEL span attributes (no track_usage call)
99
103
  span.set_attributes({
100
104
  # ✅ OTEL GenAI compliant attributes
101
105
  'gen_ai.usage.input_tokens' => input_tokens,
102
106
  'gen_ai.usage.output_tokens' => output_tokens,
107
+ 'gen_ai.usage.total_tokens' => input_tokens + output_tokens,
108
+ 'agentbill.event_name' => 'ollama_generate',
109
+ 'agentbill.latency_ms' => latency,
103
110
  # ⚠️ Backward compatibility
104
111
  'response.prompt_eval_count' => input_tokens,
105
112
  'response.eval_count' => output_tokens,
@@ -115,39 +122,12 @@ module AgentBill
115
122
  raise
116
123
  ensure
117
124
  span.finish
125
+ # v7.17.3: Auto-flush span to OTEL collector
126
+ tracer.flush_sync rescue nil
118
127
  end
119
128
  end
120
129
  end
121
130
 
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
131
+ # v7.17.3: track_usage method DELETED - all tracking now goes through OTEL spans
152
132
  end
153
133
  end
@@ -1,5 +1,6 @@
1
1
  # Perplexity AI Wrapper for AgentBill Ruby SDK
2
- require_relative 'pricing'
2
+ # v7.17.3: Fixed to use OTEL spans instead of deleted track-ai-usage endpoint
3
+
3
4
 
4
5
  module AgentBill
5
6
  class PerplexityWrapper
@@ -35,67 +36,40 @@ module AgentBill
35
36
 
36
37
  input_tokens = response.dig(:usage, :prompt_tokens) || 0
37
38
  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)
39
+ # v9.1.0: Cost calculated server-side from token counts in span
41
40
 
41
+ # v7.17.3: Track via OTEL span attributes (no track_usage call)
42
42
  span.set_attributes({
43
43
  # ✅ OTEL GenAI compliant attributes
44
44
  'gen_ai.usage.input_tokens' => input_tokens,
45
45
  'gen_ai.usage.output_tokens' => output_tokens,
46
+ 'gen_ai.usage.total_tokens' => input_tokens + output_tokens,
46
47
  'gen_ai.response.id' => response.dig(:id),
48
+ 'agentbill.event_name' => 'ai_request',
49
+ 'agentbill.latency_ms' => latency,
47
50
  # ⚠️ Backward compatibility
48
51
  'response.prompt_tokens' => input_tokens,
49
52
  'response.completion_tokens' => output_tokens,
50
- 'latency_ms' => latency,
51
- 'cost' => cost
53
+ 'latency_ms' => latency
52
54
  })
53
55
  span.set_status(0)
56
+ span.set_status(0)
54
57
 
55
- puts "[AgentBill] ✓ Perplexity chat tracked: $#{format('%.6f', cost)}" if config[:debug]
58
+ puts "[AgentBill] ✓ Perplexity call: #{input_tokens}in/#{output_tokens}out tokens (cost calculated server-side)" if config[:debug]
56
59
  response
57
60
  rescue => e
58
61
  span.set_status(1, e.message)
59
62
  raise
60
63
  ensure
61
64
  span.finish
65
+ # v7.17.3: Auto-flush span to OTEL collector
66
+ tracer.flush_sync rescue nil
62
67
  end
63
68
  end
64
69
 
65
70
  @client
66
71
  end
67
72
 
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
73
+ # v7.17.3: track_usage method DELETED - all tracking now goes through OTEL spans
100
74
  end
101
75
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'securerandom'
4
4
  require 'net/http'
5
+ require 'digest'
5
6
  require 'json'
6
7
  require 'uri'
7
8
  require 'time'
@@ -12,7 +13,7 @@ module AgentBill
12
13
  # Signal module for tracking business events and linking revenue to AI traces.
13
14
  module Signals
14
15
  BASE_URL = 'https://api.agentbill.io'
15
- VERSION = '7.16.1'
16
+ VERSION = '9.4.0'
16
17
 
17
18
  @config = {}
18
19
 
@@ -46,6 +47,9 @@ module AgentBill
46
47
  # @option options [Float] :event_value Event value
47
48
  # @option options [String] :order_id Order ID to link to (v7.8.0)
48
49
  # @option options [String] :order_external_id External order ID (v7.8.0)
50
+ # @option options [Hash] :group_by Business grouping keys (v9.2.0) — session_id, workflow_id, batch_id, correlation_id
51
+ # @option options [String] :idempotency_key Dedup key (v9.2.0) — auto-generated if not provided
52
+ # @option options [Hash] :cost_data Manual cost/usage data (v9.2.0 Phase 3)
49
53
  #
50
54
  # @return [Hash] Result with success status
51
55
  #
@@ -72,6 +76,10 @@ module AgentBill
72
76
  generated_span_id = options[:span_id] || SecureRandom.hex(8)
73
77
  generated_trace_id = trace_id || SecureRandom.hex(16)
74
78
  now_ns = (Time.now.to_f * 1_000_000_000).to_i
79
+ timestamp_s = Time.now.to_i
80
+
81
+ # Auto-generate idempotency key
82
+ idempotency_key = options[:idempotency_key] || Digest::SHA256.hexdigest("#{generated_trace_id}:#{event_name}:#{timestamp_s}")[0, 32]
75
83
 
76
84
  # Build OTEL-compliant span attributes
77
85
  attributes = [
@@ -96,6 +104,37 @@ module AgentBill
96
104
  attributes << { key: 'agentbill.order_external_id', value: { stringValue: options[:order_external_id] } } if options[:order_external_id]
97
105
  attributes << { key: 'agentbill.metadata', value: { stringValue: JSON.generate(options[:metadata]) } } if options[:metadata]
98
106
 
107
+ # v9.2.0: Idempotency key
108
+ attributes << { key: 'agentbill.idempotency_key', value: { stringValue: idempotency_key } }
109
+
110
+ # v9.2.0: Business grouping (whitelisted keys only)
111
+ grouping_keys = %w[session_id workflow_id batch_id correlation_id].freeze
112
+ if options[:group_by].is_a?(Hash)
113
+ options[:group_by].each do |key, value|
114
+ attributes << { key: "agentbill.#{key}", value: { stringValue: value.to_s } } if grouping_keys.include?(key.to_s)
115
+ end
116
+ end
117
+
118
+ # v9.4.0: enable_cost_tracing flag
119
+ if config[:enable_cost_tracing]
120
+ attributes << { key: 'agentbill.cost_tracing.enabled', value: { boolValue: true } }
121
+ end
122
+
123
+ # v9.2.0: Manual cost (Phase 3 – two-variant model)
124
+ if options[:cost_data].is_a?(Hash)
125
+ cd = options[:cost_data]
126
+ attributes << { key: 'agentbill.cost.provider', value: { stringValue: cd[:vendor] } } if cd[:vendor]
127
+ if cd[:cost].is_a?(Hash)
128
+ attributes << { key: 'agentbill.cost.provided', value: { boolValue: true } }
129
+ attributes << { key: 'agentbill.cost.amount', value: { doubleValue: cd[:cost][:amount] } }
130
+ attributes << { key: 'agentbill.cost.currency', value: { stringValue: cd[:cost][:currency] || 'USD' } }
131
+ end
132
+ if cd[:attributes].is_a?(Hash)
133
+ attributes << { key: 'gen_ai.request.model', value: { stringValue: cd[:attributes][:model] } } if cd[:attributes][:model]
134
+ attributes << { key: 'gen_ai.usage.prompt_tokens', value: { intValue: cd[:attributes][:input_tokens] } } if cd[:attributes][:input_tokens]
135
+ attributes << { key: 'gen_ai.usage.completion_tokens', value: { intValue: cd[:attributes][:output_tokens] } } if cd[:attributes][:output_tokens]
136
+ end
137
+ end
99
138
  # Build OTEL payload
100
139
  payload = {
101
140
  resourceSpans: [{
@@ -195,5 +234,38 @@ module AgentBill
195
234
  **options
196
235
  )
197
236
  end
237
+
238
+ # Create a signal explicitly linked to an AI trace for cost attribution.
239
+ #
240
+ # This is the recommended way to link business events to AI costs.
241
+ # Requires trace_id from the AI call you want to attribute costs to.
242
+ # Sets enable_cost_tracing=true automatically.
243
+ #
244
+ # v9.4.0: Added as part of P2 Architecture Alignment (Python parity).
245
+ #
246
+ # @param event_name [String] Name of the business event
247
+ # @param trace_id [String] Trace ID from the AI call
248
+ # @param revenue [Float] Revenue amount
249
+ # @param options [Hash] Additional signal options
250
+ # @return [Hash] Result with status
251
+ #
252
+ # @example
253
+ # AgentBill.cost_attributed_signal('purchase', 'abc123', 99.99, metadata: { product_id: 'prod-123' })
254
+ def cost_attributed_signal(event_name, trace_id:, revenue:, **options)
255
+ config = Signals.get_config
256
+ prev_config = config.dup
257
+ Signals.set_config(config.merge(enable_cost_tracing: true))
258
+
259
+ begin
260
+ signal(
261
+ event_name,
262
+ trace_id: trace_id,
263
+ revenue: revenue,
264
+ **options
265
+ )
266
+ ensure
267
+ Signals.set_config(prev_config)
268
+ end
269
+ end
198
270
  end
199
271
  end
@@ -1,3 +1,3 @@
1
1
  module AgentBill
2
- VERSION = "7.17.0"
2
+ VERSION = "9.4.0"
3
3
  end
@@ -15,7 +15,7 @@ module AgentBill
15
15
  # Base class for AgentBill wrapper classes.
16
16
  class BaseWrapper
17
17
  BASE_URL = 'https://api.agentbill.io/functions/v1'
18
- VERSION = '7.16.1'
18
+ VERSION = '7.17.3'
19
19
 
20
20
  attr_reader :provider_name
21
21
 
data/lib/agentbill.rb CHANGED
@@ -4,7 +4,7 @@ require 'securerandom'
4
4
  require 'digest'
5
5
  require_relative 'agentbill/version'
6
6
  require_relative 'agentbill/tracer'
7
- require_relative 'agentbill/pricing'
7
+
8
8
  require_relative 'agentbill/customers'
9
9
  require_relative 'agentbill/agents'
10
10
  require_relative 'agentbill/orders'
@@ -32,9 +32,7 @@ module AgentBill
32
32
  [1, text.to_s.length / 4].max
33
33
  end
34
34
 
35
- def estimate_cost(model, input_tokens, output_tokens, provider = 'openai')
36
- Pricing.calculate_cost(model, input_tokens, output_tokens, provider)
37
- end
35
+ # v9.1.0: estimate_cost() REMOVED. Cost is 100% server-side via model_pricing table.
38
36
 
39
37
  def validate_request(model, messages, estimated_output_tokens = 1000)
40
38
  # Always validate when customer_id is present - backend will check DB policies
@@ -72,60 +70,8 @@ module AgentBill
72
70
  end
73
71
  end
74
72
 
75
- # NOTE: track_usage is deprecated in v7.0.2 - signals now go through OTEL collector
76
- def track_usage(model, provider, input_tokens, output_tokens, latency_ms, cost, event_name = 'ai_request', trace_id = nil, span_id = nil)
77
- # v7.0.2: Route signals through otel-collector with proper span format
78
- uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/otel-collector")
79
-
80
- trace_id ||= SecureRandom.hex(16)
81
- span_id ||= SecureRandom.hex(8)
82
- now_ns = (Time.now.to_f * 1_000_000_000).to_i.to_s
83
-
84
- payload = {
85
- resourceSpans: [{
86
- resource: {
87
- attributes: [
88
- { key: 'service.name', value: { stringValue: 'agentbill-ruby-sdk' } },
89
- { key: 'agentbill.customer_id', value: { stringValue: @config[:customer_id] || '' } }
90
- ]
91
- },
92
- scopeSpans: [{
93
- scope: { name: 'agentbill.signals', version: '7.16.1' },
94
- spans: [{
95
- traceId: trace_id,
96
- spanId: span_id,
97
- name: 'agentbill.trace.signal',
98
- kind: 1,
99
- startTimeUnixNano: now_ns,
100
- endTimeUnixNano: now_ns,
101
- attributes: [
102
- { key: 'agentbill.event_name', value: { stringValue: event_name } },
103
- { key: 'agentbill.is_business_event', value: { boolValue: true } },
104
- { key: 'gen_ai.request.model', value: { stringValue: model } },
105
- { key: 'gen_ai.system', value: { stringValue: provider } }
106
- ],
107
- status: { code: 1 }
108
- }]
109
- }]
110
- }]
111
- }
112
-
113
- begin
114
- http = Net::HTTP.new(uri.host, uri.port)
115
- http.use_ssl = true
116
- http.read_timeout = 10
117
-
118
- request = Net::HTTP::Post.new(uri.path)
119
- request['Content-Type'] = 'application/json'
120
- request['x-api-key'] = @config[:api_key]
121
- request.body = payload.to_json
122
-
123
- http.request(request)
124
- puts "[AgentBill] Usage tracked via OTEL: $#{format('%.4f', cost)}" if @config[:debug]
125
- rescue => e
126
- puts "[AgentBill] Tracking failed: #{e.message}" if @config[:debug]
127
- end
128
- end
73
+ # v7.17.3: track_usage method DELETED - all tracking now goes through OTEL spans
74
+ # Use self.tracer.start_span() + span.finish() + self.tracer.flush_sync() instead
129
75
 
130
76
  public
131
77
 
@@ -218,7 +164,7 @@ module AgentBill
218
164
  # Signals should be created explicitly via track_signal()
219
165
  input_tokens = response.dig(:usage, :prompt_tokens) || 0
220
166
  output_tokens = response.dig(:usage, :completion_tokens) || 0
221
- cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens)
167
+ # v9.1.0: Cost calculated server-side from token counts in span
222
168
 
223
169
  span.set_attributes({
224
170
  # ✅ OTEL GenAI compliant attributes
@@ -234,13 +180,14 @@ module AgentBill
234
180
  })
235
181
  span.set_status(0)
236
182
 
237
- puts "[AgentBill] ✓ OTEL span created: $#{format('%.4f', cost)} (use track_signal() for signals)" if config[:debug]
183
+ puts "[AgentBill] ✓ OpenAI call: #{input_tokens}in/#{output_tokens}out tokens (cost calculated server-side)" if config[:debug]
238
184
 
239
185
  # v7.5.0: Cache AI response for semantic caching
240
186
  response_content = response.dig(:choices, 0, :message, :content) || ''
241
- prompt_text = messages.map { |m| m[:content] }.join(' ')
242
- prompt_hash = Digest::SHA256.hexdigest(prompt_text)[0..15]
243
- config[:_client].send(:cache_response, model, prompt_hash, response_content, input_tokens + output_tokens, cost)
187
+ # v9.1.0: Fixed prompt_hash to match backend algorithm (SHA-256 of JSON, full hash)
188
+ prompt_text = JSON.generate(messages)
189
+ prompt_hash = Digest::SHA256.hexdigest(prompt_text)
190
+ config[:_client].send(:cache_response, model, prompt_hash, response_content, input_tokens + output_tokens, 0)
244
191
 
245
192
  response
246
193
  rescue => e
@@ -296,7 +243,7 @@ module AgentBill
296
243
  # NOTE: wrap() only creates OTEL spans, NOT signals
297
244
  input_tokens = response.dig(:usage, :input_tokens) || 0
298
245
  output_tokens = response.dig(:usage, :output_tokens) || 0
299
- cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens, 'anthropic')
246
+ # v9.1.0: Cost calculated server-side from token counts in span
300
247
 
301
248
  span.set_attributes({
302
249
  # ✅ OTEL GenAI compliant attributes
@@ -367,25 +314,59 @@ module AgentBill
367
314
  def track_signal(**params)
368
315
  raise ArgumentError, "event_name is required" unless params[:event_name]
369
316
 
370
- uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/record-signals")
317
+ # v7.17.1: Unified OTEL model - route through otel-collector
318
+ uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/otel-collector")
371
319
 
372
- # Add timestamp if not provided
373
- params[:timestamp] ||= Time.now.to_f
320
+ # Generate trace context
321
+ trace_id = params[:trace_id] || SecureRandom.hex(16)
322
+ span_id = params[:span_id] || SecureRandom.hex(8)
323
+ now_ns = (Time.now.to_f * 1_000_000_000).to_i
374
324
 
375
- # Auto-fill customer_id or customer_external_id from config if not provided
376
- if !params[:customer_id] && !params[:customer_external_id] && @config[:customer_id]
377
- # Check if it's a UUID format - send as customer_id, else customer_external_id
378
- uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
379
- is_uuid = @config[:customer_id].match?(uuid_regex)
380
- if is_uuid
381
- params[:customer_id] = @config[:customer_id]
382
- else
383
- params[:customer_external_id] = @config[:customer_id]
384
- end
385
- end
325
+ # Auto-fill customer_id from config if not provided
326
+ customer_id = params[:customer_id] || params[:customer_external_id] || @config[:customer_id] || ""
327
+ agent_id = params[:agent_id] || params[:agent_external_id] || @config[:agent_id] || ""
386
328
 
387
- # Remove nil values
388
- payload = params.reject { |_, v| v.nil? }
329
+ # Build OTEL-compliant span attributes
330
+ attributes = [
331
+ { "key" => "agentbill.event_name", "value" => { "stringValue" => params[:event_name] } },
332
+ { "key" => "agentbill.is_business_event", "value" => { "boolValue" => true } },
333
+ { "key" => "agentbill.data_source", "value" => { "stringValue" => "ruby-sdk" } },
334
+ ]
335
+
336
+ attributes << { "key" => "agentbill.customer_id", "value" => { "stringValue" => customer_id.to_s } } if customer_id && customer_id != ""
337
+ attributes << { "key" => "agentbill.agent_id", "value" => { "stringValue" => agent_id.to_s } } if agent_id && agent_id != ""
338
+ attributes << { "key" => "agentbill.revenue", "value" => { "doubleValue" => params[:revenue] } } if params[:revenue]
339
+ attributes << { "key" => "agentbill.currency", "value" => { "stringValue" => params[:currency] || "USD" } } if params[:revenue]
340
+ attributes << { "key" => "agentbill.model", "value" => { "stringValue" => params[:model] } } if params[:model]
341
+ attributes << { "key" => "agentbill.session_id", "value" => { "stringValue" => params[:session_id] } } if params[:session_id]
342
+ attributes << { "key" => "agentbill.metadata", "value" => { "stringValue" => params[:metadata].to_json } } if params[:metadata]
343
+
344
+ # Build OTEL payload
345
+ otel_payload = {
346
+ "resourceSpans" => [{
347
+ "resource" => {
348
+ "attributes" => [
349
+ { "key" => "service.name", "value" => { "stringValue" => "agentbill-ruby-sdk" } },
350
+ { "key" => "agentbill.customer_id", "value" => { "stringValue" => customer_id.to_s } },
351
+ { "key" => "agentbill.agent_id", "value" => { "stringValue" => agent_id.to_s } },
352
+ ]
353
+ },
354
+ "scopeSpans" => [{
355
+ "scope" => { "name" => "agentbill.signals", "version" => "7.17.3" },
356
+ "spans" => [{
357
+ "traceId" => trace_id,
358
+ "spanId" => span_id,
359
+ "parentSpanId" => params[:parent_span_id] || "",
360
+ "name" => "agentbill.trace.signal",
361
+ "kind" => 1,
362
+ "startTimeUnixNano" => now_ns.to_s,
363
+ "endTimeUnixNano" => now_ns.to_s,
364
+ "attributes" => attributes,
365
+ "status" => { "code" => 1 }
366
+ }]
367
+ }]
368
+ }]
369
+ }
389
370
 
390
371
  begin
391
372
  http = Net::HTTP.new(uri.host, uri.port)
@@ -394,13 +375,12 @@ module AgentBill
394
375
  request = Net::HTTP::Post.new(uri.path)
395
376
  request['X-API-Key'] = @config[:api_key]
396
377
  request['Content-Type'] = 'application/json'
397
- request.body = payload.to_json
378
+ request.body = otel_payload.to_json
398
379
 
399
380
  response = http.request(request)
400
381
 
401
382
  if @config[:debug]
402
- trace_info = params[:trace_id] ? " (trace: #{params[:trace_id]})" : ""
403
- puts "[AgentBill] Signal tracked: #{params[:event_name]}#{trace_info}"
383
+ puts "[AgentBill] Signal tracked via OTEL: #{params[:event_name]} (trace: #{trace_id})"
404
384
  end
405
385
 
406
386
  response.code == '200'
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: 7.17.0
4
+ version: 9.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - AgentBill
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-02 00:00:00.000000000 Z
11
+ date: 2026-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -72,13 +72,13 @@ files:
72
72
  - examples/zero_config.rb
73
73
  - lib/agentbill.rb
74
74
  - lib/agentbill/agents.rb
75
+ - lib/agentbill/attributes.rb
75
76
  - lib/agentbill/customers.rb
76
77
  - lib/agentbill/distributed.rb
77
78
  - lib/agentbill/exceptions.rb
78
79
  - lib/agentbill/ollama_wrapper.rb
79
80
  - lib/agentbill/orders.rb
80
81
  - lib/agentbill/perplexity_wrapper.rb
81
- - lib/agentbill/pricing.rb
82
82
  - lib/agentbill/signal_types.rb
83
83
  - lib/agentbill/signals.rb
84
84
  - lib/agentbill/tracer.rb
@@ -1,52 +0,0 @@
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