Remove model family from tui (#8488)

- Remove model family from tui
This commit is contained in:
Ahmed Ibrahim 2026-01-02 11:30:04 -08:00 committed by GitHub
parent 7078a0b676
commit 2de731490e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 189 additions and 252 deletions

View file

@ -372,10 +372,6 @@ impl App {
}
let enhanced_keys_supported = tui.enhanced_keys_supported();
let model_family = conversation_manager
.get_models_manager()
.construct_model_family(model.as_str(), &config)
.await;
let mut chat_widget = match resume_selection {
ResumeSelection::StartFresh | ResumeSelection::Exit => {
let init = crate::chatwidget::ChatWidgetInit {
@ -389,7 +385,7 @@ impl App {
models_manager: conversation_manager.get_models_manager(),
feedback: feedback.clone(),
is_first_run,
model_family: model_family.clone(),
model: model.clone(),
};
ChatWidget::new(init, conversation_manager.clone())
}
@ -415,7 +411,7 @@ impl App {
models_manager: conversation_manager.get_models_manager(),
feedback: feedback.clone(),
is_first_run,
model_family: model_family.clone(),
model: model.clone(),
};
ChatWidget::new_from_existing(
init,
@ -582,7 +578,7 @@ impl App {
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
model_family: model_family.clone(),
model: self.current_model.clone(),
};
self.chat_widget = ChatWidget::new(init, self.server.clone());
self.current_model = model_family.get_model_slug().to_string();
@ -632,7 +628,7 @@ impl App {
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
model_family: model_family.clone(),
model: self.current_model.clone(),
};
self.chat_widget = ChatWidget::new_from_existing(
init,
@ -767,12 +763,7 @@ impl App {
self.on_update_reasoning_effort(effort);
}
AppEvent::UpdateModel(model) => {
let model_family = self
.server
.get_models_manager()
.construct_model_family(&model, &self.config)
.await;
self.chat_widget.set_model(&model, model_family);
self.chat_widget.set_model(&model);
self.current_model = model;
}
AppEvent::OpenReasoningPopup { model } => {
@ -1357,7 +1348,7 @@ mod tests {
async fn make_test_app() -> App {
let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await;
let config = chat_widget.config_ref().clone();
let current_model = chat_widget.get_model_family().get_model_slug().to_string();
let current_model = "gpt-5.2-codex".to_string();
let server = Arc::new(ConversationManager::with_models_provider(
CodexAuth::from_api_key("Test API Key"),
config.model_provider.clone(),
@ -1396,7 +1387,7 @@ mod tests {
) {
let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await;
let config = chat_widget.config_ref().clone();
let current_model = chat_widget.get_model_family().get_model_slug().to_string();
let current_model = "gpt-5.2-codex".to_string();
let server = Arc::new(ConversationManager::with_models_provider(
CodexAuth::from_api_key("Test API Key"),
config.model_provider.clone(),

View file

@ -338,10 +338,9 @@ impl App {
) {
let conv = new_conv.conversation;
let session_configured = new_conv.session_configured;
let model_family = self.chat_widget.get_model_family();
let init = crate::chatwidget::ChatWidgetInit {
config: cfg,
model_family: model_family.clone(),
model: self.current_model.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
@ -354,7 +353,6 @@ impl App {
};
self.chat_widget =
crate::chatwidget::ChatWidget::new_from_existing(init, conv, session_configured);
self.current_model = model_family.get_model_slug().to_string();
// Trim transcript up to the selected user message and re-render it.
self.trim_transcript_for_backtrack(nth_user_message);
self.render_transcript_once(tui);

View file

@ -15,7 +15,6 @@ use codex_core::features::Feature;
use codex_core::git_info::current_branch_name;
use codex_core::git_info::local_git_branches;
use codex_core::models_manager::manager::ModelsManager;
use codex_core::models_manager::model_family::ModelFamily;
use codex_core::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
use codex_core::protocol::AgentMessageDeltaEvent;
use codex_core::protocol::AgentMessageEvent;
@ -291,7 +290,7 @@ pub(crate) struct ChatWidgetInit {
pub(crate) models_manager: Arc<ModelsManager>,
pub(crate) feedback: codex_feedback::CodexFeedback,
pub(crate) is_first_run: bool,
pub(crate) model_family: ModelFamily,
pub(crate) model: String,
}
#[derive(Default)]
@ -316,7 +315,7 @@ pub(crate) struct ChatWidget {
bottom_pane: BottomPane,
active_cell: Option<Box<dyn HistoryCell>>,
config: Config,
model_family: ModelFamily,
model: String,
auth_manager: Arc<AuthManager>,
models_manager: Arc<ModelsManager>,
session_header: SessionHeader,
@ -608,12 +607,10 @@ impl ChatWidget {
}
fn context_remaining_percent(&self, info: &TokenUsageInfo) -> Option<i64> {
info.model_context_window
.or(self.model_family.context_window)
.map(|window| {
info.last_token_usage
.percent_of_context_window_remaining(window)
})
info.model_context_window.map(|window| {
info.last_token_usage
.percent_of_context_window_remaining(window)
})
}
fn context_used_tokens(&self, info: &TokenUsageInfo, percent_known: bool) -> Option<i64> {
@ -681,7 +678,7 @@ impl ChatWidget {
if high_usage
&& !self.rate_limit_switch_prompt_hidden()
&& self.model_family.get_model_slug() != NUDGE_MODEL_SLUG
&& self.model != NUDGE_MODEL_SLUG
&& !matches!(
self.rate_limit_switch_prompt,
RateLimitSwitchPromptState::Shown
@ -715,9 +712,6 @@ impl ChatWidget {
self.stream_controller = None;
self.maybe_show_pending_rate_limit_prompt();
}
pub(crate) fn get_model_family(&self) -> ModelFamily {
self.model_family.clone()
}
fn on_error(&mut self, message: String) {
self.finalize_turn();
@ -1420,11 +1414,10 @@ impl ChatWidget {
models_manager,
feedback,
is_first_run,
model_family,
model,
} = common;
let model_slug = model_family.get_model_slug().to_string();
let mut config = config;
config.model = Some(model_slug.clone());
config.model = Some(model.clone());
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager);
@ -1445,10 +1438,10 @@ impl ChatWidget {
}),
active_cell: None,
config,
model_family,
model: model.clone(),
auth_manager,
models_manager,
session_header: SessionHeader::new(model_slug),
session_header: SessionHeader::new(model),
initial_user_message: create_initial_user_message(
initial_prompt.unwrap_or_default(),
initial_images,
@ -1506,10 +1499,9 @@ impl ChatWidget {
auth_manager,
models_manager,
feedback,
model_family,
model,
..
} = common;
let model_slug = model_family.get_model_slug().to_string();
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
@ -1532,10 +1524,10 @@ impl ChatWidget {
}),
active_cell: None,
config,
model_family,
model: model.clone(),
auth_manager,
models_manager,
session_header: SessionHeader::new(model_slug),
session_header: SessionHeader::new(model),
initial_user_message: create_initial_user_message(
initial_prompt.unwrap_or_default(),
initial_images,
@ -2249,22 +2241,20 @@ impl ChatWidget {
pub(crate) fn add_status_output(&mut self) {
let default_usage = TokenUsage::default();
let (total_usage, context_usage) = if let Some(ti) = &self.token_info {
(&ti.total_token_usage, Some(&ti.last_token_usage))
} else {
(&default_usage, Some(&default_usage))
};
let token_info = self.token_info.as_ref();
let total_usage = token_info
.map(|ti| &ti.total_token_usage)
.unwrap_or(&default_usage);
self.add_to_history(crate::status::new_status_output(
&self.config,
self.auth_manager.as_ref(),
&self.model_family,
token_info,
total_usage,
context_usage,
&self.conversation_id,
self.rate_limit_snapshot.as_ref(),
self.plan_type,
Local::now(),
self.model_family.get_model_slug(),
&self.model,
));
}
@ -2417,7 +2407,6 @@ impl ChatWidget {
/// Open a popup to choose a quick auto model. Selecting "All models"
/// opens the full picker with every available preset.
pub(crate) fn open_model_popup(&mut self) {
let current_model = self.model_family.get_model_slug().to_string();
let presets: Vec<ModelPreset> =
// todo(aibrahim): make this async function
match self.models_manager.try_list_models(&self.config) {
@ -2434,9 +2423,9 @@ impl ChatWidget {
let current_label = presets
.iter()
.find(|preset| preset.model == current_model)
.find(|preset| preset.model == self.model)
.map(|preset| preset.display_name.to_string())
.unwrap_or_else(|| current_model.clone());
.unwrap_or_else(|| self.model.clone());
let (mut auto_presets, other_presets): (Vec<ModelPreset>, Vec<ModelPreset>) = presets
.into_iter()
@ -2462,7 +2451,7 @@ impl ChatWidget {
SelectionItem {
name: preset.display_name.clone(),
description,
is_current: model == current_model,
is_current: model == self.model,
is_default: preset.is_default,
actions,
dismiss_on_select: true,
@ -2525,12 +2514,11 @@ impl ChatWidget {
return;
}
let current_model = self.model_family.get_model_slug().to_string();
let mut items: Vec<SelectionItem> = Vec::new();
for preset in presets.into_iter() {
let description =
(!preset.description.is_empty()).then_some(preset.description.to_string());
let is_current = preset.model == current_model;
let is_current = preset.model == self.model;
let single_supported_effort = preset.supported_reasoning_efforts.len() == 1;
let preset_for_action = preset.clone();
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
@ -2656,7 +2644,7 @@ impl ChatWidget {
.or(Some(default_effort));
let model_slug = preset.model.to_string();
let is_current_model = self.model_family.get_model_slug() == preset.model;
let is_current_model = self.model == preset.model;
let highlight_choice = if is_current_model {
self.config.model_reasoning_effort
} else {
@ -3246,9 +3234,9 @@ impl ChatWidget {
}
/// Set the model in the widget's config copy.
pub(crate) fn set_model(&mut self, model: &str, model_family: ModelFamily) {
pub(crate) fn set_model(&mut self, model: &str) {
self.session_header.set_model(model);
self.model_family = model_family;
self.model = model.to_string();
}
pub(crate) fn add_info_message(&mut self, message: String, hint: Option<String>) {

View file

@ -313,7 +313,6 @@ async fn helpers_are_available_and_do_not_panic() {
let tx = AppEventSender::new(tx_raw);
let cfg = test_config().await;
let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref());
let model_family = ModelsManager::construct_model_family_offline(&resolved_model, &cfg);
let conversation_manager = Arc::new(ConversationManager::with_models_provider(
CodexAuth::from_api_key("test"),
cfg.model_provider.clone(),
@ -330,7 +329,7 @@ async fn helpers_are_available_and_do_not_panic() {
models_manager: conversation_manager.get_models_manager(),
feedback: codex_feedback::CodexFeedback::new(),
is_first_run: true,
model_family,
model: resolved_model,
};
let mut w = ChatWidget::new(init, conversation_manager);
// Basic construction sanity.
@ -371,11 +370,11 @@ async fn make_chatwidget_manual(
codex_op_tx: op_tx,
bottom_pane: bottom,
active_cell: None,
config: cfg.clone(),
model_family: ModelsManager::construct_model_family_offline(&resolved_model, &cfg),
config: cfg,
model: resolved_model.clone(),
auth_manager: auth_manager.clone(),
models_manager: Arc::new(ModelsManager::new(auth_manager)),
session_header: SessionHeader::new(resolved_model.clone()),
session_header: SessionHeader::new(resolved_model),
initial_user_message: None,
token_info: None,
rate_limit_snapshot: None,

View file

@ -7,10 +7,10 @@ use chrono::DateTime;
use chrono::Local;
use codex_common::create_config_summary_entries;
use codex_core::config::Config;
use codex_core::models_manager::model_family::ModelFamily;
use codex_core::protocol::NetworkAccess;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::ConversationId;
use codex_protocol::account::PlanType;
use ratatui::prelude::*;
@ -72,9 +72,8 @@ struct StatusHistoryCell {
pub(crate) fn new_status_output(
config: &Config,
auth_manager: &AuthManager,
model_family: &ModelFamily,
token_info: Option<&TokenUsageInfo>,
total_usage: &TokenUsage,
context_usage: Option<&TokenUsage>,
session_id: &Option<ConversationId>,
rate_limits: Option<&RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
@ -85,9 +84,8 @@ pub(crate) fn new_status_output(
let card = StatusHistoryCell::new(
config,
auth_manager,
model_family,
token_info,
total_usage,
context_usage,
session_id,
rate_limits,
plan_type,
@ -103,9 +101,8 @@ impl StatusHistoryCell {
fn new(
config: &Config,
auth_manager: &AuthManager,
model_family: &ModelFamily,
token_info: Option<&TokenUsageInfo>,
total_usage: &TokenUsage,
context_usage: Option<&TokenUsage>,
session_id: &Option<ConversationId>,
rate_limits: Option<&RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
@ -134,12 +131,15 @@ impl StatusHistoryCell {
let agents_summary = compose_agents_summary(config);
let account = compose_account_display(auth_manager, plan_type);
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
let context_window = model_family.context_window.and_then(|window| {
context_usage.map(|usage| StatusContextWindowData {
percent_remaining: usage.percent_of_context_window_remaining(window),
tokens_in_context: usage.tokens_in_context_window(),
window,
})
let default_usage = TokenUsage::default();
let (context_usage, context_window) = match token_info {
Some(info) => (&info.last_token_usage, info.model_context_window),
None => (&default_usage, config.model_context_window),
};
let context_window = context_window.map(|window| StatusContextWindowData {
percent_remaining: context_usage.percent_of_context_window_remaining(window),
tokens_in_context: context_usage.tokens_in_context_window(),
window,
});
let token_usage = StatusTokenUsageData {
@ -348,6 +348,7 @@ impl HistoryCell for StatusHistoryCell {
if self.token_usage.context_window.is_some() {
push_label(&mut labels, &mut seen, "Context window");
}
self.collect_rate_limit_labels(&mut seen, &mut labels);
let formatter = FieldFormatter::from_labels(labels.iter().map(String::as_str));

View file

@ -8,12 +8,12 @@ use codex_core::AuthManager;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::models_manager::manager::ModelsManager;
use codex_core::models_manager::model_family::ModelFamily;
use codex_core::protocol::CreditsSnapshot;
use codex_core::protocol::RateLimitSnapshot;
use codex_core::protocol::RateLimitWindow;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ReasoningEffort;
use insta::assert_snapshot;
@ -37,8 +37,15 @@ fn test_auth_manager(config: &Config) -> AuthManager {
)
}
fn test_model_family(model_slug: &str, config: &Config) -> ModelFamily {
ModelsManager::construct_model_family_offline(model_slug, config)
fn token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo {
let context_window = ModelsManager::construct_model_family_offline(model_slug, config)
.context_window
.or(config.model_context_window);
TokenUsageInfo {
total_token_usage: usage.clone(),
last_token_usage: usage.clone(),
model_context_window: context_window,
}
}
fn render_lines(lines: &[Line<'static>]) -> Vec<String> {
@ -132,14 +139,13 @@ async fn status_snapshot_includes_reasoning_details() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -190,13 +196,12 @@ async fn status_snapshot_includes_monthly_limit() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -235,13 +240,12 @@ async fn status_snapshot_shows_unlimited_credits() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -279,13 +283,12 @@ async fn status_snapshot_shows_positive_credits() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -323,13 +326,12 @@ async fn status_snapshot_hides_zero_credits() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -365,13 +367,12 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -407,13 +408,12 @@ async fn status_card_token_usage_excludes_cached_tokens() {
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
None,
None,
@ -464,13 +464,12 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -510,13 +509,12 @@ async fn status_snapshot_shows_missing_limits_message() {
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
None,
None,
@ -574,13 +572,12 @@ async fn status_snapshot_includes_credits_and_limits() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -626,13 +623,12 @@ async fn status_snapshot_shows_empty_limits_message() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -687,13 +683,12 @@ async fn status_snapshot_shows_stale_limits_message() {
let now = captured_at + ChronoDuration::minutes(20);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -752,13 +747,12 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
let now = captured_at + ChronoDuration::minutes(20);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -803,13 +797,16 @@ async fn status_context_window_uses_last_usage() {
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = TokenUsageInfo {
total_token_usage: total_usage.clone(),
last_token_usage: last_usage,
model_context_window: config.model_context_window,
};
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&total_usage,
Some(&last_usage),
&None,
None,
None,

View file

@ -410,10 +410,6 @@ impl App {
}
let enhanced_keys_supported = tui.enhanced_keys_supported();
let model_family = conversation_manager
.get_models_manager()
.construct_model_family(model.as_str(), &config)
.await;
let mut chat_widget = match resume_selection {
ResumeSelection::StartFresh | ResumeSelection::Exit => {
let init = crate::chatwidget::ChatWidgetInit {
@ -427,7 +423,7 @@ impl App {
models_manager: conversation_manager.get_models_manager(),
feedback: feedback.clone(),
is_first_run,
model_family: model_family.clone(),
model: model.clone(),
};
ChatWidget::new(init, conversation_manager.clone())
}
@ -453,7 +449,7 @@ impl App {
models_manager: conversation_manager.get_models_manager(),
feedback: feedback.clone(),
is_first_run,
model_family: model_family.clone(),
model: model.clone(),
};
ChatWidget::new_from_existing(
init,
@ -1328,11 +1324,6 @@ impl App {
}
async fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result<bool> {
let model_family = self
.server
.get_models_manager()
.construct_model_family(self.current_model.as_str(), &self.config)
.await;
match event {
AppEvent::NewSession => {
let summary = session_summary(
@ -1351,10 +1342,9 @@ impl App {
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
model_family: model_family.clone(),
model: self.current_model.clone(),
};
self.chat_widget = ChatWidget::new(init, self.server.clone());
self.current_model = model_family.get_model_slug().to_string();
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> = vec![summary.usage_line.clone().into()];
if let Some(command) = summary.resume_command {
@ -1401,14 +1391,13 @@ impl App {
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
model_family: model_family.clone(),
model: self.current_model.clone(),
};
self.chat_widget = ChatWidget::new_from_existing(
init,
resumed.conversation,
resumed.session_configured,
);
self.current_model = model_family.get_model_slug().to_string();
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> =
vec![summary.usage_line.clone().into()];
@ -1534,12 +1523,7 @@ impl App {
self.on_update_reasoning_effort(effort);
}
AppEvent::UpdateModel(model) => {
let model_family = self
.server
.get_models_manager()
.construct_model_family(&model, &self.config)
.await;
self.chat_widget.set_model(&model, model_family);
self.chat_widget.set_model(&model);
self.current_model = model;
}
AppEvent::OpenReasoningPopup { model } => {
@ -2087,7 +2071,7 @@ mod tests {
async fn make_test_app() -> App {
let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await;
let config = chat_widget.config_ref().clone();
let current_model = chat_widget.get_model_family().get_model_slug().to_string();
let current_model = "gpt-5.2-codex".to_string();
let server = Arc::new(ConversationManager::with_models_provider(
CodexAuth::from_api_key("Test API Key"),
config.model_provider.clone(),
@ -2136,7 +2120,7 @@ mod tests {
) {
let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await;
let config = chat_widget.config_ref().clone();
let current_model = chat_widget.get_model_family().get_model_slug().to_string();
let current_model = "gpt-5.2-codex".to_string();
let server = Arc::new(ConversationManager::with_models_provider(
CodexAuth::from_api_key("Test API Key"),
config.model_provider.clone(),

View file

@ -338,10 +338,9 @@ impl App {
) {
let conv = new_conv.conversation;
let session_configured = new_conv.session_configured;
let model_family = self.chat_widget.get_model_family();
let init = crate::chatwidget::ChatWidgetInit {
config: cfg,
model_family: model_family.clone(),
model: self.current_model.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
@ -354,7 +353,6 @@ impl App {
};
self.chat_widget =
crate::chatwidget::ChatWidget::new_from_existing(init, conv, session_configured);
self.current_model = model_family.get_model_slug().to_string();
// Trim transcript up to the selected user message and re-render it.
self.trim_transcript_for_backtrack(nth_user_message);
self.render_transcript_once(tui);

View file

@ -13,7 +13,6 @@ use codex_core::config::types::Notifications;
use codex_core::git_info::current_branch_name;
use codex_core::git_info::local_git_branches;
use codex_core::models_manager::manager::ModelsManager;
use codex_core::models_manager::model_family::ModelFamily;
use codex_core::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
use codex_core::protocol::AgentMessageDeltaEvent;
use codex_core::protocol::AgentMessageEvent;
@ -267,7 +266,7 @@ pub(crate) struct ChatWidgetInit {
pub(crate) models_manager: Arc<ModelsManager>,
pub(crate) feedback: codex_feedback::CodexFeedback,
pub(crate) is_first_run: bool,
pub(crate) model_family: ModelFamily,
pub(crate) model: String,
}
#[derive(Default)]
@ -284,7 +283,7 @@ pub(crate) struct ChatWidget {
bottom_pane: BottomPane,
active_cell: Option<Box<dyn HistoryCell>>,
config: Config,
model_family: ModelFamily,
model: String,
auth_manager: Arc<AuthManager>,
models_manager: Arc<ModelsManager>,
session_header: SessionHeader,
@ -573,12 +572,10 @@ impl ChatWidget {
}
fn context_remaining_percent(&self, info: &TokenUsageInfo) -> Option<i64> {
info.model_context_window
.or(self.model_family.context_window)
.map(|window| {
info.last_token_usage
.percent_of_context_window_remaining(window)
})
info.model_context_window.map(|window| {
info.last_token_usage
.percent_of_context_window_remaining(window)
})
}
fn context_used_tokens(&self, info: &TokenUsageInfo, percent_known: bool) -> Option<i64> {
@ -646,7 +643,7 @@ impl ChatWidget {
if high_usage
&& !self.rate_limit_switch_prompt_hidden()
&& self.model_family.get_model_slug() != NUDGE_MODEL_SLUG
&& self.model != NUDGE_MODEL_SLUG
&& !matches!(
self.rate_limit_switch_prompt,
RateLimitSwitchPromptState::Shown
@ -680,9 +677,6 @@ impl ChatWidget {
self.stream_controller = None;
self.maybe_show_pending_rate_limit_prompt();
}
pub(crate) fn get_model_family(&self) -> ModelFamily {
self.model_family.clone()
}
fn on_error(&mut self, message: String) {
self.finalize_turn();
@ -1280,11 +1274,10 @@ impl ChatWidget {
models_manager,
feedback,
is_first_run,
model_family,
model,
} = common;
let model_slug = model_family.get_model_slug().to_string();
let mut config = config;
config.model = Some(model_slug.clone());
config.model = Some(model.clone());
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager);
@ -1305,10 +1298,10 @@ impl ChatWidget {
}),
active_cell: None,
config,
model_family,
model: model.clone(),
auth_manager,
models_manager,
session_header: SessionHeader::new(model_slug),
session_header: SessionHeader::new(model),
initial_user_message: create_initial_user_message(
initial_prompt.unwrap_or_default(),
initial_images,
@ -1364,10 +1357,9 @@ impl ChatWidget {
auth_manager,
models_manager,
feedback,
model_family,
model,
..
} = common;
let model_slug = model_family.get_model_slug().to_string();
let mut rng = rand::rng();
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
@ -1390,10 +1382,10 @@ impl ChatWidget {
}),
active_cell: None,
config,
model_family,
model: model.clone(),
auth_manager,
models_manager,
session_header: SessionHeader::new(model_slug),
session_header: SessionHeader::new(model),
initial_user_message: create_initial_user_message(
initial_prompt.unwrap_or_default(),
initial_images,
@ -2052,22 +2044,20 @@ impl ChatWidget {
pub(crate) fn add_status_output(&mut self) {
let default_usage = TokenUsage::default();
let (total_usage, context_usage) = if let Some(ti) = &self.token_info {
(&ti.total_token_usage, Some(&ti.last_token_usage))
} else {
(&default_usage, Some(&default_usage))
};
let token_info = self.token_info.as_ref();
let total_usage = token_info
.map(|ti| &ti.total_token_usage)
.unwrap_or(&default_usage);
self.add_to_history(crate::status::new_status_output(
&self.config,
self.auth_manager.as_ref(),
&self.model_family,
token_info,
total_usage,
context_usage,
&self.conversation_id,
self.rate_limit_snapshot.as_ref(),
self.plan_type,
Local::now(),
self.model_family.get_model_slug(),
&self.model,
));
}
fn stop_rate_limit_poller(&mut self) {
@ -2210,7 +2200,6 @@ impl ChatWidget {
/// Open a popup to choose a quick auto model. Selecting "All models"
/// opens the full picker with every available preset.
pub(crate) fn open_model_popup(&mut self) {
let current_model = self.model_family.get_model_slug().to_string();
let presets: Vec<ModelPreset> =
// todo(aibrahim): make this async function
match self.models_manager.try_list_models(&self.config) {
@ -2227,9 +2216,9 @@ impl ChatWidget {
let current_label = presets
.iter()
.find(|preset| preset.model == current_model)
.find(|preset| preset.model == self.model)
.map(|preset| preset.display_name.to_string())
.unwrap_or_else(|| current_model.clone());
.unwrap_or_else(|| self.model.clone());
let (mut auto_presets, other_presets): (Vec<ModelPreset>, Vec<ModelPreset>) = presets
.into_iter()
@ -2255,7 +2244,7 @@ impl ChatWidget {
SelectionItem {
name: preset.display_name.clone(),
description,
is_current: model == current_model,
is_current: model == self.model,
is_default: preset.is_default,
actions,
dismiss_on_select: true,
@ -2318,12 +2307,11 @@ impl ChatWidget {
return;
}
let current_model = self.model_family.get_model_slug().to_string();
let mut items: Vec<SelectionItem> = Vec::new();
for preset in presets.into_iter() {
let description =
(!preset.description.is_empty()).then_some(preset.description.to_string());
let is_current = preset.model == current_model;
let is_current = preset.model == self.model;
let single_supported_effort = preset.supported_reasoning_efforts.len() == 1;
let preset_for_action = preset.clone();
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
@ -2449,7 +2437,7 @@ impl ChatWidget {
.or(Some(default_effort));
let model_slug = preset.model.to_string();
let is_current_model = self.model_family.get_model_slug() == preset.model;
let is_current_model = self.model == preset.model;
let highlight_choice = if is_current_model {
self.config.model_reasoning_effort
} else {
@ -3008,9 +2996,9 @@ impl ChatWidget {
}
/// Set the model in the widget's config copy.
pub(crate) fn set_model(&mut self, model: &str, model_family: ModelFamily) {
pub(crate) fn set_model(&mut self, model: &str) {
self.session_header.set_model(model);
self.model_family = model_family;
self.model = model.to_string();
}
pub(crate) fn add_info_message(&mut self, message: String, hint: Option<String>) {

View file

@ -311,7 +311,6 @@ async fn helpers_are_available_and_do_not_panic() {
let tx = AppEventSender::new(tx_raw);
let cfg = test_config().await;
let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref());
let model_family = ModelsManager::construct_model_family_offline(&resolved_model, &cfg);
let conversation_manager = Arc::new(ConversationManager::with_models_provider(
CodexAuth::from_api_key("test"),
cfg.model_provider.clone(),
@ -328,7 +327,7 @@ async fn helpers_are_available_and_do_not_panic() {
models_manager: conversation_manager.get_models_manager(),
feedback: codex_feedback::CodexFeedback::new(),
is_first_run: true,
model_family,
model: resolved_model,
};
let mut w = ChatWidget::new(init, conversation_manager);
// Basic construction sanity.
@ -369,11 +368,11 @@ async fn make_chatwidget_manual(
codex_op_tx: op_tx,
bottom_pane: bottom,
active_cell: None,
config: cfg.clone(),
model_family: ModelsManager::construct_model_family_offline(&resolved_model, &cfg),
config: cfg,
model: resolved_model.clone(),
auth_manager: auth_manager.clone(),
models_manager: Arc::new(ModelsManager::new(auth_manager)),
session_header: SessionHeader::new(resolved_model.clone()),
session_header: SessionHeader::new(resolved_model),
initial_user_message: None,
token_info: None,
rate_limit_snapshot: None,

View file

@ -7,10 +7,10 @@ use chrono::DateTime;
use chrono::Local;
use codex_common::create_config_summary_entries;
use codex_core::config::Config;
use codex_core::models_manager::model_family::ModelFamily;
use codex_core::protocol::NetworkAccess;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::ConversationId;
use codex_protocol::account::PlanType;
use ratatui::prelude::*;
@ -72,9 +72,8 @@ struct StatusHistoryCell {
pub(crate) fn new_status_output(
config: &Config,
auth_manager: &AuthManager,
model_family: &ModelFamily,
token_info: Option<&TokenUsageInfo>,
total_usage: &TokenUsage,
context_usage: Option<&TokenUsage>,
session_id: &Option<ConversationId>,
rate_limits: Option<&RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
@ -85,9 +84,8 @@ pub(crate) fn new_status_output(
let card = StatusHistoryCell::new(
config,
auth_manager,
model_family,
token_info,
total_usage,
context_usage,
session_id,
rate_limits,
plan_type,
@ -103,9 +101,8 @@ impl StatusHistoryCell {
fn new(
config: &Config,
auth_manager: &AuthManager,
model_family: &ModelFamily,
token_info: Option<&TokenUsageInfo>,
total_usage: &TokenUsage,
context_usage: Option<&TokenUsage>,
session_id: &Option<ConversationId>,
rate_limits: Option<&RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
@ -134,12 +131,15 @@ impl StatusHistoryCell {
let agents_summary = compose_agents_summary(config);
let account = compose_account_display(auth_manager, plan_type);
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
let context_window = model_family.context_window.and_then(|window| {
context_usage.map(|usage| StatusContextWindowData {
percent_remaining: usage.percent_of_context_window_remaining(window),
tokens_in_context: usage.tokens_in_context_window(),
window,
})
let default_usage = TokenUsage::default();
let (context_usage, context_window) = match token_info {
Some(info) => (&info.last_token_usage, info.model_context_window),
None => (&default_usage, config.model_context_window),
};
let context_window = context_window.map(|window| StatusContextWindowData {
percent_remaining: context_usage.percent_of_context_window_remaining(window),
tokens_in_context: context_usage.tokens_in_context_window(),
window,
});
let token_usage = StatusTokenUsageData {

View file

@ -8,12 +8,12 @@ use codex_core::AuthManager;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::models_manager::manager::ModelsManager;
use codex_core::models_manager::model_family::ModelFamily;
use codex_core::protocol::CreditsSnapshot;
use codex_core::protocol::RateLimitSnapshot;
use codex_core::protocol::RateLimitWindow;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ReasoningEffort;
use insta::assert_snapshot;
@ -37,8 +37,15 @@ fn test_auth_manager(config: &Config) -> AuthManager {
)
}
fn test_model_family(model_slug: &str, config: &Config) -> ModelFamily {
ModelsManager::construct_model_family_offline(model_slug, config)
fn token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo {
let context_window = ModelsManager::construct_model_family_offline(model_slug, config)
.context_window
.or(config.model_context_window);
TokenUsageInfo {
total_token_usage: usage.clone(),
last_token_usage: usage.clone(),
model_context_window: context_window,
}
}
fn render_lines(lines: &[Line<'static>]) -> Vec<String> {
@ -132,14 +139,13 @@ async fn status_snapshot_includes_reasoning_details() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -155,7 +161,6 @@ async fn status_snapshot_includes_reasoning_details() {
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_snapshot_includes_monthly_limit() {
let temp_home = TempDir::new().expect("temp home");
@ -190,13 +195,12 @@ async fn status_snapshot_includes_monthly_limit() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -212,7 +216,6 @@ async fn status_snapshot_includes_monthly_limit() {
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_snapshot_shows_unlimited_credits() {
let temp_home = TempDir::new().expect("temp home");
@ -235,13 +238,12 @@ async fn status_snapshot_shows_unlimited_credits() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -279,13 +281,12 @@ async fn status_snapshot_shows_positive_credits() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -323,13 +324,12 @@ async fn status_snapshot_hides_zero_credits() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -365,13 +365,12 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -407,13 +406,12 @@ async fn status_card_token_usage_excludes_cached_tokens() {
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
None,
None,
@ -464,13 +462,12 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -510,13 +507,12 @@ async fn status_snapshot_shows_missing_limits_message() {
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
None,
None,
@ -532,7 +528,6 @@ async fn status_snapshot_shows_missing_limits_message() {
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_snapshot_includes_credits_and_limits() {
let temp_home = TempDir::new().expect("temp home");
@ -574,13 +569,12 @@ async fn status_snapshot_includes_credits_and_limits() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -626,13 +620,12 @@ async fn status_snapshot_shows_empty_limits_message() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -687,13 +680,12 @@ async fn status_snapshot_shows_stale_limits_message() {
let now = captured_at + ChronoDuration::minutes(20);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -752,13 +744,12 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
let now = captured_at + ChronoDuration::minutes(20);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&usage,
Some(&usage),
&None,
Some(&rate_display),
None,
@ -803,13 +794,16 @@ async fn status_context_window_uses_last_usage() {
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_family = test_model_family(&model_slug, &config);
let token_info = TokenUsageInfo {
total_token_usage: total_usage.clone(),
last_token_usage: last_usage,
model_context_window: config.model_context_window,
};
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
Some(&token_info),
&total_usage,
Some(&last_usage),
&None,
None,
None,