braintrust 0.0.12 → 0.1.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/README.md +213 -180
- data/exe/braintrust +143 -0
- data/lib/braintrust/contrib/anthropic/deprecated.rb +24 -0
- data/lib/braintrust/contrib/anthropic/instrumentation/common.rb +53 -0
- data/lib/braintrust/contrib/anthropic/instrumentation/messages.rb +232 -0
- data/lib/braintrust/contrib/anthropic/integration.rb +53 -0
- data/lib/braintrust/contrib/anthropic/patcher.rb +62 -0
- data/lib/braintrust/contrib/context.rb +56 -0
- data/lib/braintrust/contrib/integration.rb +160 -0
- data/lib/braintrust/contrib/openai/deprecated.rb +22 -0
- data/lib/braintrust/contrib/openai/instrumentation/chat.rb +298 -0
- data/lib/braintrust/contrib/openai/instrumentation/common.rb +134 -0
- data/lib/braintrust/contrib/openai/instrumentation/responses.rb +187 -0
- data/lib/braintrust/contrib/openai/integration.rb +58 -0
- data/lib/braintrust/contrib/openai/patcher.rb +130 -0
- data/lib/braintrust/contrib/patcher.rb +76 -0
- data/lib/braintrust/contrib/rails/railtie.rb +16 -0
- data/lib/braintrust/contrib/registry.rb +107 -0
- data/lib/braintrust/contrib/ruby_llm/deprecated.rb +45 -0
- data/lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb +464 -0
- data/lib/braintrust/contrib/ruby_llm/instrumentation/common.rb +58 -0
- data/lib/braintrust/contrib/ruby_llm/integration.rb +54 -0
- data/lib/braintrust/contrib/ruby_llm/patcher.rb +44 -0
- data/lib/braintrust/contrib/ruby_openai/deprecated.rb +24 -0
- data/lib/braintrust/contrib/ruby_openai/instrumentation/chat.rb +149 -0
- data/lib/braintrust/contrib/ruby_openai/instrumentation/common.rb +138 -0
- data/lib/braintrust/contrib/ruby_openai/instrumentation/responses.rb +146 -0
- data/lib/braintrust/contrib/ruby_openai/integration.rb +58 -0
- data/lib/braintrust/contrib/ruby_openai/patcher.rb +85 -0
- data/lib/braintrust/contrib/setup.rb +168 -0
- data/lib/braintrust/contrib/support/openai.rb +72 -0
- data/lib/braintrust/contrib/support/otel.rb +23 -0
- data/lib/braintrust/contrib.rb +205 -0
- data/lib/braintrust/internal/env.rb +33 -0
- data/lib/braintrust/internal/time.rb +44 -0
- data/lib/braintrust/setup.rb +50 -0
- data/lib/braintrust/state.rb +5 -0
- data/lib/braintrust/trace.rb +0 -51
- data/lib/braintrust/version.rb +1 -1
- data/lib/braintrust.rb +10 -1
- metadata +38 -7
- data/lib/braintrust/trace/contrib/anthropic.rb +0 -316
- data/lib/braintrust/trace/contrib/github.com/alexrudall/ruby-openai/ruby-openai.rb +0 -377
- data/lib/braintrust/trace/contrib/github.com/crmne/ruby_llm.rb +0 -631
- data/lib/braintrust/trace/contrib/openai.rb +0 -611
- data/lib/braintrust/trace/tokens.rb +0 -109
data/lib/braintrust.rb
CHANGED
|
@@ -6,7 +6,9 @@ require_relative "braintrust/state"
|
|
|
6
6
|
require_relative "braintrust/trace"
|
|
7
7
|
require_relative "braintrust/api"
|
|
8
8
|
require_relative "braintrust/internal/experiments"
|
|
9
|
+
require_relative "braintrust/internal/env"
|
|
9
10
|
require_relative "braintrust/eval"
|
|
11
|
+
require_relative "braintrust/contrib"
|
|
10
12
|
|
|
11
13
|
# Braintrust Ruby SDK
|
|
12
14
|
#
|
|
@@ -40,8 +42,13 @@ module Braintrust
|
|
|
40
42
|
# @param filter_ai_spans [Boolean, nil] Enable AI span filtering (overrides BRAINTRUST_OTEL_FILTER_AI_SPANS env var)
|
|
41
43
|
# @param span_filter_funcs [Array<Proc>, nil] Custom span filter functions
|
|
42
44
|
# @param exporter [Exporter, nil] Optional exporter override (for testing)
|
|
45
|
+
# @param auto_instrument [Boolean, Hash, nil] Auto-instrumentation config:
|
|
46
|
+
# - nil (default): use BRAINTRUST_AUTO_INSTRUMENT env var, default true if not set
|
|
47
|
+
# - true: explicitly enable
|
|
48
|
+
# - false: explicitly disable
|
|
49
|
+
# - Hash with :only or :except keys for filtering
|
|
43
50
|
# @return [State] the created state
|
|
44
|
-
def self.init(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, set_global: true, blocking_login: false, enable_tracing: true, tracer_provider: nil, filter_ai_spans: nil, span_filter_funcs: nil, exporter: nil)
|
|
51
|
+
def self.init(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, set_global: true, blocking_login: false, enable_tracing: true, tracer_provider: nil, filter_ai_spans: nil, span_filter_funcs: nil, exporter: nil, auto_instrument: nil)
|
|
45
52
|
state = State.from_env(
|
|
46
53
|
api_key: api_key,
|
|
47
54
|
org_name: org_name,
|
|
@@ -58,6 +65,8 @@ module Braintrust
|
|
|
58
65
|
|
|
59
66
|
State.global = state if set_global
|
|
60
67
|
|
|
68
|
+
auto_instrument!(auto_instrument)
|
|
69
|
+
|
|
61
70
|
state
|
|
62
71
|
end
|
|
63
72
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: braintrust
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Braintrust
|
|
@@ -180,18 +180,51 @@ dependencies:
|
|
|
180
180
|
description: 'Braintrust Ruby SDK for evals, tracing and more. '
|
|
181
181
|
email:
|
|
182
182
|
- info@braintrust.dev
|
|
183
|
-
executables:
|
|
183
|
+
executables:
|
|
184
|
+
- braintrust
|
|
184
185
|
extensions: []
|
|
185
186
|
extra_rdoc_files: []
|
|
186
187
|
files:
|
|
187
188
|
- LICENSE
|
|
188
189
|
- README.md
|
|
190
|
+
- exe/braintrust
|
|
189
191
|
- lib/braintrust.rb
|
|
190
192
|
- lib/braintrust/api.rb
|
|
191
193
|
- lib/braintrust/api/datasets.rb
|
|
192
194
|
- lib/braintrust/api/functions.rb
|
|
193
195
|
- lib/braintrust/api/internal/auth.rb
|
|
194
196
|
- lib/braintrust/config.rb
|
|
197
|
+
- lib/braintrust/contrib.rb
|
|
198
|
+
- lib/braintrust/contrib/anthropic/deprecated.rb
|
|
199
|
+
- lib/braintrust/contrib/anthropic/instrumentation/common.rb
|
|
200
|
+
- lib/braintrust/contrib/anthropic/instrumentation/messages.rb
|
|
201
|
+
- lib/braintrust/contrib/anthropic/integration.rb
|
|
202
|
+
- lib/braintrust/contrib/anthropic/patcher.rb
|
|
203
|
+
- lib/braintrust/contrib/context.rb
|
|
204
|
+
- lib/braintrust/contrib/integration.rb
|
|
205
|
+
- lib/braintrust/contrib/openai/deprecated.rb
|
|
206
|
+
- lib/braintrust/contrib/openai/instrumentation/chat.rb
|
|
207
|
+
- lib/braintrust/contrib/openai/instrumentation/common.rb
|
|
208
|
+
- lib/braintrust/contrib/openai/instrumentation/responses.rb
|
|
209
|
+
- lib/braintrust/contrib/openai/integration.rb
|
|
210
|
+
- lib/braintrust/contrib/openai/patcher.rb
|
|
211
|
+
- lib/braintrust/contrib/patcher.rb
|
|
212
|
+
- lib/braintrust/contrib/rails/railtie.rb
|
|
213
|
+
- lib/braintrust/contrib/registry.rb
|
|
214
|
+
- lib/braintrust/contrib/ruby_llm/deprecated.rb
|
|
215
|
+
- lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb
|
|
216
|
+
- lib/braintrust/contrib/ruby_llm/instrumentation/common.rb
|
|
217
|
+
- lib/braintrust/contrib/ruby_llm/integration.rb
|
|
218
|
+
- lib/braintrust/contrib/ruby_llm/patcher.rb
|
|
219
|
+
- lib/braintrust/contrib/ruby_openai/deprecated.rb
|
|
220
|
+
- lib/braintrust/contrib/ruby_openai/instrumentation/chat.rb
|
|
221
|
+
- lib/braintrust/contrib/ruby_openai/instrumentation/common.rb
|
|
222
|
+
- lib/braintrust/contrib/ruby_openai/instrumentation/responses.rb
|
|
223
|
+
- lib/braintrust/contrib/ruby_openai/integration.rb
|
|
224
|
+
- lib/braintrust/contrib/ruby_openai/patcher.rb
|
|
225
|
+
- lib/braintrust/contrib/setup.rb
|
|
226
|
+
- lib/braintrust/contrib/support/openai.rb
|
|
227
|
+
- lib/braintrust/contrib/support/otel.rb
|
|
195
228
|
- lib/braintrust/eval.rb
|
|
196
229
|
- lib/braintrust/eval/case.rb
|
|
197
230
|
- lib/braintrust/eval/cases.rb
|
|
@@ -202,19 +235,17 @@ files:
|
|
|
202
235
|
- lib/braintrust/eval/scorer.rb
|
|
203
236
|
- lib/braintrust/eval/summary.rb
|
|
204
237
|
- lib/braintrust/internal/encoding.rb
|
|
238
|
+
- lib/braintrust/internal/env.rb
|
|
205
239
|
- lib/braintrust/internal/experiments.rb
|
|
206
240
|
- lib/braintrust/internal/thread_pool.rb
|
|
241
|
+
- lib/braintrust/internal/time.rb
|
|
207
242
|
- lib/braintrust/logger.rb
|
|
243
|
+
- lib/braintrust/setup.rb
|
|
208
244
|
- lib/braintrust/state.rb
|
|
209
245
|
- lib/braintrust/trace.rb
|
|
210
246
|
- lib/braintrust/trace/attachment.rb
|
|
211
|
-
- lib/braintrust/trace/contrib/anthropic.rb
|
|
212
|
-
- lib/braintrust/trace/contrib/github.com/alexrudall/ruby-openai/ruby-openai.rb
|
|
213
|
-
- lib/braintrust/trace/contrib/github.com/crmne/ruby_llm.rb
|
|
214
|
-
- lib/braintrust/trace/contrib/openai.rb
|
|
215
247
|
- lib/braintrust/trace/span_filter.rb
|
|
216
248
|
- lib/braintrust/trace/span_processor.rb
|
|
217
|
-
- lib/braintrust/trace/tokens.rb
|
|
218
249
|
- lib/braintrust/version.rb
|
|
219
250
|
homepage: https://github.com/braintrustdata/braintrust-sdk-ruby
|
|
220
251
|
licenses:
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "opentelemetry/sdk"
|
|
4
|
-
require "json"
|
|
5
|
-
require_relative "../tokens"
|
|
6
|
-
|
|
7
|
-
module Braintrust
|
|
8
|
-
module Trace
|
|
9
|
-
module Anthropic
|
|
10
|
-
# Helper to safely set a JSON attribute on a span
|
|
11
|
-
# Only sets the attribute if obj is present
|
|
12
|
-
# @param span [OpenTelemetry::Trace::Span] the span to set attribute on
|
|
13
|
-
# @param attr_name [String] the attribute name (e.g., "braintrust.output_json")
|
|
14
|
-
# @param obj [Object] the object to serialize to JSON
|
|
15
|
-
# @return [void]
|
|
16
|
-
def self.set_json_attr(span, attr_name, obj)
|
|
17
|
-
return unless obj
|
|
18
|
-
span.set_attribute(attr_name, JSON.generate(obj))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Parse usage tokens from Anthropic API response
|
|
22
|
-
# @param usage [Hash, Object] usage object from Anthropic response
|
|
23
|
-
# @return [Hash<String, Integer>] metrics hash with normalized names
|
|
24
|
-
def self.parse_usage_tokens(usage)
|
|
25
|
-
Braintrust::Trace.parse_anthropic_usage_tokens(usage)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Wrap an Anthropic::Client to automatically create spans for messages and responses
|
|
29
|
-
# Supports both synchronous and streaming requests
|
|
30
|
-
# @param client [Anthropic::Client] the Anthropic client to wrap
|
|
31
|
-
# @param tracer_provider [OpenTelemetry::SDK::Trace::TracerProvider] the tracer provider (defaults to global)
|
|
32
|
-
def self.wrap(client, tracer_provider: nil)
|
|
33
|
-
tracer_provider ||= ::OpenTelemetry.tracer_provider
|
|
34
|
-
|
|
35
|
-
# Wrap messages.create
|
|
36
|
-
wrap_messages_create(client, tracer_provider)
|
|
37
|
-
|
|
38
|
-
# Wrap messages.stream (Anthropic SDK always has this method)
|
|
39
|
-
wrap_messages_stream(client, tracer_provider)
|
|
40
|
-
|
|
41
|
-
client
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Wrap messages.create API
|
|
45
|
-
# @param client [Anthropic::Client] the Anthropic client
|
|
46
|
-
# @param tracer_provider [OpenTelemetry::SDK::Trace::TracerProvider] the tracer provider
|
|
47
|
-
def self.wrap_messages_create(client, tracer_provider)
|
|
48
|
-
# Create a wrapper module that intercepts messages.create
|
|
49
|
-
wrapper = Module.new do
|
|
50
|
-
define_method(:create) do |**params|
|
|
51
|
-
tracer = tracer_provider.tracer("braintrust")
|
|
52
|
-
|
|
53
|
-
tracer.in_span("anthropic.messages.create") do |span|
|
|
54
|
-
# Initialize metadata hash
|
|
55
|
-
metadata = {
|
|
56
|
-
"provider" => "anthropic",
|
|
57
|
-
"endpoint" => "/v1/messages"
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
# Capture request metadata fields
|
|
61
|
-
metadata_fields = %i[
|
|
62
|
-
model max_tokens temperature top_p top_k stop_sequences
|
|
63
|
-
stream tools tool_choice thinking metadata service_tier
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
metadata_fields.each do |field|
|
|
67
|
-
metadata[field.to_s] = params[field] if params.key?(field)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Build input messages array, prepending system prompt if present
|
|
71
|
-
input_messages = []
|
|
72
|
-
|
|
73
|
-
# Prepend system prompt as a message if present
|
|
74
|
-
if params[:system]
|
|
75
|
-
# System can be a string or array of text blocks
|
|
76
|
-
system_content = params[:system]
|
|
77
|
-
if system_content.is_a?(Array)
|
|
78
|
-
# Extract text from array of text blocks
|
|
79
|
-
system_text = system_content.map { |block|
|
|
80
|
-
block.is_a?(Hash) ? block[:text] : block
|
|
81
|
-
}.join("\n")
|
|
82
|
-
input_messages << {role: "system", content: system_text}
|
|
83
|
-
else
|
|
84
|
-
input_messages << {role: "system", content: system_content}
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Add user/assistant messages
|
|
89
|
-
if params[:messages]
|
|
90
|
-
messages_array = params[:messages].map(&:to_h)
|
|
91
|
-
input_messages.concat(messages_array)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Set input messages as JSON
|
|
95
|
-
if input_messages.any?
|
|
96
|
-
span.set_attribute("braintrust.input_json", JSON.generate(input_messages))
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Call the original method
|
|
100
|
-
response = super(**params)
|
|
101
|
-
|
|
102
|
-
# Format output as array of messages (same format as input)
|
|
103
|
-
if response.respond_to?(:content) && response.content
|
|
104
|
-
content_array = response.content.map(&:to_h)
|
|
105
|
-
output = [{
|
|
106
|
-
role: response.respond_to?(:role) ? response.role : "assistant",
|
|
107
|
-
content: content_array
|
|
108
|
-
}]
|
|
109
|
-
span.set_attribute("braintrust.output_json", JSON.generate(output))
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Set metrics (token usage with Anthropic-specific cache tokens)
|
|
113
|
-
if response.respond_to?(:usage) && response.usage
|
|
114
|
-
metrics = Braintrust::Trace::Anthropic.parse_usage_tokens(response.usage)
|
|
115
|
-
span.set_attribute("braintrust.metrics", JSON.generate(metrics)) unless metrics.empty?
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Add response metadata fields
|
|
119
|
-
if response.respond_to?(:stop_reason) && response.stop_reason
|
|
120
|
-
metadata["stop_reason"] = response.stop_reason
|
|
121
|
-
end
|
|
122
|
-
if response.respond_to?(:stop_sequence) && response.stop_sequence
|
|
123
|
-
metadata["stop_sequence"] = response.stop_sequence
|
|
124
|
-
end
|
|
125
|
-
# Update model if present in response (in case it was resolved from "latest")
|
|
126
|
-
if response.respond_to?(:model) && response.model
|
|
127
|
-
metadata["model"] = response.model
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Set metadata ONCE at the end with complete hash
|
|
131
|
-
span.set_attribute("braintrust.metadata", JSON.generate(metadata))
|
|
132
|
-
|
|
133
|
-
response
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Prepend the wrapper to the messages resource
|
|
139
|
-
client.messages.singleton_class.prepend(wrapper)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Wrap messages.stream API
|
|
143
|
-
# @param client [Anthropic::Client] the Anthropic client
|
|
144
|
-
# @param tracer_provider [OpenTelemetry::SDK::Trace::TracerProvider] the tracer provider
|
|
145
|
-
def self.wrap_messages_stream(client, tracer_provider)
|
|
146
|
-
# Create a wrapper module that intercepts messages.stream
|
|
147
|
-
wrapper = Module.new do
|
|
148
|
-
define_method(:stream) do |**params, &block|
|
|
149
|
-
tracer = tracer_provider.tracer("braintrust")
|
|
150
|
-
|
|
151
|
-
metadata = {
|
|
152
|
-
"provider" => "anthropic",
|
|
153
|
-
"endpoint" => "/v1/messages",
|
|
154
|
-
"stream" => true
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
# Start span with proper context
|
|
158
|
-
span = tracer.start_span("anthropic.messages.create")
|
|
159
|
-
|
|
160
|
-
# Capture request metadata fields
|
|
161
|
-
metadata_fields = %i[
|
|
162
|
-
model max_tokens temperature top_p top_k stop_sequences
|
|
163
|
-
tools tool_choice thinking metadata service_tier
|
|
164
|
-
]
|
|
165
|
-
|
|
166
|
-
metadata_fields.each do |field|
|
|
167
|
-
metadata[field.to_s] = params[field] if params.key?(field)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Build input messages array, prepending system prompt if present
|
|
171
|
-
input_messages = []
|
|
172
|
-
|
|
173
|
-
if params[:system]
|
|
174
|
-
system_content = params[:system]
|
|
175
|
-
if system_content.is_a?(Array)
|
|
176
|
-
system_text = system_content.map { |block|
|
|
177
|
-
block.is_a?(Hash) ? block[:text] : block
|
|
178
|
-
}.join("\n")
|
|
179
|
-
input_messages << {role: "system", content: system_text}
|
|
180
|
-
else
|
|
181
|
-
input_messages << {role: "system", content: system_content}
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
if params[:messages]
|
|
186
|
-
messages_array = params[:messages].map(&:to_h)
|
|
187
|
-
input_messages.concat(messages_array)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
if input_messages.any?
|
|
191
|
-
span.set_attribute("braintrust.input_json", JSON.generate(input_messages))
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Set initial metadata
|
|
195
|
-
span.set_attribute("braintrust.metadata", JSON.generate(metadata))
|
|
196
|
-
|
|
197
|
-
# Call the original stream method WITHOUT passing the block
|
|
198
|
-
# We'll handle the block ourselves to aggregate events
|
|
199
|
-
begin
|
|
200
|
-
stream = super(**params)
|
|
201
|
-
rescue => e
|
|
202
|
-
span.record_exception(e)
|
|
203
|
-
span.status = ::OpenTelemetry::Trace::Status.error("Anthropic API error: #{e.message}")
|
|
204
|
-
span.finish
|
|
205
|
-
raise
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Store references on the stream object itself for the wrapper
|
|
209
|
-
stream.instance_variable_set(:@braintrust_span, span)
|
|
210
|
-
stream.instance_variable_set(:@braintrust_metadata, metadata)
|
|
211
|
-
stream.instance_variable_set(:@braintrust_span_finished, false)
|
|
212
|
-
|
|
213
|
-
# Local helper for brevity
|
|
214
|
-
set_json_attr = ->(attr_name, obj) { Braintrust::Trace::Anthropic.set_json_attr(span, attr_name, obj) }
|
|
215
|
-
|
|
216
|
-
# Helper lambda to extract stream data and set span attributes
|
|
217
|
-
# This is DRY - used by both .each() and .text() wrappers
|
|
218
|
-
extract_stream_metadata = lambda do
|
|
219
|
-
# Extract the SDK's internal accumulated message (built during streaming)
|
|
220
|
-
acc_msg = stream.instance_variable_get(:@accumated_message_snapshot)
|
|
221
|
-
return unless acc_msg
|
|
222
|
-
|
|
223
|
-
# Set output from accumulated message
|
|
224
|
-
if acc_msg.respond_to?(:content) && acc_msg.content
|
|
225
|
-
content_array = acc_msg.content.map(&:to_h)
|
|
226
|
-
output = [{
|
|
227
|
-
role: acc_msg.respond_to?(:role) ? acc_msg.role : "assistant",
|
|
228
|
-
content: content_array
|
|
229
|
-
}]
|
|
230
|
-
set_json_attr.call("braintrust.output_json", output)
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Set metrics from accumulated message
|
|
234
|
-
if acc_msg.respond_to?(:usage) && acc_msg.usage
|
|
235
|
-
metrics = Braintrust::Trace::Anthropic.parse_usage_tokens(acc_msg.usage)
|
|
236
|
-
set_json_attr.call("braintrust.metrics", metrics) unless metrics.empty?
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Update metadata with response fields
|
|
240
|
-
if acc_msg.respond_to?(:stop_reason) && acc_msg.stop_reason
|
|
241
|
-
metadata["stop_reason"] = acc_msg.stop_reason
|
|
242
|
-
end
|
|
243
|
-
if acc_msg.respond_to?(:model) && acc_msg.model
|
|
244
|
-
metadata["model"] = acc_msg.model
|
|
245
|
-
end
|
|
246
|
-
set_json_attr.call("braintrust.metadata", metadata)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Helper lambda to finish span (prevents double-finishing via closure)
|
|
250
|
-
finish_braintrust_span = lambda do
|
|
251
|
-
return if stream.instance_variable_get(:@braintrust_span_finished)
|
|
252
|
-
stream.instance_variable_set(:@braintrust_span_finished, true)
|
|
253
|
-
|
|
254
|
-
extract_stream_metadata.call
|
|
255
|
-
span.finish
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# Wrap .each() to ensure span finishes after consumption
|
|
259
|
-
original_each = stream.method(:each)
|
|
260
|
-
stream.define_singleton_method(:each) do |&user_block|
|
|
261
|
-
# Consume stream, calling user's block for each event
|
|
262
|
-
# The SDK builds @accumated_message_snapshot internally
|
|
263
|
-
original_each.call(&user_block)
|
|
264
|
-
rescue => e
|
|
265
|
-
span.record_exception(e)
|
|
266
|
-
span.status = ::OpenTelemetry::Trace::Status.error("Streaming error: #{e.message}")
|
|
267
|
-
raise
|
|
268
|
-
ensure
|
|
269
|
-
# Extract accumulated message and finish span
|
|
270
|
-
finish_braintrust_span.call
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# Wrap .text() to return an Enumerable that ensures span finishes
|
|
274
|
-
original_text = stream.method(:text)
|
|
275
|
-
stream.define_singleton_method(:text) do
|
|
276
|
-
text_enum = original_text.call
|
|
277
|
-
|
|
278
|
-
# Return wrapper Enumerable that finishes span after consumption
|
|
279
|
-
Enumerator.new do |y|
|
|
280
|
-
# Consume text enumerable (this consumes underlying stream)
|
|
281
|
-
# The SDK builds @accumated_message_snapshot internally
|
|
282
|
-
text_enum.each { |text| y << text }
|
|
283
|
-
rescue => e
|
|
284
|
-
span.record_exception(e)
|
|
285
|
-
span.status = ::OpenTelemetry::Trace::Status.error("Streaming error: #{e.message}")
|
|
286
|
-
raise
|
|
287
|
-
ensure
|
|
288
|
-
# Extract accumulated message and finish span
|
|
289
|
-
finish_braintrust_span.call
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
# Wrap .close() to ensure span finishes even if stream not consumed
|
|
294
|
-
original_close = stream.method(:close)
|
|
295
|
-
stream.define_singleton_method(:close) do
|
|
296
|
-
original_close.call
|
|
297
|
-
ensure
|
|
298
|
-
# Finish span even if stream was closed early
|
|
299
|
-
finish_braintrust_span.call
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
# If a block was provided to stream(), call each with it immediately
|
|
303
|
-
if block
|
|
304
|
-
stream.each(&block)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
stream
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
# Prepend the wrapper to the messages resource
|
|
312
|
-
client.messages.singleton_class.prepend(wrapper)
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
end
|