fix: filter dynamic deferred tools from model_visible_specs (#19771)

fixes #19486

### Problem
Right now dynamic deferred tools are filtered at normal-turn prompt
building time, rather than upstream while building the `ToolRouter`
itself. This causes issues because dynamic deferred tools are then
wrongly included in the router's `model_visible_specs`, which is what
the compaction request-building flow relies on.

### Fix
Move the dynamic deferred tool filtering to `ToolRouter` creation time
to solve this problem for every request that relies on `ToolRouter` for
`model_visible_specs`, which solves the issue generically.

### Tests
Added unit + integration tests to ensure dynamic deferred tools are
omitted from `model_visible_specs` and compaction request respectively.

Tested against live `/compact` endpoint; raw deferred dynamic tools
without `tool_search` returned `400` (current bug), while the filtered
payload (this fix) returns `200`.
This commit is contained in:
sayan-oai
2026-04-27 12:09:02 -07:00
committed by GitHub
parent e5709db6dc
commit 85c1500569
5 changed files with 293 additions and 69 deletions

View File

@@ -605,7 +605,8 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
"apps tools/call should include turn metadata turn_id: {apps_tool_call:?}"
);
let first_request_tools = tool_names(&requests[0].body_json());
let first_request_body = requests[0].body_json();
let first_request_tools = tool_names(&first_request_body);
assert!(
first_request_tools
.iter()
@@ -823,7 +824,8 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
let requests = mock.requests();
assert_eq!(requests.len(), 3);
let first_request_tools = tool_names(&requests[0].body_json());
let first_request_body = requests[0].body_json();
let first_request_tools = tool_names(&first_request_body);
assert!(
first_request_tools
.iter()
@@ -853,7 +855,8 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
})]
);
let second_request_tools = tool_names(&requests[1].body_json());
let second_request_body = requests[1].body_json();
let second_request_tools = tool_names(&second_request_body);
assert!(
!second_request_tools.iter().any(|name| name == tool_name),
"follow-up request should rely on tool_search_output history, not tool injection: {second_request_tools:?}"
@@ -870,7 +873,8 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
FunctionCallOutputPayload::from_text("dynamic-search-ok".to_string())
);
let third_request_tools = tool_names(&requests[2].body_json());
let third_request_body = requests[2].body_json();
let third_request_tools = tool_names(&third_request_body);
assert!(
!third_request_tools.iter().any(|name| name == tool_name),
"post-tool follow-up should rely on tool_search_output history, not tool injection: {third_request_tools:?}"