fix(gemini): implement dynamic API versioning and support Gemini 3
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

- 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.
This commit is contained in:
2026-03-05 15:24:47 +00:00
parent e89658fd87
commit be9fdd9a52

View File

@@ -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] #[async_trait]
impl super::Provider for GeminiProvider { impl super::Provider for GeminiProvider {
fn name(&self) -> &str { fn name(&self) -> &str {
@@ -456,10 +467,16 @@ impl super::Provider for GeminiProvider {
async fn chat_completion(&self, request: UnifiedRequest) -> Result<ProviderResponse, AppError> { async fn chat_completion(&self, request: UnifiedRequest) -> Result<ProviderResponse, AppError> {
let mut model = request.model.clone(); let mut model = request.model.clone();
// Normalize model name: If it's a known Gemini model, use it; // Normalize model name: If it's a known Gemini model version, use it;
// otherwise, if it starts with gemini- but is unknown (e.g. gemini-3-flash-preview), // otherwise, if it starts with gemini- but is an unknown legacy version,
// fallback to the default model to avoid 400 errors. // 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); tracing::info!("Mapping unknown Gemini model {} to default {}", model, self.config.default_model);
model = self.config.default_model.clone(); 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() { 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) // 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. // 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)); let max_tokens = request.max_tokens.map(|t| t.min(8192));
Some(GeminiGenerationConfig { Some(GeminiGenerationConfig {
@@ -493,7 +511,8 @@ impl super::Provider for GeminiProvider {
tool_config, 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 let response = self
.client .client
@@ -595,7 +614,12 @@ impl super::Provider for GeminiProvider {
let mut model = request.model.clone(); let mut model = request.model.clone();
// Normalize model name: fallback to default if unknown Gemini model is requested // 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); tracing::info!("Mapping unknown Gemini model {} to default {}", model, self.config.default_model);
model = self.config.default_model.clone(); model = self.config.default_model.clone();
} }
@@ -629,9 +653,10 @@ impl super::Provider for GeminiProvider {
tool_config, tool_config,
}; };
let base_url = self.get_base_url(&model);
let url = format!( let url = format!(
"{}/models/{}:streamGenerateContent?alt=sse", "{}/models/{}:streamGenerateContent?alt=sse",
self.config.base_url, model, base_url, model,
); );
// (no fallback_request needed here) // (no fallback_request needed here)
@@ -646,7 +671,7 @@ impl super::Provider for GeminiProvider {
// Prepare clones for HTTP fallback usage inside non-streaming paths. // Prepare clones for HTTP fallback usage inside non-streaming paths.
let http_client = self.client.clone(); let http_client = self.client.clone();
let http_api_key = self.api_key.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 gemini_request_clone = gemini_request.clone();
let es_result = reqwest_eventsource::EventSource::new( let es_result = reqwest_eventsource::EventSource::new(