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
- .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) ⇒ 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
178 179 180 181 182 183 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 178 def build_json_schema(schema) normalized = RubyLLM::Utils.deep_dup(schema) normalized.delete(:strict) normalized.delete('strict') RubyLLM::Utils.deep_stringify_keys(normalized) end |
.build_thinking_config(_model, thinking) ⇒ Object
33 34 35 36 37 38 39 40 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 33 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
92 93 94 95 96 97 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 92 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
167 168 169 170 171 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 167 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
120 121 122 123 124 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 120 def convert_schema_to_gemini(schema) return nil unless schema GeminiSchema.new(schema).to_h end |
.extract_text_parts(parts) ⇒ Object
138 139 140 141 142 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 138 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
144 145 146 147 148 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 144 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
150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 150 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
217 218 219 220 221 222 223 224 225 226 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 217 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
82 83 84 85 86 87 88 89 90 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 82 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
53 54 55 56 57 58 59 60 61 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 53 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
72 73 74 75 76 77 78 79 80 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 72 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
63 64 65 66 67 68 69 70 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 63 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
162 163 164 165 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 162 def function_call?(candidate) parts = candidate.dig('content', 'parts') parts&.any? { |p| p['functionCall'] } end |
.gemini_version(model) ⇒ Object
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 185 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
204 205 206 207 208 209 210 211 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 204 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
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/gemini/chat.rb', line 99 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), 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
126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 126 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? build_response_content(parts) end |
.render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 17 def render_payload(, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument @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? payload[:tools] = format_tools(tools) if tools.any? payload end |
.resolve_budget(thinking) ⇒ Object
46 47 48 49 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 46 def resolve_budget(thinking) budget = thinking.respond_to?(:budget) ? thinking.budget : thinking budget.is_a?(Integer) ? budget : nil end |
.resolve_effort_level(thinking) ⇒ Object
42 43 44 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 42 def resolve_effort_level(thinking) thinking.respond_to?(:effort) ? thinking.effort : thinking end |
.response_json_schema_supported?(model) ⇒ Boolean
173 174 175 176 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 173 def response_json_schema_supported?(model) version = gemini_version(model) version && version >= Gem::Version.new('2.5') end |
.safe_string(value) ⇒ Object
213 214 215 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 213 def safe_string(value) value&.to_s end |
.structured_output_config(schema, model) ⇒ Object
228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/ruby_llm/providers/gemini/chat.rb', line 228 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 |