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
@@ -1,377 +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 Contrib
10
- module Github
11
- module Alexrudall
12
- module RubyOpenAI
13
- # Helper to safely set a JSON attribute on a span
14
- # Only sets the attribute if obj is present
15
- # @param span [OpenTelemetry::Trace::Span] the span to set attribute on
16
- # @param attr_name [String] the attribute name (e.g., "braintrust.output_json")
17
- # @param obj [Object] the object to serialize to JSON
18
- # @return [void]
19
- def self.set_json_attr(span, attr_name, obj)
20
- return unless obj
21
- span.set_attribute(attr_name, JSON.generate(obj))
22
- end
23
-
24
- # Parse usage tokens from OpenAI API response
25
- # @param usage [Hash] usage hash from OpenAI response
26
- # @return [Hash<String, Integer>] metrics hash with normalized names
27
- def self.parse_usage_tokens(usage)
28
- Braintrust::Trace.parse_openai_usage_tokens(usage)
29
- end
30
-
31
- # Aggregate streaming chunks into a single response structure
32
- # @param chunks [Array<Hash>] array of chunk hashes from stream
33
- # @return [Hash] aggregated response with choices, usage, id, created, model
34
- def self.aggregate_streaming_chunks(chunks)
35
- return {} if chunks.empty?
36
-
37
- # Initialize aggregated structure
38
- aggregated = {
39
- "id" => nil,
40
- "created" => nil,
41
- "model" => nil,
42
- "usage" => nil,
43
- "choices" => []
44
- }
45
-
46
- # Track aggregated content for the first choice
47
- role = nil
48
- content = +""
49
-
50
- chunks.each do |chunk|
51
- # Capture top-level fields from any chunk that has them
52
- aggregated["id"] ||= chunk["id"]
53
- aggregated["created"] ||= chunk["created"]
54
- aggregated["model"] ||= chunk["model"]
55
-
56
- # Aggregate usage (usually only in last chunk if stream_options.include_usage is set)
57
- aggregated["usage"] = chunk["usage"] if chunk["usage"]
58
-
59
- # Aggregate content from first choice
60
- if chunk.dig("choices", 0, "delta", "role")
61
- role ||= chunk.dig("choices", 0, "delta", "role")
62
- end
63
- if chunk.dig("choices", 0, "delta", "content")
64
- content << chunk.dig("choices", 0, "delta", "content")
65
- end
66
- end
67
-
68
- # Build aggregated choices array
69
- aggregated["choices"] = [
70
- {
71
- "index" => 0,
72
- "message" => {
73
- "role" => role || "assistant",
74
- "content" => content
75
- },
76
- "finish_reason" => chunks.dig(-1, "choices", 0, "finish_reason")
77
- }
78
- ]
79
-
80
- aggregated
81
- end
82
-
83
- # Aggregate responses streaming chunks into a single response structure
84
- # @param chunks [Array<Hash>] array of chunk hashes from stream
85
- # @return [Hash] aggregated response with output, usage, id
86
- def self.aggregate_responses_chunks(chunks)
87
- return {} if chunks.empty?
88
-
89
- # Find the response.completed event which has the final response
90
- completed_chunk = chunks.find { |c| c["type"] == "response.completed" }
91
-
92
- if completed_chunk && completed_chunk["response"]
93
- response = completed_chunk["response"]
94
- return {
95
- "id" => response["id"],
96
- "output" => response["output"],
97
- "usage" => response["usage"]
98
- }
99
- end
100
-
101
- # Fallback if no completed event found
102
- {}
103
- end
104
-
105
- # Set span attributes from response data (works for both streaming and non-streaming)
106
- # @param span [OpenTelemetry::Trace::Span] the span to set attributes on
107
- # @param response_data [Hash] response hash with keys: choices, usage, id, created, model, system_fingerprint, service_tier
108
- # @param time_to_first_token [Float] time to first token in seconds
109
- # @param metadata [Hash] metadata hash to update with response fields
110
- def self.set_span_attributes(span, response_data, time_to_first_token, metadata)
111
- # Set output (choices) as JSON
112
- if response_data["choices"]&.any?
113
- set_json_attr(span, "braintrust.output_json", response_data["choices"])
114
- end
115
-
116
- # Set metrics (token usage + time_to_first_token)
117
- metrics = {}
118
- if response_data["usage"]
119
- metrics = parse_usage_tokens(response_data["usage"])
120
- end
121
- metrics["time_to_first_token"] = time_to_first_token || 0.0
122
- set_json_attr(span, "braintrust.metrics", metrics) unless metrics.empty?
123
-
124
- # Update metadata with response fields
125
- %w[id created model system_fingerprint service_tier].each do |field|
126
- metadata[field] = response_data[field] if response_data[field]
127
- end
128
- end
129
-
130
- # Wrap an OpenAI::Client (ruby-openai gem) to automatically create spans
131
- # Supports both synchronous and streaming requests
132
- # @param client [OpenAI::Client] the OpenAI client to wrap
133
- # @param tracer_provider [OpenTelemetry::SDK::Trace::TracerProvider] the tracer provider (defaults to global)
134
- def self.wrap(client, tracer_provider: nil)
135
- tracer_provider ||= ::OpenTelemetry.tracer_provider
136
-
137
- # Store tracer provider on the client for use by wrapper modules
138
- client.instance_variable_set(:@braintrust_tracer_provider, tracer_provider)
139
-
140
- # Wrap chat completions
141
- wrap_chat(client)
142
-
143
- # Wrap responses API if available
144
- wrap_responses(client) if client.respond_to?(:responses)
145
-
146
- client
147
- end
148
-
149
- # Wrap chat API
150
- # @param client [OpenAI::Client] the OpenAI client
151
- def self.wrap_chat(client)
152
- client.singleton_class.prepend(ChatWrapper)
153
- end
154
-
155
- # Wrap responses API
156
- # @param client [OpenAI::Client] the OpenAI client
157
- def self.wrap_responses(client)
158
- # Store tracer provider on the responses object for use by wrapper module
159
- responses_obj = client.responses
160
- responses_obj.instance_variable_set(:@braintrust_tracer_provider, client.instance_variable_get(:@braintrust_tracer_provider))
161
- responses_obj.singleton_class.prepend(ResponsesCreateWrapper)
162
- end
163
-
164
- # Wrapper module for chat completions
165
- module ChatWrapper
166
- def chat(parameters:)
167
- tracer_provider = @braintrust_tracer_provider
168
- tracer = tracer_provider.tracer("braintrust")
169
-
170
- tracer.in_span("Chat Completion") do |span|
171
- # Track start time for time_to_first_token
172
- start_time = Time.now
173
- time_to_first_token = nil
174
- is_streaming = parameters.key?(:stream) && parameters[:stream].is_a?(Proc)
175
-
176
- # Initialize metadata hash
177
- metadata = {
178
- "provider" => "openai",
179
- "endpoint" => "/v1/chat/completions"
180
- }
181
-
182
- # Capture request metadata fields
183
- metadata_fields = %w[
184
- model frequency_penalty logit_bias logprobs max_tokens n
185
- presence_penalty response_format seed service_tier stop
186
- stream stream_options temperature top_p top_logprobs
187
- tools tool_choice parallel_tool_calls user functions function_call
188
- ]
189
-
190
- metadata_fields.each do |field|
191
- field_sym = field.to_sym
192
- if parameters.key?(field_sym)
193
- # Special handling for stream parameter (it's a Proc)
194
- metadata[field] = if field == "stream"
195
- true # Just mark as streaming
196
- else
197
- parameters[field_sym]
198
- end
199
- end
200
- end
201
-
202
- # Set input messages as JSON
203
- if parameters[:messages]
204
- RubyOpenAI.set_json_attr(span, "braintrust.input_json", parameters[:messages])
205
- end
206
-
207
- # Wrap streaming callback if present to capture time to first token and aggregate chunks
208
- aggregated_chunks = []
209
- if is_streaming
210
- original_stream_proc = parameters[:stream]
211
- parameters = parameters.dup
212
- parameters[:stream] = proc do |chunk, bytesize|
213
- # Capture time to first token on first chunk
214
- time_to_first_token ||= Time.now - start_time
215
- # Aggregate chunks for later processing
216
- aggregated_chunks << chunk
217
- # Call original callback
218
- original_stream_proc.call(chunk, bytesize)
219
- end
220
- end
221
-
222
- begin
223
- # Call the original method
224
- response = super(parameters: parameters)
225
-
226
- # Calculate time to first token for non-streaming
227
- time_to_first_token ||= Time.now - start_time unless is_streaming
228
-
229
- # Process response data
230
- if is_streaming && !aggregated_chunks.empty?
231
- # Aggregate streaming chunks into response-like structure
232
- aggregated_response = RubyOpenAI.aggregate_streaming_chunks(aggregated_chunks)
233
- RubyOpenAI.set_span_attributes(span, aggregated_response, time_to_first_token, metadata)
234
- else
235
- # Non-streaming: use response object directly
236
- RubyOpenAI.set_span_attributes(span, response || {}, time_to_first_token, metadata)
237
- end
238
-
239
- # Set metadata ONCE at the end with complete hash
240
- RubyOpenAI.set_json_attr(span, "braintrust.metadata", metadata)
241
-
242
- response
243
- rescue => e
244
- # Record exception in span
245
- span.record_exception(e)
246
- span.status = OpenTelemetry::Trace::Status.error("Exception: #{e.class} - #{e.message}")
247
- raise
248
- end
249
- end
250
- end
251
- end
252
-
253
- # Wrapper module for responses API create method
254
- module ResponsesCreateWrapper
255
- def create(parameters:)
256
- tracer_provider = @braintrust_tracer_provider
257
- tracer = tracer_provider.tracer("braintrust")
258
-
259
- tracer.in_span("openai.responses.create") do |span|
260
- # Track start time for time_to_first_token
261
- start_time = Time.now
262
- time_to_first_token = nil
263
- is_streaming = parameters.key?(:stream) && parameters[:stream].is_a?(Proc)
264
-
265
- # Initialize metadata hash
266
- metadata = {
267
- "provider" => "openai",
268
- "endpoint" => "/v1/responses"
269
- }
270
-
271
- # Capture request metadata fields
272
- metadata_fields = %w[
273
- model instructions modalities tools parallel_tool_calls
274
- tool_choice temperature max_tokens top_p frequency_penalty
275
- presence_penalty seed user store response_format
276
- reasoning previous_response_id truncation
277
- ]
278
-
279
- metadata_fields.each do |field|
280
- field_sym = field.to_sym
281
- if parameters.key?(field_sym)
282
- metadata[field] = parameters[field_sym]
283
- end
284
- end
285
-
286
- # Mark as streaming if applicable
287
- metadata["stream"] = true if is_streaming
288
-
289
- # Set input as JSON
290
- if parameters[:input]
291
- RubyOpenAI.set_json_attr(span, "braintrust.input_json", parameters[:input])
292
- end
293
-
294
- # Wrap streaming callback if present to capture time to first token and aggregate chunks
295
- aggregated_chunks = []
296
- if is_streaming
297
- original_stream_proc = parameters[:stream]
298
- parameters = parameters.dup
299
- parameters[:stream] = proc do |chunk, event|
300
- # Capture time to first token on first chunk
301
- time_to_first_token ||= Time.now - start_time
302
- # Aggregate chunks for later processing
303
- aggregated_chunks << chunk
304
- # Call original callback
305
- original_stream_proc.call(chunk, event)
306
- end
307
- end
308
-
309
- begin
310
- # Call the original method
311
- response = super(parameters: parameters)
312
-
313
- # Calculate time to first token for non-streaming
314
- time_to_first_token ||= Time.now - start_time unless is_streaming
315
-
316
- # Process response data
317
- if is_streaming && !aggregated_chunks.empty?
318
- # Aggregate streaming chunks into response-like structure
319
- aggregated_response = RubyOpenAI.aggregate_responses_chunks(aggregated_chunks)
320
-
321
- # Set output as JSON
322
- if aggregated_response["output"]
323
- RubyOpenAI.set_json_attr(span, "braintrust.output_json", aggregated_response["output"])
324
- end
325
-
326
- # Set metrics (token usage + time_to_first_token)
327
- metrics = {}
328
- if aggregated_response["usage"]
329
- metrics = RubyOpenAI.parse_usage_tokens(aggregated_response["usage"])
330
- end
331
- metrics["time_to_first_token"] = time_to_first_token || 0.0
332
- RubyOpenAI.set_json_attr(span, "braintrust.metrics", metrics) unless metrics.empty?
333
-
334
- # Update metadata with response fields
335
- metadata["id"] = aggregated_response["id"] if aggregated_response["id"]
336
- else
337
- # Non-streaming: use response object directly
338
- if response && response["output"]
339
- RubyOpenAI.set_json_attr(span, "braintrust.output_json", response["output"])
340
- end
341
-
342
- # Set metrics (token usage + time_to_first_token)
343
- metrics = {}
344
- if response && response["usage"]
345
- metrics = RubyOpenAI.parse_usage_tokens(response["usage"])
346
- end
347
- metrics["time_to_first_token"] = time_to_first_token || 0.0
348
- RubyOpenAI.set_json_attr(span, "braintrust.metrics", metrics) unless metrics.empty?
349
-
350
- # Update metadata with response fields
351
- metadata["id"] = response["id"] if response && response["id"]
352
- end
353
-
354
- # Set metadata ONCE at the end with complete hash
355
- RubyOpenAI.set_json_attr(span, "braintrust.metadata", metadata)
356
-
357
- response
358
- rescue => e
359
- # Record exception in span
360
- span.record_exception(e)
361
- span.status = OpenTelemetry::Trace::Status.error("Exception: #{e.class} - #{e.message}")
362
- raise
363
- end
364
- end
365
- end
366
- end
367
- end
368
- end
369
- end
370
- end
371
-
372
- # Backwards compatibility: this module was originally at Braintrust::Trace::AlexRudall::RubyOpenAI
373
- module AlexRudall
374
- RubyOpenAI = Contrib::Github::Alexrudall::RubyOpenAI
375
- end
376
- end
377
- end