Module: RubyLLM::Providers::OpenRouter::Chat

Included in:
ChatCompletions
Defined in:
lib/ruby_llm/providers/openrouter/chat.rb

Overview

Chat methods of the OpenRouter API integration

Constant Summary collapse

OPENROUTER_INLINE_FILE_THRESHOLD =
50 * 1024 * 1024
OPENROUTER_FILE_UPLOAD_LIMIT =
100 * 1024 * 1024
CACHE_CONTROL_TYPE =
'ephemeral'
PROMPT_CACHE_OPTIONS =
%i[ttl].freeze
COMPACTION_PLUGIN_ID =
'context-compression'

Class Method Summary collapse

Class Method Details

.add_reasoning_toggle(reasoning, thinking) ⇒ Object



79
80
81
82
83
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 79

def add_reasoning_toggle(reasoning, thinking)
  return unless thinking.respond_to?(:enabled) && !thinking.enabled.nil?

  reasoning[:enabled] = thinking.enabled
end

.apply_compaction(payload, compaction) ⇒ Object

OpenRouter compacts through its context-compression plugin, which drops messages from the middle of the conversation once the prompt would overflow the model's context window. It summarizes nothing and takes no threshold of its own, so the portable options have nowhere to go.



25
26
27
28
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 25

def apply_compaction(payload, compaction)
  log_ignored_compaction_options(compaction)
  payload.merge(plugins: Array(payload[:plugins]) + [{ id: COMPACTION_PLUGIN_ID }])
end

.apply_end_user(payload, identifier) ⇒ Object



16
17
18
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 16

def apply_end_user(payload, identifier)
  payload.merge(user: identifier)
end

.build_reasoning(thinking) ⇒ Object



68
69
70
71
72
73
74
75
76
77
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 68

def build_reasoning(thinking)
  return nil unless thinking&.enabled?

  reasoning = {}
  reasoning[:effort] = thinking.effort.to_s if thinking.respond_to?(:effort) && thinking.effort
  reasoning[:max_tokens] = thinking.budget if thinking.respond_to?(:budget) && thinking.budget
  add_reasoning_toggle(reasoning, thinking)
  reasoning[:enabled] = true if reasoning.empty?
  reasoning
end

.default_large_file_upload_thresholdObject



165
166
167
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 165

def default_large_file_upload_threshold
  OPENROUTER_INLINE_FILE_THRESHOLD
end

.extract_raw_reasoning(message_data) ⇒ Object

OpenRouter requires the reasoning_details array back untouched for signed reasoning to survive multi-turn tool calls.



211
212
213
214
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 211

def extract_raw_reasoning(message_data)
  details = message_data['reasoning_details']
  details if details.is_a?(Array) && !details.empty?
end

.extract_thinking_signature(message_data) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 196

def extract_thinking_signature(message_data)
  details = message_data['reasoning_details']
  return nil unless details.is_a?(Array)

  signature = details.filter_map do |detail|
    detail['signature'] if detail['signature'].is_a?(String)
  end.first
  return signature if signature

  encrypted = details.find { |detail| detail['type'] == 'reasoning.encrypted' && detail['data'].is_a?(String) }
  encrypted&.dig('data')
end

.extract_thinking_text(message_data) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 177

def extract_thinking_text(message_data)
  candidate = message_data['reasoning']
  return candidate if candidate.is_a?(String)

  details = message_data['reasoning_details']
  return nil unless details.is_a?(Array)

  text = details.filter_map do |detail|
    case detail['type']
    when 'reasoning.text'
      detail['text']
    when 'reasoning.summary'
      detail['summary']
    end
  end.join

  text.empty? ? nil : text
end

.format_cache_option_keys(keys) ⇒ Object



145
146
147
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 145

def format_cache_option_keys(keys)
  keys.map { |key| ":#{key}" }.join(', ')
end

.format_content(content, attachments = []) ⇒ Object



38
39
40
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 38

def format_content(content, attachments = [])
  OpenRouter::Media.format_content(content, attachments)
end

.format_message_content(msg, caching: nil) ⇒ Object



109
110
111
112
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 109

def format_message_content(msg, caching: nil)
  content = super
  caching != false && msg.cache_until_here? ? inject_cache_control(content, caching:) : content
