braintrust 0.1.0 → 0.1.2
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 +1 -0
- data/lib/braintrust/api/functions.rb +11 -0
- data/lib/braintrust/contrib/anthropic/instrumentation/beta_messages.rb +242 -0
- data/lib/braintrust/contrib/anthropic/integration.rb +2 -2
- data/lib/braintrust/contrib/anthropic/patcher.rb +83 -0
- data/lib/braintrust/contrib/openai/instrumentation/moderations.rb +93 -0
- data/lib/braintrust/contrib/openai/integration.rb +1 -1
- data/lib/braintrust/contrib/openai/patcher.rb +43 -0
- data/lib/braintrust/contrib/ruby_openai/instrumentation/moderations.rb +94 -0
- data/lib/braintrust/contrib/ruby_openai/integration.rb +1 -1
- data/lib/braintrust/contrib/ruby_openai/patcher.rb +35 -0
- data/lib/braintrust/internal/env.rb +6 -0
- data/lib/braintrust/internal/template.rb +91 -0
- data/lib/braintrust/prompt.rb +143 -0
- data/lib/braintrust/state.rb +1 -1
- data/lib/braintrust/trace.rb +41 -0
- data/lib/braintrust/vendor/mustache/context.rb +180 -0
- data/lib/braintrust/vendor/mustache/context_miss.rb +22 -0
- data/lib/braintrust/vendor/mustache/enumerable.rb +14 -0
- data/lib/braintrust/vendor/mustache/generator.rb +188 -0
- data/lib/braintrust/vendor/mustache/mustache.rb +260 -0
- data/lib/braintrust/vendor/mustache/parser.rb +364 -0
- data/lib/braintrust/vendor/mustache/settings.rb +252 -0
- data/lib/braintrust/vendor/mustache/template.rb +138 -0
- data/lib/braintrust/vendor/mustache/utils.rb +42 -0
- data/lib/braintrust/vendor/mustache.rb +16 -0
- data/lib/braintrust/version.rb +1 -1
- data/lib/braintrust.rb +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 626876b443795d28b4ba5d12f8bf10381c3052d5d196adb01207d545303f3d1e
|
|
4
|
+
data.tar.gz: 347ca89ea9f485ca6521a38c067bdd15074db4e6a4757523901888a8d4cc3e9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b827a4f92e2bc4b39e41174e62dacdc431fdc0b6c8d13882bdcaaa369af9621174fc01bafa3d0d594b71464ea374c6904e9834631957951dad87d6583a58dc9
|
|
7
|
+
data.tar.gz: bb6f2d3807765ef4ad591849e0972379fc3f97ef8d90bda0785e1d4dab87ce5e91d954d9d3c8fc7eff6c9295d120d6cbe07acb5bb348873c842d791a3fbdce84
|
data/README.md
CHANGED
|
@@ -136,6 +136,7 @@ Braintrust.init(
|
|
|
136
136
|
| `BRAINTRUST_AUTO_INSTRUMENT` | Set to `false` to disable auto-instrumentation |
|
|
137
137
|
| `BRAINTRUST_DEBUG` | Set to `true` to enable debug logging |
|
|
138
138
|
| `BRAINTRUST_DEFAULT_PROJECT` | Default project for spans |
|
|
139
|
+
| `BRAINTRUST_FLUSH_ON_EXIT` | Set to `false` to disable automatic span flushing on program exit |
|
|
139
140
|
| `BRAINTRUST_INSTRUMENT_EXCEPT` | Comma-separated list of integrations to skip |
|
|
140
141
|
| `BRAINTRUST_INSTRUMENT_ONLY` | Comma-separated list of integrations to enable (e.g., `openai,anthropic`) |
|
|
141
142
|
| `BRAINTRUST_ORG_NAME` | Organization name |
|
|
@@ -85,6 +85,17 @@ module Braintrust
|
|
|
85
85
|
http_post_json("/v1/function/#{id}/invoke", payload)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Get a function by ID (includes full prompt_data)
|
|
89
|
+
# GET /v1/function/{id}
|
|
90
|
+
# @param id [String] Function UUID
|
|
91
|
+
# @param version [String, nil] Retrieve prompt at a specific version (transaction ID or version identifier)
|
|
92
|
+
# @return [Hash] Full function data including prompt_data
|
|
93
|
+
def get(id:, version: nil)
|
|
94
|
+
params = {}
|
|
95
|
+
params["version"] = version if version
|
|
96
|
+
http_get("/v1/function/#{id}", params)
|
|
97
|
+
end
|
|
98
|
+
|
|
88
99
|
# Delete a function by ID
|
|
89
100
|
# DELETE /v1/function/{id}
|
|
90
101
|
# @param id [String] Function UUID
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opentelemetry/sdk"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../../support/otel"
|
|
6
|
+
require_relative "common"
|
|
7
|
+
require_relative "../../../internal/time"
|
|
8
|
+
|
|
9
|
+
module Braintrust
|
|
10
|
+
module Contrib
|
|
11
|
+
module Anthropic
|
|
12
|
+
module Instrumentation
|
|
13
|
+
# Beta Messages instrumentation for Anthropic.
|
|
14
|
+
# Wraps client.beta.messages.create() and stream() methods to create spans.
|
|
15
|
+
#
|
|
16
|
+
# @note Beta APIs are experimental and subject to change between SDK versions.
|
|
17
|
+
# This module includes defensive coding to handle response format changes.
|
|
18
|
+
module BetaMessages
|
|
19
|
+
def self.included(base)
|
|
20
|
+
base.prepend(InstanceMethods) unless applied?(base)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.applied?(base)
|
|
24
|
+
base.ancestors.include?(InstanceMethods)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module InstanceMethods
|
|
28
|
+
# Standard metadata fields (shared with stable API)
|
|
29
|
+
METADATA_FIELDS = %i[
|
|
30
|
+
model max_tokens temperature top_p top_k stop_sequences
|
|
31
|
+
stream tools tool_choice thinking metadata service_tier
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# Beta-specific metadata fields
|
|
35
|
+
BETA_METADATA_FIELDS = %i[
|
|
36
|
+
betas output_format
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# Wrap synchronous beta.messages.create
|
|
40
|
+
def create(**params)
|
|
41
|
+
client = instance_variable_get(:@client)
|
|
42
|
+
tracer = Braintrust::Contrib.tracer_for(client)
|
|
43
|
+
|
|
44
|
+
tracer.in_span("anthropic.messages.create") do |span|
|
|
45
|
+
# Pre-call instrumentation (swallow errors)
|
|
46
|
+
metadata = nil
|
|
47
|
+
begin
|
|
48
|
+
metadata = build_metadata(params)
|
|
49
|
+
set_input(span, params)
|
|
50
|
+
rescue => e
|
|
51
|
+
Braintrust::Log.debug("Beta API: Failed to capture request: #{e.message}")
|
|
52
|
+
metadata ||= {"provider" => "anthropic", "api_version" => "beta"}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# API call - let errors propagate naturally
|
|
56
|
+
response = nil
|
|
57
|
+
time_to_first_token = Braintrust::Internal::Time.measure do
|
|
58
|
+
response = super(**params)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Post-call instrumentation (swallow errors)
|
|
62
|
+
begin
|
|
63
|
+
set_output(span, response)
|
|
64
|
+
set_metrics(span, response, time_to_first_token)
|
|
65
|
+
finalize_metadata(span, metadata, response)
|
|
66
|
+
rescue => e
|
|
67
|
+
Braintrust::Log.debug("Beta API: Failed to capture response: #{e.message}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
response
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Wrap streaming beta.messages.stream
|
|
75
|
+
# Stores context on stream object for span creation during consumption
|
|
76
|
+
def stream(**params)
|
|
77
|
+
client = instance_variable_get(:@client)
|
|
78
|
+
tracer = Braintrust::Contrib.tracer_for(client)
|
|
79
|
+
|
|
80
|
+
# Pre-call instrumentation (swallow errors)
|
|
81
|
+
metadata = nil
|
|
82
|
+
begin
|
|
83
|
+
metadata = build_metadata(params, stream: true)
|
|
84
|
+
rescue => e
|
|
85
|
+
Braintrust::Log.debug("Beta API: Failed to build stream metadata: #{e.message}")
|
|
86
|
+
metadata = {"provider" => "anthropic", "api_version" => "beta", "stream" => true}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# API call - let errors propagate naturally
|
|
90
|
+
stream_obj = super
|
|
91
|
+
|
|
92
|
+
# Post-call instrumentation (swallow errors)
|
|
93
|
+
begin
|
|
94
|
+
Braintrust::Contrib::Context.set!(stream_obj,
|
|
95
|
+
tracer: tracer,
|
|
96
|
+
params: params,
|
|
97
|
+
metadata: metadata,
|
|
98
|
+
messages_instance: self,
|
|
99
|
+
start_time: Braintrust::Internal::Time.measure)
|
|
100
|
+
rescue => e
|
|
101
|
+
Braintrust::Log.debug("Beta API: Failed to set stream context: #{e.message}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
stream_obj
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def finalize_stream_span(span, stream_obj, metadata, time_to_first_token)
|
|
110
|
+
if stream_obj.respond_to?(:accumulated_message)
|
|
111
|
+
begin
|
|
112
|
+
msg = stream_obj.accumulated_message
|
|
113
|
+
set_output(span, msg)
|
|
114
|
+
set_metrics(span, msg, time_to_first_token)
|
|
115
|
+
metadata["stop_reason"] = msg.stop_reason if msg.respond_to?(:stop_reason) && msg.stop_reason
|
|
116
|
+
metadata["model"] = msg.model if msg.respond_to?(:model) && msg.model
|
|
117
|
+
rescue => e
|
|
118
|
+
Braintrust::Log.debug("Beta API: Failed to get accumulated message: #{e.message}")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
Support::OTel.set_json_attr(span, "braintrust.metadata", metadata)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_metadata(params, stream: false)
|
|
125
|
+
metadata = {
|
|
126
|
+
"provider" => "anthropic",
|
|
127
|
+
"endpoint" => "/v1/messages",
|
|
128
|
+
"api_version" => "beta"
|
|
129
|
+
}
|
|
130
|
+
metadata["stream"] = true if stream
|
|
131
|
+
|
|
132
|
+
# Capture standard fields
|
|
133
|
+
METADATA_FIELDS.each do |field|
|
|
134
|
+
metadata[field.to_s] = params[field] if params.key?(field)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Capture beta-specific fields with defensive handling
|
|
138
|
+
capture_beta_fields(metadata, params)
|
|
139
|
+
|
|
140
|
+
metadata
|
|
141
|
+
rescue => e
|
|
142
|
+
Braintrust::Log.debug("Beta API: Failed to build metadata: #{e.message}")
|
|
143
|
+
{"provider" => "anthropic", "api_version" => "beta"}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def capture_beta_fields(metadata, params)
|
|
147
|
+
# Capture betas array (e.g., ["structured-outputs-2025-11-13"])
|
|
148
|
+
if params.key?(:betas)
|
|
149
|
+
betas = params[:betas]
|
|
150
|
+
metadata["betas"] = betas.is_a?(Array) ? betas : [betas]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Capture output_format for structured outputs
|
|
154
|
+
if params.key?(:output_format)
|
|
155
|
+
output_format = params[:output_format]
|
|
156
|
+
metadata["output_format"] = begin
|
|
157
|
+
if output_format.respond_to?(:to_h)
|
|
158
|
+
output_format.to_h
|
|
159
|
+
else
|
|
160
|
+
output_format
|
|
161
|
+
end
|
|
162
|
+
rescue
|
|
163
|
+
output_format.to_s
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def set_input(span, params)
|
|
169
|
+
input_messages = []
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
if params[:system]
|
|
173
|
+
system_content = params[:system]
|
|
174
|
+
if system_content.is_a?(Array)
|
|
175
|
+
system_text = system_content.map { |blk|
|
|
176
|
+
blk.is_a?(Hash) ? blk[:text] : blk
|
|
177
|
+
}.join("\n")
|
|
178
|
+
input_messages << {role: "system", content: system_text}
|
|
179
|
+
else
|
|
180
|
+
input_messages << {role: "system", content: system_content}
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if params[:messages]
|
|
185
|
+
messages_array = params[:messages].map { |m| m.respond_to?(:to_h) ? m.to_h : m }
|
|
186
|
+
input_messages.concat(messages_array)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
Support::OTel.set_json_attr(span, "braintrust.input_json", input_messages) if input_messages.any?
|
|
190
|
+
rescue => e
|
|
191
|
+
Braintrust::Log.debug("Beta API: Failed to capture input: #{e.message}")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def set_output(span, response)
|
|
196
|
+
return unless response
|
|
197
|
+
|
|
198
|
+
begin
|
|
199
|
+
return unless response.respond_to?(:content) && response.content
|
|
200
|
+
|
|
201
|
+
content_array = response.content.map { |c| c.respond_to?(:to_h) ? c.to_h : c }
|
|
202
|
+
output = [{
|
|
203
|
+
role: response.respond_to?(:role) ? response.role : "assistant",
|
|
204
|
+
content: content_array
|
|
205
|
+
}]
|
|
206
|
+
Support::OTel.set_json_attr(span, "braintrust.output_json", output)
|
|
207
|
+
rescue => e
|
|
208
|
+
Braintrust::Log.debug("Beta API: Failed to capture output: #{e.message}")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def set_metrics(span, response, time_to_first_token)
|
|
213
|
+
metrics = {}
|
|
214
|
+
|
|
215
|
+
begin
|
|
216
|
+
if response.respond_to?(:usage) && response.usage
|
|
217
|
+
metrics = Common.parse_usage_tokens(response.usage)
|
|
218
|
+
end
|
|
219
|
+
metrics["time_to_first_token"] = time_to_first_token if time_to_first_token
|
|
220
|
+
Support::OTel.set_json_attr(span, "braintrust.metrics", metrics) unless metrics.empty?
|
|
221
|
+
rescue => e
|
|
222
|
+
Braintrust::Log.debug("Beta API: Failed to capture metrics: #{e.message}")
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def finalize_metadata(span, metadata, response)
|
|
227
|
+
begin
|
|
228
|
+
metadata["stop_reason"] = response.stop_reason if response.respond_to?(:stop_reason) && response.stop_reason
|
|
229
|
+
metadata["stop_sequence"] = response.stop_sequence if response.respond_to?(:stop_sequence) && response.stop_sequence
|
|
230
|
+
metadata["model"] = response.model if response.respond_to?(:model) && response.model
|
|
231
|
+
rescue => e
|
|
232
|
+
Braintrust::Log.debug("Beta API: Failed to finalize metadata: #{e.message}")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
Support::OTel.set_json_attr(span, "braintrust.metadata", metadata)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -40,12 +40,12 @@ module Braintrust
|
|
|
40
40
|
defined?(::Anthropic::Client) ? true : false
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# Lazy-load the
|
|
43
|
+
# Lazy-load the patchers only when actually patching.
|
|
44
44
|
# This keeps the integration stub lightweight.
|
|
45
45
|
# @return [Array<Class>] The patcher classes
|
|
46
46
|
def self.patchers
|
|
47
47
|
require_relative "patcher"
|
|
48
|
-
[MessagesPatcher]
|
|
48
|
+
[MessagesPatcher, BetaMessagesPatcher]
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../patcher"
|
|
4
4
|
require_relative "instrumentation/messages"
|
|
5
|
+
require_relative "instrumentation/beta_messages"
|
|
5
6
|
|
|
6
7
|
module Braintrust
|
|
7
8
|
module Contrib
|
|
@@ -57,6 +58,88 @@ module Braintrust
|
|
|
57
58
|
end
|
|
58
59
|
end
|
|
59
60
|
end
|
|
61
|
+
|
|
62
|
+
# Patcher for Anthropic beta messages API.
|
|
63
|
+
# Instruments client.beta.messages.create and stream methods.
|
|
64
|
+
#
|
|
65
|
+
# @note Beta APIs are experimental and subject to change between SDK versions.
|
|
66
|
+
# Braintrust will make reasonable efforts to maintain compatibility, but
|
|
67
|
+
# breaking changes may require SDK updates.
|
|
68
|
+
#
|
|
69
|
+
# @see https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs
|
|
70
|
+
# for structured outputs documentation
|
|
71
|
+
class BetaMessagesPatcher < Braintrust::Contrib::Patcher
|
|
72
|
+
# Version constraints for beta patcher.
|
|
73
|
+
# Set MAXIMUM_VERSION when a breaking change is discovered to disable
|
|
74
|
+
# beta instrumentation on incompatible versions until a fix is released.
|
|
75
|
+
# Currently nil = rely on class existence check only.
|
|
76
|
+
MAXIMUM_VERSION = nil
|
|
77
|
+
|
|
78
|
+
class << self
|
|
79
|
+
def applicable?
|
|
80
|
+
return false unless defined?(::Anthropic::Client)
|
|
81
|
+
return false unless defined?(::Anthropic::Resources::Beta::Messages)
|
|
82
|
+
return false if MAXIMUM_VERSION && !version_compatible?
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def patched?(**options)
|
|
87
|
+
target_class = get_singleton_class(options[:target]) || ::Anthropic::Resources::Beta::Messages
|
|
88
|
+
Instrumentation::BetaMessages.applied?(target_class)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Perform the actual patching.
|
|
92
|
+
# @param options [Hash] Configuration options passed from integration
|
|
93
|
+
# @option options [Object] :target Optional target instance to patch
|
|
94
|
+
# @option options [OpenTelemetry::SDK::Trace::TracerProvider] :tracer_provider Optional tracer provider
|
|
95
|
+
# @return [void]
|
|
96
|
+
def perform_patch(**options)
|
|
97
|
+
return unless applicable?
|
|
98
|
+
|
|
99
|
+
Braintrust::Log.debug("Instrumenting Anthropic beta.messages API (experimental)")
|
|
100
|
+
|
|
101
|
+
# MessageStream is shared with stable API - already patched by MessagesPatcher.
|
|
102
|
+
# The BetaMessages instrumentation sets api_version: "beta" in context,
|
|
103
|
+
# which MessageStream uses to include in metadata.
|
|
104
|
+
patch_message_stream
|
|
105
|
+
|
|
106
|
+
if options[:target]
|
|
107
|
+
# Instance-level (for only this client instance)
|
|
108
|
+
raise ArgumentError, "target must be a kind of ::Anthropic::Client" unless options[:target].is_a?(::Anthropic::Client)
|
|
109
|
+
|
|
110
|
+
get_singleton_class(options[:target]).include(Instrumentation::BetaMessages)
|
|
111
|
+
else
|
|
112
|
+
# Class-level (for all client instances)
|
|
113
|
+
::Anthropic::Resources::Beta::Messages.include(Instrumentation::BetaMessages)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def version_compatible?
|
|
120
|
+
return true unless MAXIMUM_VERSION
|
|
121
|
+
|
|
122
|
+
spec = Gem.loaded_specs["anthropic"]
|
|
123
|
+
return true unless spec
|
|
124
|
+
|
|
125
|
+
spec.version <= Gem::Version.new(MAXIMUM_VERSION)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def get_singleton_class(client)
|
|
129
|
+
client&.beta&.messages&.singleton_class
|
|
130
|
+
rescue
|
|
131
|
+
# Defensive: beta namespace may not exist or may have changed
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def patch_message_stream
|
|
136
|
+
return unless defined?(::Anthropic::Helpers::Streaming::MessageStream)
|
|
137
|
+
return if Instrumentation::MessageStream.applied?(::Anthropic::Helpers::Streaming::MessageStream)
|
|
138
|
+
|
|
139
|
+
::Anthropic::Helpers::Streaming::MessageStream.include(Instrumentation::MessageStream)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
60
143
|
end
|
|
61
144
|
end
|
|
62
145
|
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opentelemetry/sdk"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "../../../internal/time"
|
|
7
|
+
require_relative "../../support/otel"
|
|
8
|
+
|
|
9
|
+
module Braintrust
|
|
10
|
+
module Contrib
|
|
11
|
+
module OpenAI
|
|
12
|
+
module Instrumentation
|
|
13
|
+
# Moderations API instrumentation for OpenAI.
|
|
14
|
+
# Wraps create() method to create spans.
|
|
15
|
+
module Moderations
|
|
16
|
+
def self.included(base)
|
|
17
|
+
base.prepend(InstanceMethods) unless applied?(base)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.applied?(base)
|
|
21
|
+
base.ancestors.include?(InstanceMethods)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
METADATA_FIELDS = %i[
|
|
25
|
+
model
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
module InstanceMethods
|
|
29
|
+
# Wrap non-streaming create method
|
|
30
|
+
def create(**params)
|
|
31
|
+
client = instance_variable_get(:@client)
|
|
32
|
+
tracer = Braintrust::Contrib.tracer_for(client)
|
|
33
|
+
|
|
34
|
+
tracer.in_span("openai.moderations.create") do |span|
|
|
35
|
+
metadata = build_metadata(params)
|
|
36
|
+
|
|
37
|
+
set_input(span, params)
|
|
38
|
+
|
|
39
|
+
response = nil
|
|
40
|
+
time_to_first_token = Braintrust::Internal::Time.measure do
|
|
41
|
+
response = super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
set_output(span, response)
|
|
45
|
+
set_metrics(span, time_to_first_token)
|
|
46
|
+
finalize_metadata(span, metadata, response)
|
|
47
|
+
|
|
48
|
+
response
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def build_metadata(params)
|
|
55
|
+
metadata = {
|
|
56
|
+
"provider" => "openai",
|
|
57
|
+
"endpoint" => "/v1/moderations"
|
|
58
|
+
}
|
|
59
|
+
Moderations::METADATA_FIELDS.each do |field|
|
|
60
|
+
metadata[field.to_s] = params[field] if params.key?(field)
|
|
61
|
+
end
|
|
62
|
+
metadata
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def set_input(span, params)
|
|
66
|
+
return unless params[:input]
|
|
67
|
+
|
|
68
|
+
Support::OTel.set_json_attr(span, "braintrust.input_json", params[:input])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def set_output(span, response)
|
|
72
|
+
return unless response.respond_to?(:results) && response.results
|
|
73
|
+
|
|
74
|
+
Support::OTel.set_json_attr(span, "braintrust.output_json", response.results)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def set_metrics(span, time_to_first_token)
|
|
78
|
+
metrics = {}
|
|
79
|
+
metrics["time_to_first_token"] = time_to_first_token
|
|
80
|
+
Support::OTel.set_json_attr(span, "braintrust.metrics", metrics) unless metrics.empty?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def finalize_metadata(span, metadata, response)
|
|
84
|
+
metadata["id"] = response.id if response.respond_to?(:id) && response.id
|
|
85
|
+
metadata["model"] = response.model if response.respond_to?(:model) && response.model
|
|
86
|
+
Support::OTel.set_json_attr(span, "braintrust.metadata", metadata)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../patcher"
|
|
4
4
|
require_relative "instrumentation/chat"
|
|
5
5
|
require_relative "instrumentation/responses"
|
|
6
|
+
require_relative "instrumentation/moderations"
|
|
6
7
|
|
|
7
8
|
module Braintrust
|
|
8
9
|
module Contrib
|
|
@@ -125,6 +126,48 @@ module Braintrust
|
|
|
125
126
|
end
|
|
126
127
|
end
|
|
127
128
|
end
|
|
129
|
+
|
|
130
|
+
# Patcher for OpenAI Moderations API - implements class-level patching.
|
|
131
|
+
# All new OpenAI::Client instances created after patch! will be automatically instrumented.
|
|
132
|
+
class ModerationsPatcher < Braintrust::Contrib::Patcher
|
|
133
|
+
class << self
|
|
134
|
+
def applicable?
|
|
135
|
+
defined?(::OpenAI::Client) && ::OpenAI::Client.instance_methods.include?(:moderations)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def patched?(**options)
|
|
139
|
+
# Use the target's singleton class if provided, otherwise check the base class.
|
|
140
|
+
target_class = get_singleton_class(options[:target]) || ::OpenAI::Resources::Moderations
|
|
141
|
+
|
|
142
|
+
Instrumentation::Moderations.applied?(target_class)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Perform the actual patching.
|
|
146
|
+
# @param options [Hash] Configuration options passed from integration
|
|
147
|
+
# @option options [Object] :target Optional target instance to patch
|
|
148
|
+
# @option options [OpenTelemetry::SDK::Trace::TracerProvider] :tracer_provider Optional tracer provider
|
|
149
|
+
# @return [void]
|
|
150
|
+
def perform_patch(**options)
|
|
151
|
+
return unless applicable?
|
|
152
|
+
|
|
153
|
+
if options[:target]
|
|
154
|
+
# Instance-level (for only this client)
|
|
155
|
+
raise ArgumentError, "target must be a kind of ::OpenAI::Client" unless options[:target].is_a?(::OpenAI::Client)
|
|
156
|
+
|
|
157
|
+
get_singleton_class(options[:target]).include(Instrumentation::Moderations)
|
|
158
|
+
else
|
|
159
|
+
# Class-level (for all clients)
|
|
160
|
+
::OpenAI::Resources::Moderations.include(Instrumentation::Moderations)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def get_singleton_class(client)
|
|
167
|
+
client&.moderations&.singleton_class
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
128
171
|
end
|
|
129
172
|
end
|
|
130
173
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opentelemetry/sdk"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "../../support/otel"
|
|
7
|
+
require_relative "../../../internal/time"
|
|
8
|
+
|
|
9
|
+
module Braintrust
|
|
10
|
+
module Contrib
|
|
11
|
+
module RubyOpenAI
|
|
12
|
+
module Instrumentation
|
|
13
|
+
# Moderations API instrumentation for ruby-openai.
|
|
14
|
+
# Provides module that can be prepended to OpenAI::Client to instrument the moderations method.
|
|
15
|
+
module Moderations
|
|
16
|
+
def self.included(base)
|
|
17
|
+
base.prepend(InstanceMethods) unless applied?(base)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.applied?(base)
|
|
21
|
+
base.ancestors.include?(InstanceMethods)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
METADATA_FIELDS = %i[
|
|
25
|
+
model
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
module InstanceMethods
|
|
29
|
+
# Wrap moderations method for ruby-openai gem
|
|
30
|
+
# ruby-openai API: client.moderations(parameters: {...})
|
|
31
|
+
def moderations(parameters:)
|
|
32
|
+
tracer = Braintrust::Contrib.tracer_for(self)
|
|
33
|
+
|
|
34
|
+
tracer.in_span("openai.moderations.create") do |span|
|
|
35
|
+
metadata = build_moderations_metadata(parameters)
|
|
36
|
+
set_moderations_input(span, parameters)
|
|
37
|
+
|
|
38
|
+
response = nil
|
|
39
|
+
time_to_first_token = Braintrust::Internal::Time.measure do
|
|
40
|
+
response = super(parameters: parameters)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
set_moderations_output(span, response)
|
|
44
|
+
set_moderations_metrics(span, time_to_first_token)
|
|
45
|
+
finalize_moderations_metadata(span, metadata, response)
|
|
46
|
+
|
|
47
|
+
response
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def build_moderations_metadata(parameters)
|
|
54
|
+
metadata = {
|
|
55
|
+
"provider" => "openai",
|
|
56
|
+
"endpoint" => "/v1/moderations"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Moderations::METADATA_FIELDS.each do |field|
|
|
60
|
+
metadata[field.to_s] = parameters[field] if parameters.key?(field)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
metadata
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def set_moderations_input(span, parameters)
|
|
67
|
+
return unless parameters[:input]
|
|
68
|
+
Support::OTel.set_json_attr(span, "braintrust.input_json", parameters[:input])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def set_moderations_output(span, response)
|
|
72
|
+
results = response["results"] || response[:results]
|
|
73
|
+
return unless results
|
|
74
|
+
Support::OTel.set_json_attr(span, "braintrust.output_json", results)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def set_moderations_metrics(span, time_to_first_token)
|
|
78
|
+
metrics = {"time_to_first_token" => time_to_first_token}
|
|
79
|
+
Support::OTel.set_json_attr(span, "braintrust.metrics", metrics)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def finalize_moderations_metadata(span, metadata, response)
|
|
83
|
+
%w[id model].each do |field|
|
|
84
|
+
value = response[field] || response[field.to_sym]
|
|
85
|
+
metadata[field] = value if value
|
|
86
|
+
end
|
|
87
|
+
Support::OTel.set_json_attr(span, "braintrust.metadata", metadata)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|