fix(inference): preserve Gemini thought_signature across tool-call turns (TAURI-RUST-4PK) (#3548) (#3553)

This commit is contained in:
oxoxDev
2026-06-09 08:15:38 -07:00
committed by GitHub
parent 4107c4e5b7
commit 631525ebdc
42 changed files with 318 additions and 0 deletions
+4
View File
@@ -26,6 +26,7 @@ fn native_dispatcher_roundtrip() {
id: "tc1".into(),
name: "file_read".into(),
arguments: "{\"path\":\"a.txt\"}".into(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -268,6 +269,7 @@ fn assistant_tool_calls(id: &str) -> ConversationMessage {
id: id.into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: None,
}
@@ -397,6 +399,7 @@ fn assistant_tool_calls_multi(ids: &[&str]) -> ConversationMessage {
id: (*id).into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
})
.collect(),
reasoning_content: None,
@@ -413,6 +416,7 @@ fn native_dispatcher_serializes_reasoning_content_for_tool_call_turns() {
id: "tc-1".into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: Some("chain-of-thought replay blob".into()),
},
@@ -80,6 +80,7 @@ async fn native_tool_call_decodes_json_encoded_arguments_string() {
id: "c1".into(),
name: "captured".into(),
arguments: "{\"city\":\"Berlin\",\"n\":3}".to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -143,6 +144,7 @@ async fn documents_silent_drop_of_non_json_arguments_string() {
name: "captured".into(),
// Not valid JSON — the model "meant" a plain string.
arguments: "world".to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -353,6 +355,7 @@ async fn native_tool_calls_take_precedence_over_xml_in_text() {
id: "c1".into(),
name: "tool_a".into(),
arguments: "{\"src\":\"native\"}".into(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -261,6 +261,7 @@ fn structured_tool_call_and_history_helpers_round_trip_expected_shapes() {
id: "call-1".into(),
name: "echo".into(),
arguments: "{\"value\":\"hello\"}".into(),
extra_content: None,
}];
let parsed = parse_structured_tool_calls(&tool_calls);
@@ -456,6 +456,8 @@ impl Agent {
.unwrap_or_else(|| format!("parsed-{}-{}", iteration + 1, idx + 1)),
name: call.name.clone(),
arguments: call.arguments.to_string(),
// Prompt-based tool calls carry no provider extra_content.
extra_content: None,
})
.collect()
}
@@ -312,6 +312,7 @@ fn helper_paths_cover_no_overlap_native_calls_and_truncation() {
id: "native-1".into(),
name: "echo".into(),
arguments: "{}".into(),
extra_content: None,
}];
let response = crate::openhuman::inference::provider::ChatResponse {
text: Some(String::new()),
@@ -461,6 +461,7 @@ async fn turn_with_native_dispatcher_handles_tool_results_variant() {
id: "tc1".into(),
name: "echo".into(),
arguments: "{}".into(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -604,6 +605,7 @@ async fn turn_dispatches_spawn_subagent_through_full_path() {
"prompt": "find out about X"
})
.to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -78,6 +78,9 @@ fn persisted_tool_calls(
id,
name: c.name.clone(),
arguments: c.arguments.to_string(),
// Prompt-parsed calls carry no provider extra_content; the
// native (Gemini) path returns early above, preserving it.
extra_content: None,
}
})
.collect()
@@ -414,6 +414,7 @@ fn trim_history_snaps_past_orphaned_tool_results() {
id: "call_x".into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: None,
},
@@ -302,6 +302,7 @@ fn tool_response(name: &str, args: &str) -> ChatResponse {
id: "call-1".into(),
name: name.into(),
arguments: args.into(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -263,6 +263,7 @@ impl Provider for KeywordScriptedProvider {
id: format!("call_{id}"),
name: c.name.clone(),
arguments: c.arguments.to_string(),
extra_content: None,
}
})
.collect()
@@ -372,6 +372,7 @@ mod tests {
id: "1".into(),
name: "echo".into(),
arguments: "{\"value\":\"x\"}".into(),
extra_content: None,
}],
reasoning_content: None,
};
@@ -396,6 +397,7 @@ mod tests {
id: id.into(),
name: "f".into(),
arguments: "{}".into(),
extra_content: None,
}
}
@@ -398,6 +398,7 @@ async fn run_tool_call_loop_persists_native_tool_results_as_tool_messages() {
id: "call-1".into(),
name: "echo".into(),
arguments: "{}".into(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
+18
View File
@@ -382,6 +382,7 @@ async fn turn_executes_single_tool_then_returns() {
id: "tc1".into(),
name: "echo".into(),
arguments: r#"{"message": "hello from tool"}"#.into(),
extra_content: None,
}]),
text_response("I ran the tool"),
]));
@@ -412,16 +413,19 @@ async fn turn_handles_multi_step_tool_chain() {
id: "tc1".into(),
name: "counter".into(),
arguments: "{}".into(),
extra_content: None,
}]),
tool_response(vec![ToolCall {
id: "tc2".into(),
name: "counter".into(),
arguments: "{}".into(),
extra_content: None,
}]),
tool_response(vec![ToolCall {
id: "tc3".into(),
name: "counter".into(),
arguments: "{}".into(),
extra_content: None,
}]),
text_response("Done after 3 calls"),
]));
@@ -460,6 +464,7 @@ async fn turn_emits_checkpoint_at_max_iterations() {
id: format!("tc{i}"),
name: "echo".into(),
arguments: r#"{"message": "loop"}"#.into(),
extra_content: None,
}]));
}
@@ -504,6 +509,7 @@ async fn turn_handles_unknown_tool_gracefully() {
id: "tc1".into(),
name: "nonexistent_tool".into(),
arguments: "{}".into(),
extra_content: None,
}]),
text_response("I couldn't find that tool"),
]));
@@ -544,6 +550,7 @@ async fn turn_recovers_from_tool_failure() {
id: "tc1".into(),
name: "fail".into(),
arguments: "{}".into(),
extra_content: None,
}]),
text_response("Tool failed but I recovered"),
]));
@@ -568,6 +575,7 @@ async fn turn_recovers_from_tool_error() {
id: "tc1".into(),
name: "panicker".into(),
arguments: "{}".into(),
extra_content: None,
}]),
text_response("I recovered from the error"),
]));
@@ -794,6 +802,7 @@ async fn turn_preserves_text_alongside_tool_calls() {
id: "tc1".into(),
name: "echo".into(),
arguments: r#"{"message": "hi"}"#.into(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -835,16 +844,19 @@ async fn turn_handles_multiple_tools_in_one_response() {
id: "tc1".into(),
name: "counter".into(),
arguments: "{}".into(),
extra_content: None,
},
ToolCall {
id: "tc2".into(),
name: "counter".into(),
arguments: "{}".into(),
extra_content: None,
},
ToolCall {
id: "tc3".into(),
name: "counter".into(),
arguments: "{}".into(),
extra_content: None,
},
]),
text_response("All 3 done"),
@@ -982,6 +994,7 @@ async fn history_contains_all_expected_entries_after_tool_loop() {
id: "tc1".into(),
name: "echo".into(),
arguments: r#"{"message": "tool-out"}"#.into(),
extra_content: None,
}]),
text_response("final answer"),
]));
@@ -1087,6 +1100,7 @@ async fn native_dispatcher_handles_stringified_arguments() {
id: "tc1".into(),
name: "echo".into(),
arguments: r#"{"message": "hello"}"#.into(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -1176,6 +1190,7 @@ fn conversation_message_serialization_roundtrip() {
id: "tc1".into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: Some("thinking".into()),
},
@@ -1302,6 +1317,7 @@ fn xml_dispatcher_converts_history_to_provider_messages() {
id: "tc1".into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: None,
},
@@ -1337,11 +1353,13 @@ fn native_dispatcher_converts_tool_results_to_tool_messages() {
id: "tc1".into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
},
ToolCall {
id: "tc2".into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
},
],
reasoning_content: None,
@@ -500,6 +500,7 @@ fn tool_response(name: &str, arguments: serde_json::Value) -> ChatResponse {
id: format!("call-{name}"),
name: name.to_string(),
arguments: arguments.to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
+1
View File
@@ -14,6 +14,7 @@ fn call(id: &str) -> ConversationMessage {
id: id.into(),
name: "t".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: None,
}
+1
View File
@@ -115,6 +115,7 @@ mod tests {
id: id.into(),
name: name.into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: None,
}
+1
View File
@@ -269,6 +269,7 @@ mod tests {
id: id.into(),
name: "t".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: None,
}
@@ -18,6 +18,7 @@ fn call(id: &str) -> ConversationMessage {
id: id.into(),
name: "t".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: None,
}
@@ -217,6 +218,7 @@ fn transcript_renders_all_message_variants() {
id: "1".into(),
name: "shell".into(),
arguments: r#"{"cmd":"ls"}"#.into(),
extra_content: None,
}],
reasoning_content: None,
},
@@ -251,6 +251,8 @@ impl EventMapper {
id: call_id,
name,
arguments,
// Claude Code CLI events carry no OpenAI-compat extra_content.
extra_content: None,
});
}
Vec::new()
@@ -199,6 +199,9 @@ impl OpenAiCompatibleProvider {
tc.arguments,
)),
}),
// Echo Gemini's thought_signature back on
// the next turn (TAURI-RUST-4PK).
extra_content: tc.extra_content,
})
.collect::<Vec<_>>();
@@ -405,6 +408,10 @@ impl OpenAiCompatibleProvider {
id: tc.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
name,
arguments,
// Preserve Gemini's thought_signature (TAURI-RUST-4PK) so it
// can be echoed on the next turn; None for providers that
// don't send extra_content.
extra_content: tc.extra_content,
})
})
.collect::<Vec<_>>();
@@ -420,6 +427,8 @@ impl OpenAiCompatibleProvider {
id: uuid::Uuid::new_v4().to_string(),
name: name.clone(),
arguments: normalize_function_arguments(function.arguments.clone()),
// Legacy `function_call` shape carries no extra_content.
extra_content: None,
});
}
}
@@ -156,6 +156,9 @@ pub(crate) fn parse_provider_tool_call_from_value(
arguments: normalize_function_arguments(Some(serde_json::Value::String(
call.arguments,
))),
// Preserve Gemini's thought_signature through the stored-history
// recovery path so it is echoed on the next turn (TAURI-RUST-4PK).
extra_content: call.extra_content,
});
}
}
@@ -176,6 +179,9 @@ pub(crate) fn parse_provider_tool_call_from_value(
id,
name: name.to_string(),
arguments: normalize_function_arguments(function.get("arguments").cloned()),
// Carry Gemini's thought_signature if the stored value had one
// (TAURI-RUST-4PK).
extra_content: value.get("extra_content").cloned(),
})
}
@@ -438,6 +438,9 @@ impl Provider for OpenAiCompatibleProvider {
id: tc.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
name,
arguments,
// Non-streaming response: preserve Gemini's thought_signature
// so it round-trips on the next turn (TAURI-RUST-4PK).
extra_content: tc.extra_content,
})
})
.collect::<Vec<_>>();
@@ -254,6 +254,16 @@ impl OpenAiCompatibleProvider {
let idx = tc.index.unwrap_or(0);
let entry = tool_accum.entry(idx).or_default();
// Capture the first non-null extra_content seen for
// this index (Gemini's thought_signature, TAURI-RUST-4PK).
if entry.extra_content.is_none() {
if let Some(ec) = tc.extra_content.as_ref() {
if !ec.is_null() {
entry.extra_content = Some(ec.clone());
}
}
}
if let Some(id) = tc.id.as_ref() {
if entry.id.is_none() {
log::debug!(
@@ -380,6 +390,9 @@ impl OpenAiCompatibleProvider {
)
},
}),
// Carry Gemini's thought_signature through to parse_native_response
// so it lands on the harness ToolCall (TAURI-RUST-4PK).
extra_content: c.extra_content,
})
.collect();
@@ -1157,6 +1157,7 @@ fn parse_native_response_preserves_tool_call_id() {
r#"{"command":"pwd"}"#.to_string(),
)),
}),
extra_content: None,
}]),
function_call: None,
reasoning_content: None,
@@ -1183,6 +1184,7 @@ fn parse_native_response_captures_reasoning_content() {
name: Some("shell".to_string()),
arguments: Some(serde_json::Value::String("{}".to_string())),
}),
extra_content: None,
}]),
function_call: None,
reasoning_content: Some(" weighing the options ".to_string()),
@@ -1234,6 +1236,189 @@ fn convert_messages_for_native_maps_tool_result_payload() {
);
}
// ── TAURI-RUST-4PK: Gemini thought_signature round-trip ──────────────────────
/// The wire `ToolCall` must capture Gemini's `extra_content` from the response
/// and re-emit it verbatim on the request, so the thought_signature survives the
/// round-trip. A tool call without it omits the field entirely (non-Gemini
/// providers stay byte-identical on the wire).
#[test]
fn tool_call_wire_round_trips_extra_content() {
let json = r#"{"id":"call_g","type":"function","function":{"name":"shell","arguments":"{}"},"extra_content":{"google":{"thought_signature":"SIG123"}}}"#;
let tc: ToolCall = serde_json::from_str(json).unwrap();
assert_eq!(
tc.extra_content
.as_ref()
.and_then(|v| v.pointer("/google/thought_signature"))
.and_then(|v| v.as_str()),
Some("SIG123"),
"extra_content must be captured from the Gemini response"
);
let reemitted = serde_json::to_value(&tc).unwrap();
assert_eq!(
reemitted
.pointer("/extra_content/google/thought_signature")
.and_then(|v| v.as_str()),
Some("SIG123"),
"extra_content must be echoed verbatim on the request body"
);
let bare: ToolCall = serde_json::from_str(
r#"{"id":"c","type":"function","function":{"name":"x","arguments":"{}"}}"#,
)
.unwrap();
assert!(bare.extra_content.is_none());
assert!(
serde_json::to_value(&bare)
.unwrap()
.get("extra_content")
.is_none(),
"providers that don't send extra_content keep a byte-identical wire body"
);
}
/// `parse_native_response` lifts the tool-call `extra_content` onto the harness
/// ToolCall so it can be persisted and echoed (TAURI-RUST-4PK).
#[test]
fn parse_native_response_captures_tool_call_extra_content() {
let message = ResponseMessage {
content: None,
tool_calls: Some(vec![ToolCall {
id: Some("call_g".to_string()),
kind: Some("function".to_string()),
function: Some(Function {
name: Some("shell".to_string()),
arguments: Some(serde_json::Value::String("{}".to_string())),
}),
extra_content: Some(serde_json::json!({"google":{"thought_signature":"SIG_RESP"}})),
}]),
function_call: None,
reasoning_content: None,
};
let parsed =
OpenAiCompatibleProvider::parse_native_response(wrap_message(message), "google").unwrap();
assert_eq!(parsed.tool_calls.len(), 1);
assert_eq!(
parsed.tool_calls[0]
.extra_content
.as_ref()
.and_then(|v| v.pointer("/google/thought_signature"))
.and_then(|v| v.as_str()),
Some("SIG_RESP"),
"the signature must land on the harness ToolCall"
);
}
/// On rebuild, a persisted assistant tool-call message whose stored JSON carries
/// `extra_content` must re-emit it on the wire tool_calls, so Gemini sees the
/// signature on the follow-up turn (TAURI-RUST-4PK).
#[test]
fn convert_messages_for_native_echoes_tool_call_extra_content() {
let input = vec![
ChatMessage::assistant(
r#"{"content":"on it","tool_calls":[{"id":"call_g","name":"shell","arguments":"{}","extra_content":{"google":{"thought_signature":"SIG_ECHO"}}}]}"#,
),
ChatMessage::tool(r#"{"tool_call_id":"call_g","content":"done"}"#),
];
let converted = OpenAiCompatibleProvider::convert_messages_for_native(&input);
let tool_calls = converted[0]
.tool_calls
.as_ref()
.expect("assistant tool_calls present");
assert_eq!(
tool_calls[0]
.extra_content
.as_ref()
.and_then(|v| v.pointer("/google/thought_signature"))
.and_then(|v| v.as_str()),
Some("SIG_ECHO"),
"stored extra_content must be echoed back on the rebuilt request"
);
assert_eq!(
serde_json::to_value(tool_calls)
.unwrap()
.pointer("/0/extra_content/google/thought_signature")
.and_then(|v| v.as_str()),
Some("SIG_ECHO"),
"echoed signature must appear on the serialized wire body"
);
}
/// A non-Gemini stored tool call (no `extra_content`) rebuilds with the field
/// omitted — every other provider's wire body stays byte-identical.
#[test]
fn convert_messages_for_native_tool_call_without_extra_content_stays_none() {
let input = vec![
ChatMessage::assistant(
r#"{"content":"on it","tool_calls":[{"id":"call_x","name":"shell","arguments":"{}"}]}"#,
),
ChatMessage::tool(r#"{"tool_call_id":"call_x","content":"done"}"#),
];
let converted = OpenAiCompatibleProvider::convert_messages_for_native(&input);
let tool_calls = converted[0]
.tool_calls
.as_ref()
.expect("assistant tool_calls present");
assert!(tool_calls[0].extra_content.is_none());
assert!(serde_json::to_value(&tool_calls[0])
.unwrap()
.get("extra_content")
.is_none());
}
/// Streaming: Gemini sends the thought_signature in the tool-call delta's
/// `extra_content` on the first chunk. The accumulator must preserve it onto the
/// aggregated tool call so it reaches history (TAURI-RUST-4PK).
#[tokio::test]
async fn streaming_tool_call_captures_extra_content() {
let mock_server = MockServer::start().await;
let body = "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_g\",\"type\":\"function\",\"function\":{\"name\":\"shell\",\"arguments\":\"{}\"},\"extra_content\":{\"google\":{\"thought_signature\":\"SIG_STREAM\"}}}]}}]}\n\n\
data: [DONE]\n\n";
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(ResponseTemplate::new(200).set_body_raw(body, "text/event-stream"))
.mount(&mock_server)
.await;
let provider =
OpenAiCompatibleProvider::new("google", &mock_server.uri(), None, AuthStyle::None);
let request = NativeChatRequest {
model: "models/gemini-3.5-flash".to_string(),
messages: vec![NativeMessage {
role: "user".to_string(),
content: Some("hi".into()),
tool_call_id: None,
tool_calls: None,
reasoning_content: None,
}],
temperature: Some(0.7),
stream: Some(true),
tools: None,
tool_choice: None,
thread_id: None,
stream_options: Some(super::compatible_types::OpenAiStreamOptions {
include_usage: true,
}),
options: None,
frequency_penalty: None,
};
let (delta_tx, _delta_rx) = tokio::sync::mpsc::channel(64);
let resp = provider
.stream_native_chat(None, &request, &delta_tx, 0)
.await
.unwrap();
assert_eq!(resp.tool_calls.len(), 1);
assert_eq!(
resp.tool_calls[0]
.extra_content
.as_ref()
.and_then(|v| v.pointer("/google/thought_signature"))
.and_then(|v| v.as_str()),
Some("SIG_STREAM"),
"streaming must preserve the thought_signature onto the aggregated tool call"
);
}
/// Helper: roles in serialized order.
fn roles(messages: &[NativeMessage]) -> Vec<&str> {
messages.iter().map(|m| m.role.as_str()).collect()
@@ -1550,6 +1735,7 @@ fn enforce_invariants_clears_reasoning_when_assistant_collapses_to_text() {
name: Some("web_fetch".to_string()),
arguments: Some(serde_json::Value::String("{}".to_string())),
}),
extra_content: None,
}]),
reasoning_content: Some("deep reasoning".to_string()),
},
@@ -1897,6 +2083,7 @@ fn response_with_tool_call_object_arguments_deserializes() {
name: Some("get_weather".to_string()),
arguments: Some(serde_json::json!({"location":"London","unit":"c"})),
}),
extra_content: None,
}]),
function_call: None,
}),
@@ -411,6 +411,20 @@ pub(crate) struct ToolCall {
#[serde(rename = "type")]
pub(crate) kind: Option<String>,
pub(crate) function: Option<Function>,
/// Provider-specific passthrough metadata attached to a tool call.
///
/// Google's Gemini OpenAI-compat endpoint returns a cryptographically
/// signed reasoning token here as
/// `extra_content.google.thought_signature`, and **requires** it echoed
/// back verbatim on the assistant tool-call turn of every subsequent
/// request — otherwise it 400s with "Function call is missing a
/// thought_signature" (TAURI-RUST-4PK). Captured on the response and
/// re-emitted on the request as an opaque value so any future
/// `extra_content.*` keys round-trip unchanged. `skip_serializing_if`
/// keeps the wire body byte-identical for every provider that doesn't
/// send it.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) extra_content: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -517,6 +531,12 @@ pub(crate) struct StreamToolCallDelta {
pub(crate) kind: Option<String>,
#[serde(default)]
pub(crate) function: Option<StreamToolCallFunction>,
/// Provider passthrough metadata (Gemini's `extra_content`, carrying
/// `google.thought_signature`). Arrives on the first chunk for a given
/// tool-call index; accumulated and re-emitted so the signature survives
/// the streaming path (TAURI-RUST-4PK).
#[serde(default)]
pub(crate) extra_content: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
@@ -545,4 +565,8 @@ pub(crate) struct StreamingToolCall {
pub(crate) arguments: String,
pub(crate) emitted_start: bool,
pub(crate) emitted_chars: usize,
/// First non-null `extra_content` seen for this tool-call index (Gemini's
/// thought_signature). Re-emitted on the aggregated [`ToolCall`] so it can
/// be echoed on the next turn (TAURI-RUST-4PK).
pub(crate) extra_content: Option<serde_json::Value>,
}
@@ -59,6 +59,14 @@ pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: String,
/// Provider-specific passthrough metadata for this call, captured from the
/// response and echoed back verbatim on the next assistant turn. Carries
/// Google Gemini's required `extra_content.google.thought_signature` so
/// multi-turn tool calling round-trips without a 400 (TAURI-RUST-4PK).
/// `None`/omitted for every provider that doesn't emit it, so non-Gemini
/// history stays byte-identical.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra_content: Option<serde_json::Value>,
}
/// Token usage information returned by the provider after an inference call.
@@ -55,6 +55,7 @@ fn chat_response_helpers() {
id: "1".into(),
name: "shell".into(),
arguments: "{}".into(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -69,6 +70,7 @@ fn tool_call_serialization() {
id: "call_123".into(),
name: "file_read".into(),
arguments: r#"{"path":"test.txt"}"#.into(),
extra_content: None,
};
let json = serde_json::to_string(&tc).unwrap();
assert!(json.contains("call_123"));
@@ -86,6 +86,7 @@ fn tool_call(id: &str, name: &str, args: serde_json::Value) -> ChatResponse {
id: id.into(),
name: name.into(),
arguments: args.to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
+1
View File
@@ -97,6 +97,7 @@ fn tool_call_resp(id: &str, name: &str, args: serde_json::Value) -> ChatResponse
id: id.into(),
name: name.into(),
arguments: args.to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -223,6 +223,7 @@ fn tool_response(name: &str, arguments: serde_json::Value) -> ChatResponse {
id: "round21-call".to_string(),
name: name.to_string(),
arguments: arguments.to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: Some("scripted tool use".to_string()),
@@ -262,6 +262,7 @@ fn tool_response(id: &str, name: &str, arguments: serde_json::Value) -> ChatResp
id: id.to_string(),
name: name.to_string(),
arguments: arguments.to_string(),
extra_content: None,
}],
usage: Some(UsageInfo {
input_tokens: 7,
+1
View File
@@ -187,6 +187,7 @@ fn tool_call(id: &str, name: &str, arguments: serde_json::Value) -> ToolCall {
id: id.to_string(),
name: name.to_string(),
arguments: arguments.to_string(),
extra_content: None,
}
}
@@ -225,6 +225,7 @@ fn tool_response(name: &str, arguments: serde_json::Value) -> ChatResponse {
id: "round18-call".to_string(),
name: name.to_string(),
arguments: arguments.to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: Some("test reasoning".to_string()),
@@ -487,6 +487,7 @@ fn native_tool_response(id: &str, name: &str, args: serde_json::Value) -> ChatRe
id: id.to_string(),
name: name.to_string(),
arguments: args.to_string(),
extra_content: None,
}],
usage: Some(UsageInfo {
input_tokens: 21,
@@ -330,6 +330,7 @@ fn native_tool_response(name: &str, arguments: serde_json::Value) -> ChatRespons
id: format!("call-{name}"),
name: name.to_string(),
arguments: arguments.to_string(),
extra_content: None,
}],
usage: Some(UsageInfo {
input_tokens: 13,
@@ -290,6 +290,7 @@ fn native_tool_response(name: &str, arguments: &str) -> ChatResponse {
id: "round20-native-1".to_string(),
name: name.to_string(),
arguments: arguments.to_string(),
extra_content: None,
}],
usage: Some(UsageInfo {
input_tokens: 7_000,
+1
View File
@@ -53,6 +53,7 @@ impl Provider for MockCalendarProvider {
"timeMax": "2026-05-04T00:00:00Z"
})
.to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -218,6 +218,7 @@ impl Provider for StubProvider {
id: "call_1".into(),
name: "composio_list_tools".into(),
arguments: json!({ "toolkits": ["gmail"] }).to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
@@ -3798,11 +3798,13 @@ fn agent_dispatchers_and_host_runtime_cover_public_edge_paths() {
id: "call-ok".into(),
name: "search_docs".into(),
arguments: "{\"query\":\"native\"}".into(),
extra_content: None,
},
ToolCall {
id: "call-bad-json".into(),
name: "search_docs".into(),
arguments: "{not-json".into(),
extra_content: None,
},
],
..Default::default()
@@ -3835,6 +3837,7 @@ fn agent_dispatchers_and_host_runtime_cover_public_edge_paths() {
id: "call-1".into(),
name: "search_docs".into(),
arguments: "{\"query\":\"paired\"}".into(),
extra_content: None,
}],
reasoning_content: Some("thinking".into()),
},
@@ -3848,6 +3851,7 @@ fn agent_dispatchers_and_host_runtime_cover_public_edge_paths() {
id: "missing-result".into(),
name: "search_docs".into(),
arguments: "{}".into(),
extra_content: None,
}],
reasoning_content: None,
},
@@ -671,6 +671,7 @@ impl Provider for Round22Provider {
id: "round22-call".to_string(),
name: "round22_tool".to_string(),
arguments: "{}".to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: None,
+1
View File
@@ -218,6 +218,7 @@ fn tool_response(id: &str, name: &str, arguments: serde_json::Value) -> ChatResp
id: id.to_string(),
name: name.to_string(),
arguments: arguments.to_string(),
extra_content: None,
}],
usage: None,
reasoning_content: Some(format!("need {name}")),
@@ -725,6 +725,7 @@ async fn round16_agent_builder_turn_uses_public_harness_paths() {
id: "call-round16".into(),
name: "echo".into(),
arguments: json!({ "message": "builder" }).to_string(),
extra_content: None,
}],
),
response(Some("builder final"), Vec::new()),