Class: AgentHarness::Providers::Gemini

Inherits:
Base
  • Object
show all
Defined in:
lib/agent_harness/providers/gemini.rb

Overview

Google Gemini CLI provider

Provides integration with the Google Gemini CLI tool.

Constant Summary collapse

MODEL_PATTERN =

Model name pattern for Gemini models

/^gemini-[\d.]+-(?:pro|flash|ultra)(?:-\d+)?$/i
CLI_PACKAGE =
"@google/gemini-cli"
SUPPORTED_CLI_VERSION =
"0.35.3"
SUPPORTED_CLI_REQUIREMENT =
Gem::Requirement.new("= #{SUPPORTED_CLI_VERSION}").freeze

Constants inherited from Base

Base::COMMON_ERROR_PATTERNS, Base::DEFAULT_SMOKE_TEST_CONTRACT

Instance Attribute Summary

Attributes inherited from Base

#config, #executor, #logger

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#configure, #initialize, #sandboxed_environment?, #send_message

Methods included from Adapter

#build_mcp_flags, #dangerous_mode_flags, #fetch_mcp_servers, included, #parse_rate_limit_reset, #send_message, #session_flags, #smoke_test, #smoke_test_contract, #supported_mcp_transports, #supports_dangerous_mode?, #supports_mcp?, #supports_sessions?, #validate_mcp_servers!

Constructor Details

This class inherits a constructor from AgentHarness::Providers::Base

Class Method Details

.available?Boolean

Returns:

  • (Boolean)


28
29
30
31
# File 'lib/agent_harness/providers/gemini.rb', line 28

def available?
  executor = AgentHarness.configuration.command_executor
  !!executor.which(binary_name)
end

.binary_nameObject



24
25
26
# File 'lib/agent_harness/providers/gemini.rb', line 24

def binary_name
  "gemini"
end

.discover_modelsObject



81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/agent_harness/providers/gemini.rb', line 81

def discover_models
  return [] unless available?

  # Gemini CLI doesn't have a standard model listing command
  # Return common models
  [
    {name: "gemini-2.0-flash", family: "gemini-2-0-flash", tier: "standard", provider: "gemini"},
    {name: "gemini-2.5-pro", family: "gemini-2-5-pro", tier: "advanced", provider: "gemini"},
    {name: "gemini-1.5-pro", family: "gemini-1-5-pro", tier: "standard", provider: "gemini"},
    {name: "gemini-1.5-flash", family: "gemini-1-5-flash", tier: "mini", provider: "gemini"}
  ]
end

.firewall_requirementsObject



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/agent_harness/providers/gemini.rb', line 59

def firewall_requirements
  {
    domains: [
      "generativelanguage.googleapis.com",
      "oauth2.googleapis.com",
      "accounts.google.com",
      "www.googleapis.com"
    ],
    ip_ranges: []
  }
end

.install_contract(version: SUPPORTED_CLI_VERSION) ⇒ Object



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
# File 'lib/agent_harness/providers/gemini.rb', line 33

def install_contract(version: SUPPORTED_CLI_VERSION)
  parsed_version = begin
    Gem::Version.new(version)
  rescue ArgumentError
    raise ArgumentError, "Unsupported Gemini CLI version #{version.inspect}. Supported requirement: #{SUPPORTED_CLI_REQUIREMENT}"
  end

  unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(parsed_version)
    raise ArgumentError, "Unsupported Gemini CLI version #{version.inspect}. Supported requirement: #{SUPPORTED_CLI_REQUIREMENT}"
  end

  package_spec = "#{CLI_PACKAGE}@#{version}"

  {
    provider: provider_name,
    source_type: :npm,
    package_name: CLI_PACKAGE,
    supported_version_requirement: SUPPORTED_CLI_REQUIREMENT,
    default_version: SUPPORTED_CLI_VERSION,
    resolved_version: version,
    binary_name: binary_name,
    install_command: ["npm", "install", "-g", "--ignore-scripts", package_spec],
    install_command_string: "npm install -g --ignore-scripts #{package_spec}"
  }
end

.instruction_file_pathsObject



71
72
73
74
75
76
77
78
79
# File 'lib/agent_harness/providers/gemini.rb', line 71

def instruction_file_paths
  [
    {
      path: "GEMINI.md",
      description: "Google Gemini agent instructions",
      symlink: true
    }
  ]
end

.model_family(provider_model_name) ⇒ Object



94
95
96
97
# File 'lib/agent_harness/providers/gemini.rb', line 94

def model_family(provider_model_name)
  # Strip version suffix: "gemini-1.5-pro-001" -> "gemini-1.5-pro"
  provider_model_name.sub(/-\d+$/, "")
end

.provider_model_name(family_name) ⇒ Object



99
100
101
# File 'lib/agent_harness/providers/gemini.rb', line 99

def provider_model_name(family_name)
  family_name
end

.provider_nameObject



20
21
22
# File 'lib/agent_harness/providers/gemini.rb', line 20

def provider_name
  :gemini
end

.smoke_test_contractObject



107
108
109
# File 'lib/agent_harness/providers/gemini.rb', line 107

