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. A tool-call turn (the assistant asking for a tool run) keeps its tool names + arguments on the output side too — without them the agent loop's model turns are invisible activity.
lambda do || next nil unless .respond_to?(:content) input_tokens = .respond_to?(:input_tokens) ? .input_tokens : nil output_tokens = .respond_to?(:output_tokens) ? .output_tokens : nil = { "role" => (.respond_to?(:role) ? .role.to_s : "assistant"), "content" => RubyLLMShim.content_of(), } tool_calls = RubyLLMShim.serialize_tool_calls() ["tool_calls"] = tool_calls if tool_calls LLM.response_attributes( model: .respond_to?(:model_id) ? .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: [] ) end
Class Method Summary collapse
- .content_of(message) ⇒ Object
- .install! ⇒ Object
- .installed? ⇒ Boolean
- .present? ⇒ Boolean
- .provider_name(provider) ⇒ Object
- .request(provider, messages, model) ⇒ Object
-
.serialize_messages(messages) ⇒ Object
RubyLLM::Message list → plain message hashes for the RAW input.messages JSON (Content objects degrade to_s, never dropped).
-
.serialize_tool_calls(message) ⇒ Object
RubyLLM::Message#tool_calls is a Hash=> ToolCall — each call rides as name:, arguments: verbatim.
- .supported_version? ⇒ Boolean
Class Method Details
.content_of(message) ⇒ Object
118 119 120 121 122 123 124 125 |
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 118 def content_of() content = .respond_to?(:content) ? .content : 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
43 |
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 43 def installed? = @installed |
.present? ⇒ 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, , model) model_id = model.respond_to?(:id) ? model.id : model LLM.request_attributes( provider: provider_name(provider), operation: "chat", model: model_id, input_messages: () ) end |
.serialize_messages(messages) ⇒ Object
RubyLLM::Message list → plain message hashes for the RAW input.messages JSON (Content objects degrade to_s, never dropped). Agent-loop turns keep their tool activity (rule 8a/8c floor — tool names/arguments are activity signal): an assistant tool-call turn carries "tool_calls" (id/name/arguments verbatim) and a tool-result turn carries its "tool_call_id" linkage.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 82 def () Array().map do || entry = { "role" => (.respond_to?(:role) ? .role.to_s : "user"), "content" => content_of(), } tool_calls = serialize_tool_calls() entry["tool_calls"] = tool_calls if tool_calls tool_call_id = .respond_to?(:tool_call_id) ? .tool_call_id : nil entry["tool_call_id"] = tool_call_id.to_s if tool_call_id && !tool_call_id.to_s.empty? entry end rescue StandardError, SystemStackError nil end |
.serialize_tool_calls(message) ⇒ Object
RubyLLM::Message#tool_calls is a Hash=> ToolCall — each call rides as name:, arguments: verbatim. nil when absent/empty (the key is omitted, matching the non-agentic message shape).
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/foam/otel/llm/ruby_llm_shim.rb', line 101 def serialize_tool_calls() return nil unless .respond_to?(:tool_calls) calls = .tool_calls calls = calls.values if calls.is_a?(Hash) serialized = Array(calls).map do |call| { "id" => (call.respond_to?(:id) ? call.id : nil), "name" => (call.respond_to?(:name) ? call.name : nil), "arguments" => (call.respond_to?(:arguments) ? call.arguments : nil), } end serialized.empty? ? nil : serialized rescue StandardError, SystemStackError nil end |
.supported_version? ⇒ 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 |