mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-04-25 17:25:11 +02:00
Merge pull request #1011 from nldhuyen0047/fix/gemini-function-call-turn-ordering
fix: ensure message history starts with a user turn for Gemini compatibility
This commit is contained in:
@@ -349,6 +349,10 @@ pub async fn run_agent_loop(
|
||||
// pair across the cut boundary, leaving orphaned blocks that cause the LLM
|
||||
// to return empty responses (input_tokens=0).
|
||||
messages = crate::session_repair::validate_and_repair(&messages);
|
||||
// Ensure history starts with a user turn: trimming may have left an
|
||||
// assistant turn at position 0, which strict providers (e.g. Gemini)
|
||||
// reject with INVALID_ARGUMENT on function-call turns.
|
||||
messages = crate::session_repair::ensure_starts_with_user(messages);
|
||||
}
|
||||
|
||||
// Use autonomous config max_iterations if set, else default
|
||||
@@ -388,6 +392,8 @@ pub async fn run_agent_loop(
|
||||
// which may have broken assistant→tool ordering invariants.
|
||||
if recovery != RecoveryStage::None {
|
||||
messages = crate::session_repair::validate_and_repair(&messages);
|
||||
// Ensure history starts with a user turn after overflow recovery.
|
||||
messages = crate::session_repair::ensure_starts_with_user(messages);
|
||||
}
|
||||
|
||||
// Context guard: compact oversized tool results before LLM call
|
||||
@@ -1512,6 +1518,10 @@ pub async fn run_agent_loop_streaming(
|
||||
// pair across the cut boundary, leaving orphaned blocks that cause the LLM
|
||||
// to return empty responses (input_tokens=0).
|
||||
messages = crate::session_repair::validate_and_repair(&messages);
|
||||
// Ensure history starts with a user turn: trimming may have left an
|
||||
// assistant turn at position 0, which strict providers (e.g. Gemini)
|
||||
// reject with INVALID_ARGUMENT on function-call turns.
|
||||
messages = crate::session_repair::ensure_starts_with_user(messages);
|
||||
}
|
||||
|
||||
// Use autonomous config max_iterations if set, else default
|
||||
@@ -1569,6 +1579,8 @@ pub async fn run_agent_loop_streaming(
|
||||
// be followed by tool messages" errors after context overflow recovery.)
|
||||
if recovery != RecoveryStage::None {
|
||||
messages = crate::session_repair::validate_and_repair(&messages);
|
||||
// Ensure history starts with a user turn after overflow recovery.
|
||||
messages = crate::session_repair::ensure_starts_with_user(messages);
|
||||
}
|
||||
|
||||
// Context guard: compact oversized tool results before LLM call
|
||||
|
||||
@@ -370,7 +370,11 @@ fn sanitize_gemini_turns(contents: Vec<GeminiContent>) -> Vec<GeminiContent> {
|
||||
}
|
||||
|
||||
// Step 2: Drop orphaned functionCall parts from model turns.
|
||||
// A model turn with functionCall must be followed by a user turn with functionResponse.
|
||||
// A model turn with functionCall must be:
|
||||
// (a) followed by a user turn with functionResponse, AND
|
||||
// (b) preceded by a user turn (i.e. not at position 0).
|
||||
// Gemini rejects with INVALID_ARGUMENT if a functionCall turn is at
|
||||
// position 0 with no preceding user turn, even when (a) is satisfied.
|
||||
let len = merged.len();
|
||||
for i in 0..len {
|
||||
let is_model = merged[i].role.as_deref() == Some("model");
|
||||
@@ -394,7 +398,9 @@ fn sanitize_gemini_turns(contents: Vec<GeminiContent>) -> Vec<GeminiContent> {
|
||||
.iter()
|
||||
.any(|p| matches!(p, GeminiPart::FunctionResponse { .. }));
|
||||
|
||||
if !next_has_response {
|
||||
// After Step 1 merge, i > 0 guarantees a user turn precedes this model
|
||||
// turn (alternating roles). i == 0 means no preceding user turn.
|
||||
if i == 0 || !next_has_response {
|
||||
// Drop the functionCall parts from this model turn (keep text parts)
|
||||
merged[i]
|
||||
.parts
|
||||
|
||||
@@ -196,6 +196,33 @@ pub fn validate_and_repair_with_stats(messages: &[Message]) -> (Vec<Message>, Re
|
||||
(merged, stats)
|
||||
}
|
||||
|
||||
/// Ensure the message history starts with a user turn.
|
||||
///
|
||||
/// After context trimming the drain boundary may land on an assistant turn,
|
||||
/// leaving it at position 0. Providers (especially Gemini) require the first
|
||||
/// message to be from the user. This function drops leading assistant messages
|
||||
/// and re-validates to clean up newly-orphaned ToolResults.
|
||||
///
|
||||
/// The loop handles the edge case where the first user turn consisted entirely
|
||||
/// of ToolResult blocks that became orphaned (dropped by `validate_and_repair`),
|
||||
/// which would re-expose another leading assistant turn.
|
||||
pub fn ensure_starts_with_user(mut messages: Vec<Message>) -> Vec<Message> {
|
||||
loop {
|
||||
match messages.iter().position(|m| m.role == Role::User) {
|
||||
Some(0) | None => break,
|
||||
Some(i) => {
|
||||
warn!(
|
||||
dropped = i,
|
||||
"Dropping leading assistant turn(s) to ensure history starts with user"
|
||||
);
|
||||
messages.drain(..i);
|
||||
messages = validate_and_repair(&messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
messages
|
||||
}
|
||||
|
||||
/// Phase 2b: Reorder misplaced ToolResults -- ensure each result follows its use.
|
||||
///
|
||||
/// Builds a map of tool_use_id to the index of the assistant message containing it.
|
||||
|
||||
Reference in New Issue
Block a user