diff --git a/config/livereview.toml.example b/config/livereview.toml.example index 1d651794..6cb09625 100644 --- a/config/livereview.toml.example +++ b/config/livereview.toml.example @@ -44,9 +44,9 @@ url = "https://gitea.example.com" [ai.langchain] # Model name (varies by provider) # Ollama: "llama3.2", "codellama", "mistral", etc. -# Google: "gemini-2.0-flash", "gemini-2.5-pro", etc. +# Google: "gemini-3.8-flash", "gemini-3.5-pro", etc. # OpenAI: "gpt-4", "gpt-3.5-turbo", etc. -model = "gemini-2.0-flash" +model = "gemini-3.8-flash" # Provider type: "ollama", "google", "openai", "anthropic" # provider_type = "ollama" @@ -55,9 +55,6 @@ model = "gemini-2.0-flash" # Default: http://localhost:11434 # base_url = "http://localhost:11434" -# Temperature for generation (0.0 = deterministic, 1.0 = creative) -temperature = 0.2 - # Maximum tokens per batch for processing max_tokens_per_batch = 8000 diff --git a/docs/gemini_3.8_migration.md b/docs/gemini_3.8_migration.md new file mode 100644 index 00000000..eef85947 --- /dev/null +++ b/docs/gemini_3.8_migration.md @@ -0,0 +1,36 @@ +# Gemini 3.8 Flash Migration & Parameter Deprecation Fixes + +**Date**: October 10, 2026 + +## Overview +Google AI Studio deprecated the `gemini-3.5-flash` model, automatically routing to `3.6-flash`, while announcing the general availability of `gemini-3.8-flash`. Furthermore, the Gemini 3.x models officially deprecated all sampling parameters (`temperature`, `top_p`, `top_k`) and the `thinking_budget` variable. Sending these parameters now strictly returns a `400 INVALID_ARGUMENT` error. + +This document serves as the permanent record of the codebase updates applied to ensure LiveReview remains 100% compliant with the Gemini 3.8 API rules without crashing. + +## Changes Implemented + +### 1. `langchaingo` Integration Sanitization +**File**: `internal/ai/langchain/provider.go` +- **Issue**: The underlying `github.com/tmc/langchaingo/llms/googleai` SDK sets a default temperature of `0.5`, and LiveReview was actively passing an `effectiveTemperature` payload downstream. Additionally, a linter flagged a long string of boolean `OR` comparisons for provider checking as a maintainability risk. +- **Fix**: Created a localized `buildCallOptions()` helper function mimicking the behavior of `internal/aiconnectors/connector.go`. This dynamically intercepts calls to `GenerateFromSinglePrompt` and `AggregateAndCombineOutputs`. If the active provider is `gemini`, `googleai`, `gemini-enterprise`, or `vertex` (now cleanly verified using a `switch` statement), it entirely skips attaching the `llms.WithTemperature()` option to the request, protecting the payload. + +### 2. PreLivi Python Script Fix +**File**: `scripts/prelivi/interpretation.py` +- **Issue**: The direct REST call to `generativelanguage.googleapis.com` had a hardcoded `"temperature": 0.2` injected into the `generationConfig` block. +- **Fix**: Deleted the `temperature` key entirely. The request now solely depends on `maxOutputTokens`. + +### 3. Native Gemini Connector +**File**: `internal/ai/gemini/gemini.go` +- **Issue**: Historical references to `temperature` still existed in the struct logging (`p.Temperature`) and initialization fields, which caused build compilation errors when purged. +- **Fix**: Dropped the temperature assignment and `fmt.Fprintf(f, "Temperature: %f\n", p.Temperature)` logging. The `internal/ai/gemini` package compiles and runs without expecting sampling configs. + +### 4. Configuration Hygiene +**Files**: `config/livereview.toml.example`, `internal/config/config.go` +- **Issue**: Example TOML configs and the automatic config generator still pointed to `gemini-2.0-flash` and generated sample configurations with `temperature = 0.2`. +- **Fix**: Refactored the sample generator and TOML files to use `gemini-3.8-flash` as the standard, actively scrubbing out the temperature variables so new deployments don't try to submit invalid configs. + +### 5. Cleanup +- `internal/ai/gemini/gemini.go.bak` and `internal/ai/gemini/.gemini.go.broken` were removed as they were dead-tracked code still reflecting the old temperature logic. + +## Note on Database Migrations +We deliberately opted **not** to force a database-level SQL migration over the `ai_connectors` table for existing users configured to `3.5-flash` or `3.6-flash`. Because Google handles routing on their backend seamlessly, active user settings have been preserved as-is. diff --git a/go.mod b/go.mod index cf626a11..67c5a2a6 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( github.com/slack-go/slack v0.27.0 github.com/stephenafamo/goldmark-pdf v0.4.2 github.com/stretchr/testify v1.12.1 - github.com/tmc/langchaingo v0.1.14 + github.com/tmc/langchaingo v0.1.15 github.com/wasilibs/go-pgquery v0.0.0-20260728010200-155ebad2880e github.com/xuri/excelize/v2 v2.11.1-0.20261009041834-958cda5363be github.com/yalue/onnxruntime_go v1.32.1 diff --git a/go.sum b/go.sum index 70e7eed5..63cf8587 100644 --- a/go.sum +++ b/go.sum @@ -571,8 +571,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= -github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc= -github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= +github.com/tmc/langchaingo v0.1.15 h1:zbRfW3RiX8SSX9ck0P1y3Q7FV6VRwusRSKFykaZOh/4= +github.com/tmc/langchaingo v0.1.15/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= diff --git a/internal/ai/gemini/gemini.go b/internal/ai/gemini/gemini.go index 070cbb6a..843f2908 100644 --- a/internal/ai/gemini/gemini.go +++ b/internal/ai/gemini/gemini.go @@ -21,7 +21,7 @@ import ( ) // DefaultModel is the Gemini model used when no model is configured. -const DefaultModel = "gemini-3.6-flash" +const DefaultModel = "gemini-3.8-flash" type GeminiProvider struct { TestableFields @@ -32,7 +32,6 @@ type GeminiProvider struct { type GeminiConfig struct { APIKey string `koanf:"api_key"` Model string `koanf:"model"` - Temperature float64 `koanf:"temperature"` MaxTokensPerBatch int `koanf:"max_tokens_per_batch"` } @@ -40,7 +39,6 @@ type GeminiConfig struct { type TestableFields struct { APIKey string Model string - Temperature float64 MaxTokensPerBatch int HTTPClient *http.Client } @@ -58,10 +56,6 @@ func New(config GeminiConfig) (*GeminiProvider, error) { config.Model = DefaultModel } - if config.Temperature == 0 { - config.Temperature = 0.4 // Increased from 0.2 to encourage more thorough analysis - } - if config.MaxTokensPerBatch == 0 { config.MaxTokensPerBatch = 10000 } @@ -72,7 +66,6 @@ func New(config GeminiConfig) (*GeminiProvider, error) { TestableFields: TestableFields{ APIKey: config.APIKey, Model: config.Model, - Temperature: config.Temperature, MaxTokensPerBatch: config.MaxTokensPerBatch, HTTPClient: &http.Client{}, }, @@ -106,10 +99,7 @@ type safetySettings struct { } type generationConfig struct { - Temperature float64 `json:"temperature"` - MaxOutputTokens int `json:"maxOutputTokens"` - TopK int `json:"topK"` - TopP float64 `json:"topP"` + MaxOutputTokens int `json:"maxOutputTokens"` } // reviewResponse represents a response from the Gemini API @@ -201,10 +191,7 @@ func (p *GeminiProvider) callGeminiAPI(ctx context.Context, prompt string) (stri }, }, GenerationConfig: generationConfig{ - Temperature: p.Temperature, MaxOutputTokens: 8192, // Increased from 4096 to allow for more detailed reviews - TopK: 40, - TopP: 0.95, }, } @@ -1439,10 +1426,6 @@ func (p *GeminiProvider) Configure(config map[string]interface{}) error { p.TestableFields.Model = model } - if temp, ok := config["temperature"].(float64); ok && temp > 0 { - p.TestableFields.Temperature = temp - } - if maxTokens, ok := config["max_tokens_per_batch"].(int); ok && maxTokens > 0 { p.TestableFields.MaxTokensPerBatch = maxTokens } else if maxTokens, ok := config["max_tokens_per_batch"].(float64); ok && maxTokens > 0 { @@ -1483,7 +1466,6 @@ func (p *GeminiProvider) logRequestAndResponse(prompt string, reqJSON []byte, re fmt.Fprintf(f, "=== GEMINI API REQUEST LOG ===\n") fmt.Fprintf(f, "Timestamp: %s\n", fmt.Sprintf("%d", os.Getpid())) fmt.Fprintf(f, "Model: %s\n", p.Model) - fmt.Fprintf(f, "Temperature: %f\n", p.Temperature) fmt.Fprintf(f, "\n--- PROMPT ---\n") fmt.Fprintf(f, "%s\n", prompt) fmt.Fprintf(f, "\n--- REQUEST JSON ---\n") diff --git a/internal/ai/langchain/provider.go b/internal/ai/langchain/provider.go index ab16f057..3327c33f 100644 --- a/internal/ai/langchain/provider.go +++ b/internal/ai/langchain/provider.go @@ -300,6 +300,23 @@ func (p *LangchainProvider) effectiveTemperature() float64 { return normalizeTemperature(p.temperature, p.temperatureSet) } +// buildCallOptions creates a slice of CallOption with the provider's default model configuration. +func (p *LangchainProvider) buildCallOptions(extra ...llms.CallOption) []llms.CallOption { + var opts []llms.CallOption + + isGemini := false + switch strings.ToLower(p.providerType) { + case "gemini", "googleai", "gemini-enterprise", "vertex": + isGemini = true + } + + if !isGemini { + opts = append(opts, llms.WithTemperature(p.effectiveTemperature())) + } + opts = append(opts, extra...) + return opts +} + func isOpenAISeriesModel(model string) bool { model = strings.TrimSpace(strings.ToLower(model)) if len(model) < 2 || model[0] != 'o' { @@ -1151,7 +1168,7 @@ func (p *LangchainProvider) reviewCodeBatchFormatted(ctx context.Context, diffs timeoutCtx, p.llm, prompt, - llms.WithTemperature(effectiveTemp), + p.buildCallOptions()..., ) if callErr == nil { responseBuilder.WriteString(out) @@ -1208,8 +1225,7 @@ func (p *LangchainProvider) reviewCodeBatchFormatted(ctx context.Context, diffs timeoutCtx, p.llm, prompt, - llms.WithTemperature(effectiveTemp), - llms.WithStreamingFunc(streamingFunc), + p.buildCallOptions(llms.WithStreamingFunc(streamingFunc))..., ) fmt.Printf("[STREAM DEBUG] GenerateFromSinglePrompt returned, err=%v\n", err) } @@ -1270,7 +1286,7 @@ func (p *LangchainProvider) reviewCodeBatchFormatted(ctx context.Context, diffs fbCtx, p.llm, prompt, - llms.WithTemperature(effectiveTemp), + p.buildCallOptions()..., ) close(waitingDone) @@ -1471,7 +1487,7 @@ func (p *LangchainProvider) ReviewCodeWithBatching(ctx context.Context, diffs [] ctx, p.llm, successfulBatches, - llms.WithTemperature(p.effectiveTemperature()), + p.buildCallOptions()..., ) if err != nil { return nil, fmt.Errorf("failed to aggregate results from %d successful batches: %w", len(successfulBatches), err) diff --git a/internal/aiconnectors/connector.go b/internal/aiconnectors/connector.go index ef6ff5d9..23c299f1 100644 --- a/internal/aiconnectors/connector.go +++ b/internal/aiconnectors/connector.go @@ -596,19 +596,26 @@ func (t *atlasLoggingTransport) RoundTrip(req *http.Request) (*http.Response, er // buildCallOptions creates a slice of llms.CallOption with the connector's // default model configuration, appending any extra options. func (c *Connector) buildCallOptions(options ...llms.CallOption) []llms.CallOption { - callOptions := []llms.CallOption{ - llms.WithTemperature(c.options.ModelConfig.Temperature), + var callOptions []llms.CallOption + + // Google AI Studio deprecated sampling parameters for Gemini 3.x models. + // Sending them will soon return a 400 INVALID_ARGUMENT error. + isGemini := c.provider == ProviderGemini || c.provider == ProviderGeminiEnterprise + + if !isGemini { + callOptions = append(callOptions, llms.WithTemperature(c.options.ModelConfig.Temperature)) } + if c.options.ModelConfig.Model != "" { callOptions = append(callOptions, llms.WithModel(c.options.ModelConfig.Model)) } if c.options.ModelConfig.MaxTokens > 0 { callOptions = append(callOptions, llms.WithMaxTokens(c.options.ModelConfig.MaxTokens)) } - if c.options.ModelConfig.TopP > 0 { + if c.options.ModelConfig.TopP > 0 && !isGemini { callOptions = append(callOptions, llms.WithTopP(c.options.ModelConfig.TopP)) } - if c.options.ModelConfig.TopK > 0 { + if c.options.ModelConfig.TopK > 0 && !isGemini { callOptions = append(callOptions, llms.WithTopK(int(c.options.ModelConfig.TopK))) } callOptions = append(callOptions, options...) diff --git a/internal/aiconnectors/models_sync.go b/internal/aiconnectors/models_sync.go index 165dd959..ac358ec2 100644 --- a/internal/aiconnectors/models_sync.go +++ b/internal/aiconnectors/models_sync.go @@ -66,7 +66,7 @@ var defaultProviderModels = map[string]string{ "gemini": gemini.DefaultModel, "deepseek": "deepseek-v4-flash", "cohere": "command-a", - "openrouter": "deepseek/deepseek-v4-flash", + "openrouter": "deepseek/deepseek-pro-v4", "atlas": "deepseek-ai/deepseek-v4-flash", } diff --git a/internal/config/config.go b/internal/config/config.go index da145a7a..67cd6c22 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -88,7 +88,6 @@ token = "your-gitlab-token" [ai.gemini] api_key = "your-gemini-api-key" model = "%s" -temperature = 0.2 `, gemini.DefaultModel) return os.WriteFile(configPath, []byte(sampleConfig), 0644) diff --git a/scripts/prelivi/interpretation.py b/scripts/prelivi/interpretation.py index 34ef517d..ec0a7890 100644 --- a/scripts/prelivi/interpretation.py +++ b/scripts/prelivi/interpretation.py @@ -200,7 +200,6 @@ def call_gemini(api_keys: list[str], user_message: str) -> tuple[dict, dict]: "system_instruction": {"parts": [{"text": SYSTEM_PROMPT}]}, "contents": [{"role": "user", "parts": [{"text": user_message}]}], "generationConfig": { - "temperature": 0.2, "maxOutputTokens": 8192, "responseMimeType": "application/json" } diff --git a/ui/src/pages/AIProviders/AIProviders.tsx b/ui/src/pages/AIProviders/AIProviders.tsx index 9e353cb1..d039b4a9 100644 --- a/ui/src/pages/AIProviders/AIProviders.tsx +++ b/ui/src/pages/AIProviders/AIProviders.tsx @@ -48,7 +48,8 @@ const providerUIMetadata: AIProvider[] = [ url: 'https://ai.google.dev/', description: 'High quality, multimodal reasoning with balanced cost and performance.', icon: , - apiKeyPlaceholder: 'gemini-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + apiKeyPlaceholder: 'gemini-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + defaultModel: 'gemini-3.8-flash' }, { id: 'gemini-enterprise', @@ -56,7 +57,8 @@ const providerUIMetadata: AIProvider[] = [ url: 'https://cloud.google.com/vertex-ai', description: 'Enterprise-grade LLM services via GCP Vertex AI with IAM authentication.', icon: , - apiKeyPlaceholder: 'Paste Service Account JSON content here' + apiKeyPlaceholder: 'Paste Service Account JSON content here', + defaultModel: 'gemini-3.8-flash' }, { id: 'deepseek', @@ -71,7 +73,7 @@ const providerUIMetadata: AIProvider[] = [ id: 'openrouter', name: 'OpenRouter', url: 'https://openrouter.ai/', - description: 'Bring your own key and choose any OpenRouter model. Defaults to the free DeepSeek route.', + description: 'Bring your own key and choose any OpenRouter model.', icon: , apiKeyPlaceholder: 'sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', baseURLPlaceholder: 'https://openrouter.ai/api/v1' diff --git a/ui/src/pages/AIProviders/components/ConnectorForm.tsx b/ui/src/pages/AIProviders/components/ConnectorForm.tsx index 1fe42b40..9e20883b 100644 --- a/ui/src/pages/AIProviders/components/ConnectorForm.tsx +++ b/ui/src/pages/AIProviders/components/ConnectorForm.tsx @@ -244,6 +244,8 @@ const ConnectorForm: React.FC = ({ const usesCustomModel = !!currentModelValue && !providerModels.includes(currentModelValue); const shouldShowCustomModelInput = customModelMode || usesCustomModel; + // Models come straight from the provider API catalog (dynamic), filtered only by + // the search box. No client-side pruning: whatever the backend returns is shown. const filteredModels = React.useMemo(() => { if (!searchQuery) return providerModels; return providerModels.filter((model: string) => @@ -522,7 +524,7 @@ const ConnectorForm: React.FC = ({ /> )}

- {providerDetails.id === 'openrouter' ? 'OpenRouter model ID (defaults to free DeepSeek route)' : providerDetails.id === 'atlas' ? 'Atlas Cloud model ID (defaults to DeepSeek-V3)' : 'Select a model or enter a custom model ID'} + {providerDetails.id === 'openrouter' ? 'OpenRouter model ID' : providerDetails.id === 'atlas' ? 'Atlas Cloud model ID (defaults to DeepSeek-V3)' : 'Select a model or enter a custom model ID'}

)}