Class: Riffer::Providers::Gemini
- Defined in:
- lib/riffer/providers/gemini.rb,
sig/_private/riffer/providers/gemini.rbs,
sig/generated/riffer/providers/gemini.rbs
Overview
Google Gemini provider for Gemini models via the Gemini REST API.
Defined Under Namespace
Classes: Client
Constant Summary collapse
- VALID_MODEL_PATTERN =
/\A[a-zA-Z0-9._-]+\z/- 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_client ⇒ Object
-- : () -> untyped.
-
#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. - #client ⇒ Riffer::Providers::Gemini::Client
-
#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.
-
#global_client ⇒ Object
-- : () -> untyped.
-
#partition_messages(messages) ⇒ Hash[Symbol, untyped]
-- : (Array) -> Hash[Symbol, untyped].
-
#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
Class Method Details
.semconv_provider_name ⇒ String
The GenAI semconv well-known provider name.
: () -> String
26 27 28 |
# File 'lib/riffer/providers/gemini.rb', line 26 def self.semconv_provider_name "gcp.gemini" end |
Instance Method Details
#api_path(model, method) ⇒ String
-- : (String, String) -> String
305 306 307 308 |
# File 'lib/riffer/providers/gemini.rb', line 305 def api_path(model, method) validate_model!(model) "v1beta/models/#{model}:#{method}" end |
#build_client ⇒ Object
-- : () -> untyped
40 41 42 |
# File 'lib/riffer/providers/gemini.rb', line 40 def build_client Riffer::Providers::Gemini::Client.new(**{ api_key: Riffer.config.gemini.api_key }.compact) 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?
136 137 138 139 140 141 142 143 |
# File 'lib/riffer/providers/gemini.rb', line 136 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]
46 47 48 49 50 51 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 |
# File 'lib/riffer/providers/gemini.rb', line 46 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/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
149 150 151 152 153 154 155 156 157 |
# File 'lib/riffer/providers/gemini.rb', line 149 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 |
#client ⇒ Riffer::Providers::Gemini::Client
6 |
# File 'sig/_private/riffer/providers/gemini.rbs', line 6
def client: () -> Riffer::Providers::Gemini::Client
|
#convert_assistant_to_gemini_format(message) ⇒ Hash[Symbol, untyped]
-- : (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/riffer/providers/gemini.rb', line 258 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]
276 277 278 279 280 281 282 283 |
# File 'lib/riffer/providers/gemini.rb', line 276 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]
287 288 289 290 291 292 293 |
# File 'lib/riffer/providers/gemini.rb', line 287 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
297 298 299 300 301 |
# File 'lib/riffer/providers/gemini.rb', line 297 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]
82 83 84 85 86 |
# File 'lib/riffer/providers/gemini.rb', line 82 def execute_generate(params) model = params[:model] body = params.except(:model) client.post(api_path(model, "generateContent"), body) end |
#execute_stream(params, yielder) ⇒ void
This method returns an undefined value.
-- : (Hash[Symbol, untyped], Riffer::Providers::_EventSink) -> void
161 162 163 164 165 166 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 |
# File 'lib/riffer/providers/gemini.rb', line 161 def execute_stream(params, yielder) model = params[:model] body = params.except(:model) 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 path = "#{api_path(model, 'streamGenerateContent')}?alt=sse" client.post_stream(path, body) { |chunk| process_chunk.call(chunk) } 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
90 91 92 93 94 95 |
# File 'lib/riffer/providers/gemini.rb', line 90 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?
126 127 128 129 130 |
# File 'lib/riffer/providers/gemini.rb', line 126 def extract_finish_reason(response) parts = response.dig(:candidates, 0, :content, :parts) has_function_call = parts&.any? { |part| part[:functionCall] } || false 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?
117 118 119 120 121 122 |
# File 'lib/riffer/providers/gemini.rb', line 117 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
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/riffer/providers/gemini.rb', line 99 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 |
#global_client ⇒ Object
-- : () -> untyped
34 35 36 |
# File 'lib/riffer/providers/gemini.rb', line 34 def global_client Riffer.config.gemini.client end |
#partition_messages(messages) ⇒ Hash[Symbol, untyped]
-- : (Array) -> Hash[Symbol, untyped]
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/riffer/providers/gemini.rb', line 220 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 |
#strip_additional_properties(schema) ⇒ Hash[Symbol, untyped]
-- : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
322 323 324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/riffer/providers/gemini.rb', line 322 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 schema[:items] = strip_additional_properties(schema[:items]) if schema[:items].is_a?(Hash) schema end |
#validate_model!(model) ⇒ void
This method returns an undefined value.
-- : (String) -> void
312 313 314 315 316 317 318 |
# File 'lib/riffer/providers/gemini.rb', line 312 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 |