Skip to content

Conversation

@delner
Copy link
Collaborator

@delner delner commented Jan 9, 2026

Purpose

This PR migrates the ruby-openai gem (alexrudall) instrumentation from the legacy monolithic wrap() API to the new Integration API framework. This brings ruby-openai in line with the official openai gem integration and establishes a consistent instrumentation interface across all OpenAI-compatible providers. It also enables us to enable auto-instrumentation for this library.

Key goals:

  • Unified Braintrust.instrument!(:ruby_openai) interface for both class-level and instance-level instrumentation
  • Shared utilities between openai and ruby-openai integrations to reduce code duplication
  • Deprecate the old Braintrust::Trace::AlexRudall::RubyOpenAI.wrap() API with a clear migration path

Architectural Overview

New Modular Structure

lib/braintrust/contrib/
├── ruby_openai/
│   ├── integration.rb          # Integration metadata & detection
│   ├── patcher.rb              # ChatPatcher & ResponsesPatcher
│   ├── deprecated.rb           # Backward-compat shim for wrap()
│   └── instrumentation/
│       ├── chat.rb             # Chat completions instrumentation
│       ├── responses.rb        # Responses API instrumentation
│       └── common.rb           # Streaming aggregation (ruby-openai specific)
└── support/
    ├── openai.rb               # Shared OpenAI utilities (token parsing)
    └── otel.rb                 # Shared OTel utilities (span attributes)

Three-Layer Design

  1. Integration Layer (integration.rb): Manages gem detection, version requirements, and delegates to patchers via instrument!

  2. Patcher Layer (patcher.rb): Thread-safe patching at both class and instance levels:

    • ChatPatcher - patches OpenAI::Client#chat
    • ResponsesPatcher - patches OpenAI::Responses#create
  3. Instrumentation Modules (instrumentation/*.rb): Method wrappers that create OpenTelemetry spans with input/output/metrics attributes

Shared Support Utilities

Common code extracted to lib/braintrust/contrib/support/:

  • Support::OpenAI.parse_usage_tokens: Normalizes token usage from both Chat Completions API (prompt_tokens, completion_tokens) and Responses API (input_tokens, output_tokens)
  • Support::OTel.set_json_attr: Safely sets JSON-serialized span attributes

This eliminates duplication between openai and ruby-openai integrations.

Usage Examples

Class-Level Instrumentation (All Clients)

Instrument all OpenAI::Client instances created after the instrument! call:

require "braintrust"
require "openai"

Braintrust.init
Braintrust.instrument!(:ruby_openai)

# All clients are now automatically traced
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])

response = client.chat(
  parameters: {
    model: "gpt-4o-mini",
    messages: [{ role: "user", content: "Hello!" }]
  }
)

Instance-Level Instrumentation (Specific Client)

Instrument only a specific client instance, leaving others untraced:

require "braintrust"
require "openai"

Braintrust.init

client_traced = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
client_untraced = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])

# Only instrument the first client
Braintrust.instrument!(:ruby_openai, target: client_traced)

# This call is traced
client_traced.chat(parameters: { model: "gpt-4o-mini", messages: [...] })

# This call is NOT traced
client_untraced.chat(parameters: { model: "gpt-4o-mini", messages: [...] })

Streaming Support

Both chat and responses streaming are fully supported:

Braintrust.instrument!(:ruby_openai)
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])

# Chat streaming with callback
client.chat(
  parameters: {
    model: "gpt-4o-mini",
    messages: [{ role: "user", content: "Count to 5" }],
    stream_options: { include_usage: true },
    stream: proc do |chunk, _bytesize|
      print chunk.dig("choices", 0, "delta", "content")
    end
  }
)

# Responses API streaming (if available)
client.responses.create(
  parameters: {
    model: "gpt-4o-mini",
    input: "Name 3 colors",
    stream: proc do |chunk, _event|
      print chunk["delta"] if chunk["type"] == "response.output_text.delta"
    end
  }
)

Migration from Legacy API

The old wrap() API is deprecated but continues to work:

# OLD (deprecated)
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
Braintrust::Trace::AlexRudall::RubyOpenAI.wrap(client)

# NEW (recommended)
Braintrust.instrument!(:ruby_openai, target: client)

# Or for all clients:
Braintrust.instrument!(:ruby_openai)
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])

A deprecation warning is logged when using the old API.

Changes Summary

Added

  • lib/braintrust/contrib/ruby_openai/ - New modular integration structure
  • lib/braintrust/contrib/support/openai.rb - Shared token parsing utilities
  • lib/braintrust/contrib/support/otel.rb - Shared OTel span utilities
  • examples/contrib/ruby_openai/ - Updated examples for new API
  • Comprehensive test coverage for all instrumentation paths

Changed

  • lib/braintrust/contrib/openai/ - Now uses shared support utilities
  • Examples reorganized under examples/contrib/ruby_openai/

Removed

  • lib/braintrust/trace/contrib/github.com/alexrudall/ruby-openai/ruby-openai.rb (377 lines)
  • lib/braintrust/trace/tokens.rb - OpenAI token parsing moved to shared support
  • Auto-loading of ruby-openai in lib/braintrust/trace.rb

@delner delner requested review from clutchski and realark January 9, 2026 15:46
@delner delner self-assigned this Jan 9, 2026
@delner delner added the enhancement New feature or request label Jan 9, 2026
@delner delner merged commit 0408a97 into feature/auto_instrument Jan 9, 2026
@delner delner deleted the auto_instrument/ruby-openai branch January 9, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants