Skip to content

Commit d05ebd9

Browse files
committed
changes
1 parent 51864b0 commit d05ebd9

19 files changed

Lines changed: 325 additions & 7 deletions

File tree

codex-rs/app-server-protocol/src/protocol/v2/thread.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ pub struct ThreadStartParams {
5757
pub model: Option<String>,
5858
#[ts(optional = nullable)]
5959
pub model_provider: Option<String>,
60+
/// Allow a provider with an authoritative static model catalog to replace an unavailable
61+
/// requested model with its default.
62+
#[experimental("thread/start.allowProviderModelFallback")]
63+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
64+
pub allow_provider_model_fallback: bool,
6065
#[serde(
6166
default,
6267
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",

codex-rs/app-server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Example with notification opt-out:
137137

138138
## API Overview
139139

140-
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive.
140+
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `allowProviderModelFallback` lets providers backed by an authoritative static model catalog replace an unavailable requested `model` with the catalog default; dynamic or cached catalogs preserve the requested model. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; paths must be absolute. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Deprecated experimental `multiAgentMode` is ignored; use Ultra reasoning effort for proactive multi-agent behavior. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. Experimental `selectedCapabilityRoots` selects environment-owned plugin or standalone-skill roots using environment-native absolute paths. Skills found below those roots are listed and read through the owning environment. Stdio MCP servers declared by selected plugins are also started in that environment; HTTP MCP declarations remain inactive.
141141
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
142142
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
143143
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their deprecated experimental `multiAgentMode` field, and the corresponding thread setting, always report `explicitRequestOnly`; Ultra reasoning effort is the source of proactive multi-agent behavior.

codex-rs/app-server/src/request_processors/external_agent_session_import.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,11 @@ impl ExternalAgentSessionImporter {
184184
.map_err(|err| format!("failed to load imported session config: {err}"))?;
185185
let models_manager = self.thread_manager.get_models_manager();
186186
let model = models_manager
187-
.get_default_model(&config.model, RefreshStrategy::Offline)
187+
.get_default_model(
188+
&config.model,
189+
/*allow_provider_model_fallback*/ false,
190+
RefreshStrategy::Offline,
191+
)
188192
.await;
189193
let model_info = models_manager
190194
.get_model_info(model.as_str(), &config.to_models_manager_config())

codex-rs/app-server/src/request_processors/thread_processor.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ impl ThreadRequestProcessor {
887887
let ThreadStartParams {
888888
model,
889889
model_provider,
890+
allow_provider_model_fallback,
890891
service_tier,
891892
cwd,
892893
runtime_workspace_roots,
@@ -963,6 +964,7 @@ impl ThreadRequestProcessor {
963964
thread_source.map(Into::into),
964965
environment_selections,
965966
service_name,
967+
allow_provider_model_fallback,
966968
experimental_raw_events,
967969
request_trace,
968970
)
@@ -1037,6 +1039,7 @@ impl ThreadRequestProcessor {
10371039
thread_source: Option<codex_protocol::protocol::ThreadSource>,
10381040
environments: Option<Vec<TurnEnvironmentSelection>>,
10391041
service_name: Option<String>,
1042+
allow_provider_model_fallback: bool,
10401043
experimental_raw_events: bool,
10411044
request_trace: Option<W3cTraceContext>,
10421045
) -> Result<(), JSONRPCErrorError> {
@@ -1144,6 +1147,7 @@ impl ThreadRequestProcessor {
11441147
.thread_manager
11451148
.start_thread_with_options(StartThreadOptions {
11461149
config,
1150+
allow_provider_model_fallback,
11471151
initial_history: match session_start_source
11481152
.unwrap_or(codex_app_server_protocol::ThreadStartSource::Startup)
11491153
{

codex-rs/app-server/tests/suite/v2/skills_list.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
882882
.send_thread_start_request(ThreadStartParams {
883883
model: None,
884884
model_provider: None,
885+
allow_provider_model_fallback: false,
885886
service_tier: None,
886887
cwd: None,
887888
runtime_workspace_roots: None,

codex-rs/app-server/tests/suite/v2/thread_start.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,115 @@ use super::analytics::wait_for_analytics_payload;
5454
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
5555
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
5656

57+
async fn start_thread_with_model(
58+
mcp: &mut TestAppServer,
59+
model: &str,
60+
allow_provider_model_fallback: bool,
61+
) -> Result<ThreadStartResponse> {
62+
let request_id = mcp
63+
.send_thread_start_request_with_auto_env(ThreadStartParams {
64+
model: Some(model.to_string()),
65+
allow_provider_model_fallback,
66+
..Default::default()
67+
})
68+
.await?;
69+
let response: JSONRPCResponse = timeout(
70+
DEFAULT_READ_TIMEOUT,
71+
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
72+
)
73+
.await??;
74+
to_response(response)
75+
}
76+
77+
#[tokio::test]
78+
async fn thread_start_provider_model_fallback_applies_to_configured_model() -> Result<()> {
79+
let codex_home = TempDir::new()?;
80+
std::fs::write(
81+
codex_home.path().join("config.toml"),
82+
r#"model_provider = "amazon-bedrock"
83+
model = "gpt-5.4-mini"
84+
"#,
85+
)?;
86+
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
87+
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
88+
89+
let request_id = mcp
90+
.send_thread_start_request_with_auto_env(ThreadStartParams {
91+
allow_provider_model_fallback: true,
92+
..Default::default()
93+
})
94+
.await?;
95+
let response: JSONRPCResponse = timeout(
96+
DEFAULT_READ_TIMEOUT,
97+
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
98+
)
99+
.await??;
100+
let response: ThreadStartResponse = to_response(response)?;
101+
102+
assert_eq!(response.model, "openai.gpt-5.5");
103+
Ok(())
104+
}
105+
106+
#[tokio::test]
107+
async fn thread_start_provider_model_fallback_uses_bedrock_static_catalog() -> Result<()> {
108+
let codex_home = TempDir::new()?;
109+
std::fs::write(
110+
codex_home.path().join("config.toml"),
111+
r#"model_provider = "amazon-bedrock"
112+
"#,
113+
)?;
114+
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
115+
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
116+
117+
let unsupported_with_fallback = start_thread_with_model(
118+
&mut mcp,
119+
"gpt-5.4-mini",
120+
/*allow_provider_model_fallback*/ true,
121+
)
122+
.await?;
123+
let supported_with_fallback = start_thread_with_model(
124+
&mut mcp,
125+
"openai.gpt-5.4",
126+
/*allow_provider_model_fallback*/ true,
127+
)
128+
.await?;
129+
let unsupported_without_fallback = start_thread_with_model(
130+
&mut mcp,
131+
"gpt-5.4-mini",
132+
/*allow_provider_model_fallback*/ false,
133+
)
134+
.await?;
135+
136+
assert_eq!(
137+
vec![
138+
unsupported_with_fallback.model,
139+
supported_with_fallback.model,
140+
unsupported_without_fallback.model,
141+
],
142+
vec!["openai.gpt-5.5", "openai.gpt-5.4", "gpt-5.4-mini"]
143+
);
144+
Ok(())
145+
}
146+
147+
#[tokio::test]
148+
async fn thread_start_provider_model_fallback_ignores_dynamic_catalog() -> Result<()> {
149+
let server = create_mock_responses_server_repeating_assistant("Done").await;
150+
let codex_home = TempDir::new()?;
151+
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
152+
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
153+
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
154+
155+
let response = start_thread_with_model(
156+
&mut mcp,
157+
"unlisted-dynamic-model",
158+
/*allow_provider_model_fallback*/ true,
159+
)
160+
.await?;
161+
162+
assert_eq!(response.model, "unlisted-dynamic-model");
163+
Ok(())
164+
}
165+
57166
#[tokio::test]
58167
async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
59168
// Provide a mock server and config so model wiring is valid.

codex-rs/core/src/agent/control_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2213,6 +2213,7 @@ async fn spawn_thread_subagents_persist_parent_originator_across_new_and_truncat
22132213
.manager
22142214
.start_thread_with_options(StartThreadOptions {
22152215
config: harness.config.clone(),
2216+
allow_provider_model_fallback: false,
22162217
initial_history: InitialHistory::New,
22172218
session_source: None,
22182219
thread_source: None,

codex-rs/core/src/codex_delegate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub(crate) async fn run_codex_thread_interactive(
8686
};
8787
let CodexSpawnOk { codex, .. } = Box::pin(Codex::spawn(CodexSpawnArgs {
8888
config,
89+
allow_provider_model_fallback: false,
8990
user_instructions,
9091
installation_id: parent_session.installation_id.clone(),
9192
auth_manager,

codex-rs/core/src/session/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ pub struct CodexSpawnOk {
417417

418418
pub(crate) struct CodexSpawnArgs {
419419
pub(crate) config: Config,
420+
pub(crate) allow_provider_model_fallback: bool,
420421
pub(crate) user_instructions: LoadedUserInstructions,
421422
pub(crate) installation_id: String,
422423
pub(crate) auth_manager: Arc<AuthManager>,
@@ -506,6 +507,7 @@ impl Codex {
506507
async fn spawn_internal(args: CodexSpawnArgs) -> CodexResult<CodexSpawnOk> {
507508
let CodexSpawnArgs {
508509
mut config,
510+
allow_provider_model_fallback,
509511
user_instructions,
510512
installation_id,
511513
auth_manager,
@@ -579,8 +581,23 @@ impl Codex {
579581
let _ = models_manager.list_models(refresh_strategy).await;
580582
}
581583
let model = models_manager
582-
.get_default_model(&config.model, refresh_strategy)
584+
.get_default_model(
585+
&config.model,
586+
allow_provider_model_fallback,
587+
refresh_strategy,
588+
)
583589
.await;
590+
if allow_provider_model_fallback
591+
&& let Some(requested_model) = config.model.as_ref()
592+
&& model != *requested_model
593+
{
594+
info!(
595+
model_provider = %config.model_provider_id,
596+
requested_model,
597+
fallback_model = %model,
598+
"replaced unavailable requested model with provider default"
599+
);
600+
}
584601

585602
// Resolve base instructions for the session. Priority order:
586603
// 1. config.base_instructions override

codex-rs/core/src/session/tests/guardian_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
717717

718718
let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs {
719719
config,
720+
allow_provider_model_fallback: false,
720721
user_instructions: Default::default(),
721722
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
722723
auth_manager,

0 commit comments

Comments
 (0)