fix(gemini): resolve 400 stream errors and improve client compatibility
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

- Filter out empty text parts in Gemini requests to avoid 400 errors.
- Inject 'assistant' role into the first streaming chunk for better compatibility with clients like opencode.
- Fallback to tool_call_id for Gemini function responses when name is missing.
This commit is contained in:
2026-03-05 15:16:19 +00:00
parent b0bd1fd143
commit a022bd1272
2 changed files with 28 additions and 15 deletions

View File

@@ -200,12 +200,14 @@ impl GeminiProvider {
if msg.role == "system" {
for part in msg.content {
if let ContentPart::Text { text } = part {
system_parts.push(GeminiPart {
text: Some(text),
inline_data: None,
function_call: None,
function_response: None,
});
if !text.trim().is_empty() {
system_parts.push(GeminiPart {
text: Some(text),
inline_data: None,
function_call: None,
function_response: None,
});
}
}
}
continue;
@@ -230,7 +232,8 @@ impl GeminiProvider {
})
.unwrap_or_default();
let name = msg.name.clone().unwrap_or_default();
// Gemini function response MUST have a name. Fallback to tool_call_id if name is missing.
let name = msg.name.clone().or_else(|| msg.tool_call_id.clone()).unwrap_or_else(|| "unknown_function".to_string());
let response_value = serde_json::from_str::<Value>(&text_content)
.unwrap_or_else(|_| serde_json::json!({ "result": text_content }));
@@ -249,7 +252,7 @@ impl GeminiProvider {
// Include text content if present
for p in &msg.content {
if let ContentPart::Text { text } = p {
if !text.is_empty() {
if !text.trim().is_empty() {
parts.push(GeminiPart {
text: Some(text.clone()),
inline_data: None,
@@ -279,12 +282,14 @@ impl GeminiProvider {
for part in msg.content {
match part {
ContentPart::Text { text } => {
parts.push(GeminiPart {
text: Some(text),
inline_data: None,
function_call: None,
function_response: None,
});
if !text.trim().is_empty() {
parts.push(GeminiPart {
text: Some(text),
inline_data: None,
function_call: None,
function_response: None,
});
}
}
ContentPart::Image(image_input) => {
let (base64_data, mime_type) = image_input