Class: Spectre::Gemini::Completions
- Inherits:
-
Object
- Object
- Spectre::Gemini::Completions
- Defined in:
- lib/spectre/gemini/completions.rb
Constant Summary collapse
- API_URL =
Using Google’s OpenAI-compatible endpoint
'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions'- DEFAULT_MODEL =
'gemini-2.5-flash'- DEFAULT_TIMEOUT =
60
Class Method Summary collapse
-
.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) ⇒ Hash
Class method to generate a completion based on user messages and optional tools.
-
.generate_body(messages, model, json_schema, max_tokens, tools, forwarded) ⇒ Object
Helper method to generate the request body (OpenAI-compatible).
-
.handle_response(response) ⇒ Object
Handles the API response, mirroring OpenAI semantics.
-
.validate_messages!(messages) ⇒ Object
Validate the structure and content of the messages array.
Class Method Details
.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) ⇒ Hash
Class method to generate a completion based on user messages and optional tools
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/spectre/gemini/completions.rb', line 26 def self.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) api_key = Spectre.gemini_configuration&.api_key raise APIKeyNotConfiguredError, "API key is not configured" unless api_key () uri = URI(API_URL) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.read_timeout = args.fetch(:read_timeout, DEFAULT_TIMEOUT) http.open_timeout = args.fetch(:open_timeout, DEFAULT_TIMEOUT) request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{api_key}" }) max_tokens = args[:max_tokens] # Forward extra args (like temperature) into the body, excluding control/network keys forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :max_tokens].include?(k) } request.body = generate_body(, model, json_schema, max_tokens, tools, forwarded).to_json response = http.request(request) unless response.is_a?(Net::HTTPSuccess) raise "Gemini API Error: #{response.code} - #{response.message}: #{response.body}" end parsed_response = JSON.parse(response.body) handle_response(parsed_response) rescue JSON::ParserError => e raise "JSON Parse Error: #{e.message}" end |
.generate_body(messages, model, json_schema, max_tokens, tools, forwarded) ⇒ Object
Helper method to generate the request body (OpenAI-compatible)
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/spectre/gemini/completions.rb', line 81 def self.generate_body(, model, json_schema, max_tokens, tools, forwarded) body = { model: model, messages: } body[:max_tokens] = max_tokens if max_tokens body[:response_format] = { type: 'json_schema', json_schema: json_schema } if json_schema body[:tools] = tools if tools # Merge any extra forwarded options (e.g., temperature, top_p) if forwarded && !forwarded.empty? body.merge!(forwarded.transform_keys(&:to_sym)) end body end |
.handle_response(response) ⇒ Object
Handles the API response, mirroring OpenAI semantics
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/spectre/gemini/completions.rb', line 100 def self.handle_response(response) = response.dig('choices', 0, 'message') finish_reason = response.dig('choices', 0, 'finish_reason') if && ['refusal'] raise "Refusal: #{message['refusal']}" end if finish_reason == 'length' raise "Incomplete response: The completion was cut off due to token limit." end if finish_reason == 'content_filter' raise "Content filtered: The model's output was blocked due to policy violations." end if finish_reason == 'function_call' || finish_reason == 'tool_calls' return { tool_calls: ['tool_calls'], content: ['content'] } end if finish_reason == 'stop' return { content: ['content'] } end raise "Unexpected finish_reason: #{finish_reason}" end |
.validate_messages!(messages) ⇒ Object
Validate the structure and content of the messages array.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/spectre/gemini/completions.rb', line 63 def self.() unless .is_a?(Array) && .all? { |msg| msg.is_a?(Hash) } raise ArgumentError, "Messages must be an array of message hashes." end if .empty? raise ArgumentError, "Messages cannot be empty." end # Gemini's OpenAI-compatible chat endpoint requires that single-turn # and general requests end with a user message. If not, return a clear error. last_role = (.last[:role] || .last['role']).to_s unless last_role == 'user' raise ArgumentError, "Gemini: the last message must have role 'user'. Got '#{last_role}'." end end |