mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Keep threads on their saved model provider when defaults change (#59503)
## Why Changing the default model provider should not prevent users from resuming or continuing a thread that uses another configured provider. ## What changed - Resolve cold resumes using the thread's saved provider and reject explicit attempts to switch it on resume. - Use retained provider configuration for loaded threads without reloading the current default selection, while continuing to enforce managed provider policy. - Preserve errors for missing provider definitions and policy conflicts. ## Testing Update app-server and TUI coverage for resuming with a different default, recovering after restoring a missing provider definition, preserving drafts across resume retries, and continuing loaded threads with malformed system defaults. GitOrigin-RevId: c5bf3b03cacde7349480c94c2f10bac827a0eb1f
This commit is contained in:
@@ -434,27 +434,6 @@ impl ConfigManager {
|
||||
Ok(CloudStartupConfig { bootstrap, config })
|
||||
}
|
||||
|
||||
/// Admits resumed threads and input only on the currently selected provider.
|
||||
pub(crate) async fn check_thread_model_provider_for_input(
|
||||
&self,
|
||||
current: &Config,
|
||||
) -> std::io::Result<()> {
|
||||
self.check_thread_model_provider(current).await?;
|
||||
let configured = self
|
||||
.load_latest_config(Some(current.cwd.to_path_buf()))
|
||||
.await?;
|
||||
if current.model_provider_id != configured.model_provider_id {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
ModelProviderError::ModelProviderMismatch {
|
||||
model_provider: current.model_provider_id.clone(),
|
||||
current_model_provider: configured.model_provider_id,
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Installs local policy before cloud access; failed policy loads block traffic while
|
||||
/// non-strict startup remains available for configuration repair.
|
||||
pub(crate) async fn load_startup_config(
|
||||
@@ -544,7 +523,7 @@ impl ConfigManager {
|
||||
&self,
|
||||
saved_provider: Option<&str>,
|
||||
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
mut typesafe_overrides: ConfigOverrides,
|
||||
cwd: Option<PathBuf>,
|
||||
) -> std::io::Result<Config> {
|
||||
let requested_provider = typesafe_overrides.model_provider.clone().or_else(|| {
|
||||
@@ -554,11 +533,9 @@ impl ConfigManager {
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_owned)
|
||||
});
|
||||
// Resolve the selection before the saved provider is applied as a resume override.
|
||||
let current_provider = self
|
||||
.load_latest_config(typesafe_overrides.cwd.clone().or_else(|| cwd.clone()))
|
||||
.await
|
||||
.map(|config| config.model_provider_id);
|
||||
if let Some(saved) = saved_provider {
|
||||
typesafe_overrides.model_provider = Some(saved.to_owned());
|
||||
}
|
||||
self.load_with_validation(
|
||||
&self.current_cli_overrides(),
|
||||
request_overrides,
|
||||
@@ -567,6 +544,7 @@ impl ConfigManager {
|
||||
|config, _overrides, layers| {
|
||||
let requested_provider = requested_provider
|
||||
.as_deref()
|
||||
.or(saved_provider)
|
||||
.or(config.model_provider.as_deref())
|
||||
.unwrap_or("openai");
|
||||
// Validate the saved route even when a client sends a provider override.
|
||||
@@ -597,20 +575,13 @@ impl ConfigManager {
|
||||
));
|
||||
}
|
||||
}
|
||||
let current_provider = current_provider?;
|
||||
for provider in [
|
||||
saved_provider.unwrap_or(requested_provider),
|
||||
requested_provider,
|
||||
] {
|
||||
if provider != current_provider {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
ModelProviderError::ModelProviderMismatch {
|
||||
model_provider: provider.to_owned(),
|
||||
current_model_provider: current_provider,
|
||||
},
|
||||
));
|
||||
}
|
||||
if let Some(saved) = saved_provider
|
||||
&& saved != requested_provider
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("Cannot change this thread's model provider from `{saved}` to `{requested_provider}` on resume."),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
|
||||
@@ -261,62 +261,3 @@ async fn catalog_reload_uses_latest_model_provider() -> Result<()> {
|
||||
assert_eq!(refreshed.model_provider_id, "openai");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Invocation overrides and selected profiles still define the current provider.
|
||||
#[test_case("model_provider", "gateway"; "explicit_provider")]
|
||||
#[test_case("profile", "gateway-profile"; "selected_profile")]
|
||||
#[tokio::test]
|
||||
async fn input_provider_check_uses_effective_selection(key: &str, value: &str) -> Result<()> {
|
||||
let home = tempdir()?;
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
r#"
|
||||
model_provider = "openai"
|
||||
[model_providers.gateway]
|
||||
name = "Gateway"
|
||||
"#,
|
||||
)?;
|
||||
let mut loader_overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
let cli_overrides = if key == "profile" {
|
||||
std::fs::write(
|
||||
home.path().join(format!("{value}.config.toml")),
|
||||
"model_provider = 'gateway'\n[model_providers.gateway]\nname = 'Gateway'\n",
|
||||
)?;
|
||||
loader_overrides.user_config_path = Some(AbsolutePathBuf::from_absolute_path(
|
||||
home.path().join(format!("{value}.config.toml")),
|
||||
)?);
|
||||
loader_overrides.user_config_profile = Some(value.parse()?);
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![(key.into(), TomlValue::String(value.into()))]
|
||||
};
|
||||
let manager = ConfigManager::new_for_tests(
|
||||
home.path().to_path_buf(),
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
CloudConfigBundleLoader::default(),
|
||||
);
|
||||
let current = manager
|
||||
.load_latest_config(Some(home.path().to_path_buf()))
|
||||
.await?;
|
||||
assert_eq!(current.model_provider_id, "gateway");
|
||||
manager
|
||||
.check_thread_model_provider_for_input(¤t)
|
||||
.await?;
|
||||
let default_manager =
|
||||
ConfigManager::without_managed_config_for_tests(home.path().to_path_buf());
|
||||
let error = default_manager
|
||||
.check_thread_model_provider_for_input(¤t)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error
|
||||
.get_ref()
|
||||
.and_then(|error| error.downcast_ref::<ModelProviderError>()),
|
||||
Some(&ModelProviderError::ModelProviderMismatch {
|
||||
model_provider: "gateway".into(),
|
||||
current_model_provider: "openai".into(),
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ impl ThreadGoalRequestProcessor {
|
||||
&& let Ok(thread) = self.thread_manager.get_thread(thread_id).await
|
||||
{
|
||||
self.config_manager
|
||||
.check_thread_model_provider_for_input(thread.config().await.as_ref())
|
||||
.check_thread_model_provider(thread.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|error| config_load_error(&error))?;
|
||||
}
|
||||
|
||||
@@ -2608,7 +2608,7 @@ impl ThreadRequestProcessor {
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
ensure_direct_input_allowed(thread.as_ref()).await?;
|
||||
self.config_manager
|
||||
.check_thread_model_provider_for_input(thread.config().await.as_ref())
|
||||
.check_thread_model_provider(thread.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|error| config_load_error(&error))?;
|
||||
self.submit_core_op(request_id, thread.as_ref(), Op::Compact)
|
||||
@@ -4790,7 +4790,7 @@ impl ThreadRequestProcessor {
|
||||
);
|
||||
}
|
||||
self.config_manager
|
||||
.check_thread_model_provider_for_input(existing_thread.config().await.as_ref())
|
||||
.check_thread_model_provider(existing_thread.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
let redact_resume_payloads =
|
||||
|
||||
@@ -193,7 +193,7 @@ impl ThreadQueueRequestProcessor {
|
||||
let thread = loaded_thread
|
||||
.ok_or_else(|| invalid_request("resume the thread before starting a queued message"))?;
|
||||
self.config_manager
|
||||
.check_thread_model_provider_for_input(thread.config().await.as_ref())
|
||||
.check_thread_model_provider(thread.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|error| config_load_error(&error))?;
|
||||
let submission = self
|
||||
|
||||
@@ -194,7 +194,7 @@ impl TurnRequestProcessor {
|
||||
self.ensure_direct_input_allowed(request_id, thread.as_ref())
|
||||
.await?;
|
||||
self.config_manager
|
||||
.check_thread_model_provider_for_input(thread.config().await.as_ref())
|
||||
.check_thread_model_provider(thread.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|error| config_load_error(&error))?;
|
||||
match self
|
||||
@@ -389,7 +389,7 @@ impl TurnRequestProcessor {
|
||||
self.ensure_direct_input_allowed(request_id, thread.as_ref())
|
||||
.await?;
|
||||
self.config_manager
|
||||
.check_thread_model_provider_for_input(thread.config().await.as_ref())
|
||||
.check_thread_model_provider(thread.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|error| config_load_error(&error))?;
|
||||
let (review_request, display_text) = Self::review_request_from_target(params.target)?;
|
||||
@@ -559,7 +559,7 @@ impl TurnRequestProcessor {
|
||||
self.ensure_direct_input_allowed(&request_id, thread.as_ref())
|
||||
.await?;
|
||||
self.config_manager
|
||||
.check_thread_model_provider_for_input(thread.config().await.as_ref())
|
||||
.check_thread_model_provider(thread.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|error| config_load_error(&error))?;
|
||||
if let Some(tool_output) = ¶ms.tool_output {
|
||||
@@ -1095,7 +1095,7 @@ impl TurnRequestProcessor {
|
||||
self.ensure_direct_input_allowed(request_id, thread.as_ref())
|
||||
.await?;
|
||||
self.config_manager
|
||||
.check_thread_model_provider_for_input(thread.config().await.as_ref())
|
||||
.check_thread_model_provider(thread.config().await.as_ref())
|
||||
.await
|
||||
.map_err(|error| config_load_error(&error))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user