support 'flex' tier in app-server in addition to 'fast' (#13391)

This commit is contained in:
Val Kharitonov
2026-03-03 22:46:05 -08:00
committed by GitHub
Unverified
parent 7134220f3c
commit 4f6c4bb143
22 changed files with 123 additions and 30 deletions
@@ -1728,7 +1728,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -5460,7 +5460,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -12134,7 +12134,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -10770,7 +10770,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -707,7 +707,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -53,7 +53,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
}
@@ -744,7 +744,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -759,7 +759,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -744,7 +744,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -78,7 +78,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
}
@@ -744,7 +744,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -305,7 +305,8 @@
},
"ServiceTier": {
"enum": [
"fast"
"fast",
"flex"
],
"type": "string"
},
@@ -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";
+3
View File
@@ -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",
@@ -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::<ThreadStartResponse>(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;
+3 -2
View File
@@ -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": [
+2 -1
View File
@@ -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,
+26
View File
@@ -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}}"#;
+10 -11
View File
@@ -189,7 +189,7 @@ pub struct Config {
/// Optional override of model selection.
pub model: Option<String>,
/// Effective service tier preference for new turns.
/// Effective service tier preference for new turns (`fast` or `flex`).
pub service_tier: Option<ServiceTier>,
/// Model used specifically for review sessions.
@@ -1191,7 +1191,7 @@ pub struct ConfigToml {
/// Optionally specify a personality for the model
pub personality: Option<Personality>,
/// Optional explicit service tier preference for new turns.
/// Optional explicit service tier preference for new turns (`fast` or `flex`).
pub service_tier: Option<ServiceTier>,
/// 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| {
@@ -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(()));
+1
View File
@@ -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)]
+4 -3
View File
@@ -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(),