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 +4 -4
- data/CHANGELOG.md +24 -1
- data/lib/agentbill/attributes.rb +95 -0
- data/lib/agentbill/ollama_wrapper.rb +15 -35
- data/lib/agentbill/perplexity_wrapper.rb +13 -39
- data/lib/agentbill/signals.rb +73 -1
- data/lib/agentbill/version.rb +1 -1
- data/lib/agentbill/wrappers.rb +1 -1
- data/lib/agentbill.rb +63 -83
- metadata +3 -3
- data/lib/agentbill/pricing.rb +0 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ee9e7fe3afb95e290b137974dadcb40147cc4b3855985688bf5ece9c037930b
|
|
4
|
+
data.tar.gz: 52fdd0c0634edc3bd36425244a0fc923e2b820bfced8fe8a14ace7538e55225d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/agentbill/signals.rb
CHANGED
|
@@ -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 = '
|
|
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
|
data/lib/agentbill/version.rb
CHANGED
data/lib/agentbill/wrappers.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
76
|
-
|
|
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
|
-
|
|
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] ✓
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
373
|
-
params[:
|
|
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
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
#
|
|
388
|
-
|
|
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 =
|
|
378
|
+
request.body = otel_payload.to_json
|
|
398
379
|
|
|
399
380
|
response = http.request(request)
|
|
400
381
|
|
|
401
382
|
if @config[:debug]
|
|
402
|
-
|
|
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:
|
|
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-
|
|
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
|
data/lib/agentbill/pricing.rb
DELETED
|
@@ -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
|