Add managed new-thread model settings

This commit is contained in:
hefuc-oai
2026-06-24 15:47:38 -07:00
parent 4273557a97
commit 0d7a89e4b0
13 changed files with 151 additions and 29 deletions
@@ -12967,6 +12967,22 @@
"string",
"null"
]
},
"modelReasoningEffort": {
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningEffort"
},
{
"type": "null"
}
]
},
"serviceTier": {
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -9371,6 +9371,22 @@
"string",
"null"
]
},
"modelReasoningEffort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"serviceTier": {
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -516,10 +516,31 @@
"string",
"null"
]
},
"modelReasoningEffort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"serviceTier": {
"type": [
"string",
"null"
]
}
},
"type": "object"
},
"ReasoningEffort": {
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ResidencyRequirement": {
"enum": [
"us"
@@ -1,5 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ReasoningEffort } from "../ReasoningEffort";
export type NewThreadModelDefaults = { model: string | null, };
export type NewThreadModelDefaults = { model: string | null, modelReasoningEffort: ReasoningEffort | null, serviceTier: string | null, };
@@ -405,6 +405,8 @@ pub struct ModelsRequirements {
#[ts(export_to = "v2/")]
pub struct NewThreadModelDefaults {
pub model: Option<String>,
pub model_reasoning_effort: Option<ReasoningEffort>,
pub service_tier: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
+1 -1
View File
@@ -242,7 +242,7 @@ Example with notification opt-out:
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any `details` returned by detect. Callers may pass `source` to identify the product that initiated the import; omitted or `null` means unspecified. The response acknowledges the synchronous import phase with an `importId`. Expected migration failures are reported as per-item failures rather than JSON-RPC errors, so the server still returns that `importId` and emits `externalAgentConfig/import/completed` with the same ID once all synchronous and background work finishes. The completion notification contains type-level `itemTypeResults` with successes and failures, including raw failure messages for the client to report separately.
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, `models.newThread.model`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
### Example: Start or resume a thread
@@ -380,6 +380,8 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
models: requirements.models.map(|models| ModelsRequirements {
new_thread: models.new_thread.map(|new_thread| NewThreadModelDefaults {
model: new_thread.model,
model_reasoning_effort: new_thread.model_reasoning_effort,
service_tier: new_thread.service_tier,
}),
}),
}
@@ -578,6 +580,7 @@ mod tests {
use codex_config::ModelsRequirementsToml;
use codex_config::NewThreadModelDefaultsToml;
use codex_config::WindowsRequirementsToml;
use codex_protocol::openai_models::ReasoningEffort;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
@@ -638,23 +641,28 @@ mod tests {
}
#[test]
fn requirements_api_includes_new_thread_model() {
fn requirements_api_includes_new_thread_model_defaults() {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
models: Some(ModelsRequirementsToml {
new_thread: Some(NewThreadModelDefaultsToml {
model: Some("gpt-managed".to_string()),
model_reasoning_effort: Some(ReasoningEffort::Medium),
service_tier: Some("fast".to_string()),
}),
}),
..ConfigRequirementsToml::default()
});
let defaults = mapped
.models
.and_then(|models| models.new_thread)
.expect("new-thread defaults");
assert_eq!(defaults.model.as_deref(), Some("gpt-managed"));
assert_eq!(
mapped
.models
.and_then(|models| models.new_thread)
.and_then(|new_thread| new_thread.model),
Some("gpt-managed".to_string())
defaults.model_reasoning_effort,
Some(ReasoningEffort::Medium)
);
assert_eq!(defaults.service_tier.as_deref(), Some("fast"));
}
#[test]
@@ -76,13 +76,15 @@ async fn config_requirements_read_includes_allow_remote_control() -> Result<()>
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_includes_new_thread_model() -> Result<()> {
async fn config_requirements_read_includes_new_thread_model_defaults() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("requirements.toml"),
r#"
[models.new_thread]
model = "gpt-managed"
model_reasoning_effort = "medium"
service_tier = "fast"
"#,
)?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
@@ -96,15 +98,17 @@ model = "gpt-managed"
.await??;
let response: ConfigRequirementsReadResponse = to_response(response)?;
let defaults = response
.requirements
.and_then(|requirements| requirements.models)
.and_then(|models| models.new_thread)
.expect("managed new-thread defaults");
assert_eq!(defaults.model.as_deref(), Some("gpt-managed"));
assert_eq!(
response
.requirements
.and_then(|requirements| requirements.models)
.and_then(|models| models.new_thread)
.and_then(|new_thread| new_thread.model)
.as_deref(),
Some("gpt-managed")
defaults.model_reasoning_effort,
Some(ReasoningEffort::Medium)
);
assert_eq!(defaults.service_tier.as_deref(), Some("fast"));
Ok(())
}
@@ -27,6 +27,7 @@ use codex_app_server_protocol::UserInput as V2UserInput;
use codex_config::loader::project_trust_key;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::config::set_project_trust_level;
use codex_core::test_support::all_model_presets;
use codex_exec_server::LOCAL_FS;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
@@ -200,16 +201,30 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
}
#[tokio::test]
async fn thread_start_uses_managed_new_thread_model() -> Result<()> {
async fn thread_start_uses_managed_new_thread_model_defaults() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
let managed_model = all_model_presets()
.iter()
.find(|preset| {
preset
.service_tiers
.iter()
.any(|tier| tier.id == "priority")
})
.context("expected a model with the fast service tier")?;
std::fs::write(
codex_home.path().join("requirements.toml"),
r#"
format!(
r#"
[models.new_thread]
model = "gpt-managed"
model = "{}"
model_reasoning_effort = "medium"
service_tier = "fast"
"#,
managed_model.id
),
)?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
@@ -228,7 +243,9 @@ model = "gpt-managed"
.await??;
let response: ThreadStartResponse = to_response(response)?;
assert_eq!(response.model, "gpt-managed");
assert_eq!(response.model, managed_model.id);
assert_eq!(response.reasoning_effort, Some(ReasoningEffort::Medium));
assert_eq!(response.service_tier.as_deref(), Some("priority"));
Ok(())
}
+11 -2
View File
@@ -2,6 +2,7 @@ use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
@@ -907,11 +908,13 @@ impl ModelsRequirementsToml {
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct NewThreadModelDefaultsToml {
pub model: Option<String>,
pub model_reasoning_effort: Option<ReasoningEffort>,
pub service_tier: Option<String>,
}
impl NewThreadModelDefaultsToml {
fn is_empty(&self) -> bool {
self.model.is_none()
self.model.is_none() && self.model_reasoning_effort.is_none() && self.service_tier.is_none()
}
}
@@ -1831,11 +1834,13 @@ mod tests {
}
#[test]
fn deserialize_new_thread_model_default() -> Result<()> {
fn deserialize_new_thread_model_defaults() -> Result<()> {
let requirements: ConfigRequirementsToml = from_str(
r#"
[models.new_thread]
model = "managed-model"
model_reasoning_effort = "medium"
service_tier = "fast"
"#,
)?;
@@ -1844,6 +1849,8 @@ mod tests {
Some(ModelsRequirementsToml {
new_thread: Some(NewThreadModelDefaultsToml {
model: Some("managed-model".to_string()),
model_reasoning_effort: Some(ReasoningEffort::Medium),
service_tier: Some("fast".to_string()),
}),
})
);
@@ -1876,6 +1883,8 @@ mod tests {
let models = ModelsRequirementsToml {
new_thread: Some(NewThreadModelDefaultsToml {
model: Some("managed-model".to_string()),
model_reasoning_effort: Some(ReasoningEffort::Medium),
service_tier: Some("fast".to_string()),
}),
};
let enforce_residency = ResidencyRequirement::Us;
@@ -110,7 +110,7 @@ allow_remote_control = false
}
#[test]
fn new_thread_model_default_uses_toml_priority() {
fn new_thread_model_defaults_use_toml_priority() {
let composed = compose(vec![
layer(
"req_low",
@@ -118,6 +118,8 @@ fn new_thread_model_default_uses_toml_priority() {
r#"
[models.new_thread]
model = "low-priority-model"
model_reasoning_effort = "low"
service_tier = "flex"
"#,
),
layer(
@@ -126,6 +128,8 @@ model = "low-priority-model"
r#"
[models.new_thread]
model = "high-priority-model"
model_reasoning_effort = "high"
service_tier = "fast"
"#,
),
])
@@ -138,6 +142,8 @@ model = "high-priority-model"
r#"
[models.new_thread]
model = "high-priority-model"
model_reasoning_effort = "high"
service_tier = "fast"
"#
)
);
+11
View File
@@ -41,6 +41,7 @@ use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::openai_models::ModelPreset;
@@ -244,6 +245,16 @@ fn apply_managed_new_thread_defaults(
if let Some(model) = defaults.model.as_ref() {
config.model = Some(model.clone());
}
if let Some(model_reasoning_effort) = defaults.model_reasoning_effort.as_ref() {
config.model_reasoning_effort = Some(model_reasoning_effort.clone());
}
if let Some(service_tier) = defaults.service_tier.as_ref() {
config.service_tier = Some(
ServiceTier::from_request_value(service_tier)
.map(|tier| tier.request_value().to_string())
.unwrap_or_else(|| service_tier.clone()),
);
}
}
pub(crate) struct ResumeThreadWithHistoryOptions {
+17 -6
View File
@@ -16,6 +16,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::InternalSessionSource;
@@ -82,6 +83,8 @@ impl codex_agent_graph_store::AgentGraphStore for FakeAgentGraphStore {
async fn managed_new_thread_defaults_only_apply_to_root_new_threads() {
let mut base_config = test_config().await;
base_config.model = Some("local-model".to_string());
base_config.model_reasoning_effort = Some(ReasoningEffort::Low);
base_config.service_tier = Some("flex".to_string());
base_config.config_layer_stack = codex_config::ConfigLayerStack::new(
Vec::new(),
codex_config::ConfigRequirements::default(),
@@ -89,6 +92,8 @@ async fn managed_new_thread_defaults_only_apply_to_root_new_threads() {
models: Some(codex_config::ModelsRequirementsToml {
new_thread: Some(codex_config::NewThreadModelDefaultsToml {
model: Some("managed-model".to_string()),
model_reasoning_effort: Some(ReasoningEffort::Medium),
service_tier: Some("fast".to_string()),
}),
}),
..Default::default()
@@ -97,28 +102,34 @@ async fn managed_new_thread_defaults_only_apply_to_root_new_threads() {
.expect("build managed requirements stack");
let cases = [
(InitialHistory::New, SessionSource::VSCode, "managed-model"),
(
InitialHistory::New,
SessionSource::VSCode,
("managed-model", ReasoningEffort::Medium, "priority"),
),
(
InitialHistory::Cleared,
SessionSource::VSCode,
"managed-model",
("managed-model", ReasoningEffort::Medium, "priority"),
),
(
InitialHistory::Forked(Vec::new()),
SessionSource::VSCode,
"local-model",
("local-model", ReasoningEffort::Low, "flex"),
),
(
InitialHistory::New,
SessionSource::SubAgent(SubAgentSource::Review),
"local-model",
("local-model", ReasoningEffort::Low, "flex"),
),
];
for (history, source, expected) in cases {
for (history, source, (expected_model, expected_effort, expected_service_tier)) in cases {
let mut config = base_config.clone();
apply_managed_new_thread_defaults(&mut config, &history, &source);
assert_eq!(config.model.as_deref(), Some(expected));
assert_eq!(config.model.as_deref(), Some(expected_model));
assert_eq!(config.model_reasoning_effort, Some(expected_effort));
assert_eq!(config.service_tier.as_deref(), Some(expected_service_tier));
}
}