From be9fdd9a52d9aa8f2f0f63c0691a4303c3f3be42 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Thu, 5 Mar 2026 15:24:47 +0000 Subject: [PATCH] fix(gemini): implement dynamic API versioning and support Gemini 3 - Switch to v1beta endpoint for 'preview' and 'thinking' models. - Update model version checks to include gemini-3 as a known version. - Use get_base_url helper to construct dynamic URLs for both streaming and non-streaming requests. --- src/providers/gemini.rs | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/providers/gemini.rs b/src/providers/gemini.rs index b61ceb2c..64ca8e1b 100644 --- a/src/providers/gemini.rs +++ b/src/providers/gemini.rs @@ -439,6 +439,17 @@ impl GeminiProvider { } } + /// Determine the appropriate base URL for the model. + /// "preview" models often require the v1beta endpoint. + fn get_base_url(&self, model: &str) -> String { + if model.contains("preview") || model.contains("thinking") { + self.config.base_url.replace("/v1", "/v1beta") + } else { + self.config.base_url.clone() + } + } +} + #[async_trait] impl super::Provider for GeminiProvider { fn name(&self) -> &str { @@ -456,10 +467,16 @@ impl super::Provider for GeminiProvider { async fn chat_completion(&self, request: UnifiedRequest) -> Result { let mut model = request.model.clone(); - // Normalize model name: If it's a known Gemini model, use it; - // otherwise, if it starts with gemini- but is unknown (e.g. gemini-3-flash-preview), + // Normalize model name: If it's a known Gemini model version, use it; + // otherwise, if it starts with gemini- but is an unknown legacy version, // fallback to the default model to avoid 400 errors. - if !model.starts_with("gemini-1.5") && !model.starts_with("gemini-2.0") && model.starts_with("gemini-") { + // We now allow gemini-3+ as valid versions. + let is_known_version = model.starts_with("gemini-1.5") || + model.starts_with("gemini-2.0") || + model.starts_with("gemini-2.5") || + model.starts_with("gemini-3"); + + if !is_known_version && model.starts_with("gemini-") { tracing::info!("Mapping unknown Gemini model {} to default {}", model, self.config.default_model); model = self.config.default_model.clone(); } @@ -475,6 +492,7 @@ impl super::Provider for GeminiProvider { let generation_config = if request.temperature.is_some() || request.max_tokens.is_some() { // Some Gemini models (especially 1.5) have lower max_output_tokens limits (e.g. 8192) // than what clients like opencode might request. Clamp to a safe maximum. + // Note: Gemini 2.0+ supports much higher limits, but 8192 is a safe universal floor. let max_tokens = request.max_tokens.map(|t| t.min(8192)); Some(GeminiGenerationConfig { @@ -493,7 +511,8 @@ impl super::Provider for GeminiProvider { tool_config, }; - let url = format!("{}/models/{}:generateContent", self.config.base_url, model); + let base_url = self.get_base_url(&model); + let url = format!("{}/models/{}:generateContent", base_url, model); let response = self .client @@ -595,7 +614,12 @@ impl super::Provider for GeminiProvider { let mut model = request.model.clone(); // Normalize model name: fallback to default if unknown Gemini model is requested - if !model.starts_with("gemini-1.5") && !model.starts_with("gemini-2.0") && model.starts_with("gemini-") { + let is_known_version = model.starts_with("gemini-1.5") || + model.starts_with("gemini-2.0") || + model.starts_with("gemini-2.5") || + model.starts_with("gemini-3"); + + if !is_known_version && model.starts_with("gemini-") { tracing::info!("Mapping unknown Gemini model {} to default {}", model, self.config.default_model); model = self.config.default_model.clone(); } @@ -629,9 +653,10 @@ impl super::Provider for GeminiProvider { tool_config, }; + let base_url = self.get_base_url(&model); let url = format!( "{}/models/{}:streamGenerateContent?alt=sse", - self.config.base_url, model, + base_url, model, ); // (no fallback_request needed here) @@ -646,7 +671,7 @@ impl super::Provider for GeminiProvider { // Prepare clones for HTTP fallback usage inside non-streaming paths. let http_client = self.client.clone(); let http_api_key = self.api_key.clone(); - let http_base = self.config.base_url.clone(); + let http_base = base_url.clone(); let gemini_request_clone = gemini_request.clone(); let es_result = reqwest_eventsource::EventSource::new(