mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Add managed new-thread model default
This commit is contained in:
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user