Add managed new-thread model default

This commit is contained in:
hefuc-oai
2026-06-24 15:47:02 -07:00
parent 81f340436c
commit 78849ae6dc
8 changed files with 192 additions and 2 deletions
+70 -2
View File
@@ -887,9 +887,34 @@ pub struct ConfigRequirementsToml {
#[serde(rename = "experimental_network")]
pub network: Option<NetworkRequirementsToml>,
pub permissions: Option<PermissionsRequirementsToml>,
pub models: Option<ModelsRequirementsToml>,
pub guardian_policy_config: Option<String>,
}
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct ModelsRequirementsToml {
pub new_thread: Option<NewThreadModelDefaultsToml>,
}
impl ModelsRequirementsToml {
fn is_empty(&self) -> bool {
self.new_thread
.as_ref()
.is_none_or(NewThreadModelDefaultsToml::is_empty)
}
}
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct NewThreadModelDefaultsToml {
pub model: Option<String>,
}
impl NewThreadModelDefaultsToml {
fn is_empty(&self) -> bool {
self.model.is_none()
}
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct RemoteSandboxConfigToml {
pub hostname_patterns: Vec<String>,
@@ -941,6 +966,7 @@ pub struct ConfigRequirementsWithSources {
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
pub network: Option<Sourced<NetworkRequirementsToml>>,
pub permissions: Option<Sourced<PermissionsRequirementsToml>>,
pub models: Option<Sourced<ModelsRequirementsToml>>,
pub guardian_policy_config: Option<Sourced<String>>,
}
@@ -985,6 +1011,7 @@ impl ConfigRequirementsWithSources {
enforce_residency: _,
network: _,
permissions: _,
models: _,
guardian_policy_config: _,
} = &other;
@@ -1021,6 +1048,7 @@ impl ConfigRequirementsWithSources {
enforce_residency,
network,
permissions,
models,
guardian_policy_config,
}
);
@@ -1057,6 +1085,7 @@ impl ConfigRequirementsWithSources {
enforce_residency,
network,
permissions,
models,
guardian_policy_config,
} = self;
ConfigRequirementsToml {
@@ -1082,6 +1111,7 @@ impl ConfigRequirementsWithSources {
enforce_residency: enforce_residency.map(|sourced| sourced.value),
network: network.map(|sourced| sourced.value),
permissions: permissions.map(|sourced| sourced.value),
models: models.map(|sourced| sourced.value),
guardian_policy_config: guardian_policy_config.map(|sourced| sourced.value),
}
}
@@ -1194,6 +1224,10 @@ impl ConfigRequirementsToml {
&& self.enforce_residency.is_none()
&& self.network.is_none()
&& self.permissions.is_none()
&& self
.models
.as_ref()
.is_none_or(ModelsRequirementsToml::is_empty)
&& self
.guardian_policy_config
.as_deref()
@@ -1206,8 +1240,9 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
fn try_from(toml: ConfigRequirementsWithSources) -> Result<Self, Self::Error> {
// Profile catalog selection remains on ConfigRequirementsToml for
// config loading and requirements API projection. The normalized
// constraints below only need the compiled PermissionProfile envelope.
// config loading and requirements API projection. Managed new-thread
// defaults also remain there because they are initialization values,
// not runtime constraints.
let ConfigRequirementsWithSources {
allowed_approval_policies,
allowed_approvals_reviewers,
@@ -1230,6 +1265,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
enforce_residency,
network,
permissions,
models: _,
guardian_policy_config,
} = toml;
@@ -1605,6 +1641,7 @@ mod tests {
enforce_residency,
network,
permissions,
models,
guardian_policy_config,
} = toml;
ConfigRequirementsWithSources {
@@ -1640,6 +1677,7 @@ mod tests {
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
network: network.map(|value| Sourced::new(value, RequirementSource::Unknown)),
permissions: permissions.map(|value| Sourced::new(value, RequirementSource::Unknown)),
models: models.map(|value| Sourced::new(value, RequirementSource::Unknown)),
guardian_policy_config: guardian_policy_config
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
}
@@ -1792,6 +1830,27 @@ mod tests {
Ok(())
}
#[test]
fn deserialize_new_thread_model_default() -> Result<()> {
let requirements: ConfigRequirementsToml = from_str(
r#"
[models.new_thread]
model = "managed-model"
"#,
)?;
assert_eq!(
requirements.models,
Some(ModelsRequirementsToml {
new_thread: Some(NewThreadModelDefaultsToml {
model: Some("managed-model".to_string()),
}),
})
);
assert!(!requirements.is_empty());
Ok(())
}
#[test]
fn merge_unset_fields_copies_every_field_and_sets_sources() {
let mut target = ConfigRequirementsWithSources::default();
@@ -1814,6 +1873,11 @@ mod tests {
let computer_use = ComputerUseRequirementsToml {
allow_locked_computer_use: Some(false),
};
let models = ModelsRequirementsToml {
new_thread: Some(NewThreadModelDefaultsToml {
model: Some("managed-model".to_string()),
}),
};
let enforce_residency = ResidencyRequirement::Us;
let enforce_source = source.clone();
let guardian_policy_config = "Use the company-managed guardian policy.".to_string();
@@ -1843,6 +1907,7 @@ mod tests {
enforce_residency: Some(enforce_residency),
network: None,
permissions: None,
models: Some(models.clone()),
guardian_policy_config: Some(guardian_policy_config.clone()),
};
@@ -1893,6 +1958,7 @@ mod tests {
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
network: None,
permissions: None,
models: Some(Sourced::new(models, source.clone())),
guardian_policy_config: Some(Sourced::new(guardian_policy_config, source)),
}
);
@@ -1940,6 +2006,7 @@ mod tests {
enforce_residency: None,
network: None,
permissions: None,
models: None,
guardian_policy_config: None,
}
);
@@ -1995,6 +2062,7 @@ mod tests {
enforce_residency: None,
network: None,
permissions: None,
models: None,
guardian_policy_config: None,
}
);
+2
View File
@@ -68,12 +68,14 @@ pub use config_requirements::MarketplaceAllowedSourceToml;
pub use config_requirements::MarketplaceRequirementsToml;
pub use config_requirements::McpServerIdentity;
pub use config_requirements::McpServerRequirement;
pub use config_requirements::ModelsRequirementsToml;
pub use config_requirements::NetworkConstraints;
pub use config_requirements::NetworkDomainPermissionToml;
pub use config_requirements::NetworkDomainPermissionsToml;
pub use config_requirements::NetworkRequirementsToml;
pub use config_requirements::NetworkUnixSocketPermissionToml;
pub use config_requirements::NetworkUnixSocketPermissionsToml;
pub use config_requirements::NewThreadModelDefaultsToml;
pub use config_requirements::PluginRequirementsToml;
pub use config_requirements::RemoteSandboxConfigToml;
pub use config_requirements::RequirementSource;
@@ -228,6 +228,7 @@ fn populate_merged_regular_fields_with_sources(
enforce_residency,
network,
permissions,
models,
guardian_policy_config,
} = requirements;
@@ -256,6 +257,7 @@ fn populate_merged_regular_fields_with_sources(
set_sourced!(enforce_residency, &["enforce_residency"]);
set_sourced!(network, &["experimental_network"]);
set_sourced!(permissions, &["permissions"]);
set_sourced!(models, &["models"]);
if let Some(guardian_policy_config) =
guardian_policy_config.filter(|value| !value.trim().is_empty())
@@ -109,6 +109,40 @@ allow_remote_control = false
);
}
#[test]
fn new_thread_model_default_uses_toml_priority() {
let composed = compose(vec![
layer(
"req_low",
"Low",
r#"
[models.new_thread]
model = "low-priority-model"
"#,
),
layer(
"req_high",
"High",
r#"
[models.new_thread]
model = "high-priority-model"
"#,
),
])
.expect("compose requirements")
.expect("requirements present");
assert_eq!(
composed,
expected_requirements(
r#"
[models.new_thread]
model = "high-priority-model"
"#
)
);
}
#[test]
fn composition_strategy_applies_to_non_cloud_layers() {
let mdm_source = RequirementSource::MdmManagedPreferences {
+1
View File
@@ -8686,6 +8686,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
enforce_residency: None,
network: None,
permissions: None,
models: None,
guardian_policy_config: None,
};
let requirement_source = codex_config::RequirementSource::Unknown;
+32
View File
@@ -473,6 +473,24 @@ pub(crate) fn resolve_multi_agent_version(
})
}
fn resolve_initial_model(
configured_model: Option<String>,
managed_new_thread_model: Option<String>,
conversation_history: &InitialHistory,
session_source: &SessionSource,
) -> Option<String> {
if session_source.is_non_root_agent() {
return configured_model;
}
match conversation_history {
InitialHistory::New | InitialHistory::Cleared => {
managed_new_thread_model.or(configured_model)
}
InitialHistory::Resumed(_) | InitialHistory::Forked(_) => configured_model,
}
}
pub(crate) const INITIAL_SUBMIT_ID: &str = "";
pub(crate) const SUBMISSION_CHANNEL_CAPACITY: usize = 512;
const CYBER_VERIFY_URL: &str = "https://chatgpt.com/cyber";
@@ -566,6 +584,20 @@ impl Codex {
)
};
let managed_new_thread_model = config
.config_layer_stack
.requirements_toml()
.models
.as_ref()
.and_then(|models| models.new_thread.as_ref())
.and_then(|defaults| defaults.model.clone());
config.model = resolve_initial_model(
config.model.take(),
managed_new_thread_model,
&conversation_history,
&session_source,
);
let config = Arc::new(config);
let refresh_strategy = if session_source.is_non_root_agent() {
codex_models_manager::manager::RefreshStrategy::Offline
+49
View File
@@ -2050,6 +2050,55 @@ fn resolve_multi_agent_version_handles_unset_and_legacy_history() {
);
}
#[test]
fn managed_new_thread_model_only_overrides_root_new_threads() {
let cases = [
(
InitialHistory::New,
SessionSource::VSCode,
Some("managed-model"),
"managed-model",
),
(
InitialHistory::New,
SessionSource::VSCode,
None,
"local-model",
),
(
InitialHistory::Cleared,
SessionSource::VSCode,
Some("managed-model"),
"managed-model",
),
(
InitialHistory::Forked(Vec::new()),
SessionSource::VSCode,
Some("managed-model"),
"local-model",
),
(
InitialHistory::New,
SessionSource::SubAgent(SubAgentSource::Review),
Some("managed-model"),
"local-model",
),
];
for (history, source, managed_model, expected) in cases {
assert_eq!(
resolve_initial_model(
Some("local-model".to_string()),
managed_model.map(str::to_string),
&history,
&source,
)
.as_deref(),
Some(expected)
);
}
}
#[tokio::test]
async fn record_initial_history_new_defers_initial_context_until_first_turn() {
let (session, _turn_context) = make_session_and_context().await;
+2
View File
@@ -791,6 +791,7 @@ mod tests {
enforce_residency: Some(ResidencyRequirement::Us),
network: None,
permissions: None,
models: None,
};
let user_file = if cfg!(windows) {
@@ -1149,6 +1150,7 @@ approval_policy = "never"
enforce_residency: None,
network: None,
permissions: None,
models: None,
};
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)