feat: add Gemini CLI provider integration (#647)

* feat: add Gemini CLI provider for AI model execution

- Add GeminiProvider class extending CliProvider for Gemini CLI integration
- Add Gemini models (Gemini 3 Pro/Flash Preview, 2.5 Pro/Flash/Flash-Lite)
- Add gemini-models.ts with model definitions and types
- Update ModelProvider type to include 'gemini'
- Add isGeminiModel() to provider-utils.ts for model detection
- Register Gemini provider in provider-factory with priority 4
- Add Gemini setup detection routes (status, auth, deauth)
- Add GeminiCliStatus to setup store for UI state management
- Add Gemini to PROVIDER_ICON_COMPONENTS for UI icon display
- Add GEMINI_MODELS to model-display for dropdown population
- Support thinking levels: off, low, medium, high

Based on https://github.com/google-gemini/gemini-cli

* chore: update package-lock.json

* feat(ui): add Gemini provider to settings and setup wizard

- Add GeminiCliStatus component for CLI detection display
- Add GeminiSettingsTab component for global settings
- Update provider-tabs.tsx to include Gemini as 5th tab
- Update providers-setup-step.tsx with Gemini provider detection
- Add useGeminiCliStatus hook for querying CLI status
- Add getGeminiStatus, authGemini, deauthGemini to HTTP API client
- Add gemini query key for React Query
- Fix GeminiModelId type to not double-prefix model IDs

* feat(ui): add Gemini to settings sidebar navigation

- Add 'gemini-provider' to SettingsViewId type
- Add GeminiIcon and gemini-provider to navigation config
- Add gemini-provider to NAV_ID_TO_PROVIDER mapping
- Add gemini-provider case in settings-view switch
- Export GeminiSettingsTab from providers index

This fixes the missing Gemini entry in the AI Providers sidebar menu.

* feat(ui): add Gemini model configuration in settings

- Create GeminiModelConfiguration component for model selection
- Add enabledGeminiModels and geminiDefaultModel state to app-store
- Add setEnabledGeminiModels, setGeminiDefaultModel, toggleGeminiModel actions
- Update GeminiSettingsTab to show model configuration when CLI is installed
- Import GeminiModelId and getAllGeminiModelIds from types

This adds the ability to configure which Gemini models are available
in the feature modal, similar to other providers like Codex and OpenCode.

* feat(ui): add Gemini models to all model dropdowns

- Add GEMINI_MODELS to model-constants.ts for UI dropdowns
- Add Gemini to ALL_MODELS array used throughout the app
- Add GeminiIcon to PROFILE_ICONS mapping
- Fix GEMINI_MODELS in model-display.ts to use correct model IDs
- Update getModelDisplayName to handle Gemini models correctly

Gemini models now appear in all model selection dropdowns including
Model Defaults, Feature Defaults, and feature card settings.

* fix(gemini): fix CLI integration and event handling

- Fix model ID prefix handling: strip gemini- prefix in agent-service,
  add it back in buildCliArgs for CLI invocation
- Fix event normalization to match actual Gemini CLI output format:
  - type: 'init' (not 'system')
  - type: 'message' with role (not 'assistant')
  - tool_name/tool_id/parameters/output field names
- Add --sandbox false and --approval-mode yolo for faster execution
- Remove thinking level selector from UI (Gemini CLI doesn't support it)
- Update auth status to show errors properly

* test: update provider-factory tests for Gemini provider

- Add GeminiProvider import and spy mock
- Update expected provider count from 4 to 5
- Add test for GeminiProvider inclusion
- Add gemini key to checkAllProviders test

* fix(gemini): address PR review feedback

- Fix npm package name from @anthropic-ai/gemini-cli to @google/gemini-cli
- Fix comments in gemini-provider.ts to match actual CLI output format
- Convert sync fs operations to async using fs/promises

* fix(settings): add Gemini and Codex settings to sync

Add enabledGeminiModels, geminiDefaultModel, enabledCodexModels, and
codexDefaultModel to SETTINGS_FIELDS_TO_SYNC for persistence across sessions.

* fix(gemini): address additional PR review feedback

- Use 'Speed' badge for non-thinking Gemini models (consistency)
- Fix installCommand mapping in gemini-settings-tab.tsx
- Add hasEnvApiKey to GeminiCliStatus interface for API parity
- Clarify GeminiThinkingLevel comment (CLI doesn't support --thinking-level)

* fix(settings): restore Codex and Gemini settings from server

Add sanitization and restoration logic for enabledCodexModels,
codexDefaultModel, enabledGeminiModels, and geminiDefaultModel
in refreshSettingsFromServer() to match the fields in SETTINGS_FIELDS_TO_SYNC.

* feat(gemini): normalize tool names and fix workspace restrictions

- Add tool name mapping to normalize Gemini CLI tool names to standard
  names (e.g., write_todos -> TodoWrite, read_file -> Read)
- Add normalizeGeminiToolInput to convert write_todos format to TodoWrite
  format (description -> content, handle cancelled status)
- Pass --include-directories with cwd to fix workspace restriction errors
  when Gemini CLI has a different cached workspace from previous sessions

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Stefan de Vogelaere
2026-01-23 01:42:17 +01:00
committed by GitHub
parent 7773db559d
commit f480386905
33 changed files with 2408 additions and 27 deletions

View File

@@ -0,0 +1,101 @@
/**
* Gemini CLI Model Definitions
*
* Defines available models for Gemini CLI integration.
* Based on https://github.com/google-gemini/gemini-cli
*/
/**
* Gemini model configuration
*/
export interface GeminiModelConfig {
label: string;
description: string;
supportsVision: boolean;
supportsThinking: boolean;
contextWindow?: number;
}
/**
* Available Gemini models via the Gemini CLI
* Models from Gemini 2.5 and 3.0 series
*
* Model IDs use 'gemini-' prefix for consistent provider routing (like Cursor).
* When passed to the CLI, the prefix is part of the actual model name.
*/
export const GEMINI_MODEL_MAP = {
// Gemini 3 Series (latest)
'gemini-3-pro-preview': {
label: 'Gemini 3 Pro Preview',
description: 'Most advanced Gemini model with deep reasoning capabilities.',
supportsVision: true,
supportsThinking: true,
contextWindow: 1000000,
},
'gemini-3-flash-preview': {
label: 'Gemini 3 Flash Preview',
description: 'Fast Gemini 3 model for quick tasks.',
supportsVision: true,
supportsThinking: true,
contextWindow: 1000000,
},
// Gemini 2.5 Series
'gemini-2.5-pro': {
label: 'Gemini 2.5 Pro',
description: 'Advanced model with strong reasoning and 1M context.',
supportsVision: true,
supportsThinking: true,
contextWindow: 1000000,
},
'gemini-2.5-flash': {
label: 'Gemini 2.5 Flash',
description: 'Balanced speed and capability for most tasks.',
supportsVision: true,
supportsThinking: true,
contextWindow: 1000000,
},
'gemini-2.5-flash-lite': {
label: 'Gemini 2.5 Flash Lite',
description: 'Fastest Gemini model for simple tasks.',
supportsVision: true,
supportsThinking: false,
contextWindow: 1000000,
},
} as const satisfies Record<string, GeminiModelConfig>;
/**
* Gemini model ID type (keys already have gemini- prefix)
*/
export type GeminiModelId = keyof typeof GEMINI_MODEL_MAP;
/**
* Get all Gemini model IDs
*/
export function getAllGeminiModelIds(): GeminiModelId[] {
return Object.keys(GEMINI_MODEL_MAP) as GeminiModelId[];
}
/**
* Default Gemini model (balanced choice)
*/
export const DEFAULT_GEMINI_MODEL: GeminiModelId = 'gemini-2.5-flash';
/**
* Thinking level configuration for Gemini models
* Note: The Gemini CLI does not currently expose a --thinking-level flag.
* Thinking control (thinkingLevel/thinkingBudget) is available via the Gemini API.
* This type is defined for potential future CLI support or API-level configuration.
*/
export type GeminiThinkingLevel = 'off' | 'low' | 'medium' | 'high';
/**
* Gemini CLI authentication status
*/
export interface GeminiAuthStatus {
authenticated: boolean;
method: 'google_login' | 'api_key' | 'vertex_ai' | 'none';
hasApiKey?: boolean;
hasEnvApiKey?: boolean;
hasCredentialsFile?: boolean;
error?: string;
}

View File

@@ -205,6 +205,7 @@ export {
export type { ModelOption, ThinkingLevelOption, ReasoningEffortOption } from './model-display.js';
export {
CLAUDE_MODELS,
GEMINI_MODELS,
THINKING_LEVELS,
THINKING_LEVEL_LABELS,
REASONING_EFFORT_LEVELS,
@@ -249,6 +250,9 @@ export * from './cursor-cli.js';
// OpenCode types
export * from './opencode-models.js';
// Gemini types
export * from './gemini-models.js';
// Provider utilities
export {
PROVIDER_PREFIXES,
@@ -256,6 +260,7 @@ export {
isClaudeModel,
isCodexModel,
isOpencodeModel,
isGeminiModel,
getModelProvider,
stripProviderPrefix,
addProviderPrefix,

View File

@@ -10,20 +10,21 @@ import type { ReasoningEffort } from './provider.js';
import type { CursorModelId } from './cursor-models.js';
import type { AgentModel, CodexModelId } from './model.js';
import { CODEX_MODEL_MAP } from './model.js';
import { GEMINI_MODEL_MAP, type GeminiModelId } from './gemini-models.js';
/**
* ModelOption - Display metadata for a model option in the UI
*/
export interface ModelOption {
/** Model identifier (supports both Claude and Cursor models) */
id: ModelAlias | CursorModelId;
/** Model identifier (supports Claude, Cursor, Gemini models) */
id: ModelAlias | CursorModelId | GeminiModelId;
/** Display name shown to user */
label: string;
/** Descriptive text explaining model capabilities */
description: string;
/** Optional badge text (e.g., "Speed", "Balanced", "Premium") */
badge?: string;
/** AI provider (supports 'claude' and 'cursor') */
/** AI provider */
provider: ModelProvider;
}
@@ -113,6 +114,22 @@ export const CODEX_MODELS: (ModelOption & { hasReasoning?: boolean })[] = [
},
];
/**
* Gemini model options with full metadata for UI display
* Based on https://github.com/google-gemini/gemini-cli
* Model IDs match the keys in GEMINI_MODEL_MAP (e.g., 'gemini-2.5-flash')
*/
export const GEMINI_MODELS: (ModelOption & { hasThinking?: boolean })[] = Object.entries(
GEMINI_MODEL_MAP
).map(([id, config]) => ({
id: id as GeminiModelId,
label: config.label,
description: config.description,
badge: config.supportsThinking ? 'Thinking' : 'Speed',
provider: 'gemini' as const,
hasThinking: config.supportsThinking,
}));
/**
* Thinking level options with display labels
*
@@ -200,5 +217,16 @@ export function getModelDisplayName(model: ModelAlias | string): string {
[CODEX_MODEL_MAP.gpt52]: 'GPT-5.2',
[CODEX_MODEL_MAP.gpt51]: 'GPT-5.1',
};
return displayNames[model] || model;
// Check direct match first
if (model in displayNames) {
return displayNames[model];
}
// Check Gemini model map - IDs are like 'gemini-2.5-flash'
if (model in GEMINI_MODEL_MAP) {
return GEMINI_MODEL_MAP[model as keyof typeof GEMINI_MODEL_MAP].label;
}
return model;
}

View File

@@ -3,6 +3,7 @@
*/
import type { CursorModelId } from './cursor-models.js';
import type { OpencodeModelId } from './opencode-models.js';
import type { GeminiModelId } from './gemini-models.js';
/**
* Canonical Claude model IDs with provider prefix
@@ -119,6 +120,7 @@ export type DynamicModelId = `${string}/${string}`;
*/
export type PrefixedCursorModelId = `cursor-${string}`;
export type PrefixedOpencodeModelId = `opencode-${string}`;
export type PrefixedGeminiModelId = `gemini-${string}`;
/**
* ModelId - Unified model identifier across providers
@@ -127,7 +129,9 @@ export type ModelId =
| ModelAlias
| CodexModelId
| CursorModelId
| GeminiModelId
| OpencodeModelId
| DynamicModelId
| PrefixedCursorModelId
| PrefixedOpencodeModelId;
| PrefixedOpencodeModelId
| PrefixedGeminiModelId;

View File

@@ -10,12 +10,14 @@ import type { ModelProvider } from './settings.js';
import { CURSOR_MODEL_MAP, LEGACY_CURSOR_MODEL_MAP } from './cursor-models.js';
import { CLAUDE_MODEL_MAP, CODEX_MODEL_MAP } from './model.js';
import { OPENCODE_MODEL_CONFIG_MAP, LEGACY_OPENCODE_MODEL_MAP } from './opencode-models.js';
import { GEMINI_MODEL_MAP } from './gemini-models.js';
/** Provider prefix constants */
export const PROVIDER_PREFIXES = {
cursor: 'cursor-',
codex: 'codex-',
opencode: 'opencode-',
gemini: 'gemini-',
} as const;
/**
@@ -90,6 +92,28 @@ export function isCodexModel(model: string | undefined | null): boolean {
return model in CODEX_MODEL_MAP;
}
/**
* Check if a model string represents a Gemini model
*
* @param model - Model string to check (e.g., "gemini-2.5-pro", "gemini-3-pro-preview")
* @returns true if the model is a Gemini model
*/
export function isGeminiModel(model: string | undefined | null): boolean {
if (!model || typeof model !== 'string') return false;
// Canonical format: gemini- prefix (e.g., "gemini-2.5-flash")
if (model.startsWith(PROVIDER_PREFIXES.gemini)) {
return true;
}
// Check if it's a known Gemini model ID (map keys include gemini- prefix)
if (model in GEMINI_MODEL_MAP) {
return true;
}
return false;
}
/**
* Check if a model string represents an OpenCode model
*
@@ -151,7 +175,11 @@ export function isOpencodeModel(model: string | undefined | null): boolean {
* @returns The provider type, defaults to 'claude' for unknown models
*/
export function getModelProvider(model: string | undefined | null): ModelProvider {
// Check OpenCode first since it uses provider-prefixed formats that could conflict
// Check Gemini first since it uses gemini- prefix
if (isGeminiModel(model)) {
return 'gemini';
}
// Check OpenCode next since it uses provider-prefixed formats that could conflict
if (isOpencodeModel(model)) {
return 'opencode';
}
@@ -199,6 +227,7 @@ export function stripProviderPrefix(model: string): string {
* addProviderPrefix('cursor-composer-1', 'cursor') // 'cursor-composer-1' (no change)
* addProviderPrefix('gpt-5.2', 'codex') // 'codex-gpt-5.2'
* addProviderPrefix('sonnet', 'claude') // 'sonnet' (Claude doesn't use prefix)
* addProviderPrefix('2.5-flash', 'gemini') // 'gemini-2.5-flash'
*/
export function addProviderPrefix(model: string, provider: ModelProvider): string {
if (!model || typeof model !== 'string') return model;
@@ -215,6 +244,10 @@ export function addProviderPrefix(model: string, provider: ModelProvider): strin
if (!model.startsWith(PROVIDER_PREFIXES.opencode)) {
return `${PROVIDER_PREFIXES.opencode}${model}`;
}
} else if (provider === 'gemini') {
if (!model.startsWith(PROVIDER_PREFIXES.gemini)) {
return `${PROVIDER_PREFIXES.gemini}${model}`;
}
}
// Claude models don't use prefixes
return model;
@@ -250,6 +283,7 @@ export function normalizeModelString(model: string | undefined | null): string {
model.startsWith(PROVIDER_PREFIXES.cursor) ||
model.startsWith(PROVIDER_PREFIXES.codex) ||
model.startsWith(PROVIDER_PREFIXES.opencode) ||
model.startsWith(PROVIDER_PREFIXES.gemini) ||
model.startsWith('claude-')
) {
return model;

View File

@@ -99,7 +99,7 @@ export function getThinkingTokenBudget(level: ThinkingLevel | undefined): number
}
/** ModelProvider - AI model provider for credentials and API key management */
export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode';
export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode' | 'gemini';
// ============================================================================
// Claude-Compatible Providers - Configuration for Claude-compatible API endpoints