module OpenUI::Rails::Adapters::RubyLLM

Optional RubyLLM adapter. It owns RubyLLM-specific event normalization and delegates sequencing, coalescing and size limits to the Stream bridge, so a host keeps using RubyLLM’s own Rails streaming pattern (‘chat.complete { |chunk| … }` in a job) instead of replacing it.

‘ruby_llm` is never required by the Gem and no `RubyLLM` constant is referenced: failures are classified by class name, so this unit loads and is tested without the dependency installed. The supported RubyLLM version is pinned in the README compatibility matrix.

Constants

STRATEGIES

Public Class Methods

stream(chat:, question:, surface_id:, message_id: nil, library_version: nil, instruction: nil, strategy: :complete, configuration: OpenUI::Rails.configuration, &block) click to toggle source

Stream a RubyLLM completion, yielding ordered {Stream::Envelope}s.

@param chat [#complete, ask] a RubyLLM chat (or a duck type) @param question [String] the user prompt @param surface_id [String, to_s] the target surface @param strategy [Symbol] :complete (default) or :ask @yieldparam envelope [Stream::Envelope] @return [Stream::Bridge::Writer] for inspection in tests

# File lib/openui/rails/adapters/ruby_llm.rb, line 29
def stream(chat:, question:, surface_id:, message_id: nil, library_version: nil,
           instruction: nil, strategy: :complete,
           configuration: OpenUI::Rails.configuration, &block)
  raise ConfigurationError, "surface_id is required" if surface_id.nil?
  raise ConfigurationError, "a block is required" unless block
  unless STRATEGIES.include?(strategy)
    raise ConfigurationError, "unsupported strategy #{strategy.inspect}; expected one of #{STRATEGIES.inspect}"
  end

  instruction ||= PromptBuilder.load(configuration).system_instruction
  library_version ||= configuration.library_fingerprint
  chat = apply_instruction(chat, instruction)

  writer = Stream::Bridge.new(
    message_id: message_id || surface_id.to_s,
    library_version: library_version,
    configuration: configuration
  ).writer

  begin
    consumer_failed = false
    deliver = lambda do |envelope|
      begin
        block.call(envelope)
      rescue StandardError
        consumer_failed = true
        raise
      end
    end
    consume(chat, question, strategy) do |chunk|
      text = normalize_chunk(chunk)
      next if text.nil?

      writer.push(text).each(&deliver)
    end
    writer.finish.each(&deliver)
  rescue StandardError => e
    raise if consumer_failed

    envelope = writer.error_for(e, code: error_code_for(e))
    block.call(envelope) if envelope
  end

  writer
end