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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +213 -180
  3. data/exe/braintrust +143 -0
  4. data/lib/braintrust/contrib/anthropic/deprecated.rb +24 -0
  5. data/lib/braintrust/contrib/anthropic/instrumentation/common.rb +53 -0
  6. data/lib/braintrust/contrib/anthropic/instrumentation/messages.rb +232 -0
  7. data/lib/braintrust/contrib/anthropic/integration.rb +53 -0
  8. data/lib/braintrust/contrib/anthropic/patcher.rb +62 -0
  9. data/lib/braintrust/contrib/context.rb +56 -0
  10. data/lib/braintrust/contrib/integration.rb +160 -0
  11. data/lib/braintrust/contrib/openai/deprecated.rb +22 -0
  12. data/lib/braintrust/contrib/openai/instrumentation/chat.rb +298 -0
  13. data/lib/braintrust/contrib/openai/instrumentation/common.rb +134 -0
  14. data/lib/braintrust/contrib/openai/instrumentation/responses.rb +187 -0
  15. data/lib/braintrust/contrib/openai/integration.rb +58 -0
  16. data/lib/braintrust/contrib/openai/patcher.rb +130 -0
  17. data/lib/braintrust/contrib/patcher.rb +76 -0
  18. data/lib/braintrust/contrib/rails/railtie.rb +16 -0
  19. data/lib/braintrust/contrib/registry.rb +107 -0
  20. data/lib/braintrust/contrib/ruby_llm/deprecated.rb +45 -0
  21. data/lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb +464 -0
  22. data/lib/braintrust/contrib/ruby_llm/instrumentation/common.rb +58 -0
  23. data/lib/braintrust/contrib/ruby_llm/integration.rb +54 -0
  24. data/lib/braintrust/contrib/ruby_llm/patcher.rb +44 -0
  25. data/lib/braintrust/contrib/ruby_openai/deprecated.rb +24 -0
  26. data/lib/braintrust/contrib/ruby_openai/instrumentation/chat.rb +149 -0
  27. data/lib/braintrust/contrib/ruby_openai/instrumentation/common.rb +138 -0
  28. data/lib/braintrust/contrib/ruby_openai/instrumentation/responses.rb +146 -0
  29. data/lib/braintrust/contrib/ruby_openai/integration.rb +58 -0
  30. data/lib/braintrust/contrib/ruby_openai/patcher.rb +85 -0
  31. data/lib/braintrust/contrib/setup.rb +168 -0
  32. data/lib/braintrust/contrib/support/openai.rb +72 -0
  33. data/lib/braintrust/contrib/support/otel.rb +23 -0
  34. data/lib/braintrust/contrib.rb +205 -0
  35. data/lib/braintrust/internal/env.rb +33 -0
  36. data/lib/braintrust/internal/time.rb +44 -0
  37. data/lib/braintrust/setup.rb +50 -0
  38. data/lib/braintrust/state.rb +5 -0
  39. data/lib/braintrust/trace.rb +0 -51
  40. data/lib/braintrust/version.rb +1 -1
  41. data/lib/braintrust.rb +10 -1
  42. metadata +38 -7
  43. data/lib/braintrust/trace/contrib/anthropic.rb +0 -316
  44. data/lib/braintrust/trace/contrib/github.com/alexrudall/ruby-openai/ruby-openai.rb +0 -377
  45. data/lib/braintrust/trace/contrib/github.com/crmne/ruby_llm.rb +0 -631
  46. data/lib/braintrust/trace/contrib/openai.rb +0 -611
  47. 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.12
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