agentbill-sdk 5.0.1 → 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: 7e9b487391274ecb94dbc031a40f6e30a167e6fa0e30bb257464f68e497871f2
4
- data.tar.gz: cc6bc10bfdddf68981cd5454f80b4cffc6ea10a244f49f7e1f6582dd154e9b6c
3
+ metadata.gz: 0ee9e7fe3afb95e290b137974dadcb40147cc4b3855985688bf5ece9c037930b
4
+ data.tar.gz: 52fdd0c0634edc3bd36425244a0fc923e2b820bfced8fe8a14ace7538e55225d
5
5
  SHA512:
6
- metadata.gz: a4409ffa8bac425a994a09c45d513877d5167e7ca9706c86229de4a6a50f8bf1da729ec1ccab5448c1dda808ed43cb04b2c314840f894801bd203cd8ca608aff
7
- data.tar.gz: 285d37fb82ae7b3173474c72c47647e2585d8cf983730b16a24adf068a7baae3004078e10c79c493f66960d19921fd531d4aac1b7b6f35688ec7b9e1b541c987
6
+ metadata.gz: c567fa89b467b00d36f1df21ac752ea772d6c333af73df20f4ea986739acc2490e57d52568d1604a8b4067588f1796d7e94ae2e7cf3ee3ecfca71c6cdfe4c5c2
7
+ data.tar.gz: 3a18a47c140ed7eb1d6a46f5ee8ddc04ab91ce822bae23f7f8dcecbadf444b9b80f95f83f5006e56ddaa6cd468ddd7bad3b3940ffc030fc92116e8341b14a99b
@@ -1,10 +1,11 @@
1
1
  name: CI
2
2
 
3
3
  on:
4
- push:
5
- branches: [ main, develop ]
6
4
  pull_request:
7
5
  branches: [ main, develop ]
6
+ paths:
7
+ - 'sdks/ruby/**'
8
+ - '.github/workflows/ci.yml'
8
9
 
9
10
  jobs:
10
11
  test:
@@ -13,8 +14,8 @@ jobs:
13
14
 
14
15
  strategy:
15
16
  matrix:
16
- ruby-version: ['3.0', '3.1', '3.2', '3.3']
17
- os: [ubuntu-latest, macos-latest, windows-latest]
17
+ ruby-version: ['3.3']
18
+ os: [ubuntu-latest]
18
19
 
19
20
  steps:
20
21
  - uses: actions/checkout@v4
data/CHANGELOG.md CHANGED
@@ -5,6 +5,58 @@ 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
+
31
+ ## [7.6.3] - 2025-12-17
32
+
33
+ ### Changed
34
+ - **Version Alignment**: Aligned with Python SDK v7.6.3 wrap_openai() semantic cache fix
35
+ - Ruby SDK already had cache hit check and cache population in `wrap_openai()`
36
+
37
+ ## [6.8.8] - 2025-12-09
38
+
39
+ ### Fixed
40
+ - **Version Bump**: Aligned with Python SDK v6.8.8 wrapper trace context auto-generation fix
41
+
42
+ ## [6.8.5] - 2025-12-09
43
+
44
+ ### Fixed
45
+ - **Tracer Version Alignment**: Updated `service.version` and `scope.version` in OTLP payload to 6.8.5
46
+
47
+ ## [6.8.4] - 2025-12-08
48
+
49
+ ### Fixed
50
+ - **CRITICAL**: Fixed `agent_id` and `customer_id` not being sent as OTEL resource attributes
51
+ - The otel-collector extracts these from resource attributes, not top-level payload
52
+ - Added `agent_id` config option to SDK initialization
53
+
54
+ ## [6.8.3] - 2025-12-08
55
+
56
+ ### Fixed
57
+ - **Version Bump**: Aligned with Python SDK v6.8.3 trace context fix for decorator pattern
58
+ - No functional changes in Ruby SDK (fix was Python-specific)
59
+
8
60
  ## [5.0.1] - 2025-11-06
9
61
 
10
62
  ### Added
