Module: RubyLLM::Providers::Gemini::Chat
- Included in:
- RubyLLM::Providers::Gemini
- Defined in:
- lib/ruby_llm/providers/gemini/chat.rb
Overview
Chat methods for the Gemini API implementation
Defined Under Namespace
Classes: GeminiSchema, MessageFormatter
Class Method Summary collapse
- .build_json_schema(schema) ⇒ Object
-
.build_thinking_config(_model, thinking) ⇒ Object
rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument.
- .build_thought_part(thinking) ⇒ Object
- .calculate_output_tokens(data) ⇒ Object
- .completion_url ⇒ Object
- .convert_schema_to_gemini(schema) ⇒ Object
- .extract_text_parts(parts) ⇒ Object
- .extract_thought_parts(parts) ⇒ Object
- .extract_thought_signature(parts) ⇒ Object
- .extract_version(text) ⇒ Object
- .format_message_parts(msg) ⇒ Object
- .format_messages(messages) ⇒ Object
- .format_parts(msg) ⇒ Object
- .format_role(role) ⇒ Object
- .function_call?(candidate) ⇒ Boolean
- .gemini_version(model) ⇒ Object
- .model_metadata_value(model, key) ⇒ Object
- .parse_completion_response(response) ⇒ Object
- .parse_content(data) ⇒ Object
-
.render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil, tool_prefs: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument.
- .resolve_budget(thinking) ⇒ Object
- .resolve_effort_level(thinking) ⇒ Object
- .response_json_schema_supported?(model) ⇒ Boolean
- .safe_string(value) ⇒ Object
- .structured_output_config(schema, model) ⇒ Object
Class Method Details
.build_json_schema(schema) ⇒ Object
194 195 196 197 198 199 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 194 def build_json_schema(schema) normalized = RubyLLM::Utils.deep_dup(schema[:schema]) normalized.delete(:strict) normalized.delete('strict') RubyLLM::Utils.deep_stringify_keys(normalized) end |
.build_thinking_config(_model, thinking) ⇒ Object
rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument
42 43 44 45 46 47 48 49 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 42 def build_thinking_config(_model, thinking) config = { includeThoughts: true } config[:thinkingLevel] = resolve_effort_level(thinking) if thinking&.effort config[:thinkingBudget] = resolve_budget(thinking) if thinking&.budget config end |
.build_thought_part(thinking) ⇒ Object
101 102 103 104 105 106 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 101 def build_thought_part(thinking) part = { thought: true } part[:text] = thinking.text if thinking.text part[:thoughtSignature] = thinking.signature if thinking.signature part end |
.calculate_output_tokens(data) ⇒ Object
183 184 185 186 187 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 183 def calculate_output_tokens(data) candidates = data.dig('usageMetadata', 'candidatesTokenCount') || 0 thoughts = data.dig('usageMetadata', 'thoughtsTokenCount') || 0 candidates + thoughts end |
.completion_url ⇒ Object
13 14 15 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 13 def completion_url "models/#{@model}:generateContent" end |
.convert_schema_to_gemini(schema) ⇒ Object
130 131 132 133 134 135 136 137 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 130 def convert_schema_to_gemini(schema) return nil unless schema # Extract inner schema if wrapper format (e.g., from RubyLLM::Schema.to_json_schema) schema = schema[:schema] || schema GeminiSchema.new(schema).to_h end |
.extract_text_parts(parts) ⇒ Object
154 155 156 157 158 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 154 def extract_text_parts(parts) text_parts = parts.reject { |p| p['thought'] } content = text_parts.filter_map { |p| p['text'] }.join content.empty? ? nil : content end |
.extract_thought_parts(parts) ⇒ Object
160 161 162 163 164 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 160 def extract_thought_parts(parts) thought_parts = parts.select { |p| p['thought'] } thoughts = thought_parts.filter_map { |p| p['text'] }.join thoughts.empty? ? nil : thoughts end |
.extract_thought_signature(parts) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 166 def extract_thought_signature(parts) parts.each do |part| signature = part['thoughtSignature'] || part['thought_signature'] || part.dig('functionCall', 'thoughtSignature') || part.dig('functionCall', 'thought_signature') return signature if signature end nil end |
.extract_version(text) ⇒ Object
233 234 235 236 237 238 239 240 241 242 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 233 def extract_version(text) return nil unless text match = text.match(/(\d+\.\d+|\d+)/) return nil unless match Gem::Version.new(match[1]) rescue ArgumentError nil end |
.format_message_parts(msg) ⇒ Object
91 92 93 94 95 96 97 98 99 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 91 def (msg) parts = [] parts << build_thought_part(msg.thinking) if msg.role == :assistant && msg.thinking content_parts = Media.format_content(msg.content) parts.concat(content_parts.is_a?(Array) ? content_parts : [content_parts]) parts end |
.format_messages(messages) ⇒ Object
62 63 64 65 66 67 68 69 70 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 62 def () formatter = MessageFormatter.new( , format_role: method(:format_role), format_parts: method(:format_parts), format_tool_result: method(:format_tool_result) ) formatter.format end |
.format_parts(msg) ⇒ Object
81 82 83 84 85 86 87 88 89 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 81 def format_parts(msg) if msg.tool_call? format_tool_call(msg) elsif msg.tool_result? format_tool_result(msg) else (msg) end end |
.format_role(role) ⇒ Object
72 73 74 75 76 77 78 79 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 72 def format_role(role) case role when :assistant then 'model' when :system then 'user' when :tool then 'function' else role.to_s end end |
.function_call?(candidate) ⇒ Boolean
178 179 180 181 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 178 def function_call?(candidate) parts = candidate.dig('content', 'parts') parts&.any? { |p| p['functionCall'] } end |
.gemini_version(model) ⇒ Object
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 201 def gemini_version(model) return nil unless model candidates = [ safe_string(model.id), safe_string(model.respond_to?(:family) ? model.family : nil), safe_string((model, :version)), safe_string((model, 'version')), safe_string((model, :description)) ].compact candidates.each do |candidate| version = extract_version(candidate) return version if version end nil end |
.model_metadata_value(model, key) ⇒ Object
220 221 222 223 224 225 226 227 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 220 def (model, key) return unless model.respond_to?(:metadata) = model. return unless .is_a?(Hash) [key] || [key.to_s] end |
.parse_completion_response(response) ⇒ Object
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 108 def parse_completion_response(response) data = response.body parts = data.dig('candidates', 0, 'content', 'parts') || [] tool_calls = extract_tool_calls(data) Message.new( role: :assistant, content: extract_text_parts(parts) || parse_content(data), thinking: Thinking.build( text: extract_thought_parts(parts), signature: extract_thought_signature(parts) ), tool_calls: tool_calls, input_tokens: data.dig('usageMetadata', 'promptTokenCount'), output_tokens: calculate_output_tokens(data), cached_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'), thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'), model_id: data['modelVersion'] || response.env.url.path.split('/')[3].split(':')[0], raw: response ) end |
.parse_content(data) ⇒ Object
139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 139 def parse_content(data) candidate = data.dig('candidates', 0) return '' unless candidate return '' if function_call?(candidate) parts = candidate.dig('content', 'parts') return '' unless parts&.any? non_thought_parts = parts.reject { |part| part['thought'] } return '' unless non_thought_parts.any? build_response_content(non_thought_parts) end |
.render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil, tool_prefs: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 18 def render_payload(, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil, tool_prefs: nil) tool_prefs ||= {} @model = model.id payload = { contents: (), generationConfig: {} } payload[:generationConfig][:temperature] = temperature unless temperature.nil? payload[:generationConfig].merge!(structured_output_config(schema, model)) if schema payload[:generationConfig][:thinkingConfig] = build_thinking_config(model, thinking) if thinking&.enabled? if tools.any? payload[:tools] = format_tools(tools) # Gemini doesn't support controlling parallel tool calls payload[:toolConfig] = build_tool_config(tool_prefs[:choice]) unless tool_prefs[:choice].nil? end payload end |
.resolve_budget(thinking) ⇒ Object
55 56 57 58 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 55 def resolve_budget(thinking) budget = thinking.respond_to?(:budget) ? thinking.budget : thinking budget.is_a?(Integer) ? budget : nil end |
.resolve_effort_level(thinking) ⇒ Object
51 52 53 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 51 def resolve_effort_level(thinking) thinking.respond_to?(:effort) ? thinking.effort : thinking end |
.response_json_schema_supported?(model) ⇒ Boolean
189 190 191 192 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 189 def response_json_schema_supported?(model) version = gemini_version(model) version && version >= Gem::Version.new('2.5') end |
.safe_string(value) ⇒ Object
229 230 231 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 229 def safe_string(value) value&.to_s end |
.structured_output_config(schema, model) ⇒ Object
244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 244 def structured_output_config(schema, model) { responseMimeType: 'application/json' }.tap do |config| if response_json_schema_supported?(model) config[:responseJsonSchema] = build_json_schema(schema) else config[:responseSchema] = convert_schema_to_gemini(schema) end end end |