mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
codex: select multi-agent version from model info
This commit is contained in:
@@ -54,6 +54,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ async fn models_client_hits_models_endpoint() {
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
@@ -428,11 +428,20 @@ pub(crate) struct CodexSpawnArgs {
|
||||
fn resolve_multi_agent_version(
|
||||
conversation_history: &InitialHistory,
|
||||
inherited_multi_agent_version: Option<MultiAgentVersion>,
|
||||
session_source: &SessionSource,
|
||||
model_info: &ModelInfo,
|
||||
config: &Config,
|
||||
) -> Option<MultiAgentVersion> {
|
||||
conversation_history
|
||||
.get_multi_agent_version()
|
||||
.or(inherited_multi_agent_version)
|
||||
.or_else(|| {
|
||||
if session_source.is_non_root_agent() {
|
||||
None
|
||||
} else {
|
||||
model_info.multi_agent_version
|
||||
}
|
||||
})
|
||||
.or_else(|| config.multi_agent_version_from_features())
|
||||
}
|
||||
|
||||
@@ -554,6 +563,8 @@ impl Codex {
|
||||
let multi_agent_version = resolve_multi_agent_version(
|
||||
&conversation_history,
|
||||
inherited_multi_agent_version,
|
||||
&session_source,
|
||||
&model_info,
|
||||
&config,
|
||||
);
|
||||
let _ = config
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_core::config::Config;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -11,15 +12,18 @@ use codex_protocol::openai_models::ModelVisibility;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ToolMode;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_models_once;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::mount_sse_once_match;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::submit_thread_settings;
|
||||
@@ -27,9 +31,18 @@ use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::io::Cursor;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
use tokio::time::sleep;
|
||||
use wiremock::Request;
|
||||
|
||||
const CHILD_PROMPT: &str = "inspect the child runtime";
|
||||
const CHILD_MODEL: &str = "test-multi-agent-child";
|
||||
const ROOT_MODEL: &str = "test-multi-agent-root";
|
||||
const ROOT_PROMPT: &str = "spawn a child";
|
||||
const SPAWN_CALL_ID: &str = "spawn-call-1";
|
||||
|
||||
fn remote_model(slug: &str) -> ModelInfo {
|
||||
ModelInfo {
|
||||
@@ -39,6 +52,26 @@ fn remote_model(slug: &str) -> ModelInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn body_contains(req: &Request, text: &str) -> bool {
|
||||
let is_zstd = req
|
||||
.headers
|
||||
.get("content-encoding")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| {
|
||||
value
|
||||
.split(',')
|
||||
.any(|entry| entry.trim().eq_ignore_ascii_case("zstd"))
|
||||
});
|
||||
let bytes = if is_zstd {
|
||||
zstd::stream::decode_all(Cursor::new(&req.body)).ok()
|
||||
} else {
|
||||
Some(req.body.clone())
|
||||
};
|
||||
bytes
|
||||
.and_then(|body| String::from_utf8(body).ok())
|
||||
.is_some_and(|body| body.contains(text))
|
||||
}
|
||||
|
||||
fn tool_names(body: &Value) -> Vec<String> {
|
||||
body.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
@@ -171,3 +204,114 @@ async fn remote_tool_mode_selector_overrides_feature_flags() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_multi_agent_selector_overrides_features_and_child_model_info() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = wiremock::MockServer::start().await;
|
||||
let mut root_model = remote_model(ROOT_MODEL);
|
||||
root_model.multi_agent_version = Some(MultiAgentVersion::V2);
|
||||
let mut child_model = remote_model(CHILD_MODEL);
|
||||
child_model.multi_agent_version = Some(MultiAgentVersion::V1);
|
||||
let models_mock = mount_models_once(
|
||||
&server,
|
||||
ModelsResponse {
|
||||
models: vec![root_model, child_model],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let spawn_args = serde_json::to_string(&json!({
|
||||
"message": CHILD_PROMPT,
|
||||
"task_name": "worker",
|
||||
"model": CHILD_MODEL,
|
||||
"fork_turns": "none",
|
||||
}))?;
|
||||
mount_sse_once_match(
|
||||
&server,
|
||||
|req: &Request| body_contains(req, ROOT_PROMPT),
|
||||
sse(vec![
|
||||
ev_response_created("resp-root-1"),
|
||||
ev_function_call(SPAWN_CALL_ID, "spawn_agent", &spawn_args),
|
||||
ev_completed("resp-root-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
mount_sse_once_match(
|
||||
&server,
|
||||
|req: &Request| body_contains(req, CHILD_PROMPT) && !body_contains(req, SPAWN_CALL_ID),
|
||||
sse(vec![
|
||||
ev_response_created("resp-child-1"),
|
||||
ev_assistant_message("msg-child-1", "child done"),
|
||||
ev_completed("resp-child-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let root_followup_mock = mount_sse_once_match(
|
||||
&server,
|
||||
|req: &Request| body_contains(req, SPAWN_CALL_ID),
|
||||
sse(vec![
|
||||
ev_response_created("resp-root-2"),
|
||||
ev_assistant_message("msg-root-2", "root done"),
|
||||
ev_completed("resp-root-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Collab)
|
||||
.expect("test config should allow feature update");
|
||||
config.model = Some(ROOT_MODEL.to_string());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
assert_eq!(
|
||||
(
|
||||
models_mock.requests().len(),
|
||||
test.codex.multi_agent_version(),
|
||||
),
|
||||
(1, Some(MultiAgentVersion::V2))
|
||||
);
|
||||
test.submit_turn(ROOT_PROMPT).await?;
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
let child_id = loop {
|
||||
if let Some(child_id) = test
|
||||
.thread_manager
|
||||
.list_thread_ids()
|
||||
.await
|
||||
.into_iter()
|
||||
.find(|thread_id| *thread_id != test.session_configured.thread_id)
|
||||
{
|
||||
break child_id;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
bail!(
|
||||
"timed out waiting for spawn_agent to create a child thread: root lock {:?}, spawn output {:?}",
|
||||
test.codex.multi_agent_version(),
|
||||
root_followup_mock.function_call_output_text(SPAWN_CALL_ID),
|
||||
);
|
||||
}
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
};
|
||||
let child = test.thread_manager.get_thread(child_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
models_mock.requests().len(),
|
||||
test.codex.multi_agent_version(),
|
||||
child.config_snapshot().await.model,
|
||||
child.multi_agent_version(),
|
||||
),
|
||||
(
|
||||
1,
|
||||
Some(MultiAgentVersion::V2),
|
||||
CHILD_MODEL.to_string(),
|
||||
Some(MultiAgentVersion::V2),
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ fn test_model_info(
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
@@ -933,6 +934,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
|
||||
@@ -372,5 +372,6 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +594,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
};
|
||||
|
||||
let _models_mock = mount_models_once(
|
||||
@@ -706,6 +707,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
};
|
||||
|
||||
let _models_mock = mount_models_once(
|
||||
|
||||
@@ -479,6 +479,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
@@ -730,6 +731,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
@@ -1215,6 +1217,7 @@ fn test_remote_model_with_policy(
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
|
||||
@@ -1351,6 +1351,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -62,6 +62,7 @@ fn test_model_info(
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers,
|
||||
|
||||
@@ -1357,6 +1357,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
|
||||
@@ -101,6 +101,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::config_types::ReasoningSummary;
|
||||
use crate::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use crate::config_types::ServiceTier;
|
||||
use crate::config_types::Verbosity;
|
||||
use crate::protocol::MultiAgentVersion;
|
||||
|
||||
const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
|
||||
pub const SPEED_TIER_FAST: &str = "fast";
|
||||
@@ -347,6 +348,12 @@ pub struct ModelInfo {
|
||||
deserialize_with = "deserialize_optional_model_selector"
|
||||
)]
|
||||
pub tool_mode: Option<ToolMode>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_optional_model_selector"
|
||||
)]
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
}
|
||||
|
||||
impl ModelInfo {
|
||||
@@ -643,6 +650,7 @@ mod tests {
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,6 +909,22 @@ mod tests {
|
||||
assert!(!object.contains_key("tool_mode"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_info_treats_unknown_multi_agent_version_as_omitted() {
|
||||
let mut value =
|
||||
serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");
|
||||
let object = value
|
||||
.as_object_mut()
|
||||
.expect("model info should be an object");
|
||||
object.insert(
|
||||
"multi_agent_version".to_string(),
|
||||
serde_json::Value::String("future_multi_agent_version".to_string()),
|
||||
);
|
||||
let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");
|
||||
|
||||
assert_eq!(model.multi_agent_version, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_context_window_prefers_context_window() {
|
||||
let model = ModelInfo {
|
||||
|
||||
@@ -46,6 +46,7 @@ fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
|
||||
supports_search_tool: false,
|
||||
auto_review_model_override: None,
|
||||
tool_mode: None,
|
||||
multi_agent_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user