end

.format_thinking(msg) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 85

def format_thinking(msg)
  return {} unless msg.role == :assistant
  return { reasoning_details: msg.raw_reasoning } if msg.raw_reasoning.is_a?(Array)

  thinking = msg.thinking
  return {} unless thinking

  details = []
  if thinking.text
    details << {
      type: 'reasoning.text',
      text: thinking.text,
      signature: thinking.signature
    }.compact
  elsif thinking.signature
    details << {
      type: 'reasoning.encrypted',
      data: thinking.signature
    }
  end

  details.empty? ? {} : { reasoning_details: details }
end

.inject_cache_control(content, caching: nil) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 114

def inject_cache_control(content, caching: nil)
  blocks = content.is_a?(Array) ? content.dup : [{ type: 'text', text: content }]
  return blocks if blocks.empty?

  last = blocks.last
  return blocks unless last.is_a?(Hash)
  return blocks if last[:cache_control] || last['cache_control']

  blocks[-1] = last.merge(cache_control: prompt_cache_control(caching))
  blocks
end

.log_ignored_compaction_options(compaction) ⇒ Object



30
31
32
33
34
35
36
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 30

def log_ignored_compaction_options(compaction)
  return if compaction.empty?

  RubyLLM.logger.debug do
    "#{@provider.name} compresses context at the model's own limit, dropping #{compaction.inspect}"
  end
end

.openai_prompt_caching?Boolean

Returns:

  • (Boolean)


149
150
151
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 149

def openai_prompt_caching?
  false
end

.prompt_cache_control(caching = nil) ⇒ Object



126
127
128
129
130
131
132
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 126

def prompt_cache_control(caching = nil)
  options = prompt_cache_options(caching)

  { type: CACHE_CONTROL_TYPE }.tap do |control|
    control[:ttl] = options[:ttl] if options[:ttl]
  end
end

.prompt_cache_options(caching) ⇒ Object

Raises:

  • (ArgumentError)


134
135
136
137
138
139
140
141
142
143
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 134

def prompt_cache_options(caching)
  return {} unless caching

  options = caching.to_h.transform_keys(&:to_sym)
  unsupported = options.keys - PROMPT_CACHE_OPTIONS
  return options if unsupported.empty?

  raise ArgumentError,
        "OpenRouter prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
end

.provider_file_attachable?(attachment) ⇒ Boolean

Returns:

  • (Boolean)


173
174
175
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 173

def provider_file_attachable?(attachment)
  attachment.pdf?
end

.provider_file_upload_limitObject



169
170
171
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 169

def provider_file_upload_limit
  OPENROUTER_FILE_UPLOAD_LIMIT
end

.render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil, schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 42

def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil, schema: nil,
                   thinking: nil, citations: false, caching: nil, tool_prefs: nil)
  payload = super
  payload.delete(:reasoning_effort)
  strip_schema_strict(payload)
  if tool_prefs&.dig(:choice) == :none
    payload.delete(:tools)
    payload.delete(:parallel_tool_calls)
  end

  reasoning = build_reasoning(thinking)
  payload[:reasoning] = reasoning if reasoning
  payload[:cache_control] = prompt_cache_control(caching) if caching
  payload
end

.reported_cost(usage) ⇒ Object



153
154
155
156
157
158
159
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 153

def reported_cost(usage)
  cost = usage['cost']
  return nil unless cost

  cost += usage.dig('cost_details', 'upstream_inference_cost').to_f if usage['is_byok']
  cost
end

.strip_schema_strict(payload) ⇒ Object



58
59
60
61
62
63
64
65
66
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 58

def strip_schema_strict(payload)
  schema_def = payload.dig(:response_format, :json_schema, :schema)
  return unless schema_def.is_a?(Hash)

  schema_def = RubyLLM::Support::Utils.deep_dup(schema_def)
  schema_def.delete(:strict)
  schema_def.delete('strict')
  payload[:response_format][:json_schema][:schema] = schema_def
end

.supports_provider_file_references?Boolean

Returns:

  • (Boolean)


161
162
163
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 161

def supports_provider_file_references?
  true
end