Module: Foam::Otel::LLM::GeminiShim

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

Overview

Gap-filler for Google Gemini via the gemini-ai gem — the de-facto Gemini Ruby SDK (Google ships NO official Ruby SDK; provider-parity ruling 2026-07-26 requires Gemini coverage in every server language). Patches the public call sites generate_content and stream_generate_content (the gem consumes the SSE stream INSIDE the call and returns the collected events, so both seams yield the full response synchronously). gen_ai.provider.name uses the semconv well-known value for the Gemini API: "gcp.gemini".

Defined Under Namespace

Modules: ClientPatch

Constant Summary collapse

PROVIDER =
"gcp.gemini"
SUPPORTED =
Gem::Requirement.new(">= 4.0", "< 5")
RESPONSE =

Gemini REST response (a parsed hash, or the array of SSE events) → response attributes. Candidates' content rides VERBATIM.

lambda do |result|
  events = (result.is_a?(Array) ? result : [result]).select { |e| e.is_a?(Hash) }
  next nil if events.empty?

  candidates = events.flat_map { |e| Array(e["candidates"]) }.select { |c| c.is_a?(Hash) }
  usage = events.reverse.filter_map { |e| e["usageMetadata"] }.find { |u| u.is_a?(Hash) }
  input_tokens = usage && usage["promptTokenCount"]
  output_tokens = usage && usage["candidatesTokenCount"]
  LLM.response_attributes(
    model: events.filter_map { |e| e["modelVersion"] }.first,
    id: events.filter_map { |e| e["responseId"] }.first,
    finish_reasons: candidates.filter_map { |c| c["finishReason"] },
    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: candidates.filter_map { |c| c["content"] }
  )
end

Class Method Summary collapse

Class Method Details

.install!Object



25
26
27
28
29
30
31
# File 'lib/foam/otel/llm/gemini_shim.rb', line 25

def install!
  return true if @installed

  ::Gemini::Controllers::Client.prepend(ClientPatch)
  Diagnostics.info("LLM shim installed: gemini-ai (generate_content, stream_generate_content)")
  @installed = true
end

.installed?Boolean

Returns:

  • (Boolean)


33
# File 'lib/foam/otel/llm/gemini_shim.rb', line 33

def installed? = @installed

.present?Boolean

Returns:

  • (Boolean)


19
20
21
22
23
# File 'lib/foam/otel/llm/gemini_shim.rb', line 19

def present?
  defined?(::Gemini::Controllers::Client) &&
    ::Gemini::Controllers::Client.method_defined?(:generate_content) &&
    supported_version?
end

.request(client, payload) ⇒ Object

The request model lives on the CLIENT (its model address), not in the payload: "models/gemini-2.0-flash" / "publishers/google/ models/..." → the trailing segment.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/foam/otel/llm/gemini_shim.rb', line 53

def request(client, payload)
  payload = payload.is_a?(Hash) ? payload : {}
  model = begin
    client.instance_variable_get(:@model_address).to_s.split("/").last
  rescue StandardError
    nil
  end
  LLM.request_attributes(
    provider: PROVIDER, operation: "chat",
    model: model,
    input_messages: payload[:contents] || payload["contents"],
    system_instructions: payload[:system_instruction] || payload["system_instruction"]
  )
end

.supported_version?Boolean

Returns:

  • (Boolean)


35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/foam/otel/llm/gemini_shim.rb', line 35

def supported_version?
  spec = Gem.loaded_specs["gemini-ai"]
  return true if spec.nil?

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