Class: Riffer::Providers::Gemini
- Defined in:
- lib/riffer/providers/gemini.rb,
sig/generated/riffer/providers/gemini.rbs
Overview
Google Gemini provider for Gemini models via the Gemini REST API.
Constant Summary collapse
- BASE_URI =
URI("https://generativelanguage.googleapis.com")
- VALID_MODEL_PATTERN =
: Regexp
/\A[a-zA-Z0-9._-]+\z/- DEFAULT_OPEN_TIMEOUT =
: Integer
10- DEFAULT_READ_TIMEOUT =
: Integer
60- FINISH_REASONS =
{ "STOP" => :stop, "MAX_TOKENS" => :length, "SAFETY" => :content_filter, "RECITATION" => :content_filter, "BLOCKLIST" => :content_filter, "PROHIBITED_CONTENT" => :content_filter, "SPII" => :content_filter, "IMAGE_SAFETY" => :content_filter, "MALFORMED_FUNCTION_CALL" => :error }.freeze
Constants inherited from Base
Base::REQUEST_PARAM_ATTRIBUTES, Base::WIRE_SEPARATOR
Class Method Summary collapse
-
.semconv_provider_name ⇒ String
The GenAI semconv well-known provider name.
Instance Method Summary collapse
-
#api_path(model, method) ⇒ String
-- : (String, String) -> String.
-
#build_finish_reason(raw_reason, tool_calls:) ⇒ Riffer::Providers::FinishReason?
Gemini reports STOP even when the candidate carries functionCall parts, so tool-call presence overrides the raw value.
-
#build_request_params(messages, model, options) ⇒ Hash[Symbol, untyped]
-- : (Array, String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped].
-
#build_token_usage(usage) ⇒ Riffer::Providers::TokenUsage
Gemini reports thinking tokens outside
candidatesTokenCount; TokenUsage's output includes them. -
#convert_assistant_to_gemini_format(message) ⇒ Hash[Symbol, untyped]
-- : (Riffer::Messages::Assistant) -> Hash[Symbol, untyped].
-
#convert_file_part_to_gemini_format(file) ⇒ Hash[Symbol, untyped]
-- : (Riffer::Messages::FilePart) -> Hash[Symbol, untyped].
-
#convert_tool_to_gemini_format(tool) ⇒ Hash[Symbol, untyped]
-- : (singleton(Riffer::Tool)) -> Hash[Symbol, untyped].
-
#encode_tool_arguments(args) ⇒ String
-- : (untyped) -> String.
-
#execute_generate(params) ⇒ Hash[Symbol, untyped]
-- : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped].
-
#execute_stream(params, yielder) ⇒ void
-- : (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void.
-
#extract_content(response) ⇒ String
-- : (Hash[Symbol, untyped]) -> String.
-
#extract_finish_reason(response) ⇒ Riffer::Providers::FinishReason?
-- : (Hash[Symbol, untyped]) -> Riffer::Providers::FinishReason?.
-
#extract_token_usage(response) ⇒ Riffer::Providers::TokenUsage?
-- : (Hash[Symbol, untyped]) -> Riffer::Providers::TokenUsage?.
-
#extract_tool_calls(response) ⇒ Array[Riffer::Messages::Assistant::ToolCall]
-- : (Hash[Symbol, untyped]) -> Array.
-
#handle_api_error!(response) ⇒ void
-- : (Net::HTTPResponse) -> void.
-
#initialize(api_key: nil, open_timeout: nil, read_timeout: nil, **options) ⇒ Gemini
constructor
-- : (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void.
-
#partition_messages(messages) ⇒ Hash[Symbol, untyped]
-- : (Array) -> Hash[Symbol, untyped].
-
#post_request(path, body) ⇒ Net::HTTPResponse
-- : (String, Hash[Symbol, untyped]) -> Net::HTTPResponse.
-
#strip_additional_properties(schema) ⇒ Hash[Symbol, untyped]
-- : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped].
-
#validate_model!(model) ⇒ void
-- : (String) -> void.
Methods inherited from Base
#apply_pricing, #capture_input, #capture_messages?, #capture_output, #chat_span_attributes, #decode_tool_name, #depends_on, #encode_tool_name, #generate_text, #in_chat_span, #merge_consecutive_messages, #normalize_messages, #parse_structured_output, #parse_tool_arguments, #pricing_rates, #record_finish_reason, #record_stream_outcome, skills_adapter, #stream_text, #tag_attributes, #validate_input!, #validate_normalized_messages!, #yield_finish_reason
Constructor Details
#initialize(api_key: nil, open_timeout: nil, read_timeout: nil, **options) ⇒ Gemini
-- : (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void
41 42 43 44 45 46 |
# File 'lib/riffer/providers/gemini.rb', line 41 def initialize(api_key: nil, open_timeout: nil, read_timeout: nil, **) api_key ||= Riffer.config.gemini.api_key @api_key = api_key @open_timeout = open_timeout || Riffer.config.gemini.open_timeout || DEFAULT_OPEN_TIMEOUT @read_timeout = read_timeout || Riffer.config.gemini.read_timeout || DEFAULT_READ_TIMEOUT end |
Class Method Details
.semconv_provider_name ⇒ String
The GenAI semconv well-known provider name.
: () -> String
35 36 37 |
# File 'lib/riffer/providers/gemini.rb', line 35 def self.semconv_provider_name "gcp.gemini" end |
Instance Method Details
#api_path(model, method) ⇒ String
-- : (String, String) -> String
339 340 341 342 |
# File 'lib/riffer/providers/gemini.rb', line 339 def api_path(model, method) validate_model!(model) "v1beta/models/#{model}:#{method}" end |
#build_finish_reason(raw_reason, tool_calls:) ⇒ Riffer::Providers::FinishReason?
Gemini reports STOP even when the candidate carries functionCall parts, so tool-call presence overrides the raw value.
: (String?, tool_calls: bool) -> Riffer::Providers::FinishReason?
144 145 146 147 148 149 150 151 |
# File 'lib/riffer/providers/gemini.rb', line 144 def build_finish_reason(raw_reason, tool_calls:) return nil unless raw_reason raw = raw_reason.to_s reason = FINISH_REASONS.fetch(raw, :other) reason = :tool_calls if reason == :stop && tool_calls Riffer::Providers::FinishReason.new(reason: reason, raw: raw) end |
#build_request_params(messages, model, options) ⇒ Hash[Symbol, untyped]
-- : (Array, String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/riffer/providers/gemini.rb', line 52 def build_request_params(, model, ) partitioned = () tools = [:tools] structured_output = [:structured_output] params = { model: model, contents: partitioned[:contents] } #: Hash[Symbol, untyped] params[:systemInstruction] = partitioned[:system_instruction] if partitioned[:system_instruction] if tools && !tools.empty? params[:tools] = [{ functionDeclarations: tools.map { |t| convert_tool_to_gemini_format(t) } }] end # tags propagate to observability only: the Gemini Developer API has no # request labels field (unknown body fields are rejected), so :tags is # stripped here rather than mapped. Native labels would arrive with a Vertex # adapter. See docs/10_CONFIGURATION.md. generation_config = .except(:tools, :structured_output, :tags) if structured_output generation_config[:responseMimeType] = "application/json" generation_config[:responseSchema] = strip_additional_properties(structured_output.json_schema) end params[:generationConfig] = generation_config unless generation_config.empty? params end |
#build_token_usage(usage) ⇒ Riffer::Providers::TokenUsage
Gemini reports thinking tokens outside candidatesTokenCount;
TokenUsage's output includes them.
: (Hash[Symbol, untyped]) -> Riffer::Providers::TokenUsage
157 158 159 160 161 162 163 |
# File 'lib/riffer/providers/gemini.rb', line 157 def build_token_usage(usage) apply_pricing(Riffer::Providers::TokenUsage.new( input_tokens: usage[:promptTokenCount] || 0, output_tokens: (usage[:candidatesTokenCount] || 0) + (usage[:thoughtsTokenCount] || 0), cache_read_tokens: usage[:cachedContentTokenCount] )) end |
#convert_assistant_to_gemini_format(message) ⇒ Hash[Symbol, untyped]
-- : (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/riffer/providers/gemini.rb', line 280 def convert_assistant_to_gemini_format() parts = [] #: Array[Hash[Symbol, untyped]] parts << {text: .content} if .content && !.content.empty? .tool_calls.each do |tc| parts << { functionCall: { name: tc.name, args: parse_tool_arguments(tc.arguments) } } end {role: "model", parts: parts} end |
#convert_file_part_to_gemini_format(file) ⇒ Hash[Symbol, untyped]
-- : (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
298 299 300 301 302 303 304 305 |
# File 'lib/riffer/providers/gemini.rb', line 298 def convert_file_part_to_gemini_format(file) if file.url? raise Riffer::ArgumentError, "Gemini provider does not support URL-based file references. Provide base64-encoded data instead." end {inlineData: {mimeType: file.media_type, data: file.data}} end |
#convert_tool_to_gemini_format(tool) ⇒ Hash[Symbol, untyped]
-- : (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
309 310 311 312 313 314 315 |
# File 'lib/riffer/providers/gemini.rb', line 309 def convert_tool_to_gemini_format(tool) { name: tool.name, description: tool.description, parameters: strip_additional_properties(tool.parameters_schema) } end |
#encode_tool_arguments(args) ⇒ String
-- : (untyped) -> String
319 320 321 322 323 |
# File 'lib/riffer/providers/gemini.rb', line 319 def encode_tool_arguments(args) return "{}" unless args args.is_a?(String) ? args : args.to_json end |
#execute_generate(params) ⇒ Hash[Symbol, untyped]
-- : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
88 89 90 91 92 93 94 |
# File 'lib/riffer/providers/gemini.rb', line 88 def execute_generate(params) model = params[:model] body = params.except(:model) response = post_request(api_path(model, "generateContent"), body) handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess) JSON.parse(response.body, symbolize_names: true) end |
#execute_stream(params, yielder) ⇒ void
This method returns an undefined value.
-- : (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/riffer/providers/gemini.rb', line 167 def execute_stream(params, yielder) model = params[:model] body = params.except(:model) uri = URI("#{BASE_URI}/#{api_path(model, "streamGenerateContent")}?alt=sse") host = uri.hostname #: String request = Net::HTTP::Post.new(uri) request["Content-Type"] = "application/json" request["x-goog-api-key"] = @api_key request.body = body.to_json full_text = +"" buffer = +"" raw_finish_reason = nil #: String? saw_function_call = false process_chunk = lambda do |chunk| buffer << chunk while (match = buffer.match(/\r?\n\r?\n/)) match_end = match.end(0) #: Integer frame = buffer.slice!(0, match_end).to_s.strip next unless frame.start_with?("data: ") json_str = frame.delete_prefix("data: ").strip next if json_str.empty? parsed = JSON.parse(json_str, symbolize_names: true) parts = parsed.dig(:candidates, 0, :content, :parts) parts&.each do |part| if part[:text] full_text << part[:text] yielder << Riffer::StreamEvents::TextDelta.new(part[:text]) elsif part[:functionCall] fc = part[:functionCall] saw_function_call = true call_id = "gemini_call_#{SecureRandom.hex(12)}" arguments = encode_tool_arguments(fc[:args]) yielder << Riffer::StreamEvents::ToolCallDone.new( item_id: call_id, call_id: call_id, name: fc[:name], arguments: arguments ) end end raw_finish_reason = parsed.dig(:candidates, 0, :finishReason) || raw_finish_reason usage = parsed[:usageMetadata] if usage && usage[:candidatesTokenCount] yielder << Riffer::StreamEvents::TokenUsageDone.new(token_usage: build_token_usage(usage)) end end end Net::HTTP.start(host, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) do |http| http.request(request) do |response| handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess) begin response.read_body { |chunk| process_chunk.call(chunk) } rescue IOError process_chunk.call(response.body) end end end yielder << Riffer::StreamEvents::TextDone.new(full_text) unless full_text.empty? yield_finish_reason(yielder, build_finish_reason(raw_finish_reason, tool_calls: saw_function_call)) end |
#extract_content(response) ⇒ String
-- : (Hash[Symbol, untyped]) -> String
98 99 100 101 102 103 |
# File 'lib/riffer/providers/gemini.rb', line 98 def extract_content(response) parts = response.dig(:candidates, 0, :content, :parts) return "" unless parts parts.filter_map { |part| part[:text] }.join end |
#extract_finish_reason(response) ⇒ Riffer::Providers::FinishReason?
-- : (Hash[Symbol, untyped]) -> Riffer::Providers::FinishReason?
134 135 136 137 138 |
# File 'lib/riffer/providers/gemini.rb', line 134 def extract_finish_reason(response) parts = response.dig(:candidates, 0, :content, :parts) has_function_call = !!parts&.any? { |part| part[:functionCall] } build_finish_reason(response.dig(:candidates, 0, :finishReason), tool_calls: has_function_call) end |
#extract_token_usage(response) ⇒ Riffer::Providers::TokenUsage?
-- : (Hash[Symbol, untyped]) -> Riffer::Providers::TokenUsage?
125 126 127 128 129 130 |
# File 'lib/riffer/providers/gemini.rb', line 125 def extract_token_usage(response) usage = response[:usageMetadata] return nil unless usage build_token_usage(usage) end |
#extract_tool_calls(response) ⇒ Array[Riffer::Messages::Assistant::ToolCall]
-- : (Hash[Symbol, untyped]) -> Array
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/riffer/providers/gemini.rb', line 107 def extract_tool_calls(response) parts = response.dig(:candidates, 0, :content, :parts) return [] unless parts parts.filter_map do |part| next unless part[:functionCall] fc = part[:functionCall] Riffer::Messages::Assistant::ToolCall.new( call_id: "gemini_call_#{SecureRandom.hex(12)}", name: fc[:name], arguments: encode_tool_arguments(fc[:args]) ) end end |
#handle_api_error!(response) ⇒ void
This method returns an undefined value.
-- : (Net::HTTPResponse) -> void
373 374 375 376 377 378 379 380 381 |
# File 'lib/riffer/providers/gemini.rb', line 373 def handle_api_error!(response) body = begin JSON.parse(response.body, symbolize_names: true) rescue JSON::ParserError {message: response.body} end = body.dig(:error, :message) || body[:message] || response.body raise Riffer::Error, "Gemini API error (#{response.code}): #{}" end |
#partition_messages(messages) ⇒ Hash[Symbol, untyped]
-- : (Array) -> Hash[Symbol, untyped]
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/riffer/providers/gemini.rb', line 242 def () system_parts = [] #: Array[Hash[Symbol, untyped]] contents = [] #: Array[Hash[Symbol, untyped]] .each do || case when Riffer::Messages::System system_parts << {text: .content} when Riffer::Messages::User if .files.empty? contents << {role: "user", parts: [{text: .content}]} else parts = [{text: .content}] .files.each { |file| parts << convert_file_part_to_gemini_format(file) } contents << {role: "user", parts: parts} end when Riffer::Messages::Assistant contents << convert_assistant_to_gemini_format() when Riffer::Messages::Tool contents << { role: "user", parts: [{ functionResponse: { name: .name, response: {result: .content} } }] } end end result = {contents: contents} #: Hash[Symbol, untyped] result[:system_instruction] = {parts: system_parts} unless system_parts.empty? result end |
#post_request(path, body) ⇒ Net::HTTPResponse
-- : (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
327 328 329 330 331 332 333 334 335 |
# File 'lib/riffer/providers/gemini.rb', line 327 def post_request(path, body) uri = URI("#{BASE_URI}/#{path}") host = uri.hostname #: String request = Net::HTTP::Post.new(uri) request["Content-Type"] = "application/json" request["x-goog-api-key"] = @api_key request.body = body.to_json Net::HTTP.start(host, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) { |http| http.request(request) } end |
#strip_additional_properties(schema) ⇒ Hash[Symbol, untyped]
-- : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/riffer/providers/gemini.rb', line 354 def strip_additional_properties(schema) schema = schema.dup schema.delete(:additionalProperties) if schema[:properties] schema[:properties] = schema[:properties].transform_values do |prop| strip_additional_properties(prop) end end if schema[:items].is_a?(Hash) schema[:items] = strip_additional_properties(schema[:items]) end schema end |
#validate_model!(model) ⇒ void
This method returns an undefined value.
-- : (String) -> void
346 347 348 349 350 |
# File 'lib/riffer/providers/gemini.rb', line 346 def validate_model!(model) return if model.match?(VALID_MODEL_PATTERN) raise Riffer::ArgumentError, "Invalid model name: #{model.inspect}. Model must contain only alphanumeric characters, hyphens, dots, and underscores." end |