mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(providers): dedup tool specs at wire boundary to prevent 400 "Tool names must be unique" (#2846)
This commit is contained in:
@@ -528,19 +528,33 @@ impl OpenAiCompatibleProvider {
|
||||
tools: Option<&[crate::openhuman::tools::ToolSpec]>,
|
||||
) -> Option<Vec<serde_json::Value>> {
|
||||
tools.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.parameters,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
let mut seen: std::collections::HashSet<&str> =
|
||||
std::collections::HashSet::with_capacity(items.len());
|
||||
let mut dropped: Vec<&str> = Vec::new();
|
||||
let mut out: Vec<serde_json::Value> = Vec::with_capacity(items.len());
|
||||
for tool in items {
|
||||
if !seen.insert(tool.name.as_str()) {
|
||||
dropped.push(tool.name.as_str());
|
||||
continue;
|
||||
}
|
||||
out.push(serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.parameters,
|
||||
}
|
||||
}));
|
||||
}
|
||||
if !dropped.is_empty() {
|
||||
log::warn!(
|
||||
"[providers][compatible] dropped {} duplicate tool spec(s) at wire \
|
||||
boundary (TAURI-RUST-2E): {:?}",
|
||||
dropped.len(),
|
||||
dropped
|
||||
);
|
||||
}
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1785,3 +1785,82 @@ fn native_message_reasoning_content_present_when_some() {
|
||||
"reasoning_content must be present in the wire payload when Some"
|
||||
);
|
||||
}
|
||||
|
||||
// ── convert_tool_specs — TAURI-RUST-2E wire-boundary dedup ─────────────
|
||||
|
||||
fn spec(name: &str) -> crate::openhuman::tools::ToolSpec {
|
||||
crate::openhuman::tools::ToolSpec {
|
||||
name: name.to_string(),
|
||||
description: format!("{name} desc"),
|
||||
parameters: serde_json::json!({"type": "object"}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_tool_specs_none_input_returns_none() {
|
||||
assert!(OpenAiCompatibleProvider::convert_tool_specs(None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_tool_specs_empty_slice_returns_empty_vec() {
|
||||
let out = OpenAiCompatibleProvider::convert_tool_specs(Some(&[])).unwrap();
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_tool_specs_passes_through_unique_names() {
|
||||
let specs = vec![spec("alpha"), spec("beta"), spec("gamma")];
|
||||
let out = OpenAiCompatibleProvider::convert_tool_specs(Some(&specs)).unwrap();
|
||||
assert_eq!(out.len(), 3);
|
||||
let names: Vec<&str> = out
|
||||
.iter()
|
||||
.map(|t| t["function"]["name"].as_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, vec!["alpha", "beta", "gamma"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_tool_specs_dedups_duplicate_names_first_wins() {
|
||||
// First occurrence of `alpha` (with description "alpha desc") survives;
|
||||
// the second is dropped wholesale even though its `parameters` differ.
|
||||
let mut second_alpha = spec("alpha");
|
||||
second_alpha.description = "should be dropped".to_string();
|
||||
second_alpha.parameters = serde_json::json!({"different": true});
|
||||
let specs = vec![spec("alpha"), spec("beta"), second_alpha, spec("gamma")];
|
||||
|
||||
let out = OpenAiCompatibleProvider::convert_tool_specs(Some(&specs)).unwrap();
|
||||
assert_eq!(
|
||||
out.len(),
|
||||
3,
|
||||
"duplicate `alpha` must be dropped from wire payload"
|
||||
);
|
||||
let names: Vec<&str> = out
|
||||
.iter()
|
||||
.map(|t| t["function"]["name"].as_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, vec!["alpha", "beta", "gamma"]);
|
||||
assert_eq!(
|
||||
out[0]["function"]["description"].as_str().unwrap(),
|
||||
"alpha desc",
|
||||
"first occurrence's description must survive (first-wins)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_tool_specs_dedups_many_duplicates() {
|
||||
let specs = vec![
|
||||
spec("x"),
|
||||
spec("x"),
|
||||
spec("x"),
|
||||
spec("y"),
|
||||
spec("y"),
|
||||
spec("z"),
|
||||
];
|
||||
let out = OpenAiCompatibleProvider::convert_tool_specs(Some(&specs)).unwrap();
|
||||
assert_eq!(out.len(), 3);
|
||||
let names: Vec<&str> = out
|
||||
.iter()
|
||||
.map(|t| t["function"]["name"].as_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, vec!["x", "y", "z"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user