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

Included in:
RubyLLM::Providers::OpenRouter
Defined in:
lib/ruby_llm/providers/openrouter/chat.rb

Overview

Chat methods of the OpenRouter API integration

Class Method Summary collapse

Class Method Details

.build_reasoning(thinking) ⇒ Object



89
90
91
92
93
94
95
96
97
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 89

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

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

.extract_thinking_signature(message_data) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 139

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



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 120

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_messages(messages) ⇒ Object



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

def format_messages(messages)
  messages.map do |msg|
    {
      role: format_role(msg.role),
      content: OpenAI::Media.format_content(msg.content),
      tool_calls: OpenAI::Tools.format_tool_calls(msg.tool_calls),
      tool_call_id: msg.tool_call_id
    }.compact.merge(format_thinking(msg))
  end
end

.format_role(role) ⇒ Object



80
81
82
83
84
85
86
87
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 80

def format_role(role)
  case role
  when :system
    @config.openai_use_system_role ? 'system' : 'developer'
  else
    role.to_s
  end
end

.format_thinking(msg) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 99

def format_thinking(msg)
  thinking = msg.thinking
  return {} unless thinking && msg.role == :assistant

  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

.parse_completion_response(response) ⇒ Object

Raises:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 39

def parse_completion_response(response)
  data = response.body
  return if data.empty?

  raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message')

  message_data = data.dig('choices', 0, 'message')
  return unless message_data

  usage = data['usage'] || {}
  cached_tokens = usage.dig('prompt_tokens_details', 'cached_tokens')
  thinking_tokens = usage.dig('completion_tokens_details', 'reasoning_tokens')
  thinking_text = extract_thinking_text(message_data)
  thinking_signature = extract_thinking_signature(message_data)

  Message.new(
    role: :assistant,
    content: message_data['content'],
    thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
    tool_calls: OpenAI::Tools.parse_tool_calls(message_data['tool_calls']),
    input_tokens: usage['prompt_tokens'],
    output_tokens: usage['completion_tokens'],
    cached_tokens: cached_tokens,
    cache_creation_tokens: 0,
    thinking_tokens: thinking_tokens,
    model_id: data['model'],
    raw: response
  )
end

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

rubocop:disable Metrics/ParameterLists



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/ruby_llm/providers/openrouter/chat.rb', line 10

def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) # rubocop:disable Metrics/ParameterLists
  payload = {
    model: model.id,
    messages: format_messages(messages),
    stream: stream
  }

  payload[:temperature] = temperature unless temperature.nil?
  payload[:tools] = tools.map { |_, tool| OpenAI::Tools.tool_for(tool) } if tools.any?

  if schema
    strict = schema[:strict] != false
    payload[:response_format] = {
      type: 'json_schema',
      json_schema: {
        name: 'response',
        schema: schema,
        strict: strict
      }
    }
  end

  reasoning = build_reasoning(thinking)
  payload[:reasoning] = reasoning if reasoning

  payload[:stream_options] = { include_usage: true } if stream
  payload
end