Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions codex-rs/core/src/session/code_mode_warning.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use codex_features::Feature;
use codex_features::Features;
use codex_protocol::openai_models::ModelInfo;

pub(super) fn unsupported_code_mode_warning(
model_info: &ModelInfo,
features: &Features,
) -> Option<String> {
let code_mode_enabled =
features.enabled(Feature::CodeMode) || features.enabled(Feature::CodeModeOnly);
if !code_mode_enabled
|| model_info.tool_mode.is_some()
|| model_info.used_fallback_model_metadata
{
return None;
}

let model = &model_info.slug;
Some(format!(
"Code Mode is enabled in configuration, but model `{model}` does not advertise Code Mode support. This may degrade model performance. Disable `features.code_mode` and `features.code_mode_only`, or select a model whose metadata enables Code Mode."
))
}

#[cfg(test)]
#[path = "code_mode_warning_tests.rs"]
mod tests;
70 changes: 70 additions & 0 deletions codex-rs/core/src/session/code_mode_warning_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use super::unsupported_code_mode_warning;
use codex_features::Feature;
use codex_features::Features;
use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ToolMode;
use pretty_assertions::assert_eq;

const MODEL_SLUG: &str = "test-model";

fn known_model_info() -> ModelInfo {
ModelInfo {
used_fallback_model_metadata: false,
..model_info_from_slug(MODEL_SLUG)
}
}

#[test]
fn warns_when_code_mode_is_enabled_without_model_selector() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);

assert_eq!(
unsupported_code_mode_warning(&known_model_info(), &features),
Some(format!(
"Code Mode is enabled in configuration, but model `{MODEL_SLUG}` does not advertise Code Mode support. This may degrade model performance. Disable `features.code_mode` and `features.code_mode_only`, or select a model whose metadata enables Code Mode."
))
);
}

#[test]
fn warns_when_code_mode_only_is_enabled_without_model_selector() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeModeOnly);

assert!(unsupported_code_mode_warning(&known_model_info(), &features).is_some());
}

#[test]
fn does_not_warn_when_code_mode_is_disabled() {
assert_eq!(
unsupported_code_mode_warning(&known_model_info(), &Features::with_defaults()),
None
);
}

#[test]
fn does_not_warn_when_model_has_tool_mode_selector() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeModeOnly);

for tool_mode in [ToolMode::Direct, ToolMode::CodeMode, ToolMode::CodeModeOnly] {
let model_info = ModelInfo {
tool_mode: Some(tool_mode),
..known_model_info()
};
assert_eq!(unsupported_code_mode_warning(&model_info, &features), None);
}
}

#[test]
fn fallback_metadata_only_uses_existing_warning() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);

