mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Disable Daybreak when the selected model lacks access (#60526)
## Why Switching to a model whose access metadata excludes Daybreak can leave Daybreak enabled and block subsequent turns. ## What changed Automatically turn Daybreak off when a model switch or catalog refresh confirms that it is unavailable for the selected model. Persist the disabled preference for the thread and new threads, and warn that prior cybersecurity context may still cause refusals. Switching back to a compatible model leaves Daybreak off until the user re-enables it with `/daybreak`. Require explicit access metadata before disabling Daybreak, and recheck the selected model before applying a queued change. ## Testing Extend session lifecycle coverage for pending model discovery, supported and unknown models, persisted disabling, and subsequent turns using the standard access program. Add a snapshot of the model-switch warning. GitOrigin-RevId: 6c294939d854c7176ed9a346e8ea806ffcf6f715
This commit is contained in:
@@ -8,7 +8,7 @@ impl App {
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
enabled: bool,
|
||||
) {
|
||||
) -> bool {
|
||||
if !self
|
||||
.chat_widget
|
||||
.config_ref()
|
||||
@@ -16,7 +16,7 @@ impl App {
|
||||
.enabled(Feature::CliDaybreak)
|
||||
|| self.active_thread_id != Some(thread_id)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if enabled
|
||||
&& (!self.chat_widget.daybreak_account_eligible()
|
||||
@@ -25,7 +25,7 @@ impl App {
|
||||
self.chat_widget.add_error_message(
|
||||
"Daybreak availability could not be confirmed for this account.".into(),
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
match app_server
|
||||
.thread_read(thread_id, /*include_turns*/ false)
|
||||
@@ -50,7 +50,7 @@ impl App {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to save Daybreak for this thread: {error}"
|
||||
));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
@@ -58,7 +58,7 @@ impl App {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to read the thread to save Daybreak: {error}"
|
||||
));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.chat_widget.set_daybreak_enabled(enabled);
|
||||
@@ -119,5 +119,6 @@ impl App {
|
||||
),
|
||||
/*hint*/ None,
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2890,7 +2890,18 @@ impl App {
|
||||
.await;
|
||||
}
|
||||
AppEvent::PersistDaybreakSelection { thread_id, enabled } => {
|
||||
self.persist_daybreak_selection(app_server, thread_id, enabled).await;
|
||||
Box::pin(self.persist_daybreak_selection(app_server, thread_id, enabled)).await;
|
||||
}
|
||||
AppEvent::DisableUnavailableDaybreak { thread_id, model } => {
|
||||
if self.chat_widget.current_model() == model
|
||||
&& self.chat_widget.should_disable_daybreak(&model)
|
||||
&& Box::pin(self.persist_daybreak_selection(app_server, thread_id, /*enabled*/ false)).await
|
||||
{
|
||||
self.chat_widget.add_info_message(
|
||||
format!("Daybreak has been turned off for new turns because it is not available for {model} with your current account and server. Prior cybersecurity context in this thread may cause a refusal. You can switch back to a model where you have Daybreak access and use /daybreak to re-enable it."),
|
||||
/*hint*/ None,
|
||||
);
|
||||
}
|
||||
}
|
||||
AppEvent::SelectSessionModel { model, effort, service_tier } => {
|
||||
self.app_event_tx.send(AppEvent::FollowTranscript);
|
||||
|
||||
@@ -914,7 +914,7 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> {
|
||||
};
|
||||
*model = "standard-only".into();
|
||||
while events.try_recv().is_ok() {}
|
||||
app.submit_thread_op(&mut server, thread_id, unsupported_turn)
|
||||
app.submit_thread_op(&mut server, thread_id, unsupported_turn.clone())
|
||||
.await?;
|
||||
assert!(recorded_params(&requests, "turn/start").is_empty());
|
||||
assert!(next_history_message(&mut events).contains("Daybreak support for model standard-only"));
|
||||
@@ -953,7 +953,17 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> {
|
||||
app.chat_widget.set_daybreak_enabled(/*enabled*/ true);
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::ApiKeyCyberAccessPrograms, /*enabled*/ false);
|
||||
let models = app.chat_widget.model_catalog().models.clone();
|
||||
let mut models = app.chat_widget.model_catalog().models.clone();
|
||||
for (name, cyber) in [
|
||||
("red", vec![CyberAccessProgram::DaybreakRed]),
|
||||
("empty", vec![]),
|
||||
] {
|
||||
models.push(ModelPreset {
|
||||
model: name.into(),
|
||||
available_access_programs: Some(ModelAccessPrograms { cyber }),
|
||||
..models[0].clone()
|
||||
});
|
||||
}
|
||||
app.chat_widget.update_account_state(
|
||||
Some(crate::status::StatusAccountDisplay::ApiKey),
|
||||
/*plan_type*/ None,
|
||||
@@ -968,7 +978,26 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> {
|
||||
})
|
||||
.last()
|
||||
.expect("account model refresh");
|
||||
let next_disable = |events: &mut tokio::sync::mpsc::UnboundedReceiver<_>| {
|
||||
std::iter::from_fn(|| events.try_recv().ok())
|
||||
.find(|event| matches!(event, AppEvent::DisableUnavailableDaybreak { .. }))
|
||||
};
|
||||
// A switch during discovery must wait for the current account's catalog.
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::ApiKeyCyberAccessPrograms, /*enabled*/ true);
|
||||
app.chat_widget.set_daybreak_enabled(/*enabled*/ true);
|
||||
let original_model = app.chat_widget.current_model().to_owned();
|
||||
app.chat_widget.set_model("standard-only");
|
||||
assert!(next_disable(&mut events).is_none());
|
||||
assert!(app.chat_widget.on_models_loaded(request_id, Ok(models)));
|
||||
assert!(matches!(next_disable(&mut events),
|
||||
Some(AppEvent::DisableUnavailableDaybreak { thread_id: id, model })
|
||||
if id == thread_id && model == "standard-only"
|
||||
));
|
||||
for model in ["red", "empty", "unlisted", &original_model] {
|
||||
app.chat_widget.set_model(model);
|
||||
}
|
||||
assert!(next_disable(&mut events).is_none());
|
||||
for (cli_daybreak, api_key_cyber_access_programs, enabled) in [
|
||||
(false, false, true),
|
||||
(false, true, true),
|
||||
@@ -998,11 +1027,41 @@ async fn daybreak_command_persists_and_confirms_each_selection() -> Result<()> {
|
||||
|
||||
let missing_thread_id = ThreadId::new();
|
||||
app.active_thread_id = Some(missing_thread_id);
|
||||
app.persist_daybreak_selection(&mut server, missing_thread_id, /*enabled*/ true)
|
||||
.await;
|
||||
assert!(
|
||||
!app.persist_daybreak_selection(&mut server, missing_thread_id, /*enabled*/ true)
|
||||
.await
|
||||
);
|
||||
assert!(next_history_message(&mut events).contains("Failed to read the thread"));
|
||||
assert!(!app.chat_widget.daybreak_enabled);
|
||||
assert!(!app.config.daybreak_enabled);
|
||||
|
||||
app.active_thread_id = Some(thread_id);
|
||||
app.persist_daybreak_selection(&mut server, thread_id, /*enabled*/ true)
|
||||
.await;
|
||||
assert!(next_history_message(&mut events).contains("Daybreak on"));
|
||||
app.chat_widget.set_model("standard-only");
|
||||
let disable = next_disable(&mut events).expect("model switch disables Daybreak");
|
||||
assert!(app.chat_widget.daybreak_enabled);
|
||||
app.handle_event(&mut tui, &mut server, disable).await?;
|
||||
assert!(next_history_message(&mut events).contains("Daybreak off"));
|
||||
let warning = next_history_message(&mut events);
|
||||
insta::assert_snapshot!("daybreak_model_switch_warning", warning);
|
||||
assert_eq!(
|
||||
server
|
||||
.thread_read(thread_id, /*include_turns*/ false)
|
||||
.await?
|
||||
.daybreak_enabled,
|
||||
Some(false)
|
||||
);
|
||||
assert!(!app.config.daybreak_enabled);
|
||||
app.submit_thread_op(&mut server, thread_id, unsupported_turn)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
recorded_params(&requests, "turn/start")[turns.len()]["cyberAccessProgram"],
|
||||
"standard"
|
||||
);
|
||||
app.chat_widget.set_model(&original_model);
|
||||
assert!(!app.chat_widget.daybreak_enabled);
|
||||
server.shutdown().await?;
|
||||
proxy.await??;
|
||||
Ok(())
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tui/src/app/tests/session_lifecycle_requests.rs
|
||||
assertion_line: 181
|
||||
expression: warning
|
||||
---
|
||||
• Daybreak has been turned off for new turns because it is not available for standard-only with your current account and server. Prior cybersecurity context in this thread may cause a refusal. You can switch back to a model where you have Daybreak access and use /daybreak to re-enable it.
|
||||
@@ -1513,6 +1513,11 @@ pub(crate) enum AppEvent {
|
||||
thread_id: ThreadId,
|
||||
enabled: bool,
|
||||
},
|
||||
/// Disable Daybreak after explicit account-scoped metadata excludes it for the selected model.
|
||||
DisableUnavailableDaybreak {
|
||||
thread_id: ThreadId,
|
||||
model: String,
|
||||
},
|
||||
|
||||
/// Fetch the current catalog even when cached models produce no picker.
|
||||
FetchModels {
|
||||
|
||||
@@ -87,6 +87,7 @@ impl ChatWidget {
|
||||
}
|
||||
Arc::make_mut(&mut self.model_catalog).models = presets;
|
||||
self.set_daybreak_enabled(self.daybreak_enabled);
|
||||
self.queue_unavailable_daybreak_disable(self.current_model());
|
||||
self.refresh_effective_service_tier();
|
||||
self.refresh_model_dependent_surfaces();
|
||||
true
|
||||
|
||||
@@ -311,6 +311,7 @@ impl ChatWidget {
|
||||
/// Set the model in the widget's config copy and stored collaboration mode.
|
||||
pub(crate) fn set_model(&mut self, model: &str) {
|
||||
if model != self.current_model() {
|
||||
self.queue_unavailable_daybreak_disable(model);
|
||||
if self.current_model() == crate::model_catalog::LUNA_RESERVE_MODEL {
|
||||
self.clear_reserve_return();
|
||||
} else {
|
||||
@@ -332,6 +333,26 @@ impl ChatWidget {
|
||||
self.refresh_model_dependent_surfaces();
|
||||
}
|
||||
|
||||
pub(super) fn queue_unavailable_daybreak_disable(&self, model: &str) {
|
||||
if let Some(thread_id) = self.thread_id
|
||||
&& self.should_disable_daybreak(model)
|
||||
{
|
||||
self.app_event_tx
|
||||
.send(AppEvent::DisableUnavailableDaybreak {
|
||||
thread_id,
|
||||
model: model.to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_disable_daybreak(&self, model: &str) -> bool {
|
||||
self.daybreak_enabled
|
||||
&& self.model_catalog.models.iter().any(|entry| {
|
||||
entry.model == model
|
||||
&& crate::daybreak::availability(std::slice::from_ref(entry)) == Some(false)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn current_model(&self) -> &str {
|
||||
if !self.collaboration_modes_enabled() {
|
||||
return self.current_collaboration_mode.model();
|
||||
@@ -550,6 +571,9 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
if self.current_model() != settings.model {
|
||||
self.queue_unavailable_daybreak_disable(&settings.model);
|
||||
}
|
||||
settings.collaboration_mode.settings.model = settings.model;
|
||||
settings.collaboration_mode.settings.reasoning_effort = settings.effort;
|
||||
self.set_effective_collaboration_mode(settings.collaboration_mode);
|
||||
@@ -738,6 +762,9 @@ impl ChatWidget {
|
||||
}
|
||||
self.active_collaboration_mask = Some(mask);
|
||||
self.update_collaboration_mode_indicator();
|
||||
if previous_model != self.current_model() {
|
||||
self.queue_unavailable_daybreak_disable(self.current_model());
|
||||
}
|
||||
self.refresh_model_dependent_surfaces();
|
||||
let next_mode = self.active_mode_kind();
|
||||
let next_model = self.current_model();
|
||||
|
||||
Reference in New Issue
Block a user