Finalize model-specific tool plans in ToolRouter (#41195)

## Why

Planning tools for a candidate model must not change the selected model or its
Responses Lite tool inventory. Otherwise, preparing a fallback can overwrite
metadata before the current model's request is sent.

## What changed

- Build tool registries and visible specs from an explicit `ModelInfo`.
- Store the finalized tool mode, code-mode names, namespace inventory, and
  terminal and child-management capabilities on `ToolRouter`.
- Publish namespace inventory only after selecting the step's router, and use
  that router's mode when dispatching and recording tool calls.

## Testing

Added coverage for candidate-model isolation, fallback compaction metadata,
code-mode worker selection, normalized tool names, and router capabilities.

GitOrigin-RevId: 56df2de8f42c7e93a4c4dcd117720e40a0cca3db
This commit is contained in:
Abhinav authored and copyberry committed 2026-08-27 20:01:39 +00:00
1 parent 8aea62b2d8
commit 0182ff3480
13 files changed
+532 -127

No files matched your search

+22
View File
@@ -2666,9 +2666,11 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
let mut previous_model_info =
model_info_with_context_window("gpt-5.4", /*context_window*/ 273_000);
previous_model_info.slug = previous_model_family.to_string();
previous_model_info.use_responses_lite = true;
let mut next_model_info =
model_info_with_context_window("gpt-5.4", /*context_window*/ 125_000);
next_model_info.slug = next_model.to_string();
next_model_info.use_responses_lite = false;
let models_mock = mount_models_once(
&server,
@@ -2710,6 +2712,7 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
.with_model(retired_model)
.with_config(move |config| {
config.model_provider = model_provider;
config.tool_registry.turn_metadata_includes_tool_info = true;
set_test_compact_prompt(config);
let _ = config.features.enable(Feature::RemoteCompactionV2);
});
@@ -2755,6 +2758,25 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
);
assert_eq!(requests[2].body_json()["model"].as_str(), Some(next_model));
assert_eq!(requests[3].body_json()["model"].as_str(), Some(next_model));
// Preparing the non-Lite fallback must not clear the previous model's inventory
// before its compaction attempt is sent.
let [first_metadata, compact_metadata] = [&requests[0], &requests[1]].map(|request| {
serde_json::from_str::<Value>(
request.body_json()["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.expect("request should include turn metadata"),
)
.expect("turn metadata should be valid JSON")
});
let inventory = first_metadata["tool_namespaces_info"]
.as_object()
.expect("Responses Lite request should include tool inventory");
assert!(!inventory.is_empty());
assert_eq!(
compact_metadata["tool_namespaces_info"],
first_metadata["tool_namespaces_info"],
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]