Class: Rubino::LLM::FakeProvider

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/llm/fake_provider.rb

Overview

Dev-only LLM adapter that replays a pre-recorded YAML scenario instead of hitting a real provider. The public surface mirrors RubyLLMAdapter so Agent::Loop can swap it in without further plumbing changes.

Selection:

- model_id starting with "fake/" pins the scenario (suffix is the name).
- otherwise ScenarioSelector.resolve(last_user_message_content) chooses
  one based on keyword routing, falling back to "happy-path".

Streaming:

- "content"  → yield { type: :content,  text: ... }
- "thinking" → yield { type: :thinking, text: ... } (gated by
               display.show_reasoning, mirroring RubyLLMAdapter)
- "tool_call"     → buffered onto the final AdapterResponse (NOT yielded
               mid-stream; this matches RubyLLMAdapter and is what Loop
               expects).
- "delay_seconds" → cancellable sleep between events.
- unknown    → logged and skipped.

Cancellation is checked between each event so Esc / Ctrl+C lands within one tick instead of waiting for the full scenario to drain.

Constant Summary collapse

DEFAULT_DELAY =
0.1

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model_id: nil, provider: nil, config: nil, ui: nil, event_bus: nil, tool_executor: nil, cancel_token: nil) ⇒ FakeProvider

Returns a new instance of FakeProvider.



35
36
37
38
39
40
41
42
43
44
# File 'lib/rubino/llm/fake_provider.rb', line 35

def initialize(model_id: nil, provider: nil, config: nil, ui: nil, event_bus: nil,
               tool_executor: nil, cancel_token: nil)
  @config        = config || Rubino.configuration
  @model_id      = model_id || @config.model_default || "fake/happy-path"
  @provider      = provider || "fake"
  @ui            = ui
  @event_bus     = event_bus
  @tool_executor = tool_executor
  @cancel_token  = cancel_token
end

Instance Attribute Details

#model_idObject (readonly)

Returns the value of attribute model_id.



31
32
33
# File 'lib/rubino/llm/fake_provider.rb', line 31

def model_id
  @model_id
end

#providerObject (readonly)

Returns the value of attribute provider.



31
32
33
# File 'lib/rubino/llm/fake_provider.rb', line 31

def provider
  @provider
end

Instance Method Details

#call(request) ⇒ Object

LLM boundary entry: dispatch an LLM::Request to the streaming vs non-streaming transport. Mirrors RubyLLMAdapter#call so Loop can drive the fake through the same seam.



49
50
51
52
53
54
55
56
57
# File 'lib/rubino/llm/fake_provider.rb', line 49

def call(request, &)
  if request.stream?
    stream(messages: request.messages, tools: request.tools,
           image_paths: request.image_paths, &)
  else
    chat(messages: request.messages, tools: request.tools,
         image_paths: request.image_paths)
  end
end

#chat(messages:, tools: nil, response_format: nil, image_paths: nil) ⇒ Object

Non-streaming entry point. Plays the scenario with a no-op block and returns the accumulated AdapterResponse.



61
62
63
64
# File 'lib/rubino/llm/fake_provider.rb', line 61

def chat(messages:, tools: nil, response_format: nil, image_paths: nil)
  stream(messages: messages, tools: tools, response_format: response_format,
         image_paths: image_paths) { |_chunk| }
end

#context_windowObject



125
126
127
# File 'lib/rubino/llm/fake_provider.rb', line 125

def context_window
  @config.model_context_length || 128_000
end

#model_infoObject



121
122
123
# File 'lib/rubino/llm/fake_provider.rb', line 121

def model_info
  nil
end

#resolve_scenario(messages) ⇒ Object

Convenience: returns the scenario name FakeProvider would pick for this set of messages. Useful in specs and the doctor command.



131
132
133
# File 'lib/rubino/llm/fake_provider.rb', line 131

def resolve_scenario(messages)
  pick_scenario(messages)
end

#stream(messages:, tools: nil, response_format: nil, image_paths: nil, &block) ⇒ Object

Streaming entry point. Yields chunk hashes shaped exactly like RubyLLMAdapter:

{ type: :content,  text: String }
{ type: :thinking, text: String }

Returns AdapterResponse with concatenated content, accumulated tool_calls, zero usage tokens, and the model id.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/rubino/llm/fake_provider.rb', line 72

def stream(messages:, tools: nil, response_format: nil, image_paths: nil, &block)
  # image_paths is accepted for signature parity with RubyLLMAdapter
  # (Loop passes it on every call). FakeProvider plays back recorded
  # scenarios verbatim, so it has nothing to do with attachments.
  _ = image_paths
  # If the runner is calling us back after a tool result, replaying the
  # original scenario would re-emit the same tool_call indefinitely
  # (FakeProvider has no inter-turn state). Detect the post-tool turn
  # and emit a short closing message instead so the run terminates.
  events =
    if post_tool_turn?(messages)
      closing_events
    else
      scenario_name = pick_scenario(messages)
      ScenarioLoader.load(scenario_name, scenarios_dir: scenarios_dir_from_config)
    end
  # {{input}} is the only placeholder scenarios currently use. The reference
  # had a richer template system, but in practice every scenario only
  # interpolated the user input. Keep it simple until a scenario actually
  # needs more (e.g. {{session_id}}).
  @scenario_vars = { "input" => extract_last_user_text(messages).to_s }

  buffered    = +""
  tool_calls  = []

  events.each do |event|
    @cancel_token&.check!
    dispatch_event(event, buffered: buffered, tool_calls: tool_calls, &block)
  end

  AdapterResponse.new(
    content: buffered,
    tool_calls: tool_calls,
    input_tokens: 0,
    output_tokens: 0,
    model_id: @model_id
  )
rescue Rubino::Interrupted
  # Mirror RubyLLMAdapter: surface whatever was buffered as a clean
  # AdapterResponse instead of swallowing the partial output.
  AdapterResponse.new(
    content: buffered || "",
    tool_calls: tool_calls || [],
    input_tokens: 0,
    output_tokens: 0,
    model_id: @model_id
  )
end