def smoke_test_contract
  Base::DEFAULT_SMOKE_TEST_CONTRACT
end

.supports_model_family?(family_name) ⇒ Boolean

Returns:

  • (Boolean)


103
104
105
# File 'lib/agent_harness/providers/gemini.rb', line 103

def supports_model_family?(family_name)
  MODEL_PATTERN.match?(family_name) || family_name.start_with?("gemini-")
end

Instance Method Details

#auth_statusObject



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/agent_harness/providers/gemini.rb', line 192

def auth_status
  api_key = [ENV["GEMINI_API_KEY"], ENV["GOOGLE_API_KEY"]].find { |key| key && !key.strip.empty? }
  if api_key
    return {valid: true, expires_at: nil, error: nil, auth_method: :api_key}
  end

  credentials = read_gemini_credentials
  return {valid: false, expires_at: nil, error: "No Gemini credentials found. Run 'gemini auth login' or set GEMINI_API_KEY or GOOGLE_API_KEY"} unless credentials

  token = credentials["access_token"] || credentials["oauth_token"]
  unless token.is_a?(String) && !token.strip.empty?
    return {valid: false, expires_at: nil, error: "No authentication token in Gemini credentials"}
  end

  expires_at = parse_gemini_expiry(credentials)
  if expires_at && expires_at < Time.now
    {valid: false, expires_at: expires_at, error: "Gemini session expired. Run 'gemini auth login' to re-authenticate"}
  else
    {valid: true, expires_at: expires_at, error: nil, auth_method: :oauth}
  end
rescue IOError, JSON::ParserError => e
  {valid: false, expires_at: nil, error: e.message}
end

#auth_typeObject



151
152
153
# File 'lib/agent_harness/providers/gemini.rb', line 151

def auth_type
  :oauth
end

#capabilitiesObject



139
140
141
142
143
144
145
146
147
148
149
# File 'lib/agent_harness/providers/gemini.rb', line 139

def capabilities
  {
    streaming: true,
    file_upload: true,
    vision: true,
    tool_use: true,
    json_mode: true,
    mcp: false,
    dangerous_mode: false
  }
end

#configuration_schemaObject



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/agent_harness/providers/gemini.rb', line 120

def configuration_schema
  {
    fields: [
      {
        name: :model,
        type: :string,
        label: "Model",
        required: false,
        hint: "Gemini model to use (e.g. gemini-2.5-pro, gemini-2.0-flash)",
        # accepts_arbitrary is true because supports_model_family? accepts
        # any string starting with "gemini-", not just discovered models.
        accepts_arbitrary: true
      }
    ],
    auth_modes: [:api_key, :oauth],
    openai_compatible: false
  }
end

#display_nameObject



116
117
118
# File 'lib/agent_harness/providers/gemini.rb', line 116

def display_name
  "Google Gemini"
end

#error_patternsObject



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/agent_harness/providers/gemini.rb', line 168

def error_patterns
  {
    rate_limited: [
      /rate.?limit/i,
      /quota.?exceeded/i,
      /429/
    ],
    auth_expired: [
      /authentication/i,
      /unauthorized/i,
      /invalid.?credentials/i,
      /login.*required/i,
      /not.*logged.*in/i,
      /credentials.*expired/i,
      /account.*not.*verified/i
    ],
    transient: [
      /timeout/i,
      /temporary/i,
      /503/
    ]
  }
end

#execution_semanticsObject



155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/agent_harness/providers/gemini.rb', line 155

def execution_semantics
  {
    prompt_delivery: :flag,
    output_format: :text,
    sandbox_aware: false,
    uses_subcommand: false,
    non_interactive_flag: nil,
    legitimate_exit_codes: [0],
    stderr_is_diagnostic: true,
    parses_rate_limit_reset: false
  }
end

#health_statusObject



216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/agent_harness/providers/gemini.rb', line 216

def health_status
  unless self.class.available?
    return {healthy: false, message: "Gemini CLI not found in PATH. Install from https://github.com/google-gemini/gemini-cli"}
  end

  auth = auth_status
  unless auth[:valid]
    return {healthy: false, message: auth[:error]}
  end

  {healthy: true, message: "Gemini CLI available and authenticated"}
end

#nameObject



112
113
114
# File 'lib/agent_harness/providers/gemini.rb', line 112

def name
  "gemini"
end

#validate_configObject



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/agent_harness/providers/gemini.rb', line 229

def validate_config
  errors = []

  model = @config.model
  if !model.nil? && !model.is_a?(String)
    errors << "model must be a string"
  elsif model.is_a?(String) && !model.empty?
    unless self.class.supports_model_family?(model)
      errors << "Unrecognized model '#{model}'. Expected a Gemini model (e.g., gemini-2.0-flash, gemini-2.5-pro)"
    end
  end

  flags = @config.default_flags
  unless flags.nil?
    if flags.is_a?(Array)
      invalid = flags.reject { |f| f.is_a?(String) }
      errors << "default_flags contains non-string values" if invalid.any?
    else
      errors << "default_flags must be an array of strings"
    end
  end

  {valid: errors.empty?, errors: errors}
end