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 f1d6a1af8..b972de220 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 @@ -8414,6 +8414,16 @@ "object", "null" ] + }, + "models": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ModelsRequirements" + }, + { + "type": "null" + } + ] } }, "type": "object" @@ -12834,6 +12844,21 @@ "title": "ModelVerificationNotification", "type": "object" }, + "ModelsRequirements": { + "properties": { + "newThread": { + "anyOf": [ + { + "$ref": "#/definitions/v2/NewThreadModelDefaults" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "MultiAgentMode": { "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.", "enum": [ @@ -12981,6 +13006,33 @@ ], "type": "string" }, + "NewThreadModelDefaults": { + "properties": { + "model": { + "type": [ + "string", + "null" + ] + }, + "modelReasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", 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 9983b7925..4af580e9c 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 @@ -4654,6 +4654,16 @@ "object", "null" ] + }, + "models": { + "anyOf": [ + { + "$ref": "#/definitions/ModelsRequirements" + }, + { + "type": "null" + } + ] } }, "type": "object" @@ -9238,6 +9248,21 @@ "title": "ModelVerificationNotification", "type": "object" }, + "ModelsRequirements": { + "properties": { + "newThread": { + "anyOf": [ + { + "$ref": "#/definitions/NewThreadModelDefaults" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "MultiAgentMode": { "description": "Controls whether the model receives multi-agent delegation instructions and, when it does, whether it should only spawn sub-agents after an explicit user request or may delegate proactively when doing so would help. `none` leaves the multi-agent tools available without injecting delegation instructions.", "enum": [ @@ -9385,6 +9410,33 @@ ], "type": "string" }, + "NewThreadModelDefaults": { + "properties": { + "model": { + "type": [ + "string", + "null" + ] + }, + "modelReasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "NonSteerableTurnKind": { "enum": [ "review", 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 3fe6a8978..4e35f7aea 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -169,6 +169,16 @@ "object", "null" ] + }, + "models": { + "anyOf": [ + { + "$ref": "#/definitions/ModelsRequirements" + }, + { + "type": "null" + } + ] } }, "type": "object" @@ -362,6 +372,21 @@ ], "type": "object" }, + "ModelsRequirements": { + "properties": { + "newThread": { + "anyOf": [ + { + "$ref": "#/definitions/NewThreadModelDefaults" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, "NetworkDomainPermission": { "enum": [ "allow", @@ -484,6 +509,38 @@ ], "type": "string" }, + "NewThreadModelDefaults": { + "properties": { + "model": { + "type": [ + "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/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index 3eb9a65aa..e2fd573a3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -4,8 +4,9 @@ import type { WebSearchMode } from "../WebSearchMode"; import type { AskForApproval } from "./AskForApproval"; import type { ComputerUseRequirements } from "./ComputerUseRequirements"; +import type { ModelsRequirements } from "./ModelsRequirements"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, models: ModelsRequirements | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelsRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelsRequirements.ts new file mode 100644 index 000000000..9041fff83 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelsRequirements.ts @@ -0,0 +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 { NewThreadModelDefaults } from "./NewThreadModelDefaults"; + +export type ModelsRequirements = { newThread: NewThreadModelDefaults | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts new file mode 100644 index 000000000..fed8b25ea --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NewThreadModelDefaults.ts @@ -0,0 +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, modelReasoningEffort: ReasoningEffort | null, serviceTier: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 2833e6e52..c405082c4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -267,6 +267,7 @@ export type { ModelServiceTier } from "./ModelServiceTier"; export type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; export type { ModelVerification } from "./ModelVerification"; export type { ModelVerificationNotification } from "./ModelVerificationNotification"; +export type { ModelsRequirements } from "./ModelsRequirements"; export type { NetworkAccess } from "./NetworkAccess"; export type { NetworkApprovalContext } from "./NetworkApprovalContext"; export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; @@ -275,6 +276,7 @@ export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction"; export type { NetworkRequirements } from "./NetworkRequirements"; export type { NetworkUnixSocketPermission } from "./NetworkUnixSocketPermission"; +export type { NewThreadModelDefaults } from "./NewThreadModelDefaults"; export type { NonSteerableTurnKind } from "./NonSteerableTurnKind"; export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; 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 4c9e355b4..f615b25d4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -390,6 +390,23 @@ pub struct ConfigRequirements { pub enforce_residency: Option, #[experimental("configRequirements/read.network")] pub network: Option, + pub models: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelsRequirements { + pub new_thread: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[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-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index ddd2afe8f..ea1e0424d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1753,6 +1753,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() hooks: None, enforce_residency: None, network: None, + models: None, }); assert_eq!(reason, Some("askForApproval.granular")); diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index d291f5c85..78f46f5b1 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`, 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 50bba5400..b6dbb30b0 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -24,9 +24,11 @@ use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ManagedHooksRequirements; use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse; +use codex_app_server_protocol::ModelsRequirements; use codex_app_server_protocol::NetworkDomainPermission; use codex_app_server_protocol::NetworkRequirements; use codex_app_server_protocol::NetworkUnixSocketPermission; +use codex_app_server_protocol::NewThreadModelDefaults; use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::ConfigRequirementsToml; @@ -375,6 +377,13 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .enforce_residency .map(map_residency_requirement_to_api), network: requirements.network.map(map_network_requirements_to_api), + 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, + }), + }), } } @@ -568,7 +577,10 @@ mod tests { use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::ComputerUseRequirementsToml; use codex_config::ConfigRequirementsToml; + 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; @@ -628,6 +640,31 @@ mod tests { assert_eq!(mapped.allow_remote_control, Some(false)); } + #[test] + 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!( + defaults.model_reasoning_effort, + Some(ReasoningEffort::Medium) + ); + assert_eq!(defaults.service_tier.as_deref(), Some("fast")); + } + #[test] fn requirements_api_includes_computer_use_requirements() { let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { 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 81ef145ef..07b8ccc2e 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -75,6 +75,43 @@ async fn config_requirements_read_includes_allow_remote_control() -> Result<()> Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +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?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp.send_config_requirements_read_request().await?; + let response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .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!( + defaults.model_reasoning_effort, + Some(ReasoningEffort::Medium) + ); + assert_eq!(defaults.service_tier.as_deref(), Some("fast")); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn config_read_returns_effective_and_layers() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 199ca4eaf..00af97233 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; @@ -876,9 +877,36 @@ pub struct ConfigRequirementsToml { #[serde(rename = "experimental_network")] pub network: Option, pub permissions: Option, + pub models: Option, pub guardian_policy_config: Option, } +#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct ModelsRequirementsToml { + pub new_thread: Option, +} + +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, + pub model_reasoning_effort: Option, + pub service_tier: Option, +} + +impl NewThreadModelDefaultsToml { + fn is_empty(&self) -> bool { + self.model.is_none() && self.model_reasoning_effort.is_none() && self.service_tier.is_none() + } +} + #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct RemoteSandboxConfigToml { pub hostname_patterns: Vec, @@ -930,6 +958,7 @@ pub struct ConfigRequirementsWithSources { pub enforce_residency: Option>, pub network: Option>, pub permissions: Option>, + pub models: Option>, pub guardian_policy_config: Option>, } @@ -974,6 +1003,7 @@ impl ConfigRequirementsWithSources { enforce_residency: _, network: _, permissions: _, + models: _, guardian_policy_config: _, } = &other; @@ -1010,6 +1040,7 @@ impl ConfigRequirementsWithSources { enforce_residency, network, permissions, + models, guardian_policy_config, } ); @@ -1046,6 +1077,7 @@ impl ConfigRequirementsWithSources { enforce_residency, network, permissions, + models, guardian_policy_config, } = self; ConfigRequirementsToml { @@ -1071,6 +1103,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), } } @@ -1183,6 +1216,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() @@ -1214,8 +1251,9 @@ impl TryFrom for ConfigRequirements { fn try_from(toml: ConfigRequirementsWithSources) -> Result { // 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, @@ -1238,6 +1276,7 @@ impl TryFrom for ConfigRequirements { enforce_residency, network, permissions, + models: _, guardian_policy_config, } = toml; @@ -1635,6 +1674,7 @@ mod tests { enforce_residency, network, permissions, + models, guardian_policy_config, } = toml; ConfigRequirementsWithSources { @@ -1670,6 +1710,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)), } @@ -1822,6 +1863,31 @@ mod tests { Ok(()) } + #[test] + 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" + "#, + )?; + + assert_eq!( + requirements.models, + Some(ModelsRequirementsToml { + new_thread: Some(NewThreadModelDefaultsToml { + model: Some("managed-model".to_string()), + model_reasoning_effort: Some(ReasoningEffort::Medium), + service_tier: Some("fast".to_string()), + }), + }) + ); + assert!(!requirements.is_empty()); + Ok(()) + } + #[test] fn merge_unset_fields_copies_every_field_and_sets_sources() { let mut target = ConfigRequirementsWithSources::default(); @@ -1844,6 +1910,13 @@ 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()), + model_reasoning_effort: Some(ReasoningEffort::Medium), + service_tier: Some("fast".to_string()), + }), + }; let enforce_residency = ResidencyRequirement::Us; let enforce_source = source.clone(); let guardian_policy_config = "Use the company-managed guardian policy.".to_string(); @@ -1873,6 +1946,7 @@ mod tests { enforce_residency: Some(enforce_residency), network: None, permissions: None, + models: Some(models.clone()), guardian_policy_config: Some(guardian_policy_config.clone()), }; @@ -1923,6 +1997,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)), } ); @@ -1970,6 +2045,7 @@ mod tests { enforce_residency: None, network: None, permissions: None, + models: None, guardian_policy_config: None, } ); @@ -2025,6 +2101,7 @@ mod tests { enforce_residency: None, network: None, permissions: None, + models: None, guardian_policy_config: None, } ); diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 61e6b6966..df3e45d76 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -67,12 +67,14 @@ pub use config_requirements::FilesystemDenyReadPattern; pub use config_requirements::MarketplaceAllowedSourceKind; pub use config_requirements::MarketplaceAllowedSourceToml; pub use config_requirements::MarketplaceRequirementsToml; +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; diff --git a/codex-rs/config/src/requirements_layers/stack.rs b/codex-rs/config/src/requirements_layers/stack.rs index 3be1d11b5..9a42650bf 100644 --- a/codex-rs/config/src/requirements_layers/stack.rs +++ b/codex-rs/config/src/requirements_layers/stack.rs @@ -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()) diff --git a/codex-rs/config/src/requirements_layers/stack_tests.rs b/codex-rs/config/src/requirements_layers/stack_tests.rs index a775f68a8..92e6bd8b4 100644 --- a/codex-rs/config/src/requirements_layers/stack_tests.rs +++ b/codex-rs/config/src/requirements_layers/stack_tests.rs @@ -109,6 +109,46 @@ allow_remote_control = false ); } +#[test] +fn new_thread_model_defaults_use_toml_priority() { + let composed = compose(vec![ + layer( + "req_low", + "Low", + r#" +[models.new_thread] +model = "low-priority-model" +model_reasoning_effort = "low" +service_tier = "flex" +"#, + ), + layer( + "req_high", + "High", + r#" +[models.new_thread] +model = "high-priority-model" +model_reasoning_effort = "high" +service_tier = "fast" +"#, + ), + ]) + .expect("compose requirements") + .expect("requirements present"); + + assert_eq!( + composed, + expected_requirements( + r#" +[models.new_thread] +model = "high-priority-model" +model_reasoning_effort = "high" +service_tier = "fast" +"# + ) + ); +} + #[test] fn composition_strategy_applies_to_non_cloud_layers() { let mdm_source = RequirementSource::MdmManagedPreferences { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 6f7114928..c34447554 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8867,6 +8867,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; diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 7a35cc450..c02d4c826 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -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)