feat: add Google Gemini as alternative LLM provider#1968
Conversation
- Add `gemini_api_key` and `gemini_model` fields to Setting (encrypted) - Add `gemini` factory to Provider::Registry using Gemini's OpenAI-compatible endpoint (https://generativelanguage.googleapis.com/v1beta/openai/) - Fall back from OpenAI provider to Gemini when no OpenAI key is configured; OpenAI takes priority when both are set - Fix `chat_response` to use the configured model for custom providers (Gemini, Ollama, etc.) instead of the caller-supplied OpenAI model name - Fix `ai_available?` to also check for Gemini API key so "Enable AI Chats" is accessible when only Gemini is configured - Update chat unavailable copy to mention both OpenAI and Gemini - Restructure Self-Hosting settings UI: OpenAI and Gemini displayed as peers with an "or" divider; token budget extracted into its own section - Add Gemini settings partial with optional model field (defaults to gemini-2.5-flash) - Add tests: 6 registry tests, 1 user test, 2 openai model-routing tests
📝 WalkthroughWalkthroughThis PR extends the application to support Google Gemini as an alternative AI provider alongside OpenAI. It adds Gemini configuration fields with encryption, implements provider selection with fallback logic, refines OpenAI's model handling for custom endpoints, and provides UI and localization for the new settings. ChangesGemini AI Provider Integration
Sequence Diagram(s)sequenceDiagram
participant RegistryClient
participant ProviderRegistry
participant SettingModel
participant ProviderOpenai
RegistryClient->>ProviderRegistry: openai() / gemini()
ProviderRegistry->>SettingModel: gemini_api_key, gemini_model
ProviderRegistry->>ProviderRegistry: Gemini API key missing?
alt Gemini key exists
ProviderRegistry->>ProviderOpenai: new with Gemini base URL
ProviderOpenai-->>ProviderRegistry: Provider::Openai instance
else OpenAI missing, fallback
ProviderRegistry->>ProviderRegistry: openai() → gemini()
else Both absent
ProviderRegistry-->>RegistryClient: nil
end
ProviderRegistry-->>RegistryClient: Provider instance or nil
flowchart
chat_response[chat_response called]
chat_response --> custom_check{custom_provider?}
custom_check -->|yes| use_default["effective_model = `@default_model`"]
custom_check -->|no| use_caller["effective_model = model param"]
use_default --> delegate["pass effective_model to native/generic response"]
use_caller --> delegate
delegate --> result["Return response"]
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 526ccf000c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| # Fall back to Gemini (via its OpenAI-compatible endpoint) when no | ||
| # OpenAI key is configured — all existing call-sites continue to work. | ||
| return gemini unless access_token.present? |
There was a problem hiding this comment.
Avoid OpenAI→Gemini fallback for OpenAI-specific model calls
Returning gemini from openai here changes all Provider::Registry.get_provider(:openai) call sites to use Gemini when no OpenAI key is set, but some of those paths still pass OpenAI model names explicitly. For example, Assistant::Function::ImportBankStatement#execute always sends model: openai_model (defaulting to gpt-4.1), and Provider::Openai#extract_bank_statement does not override that model for custom providers, so Gemini-only deployments will send an invalid model and fail extraction requests.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/models/setting.rb`:
- Around line 13-14: Add a setter for gemini_model that mirrors openai_model=:
implement a gemini_model=(val) method in the Setting model that compares the new
value to the current value, writes the attribute (same persistence approach used
by openai_model=, e.g. write_attribute or self[:gemini_model]=) and, if the
model changed, calls the same cache invalidation routine used by openai_model=
(the method used there — e.g. clear_ai_cache!, clear_all_ai_cache, or
equivalent) so Gemini model changes flush the AI cache.
In `@test/models/user_test.rb`:
- Around line 279-292: The test mutates Setting.openai_access_token but only
restores Setting.gemini_api_key in the ensure block; capture the original
Setting.openai_access_token at the start of the test (e.g., store it in a local
variable like previous_openai), and then restore it alongside previous for
Setting.gemini_api_key in the ensure block so both settings (openai_access_token
and gemini_api_key) are restored after the test in the test "ai_available?
returns true when gemini api key set in settings".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d38f1afa-2cca-4e88-a057-078200b83491
📒 Files selected for processing (14)
app/controllers/settings/hostings_controller.rbapp/models/provider/openai.rbapp/models/provider/registry.rbapp/models/setting.rbapp/models/user.rbapp/views/settings/hostings/_gemini_settings.html.erbapp/views/settings/hostings/_llm_token_budget.html.erbapp/views/settings/hostings/_openai_settings.html.erbapp/views/settings/hostings/show.html.erbconfig/locales/views/chats/en.ymlconfig/locales/views/settings/hostings/en.ymltest/models/provider/openai_test.rbtest/models/provider/registry_test.rbtest/models/user_test.rb
💤 Files with no reviewable changes (1)
- app/views/settings/hostings/_openai_settings.html.erb
| field :gemini_api_key, type: :string, default: ENV["GEMINI_API_KEY"] | ||
| field :gemini_model, type: :string, default: ENV["GEMINI_MODEL"] |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Mirror cache invalidation behavior for gemini_model changes.
openai_model= clears AI cache when the model changes, but gemini_model currently has no equivalent hook. This can leave stale cached AI outputs after switching Gemini models.
💡 Suggested parity fix
class << self
alias_method :raw_onboarding_state, :onboarding_state
alias_method :raw_onboarding_state=, :onboarding_state=
alias_method :raw_openai_model, :openai_model
alias_method :raw_openai_model=, :openai_model=
+ alias_method :raw_gemini_model, :gemini_model
+ alias_method :raw_gemini_model=, :gemini_model=
@@
def openai_model=(value)
old_value = raw_openai_model
self.raw_openai_model = value
if old_value != value && old_value.present?
Rails.logger.info("OpenAI model changed from #{old_value} to #{value}, clearing AI cache for all families")
Family.find_each do |family|
ClearAiCacheJob.perform_later(family)
end
end
end
+
+ def gemini_model=(value)
+ old_value = raw_gemini_model
+ self.raw_gemini_model = value
+
+ if old_value != value && old_value.present?
+ Rails.logger.info("Gemini model changed from #{old_value} to #{value}, clearing AI cache for all families")
+ Family.find_each do |family|
+ ClearAiCacheJob.perform_later(family)
+ end
+ end
+ end
end🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/models/setting.rb` around lines 13 - 14, Add a setter for gemini_model
that mirrors openai_model=: implement a gemini_model=(val) method in the Setting
model that compares the new value to the current value, writes the attribute
(same persistence approach used by openai_model=, e.g. write_attribute or
self[:gemini_model]=) and, if the model changed, calls the same cache
invalidation routine used by openai_model= (the method used there — e.g.
clear_ai_cache!, clear_all_ai_cache, or equivalent) so Gemini model changes
flush the AI cache.
| test "ai_available? returns true when gemini api key set in settings" do | ||
| Rails.application.config.app_mode.stubs(:self_hosted?).returns(true) | ||
| previous = Setting.gemini_api_key | ||
| with_env_overrides OPENAI_ACCESS_TOKEN: nil, GEMINI_API_KEY: nil, EXTERNAL_ASSISTANT_URL: nil, EXTERNAL_ASSISTANT_TOKEN: nil do | ||
| Setting.openai_access_token = nil | ||
| Setting.gemini_api_key = nil | ||
| assert_not @user.ai_available? | ||
|
|
||
| Setting.gemini_api_key = "gemini-test-key" | ||
| assert @user.ai_available? | ||
| end | ||
| ensure | ||
| Setting.gemini_api_key = previous | ||
| end |
There was a problem hiding this comment.
Restore OpenAI setting in teardown path for this test.
Line 283 mutates Setting.openai_access_token, but the ensure block only restores Gemini. This leaks global state and can make later tests order-dependent.
Suggested fix
test "ai_available? returns true when gemini api key set in settings" do
Rails.application.config.app_mode.stubs(:self_hosted?).returns(true)
- previous = Setting.gemini_api_key
+ previous_openai = Setting.openai_access_token
+ previous_gemini = Setting.gemini_api_key
with_env_overrides OPENAI_ACCESS_TOKEN: nil, GEMINI_API_KEY: nil, EXTERNAL_ASSISTANT_URL: nil, EXTERNAL_ASSISTANT_TOKEN: nil do
Setting.openai_access_token = nil
Setting.gemini_api_key = nil
assert_not `@user.ai_available`?
Setting.gemini_api_key = "gemini-test-key"
assert `@user.ai_available`?
end
ensure
- Setting.gemini_api_key = previous
+ Setting.openai_access_token = previous_openai
+ Setting.gemini_api_key = previous_gemini
end🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/models/user_test.rb` around lines 279 - 292, The test mutates
Setting.openai_access_token but only restores Setting.gemini_api_key in the
ensure block; capture the original Setting.openai_access_token at the start of
the test (e.g., store it in a local variable like previous_openai), and then
restore it alongside previous for Setting.gemini_api_key in the ensure block so
both settings (openai_access_token and gemini_api_key) are restored after the
test in the test "ai_available? returns true when gemini api key set in
settings".
|
Reusing Transparent fallback in return gemini unless access_token.present?Any code that calls Hardcoded Gemini endpoint URL uri_base: "https://generativelanguage.googleapis.com/v1beta/openai/"There's no
The diff references Both providers listed in Adding Generated by Claude Code |

Summary
https://generativelanguage.googleapis.com/v1beta/openai/) via the existingProvider::Openaiclass — no new provider class neededOPENAI_ACCESS_TOKEN/openai_access_tokensetting is present; falls through to Gemini otherwise; if both are set, OpenAI winschat_responsewas always forwarding the caller-supplied model name (e.g."gpt-4.1") to custom providers, causing 404s from Gemini's endpoint. Now uses the provider's configured@default_modelfor any custom (non-standard-OpenAI) providerai_available?only checked OpenAI keys; updated to also check Gemini key so "Enable AI Chats" appears when only Gemini is configuredgemini-2.5-flash)Test plan
test/models/provider/registry_test.rb— 6 new tests covering Gemini provider creation, model precedence, nil when no key, and OpenAI↔Gemini fallback/prioritytest/models/user_test.rb— 1 new test:ai_available?returns true when only Gemini key is settest/models/provider/openai_test.rb— 2 new tests: custom providers use configured model; standard OpenAI passes caller-supplied model throughGEMINI_API_KEYonly → "Enable AI Chats" button appears, chat works🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation