mirror of
https://github.com/openai/codex.git
synced 2026-11-08 08:37:30 +00:00
Use captured step settings for request metadata and tool hooks (#45248)
## Why Model and reasoning effort updates during a turn can leave request metadata and tool hooks reporting the turn's initial settings. Metadata should describe the step that issued the request or tool call. ## What changed - Share captured execution metadata across Responses, MCP, and extension tool calls, including model, reasoning effort, and automatic review and Node REPL flags. - Build Responses tool inventory metadata from the issuing step's finalized tool router. Attach the finalized inventory separately for remote compaction. - Use captured step settings for pre- and post-tool hooks, and captured review settings for permission-request hooks. ## Testing Add and extend regression tests for model and effort changes during a turn, MCP metadata, pre-tool hook model attribution, captured review flags, and tool inventory matching the issuing request. GitOrigin-RevId: 9dee46a8b4839de8b434cdad4ed441cc0bb6ff9b
This commit is contained in:
1 parent
36f0dbe796
commit
16537b20a5
20 files changed
+545
-255
No files matched your search
@@ -1,4 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -20,6 +19,7 @@ use codex_history::InitialHistory;
|
||||
use codex_history::RolloutItem;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::built_in_model_providers;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_plugin::PluginHookSource;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_protocol::items::parse_hook_prompt_fragment;
|
||||
@@ -28,6 +28,8 @@ use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -36,9 +38,13 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::protocol::TurnSettingsUpdate;
|
||||
use codex_protocol::protocol::TurnSettingsUpdateOutcome;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -70,10 +76,12 @@ use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::test_target_os;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use core_test_support::wait_for_mcp_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
@@ -3605,6 +3613,143 @@ async fn pre_tool_use_json_deny_blocks_exec_command_before_execution() -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_tool_use_hook_model_tracks_step_after_a_turn_update() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let request_user_input_call_id = "pause-before-pretooluse-exec-command";
|
||||
let call_id = "pretooluse-exec-command";
|
||||
let model_a = "pretooluse-attribution-a";
|
||||
let model_b = "pretooluse-attribution-b";
|
||||
let command = "echo attribution";
|
||||
let args = serde_json::json!({ "cmd": command });
|
||||
let responses = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(
|
||||
request_user_input_call_id,
|
||||
"request_user_input",
|
||||
r#"{"questions":[{"id":"continue","header":"Continue","question":"Continue?","options":[{"label":"Yes (Recommended)","description":"Run it."},{"label":"No","description":"Stop."}]}]}"#,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
core_test_support::responses::ev_function_call(
|
||||
call_id,
|
||||
"exec_command",
|
||||
&serde_json::to_string(&args)?,
|
||||
),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-3"),
|
||||
ev_assistant_message("msg-1", "hook blocked it"),
|
||||
ev_completed("resp-3"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let base_model = bundled_models_response()
|
||||
.expect("bundled models should parse")
|
||||
.models
|
||||
.into_iter()
|
||||
.find(|model| model.slug == "gpt-5.4")
|
||||
.expect("bundled gpt-5.4 model");
|
||||
let models = [model_a, model_b]
|
||||
.into_iter()
|
||||
.map(|slug| {
|
||||
let mut model = base_model.clone();
|
||||
model.slug = slug.to_string();
|
||||
model
|
||||
})
|
||||
.collect();
|
||||
let mut builder = test_codex()
|
||||
.with_model(model_a)
|
||||
.with_pre_build_hook(|home| {
|
||||
write_pre_tool_use_hook(home, Some("^Bash$"), "json_deny", "blocked by pre hook")
|
||||
.expect("failed to write pre tool use hook test fixture");
|
||||
})
|
||||
.with_config(move |config| {
|
||||
trust_discovered_hooks(config);
|
||||
for feature in [
|
||||
Feature::StepModelSwitching,
|
||||
Feature::DefaultModeRequestUserInput,
|
||||
] {
|
||||
config
|
||||
.features
|
||||
.enable(feature)
|
||||
.expect("test config should allow feature update");
|
||||
}
|
||||
config.model_catalog = Some(ModelsResponse { models });
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
let (sandbox_policy, permission_profile) =
|
||||
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
|
||||
test.codex
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "run the blocked shell command".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}])
|
||||
.with_thread_settings(ThreadSettingsOverrides {
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
sandbox_policy: Some(sandbox_policy),
|
||||
permission_profile,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let request = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::RequestUserInput(request) => Some(request.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(request.call_id, request_user_input_call_id);
|
||||
let (reply, outcome) = oneshot::channel();
|
||||
test.codex
|
||||
.submit(Op::TurnSettings {
|
||||
turn_id: request.turn_id.clone(),
|
||||
update: TurnSettingsUpdate {
|
||||
model: Some(model_b.to_string()),
|
||||
effort: Some(Some(ReasoningEffort::High)),
|
||||
..Default::default()
|
||||
},
|
||||
reply,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(outcome.await?, TurnSettingsUpdateOutcome::Applied);
|
||||
test.codex
|
||||
.submit(Op::UserInputAnswer {
|
||||
id: request.turn_id,
|
||||
response: RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
"continue".to_string(),
|
||||
RequestUserInputAnswer {
|
||||
answers: vec!["Yes (Recommended)".to_string()],
|
||||
},
|
||||
)]),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
assert_eq!(responses.requests().len(), 3);
|
||||
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
|
||||
assert_eq!(hook_inputs.len(), 1);
|
||||
assert_eq!(hook_inputs[0]["model"], model_b);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_tool_use_records_additional_context_for_exec_command() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_core::TurnInputRequest;
|
||||
use codex_core::config::Config;
|
||||
use codex_features::Feature;
|
||||
use codex_history::RolloutItem;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_protocol::approvals::ElicitationRequest;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
@@ -17,12 +18,16 @@ use codex_protocol::items::McpToolCallStatus;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::ElicitationAction;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
use codex_protocol::protocol::TurnSettingsUpdate;
|
||||
use codex_protocol::protocol::TurnSettingsUpdateOutcome;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
@@ -325,6 +330,65 @@ async fn submit_user_turn(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn attribution_models(model_slugs: [&str; 2]) -> Vec<codex_protocol::openai_models::ModelInfo> {
|
||||
let base_model = bundled_models_response()
|
||||
.expect("bundled models should parse")
|
||||
.models
|
||||
.into_iter()
|
||||
.find(|model| model.slug == "gpt-5.4")
|
||||
.expect("bundled gpt-5.4 model");
|
||||
model_slugs
|
||||
.into_iter()
|
||||
.map(|slug| {
|
||||
let mut model = base_model.clone();
|
||||
model.slug = slug.to_string();
|
||||
model
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn apply_turn_attribution_update(
|
||||
test: &TestCodex,
|
||||
request_user_input_call_id: &str,
|
||||
model: &str,
|
||||
) -> Result<()> {
|
||||
let request = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::RequestUserInput(request) => Some(request.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(request.call_id, request_user_input_call_id);
|
||||
|
||||
let (reply, outcome) = tokio::sync::oneshot::channel();
|
||||
test.codex
|
||||
.submit(Op::TurnSettings {
|
||||
turn_id: request.turn_id.clone(),
|
||||
update: TurnSettingsUpdate {
|
||||
model: Some(model.to_string()),
|
||||
effort: Some(Some(ReasoningEffort::High)),
|
||||
..Default::default()
|
||||
},
|
||||
reply,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(outcome.await?, TurnSettingsUpdateOutcome::Applied);
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInputAnswer {
|
||||
id: request.turn_id,
|
||||
response: RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
"confirm_path".to_string(),
|
||||
RequestUserInputAnswer {
|
||||
answers: vec!["Yes (Recommended)".to_string()],
|
||||
},
|
||||
)]),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_mcp_tool_call_item(
|
||||
test: &TestCodex,
|
||||
call_id: &str,
|
||||
@@ -1311,7 +1375,7 @@ async fn apps_default_writes_prompts_for_writes_but_not_reads() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn mcp_tool_call_metadata_records_prior_request_user_input_tool() -> Result<()> {
|
||||
async fn mcp_tool_call_metadata_uses_captured_step_after_request_user_input() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -1368,16 +1432,26 @@ async fn mcp_tool_call_metadata_records_prior_request_user_input_tool() -> Resul
|
||||
)
|
||||
.await;
|
||||
|
||||
let model_a = "mcp-metadata-a";
|
||||
let model_b = "mcp-metadata-b";
|
||||
let models = attribution_models([model_a, model_b]);
|
||||
let mut builder = search_capable_apps_builder(apps_server.chatgpt_base_url.clone())
|
||||
.with_config(|config| {
|
||||
.with_model(model_a)
|
||||
.with_config(move |config| {
|
||||
set_calendar_approval_mode(config, AppToolApproval::Approve);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::StepModelSwitching)
|
||||
.expect("test config should allow feature update");
|
||||
config.model_catalog = Some(ModelsResponse { models });
|
||||
config.model_reasoning_effort = Some(ReasoningEffort::Low);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
submit_user_turn(
|
||||
&test,
|
||||
"Ask for confirmation, then create a calendar event.",
|
||||
AskForApproval::Never,
|
||||
AskForApproval::OnRequest,
|
||||
PermissionProfile::Disabled,
|
||||
Some(CollaborationMode {
|
||||
mode: ModeKind::Plan,
|
||||
@@ -1390,26 +1464,7 @@ async fn mcp_tool_call_metadata_records_prior_request_user_input_tool() -> Resul
|
||||
)
|
||||
.await?;
|
||||
|
||||
let request = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::RequestUserInput(request) => Some(request.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(request.call_id, request_user_input_call_id);
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInputAnswer {
|
||||
id: request.turn_id,
|
||||
response: RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
"confirm_path".to_string(),
|
||||
RequestUserInputAnswer {
|
||||
answers: vec!["Yes (Recommended)".to_string()],
|
||||
},
|
||||
)]),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
apply_turn_attribution_update(&test, request_user_input_call_id, model_b).await?;
|
||||
|
||||
let EventMsg::McpToolCallBegin(begin) = wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::McpToolCallBegin(_))
|
||||
@@ -1433,6 +1488,14 @@ async fn mcp_tool_call_metadata_records_prior_request_user_input_tool() -> Resul
|
||||
.pointer("/params/_meta/x-codex-turn-metadata/user_input_requested_during_turn"),
|
||||
Some(&json!(true))
|
||||
);
|
||||
assert_eq!(
|
||||
apps_tool_call.pointer("/params/_meta/x-codex-turn-metadata/model"),
|
||||
Some(&json!(model_b))
|
||||
);
|
||||
assert_eq!(
|
||||
apps_tool_call.pointer("/params/_meta/x-codex-turn-metadata/reasoning_effort"),
|
||||
Some(&json!("high"))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -169,14 +169,6 @@ fn direct_tool_settings_test() -> TestCodexBuilder {
|
||||
})
|
||||
}
|
||||
|
||||
fn advertises_apply_patch(request: &Value) -> bool {
|
||||
request["tools"]
|
||||
.as_array()
|
||||
.expect("provider tools")
|
||||
.iter()
|
||||
.any(|tool| tool["type"] == "custom" && tool["name"] == "apply_patch")
|
||||
}
|
||||
|
||||
fn paused_response(response_id: &str, call_id: &str) -> String {
|
||||
sse(vec![
|
||||
ev_response_created(response_id),
|
||||
@@ -288,6 +280,15 @@ fn request_turn_id(request: &ResponsesRequest) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn request_turn_metadata(request: &ResponsesRequest) -> Value {
|
||||
serde_json::from_str(
|
||||
request.body_json()["client_metadata"]["x-codex-turn-metadata"]
|
||||
.as_str()
|
||||
.expect("request should include turn metadata"),
|
||||
)
|
||||
.expect("valid turn metadata")
|
||||
}
|
||||
|
||||
// Dynamic tools return the original payload, so handler truncation cannot hide a recorder bug.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tool_result_history_keeps_originating_model_across_switch_and_replay() -> Result<()> {
|
||||
@@ -1569,7 +1570,9 @@ async fn captured_model_replans_tools_and_retains_the_issuing_request_router() -
|
||||
for feature in [Feature::ShellTool, Feature::UnifiedExec] {
|
||||
config.features.enable(feature).expect("enable shell tools");
|
||||
}
|
||||
config.tool_registry.turn_metadata_includes_tool_info = true;
|
||||
for model in &mut config.model_catalog.as_mut().expect("models").models {
|
||||
model.use_responses_lite = true;
|
||||
model.shell_type = if model.slug == MODEL_B {
|
||||
ConfigShellToolType::Disabled
|
||||
} else {
|
||||
@@ -1635,7 +1638,35 @@ async fn captured_model_replans_tools_and_retains_the_issuing_request_router() -
|
||||
assert_eq!(
|
||||
requests
|
||||
.iter()
|
||||
.map(advertises_apply_patch)
|
||||
.map(|request| {
|
||||
core_test_support::responses::namespace_child_tool(
|
||||
&request["input"][0],
|
||||
"functions",
|
||||
"apply_patch",
|
||||
)
|
||||
.is_some()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
vec![false, true, false],
|
||||
);
|
||||
let metadata = requests
|
||||
.iter()
|
||||
.map(|request| {
|
||||
serde_json::from_str::<Value>(
|
||||
request["client_metadata"]["x-codex-turn-metadata"]
|
||||
.as_str()
|
||||
.expect("request metadata"),
|
||||
)
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
assert_eq!(
|
||||
metadata
|
||||
.iter()
|
||||
.map(|metadata| {
|
||||
metadata["tool_namespaces_info"]["functions"]["functions"]
|
||||
.get("apply_patch")
|
||||
.is_some()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
vec![false, true, false],
|
||||
);
|
||||
@@ -1643,12 +1674,16 @@ async fn captured_model_replans_tools_and_retains_the_issuing_request_router() -
|
||||
requests
|
||||
.iter()
|
||||
.map(|request| {
|
||||
request["tools"]
|
||||
.as_array()
|
||||
.expect("provider tools")
|
||||
.iter()
|
||||
.filter_map(|tool| tool["name"].as_str())
|
||||
.filter(|name| matches!(*name, "exec_command" | "write_stdin"))
|
||||
["exec_command", "write_stdin"]
|
||||
.into_iter()
|
||||
.filter(|name| {
|
||||
core_test_support::responses::namespace_child_tool(
|
||||
&request["input"][0],
|
||||
"functions",
|
||||
name,
|
||||
)
|
||||
.is_some()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -1779,7 +1814,7 @@ async fn captured_model_enables_and_executes_code_mode() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn response_metadata_uses_the_captured_model_after_a_turn_update() -> Result<()> {
|
||||
async fn response_metadata_uses_the_captured_step_after_a_turn_update() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -1800,15 +1835,19 @@ async fn response_metadata_uses_the_captured_model_after_a_turn_update() -> Resu
|
||||
.await;
|
||||
let test = step_settings_test().build_with_auto_env(&server).await?;
|
||||
let request = start_paused_turn(&test.codex).await?;
|
||||
apply_turn_settings(
|
||||
&test.codex,
|
||||
&request.turn_id,
|
||||
TurnSettingsUpdate {
|
||||
model: Some(MODEL_B.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
submit_turn_settings(
|
||||
&test.codex,
|
||||
&request.turn_id,
|
||||
TurnSettingsUpdate {
|
||||
model: Some(MODEL_B.to_string()),
|
||||
effort: Some(Some(ReasoningEffort::High)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
TurnSettingsUpdateOutcome::Applied
|
||||
);
|
||||
answer_paused_turn(&test.codex, &request.turn_id).await?;
|
||||
|
||||
let mut reroutes = Vec::new();
|
||||
@@ -1836,6 +1875,29 @@ async fn response_metadata_uses_the_captured_model_after_a_turn_update() -> Resu
|
||||
.collect::<Vec<_>>(),
|
||||
vec![json!(MODEL_A), json!(MODEL_B)]
|
||||
);
|
||||
assert_eq!(
|
||||
response_mock
|
||||
.requests()
|
||||
.iter()
|
||||
.map(request_turn_metadata)
|
||||
.map(|metadata| {
|
||||
json!({
|
||||
"model": metadata["model"],
|
||||
"reasoning_effort": metadata["reasoning_effort"],
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
json!({
|
||||
"model": MODEL_A,
|
||||
"reasoning_effort": "low",
|
||||
}),
|
||||
json!({
|
||||
"model": MODEL_B,
|
||||
"reasoning_effort": "high",
|
||||
}),
|
||||
]
|
||||
);
|
||||
// B's matching response header is not a reroute from the turn's initial A.
|
||||
// Buffering metadata likewise belongs to the captured B step.
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user