@@ -31,7 +83,7 @@ agentbill.track_signal(
31
83
  - **CRITICAL**: Fixed authentication header to use `X-API-Key` instead of `Authorization: Bearer` format
32
84
  - Updated `track_signal()` method in agentbill.rb to use correct authentication
33
85
  - Updated `flush()` method in tracer.rb to use correct authentication
34
- - Signals now properly authenticate with record-signals and otel-collector edge functions
86
+ - Signals now properly authenticate with the unified OTEL pipeline (otel-collector)
35
87
 
36
88
  ## [2.0.1] - 2025-10-25
37
89
 
@@ -0,0 +1,81 @@
1
+ # AgentBill v6.0.0 - Ollama Integration Example
2
+ # Tracks local Ollama chat and generation (metrics only, $0 cost)
3
+
4
+ require 'agentbill'
5
+ require 'net/http'
6
+ require 'json'
7
+
8
+ # Mock Ollama client (replace with actual Ollama SDK)
9
+ class OllamaClient
10
+ def chat(params)
11
+ uri = URI('http://localhost:11434/api/chat')
12
+
13
+ http = Net::HTTP.new(uri.host, uri.port)
14
+
15
+ request = Net::HTTP::Post.new(uri.path)
16
+ request['Content-Type'] = 'application/json'
17
+ request.body = params.to_json
18
+
19
+ response = http.request(request)
20
+ JSON.parse(response.body, symbolize_names: true)
21
+ end
22
+
23
+ def generate(params)
24
+ uri = URI('http://localhost:11434/api/generate')
25
+
26
+ http = Net::HTTP.new(uri.host, uri.port)
27
+
28
+ request = Net::HTTP::Post.new(uri.path)
29
+ request['Content-Type'] = 'application/json'
30
+ request.body = params.to_json
31
+
32
+ response = http.request(request)
33
+ JSON.parse(response.body, symbolize_names: true)
34
+ end
35
+ end
36
+
37
+ # Initialize AgentBill
38
+ agentbill = AgentBill::Client.init({
39
+ api_key: ENV['AGENTBILL_API_KEY'],
40
+ customer_id: 'customer-123',
41
+ debug: true
42
+ })
43
+
44
+ # Create Ollama client
45
+ ollama = OllamaClient.new
46
+
47
+ # Wrap with AgentBill
48
+ ollama_wrapper = AgentBill::OllamaWrapper.new(ollama, agentbill.config, agentbill.tracer)
49
+ tracked_ollama = ollama_wrapper.wrap
50
+
51
+ puts "🦙 Using Ollama (local, free)...\n\n"
52
+
53
+ # Example 1: Chat
54
+ puts "💬 Chat example:"
55
+ chat_response = tracked_ollama.chat({
56
+ model: 'llama2',
57
+ messages: [
58
+ {
59
+ role: 'user',
60
+ content: 'Why is the sky blue? Give a brief answer.'
61
+ }
62
+ ]
63
+ })
64
+
65
+ puts "✅ Response: #{chat_response[:message][:content]}"
66
+ puts "📊 Tokens: prompt=#{chat_response[:prompt_eval_count]}, completion=#{chat_response[:eval_count]}"
67
+ puts "💰 Cost: $0.00 (local model)\n\n"
68
+
69
+ # Example 2: Generate
70
+ puts "📝 Generate example:"
71
+ generate_response = tracked_ollama.generate({
72
+ model: 'llama2',
73
+ prompt: 'Write a haiku about coding:'
74
+ })
75
+
76
+ puts "✅ Response: #{generate_response[:response]}"
77
+ puts "📊 Tokens: prompt=#{generate_response[:prompt_eval_count]}, completion=#{generate_response[:eval_count]}"
78
+ puts "💰 Cost: $0.00 (local model)\n\n"
79
+
80
+ puts "ℹ️ Ollama runs locally, so there's no API cost."
81
+ puts " AgentBill tracks metrics for monitoring and analysis.\n"
@@ -0,0 +1,66 @@
1
+ # AgentBill v6.0.0 - Perplexity AI Integration Example
2
+ # Tracks Perplexity AI chat completions with online search
3
+
4
+ require 'agentbill'
5
+ require 'net/http'
6
+ require 'json'
7
+
8
+ # Mock Perplexity client (replace with actual Perplexity SDK)
9
+ class PerplexityClient
10
+ def initialize(api_key)
11
+ @api_key = api_key
12
+ end
13
+
14
+ def chat_create(params)
15
+ uri = URI('https://api.perplexity.ai/chat/completions')
16
+
17
+ http = Net::HTTP.new(uri.host, uri.port)
18
+ http.use_ssl = true
19
+
20
+ request = Net::HTTP::Post.new(uri.path)
21
+ request['Authorization'] = "Bearer #{@api_key}"
22
+ request['Content-Type'] = 'application/json'
23
+ request.body = params.to_json
24
+
25
+ response = http.request(request)
26
+ JSON.parse(response.body, symbolize_names: true)
27
+ end
28
+ end
29
+
30
+ # Initialize AgentBill
31
+ agentbill = AgentBill::Client.init({
32
+ api_key: ENV['AGENTBILL_API_KEY'],
33
+ customer_id: 'customer-123',
34
+ debug: true
35
+ })
36
+
37
+ # Create Perplexity client
38
+ perplexity = PerplexityClient.new(ENV['PERPLEXITY_API_KEY'])
39
+
40
+ # Wrap with AgentBill
41
+ perplexity_wrapper = AgentBill::PerplexityWrapper.new(perplexity, agentbill.config, agentbill.tracer)
42
+ tracked_perplexity = perplexity_wrapper.wrap
43
+
44
+ puts "🔍 Querying Perplexity AI with online search...\n\n"
45
+
46
+ # Make a request - automatically tracked
47
+ response = tracked_perplexity.chat_create({
48
+ model: 'llama-3.1-sonar-small-128k-online',
49
+ messages: [
50
+ {
51
+ role: 'system',
52
+ content: 'Be precise and concise.'
53
+ },
54
+ {
55
+ role: 'user',
56
+ content: 'What are the latest developments in AI for 2025?'
57
+ }
58
+ ],
59
+ temperature: 0.2,
60
+ top_p: 0.9,
61
+ max_tokens: 1000
62
+ })
63
+
64
+ puts "✅ Response: #{response[:choices][0][:message][:content]}"
65
+ puts "\n📊 Usage: #{response[:usage]}"
66
+ puts "💰 Cost automatically tracked in AgentBill dashboard\n"
@@ -0,0 +1,226 @@
1
+ # AgentBill Agents Resource - CRUD operations with signal type management
2
+ #
3
+ # Example:
4
+ # ab = AgentBill::Client.init(api_key: 'agb_...')
5
+ #
6
+ # # Create agent
7
+ # agent = ab.agents.create(name: 'Support Bot', description: 'Customer support')
8
+ # puts "API Key (save this!): #{agent['api_key']}"
9
+ #
10
+ # # Assign signal types
11
+ # ab.agents.assign_signal_types(
12
+ # agent_id: agent['id'],
13
+ # signal_type_names: ['ai_call', 'completion']
14
+ # )
15
+ #
16
+ # # List agents
17
+ # result = ab.agents.list(status: 'active')
18
+ #
19
+ # # Get signal types for agent
20
+ # signal_types = ab.agents.get_signal_types(agent['id'])
21
+
22
+ require 'net/http'
23
+ require 'json'
24
+
25
+ module AgentBill
26
+ class AgentsResource
27
+ def initialize(config)
28
+ @config = config
29
+ @base_url = config[:base_url] || 'https://api.agentbill.io'
30
+ @api_key = config[:api_key]
31
+ @debug = config[:debug] || false
32
+ end
33
+
34
+ # List agents with optional status filter and pagination
35
+ #
36
+ # @param status [String, nil] Filter by status ('active', 'inactive', 'archived')
37
+ # @param limit [Integer] Number of agents to return (default: 50)
38
+ # @param offset [Integer] Offset for pagination (default: 0)
39
+ # @param search [String, nil] Search term to filter by name or description
40
+ # @return [Hash] Hash with 'data' (list of agents) and 'pagination' info
41
+ def list(status: nil, limit: 50, offset: 0, search: nil)
42
+ uri = URI("#{@base_url}/functions/v1/api-agents")
43
+ params = { limit: limit.to_s, offset: offset.to_s }
44
+ params[:status] = status if status
45
+ params[:search] = search if search
46
+ uri.query = URI.encode_www_form(params)
47
+
48
+ response = make_request(:get, uri)
49
+ handle_response(response, 'list agents')
50
+ end
51
+
52
+ # Create a new agent
53
+ # IMPORTANT: The response includes a one-time visible API key.
54
+ # Save this key securely - it cannot be retrieved again.
55
+ #
56
+ # @param name [String] Agent name (required)
57
+ # @param description [String, nil] Agent description
58
+ # @param agent_type [String, nil] Type of agent (e.g., 'support', 'sales', 'assistant')
59
+ # @param model [String, nil] Default model for this agent
60
+ # @param external_id [String, nil] Your external ID for this agent
61
+ # @param metadata [Hash, nil] Additional metadata
62
+ # @param signal_type_names [Array<String>, nil] Signal types to auto-assign (e.g., ['ai_call', 'completion'])
63
+ # @return [Hash] Created agent object with api_key (one-time visible)
64
+ def create(name:, description: nil, agent_type: nil, model: nil, external_id: nil, metadata: nil, signal_type_names: nil)
65
+ uri = URI("#{@base_url}/functions/v1/api-agents")
66
+
67
+ payload = { name: name }
68
+ payload[:description] = description if description
69
+ payload[:agent_type] = agent_type if agent_type
70
+ payload[:model] = model if model
71
+ payload[:external_id] = external_id if external_id
72
+ payload[:metadata] = metadata if metadata
73
+ payload[:signal_type_names] = signal_type_names if signal_type_names
74
+
75
+ response = make_request(:post, uri, payload)
76
+ handle_response(response, 'create agent')
77
+ end
78
+
79
+ # Get an agent by ID or external_id
80
+ #
81
+ # @param agent_id [String] Agent UUID or external_id
82
+ # @param by_external_id [Boolean] If true, treat agent_id as external_id
83
+ # @return [Hash] Agent object
84
+ def get(agent_id, by_external_id: false)
85
+ uri = URI("#{@base_url}/functions/v1/api-agents")
86
+ params = by_external_id ? { external_id: agent_id } : { id: agent_id }
87
+ uri.query = URI.encode_www_form(params)
88
+
89
+ response = make_request(:get, uri)
90
+ result = handle_response(response, 'get agent')
91
+
92
+ if result['data'].is_a?(Array)
93
+ raise "Agent not found" if result['data'].empty?
94
+ return result['data'][0]
95
+ end
96
+ result
97
+ end
98
+
99
+ # Update an agent
100
+ #
101
+ # @param agent_id [String] Agent UUID
102
+ # @param name [String, nil] New name
103
+ # @param description [String, nil] New description
104
+ # @param agent_type [String, nil] New agent type
105
+ # @param model [String, nil] New default model
106
+ # @param status [String, nil] New status ('active', 'inactive', 'archived')
107
+ # @param metadata [Hash, nil] New metadata (replaces existing)
108
+ # @return [Hash] Updated agent object
109
+ def update(agent_id, name: nil, description: nil, agent_type: nil, model: nil, status: nil, metadata: nil)
110
+ uri = URI("#{@base_url}/functions/v1/api-agents")
111
+
112
+ payload = { id: agent_id }
113
+ payload[:name] = name if name
114
+ payload[:description] = description if description
115
+ payload[:agent_type] = agent_type if agent_type
116
+ payload[:model] = model if model
117
+ payload[:status] = status if status
118
+ payload[:metadata] = metadata if metadata
119
+
120
+ response = make_request(:patch, uri, payload)
121
+ handle_response(response, 'update agent')
122
+ end
123
+
124
+ # Delete an agent
125
+ #
126
+ # @param agent_id [String] Agent UUID to delete
127
+ def delete(agent_id)
128
+ uri = URI("#{@base_url}/functions/v1/api-agents")
129
+ payload = { id: agent_id }
130
+
131
+ response = make_request(:delete, uri, payload)
132
+ handle_response(response, 'delete agent')
133
+ end
134
+
135
+ # Assign signal types to an agent (explicit assignment)
136
+ # This method explicitly configures which signal types the agent can emit.
137
+ #
138
+ # @param agent_id [String, nil] Agent UUID (provide this OR external_agent_id)
139
+ # @param external_agent_id [String, nil] External agent ID (provide this OR agent_id)
140
+ # @param signal_type_ids [Array<String>, nil] List of signal type UUIDs to assign
141
+ # @param signal_type_names [Array<String>, nil] List of signal type names (e.g., ['ai_call', 'completion'])
142
+ # @return [Hash] Hash with agent_id, signal_type_ids, and assigned_count
143
+ def assign_signal_types(agent_id: nil, external_agent_id: nil, signal_type_ids: nil, signal_type_names: nil)
144
+ raise ArgumentError, "Either agent_id or external_agent_id is required" unless agent_id || external_agent_id
145
+ raise ArgumentError, "Either signal_type_ids or signal_type_names is required" unless signal_type_ids || signal_type_names
146
+
147
+ uri = URI("#{@base_url}/functions/v1/api-agent-signal-types")
148
+
149
+ payload = {}
150
+ payload[:agent_id] = agent_id if agent_id
151
+ payload[:external_agent_id] = external_agent_id if external_agent_id
152
+ payload[:signal_type_ids] = signal_type_ids if signal_type_ids
153
+ payload[:signal_type_names] = signal_type_names if signal_type_names
154
+
155
+ response = make_request(:post, uri, payload)
156
+ handle_response(response, 'assign signal types')
157
+ end
158
+
159
+ # Get signal types assigned to an agent
160
+ #
161
+ # @param agent_id [String] Agent UUID or external_id
162
+ # @param by_external_id [Boolean] If true, treat agent_id as external_id
163
+ # @return [Hash] Hash with agent_id and list of signal_types
164
+ def get_signal_types(agent_id, by_external_id: false)
165
+ uri = URI("#{@base_url}/functions/v1/api-agent-signal-types")
166
+ params = by_external_id ? { external_agent_id: agent_id } : { agent_id: agent_id }
167
+ uri.query = URI.encode_www_form(params)
168
+
169
+ response = make_request(:get, uri)
170
+ handle_response(response, 'get signal types')
171
+ end
172
+
173
+ # Remove signal types from an agent
174
+ #
175
+ # @param agent_id [String] Agent UUID
176
+ # @param signal_type_ids [Array<String>, nil] Specific signal type IDs to remove. If nil, removes all.
177
+ def remove_signal_types(agent_id, signal_type_ids: nil)
178
+ uri = URI("#{@base_url}/functions/v1/api-agent-signal-types")
179
+
180
+ payload = { agent_id: agent_id }
181
+ payload[:signal_type_ids] = signal_type_ids if signal_type_ids
182
+
183
+ response = make_request(:delete, uri, payload)
184
+ handle_response(response, 'remove signal types')
185
+ end
186
+
187
+ private
188
+
189
+ def make_request(method, uri, payload = nil)
190
+ http = Net::HTTP.new(uri.host, uri.port)
191
+ http.use_ssl = true
192
+ http.read_timeout = 30
193
+
194
+ case method
195
+ when :get
196
+ request = Net::HTTP::Get.new(uri)
197
+ when :post
198
+ request = Net::HTTP::Post.new(uri.path)
199
+ request.body = payload.to_json if payload
200
+ when :patch
201
+ request = Net::HTTP::Patch.new(uri.path)
202
+ request.body = payload.to_json if payload
203
+ when :delete
204
+ request = Net::HTTP::Delete.new(uri.path)
205
+ request.body = payload.to_json if payload
206
+ end
207
+
208
+ request['Content-Type'] = 'application/json'
209
+ request['X-API-Key'] = @api_key
210
+
211
+ http.request(request)
212
+ end
213
+
214
+ def handle_response(response, operation)
215
+ body = response.body ? JSON.parse(response.body) : {}
216
+
217
+ unless %w[200 201 204].include?(response.code)
218
+ error_msg = body['error'] || "Failed to #{operation}"
219
+ raise error_msg
220
+ end
221
+
222
+ puts "[AgentBill] #{operation} successful" if @debug
223
+ body
224
+ end
225
+ end
226
+ end
@@ -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