Module: Foam::Otel::LLM::RubyLLMShim

Defined in:
lib/foam/otel/llm/ruby_llm_shim.rb

Overview

Gap-filler for the ruby_llm multi-provider client: ONE seam — RubyLLM::Provider#complete, the single synchronous round trip every chat/ask call funnels through — covers ALL its providers (OpenAI, Anthropic, Gemini, and the rest) with the identical gen_ai span the per-SDK shims emit. Streaming rides the same seam (ruby_llm consumes the stream inside #complete and returns the accumulated Message).

Defined Under Namespace

Modules: ProviderPatch

Constant Summary collapse

SUPPORTED =
Gem::Requirement.new(">= 1.3", "< 2")
PROVIDER_NAMES =

ruby_llm provider slugs → gen_ai.provider.name well-known values. Unlisted slugs pass through verbatim (never dropped).

{
  "openai" => "openai",
  "anthropic" => "anthropic",
  "gemini" => "gcp.gemini",
  "vertexai" => "gcp.vertex_ai",
  "bedrock" => "aws.bedrock",
  "mistral" => "mistral_ai",
  "deepseek" => "deepseek",
  "perplexity" => "perplexity",
}.freeze
RESPONSE =

The returned RubyLLM::Message → response attributes.

lambda do |message|
  next nil unless message.respond_to?(:content)

  input_tokens = message.respond_to?(:input_tokens) ? message.input_tokens : nil
  output_tokens = message.respond_to?(:output_tokens) ? message.output_tokens : nil
  LLM.response_attributes(
    model: message.respond_to?(:model_id) ? message.model_id : nil,
    input_tokens: input_tokens.is_a?(Numeric) ? input_tokens.to_i : nil,
    output_tokens: output_tokens.is_a?(Numeric) ? output_tokens.to_i : nil,
    output_messages: [{
      "role" => (message.respond_to?(:role) ? message.role.to_s : "assistant"),
      "content" => RubyLLMShim.content_of(message),
    }]
  )
end

Class Method Summary collapse

Class Method Details

.content_of(message) ⇒ Object



89
90
91
92
93
94
95
96
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 89

def content_of(message)
  content = message.respond_to?(:content) ? message.content : message
  return content if content.is_a?(String)

  content.respond_to?(:text) && content.text ? content.text : content.to_s
rescue StandardError, SystemStackError
  nil
end

.install!Object



35
36
37
38
39
40
41
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 35

def install!
  return true if @installed

  ::RubyLLM::Provider.prepend(ProviderPatch)
  Diagnostics.info("LLM shim installed: ruby_llm (Provider#complete)")
  @installed = true
end

.installed?Boolean

Returns:

  • (Boolean)


43
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 43

def installed? = @installed

.present?Boolean

Returns:

  • (Boolean)


29
30
31
32
33
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 29

def present?
  defined?(::RubyLLM::Provider) &&
    ::RubyLLM::Provider.method_defined?(:complete) &&
    supported_version?
end

.provider_name(provider) ⇒ Object



60
61
62
63
64
65
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 60

def provider_name(provider)
  slug = provider.respond_to?(:slug) ? provider.slug.to_s : provider.class.to_s
  PROVIDER_NAMES.fetch(slug, slug)
rescue StandardError
  "ruby_llm"
end

.request(provider, messages, model) ⇒ Object



67
68
69
70
71
72
73
74
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 67

def request(provider, messages, model)
  model_id = model.respond_to?(:id) ? model.id : model
  LLM.request_attributes(
    provider: provider_name(provider), operation: "chat",
    model: model_id,
    input_messages: serialize_messages(messages)
  )
end

.serialize_messages(messages) ⇒ Object

RubyLLM::Message list → plain [content:] for the RAW input.messages JSON (Content objects degrade to_s, never dropped).



78
79
80
81
82
83
84
85
86
87
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 78

def serialize_messages(messages)
  Array(messages).map do |message|
    {
      "role" => (message.respond_to?(:role) ? message.role.to_s : "user"),
      "content" => content_of(message),
    }
  end
rescue StandardError, SystemStackError
  nil
end

.supported_version?Boolean

Returns:

  • (Boolean)


45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 45

def supported_version?
  spec = Gem.loaded_specs["ruby_llm"]
  return true if spec.nil?

  supported = SUPPORTED.satisfied_by?(spec.version)
  unless supported || @version_warned
    @version_warned = true
    Diagnostics.warn("ruby_llm #{spec.version} is outside foam's LLM-shim window (#{SUPPORTED}) " \
                     "— the ruby_llm shim stays dark; file this with foam support")
  end
  supported
rescue StandardError
  true
end