mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Add managed new-thread model settings
This commit is contained in:
+16
@@ -12967,6 +12967,22 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelReasoningEffort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+16
@@ -9371,6 +9371,22 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"modelReasoningEffort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+21
@@ -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"
|
||||
|
||||
+2
-1
@@ -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)]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
"#
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user