Class: Nuntius::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/core/client.rb

Overview

Core client class for Gemini AI API communication

Constant Summary collapse

BASE_URL =
'https://generativelanguage.googleapis.com/v1beta/models'
CANONICAL_PATH =

Canonical model source: config/models.yaml (repository-wide) This file is the single source of truth. Code must not duplicate model names.

File.expand_path('../../config/models.yaml', __dir__)
CANONICAL =
begin
  if File.exist?(CANONICAL_PATH)
    YAML.safe_load(File.read(CANONICAL_PATH)) || {}
  else
    {}
  end
rescue StandardError
  {}
end.freeze
CANONICAL_DEFAULT =
(CANONICAL['default'] || 'gemini-3.7-flash').freeze
CANONICAL_FALLBACK =
(CANONICAL['fallback'] || 'gemini-3.6-flash').freeze
CANONICAL_STABLE =
(CANONICAL['stable_baseline'] || 'gemini-3.5-flash').freeze
CANONICAL_LIGHTWEIGHT =
(CANONICAL['lightweight'] || ['gemini-3.1-flash-lite', 'gemini-3.5-flash-lite']).freeze
CANONICAL_LIGHT =
CANONICAL_LIGHTWEIGHT.first || 'gemini-3.1-flash-lite'
MODELS =

generateContent text model aliases: Flash family only (Free-Tier compatible). Values are derived from config/models.yaml. Do not hard-code elsewhere.

{
  flash_latest: 'gemini-flash-latest',
  flash_3_7: CANONICAL_DEFAULT,
  flash_3_6: CANONICAL_FALLBACK,
  flash_3_5: CANONICAL_STABLE,
  flash_3_5_lite: CANONICAL_LIGHTWEIGHT[1] || 'gemini-3.5-flash-lite',
  flash_3_preview: 'gemini-3-flash-preview',
  flash_3_1_lite: CANONICAL_LIGHT,
  flash_2_5: 'gemini-2.5-flash',
  flash_2_0: 'gemini-2.0-flash',

  # Short aliases: default Flash is 3.7, fallback 3.6.
  flash: CANONICAL_DEFAULT,
  flash_fallback: CANONICAL_FALLBACK,
  flash_lite: CANONICAL_LIGHT,
  pro_2_0: 'gemini-2.0-flash' # legacy alias, resolves to Flash (gemini-2.0-flash), keep for backward compat
}.freeze
DEPRECATED_MODELS =

Deprecated/retired models: Pro family requires billing, not usable on Free-Tier. Kept for backward compat but warn and default to :flash (gemini-3.7-flash).

{
  pro_latest: 'gemini-pro-latest',
  pro: 'gemini-pro-latest',
  pro_3_preview: 'gemini-3-pro-preview',
  pro_3_1_preview: 'gemini-3.1-pro-preview',
  pro_2_5: 'gemini-2.5-pro',
  pro_1_5: 'gemini-1.5-pro',
  flash_1_5: 'gemini-1.5-flash',
  flash_8b: 'gemini-1.5-flash-8b'
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key = nil, model: :flash) ⇒ Client

Returns a new instance of Client.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/core/client.rb', line 83

def initialize(api_key = nil, model: :flash)
  # Prioritize passed API key, then environment variable
  @api_key = api_key || ENV.fetch('GEMINI_API_KEY', nil)

  # Rate limiting - track last request time
  @last_request_time = nil
  # More conservative rate limiting in CI environments
  @min_request_interval = ENV['CI'] == 'true' || ENV['GITHUB_ACTIONS'] == 'true' ? 3.0 : 1.0

  # Extensive logging for debugging
  self.class.logger.debug('Initializing Client')
  self.class.logger.debug("API Key present: #{!@api_key.nil?}")
  self.class.logger.debug("API Key length: #{@api_key&.length}")

  # Validate API key before proceeding
  validate_api_key!

  @model = resolve_model(model)

  self.class.logger.debug("Selected model: #{@model}")
end

Class Method Details

.loggerObject

Configure logging



72
73
74
75
76
77
78
79
80
81
# File 'lib/core/client.rb', line 72

def self.logger
  @logger ||= Logger.new($stdout).tap do |log|
    log.level = Logger::DEBUG
    log.formatter = proc do |severity, datetime, _progname, msg|
      # Mask any potential API key in logs
      masked_msg = msg.to_s.gsub(/AIza[a-zA-Z0-9_-]{35,}/, '[REDACTED]')
      "#{datetime}: #{severity} -- #{masked_msg}\n"
    end
  end
end

Instance Method Details

#chat(messages, options = {}) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/core/client.rb', line 143

def chat(messages, options = {})
  request_body = {
    contents: messages.map { |msg| { role: msg[:role], parts: [{ text: msg[:content] }] } },
    generationConfig: build_generation_config(options)
  }

  # Add system instruction if provided
  if options[:system_instruction]
    request_body[:systemInstruction] = {
      parts: [
        { text: options[:system_instruction] }
      ]
    }
  end

  apply_moderation(send_request(request_body), options)
end

#generate_image_text(image_base64, prompt, options = {}) ⇒ Object

Raises:



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/core/client.rb', line 126

def generate_image_text(image_base64, prompt, options = {})
  raise Error, 'Image is required' if image_base64.nil? || image_base64.empty?

  request_body = {
    contents: [
      { parts: [
        { inline_data: { mime_type: 'image/jpeg', data: image_base64 } },
        { text: prompt }
      ] }
    ],
    generationConfig: build_generation_config(options)
  }

  # Use the flash model for image-to-text tasks (Free-Tier compatible)
  apply_moderation(send_request(request_body, model: :flash), options)
end

#generate_text(prompt, options = {}) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/core/client.rb', line 105

def generate_text(prompt, options = {})
  validate_prompt!(prompt)

  request_body = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: build_generation_config(options)
  }

  # Add safety settings if provided
  if options[:safety_settings]
    request_body[:safetySettings] = options[:safety_settings].map do |setting|
      {
        category: setting[:category],
        threshold: setting[:threshold]
      }
    end
  end

  apply_moderation(send_request(request_body), options)
end