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
10 changes: 8 additions & 2 deletions codex-rs/app-server/tests/suite/v2/turn_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ use codex_app_server_protocol::UserInput as V2UserInput;
use codex_app_server_protocol::WarningNotification;
use codex_config::config_toml::ConfigToml;
use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME;
use codex_core::test_support::all_model_presets;
use codex_features::FEATURES;
use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
Expand Down Expand Up @@ -375,13 +376,19 @@ async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> {
"never",
&BTreeMap::default(),
)?;
write_models_cache(codex_home.path())?;
let service_tier_model = all_model_presets()
.iter()
.find(|preset| preset.show_in_picker && !preset.service_tiers.is_empty())
.expect("bundled model catalog should include a picker model with service tiers");
let service_tier_id = service_tier_model.service_tiers[0].id.clone();

let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
model: Some(service_tier_model.id.clone()),
..Default::default()
})
.await?;
Expand All @@ -392,7 +399,6 @@ async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> {
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;

let service_tier_id = "experimental-tier-id".to_string();
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,8 @@ impl ModelClient {
prompt.output_schema_strict,
);
let prompt_cache_key = Some(self.state.thread_id.to_string());
let service_tier =
service_tier.filter(|service_tier| model_info.supports_service_tier(service_tier));
let request = ResponsesApiRequest {
model: model_info.slug.clone(),
instructions: instructions.clone(),
Expand Down
9 changes: 5 additions & 4 deletions codex-rs/core/src/config/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use codex_config::types::SessionPickerViewMode;
use codex_config::types::ToolSuggestDisabledTool;
use codex_features::FEATURES;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::openai_models::ReasoningEffort;
use std::collections::BTreeMap;
Expand All @@ -33,7 +32,7 @@ pub enum ConfigEdit {
effort: Option<ReasoningEffort>,
},
/// Update the service tier preference for future turns.
SetServiceTier { service_tier: Option<ServiceTier> },
SetServiceTier { service_tier: Option<String> },
/// Update the active (or default) model personality.
SetModelPersonality { personality: Option<Personality> },
/// Toggle the acknowledgement flag under `[notice]`.
Expand Down Expand Up @@ -536,7 +535,9 @@ impl ConfigDocument {
}),
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value(
&["service_tier"],
service_tier.map(|service_tier| value(service_tier.to_string())),
service_tier
.as_ref()
.map(|service_tier| value(service_tier.clone())),
)),
ConfigEdit::SetModelPersonality { personality } => Ok(self.write_profile_value(
&["personality"],
Expand Down Expand Up @@ -1114,7 +1115,7 @@ impl ConfigEditsBuilder {
self
}

pub fn set_service_tier(mut self, service_tier: Option<ServiceTier>) -> Self {
pub fn set_service_tier(mut self, service_tier: Option<String>) -> Self {
self.edits.push(ConfigEdit::SetServiceTier { service_tier });
self
}
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/session/turn_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ impl Session {
&per_turn_config.agent_roles,
));

let mut per_turn_config = per_turn_config;
per_turn_config.service_tier = per_turn_config
.service_tier
.filter(|service_tier| model_info.supports_service_tier(service_tier));
let per_turn_config = Arc::new(per_turn_config);
let turn_metadata_state = Arc::new(TurnMetadataState::new(
session_id.to_string(),
Expand Down
55 changes: 54 additions & 1 deletion codex-rs/core/tests/suite/model_switching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
Expand Down Expand Up @@ -320,9 +321,28 @@ async fn flex_service_tier_is_applied_to_http_turn() -> Result<()> {
skip_if_no_network!(Ok(()));

let server = start_mock_server().await;
let model_slug = "test-flex-model";
let mut flex_model = test_model_info(
model_slug,
model_slug,
"supports flex tier",
default_input_modalities(),
);
flex_model.service_tiers = vec![ModelServiceTier {
id: ServiceTier::Flex.request_value().to_string(),
name: "flex".to_string(),
description: "Flexible processing.".to_string(),
}];
let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;

let test = test_codex().build(&server).await?;
let mut builder = test_codex()
.with_model(model_slug)
.with_config(move |config| {
config.model_catalog = Some(ModelsResponse {
models: vec![flex_model],
});
});
let test = builder.build(&server).await?;

test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::Flex))
.await?;
Expand All @@ -334,6 +354,39 @@ async fn flex_service_tier_is_applied_to_http_turn() -> Result<()> {
Ok(())
}

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

let server = start_mock_server().await;
let model_slug = "test-no-tier-model";
let model = test_model_info(
model_slug,
model_slug,
"no service tiers",
default_input_modalities(),
);
let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await;

let mut builder = test_codex()
.with_model(model_slug)
.with_config(move |config| {
config.model_catalog = Some(ModelsResponse {
models: vec![model],
});
});
let test = builder.build(&server).await?;

test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::Fast))
.await?;

let request = resp_mock.single_request();
let body = request.body_json();
assert_eq!(body.get("service_tier"), None);

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<()> {
skip_if_no_network!(Ok(()));
Expand Down
11 changes: 9 additions & 2 deletions codex-rs/model-provider/src/amazon_bedrock/catalog.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use codex_models_manager::model_info::BASE_INSTRUCTIONS;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Verbosity;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::openai_models::SPEED_TIER_FAST;
use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::openai_models::WebSearchToolType;

Expand Down Expand Up @@ -46,8 +49,12 @@ fn gpt_5_4_cmb_bedrock_model(priority: i32) -> ModelInfo {
visibility: ModelVisibility::List,
supported_in_api: true,
priority,
additional_speed_tiers: vec!["fast".to_string()],
service_tiers: Vec::new(),
additional_speed_tiers: Vec::new(),
service_tiers: vec![ModelServiceTier {
id: ServiceTier::Fast.request_value().to_string(),
name: SPEED_TIER_FAST.to_string(),
description: "Fastest inference with increased plan usage".to_string(),
}],
availability_nux: None,
upgrade: None,
base_instructions: BASE_INSTRUCTIONS.to_string(),
Expand Down
15 changes: 13 additions & 2 deletions codex-rs/protocol/src/openai_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use ts_rs::TS;

use crate::config_types::Personality;
use crate::config_types::ReasoningSummary;
use crate::config_types::ServiceTier;
use crate::config_types::Verbosity;

const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
Expand Down Expand Up @@ -479,13 +480,23 @@ impl ModelPreset {
pub fn supports_fast_mode(&self) -> bool {
self.service_tiers
.iter()
.any(|tier| tier.id == SPEED_TIER_FAST)
.any(|tier| tier.id == ServiceTier::Fast.request_value())
|| self
.additional_speed_tiers
.iter()
.any(|tier| tier == SPEED_TIER_FAST)
}
}

impl ModelInfo {
pub fn supports_service_tier(&self, service_tier: &str) -> bool {
self.service_tiers
.iter()
.any(|tier| tier.id == service_tier)
}
}

impl ModelPreset {
/// Filter models based on authentication mode.
///
/// In ChatGPT mode, all models are visible. Otherwise, only API-supported models are shown.
Expand Down Expand Up @@ -853,7 +864,7 @@ mod tests {
fn model_preset_supports_fast_mode_from_service_tiers() {
let preset = ModelPreset::from(ModelInfo {
service_tiers: vec![ModelServiceTier {
id: SPEED_TIER_FAST.to_string(),
id: ServiceTier::Fast.request_value().to_string(),
name: "Fast".to_string(),
description: "Priority processing.".to_string(),
}],
Expand Down
21 changes: 8 additions & 13 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1274,26 +1274,21 @@ impl App {
AppEvent::PersistServiceTierSelection { service_tier } => {
self.refresh_status_line();
let profile = self.active_profile.as_deref();
self.config.service_tier =
service_tier.map(|service_tier| service_tier.request_value().to_string());
self.config.service_tier = service_tier.clone();
let mut edits = ConfigEditsBuilder::new(&self.config.codex_home)
.with_profile(profile)
.set_service_tier(service_tier);
.set_service_tier(service_tier.clone());
if service_tier.is_none() {
self.config.notices.fast_default_opt_out = Some(true);
edits = edits.set_fast_default_opt_out(/*opted_out*/ true);
}
match edits.apply().await {
Ok(()) => {
let status = if matches!(
service_tier,
Some(codex_protocol::config_types::ServiceTier::Fast)
) {
"on"
let mut message = if let Some(service_tier) = service_tier {
format!("Service tier set to {service_tier}")
} else {
"off"
"Service tier cleared".to_string()
};
let mut message = format!("Fast mode set to {status}");
if let Some(profile) = profile {
message.push_str(" for ");
message.push_str(profile);
Expand All @@ -1302,14 +1297,14 @@ impl App {
self.chat_widget.add_info_message(message, /*hint*/ None);
}
Err(err) => {
tracing::error!(error = %err, "failed to persist fast mode selection");
tracing::error!(error = %err, "failed to persist service tier selection");
if let Some(profile) = profile {
self.chat_widget.add_error_message(format!(
"Failed to save Fast mode for profile `{profile}`: {err}"
"Failed to save service tier for profile `{profile}`: {err}"
));
} else {
self.chat_widget.add_error_message(format!(
"Failed to save default Fast mode: {err}"
"Failed to save default service tier: {err}"
));
}
}
Expand Down
5 changes: 1 addition & 4 deletions codex-rs/tui/src/app/session_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,10 +617,7 @@ impl App {

pub(super) fn fresh_session_config(&self) -> Config {
let mut config = self.config.clone();
config.service_tier = self
.chat_widget
.configured_service_tier()
.map(|service_tier| service_tier.request_value().to_string());
config.service_tier = self.chat_widget.configured_service_tier();
config.notices.fast_default_opt_out = self.chat_widget.fast_default_opt_out();
config
}
Expand Down
14 changes: 10 additions & 4 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3829,8 +3829,11 @@ async fn clear_ui_header_shows_fast_status_for_fast_capable_models() {
set_fast_mode_test_catalog(&mut app.chat_widget);
app.chat_widget
.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
app.chat_widget
.set_service_tier(Some(codex_protocol::config_types::ServiceTier::Fast));
app.chat_widget.set_service_tier(Some(
codex_protocol::config_types::ServiceTier::Fast
.request_value()
.to_string(),
));
set_chatgpt_auth(&mut app.chat_widget);
set_fast_mode_test_catalog(&mut app.chat_widget);

Expand Down Expand Up @@ -4481,8 +4484,11 @@ fn active_turn_steer_race_extracts_actual_turn_id_from_mismatch() {
#[tokio::test]
async fn fresh_session_config_uses_current_service_tier() {
let mut app = make_test_app().await;
app.chat_widget
.set_service_tier(Some(codex_protocol::config_types::ServiceTier::Fast));
app.chat_widget.set_service_tier(Some(
codex_protocol::config_types::ServiceTier::Fast
.request_value()
.to_string(),
));

let config = app.fresh_session_config();

Expand Down
5 changes: 1 addition & 4 deletions codex-rs/tui/src/app/thread_session_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@ impl App {
thread_name: None,
model: self.chat_widget.current_model().to_string(),
model_provider_id: self.config.model_provider_id.clone(),
service_tier: self
.chat_widget
.current_service_tier()
.map(|service_tier| service_tier.request_value().to_string()),
service_tier: self.chat_widget.current_service_tier().map(str::to_string),
approval_policy: AskForApproval::from(
self.config.permissions.approval_policy.value(),
),
Expand Down
3 changes: 1 addition & 2 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ use codex_features::Feature;
use codex_plugin::PluginCapabilitySummary;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_realtime_webrtc::RealtimeWebrtcEvent;
Expand Down Expand Up @@ -569,7 +568,7 @@ pub(crate) enum AppEvent {

/// Persist the selected service tier to the appropriate config.
PersistServiceTierSelection {
service_tier: Option<ServiceTier>,
service_tier: Option<String>,
},

/// Open the device picker for a realtime microphone or speaker.
Expand Down
Loading
Loading