diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 4ad4337142..c706cc5d58 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12967,6 +12967,22 @@ "string", "null" ] + }, + "modelReasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 3e0ac49d39..2858ac7fe9 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -9371,6 +9371,22 @@ "string", "null" ] + }, + "modelReasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 57fc61adf0..4e35f7aead 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -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" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts index 42be7a4100..fed8b25eaa 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts @@ -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, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index f54fc473f2..f615b25d44 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -405,6 +405,8 @@ pub struct ModelsRequirements { #[ts(export_to = "v2/")] pub struct NewThreadModelDefaults { pub model: Option, + pub model_reasoning_effort: Option, + pub service_tier: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index fb50109408..e9dcf33795 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -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 diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 199b4e06ee..b6dbb30b08 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -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] diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index 6eaa1bc029..07b8ccc2e8 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -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(()) } diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 09b7c5b565..d454ac743a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -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(()) } diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index b071eb3ead..2292c0a0a6 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -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, + pub model_reasoning_effort: Option, + pub service_tier: Option, } 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; diff --git a/codex-rs/config/src/requirements_layers/stack_tests.rs b/codex-rs/config/src/requirements_layers/stack_tests.rs index bd2cc25f97..92e6bd8b46 100644 --- a/codex-rs/config/src/requirements_layers/stack_tests.rs +++ b/codex-rs/config/src/requirements_layers/stack_tests.rs @@ -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" "# ) ); diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index b7a423523b..52a3a380c5 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -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 { diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index c9fff6c679..767e99147f 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -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)); } }