assert_eq!(
unsupported_code_mode_warning(&model_info_from_slug(MODEL_SLUG), &features),
None
);
}
4 changes: 2 additions & 2 deletions codex-rs/core/src/session/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ pub(super) async fn user_input_or_turn_inner(
})
.await;
}
sess.maybe_emit_unknown_model_warning_for_turn(current_context.as_ref())
sess.maybe_emit_model_warnings_for_turn(current_context.as_ref())
.await;
match sess
.steer_input(
Expand Down Expand Up @@ -673,7 +673,7 @@ pub async fn review(
review_request: ReviewRequest,
) {
let turn_context = sess.new_default_turn_with_sub_id(sub_id.clone()).await;
sess.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref())
sess.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
sess.refresh_mcp_servers_if_requested(&turn_context, Some(sess.mcp_elicitation_reviewer()))
.await;
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/session/inject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl Session {
input,
));
}
self.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref())
self.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
if self.input_queue.has_trigger_turn_mailbox_items().await {
self.clear_reserved_idle_turn(&turn_state).await;
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ use codex_protocol::error::Result as CodexResult;
#[cfg(test)]
use codex_protocol::exec_output::StreamOutput;

mod code_mode_warning;
mod config_lock;
mod handlers;
mod inject;
Expand All @@ -225,6 +226,7 @@ mod token_budget;
pub(crate) mod turn;
pub(crate) mod turn_context;
mod world_state;
use self::code_mode_warning::unsupported_code_mode_warning;
use self::config_lock::export_config_lock_if_configured;
use self::config_lock::validate_config_lock_if_configured;
#[cfg(test)]
Expand Down
9 changes: 8 additions & 1 deletion codex-rs/core/src/session/turn_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ impl Session {
turn_context
}

pub(crate) async fn maybe_emit_unknown_model_warning_for_turn(&self, tc: &TurnContext) {
pub(crate) async fn maybe_emit_model_warnings_for_turn(&self, tc: &TurnContext) {
if tc.model_info.used_fallback_model_metadata {
self.send_event(
tc,
Expand All @@ -810,6 +810,13 @@ impl Session {
)
.await;
}

if let Some(message) =
unsupported_code_mode_warning(&tc.model_info, tc.config.features.get())
{
self.send_event(tc, EventMsg::Warning(WarningEvent { message }))
.await;
}
}

pub(crate) async fn new_default_turn(&self) -> Arc<TurnContext> {
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/tasks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ impl Session {
}

let turn_context = self.new_default_turn_with_sub_id(sub_id).await;
self.maybe_emit_unknown_model_warning_for_turn(turn_context.as_ref())
self.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
.await;
self.start_task(turn_context, Vec::new(), RegularTask::new())
.await;
Expand Down
146 changes: 140 additions & 6 deletions codex-rs/core/tests/suite/model_runtime_selectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_models_once;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse;
use core_test_support::skip_if_no_network;
use core_test_support::submit_thread_settings;
Expand All @@ -36,6 +37,12 @@ use tokio::time::sleep;
const CHILD_MODEL: &str = "test-multi-agent-child";
const ROOT_MODEL: &str = "test-multi-agent-root";
const ROOT_PROMPT: &str = "spawn a child";
const UNSUPPORTED_CODE_MODE_WARNING: &str = "does not advertise Code Mode support";

struct RemoteModelResponse {
body: Value,
warnings: Vec<String>,
}

fn remote_model(slug: &str) -> ModelInfo {
ModelInfo {
Expand Down Expand Up @@ -81,10 +88,10 @@ async fn wait_for_model_available(manager: &SharedModelsManager, slug: &str) ->
}
}

async fn response_body_for_remote_model(
async fn response_for_remote_model(
remote_model: ModelInfo,
configure: impl FnOnce(&mut Config) + Send + 'static,
) -> Result<Value> {
) -> Result<RemoteModelResponse> {
let server = responses::start_mock_server().await;
let model_slug = remote_model.slug.clone();
let models_mock = mount_models_once(
Expand Down Expand Up @@ -133,12 +140,28 @@ async fn response_body_for_remote_model(
thread_settings: Default::default(),
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
let mut warnings = Vec::new();
loop {
match wait_for_event(&test.codex, |_| true).await {
EventMsg::Warning(warning) => warnings.push(warning.message),
EventMsg::TurnComplete(_) => break,
_ => {}
}
}

Ok(RemoteModelResponse {
body: response_mock.single_request().body_json(),
warnings,
})
.await;
}

Ok(response_mock.single_request().body_json())
async fn response_body_for_remote_model(
remote_model: ModelInfo,
configure: impl FnOnce(&mut Config) + Send + 'static,
) -> Result<Value> {
Ok(response_for_remote_model(remote_model, configure)
.await?
.body)
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
Expand Down Expand Up @@ -180,6 +203,117 @@ async fn remote_tool_mode_selector_overrides_feature_flags() -> Result<()> {
]
);

let unsupported_model = remote_model("test-tool-mode-unsupported");
let unsupported_response = response_for_remote_model(unsupported_model, |config| {
config
.features
.enable(Feature::CodeModeOnly)
.expect("test config should allow feature update");
})
.await?;
assert!(
tool_names(&unsupported_response.body)
.iter()
.any(|name| name == codex_code_mode::PUBLIC_TOOL_NAME)
);
assert_eq!(
unsupported_response
.warnings
.iter()
.filter(|warning| warning.contains(UNSUPPORTED_CODE_MODE_WARNING))
.count(),
1
);

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unsupported_code_mode_warning_is_emitted_each_turn() -> Result<()> {
skip_if_no_network!(Ok(()));

let server = responses::start_mock_server().await;
let model_slug = "test-tool-mode-warning-each-turn";
let models_mock = mount_models_once(
&server,
ModelsResponse {
models: vec![remote_model(model_slug)],
},
)
.await;
let response_mock = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
ev_assistant_message("msg-1", "done"),
ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-2", "done"),
ev_completed("resp-2"),
]),
],
)
.await;
let test = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
config
.features
.enable(Feature::CodeMode)
.expect("test config should allow feature update");
})
.build(&server)
.await?;
let models_manager = test.thread_manager.get_models_manager();
let available_model = wait_for_model_available(&models_manager, model_slug).await;
assert_eq!(available_model.model, model_slug);
assert_eq!(models_mock.requests().len(), 1);

submit_thread_settings(
&test.codex,
ThreadSettingsOverrides {
model: Some(model_slug.to_string()),
..Default::default()
},
)
.await?;

let mut warning_counts = Vec::new();
for prompt in ["first turn", "second turn"] {
test.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: prompt.to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;

let mut warning_count = 0;
loop {
match wait_for_event(&test.codex, |_| true).await {
EventMsg::Warning(warning)
if warning.message.contains(UNSUPPORTED_CODE_MODE_WARNING) =>
{
warning_count += 1;
}
EventMsg::TurnComplete(_) => break,
_ => {}
}
}
warning_counts.push(warning_count);
}

assert_eq!(warning_counts, vec![1, 1]);
assert_eq!(response_mock.requests().len(), 2);

Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: "lines_to_single_string(&cells[0])"
---
⚠ Code Mode is enabled in configuration, but model `gpt-5.4` does not advertise
Code Mode support. This may degrade model performance. Disable
`features.code_mode` and `features.code_mode_only`, or select a model whose
metadata enables Code Mode.
16 changes: 16 additions & 0 deletions codex-rs/tui/src/chatwidget/tests/status_and_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2079,6 +2079,22 @@ async fn warning_event_adds_warning_history_cell() {
);
}

#[tokio::test]
async fn unsupported_code_mode_warning_renders_as_warning_history_cell() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
handle_warning(
&mut chat,
"Code Mode is enabled in configuration, but model `gpt-5.4` does not advertise Code Mode support. This may degrade model performance. Disable `features.code_mode` and `features.code_mode_only`, or select a model whose metadata enables Code Mode.",
);

let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1, "expected one warning history cell");
insta::assert_snapshot!(
"unsupported_code_mode_warning",
lines_to_single_string(&cells[0])
);
}

#[tokio::test]
async fn repeated_model_metadata_warning_is_hidden_for_same_slug() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
Expand Down
Loading