Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions codex-rs/core/src/tools/handlers/multi_agents/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ async fn handle_spawn_agent(
.await
.map_err(FunctionCallError::RespondToModel)?;
}
apply_spawn_agent_service_tier(
&session,
&mut config,
turn.config.service_tier.as_deref(),
args.service_tier.as_deref(),
)
.await?;
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
apply_spawn_agent_overrides(&mut config, child_depth);

Expand Down Expand Up @@ -206,6 +213,7 @@ struct SpawnAgentArgs {
agent_type: Option<String>,
model: Option<String>,
reasoning_effort: Option<ReasoningEffort>,
service_tier: Option<String>,
#[serde(default)]
fork_context: bool,
}
Expand Down
45 changes: 45 additions & 0 deletions codex-rs/core/src/tools/handlers/multi_agents_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,51 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
Ok(())
}

pub(crate) async fn apply_spawn_agent_service_tier(
session: &Session,
config: &mut Config,
parent_service_tier: Option<&str>,
requested_service_tier: Option<&str>,
) -> Result<(), FunctionCallError> {
let Some(candidate_service_tier) = requested_service_tier.or(parent_service_tier) else {
return Ok(());
};
let model = config.model.clone().ok_or_else(|| {
FunctionCallError::RespondToModel(
"spawn_agent could not resolve the child model for service tier validation".to_string(),
)
})?;
let model_info = session
.services
.models_manager
.get_model_info(model.as_str(), &config.to_models_manager_config())
.await;

if model_info.supports_service_tier(candidate_service_tier) {
config.service_tier = Some(candidate_service_tier.to_string());
return Ok(());
}

if requested_service_tier.is_none() {
config.service_tier = None;
return Ok(());
}

let supported_service_tiers = if model_info.service_tiers.is_empty() {
"none".to_string()
} else {
model_info
.service_tiers
.iter()
.map(|tier| tier.id.as_str())
.collect::<Vec<_>>()
.join(", ")
};
Err(FunctionCallError::RespondToModel(format!(
"Service tier `{candidate_service_tier}` is not supported for model `{model}`. Supported service tiers: {supported_service_tiers}"
)))
}

fn find_spawn_agent_model_name(
available_models: &[codex_protocol::openai_models::ModelPreset],
requested_model: &str,
Expand Down
29 changes: 27 additions & 2 deletions codex-rs/core/src/tools/handlers/multi_agents_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::collections::BTreeMap;

const SPAWN_AGENT_INHERITED_MODEL_GUIDANCE: &str = "Spawned agents inherit your current model by default. Omit `model` to use that preferred default; set `model` only when an explicit override is needed.";
const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str = "Optional model override for the new agent. Leave unset to inherit the same model as the parent, which is the preferred default. Only set this when the user explicitly asks for a different model or the task clearly requires one.";
const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str = "Optional service tier override for the new agent. Leave unset unless the user explicitly asks for one.";

#[derive(Debug, Clone, Default)]
pub struct SpawnAgentToolOptions {
Expand Down Expand Up @@ -545,6 +546,12 @@ fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap<St
.to_string(),
)),
),
(
"service_tier".to_string(),
JsonSchema::string(Some(
SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION.to_string(),
)),
),
])
}

Expand Down Expand Up @@ -578,13 +585,20 @@ fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<St
.to_string(),
)),
),
(
"service_tier".to_string(),
JsonSchema::string(Some(
SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION.to_string(),
)),
),
])
}

fn hide_spawn_agent_metadata_options(properties: &mut BTreeMap<String, JsonSchema>) {
properties.remove("agent_type");
properties.remove("model");
properties.remove("reasoning_effort");
properties.remove("service_tier");
}

fn spawn_agent_tool_description(
Expand Down Expand Up @@ -712,13 +726,24 @@ fn spawn_agent_models_description(models: &[ModelPreset]) -> String {
.map(|preset| format!("{} ({})", preset.effort, preset.description))
.collect::<Vec<_>>()
.join(", ");
let service_tiers = if model.service_tiers.is_empty() {
"none".to_string()
} else {
model
.service_tiers
.iter()
.map(|tier| format!("{} ({}: {})", tier.id, tier.name, tier.description))
.collect::<Vec<_>>()
.join(", ")
};
format!(
"- {} (`{}`): {} Default reasoning effort: {}. Supported reasoning efforts: {}.",
"- {} (`{}`): {} Default reasoning effort: {}. Supported reasoning efforts: {}. Supported service tiers: {}.",
model.display_name,
model.model,
model.description,
model.default_reasoning_effort,
efforts
efforts,
service_tiers
)
})
.collect::<Vec<_>>()
Expand Down
48 changes: 47 additions & 1 deletion codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::*;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_tools::JsonSchemaPrimitiveType;
Expand All @@ -20,7 +21,11 @@ fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset {
}],
supports_personality: false,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
service_tiers: vec![ModelServiceTier {
id: "priority".to_string(),
name: "Fast".to_string(),
description: "1.5x speed, increased usage".to_string(),
}],
is_default: false,
upgrade: None,
show_in_picker,
Expand Down Expand Up @@ -70,6 +75,10 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
.contains("Available model overrides (optional; inherited parent model is preferred):")
);
assert!(description.contains("visible display (`visible-model`)"));
assert!(
description
.contains("Supported service tiers: priority (Fast: 1.5x speed, increased usage).")
);
assert!(!description.contains("hidden display (`hidden-model`)"));
assert!(properties.contains_key("task_name"));
assert!(properties.contains_key("message"));
Expand All @@ -86,6 +95,12 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
.and_then(|schema| schema.description.as_deref()),
Some(SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION)
);
assert_eq!(
properties
.get("service_tier")
.and_then(|schema| schema.description.as_deref()),
Some(SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION)
);
assert_eq!(
parameters.required.as_ref(),
Some(&vec!["task_name".to_string(), "message".to_string()])
Expand Down Expand Up @@ -127,6 +142,37 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
.and_then(|schema| schema.description.as_deref()),
Some(SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION)
);
assert_eq!(
properties
.get("service_tier")
.and_then(|schema| schema.description.as_deref()),
Some(SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION)
);
}

#[test]
fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
available_models: vec![model_preset("visible", /*show_in_picker*/ true)],
agent_type_description: "role help".to_string(),
hide_agent_type_model_reasoning: true,
include_usage_hint: true,
usage_hint_text: None,
max_concurrent_threads_per_session: Some(4),
});

let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else {
panic!("spawn_agent should be a function tool");
};
let properties = parameters
.properties
.as_ref()
.expect("spawn_agent should use object params");

assert!(!properties.contains_key("agent_type"));
assert!(!properties.contains_key("model"));
assert!(!properties.contains_key("reasoning_effort"));
assert!(!properties.contains_key("service_tier"));
}

#[test]
Expand Down
Loading
Loading