This skill is for writing integrations to the Ruby SDK. Claude acts as the engineer implementing LLM provider or agentic framework integrations. Use when adding support for OpenAI-like providers, Anthropic-like providers, or agent frameworks. Covers TDD workflow, comprehensive testing (streaming/non-streaming/tokens/multimodal), defensive coding, MCP validation, and StandardRB compliance.
This skill is for writing integrations. Claude acts as the Braintrust engineer implementing new integrations to the Ruby SDK.
Study existing integrations as examples:
lib/braintrust/trace/contrib/openai.rb (tests: test/braintrust/trace/openai_test.rb, example: examples/openai.rb)lib/braintrust/trace/contrib/anthropic.rb (tests: test/braintrust/trace/anthropic_test.rb, example: examples/anthropic.rb)Important Notes:
# frozen_string_literal: true
module Braintrust
module Trace
module YourProvider
def self.wrap(client = nil, tracer_provider: nil)
tracer_provider ||= ::OpenTelemetry.tracer_provider
# Idempotent wrapping: check if already wrapped
return client if client && client.instance_variable_get(:@braintrust_wrapped)
# Support class-level wrapping: wrap() with no args wraps class globally
if client.nil?
# Class wrapping: YourProvider.prepend(wrapper)
# Instance wrapping: client.singleton_class.prepend(wrapper)
end
wrapper = Module.new do
define_method(:your_api_method) do |**params|
tracer = tracer_provider.tracer("braintrust")
tracer.in_span("your_provider.operation") do |span|
# IMPORTANT: Start span FIRST (before metadata extraction) for accurate timing
# 1. Capture input
set_json_attr(span, "braintrust.input_json", extract_input(params))
# 2. Set metadata (provider, model, endpoint, all params)
set_json_attr(span, "braintrust.metadata", {
"provider" => "your_provider",
"endpoint" => "/v1/endpoint",
"model" => params[:model]
}.compact)
# 3. Call original
response = super(**params)
# 4. Capture output
set_json_attr(span, "braintrust.output_json", extract_output(response))
# 5. Capture metrics (normalized tokens)
set_json_attr(span, "braintrust.metrics", parse_usage_tokens(response.usage))
response
end
end
end
client.your_api.singleton_class.prepend(wrapper)
client.instance_variable_set(:@braintrust_wrapped, true) if client
client
end
## Code Organization
- Break large methods (>50 lines) into focused helpers
- Separate streaming/non-streaming into distinct handler methods (e.g., `handle_streaming_request`, `handle_non_streaming_request`)
- Extract metadata/input/output capture into helper methods (e.g., `extract_metadata`, `build_input_messages`, `capture_output`)
private
def self.set_json_attr(span, key, value)
span.set_attribute(key, JSON.generate(value)) if value
rescue => e
warn "Failed to serialize #{key}: #{e.message}"
end
def self.parse_usage_tokens(usage)
return {} unless usage
{
"prompt_tokens" => usage[:input_tokens] || usage[:prompt_tokens],
"completion_tokens" => usage[:output_tokens] || usage[:completion_tokens],
"tokens" => usage[:total_tokens]
}.compact
end
end
end
end
define_method(:stream) do |**params|
tracer = tracer_provider.tracer("braintrust")
aggregated_chunks = []
span = tracer.start_span("your_provider.operation.stream")
set_json_attr(span, "braintrust.input_json", extract_input(params))
set_json_attr(span, "braintrust.metadata", extract_metadata(params))
stream = begin
super(**params)
rescue => e
span.record_exception(e)
span.status = ::OpenTelemetry::Trace::Status.error("Error: #{e.message}")
span.finish
raise
end
original_each = stream.method(:each)
stream.define_singleton_method(:each) do |&block|
original_each.call do |chunk|
aggregated_chunks << chunk
block&.call(chunk)
end
rescue => e
span.record_exception(e)
span.status = ::OpenTelemetry::Trace::Status.error("Streaming error: #{e.message}")
raise
ensure
# CRITICAL: Always finish span even if stream partially consumed
unless aggregated_chunks.empty?
aggregated = aggregate_chunks(aggregated_chunks)
set_json_attr(span, "braintrust.output_json", aggregated)
set_json_attr(span, "braintrust.metrics", parse_usage_tokens(aggregated[:usage]))
end
span.finish
end
stream
end
Write two examples:
examples/your_provider.rb): Concise example demonstrating setup and basic usageexamples/internal/your_provider.rb): Comprehensive example using every library featureFollow existing example patterns:
examples/openai.rb):
tracer = OpenTelemetry.tracer_provider.tracer("your-provider-example")
root_span = nil
response = tracer.in_span("examples/your_provider.rb") do |span|
root_span = span
client.your_api.call(...) # Automatically traced, nested under root_span
end
Braintrust::Trace.permalink(root_span)Do in this order:
Appraisals file (latest + 2 recent + uninstalled), run bundle exec appraisal generatetest/braintrust/trace/your_provider_test.rblib/braintrust/trace/contrib/your_provider.rbtest/fixtures/vcr_cassettes/your_provider/ (record as you write tests)lib/braintrust/trace.rb with begin/rescue LoadErrorexamples/your_provider.rbexamples/internal/your_provider.rb (comprehensive internal example).env.example if neededBRAINTRUST_ENABLE_TRACE_CONSOLE_LOG=1CRITICAL: Configure appraisal at the START, before writing tests. Test latest + 2 recent versions + uninstalled.
Step 1 - Add to Appraisals file:
# Appraisals file - ADD THIS FIRST
appraise "your_provider-latest" do
gem "your_provider", ">= 2.0"
end
appraise "your_provider-1.5" do
gem "your_provider", "~> 1.5.0"
end
appraise "your_provider-1.0" do
gem "your_provider", "~> 1.0.0"
end
appraise "your_provider-uninstalled" do
remove_gem "your_provider"
end
Step 2 - Generate gemfiles:
bundle exec appraisal generate
Step 3 - Use appraisal for ALL test runs:
bundle exec appraisal rake test # Run all scenarios (use this in TDD cycle)
Determine versions: Check release history, focus on API changes, include customer-likely versions.
Use multiple testing approaches to validate your integration:
test/braintrust/trace/your_provider_test.rbbundle exec appraisal rake testbundle exec rake coverage (>90% line, >80% branch)BRAINTRUST_ENABLE_TRACE_CONSOLE_LOG=true bundle exec ruby examples/your_provider.rb
# List recent traces
mcp__braintrust__list_recent_objects(object_type: "project_logs", limit: 10)
# Inspect specific span
mcp__braintrust__resolve_object(object_type: "project_logs", object_id: "span_id")
# BTQL query
mcp__braintrust__btql_query(query: "SELECT * FROM project_logs WHERE metadata.provider = 'your_provider'")
input, output, metadata, metrics, span_attributes.braintrust.parent, span_attributes.braintrust.orgbundle exec ruby examples/your_provider.rbbundle exec ruby examples/internal/your_provider.rbbundle exec appraisal rake testBRAINTRUST_ENABLE_TRACE_CONSOLE_LOG=true to debug span structureAfter EVERY major change: test → lint → fix → commit cycle
bundle exec appraisal rake testbundle exec rake lint (fix with rake lint:fix)return {} unless usage)params[:model] || "unknown").compact)begin/rescue/ensure)set_json_attr)rescue LoadError)Lint after every change (part of TDD cycle):
bundle exec rake lint # Check StandardRB
bundle exec rake lint:fix # Auto-fix
Coverage target (check periodically):
bundle exec rake coverage # >90% line, >80% branch
CI requirements: StandardRB + tests on Ruby 3.2/3.3/3.4 + Ubuntu/macOS + all appraisal scenarios
Use shared TokenParser.parse_usage_tokens(usage) in lib/braintrust/trace/token_parser.rb to normalize tokens:
prompt_tokens (input)completion_tokens (output)tokens (total, includes cache_creation_tokens)prompt_cached_tokens (if cached)prompt_cache_creation_tokens (if cache created)completion_reasoning_tokens (if reasoning)VCR_MODE=all bundle exec rake test # Re-record all
VCR_MODE=new_episodes bundle exec rake test # Record new only
VCR_OFF=true bundle exec rake test # Skip VCR
lib/braintrust/trace/contrib/{openai,anthropic}.rbtest/braintrust/trace/{openai,anthropic}_test.rbtest/test_helper.rbexamples/{openai,anthropic}.rbRakefile, Appraisals, .github/workflows/ci.ymlnpx skills add braintrustdata/ruby-integration下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer