Preserve Gemini thought_signature on function calls

Gemini 3 models include a thoughtSignature field on function call
parts that must be returned in subsequent conversation turns. Add
provider_metadata to ToolCall to carry this through, and extract/emit
it in both the non-streaming and streaming Gemini code paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-23 10:14:19 -05:00
parent 7dddc49121
commit 3b1411a3ac
2 changed files with 34 additions and 11 deletions

View file

@ -189,11 +189,12 @@ fn parse_part(part: &serde_json::Value) -> Option<ContentPart> {
.get("args")
.cloned()
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
return Some(ContentPart::ToolCall(ToolCall::new(
uuid::Uuid::new_v4().to_string(),
name,
args,
)));
let mut tc = ToolCall::new(uuid::Uuid::new_v4().to_string(), name, args);
// Preserve thought_signature for Gemini 3 models (sibling of functionCall in the part)
if let Some(sig) = part.get("thoughtSignature") {
tc.provider_metadata = Some(serde_json::json!({"thoughtSignature": sig}));
}
return Some(ContentPart::ToolCall(tc));
}
None
}
@ -240,12 +241,23 @@ fn translate_messages(messages: &[&Message]) -> Vec<Content> {
.iter()
.filter_map(|part| match part {
ContentPart::Text(text) => Some(serde_json::json!({"text": text})),
ContentPart::ToolCall(tc) => Some(serde_json::json!({
"functionCall": {
"name": tc.name,
"args": tc.arguments,
ContentPart::ToolCall(tc) => {
let mut part_json = serde_json::json!({
"functionCall": {
"name": tc.name,
"args": tc.arguments,
}
});
// Re-attach thought_signature as sibling of functionCall
if let Some(sig) = tc
.provider_metadata
.as_ref()
.and_then(|m| m.get("thoughtSignature"))
{
part_json["thoughtSignature"] = sig.clone();
}
})),
Some(part_json)
}
ContentPart::Image(img) => {
img.url.as_ref().map_or_else(
|| {
@ -804,7 +816,12 @@ impl SseStreamState {
.get("args")
.cloned()
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
let tool_call = ToolCall::new(uuid::Uuid::new_v4().to_string(), name, args);
let mut tool_call = ToolCall::new(uuid::Uuid::new_v4().to_string(), name, args);
// Preserve thought_signature for Gemini 3 models (sibling of functionCall)
if let Some(sig) = part.get("thoughtSignature") {
tool_call.provider_metadata =
Some(serde_json::json!({"thoughtSignature": sig}));
}
// Gemini delivers function calls as complete objects in a single chunk.
self.pending_events

View file

@ -55,6 +55,11 @@ pub struct ToolCall {
pub name: String,
pub arguments: serde_json::Value,
pub raw_arguments: Option<String>,
/// Opaque provider-specific metadata (e.g. Gemini thought_signature).
/// Preserved across round-trips so the provider can include it when
/// sending conversation history back to the API.
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_metadata: Option<serde_json::Value>,
}
impl ToolCall {
@ -68,6 +73,7 @@ impl ToolCall {
name: name.into(),
arguments,
raw_arguments: None,
provider_metadata: None,
}
}
}