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
|
@@ -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
|