diff --git a/README.md b/README.md index a21519b..d71257a 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ providers: anthropic: model: "claude-haiku-4-5" # Uses Claude Code CLI - no API key needed num_suggestions: 10 # Number of commit suggestions to generate + gemini: + model: "flash" # Uses Gemini CLI - no API key needed + num_suggestions: 10 # Number of commit suggestions to generate ``` > [!NOTE] diff --git a/cmd/commit.go b/cmd/commit.go index 92b4044..9787f16 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -80,6 +80,12 @@ var commitCmd = &cobra.Command{ numSuggestions = 10 } aiProvider = provider.NewAnthropicProvider(model, numSuggestions) + case "gemini": + numSuggestions := config.GetNumSuggestions() + if numSuggestions <= 0 { + numSuggestions = 10 + } + aiProvider = provider.NewGeminiProvider(model, numSuggestions) default: // Default to copilot if provider is not set or unknown aiProvider = provider.NewCopilotProvider(apiKey, endpoint) diff --git a/cmd/config.go b/cmd/config.go index 2d9b2ed..d8f23a0 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -81,7 +81,7 @@ func runInteractiveConfig() { providerPrompt := &survey.Select{ Message: "Choose a provider:", - Options: []string{"openai", "copilot", "anthropic"}, + Options: []string{"openai", "copilot", "anthropic", "gemini"}, Default: currentProvider, } var selectedProvider string @@ -152,8 +152,8 @@ func runInteractiveConfig() { fmt.Printf("Language set to: %s\n", langValue) } - // API key configuration - skip for copilot and anthropic - if selectedProvider != "copilot" && selectedProvider != "anthropic" { + // API key configuration - skip for copilot, anthropic and gemini + if selectedProvider != "copilot" && selectedProvider != "anthropic" && selectedProvider != "gemini" { apiKeyPrompt := &survey.Input{ Message: fmt.Sprintf("Enter API Key for %s:", selectedProvider), } @@ -173,12 +173,15 @@ func runInteractiveConfig() { } } else if selectedProvider == "anthropic" { fmt.Println("Anthropic provider uses Claude Code CLI - no API key needed.") + } else if selectedProvider == "gemini" { + fmt.Println("Gemini provider uses Gemini CLI - no API key needed.") } availableModels := map[string][]string{ "openai": {}, "copilot": {}, "anthropic": {}, + "gemini": {}, } modelDisplayToID := map[string]string{} @@ -213,6 +216,12 @@ func runInteractiveConfig() { availableModels["anthropic"] = append(availableModels["anthropic"], display) modelDisplayToID[display] = m.APIModel } + case "gemini": + for _, m := range models.GeminiModels { + display := fmt.Sprintf("%s (%s)", m.Name, m.APIModel) + availableModels["gemini"] = append(availableModels["gemini"], display) + modelDisplayToID[display] = m.APIModel + } } modelPrompt := &survey.Select{ @@ -223,7 +232,7 @@ func runInteractiveConfig() { // Try to set the default to the current model if possible isValidDefault := false currentDisplay := "" - if selectedProvider == "openai" || selectedProvider == "anthropic" || selectedProvider == "copilot" { + if selectedProvider == "openai" || selectedProvider == "anthropic" || selectedProvider == "copilot" || selectedProvider == "gemini" { for display, id := range modelDisplayToID { if id == currentModel || display == currentModel { isValidDefault = true @@ -252,7 +261,7 @@ func runInteractiveConfig() { } selectedModel := selectedDisplay - if selectedProvider == "openai" || selectedProvider == "anthropic" || selectedProvider == "copilot" { + if selectedProvider == "openai" || selectedProvider == "anthropic" || selectedProvider == "copilot" || selectedProvider == "gemini" { selectedModel = modelDisplayToID[selectedDisplay] } @@ -265,8 +274,8 @@ func runInteractiveConfig() { fmt.Printf("Model set to: %s\n", selectedModel) } - // Number of suggestions configuration for anthropic - if selectedProvider == "anthropic" { + // Number of suggestions configuration for anthropic and gemini + if selectedProvider == "anthropic" || selectedProvider == "gemini" { numSuggestionsPrompt := &survey.Input{ Message: "Number of commit message suggestions (default: 10):", Default: "10", @@ -290,8 +299,8 @@ func runInteractiveConfig() { // Get current endpoint currentEndpoint, _ := config.GetEndpoint() - // Endpoint configuration prompt - skip for anthropic since it uses CLI - if selectedProvider != "anthropic" { + // Endpoint configuration prompt - skip for anthropic and gemini since they use CLI + if selectedProvider != "anthropic" && selectedProvider != "gemini" { endpointPrompt := &survey.Input{ Message: "Enter custom endpoint URL (leave empty for default):", Default: currentEndpoint, diff --git a/cmd/pr.go b/cmd/pr.go index b8c1ec9..a75a9b9 100644 --- a/cmd/pr.go +++ b/cmd/pr.go @@ -87,6 +87,9 @@ var prCmd = &cobra.Command{ // Get num_suggestions from config numSuggestions := config.GetNumSuggestions() aiProvider = provider.NewAnthropicProvider(model, numSuggestions) + case "gemini": + numSuggestions := config.GetNumSuggestions() + aiProvider = provider.NewGeminiProvider(model, numSuggestions) default: // Default to copilot if provider is not set or unknown aiProvider = provider.NewCopilotProvider(apiKey, endpoint) diff --git a/internal/config/config.go b/internal/config/config.go index b892818..3f348ce 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -45,6 +45,8 @@ func InitConfig() { viper.SetDefault("providers.anthropic.model", "claude-haiku-4-5") viper.SetDefault("providers.anthropic.num_suggestions", 10) + viper.SetDefault("providers.gemini.model", "flash") + viper.SetDefault("providers.gemini.num_suggestions", 10) viper.AutomaticEnv() if err := viper.ReadInConfig(); err != nil { @@ -149,6 +151,8 @@ func GetEndpoint() (string, error) { return "https://api.githubcopilot.com", nil case "anthropic": return "", nil // Anthropic uses CLI, no endpoint needed + case "gemini": + return "", nil // Gemini uses CLI, no endpoint needed default: return "", fmt.Errorf("no default endpoint available for provider '%s'", cfg.ActiveProvider) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 5cc80e9..600a14f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -162,8 +162,8 @@ func TestGetEndpoint_CustomEndpoint(t *testing.T) { cfg.Providers = make(map[string]ProviderConfig) } cfg.Providers[testProvider] = ProviderConfig{ - APIKey: "test-key", - Model: "test-model", + APIKey: "test-key", + Model: "test-model", EndpointURL: customEndpoint, } @@ -221,12 +221,12 @@ func TestSetEndpoint_Validation(t *testing.T) { endpoint string valid bool }{ - {"", true}, // Empty should be valid (default) - {"https://api.openai.com/v1", true}, // Valid HTTPS URL - {"http://localhost:11434", true}, // Valid HTTP URL - {"ftp://invalid.com", false}, // Invalid protocol - {"not-a-url", false}, // Invalid format - {"https://", false}, // Missing host + {"", true}, // Empty should be valid (default) + {"https://api.openai.com/v1", true}, // Valid HTTPS URL + {"http://localhost:11434", true}, // Valid HTTP URL + {"ftp://invalid.com", false}, // Invalid protocol + {"not-a-url", false}, // Invalid format + {"https://", false}, // Missing host } for _, tc := range testCases { @@ -237,4 +237,4 @@ func TestSetEndpoint_Validation(t *testing.T) { t.Errorf("Expected invalid endpoint %s to fail, but it passed", tc.endpoint) } } -} \ No newline at end of file +} diff --git a/internal/provider/anthropic.go b/internal/provider/anthropic.go index 96810f7..affa1c9 100644 --- a/internal/provider/anthropic.go +++ b/internal/provider/anthropic.go @@ -54,79 +54,12 @@ func (a *AnthropicProvider) GenerateCommitMessages(ctx context.Context, diff str fullPrompt := fmt.Sprintf("%s\n\nUser request: %s\n\nIMPORTANT: Generate exactly %d commit messages, one per line. Do not include any other text, explanations, or formatting - just the commit messages.", systemMsg, userPrompt, a.numSuggestions) - // Execute claude CLI with haiku model - // Using -p flag for print mode and --model for model selection - // Pipe prompt via stdin to avoid Windows command line length limits (8191 chars) - cmd := exec.CommandContext(ctx, "claude", "--model", a.model, "-p", "-") - - stdin, err := cmd.StdinPipe() + output, err := a.runCLI(ctx, fullPrompt) if err != nil { - return nil, fmt.Errorf("error creating stdin pipe: %w", err) - } - - var outputBuf strings.Builder - cmd.Stdout = &outputBuf - cmd.Stderr = &outputBuf - - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("error starting claude CLI: %w", err) - } - - _, writeErr := stdin.Write([]byte(fullPrompt)) - stdin.Close() - - waitErr := cmd.Wait() - - if writeErr != nil { - return nil, fmt.Errorf("error writing to claude CLI stdin: %w", writeErr) - } - - if waitErr != nil { - return nil, fmt.Errorf("error executing claude CLI: %w\nOutput: %s", waitErr, outputBuf.String()) - } - - output := []byte(outputBuf.String()) - - // Parse the output - split by newlines and clean - content := string(output) - lines := strings.Split(content, "\n") - - var commitMessages []string - for _, line := range lines { - trimmed := strings.TrimSpace(line) - // Skip empty lines and lines that look like explanatory text - if trimmed == "" { - continue - } - // Skip lines that are clearly not commit messages (too long, contain certain patterns) - if len(trimmed) > 200 { - continue - } - // Skip markdown formatting or numbered lists - if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "*") { - // Try to extract the actual commit message - parts := strings.SplitN(trimmed, " ", 2) - if len(parts) == 2 { - trimmed = strings.TrimSpace(parts[1]) - } - } - // Remove numbered list formatting like "1. " or "1) " - if len(trimmed) > 3 { - if (trimmed[0] >= '0' && trimmed[0] <= '9') && (trimmed[1] == '.' || trimmed[1] == ')') { - trimmed = strings.TrimSpace(trimmed[2:]) - } - } - - if trimmed != "" { - commitMessages = append(commitMessages, trimmed) - } - - // Stop once we have enough messages - if len(commitMessages) >= a.numSuggestions { - break - } + return nil, err } + commitMessages := parseOutputLines(output, a.numSuggestions) if len(commitMessages) == 0 { return nil, fmt.Errorf("no valid commit messages generated from Claude output") } @@ -163,77 +96,50 @@ func (a *AnthropicProvider) GeneratePRTitles(ctx context.Context, diff string) ( fullPrompt := fmt.Sprintf("%s\n\nUser request: %s\n\nIMPORTANT: Generate exactly %d pull request titles, one per line. Do not include any other text, explanations, or formatting - just the PR titles.", systemMsg, userPrompt, a.numSuggestions) + output, err := a.runCLI(ctx, fullPrompt) + if err != nil { + return nil, err + } + + prTitles := parseOutputLines(output, a.numSuggestions) + if len(prTitles) == 0 { + return nil, fmt.Errorf("no valid PR titles generated from Claude output") + } + + return prTitles, nil +} + +// runCLI executes the claude CLI with the given prompt via stdin and returns stdout. +func (a *AnthropicProvider) runCLI(ctx context.Context, prompt string) (string, error) { + // Using -p flag for print mode and --model for model selection // Pipe prompt via stdin to avoid Windows command line length limits (8191 chars) cmd := exec.CommandContext(ctx, "claude", "--model", a.model, "-p", "-") stdin, err := cmd.StdinPipe() if err != nil { - return nil, fmt.Errorf("error creating stdin pipe: %w", err) + return "", fmt.Errorf("error creating stdin pipe: %w", err) } - var outputBuf strings.Builder - cmd.Stdout = &outputBuf - cmd.Stderr = &outputBuf + var stdoutBuf, stderrBuf strings.Builder + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("error starting claude CLI: %w", err) + return "", fmt.Errorf("error starting claude CLI: %w", err) } - _, writeErr := stdin.Write([]byte(fullPrompt)) + _, writeErr := stdin.Write([]byte(prompt)) stdin.Close() waitErr := cmd.Wait() if writeErr != nil { - return nil, fmt.Errorf("error writing to claude CLI stdin: %w", writeErr) + return "", fmt.Errorf("error writing to claude CLI stdin: %w", writeErr) } if waitErr != nil { - return nil, fmt.Errorf("error executing claude CLI: %w\nOutput: %s", waitErr, outputBuf.String()) - } - - output := []byte(outputBuf.String()) - - // Parse the output - same logic as commit message generation - content := string(output) - lines := strings.Split(content, "\n") - - var prTitles []string - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "" { - continue - } - if len(trimmed) > 200 { - continue - } - // Skip markdown formatting or numbered lists - if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "*") { - parts := strings.SplitN(trimmed, " ", 2) - if len(parts) == 2 { - trimmed = strings.TrimSpace(parts[1]) - } - } - // Remove numbered list formatting like "1. " or "1) " - if len(trimmed) > 3 { - if (trimmed[0] >= '0' && trimmed[0] <= '9') && (trimmed[1] == '.' || trimmed[1] == ')') { - trimmed = strings.TrimSpace(trimmed[2:]) - } - } - - if trimmed != "" { - prTitles = append(prTitles, trimmed) - } - - // Stop once we have enough titles - if len(prTitles) >= a.numSuggestions { - break - } - } - - if len(prTitles) == 0 { - return nil, fmt.Errorf("no valid PR titles generated from Claude output") + return "", fmt.Errorf("error executing claude CLI: %w\nStderr: %s", waitErr, stderrBuf.String()) } - return prTitles, nil + return stdoutBuf.String(), nil } diff --git a/internal/provider/common.go b/internal/provider/common.go index b87ed4b..334825b 100644 --- a/internal/provider/common.go +++ b/internal/provider/common.go @@ -4,10 +4,50 @@ import ( "context" "fmt" "strings" + "unicode" "github.com/openai/openai-go" ) +// parseOutputLines parses raw LLM output into clean lines, stripping markdown +// formatting, numbered/bulleted list prefixes, and skipping empty or overly long lines. +// It returns at most maxLines results. +func parseOutputLines(raw string, maxLines int) []string { + lines := strings.Split(raw, "\n") + + var result []string + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" || len(trimmed) > 200 { + continue + } + // Strip markdown heading, bullet, or asterisk prefix + if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "*") { + parts := strings.SplitN(trimmed, " ", 2) + if len(parts) == 2 { + trimmed = strings.TrimSpace(parts[1]) + } + } + // Strip numbered list prefix like "1. ", "10) ", "3. " + if len(trimmed) > 0 && trimmed[0] >= '0' && trimmed[0] <= '9' { + i := 0 + for i < len(trimmed) && unicode.IsDigit(rune(trimmed[i])) { + i++ + } + if i < len(trimmed) && (trimmed[i] == '.' || trimmed[i] == ')') { + trimmed = strings.TrimSpace(trimmed[i+1:]) + } + } + if trimmed != "" { + result = append(result, trimmed) + } + if len(result) >= maxLines { + break + } + } + return result +} + // commonProvider holds the common fields and methods for OpenAI-compatible providers. type commonProvider struct { client *openai.Client diff --git a/internal/provider/common_test.go b/internal/provider/common_test.go new file mode 100644 index 0000000..1232bc9 --- /dev/null +++ b/internal/provider/common_test.go @@ -0,0 +1,108 @@ +package provider + +import ( + "strings" + "testing" +) + +func TestParseOutputLines(t *testing.T) { + tests := []struct { + name string + input string + maxLines int + expected []string + }{ + { + name: "plain lines", + input: "feat: add login\nfix: typo in readme\nchore: update deps", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo in readme", "chore: update deps"}, + }, + { + name: "single-digit numbered list with dot", + input: "1. feat: add login\n2. fix: typo\n3. chore: deps", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo", "chore: deps"}, + }, + { + name: "single-digit numbered list with paren", + input: "1) feat: add login\n2) fix: typo", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo"}, + }, + { + name: "multi-digit numbered list", + input: "10. feat: add login\n11. fix: typo\n12. chore: deps", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo", "chore: deps"}, + }, + { + name: "markdown bullet dashes", + input: "- feat: add login\n- fix: typo", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo"}, + }, + { + name: "markdown bullet asterisks", + input: "* feat: add login\n* fix: typo", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo"}, + }, + { + name: "markdown headings stripped", + input: "# feat: add login\n## fix: typo", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo"}, + }, + { + name: "empty lines skipped", + input: "feat: add login\n\n\nfix: typo\n\n", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo"}, + }, + { + name: "lines over 200 chars skipped", + input: "feat: add login\n" + strings.Repeat("x", 201) + "\nfix: typo", + maxLines: 10, + expected: []string{"feat: add login", "fix: typo"}, + }, + { + name: "respects maxLines limit", + input: "line1\nline2\nline3\nline4\nline5", + maxLines: 3, + expected: []string{"line1", "line2", "line3"}, + }, + { + name: "whitespace-only input", + input: " \n \n\t\n", + maxLines: 10, + expected: nil, + }, + { + name: "mixed formatting", + input: "1. feat: login\n- fix: typo\n* chore: deps\n## docs: readme\nplain message", + maxLines: 10, + expected: []string{"feat: login", "fix: typo", "chore: deps", "docs: readme", "plain message"}, + }, + { + name: "number at start but no list separator", + input: "3rd attempt at fixing auth", + maxLines: 10, + expected: []string{"3rd attempt at fixing auth"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseOutputLines(tt.input, tt.maxLines) + if len(got) != len(tt.expected) { + t.Fatalf("got %d lines %v, want %d lines %v", len(got), got, len(tt.expected), tt.expected) + } + for i := range got { + if got[i] != tt.expected[i] { + t.Errorf("line %d: got %q, want %q", i, got[i], tt.expected[i]) + } + } + }) + } +} diff --git a/internal/provider/gemini.go b/internal/provider/gemini.go new file mode 100644 index 0000000..12cddab --- /dev/null +++ b/internal/provider/gemini.go @@ -0,0 +1,144 @@ +package provider + +import ( + "context" + "fmt" + "os/exec" + "strings" +) + +type GeminiProvider struct { + model string + numSuggestions int +} + +func NewGeminiProvider(model string, numSuggestions int) *GeminiProvider { + if model == "" { + model = "flash" + } + if numSuggestions <= 0 { + numSuggestions = 10 + } + return &GeminiProvider{ + model: model, + numSuggestions: numSuggestions, + } +} + +func (g *GeminiProvider) GenerateCommitMessage(ctx context.Context, diff string) (string, error) { + msgs, err := g.GenerateCommitMessages(ctx, diff) + if err != nil { + return "", err + } + if len(msgs) == 0 { + return "", fmt.Errorf("no commit messages generated") + } + return msgs[0], nil +} + +func (g *GeminiProvider) GenerateCommitMessages(ctx context.Context, diff string) ([]string, error) { + if strings.TrimSpace(diff) == "" { + return nil, fmt.Errorf("no diff provided") + } + + // Check if gemini CLI is available + if _, err := exec.LookPath("gemini"); err != nil { + return nil, fmt.Errorf("gemini CLI not found in PATH. Please install Gemini CLI: %w", err) + } + + // Build the prompt + systemMsg := GetSystemMessage() + userPrompt := GetCommitMessagePrompt(diff) + + // Modify the prompt to request specific number of suggestions + fullPrompt := fmt.Sprintf("%s\n\nUser request: %s\n\nIMPORTANT: Generate exactly %d commit messages, one per line. Do not include any other text, explanations, or formatting - just the commit messages.", + systemMsg, userPrompt, g.numSuggestions) + + output, err := g.runCLI(ctx, fullPrompt) + if err != nil { + return nil, err + } + + commitMessages := parseOutputLines(output, g.numSuggestions) + if len(commitMessages) == 0 { + return nil, fmt.Errorf("no valid commit messages generated from Gemini output") + } + + return commitMessages, nil +} + +func (g *GeminiProvider) GeneratePRTitle(ctx context.Context, diff string) (string, error) { + titles, err := g.GeneratePRTitles(ctx, diff) + if err != nil { + return "", err + } + if len(titles) == 0 { + return "", fmt.Errorf("no PR titles generated") + } + return titles[0], nil +} + +func (g *GeminiProvider) GeneratePRTitles(ctx context.Context, diff string) ([]string, error) { + if strings.TrimSpace(diff) == "" { + return nil, fmt.Errorf("no diff provided") + } + + // Check if gemini CLI is available + if _, err := exec.LookPath("gemini"); err != nil { + return nil, fmt.Errorf("gemini CLI not found in PATH. Please install Gemini CLI: %w", err) + } + + // Build the prompt using PR title template + systemMsg := GetSystemMessage() + userPrompt := GetPRTitlePrompt(diff) + + // Modify the prompt to request specific number of suggestions + fullPrompt := fmt.Sprintf("%s\n\nUser request: %s\n\nIMPORTANT: Generate exactly %d pull request titles, one per line. Do not include any other text, explanations, or formatting - just the PR titles.", + systemMsg, userPrompt, g.numSuggestions) + + output, err := g.runCLI(ctx, fullPrompt) + if err != nil { + return nil, err + } + + prTitles := parseOutputLines(output, g.numSuggestions) + if len(prTitles) == 0 { + return nil, fmt.Errorf("no valid PR titles generated from Gemini output") + } + + return prTitles, nil +} + +// runCLI executes the gemini CLI with the given prompt via stdin and returns stdout. +func (g *GeminiProvider) runCLI(ctx context.Context, prompt string) (string, error) { + // Piping into gemini triggers Headless mode. + cmd := exec.CommandContext(ctx, "gemini", "--model", g.model) + + stdin, err := cmd.StdinPipe() + if err != nil { + return "", fmt.Errorf("error creating stdin pipe: %w", err) + } + + var stdoutBuf, stderrBuf strings.Builder + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + + if err := cmd.Start(); err != nil { + return "", fmt.Errorf("error starting gemini CLI: %w", err) + } + + _, writeErr := stdin.Write([]byte(prompt)) + stdin.Close() + + waitErr := cmd.Wait() + + if writeErr != nil { + return "", fmt.Errorf("error writing to gemini CLI stdin: %w", writeErr) + } + + if waitErr != nil { + return "", fmt.Errorf("error executing gemini CLI: %w\nStderr: %s", waitErr, stderrBuf.String()) + } + + return stdoutBuf.String(), nil +} diff --git a/internal/provider/models/models.go b/internal/provider/models/models.go index 86140f6..60b783a 100644 --- a/internal/provider/models/models.go +++ b/internal/provider/models/models.go @@ -25,6 +25,7 @@ type Model struct { const ( ProviderOpenAI ModelProvider = "openai" ProviderAnthropic ModelProvider = "anthropic" + ProviderGemini ModelProvider = "gemini" GPT41 ModelID = "gpt-4.1" GPT41Mini ModelID = "gpt-4.1-mini" @@ -40,6 +41,13 @@ const ( O4Mini ModelID = "o4-mini" ClaudeHaiku45 ModelID = "claude-haiku-4-5" + + GeminiAuto ModelID = "auto" + GeminiPro ModelID = "pro" + GeminiFlash ModelID = "flash" + GeminiFlashLite ModelID = "flash-lite" + Gemini25Pro ModelID = "gemini-2.5-pro" + Gemini25Flash ModelID = "gemini-2.5-flash" ) var OpenAIModels = map[ModelID]Model{ @@ -220,3 +228,56 @@ var AnthropicModels = map[ModelID]Model{ SupportsAttachments: true, }, } + +var GeminiModels = map[ModelID]Model{ + GeminiAuto: { + ID: GeminiAuto, + Name: "Gemini Auto", + Provider: ProviderGemini, + APIModel: "auto", + ContextWindow: 1_000_000, + SupportsAttachments: true, + }, + GeminiPro: { + ID: GeminiPro, + Name: "Gemini Pro", + Provider: ProviderGemini, + APIModel: "pro", + ContextWindow: 2_000_000, + CanReason: true, + SupportsAttachments: true, + }, + GeminiFlash: { + ID: GeminiFlash, + Name: "Gemini Flash", + Provider: ProviderGemini, + APIModel: "flash", + ContextWindow: 1_000_000, + SupportsAttachments: true, + }, + GeminiFlashLite: { + ID: GeminiFlashLite, + Name: "Gemini Flash Lite", + Provider: ProviderGemini, + APIModel: "flash-lite", + ContextWindow: 1_000_000, + SupportsAttachments: true, + }, + Gemini25Pro: { + ID: Gemini25Pro, + Name: "Gemini 2.5 Pro", + Provider: ProviderGemini, + APIModel: "gemini-2.5-pro", + ContextWindow: 2_000_000, + CanReason: true, + SupportsAttachments: true, + }, + Gemini25Flash: { + ID: Gemini25Flash, + Name: "Gemini 2.5 Flash", + Provider: ProviderGemini, + APIModel: "gemini-2.5-flash", + ContextWindow: 1_000_000, + SupportsAttachments: true, + }, +}