diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 509d44267..1ae43c01b 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1728,7 +1728,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index cafd8bc5d..fa1df138e 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -5460,7 +5460,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 94e9f3a50..a217af310 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12134,7 +12134,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 0d1de7448..b2e21a80f 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -10770,7 +10770,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index 626db8dae..dd0a86fe9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -707,7 +707,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index ff0bf3f04..d4ef3effd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -53,7 +53,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" } diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 41f4c9b7b..896546347 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -744,7 +744,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 91850f60e..a401c2141 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -759,7 +759,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 416bceb6e..e71203605 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -744,7 +744,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 311b7b40c..0cdc8d459 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -78,7 +78,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" } diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 25db79cf2..57098610c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -744,7 +744,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index 9e749d5b8..b1d5febac 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -305,7 +305,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/ServiceTier.ts b/codex-rs/app-server-protocol/schema/typescript/ServiceTier.ts index bb2b5103e..ce11286db 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServiceTier.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServiceTier.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ServiceTier = "fast"; +export type ServiceTier = "fast" | "flex"; diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 658b2e08d..068d4ee18 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -180,6 +180,7 @@ Start a fresh thread when you need a new Codex conversation. "approvalPolicy": "never", "sandbox": "workspaceWrite", "personality": "friendly", + "serviceTier": "flex", "serviceName": "my_app_server_client", // optional metrics tag (`service_name`) // Experimental: requires opt-in "dynamicTools": [ @@ -208,6 +209,7 @@ Start a fresh thread when you need a new Codex conversation. ``` Valid `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. When `"none"` is selected, the personality placeholder is replaced with an empty string. +Valid `serviceTier` values are `"fast"` and `"flex"`. To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted. You can also pass the same configuration overrides supported by `thread/start`, such as `personality`: @@ -415,6 +417,7 @@ You can optionally specify config overrides on the new turn. If specified, these "networkAccess": true }, "model": "gpt-5.1-codex", + "serviceTier": "flex", "effort": "medium", "summary": "concise", "personality": "friendly", diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 57eddd195..bc383a19d 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -12,6 +12,7 @@ use codex_app_server_protocol::ThreadStartedNotification; use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadStatusChangedNotification; use codex_core::config::set_project_trust_level; +use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::TrustLevel; use codex_protocol::openai_models::ReasoningEffort; use pretty_assertions::assert_eq; @@ -180,6 +181,34 @@ model_reasoning_effort = "high" Ok(()) } +#[tokio::test] +async fn thread_start_accepts_flex_service_tier() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let req_id = mcp + .send_thread_start_request(ThreadStartParams { + service_tier: Some(Some(ServiceTier::Flex)), + ..Default::default() + }) + .await?; + + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(req_id)), + ) + .await??; + let ThreadStartResponse { service_tier, .. } = to_response::(resp)?; + + assert_eq!(service_tier, Some(ServiceTier::Flex)); + Ok(()) +} + #[tokio::test] async fn thread_start_accepts_metrics_service_name() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 71d624d2c..3a108b854 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1336,7 +1336,8 @@ }, "ServiceTier": { "enum": [ - "fast" + "fast", + "flex" ], "type": "string" }, @@ -2146,7 +2147,7 @@ "$ref": "#/definitions/ServiceTier" } ], - "description": "Optional explicit service tier preference for new turns." + "description": "Optional explicit service tier preference for new turns (`fast` or `flex`)." }, "shell_environment_policy": { "allOf": [ diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index d81c10f39..7479623cb 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -546,7 +546,8 @@ impl ModelClientSession { include, service_tier: match service_tier { Some(ServiceTier::Fast) => Some("priority".to_string()), - _ => None, + Some(service_tier) => Some(service_tier.to_string()), + None => None, }, prompt_cache_key, text, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index cddf99d6f..21c4c5666 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -234,6 +234,7 @@ mod tests { use codex_api::common::OpenAiVerbosity; use codex_api::common::TextControls; use codex_api::create_text_param_for_request; + use codex_protocol::config_types::ServiceTier; use codex_protocol::models::FunctionCallOutputPayload; use pretty_assertions::assert_eq; @@ -342,6 +343,31 @@ mod tests { assert!(v.get("text").is_none()); } + #[test] + fn serializes_flex_service_tier_when_set() { + let req = ResponsesApiRequest { + model: "gpt-5.1".to_string(), + instructions: "i".to_string(), + input: vec![], + tools: vec![], + tool_choice: "auto".to_string(), + parallel_tool_calls: true, + reasoning: None, + store: false, + stream: true, + include: vec![], + prompt_cache_key: None, + service_tier: Some(ServiceTier::Flex.to_string()), + text: None, + }; + + let v = serde_json::to_value(&req).expect("json"); + assert_eq!( + v.get("service_tier").and_then(|tier| tier.as_str()), + Some("flex") + ); + } + #[test] fn reserializes_shell_outputs_for_function_and_custom_tool_calls() { let raw_output = r#"{"output":"hello","metadata":{"exit_code":0,"duration_seconds":0.5}}"#; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 2d833d8b5..ca99cce54 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -189,7 +189,7 @@ pub struct Config { /// Optional override of model selection. pub model: Option, - /// Effective service tier preference for new turns. + /// Effective service tier preference for new turns (`fast` or `flex`). pub service_tier: Option, /// Model used specifically for review sessions. @@ -1191,7 +1191,7 @@ pub struct ConfigToml { /// Optionally specify a personality for the model pub personality: Option, - /// Optional explicit service tier preference for new turns. + /// Optional explicit service tier preference for new turns (`fast` or `flex`). pub service_tier: Option, /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). @@ -1984,15 +1984,14 @@ impl Config { let forced_login_method = cfg.forced_login_method; let model = model.or(config_profile.model).or(cfg.model); - let service_tier = if features.enabled(Feature::FastMode) { - service_tier_override.unwrap_or_else(|| { - config_profile - .service_tier - .or(cfg.service_tier) - .filter(|tier| matches!(tier, ServiceTier::Fast)) - }) - } else { - None + let service_tier = service_tier_override + .unwrap_or_else(|| config_profile.service_tier.or(cfg.service_tier)); + let service_tier = match service_tier { + Some(ServiceTier::Fast) if features.enabled(Feature::FastMode) => { + Some(ServiceTier::Fast) + } + Some(ServiceTier::Flex) => Some(ServiceTier::Flex), + _ => None, }; let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| { diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index df02d5e25..c58fb0fea 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -22,6 +22,7 @@ use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_completed_with_tokens; 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::responses::sse_completed; @@ -247,6 +248,25 @@ async fn service_tier_change_is_applied_on_next_http_turn() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn flex_service_tier_is_applied_to_http_turn() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let resp_mock = mount_sse_once(&server, sse_completed("resp-1")).await; + + let test = test_codex().build(&server).await?; + + test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::Flex)) + .await?; + + let request = resp_mock.single_request(); + let body = request.body_json(); + assert_eq!(body["service_tier"].as_str(), Some("flex")); + + 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(())); diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 984dcf39f..b467d3485 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -118,6 +118,7 @@ pub enum WebSearchMode { #[strum(serialize_all = "lowercase")] pub enum ServiceTier { Fast, + Flex, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e4e33e795..840505f36 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3681,7 +3681,7 @@ impl ChatWidget { self.open_model_popup(); } SlashCommand::Fast => { - let next_tier = if self.config.service_tier.is_some() { + let next_tier = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) { None } else { Some(ServiceTier::Fast) @@ -3976,7 +3976,8 @@ impl ChatWidget { "on" => self.set_service_tier_selection(Some(ServiceTier::Fast)), "off" => self.set_service_tier_selection(None), "status" => { - let status = if self.config.service_tier.is_some() { + let status = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) + { "on" } else { "off" @@ -4342,7 +4343,7 @@ impl ChatWidget { .personality .filter(|_| self.config.features.enabled(Feature::Personality)) .filter(|_| self.current_model_supports_personality()); - let service_tier = self.fast_mode_enabled().then_some(self.config.service_tier); + let service_tier = self.config.service_tier.map(Some); let op = Op::UserTurn { items, cwd: self.config.cwd.clone(),