mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Honor client-resolved service tier defaults (#23537)
## Why Model catalog responses can now advertise a nullable `default_service_tier` for each model. Codex needs to preserve three distinct states all the way from config/app-server inputs to inference: - no explicit service tier, so the client may apply the current model catalog default when FastMode is enabled - explicit `default`, meaning the user intentionally wants standard routing - explicit catalog tier ids such as `priority`, `flex`, or future tiers Keeping those states distinct prevents the UI from showing one tier while core sends another, especially after model switches or app-server `thread/start` / `turn/start` updates. ## What Changed - Plumbed `default_service_tier` through model catalog protocol types, app-server model responses, generated schemas, model cache fixtures, and provider/model-manager conversions. - Added the request-only `default` service tier sentinel and normalized legacy config spelling so `fast` in `config.toml` still materializes as the runtime/request id `priority`. - Moved catalog default resolution to the TUI/client side, including recomputing the effective service tier when model/FastMode-dependent surfaces change. - Updated app-server thread lifecycle config construction so `serviceTier: null` preserves explicit standard-routing intent by mapping to `default` instead of internal `None`. - Kept core responsible for validating explicit tiers against the current model and stripping `default` before `/v1/responses`, without applying catalog defaults itself. ## Validation - `CARGO_INCREMENTAL=0 cargo build -p codex-cli` - `CARGO_INCREMENTAL=0 cargo test -p codex-app-server model_list` - `cargo test -p codex-tui service_tier` - `cargo test -p codex-protocol service_tier_for_request` - `cargo test -p codex-core get_service_tier` - `RUST_MIN_STACK=8388608 CARGO_INCREMENTAL=0 cargo test -p codex-core service_tier`
This commit is contained in:
committed by
GitHub
Unverified
parent
0e9d222178
commit
370b13afc9
+8
@@ -11346,6 +11346,14 @@
|
||||
"defaultReasoningEffort": {
|
||||
"$ref": "#/definitions/v2/ReasoningEffort"
|
||||
},
|
||||
"defaultServiceTier": {
|
||||
"default": null,
|
||||
"description": "Catalog default service tier id for this model, when one is configured.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+8
@@ -7875,6 +7875,14 @@
|
||||
"defaultReasoningEffort": {
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
"defaultServiceTier": {
|
||||
"default": null,
|
||||
"description": "Catalog default service tier id for this model, when one is configured.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
"defaultReasoningEffort": {
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
"defaultServiceTier": {
|
||||
"default": null,
|
||||
"description": "Catalog default service tier id for this model, when one is configured.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
+5
-1
@@ -12,4 +12,8 @@ export type Model = { id: string, model: string, upgrade: string | null, upgrade
|
||||
/**
|
||||
* Deprecated: use `serviceTiers` instead.
|
||||
*/
|
||||
additionalSpeedTiers: Array<string>, serviceTiers: Array<ModelServiceTier>, isDefault: boolean, };
|
||||
additionalSpeedTiers: Array<string>, serviceTiers: Array<ModelServiceTier>,
|
||||
/**
|
||||
* Catalog default service tier id for this model, when one is configured.
|
||||
*/
|
||||
defaultServiceTier: string | null, isDefault: boolean, };
|
||||
|
||||
@@ -98,6 +98,9 @@ pub struct Model {
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub service_tiers: Vec<ModelServiceTier>,
|
||||
/// Catalog default service tier id for this model, when one is configured.
|
||||
#[serde(default)]
|
||||
pub default_service_tier: Option<String>,
|
||||
// Only one model should be marked as default.
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ Example with notification opt-out:
|
||||
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`.
|
||||
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
|
||||
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
|
||||
|
||||
@@ -53,6 +53,7 @@ fn model_from_preset(preset: ModelPreset) -> Model {
|
||||
description: service_tier.description,
|
||||
})
|
||||
.collect(),
|
||||
default_service_tier: preset.default_service_tier,
|
||||
is_default: preset.is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
priority,
|
||||
additional_speed_tiers: preset.additional_speed_tiers.clone(),
|
||||
service_tiers: preset.service_tiers.clone(),
|
||||
default_service_tier: preset.default_service_tier.clone(),
|
||||
upgrade: preset.upgrade.as_ref().map(Into::into),
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -69,6 +69,7 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
|
||||
description: service_tier.description.clone(),
|
||||
})
|
||||
.collect(),
|
||||
default_service_tier: preset.default_service_tier.clone(),
|
||||
is_default: preset.is_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::test_support::all_model_presets;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
@@ -136,7 +137,7 @@ async fn thread_settings_update_while_turn_is_active_emits_notification() -> Res
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_settings_update_clears_service_tier() -> Result<()> {
|
||||
async fn thread_settings_update_null_service_tier_uses_default() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(vec![
|
||||
create_final_assistant_message_sse_response("done")?,
|
||||
])
|
||||
@@ -181,7 +182,10 @@ async fn thread_settings_update_clears_service_tier() -> Result<()> {
|
||||
let clear_updated = read_thread_settings_updated(&mut mcp).await?;
|
||||
assert_eq!(clear_updated.thread_id, thread.id);
|
||||
assert_eq!(clear_updated.thread_settings.model, model_id);
|
||||
assert_eq!(clear_updated.thread_settings.service_tier, None);
|
||||
assert_eq!(
|
||||
clear_updated.thread_settings.service_tier.as_deref(),
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE)
|
||||
);
|
||||
|
||||
start_text_turn(&mut mcp, thread.id).await?;
|
||||
timeout(
|
||||
|
||||
@@ -28,6 +28,7 @@ use codex_core::config::set_project_trust_level;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -484,7 +485,7 @@ model_reasoning_effort = "high"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_accepts_arbitrary_service_tier_id() -> Result<()> {
|
||||
async fn thread_start_drops_unsupported_service_tier_id() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -508,7 +509,39 @@ async fn thread_start_accepts_arbitrary_service_tier_id() -> Result<()> {
|
||||
.await??;
|
||||
let ThreadStartResponse { service_tier, .. } = to_response::<ThreadStartResponse>(resp)?;
|
||||
|
||||
assert_eq!(service_tier, Some(service_tier_id));
|
||||
// Unsupported catalog ids are dropped at session config time instead of echoed back.
|
||||
assert_eq!(service_tier, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_accepts_default_service_tier() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(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(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())),
|
||||
..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(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ async fn models_client_hits_models_endpoint() {
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -359,7 +359,7 @@ pub struct ConfigToml {
|
||||
pub personality: Option<Personality>,
|
||||
|
||||
/// Optional explicit service tier request id for new turns (for example
|
||||
/// `priority` or `flex`; legacy `fast` also works).
|
||||
/// `default`, `priority`, or `flex`; legacy `fast` also works).
|
||||
pub service_tier: Option<String>,
|
||||
|
||||
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
||||
|
||||
@@ -24,7 +24,7 @@ use codex_protocol::protocol::AskForApproval;
|
||||
pub struct ConfigProfile {
|
||||
pub model: Option<String>,
|
||||
/// Optional explicit service tier request id for new turns (for example
|
||||
/// `priority` or `flex`; legacy `fast` also works).
|
||||
/// `default`, `priority`, or `flex`; legacy `fast` also works).
|
||||
pub service_tier: Option<String>,
|
||||
/// The key in the `model_providers` map identifying the
|
||||
/// [`ModelProviderInfo`] to use.
|
||||
|
||||
@@ -686,7 +686,7 @@
|
||||
"$ref": "#/definitions/SandboxMode"
|
||||
},
|
||||
"service_tier": {
|
||||
"description": "Optional explicit service tier request id for new turns (for example `priority` or `flex`; legacy `fast` also works).",
|
||||
"description": "Optional explicit service tier request id for new turns (for example `default`, `priority`, or `flex`; legacy `fast` also works).",
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
@@ -4783,7 +4783,7 @@
|
||||
"description": "Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`."
|
||||
},
|
||||
"service_tier": {
|
||||
"description": "Optional explicit service tier request id for new turns (for example `priority` or `flex`; legacy `fast` also works).",
|
||||
"description": "Optional explicit service tier request id for new turns (for example `default`, `priority`, or `flex`; legacy `fast` also works).",
|
||||
"type": "string"
|
||||
},
|
||||
"shell_environment_policy": {
|
||||
|
||||
@@ -748,8 +748,7 @@ 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 service_tier = model_info.service_tier_for_request(service_tier);
|
||||
let request = ResponsesApiRequest {
|
||||
model: model_info.slug.clone(),
|
||||
instructions: instructions.clone(),
|
||||
|
||||
@@ -70,6 +70,7 @@ use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
|
||||
@@ -8277,7 +8278,7 @@ alpha = "one\ntwo"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_null_service_tier_override_sets_fast_default_opt_out() -> std::io::Result<()> {
|
||||
async fn explicit_null_service_tier_override_maps_to_default_service_tier() -> std::io::Result<()> {
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
@@ -8291,8 +8292,33 @@ async fn explicit_null_service_tier_override_sets_fast_default_opt_out() -> std:
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.service_tier, None);
|
||||
assert_eq!(config.notices.fast_default_opt_out, Some(true));
|
||||
assert_eq!(
|
||||
config.service_tier,
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
);
|
||||
assert_eq!(config.notices.fast_default_opt_out, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_service_tier_override_uses_default_request_value() -> std::io::Result<()> {
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
fixture.cfg.clone(),
|
||||
ConfigOverrides {
|
||||
cwd: Some(fixture.cwd_path()),
|
||||
service_tier: Some(Some("default".to_string())),
|
||||
..Default::default()
|
||||
},
|
||||
fixture.codex_home(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.service_tier,
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,6 @@ pub enum ConfigEdit {
|
||||
SetNoticeHideFullAccessWarning(bool),
|
||||
/// Toggle the Windows world-writable directories warning acknowledgement flag.
|
||||
SetNoticeHideWorldWritableWarning(bool),
|
||||
/// Toggle the opt-out marker for Codex-managed fast defaults.
|
||||
SetNoticeFastDefaultOptOut(bool),
|
||||
/// Toggle the rate limit model nudge acknowledgement flag.
|
||||
SetNoticeHideRateLimitModelNudge(bool),
|
||||
/// Toggle the model migration prompt acknowledgement flag.
|
||||
@@ -552,6 +550,8 @@ impl ConfigDocument {
|
||||
ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value(
|
||||
&["service_tier"],
|
||||
service_tier.as_ref().map(|service_tier| {
|
||||
// Keep the legacy config spelling stable. Runtime values use
|
||||
// `priority`, but config.toml continues to store it as `fast`.
|
||||
let config_value = match ServiceTier::from_request_value(service_tier) {
|
||||
Some(ServiceTier::Fast) => "fast",
|
||||
Some(ServiceTier::Flex) => "flex",
|
||||
@@ -574,11 +574,6 @@ impl ConfigDocument {
|
||||
&[NOTICE_TABLE_KEY, "hide_world_writable_warning"],
|
||||
value(*acknowledged),
|
||||
)),
|
||||
ConfigEdit::SetNoticeFastDefaultOptOut(opted_out) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, "fast_default_opt_out"],
|
||||
value(*opted_out),
|
||||
)),
|
||||
ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
&[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"],
|
||||
@@ -1182,12 +1177,6 @@ impl ConfigEditsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_fast_default_opt_out(mut self, opted_out: bool) -> Self {
|
||||
self.edits
|
||||
.push(ConfigEdit::SetNoticeFastDefaultOptOut(opted_out));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_hide_rate_limit_model_nudge(mut self, acknowledged: bool) -> Self {
|
||||
self.edits
|
||||
.push(ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged));
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_config::types::McpServerOAuthConfig;
|
||||
use codex_config::types::McpServerToolConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -34,6 +35,20 @@ model_reasoning_effort = "high"
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_service_tier_saves_default_as_default() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.set_service_tier(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()))
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
assert_eq!(contents, "service_tier = \"default\"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_service_tier_saves_priority_as_fast() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
|
||||
@@ -81,6 +81,7 @@ use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
@@ -552,6 +553,7 @@ pub struct Config {
|
||||
pub model: Option<String>,
|
||||
|
||||
/// Effective service tier request id preference for new turns.
|
||||
/// `default` means the user explicitly selected standard routing.
|
||||
pub service_tier: Option<String>,
|
||||
|
||||
/// Model used specifically for review sessions.
|
||||
@@ -3169,15 +3171,10 @@ impl Config {
|
||||
let forced_login_method = cfg.forced_login_method;
|
||||
|
||||
let model = model.or(config_profile.model).or(cfg.model);
|
||||
let mut notices = cfg.notice.unwrap_or_default();
|
||||
let notices = cfg.notice.unwrap_or_default();
|
||||
let service_tier = match service_tier_override {
|
||||
Some(Some(service_tier)) => Some(service_tier),
|
||||
Some(None) => {
|
||||
// Preserve explicit standard/clear intent after the nested override
|
||||
// collapses into `Config.service_tier = None`.
|
||||
notices.fast_default_opt_out = Some(true);
|
||||
None
|
||||
}
|
||||
Some(None) => Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
|
||||
None => config_profile.service_tier.or(cfg.service_tier),
|
||||
};
|
||||
let service_tier = service_tier.and_then(|service_tier| {
|
||||
|
||||
@@ -72,7 +72,6 @@ use codex_otel::current_span_trace_id;
|
||||
use codex_otel::current_span_w3c_trace_context;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkPolicyAmendment;
|
||||
@@ -80,6 +79,7 @@ use codex_protocol::approvals::NetworkPolicyRuleAction;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
@@ -320,7 +320,6 @@ use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
@@ -589,14 +588,10 @@ impl Codex {
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
let account_plan_type = auth_manager
|
||||
.auth_cached()
|
||||
.and_then(|auth| auth.account_plan_type());
|
||||
let service_tier = get_service_tier(
|
||||
config.service_tier.clone(),
|
||||
config.notices.fast_default_opt_out.unwrap_or(false),
|
||||
account_plan_type,
|
||||
config.features.enabled(Feature::FastMode),
|
||||
&model_info,
|
||||
);
|
||||
let session_configuration = SessionConfiguration {
|
||||
provider: config.model_provider.clone(),
|
||||
@@ -798,17 +793,16 @@ impl Codex {
|
||||
|
||||
fn get_service_tier(
|
||||
configured_service_tier: Option<String>,
|
||||
fast_default_opt_out: bool,
|
||||
account_plan_type: Option<AccountPlanType>,
|
||||
fast_mode_enabled: bool,
|
||||
model_info: &ModelInfo,
|
||||
) -> Option<String> {
|
||||
if configured_service_tier.is_some() || fast_default_opt_out || !fast_mode_enabled {
|
||||
return configured_service_tier;
|
||||
if !fast_mode_enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
account_plan_type
|
||||
.is_some_and(is_enterprise_default_service_tier_plan)
|
||||
.then_some(ServiceTier::Fast.request_value().to_string())
|
||||
configured_service_tier.filter(|service_tier| {
|
||||
service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE
|
||||
|| model_info.supports_service_tier(service_tier)
|
||||
})
|
||||
}
|
||||
|
||||
fn session_permission_profile_state_from_config(
|
||||
@@ -817,12 +811,6 @@ fn session_permission_profile_state_from_config(
|
||||
Ok(config.permissions.permission_profile_state().clone())
|
||||
}
|
||||
|
||||
fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool {
|
||||
plan_type == AccountPlanType::Enterprise
|
||||
|| plan_type.is_business_like()
|
||||
|| plan_type.is_team_like()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn completed_session_loop_termination() -> SessionLoopTermination {
|
||||
futures::future::ready(()).boxed().shared()
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::goals::GoalRuntimeState;
|
||||
use crate::skills::SkillError;
|
||||
use crate::state::ActiveTurn;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
@@ -223,12 +224,15 @@ impl SessionConfiguration {
|
||||
if let Some(service_tier) = updates.service_tier.clone() {
|
||||
// TODO(aibrahim): Remove once v2 clients no longer send the legacy
|
||||
// "fast" service tier value.
|
||||
next_configuration.service_tier = service_tier.map(|service_tier| {
|
||||
ServiceTier::from_request_value(&service_tier)
|
||||
.map_or(service_tier, |service_tier| {
|
||||
service_tier.request_value().to_string()
|
||||
})
|
||||
});
|
||||
next_configuration.service_tier = match service_tier {
|
||||
Some(service_tier) => Some(
|
||||
ServiceTier::from_request_value(&service_tier)
|
||||
.map_or(service_tier, |service_tier| {
|
||||
service_tier.request_value().to_string()
|
||||
}),
|
||||
),
|
||||
None => Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
|
||||
};
|
||||
}
|
||||
if let Some(personality) = updates.personality {
|
||||
next_configuration.personality = Some(personality);
|
||||
|
||||
@@ -32,7 +32,7 @@ use codex_models_manager::test_support::get_model_offline_for_tests;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
@@ -43,6 +43,7 @@ use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -3390,92 +3391,143 @@ fn session_telemetry(
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_defaults_enterprise_accounts_to_fast() {
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_default_opt_out*/ false,
|
||||
Some(AccountPlanType::Enterprise),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast.request_value().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_default_opt_out*/ false,
|
||||
Some(AccountPlanType::EnterpriseCbpUsageBased),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast.request_value().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_default_opt_out*/ false,
|
||||
Some(AccountPlanType::Business),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast.request_value().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_default_opt_out*/ false,
|
||||
Some(AccountPlanType::Team),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast.request_value().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_default_opt_out*/ false,
|
||||
Some(AccountPlanType::SelfServeBusinessUsageBased),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
Some(ServiceTier::Fast.request_value().to_string())
|
||||
);
|
||||
fn model_with_default_service_tier(default_service_tier: Option<&str>) -> ModelInfo {
|
||||
let mut model_info = model_info::model_info_from_slug("gpt-5.4");
|
||||
model_info.service_tiers = vec![ModelServiceTier {
|
||||
id: ServiceTier::Fast.request_value().to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Priority processing.".to_string(),
|
||||
}];
|
||||
model_info.default_service_tier = default_service_tier.map(str::to_string);
|
||||
model_info
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_respects_fast_default_opt_out() {
|
||||
fn get_service_tier_does_not_use_model_default_when_absent_and_fast_mode_enabled() {
|
||||
let model_info = model_with_default_service_tier(Some(ServiceTier::Fast.request_value()));
|
||||
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_default_opt_out*/ true,
|
||||
Some(AccountPlanType::Enterprise),
|
||||
/*fast_mode_enabled*/ true,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_does_not_default_non_enterprise_or_disabled_fast_mode() {
|
||||
fn get_service_tier_does_not_use_model_default_when_fast_mode_disabled() {
|
||||
let model_info = model_with_default_service_tier(Some(ServiceTier::Fast.request_value()));
|
||||
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_default_opt_out*/ false,
|
||||
Some(AccountPlanType::Pro),
|
||||
/*fast_mode_enabled*/ true,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_default_opt_out*/ false,
|
||||
Some(AccountPlanType::Enterprise),
|
||||
/*fast_mode_enabled*/ false,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_keeps_supported_explicit_tier() {
|
||||
let model_info = model_with_default_service_tier(Some(ServiceTier::Fast.request_value()));
|
||||
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some(ServiceTier::Fast.request_value().to_string()),
|
||||
/*fast_mode_enabled*/ true,
|
||||
&model_info,
|
||||
),
|
||||
Some(ServiceTier::Fast.request_value().to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_does_not_default_when_model_has_no_default() {
|
||||
let model_info = model_with_default_service_tier(/*default_service_tier*/ None);
|
||||
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_mode_enabled*/ true,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_drops_unsupported_configured_tier_when_fast_mode_enabled() {
|
||||
let model_info = model_with_default_service_tier(Some(ServiceTier::Fast.request_value()));
|
||||
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some("unsupported".to_string()),
|
||||
/*fast_mode_enabled*/ true,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some(ServiceTier::Flex.request_value().to_string()),
|
||||
/*fast_mode_enabled*/ true,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
|
||||
/*fast_mode_enabled*/ true,
|
||||
&model_info,
|
||||
),
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_service_tier_ignores_configured_tier_when_fast_mode_disabled() {
|
||||
let model_info = model_with_default_service_tier(Some(ServiceTier::Fast.request_value()));
|
||||
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some(ServiceTier::Fast.request_value().to_string()),
|
||||
/*fast_mode_enabled*/ false,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()),
|
||||
/*fast_mode_enabled*/ false,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
Some("unsupported".to_string()),
|
||||
/*fast_mode_enabled*/ false,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
get_service_tier(
|
||||
/*configured_service_tier*/ None,
|
||||
/*fast_mode_enabled*/ false,
|
||||
&model_info,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_settings_null_service_tier_update_clears_service_tier() {
|
||||
async fn session_settings_null_service_tier_update_uses_default_service_tier() {
|
||||
let session_configuration = make_session_configuration_for_tests().await;
|
||||
|
||||
let updated = session_configuration
|
||||
@@ -3485,7 +3537,10 @@ async fn session_settings_null_service_tier_update_clears_service_tier() {
|
||||
})
|
||||
.expect("null service tier update should apply");
|
||||
|
||||
assert_eq!(updated.service_tier, None);
|
||||
assert_eq!(
|
||||
updated.service_tier,
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -473,9 +473,11 @@ impl Session {
|
||||
);
|
||||
|
||||
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));
|
||||
per_turn_config.service_tier = get_service_tier(
|
||||
per_turn_config.service_tier,
|
||||
per_turn_config.features.enabled(Feature::FastMode),
|
||||
&model_info,
|
||||
);
|
||||
let per_turn_config = Arc::new(per_turn_config);
|
||||
let turn_metadata_state = Arc::new(TurnMetadataState::new(
|
||||
session_id.to_string(),
|
||||
|
||||
@@ -26,6 +26,7 @@ fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset {
|
||||
name: "Fast".to_string(),
|
||||
description: "1.5x speed, increased usage".to_string(),
|
||||
}],
|
||||
default_service_tier: None,
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker,
|
||||
|
||||
@@ -28,7 +28,6 @@ use codex_login::CodexAuth;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::built_in_model_providers;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -667,13 +666,13 @@ impl TestCodex {
|
||||
pub async fn submit_turn_with_service_tier(
|
||||
&self,
|
||||
prompt: &str,
|
||||
service_tier: Option<ServiceTier>,
|
||||
service_tier: Option<&str>,
|
||||
) -> Result<()> {
|
||||
self.submit_turn_with_permission_profile_context(
|
||||
prompt,
|
||||
AskForApproval::Never,
|
||||
PermissionProfile::Disabled,
|
||||
Some(service_tier.map(|service_tier| service_tier.request_value().to_string())),
|
||||
Some(service_tier.map(str::to_string)),
|
||||
/*environments*/ None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -269,7 +269,7 @@ async fn websocket_v2_first_turn_uses_updated_fast_tier_after_startup_prewarm()
|
||||
assert_eq!(warmup["generate"].as_bool(), Some(false));
|
||||
assert_eq!(warmup.get("service_tier"), None);
|
||||
|
||||
test.submit_turn_with_service_tier("hello", Some(ServiceTier::Fast))
|
||||
test.submit_turn_with_service_tier("hello", Some(ServiceTier::Fast.request_value()))
|
||||
.await?;
|
||||
|
||||
assert_eq!(server.handshakes().len(), 1);
|
||||
@@ -385,7 +385,7 @@ async fn websocket_v2_next_turn_uses_updated_service_tier() -> Result<()> {
|
||||
assert_eq!(warmup["generate"].as_bool(), Some(false));
|
||||
assert_eq!(warmup.get("service_tier"), None);
|
||||
|
||||
test.submit_turn_with_service_tier("first", Some(ServiceTier::Fast))
|
||||
test.submit_turn_with_service_tier("first", Some(ServiceTier::Fast.request_value()))
|
||||
.await?;
|
||||
test.submit_turn_with_service_tier("second", /*service_tier*/ None)
|
||||
.await?;
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
@@ -113,6 +114,7 @@ fn test_model_info(
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
@@ -289,7 +291,7 @@ async fn service_tier_change_is_applied_on_next_http_turn() -> Result<()> {
|
||||
|
||||
let test = test_codex().build(&server).await?;
|
||||
|
||||
test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::Fast))
|
||||
test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::Fast.request_value()))
|
||||
.await?;
|
||||
test.submit_turn_with_service_tier("standard turn", /*service_tier*/ None)
|
||||
.await?;
|
||||
@@ -334,7 +336,7 @@ async fn flex_service_tier_is_applied_to_http_turn() -> Result<()> {
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::Flex))
|
||||
test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::Flex.request_value()))
|
||||
.await?;
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
@@ -367,7 +369,85 @@ async fn unsupported_service_tier_is_omitted_from_http_turn() -> Result<()> {
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::Fast))
|
||||
test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::Fast.request_value()))
|
||||
.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 default_service_tier_override_is_omitted_from_http_turn() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let model_slug = "test-default-tier-model";
|
||||
let mut model = test_model_info(
|
||||
model_slug,
|
||||
model_slug,
|
||||
"has catalog default service tier",
|
||||
default_input_modalities(),
|
||||
);
|
||||
model.service_tiers = vec![ModelServiceTier {
|
||||
id: ServiceTier::Fast.request_value().to_string(),
|
||||
name: "fast".to_string(),
|
||||
description: "Fast processing.".to_string(),
|
||||
}];
|
||||
model.default_service_tier = Some(ServiceTier::Fast.request_value().to_string());
|
||||
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("default turn", Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE))
|
||||
.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 null_service_tier_override_is_omitted_from_http_turn_with_catalog_default() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let model_slug = "test-null-default-tier-model";
|
||||
let mut model = test_model_info(
|
||||
model_slug,
|
||||
model_slug,
|
||||
"has catalog default service tier",
|
||||
default_input_modalities(),
|
||||
);
|
||||
model.service_tiers = vec![ModelServiceTier {
|
||||
id: ServiceTier::Fast.request_value().to_string(),
|
||||
name: "fast".to_string(),
|
||||
description: "Fast processing.".to_string(),
|
||||
}];
|
||||
model.default_service_tier = Some(ServiceTier::Fast.request_value().to_string());
|
||||
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("standard turn", /*service_tier*/ None)
|
||||
.await?;
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
@@ -864,6 +944,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -347,6 +347,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -562,6 +562,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
@@ -671,6 +672,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: Some(ModelMessages {
|
||||
|
||||
@@ -475,6 +475,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
@@ -722,6 +723,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: remote_base.to_string(),
|
||||
model_messages: None,
|
||||
@@ -1203,6 +1205,7 @@ fn test_remote_model_with_policy(
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -1275,6 +1275,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -63,6 +63,7 @@ fn test_model_info(
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers,
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -1357,6 +1357,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
|
||||
@@ -55,6 +55,7 @@ fn gpt_5_4_cmb_bedrock_model(priority: i32) -> ModelInfo {
|
||||
name: SPEED_TIER_FAST.to_string(),
|
||||
description: "Fastest inference with increased plan usage".to_string(),
|
||||
}],
|
||||
default_service_tier: None,
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
@@ -96,6 +97,7 @@ fn bedrock_oss_model(slug: &str, display_name: &str, priority: i32) -> ModelInfo
|
||||
priority,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
|
||||
@@ -77,6 +77,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
|
||||
priority: 99,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: BASE_INSTRUCTIONS.to_string(),
|
||||
|
||||
@@ -432,6 +432,12 @@ pub enum ServiceTier {
|
||||
Flex,
|
||||
}
|
||||
|
||||
/// Request/config sentinel for explicit standard routing.
|
||||
///
|
||||
/// This is not a catalog service tier id. It means the user intentionally
|
||||
/// selected no service tier, so model catalog defaults should not apply.
|
||||
pub const SERVICE_TIER_DEFAULT_REQUEST_VALUE: &str = "default";
|
||||
|
||||
impl ServiceTier {
|
||||
pub const fn request_value(self) -> &'static str {
|
||||
match self {
|
||||
|
||||
@@ -17,6 +17,7 @@ use ts_rs::TS;
|
||||
|
||||
use crate::config_types::Personality;
|
||||
use crate::config_types::ReasoningSummary;
|
||||
use crate::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use crate::config_types::ServiceTier;
|
||||
use crate::config_types::Verbosity;
|
||||
|
||||
@@ -147,6 +148,9 @@ pub struct ModelPreset {
|
||||
/// Service tiers this model can run with.
|
||||
#[serde(default)]
|
||||
pub service_tiers: Vec<ModelServiceTier>,
|
||||
/// Catalog default service tier id for this model.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_service_tier: Option<String>,
|
||||
/// Whether this is the default model for new users.
|
||||
pub is_default: bool,
|
||||
/// recommended upgrade model
|
||||
@@ -270,6 +274,8 @@ pub struct ModelInfo {
|
||||
pub additional_speed_tiers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub service_tiers: Vec<ModelServiceTier>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_service_tier: Option<String>,
|
||||
pub availability_nux: Option<ModelAvailabilityNux>,
|
||||
pub upgrade: Option<ModelInfoUpgrade>,
|
||||
pub base_instructions: String,
|
||||
@@ -455,6 +461,7 @@ impl From<ModelInfo> for ModelPreset {
|
||||
supports_personality,
|
||||
additional_speed_tiers: info.additional_speed_tiers,
|
||||
service_tiers: info.service_tiers,
|
||||
default_service_tier: info.default_service_tier,
|
||||
is_default: false, // default is the highest priority available model
|
||||
upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade {
|
||||
id: upgrade.model.clone(),
|
||||
@@ -493,6 +500,13 @@ impl ModelInfo {
|
||||
.iter()
|
||||
.any(|tier| tier.id == service_tier)
|
||||
}
|
||||
|
||||
pub fn service_tier_for_request(&self, service_tier: Option<String>) -> Option<String> {
|
||||
service_tier.filter(|service_tier| {
|
||||
service_tier != SERVICE_TIER_DEFAULT_REQUEST_VALUE
|
||||
&& self.supports_service_tier(service_tier)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelPreset {
|
||||
@@ -576,6 +590,7 @@ mod tests {
|
||||
priority: 1,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: "base".to_string(),
|
||||
@@ -846,6 +861,7 @@ mod tests {
|
||||
message: "Try Spark.".to_string(),
|
||||
}),
|
||||
additional_speed_tiers: vec![SPEED_TIER_FAST.to_string()],
|
||||
default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),
|
||||
service_tiers: Vec::new(),
|
||||
..test_model(/*spec*/ None)
|
||||
});
|
||||
@@ -857,6 +873,10 @@ mod tests {
|
||||
})
|
||||
);
|
||||
assert!(preset.supports_fast_mode());
|
||||
assert_eq!(
|
||||
preset.default_service_tier,
|
||||
Some(ServiceTier::Fast.request_value().to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -872,4 +892,60 @@ mod tests {
|
||||
|
||||
assert!(preset.supports_fast_mode());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_tier_for_request_omits_explicit_default_tier() {
|
||||
let model = ModelInfo {
|
||||
default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),
|
||||
service_tiers: vec![ModelServiceTier {
|
||||
id: ServiceTier::Fast.request_value().to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Priority processing.".to_string(),
|
||||
}],
|
||||
..test_model(/*spec*/ None)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
model.service_tier_for_request(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_tier_for_request_filters_unsupported_tiers() {
|
||||
let model = ModelInfo {
|
||||
default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),
|
||||
service_tiers: vec![ModelServiceTier {
|
||||
id: ServiceTier::Fast.request_value().to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Priority processing.".to_string(),
|
||||
}],
|
||||
..test_model(/*spec*/ None)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
model.service_tier_for_request(Some(ServiceTier::Fast.request_value().to_string())),
|
||||
Some(ServiceTier::Fast.request_value().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.service_tier_for_request(Some("unsupported".to_string())),
|
||||
None
|
||||
);
|
||||
assert_eq!(model.service_tier_for_request(/*service_tier*/ None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_tier_for_request_does_not_apply_catalog_default() {
|
||||
let model = ModelInfo {
|
||||
default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),
|
||||
service_tiers: vec![ModelServiceTier {
|
||||
id: ServiceTier::Fast.request_value().to_string(),
|
||||
name: "Fast".to_string(),
|
||||
description: "Priority processing.".to_string(),
|
||||
}],
|
||||
..test_model(/*spec*/ None)
|
||||
};
|
||||
|
||||
assert_eq!(model.service_tier_for_request(/*service_tier*/ None), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
|
||||
priority: 0,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
availability_nux: None,
|
||||
upgrade: None,
|
||||
base_instructions: String::new(),
|
||||
|
||||
@@ -767,6 +767,8 @@ impl App {
|
||||
self.chat_widget.set_model(&model);
|
||||
self.sync_active_thread_model_setting(app_server, model)
|
||||
.await;
|
||||
self.sync_active_thread_service_tier_to_cached_session()
|
||||
.await;
|
||||
}
|
||||
AppEvent::UpdatePersonality(personality) => {
|
||||
self.on_update_personality(personality);
|
||||
@@ -1327,9 +1329,6 @@ impl App {
|
||||
profile,
|
||||
service_tier.as_deref(),
|
||||
);
|
||||
if service_tier.is_none() {
|
||||
self.config.notices.fast_default_opt_out = Some(true);
|
||||
}
|
||||
match crate::config_update::write_config_batch(app_server.request_handle(), edits)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -656,7 +656,6 @@ 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();
|
||||
config.notices.fast_default_opt_out = self.chat_widget.fast_default_opt_out();
|
||||
config
|
||||
}
|
||||
pub(super) async fn resume_target_session(
|
||||
|
||||
@@ -464,7 +464,6 @@ impl App {
|
||||
}
|
||||
fork_config.model_reasoning_effort = self.chat_widget.current_reasoning_effort();
|
||||
fork_config.service_tier = self.chat_widget.configured_service_tier();
|
||||
fork_config.notices.fast_default_opt_out = self.chat_widget.fast_default_opt_out();
|
||||
fork_config.ephemeral = true;
|
||||
fork_config.developer_instructions = Some(Self::side_developer_instructions(
|
||||
fork_config.developer_instructions.as_deref(),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use crate::bottom_pane::FeedbackAudience;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::permission_compat::legacy_compatible_permission_profile;
|
||||
use crate::service_tier_resolution;
|
||||
use crate::session_state::MessageHistoryMetadata;
|
||||
use crate::session_state::ThreadSessionState;
|
||||
use crate::status::StatusAccountDisplay;
|
||||
@@ -107,6 +108,7 @@ use codex_app_server_protocol::UserInput;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::approvals::GuardianAssessmentEvent;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -164,6 +166,8 @@ pub(crate) struct AppServerSession {
|
||||
remote_cwd_override: Option<PathBuf>,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
thread_settings_update_supported: bool,
|
||||
default_model: Option<String>,
|
||||
available_models: Vec<ModelPreset>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -205,6 +209,8 @@ impl AppServerSession {
|
||||
remote_cwd_override: None,
|
||||
thread_params_mode,
|
||||
thread_settings_update_supported: true,
|
||||
default_model: None,
|
||||
available_models: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +260,8 @@ impl AppServerSession {
|
||||
})
|
||||
.or_else(|| available_models.first().map(|model| model.model.clone()))
|
||||
.wrap_err("model/list returned no models for TUI bootstrap")?;
|
||||
self.default_model = Some(default_model.clone());
|
||||
self.available_models = available_models.clone();
|
||||
|
||||
let (
|
||||
account_email,
|
||||
@@ -365,12 +373,13 @@ impl AppServerSession {
|
||||
session_start_source: Option<ThreadStartSource>,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let request_id = self.next_request_id();
|
||||
let session_config = self.session_config_with_effective_service_tier(config);
|
||||
let response: ThreadStartResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadStart {
|
||||
request_id,
|
||||
params: thread_start_params_from_config(
|
||||
config,
|
||||
&session_config,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
session_start_source,
|
||||
@@ -389,12 +398,13 @@ impl AppServerSession {
|
||||
thread_id: ThreadId,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let request_id = self.next_request_id();
|
||||
let session_config = self.session_config_with_effective_service_tier(&config);
|
||||
let response: ThreadResumeResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadResume {
|
||||
request_id,
|
||||
params: thread_resume_params_from_config(
|
||||
config.clone(),
|
||||
session_config,
|
||||
thread_id,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
@@ -420,12 +430,13 @@ impl AppServerSession {
|
||||
thread_id: ThreadId,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let request_id = self.next_request_id();
|
||||
let session_config = self.session_config_with_effective_service_tier(&config);
|
||||
let response: ThreadForkResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadFork {
|
||||
request_id,
|
||||
params: thread_fork_params_from_config(
|
||||
config.clone(),
|
||||
session_config,
|
||||
thread_id,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
@@ -448,6 +459,32 @@ impl AppServerSession {
|
||||
self.thread_params_mode
|
||||
}
|
||||
|
||||
fn session_config_with_effective_service_tier(&self, config: &Config) -> Config {
|
||||
let Some(model) = config.model.as_deref().or(self.default_model.as_deref()) else {
|
||||
return config.clone();
|
||||
};
|
||||
let mut session_config = config.clone();
|
||||
match service_tier_resolution::service_tier_update_for_core(
|
||||
config,
|
||||
model,
|
||||
&self.available_models,
|
||||
) {
|
||||
Some(Some(service_tier)) => {
|
||||
session_config.service_tier = Some(service_tier);
|
||||
session_config.notices.fast_default_opt_out = None;
|
||||
}
|
||||
Some(None) => {
|
||||
session_config.service_tier = Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string());
|
||||
session_config.notices.fast_default_opt_out = None;
|
||||
}
|
||||
None => {
|
||||
session_config.service_tier = None;
|
||||
session_config.notices.fast_default_opt_out = None;
|
||||
}
|
||||
}
|
||||
session_config
|
||||
}
|
||||
|
||||
async fn fork_parent_title_from_app_server(
|
||||
&mut self,
|
||||
forked_from_id: Option<&str>,
|
||||
@@ -1159,6 +1196,7 @@ fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
|
||||
description: service_tier.description,
|
||||
})
|
||||
.collect(),
|
||||
default_service_tier: model.default_service_tier,
|
||||
is_default: model.is_default,
|
||||
upgrade,
|
||||
show_in_picker: !model.hidden,
|
||||
@@ -1219,11 +1257,10 @@ fn config_request_overrides_from_config(
|
||||
}
|
||||
|
||||
fn service_tier_override_from_config(config: &Config) -> Option<Option<String>> {
|
||||
config
|
||||
.service_tier
|
||||
.clone()
|
||||
.map(Some)
|
||||
.or_else(|| (config.notices.fast_default_opt_out == Some(true)).then_some(None))
|
||||
config.service_tier.clone().map(Some).or_else(|| {
|
||||
(config.notices.fast_default_opt_out == Some(true))
|
||||
.then(|| Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
fn sandbox_mode_from_permission_profile(
|
||||
|
||||
@@ -64,7 +64,11 @@ impl ChatWidget {
|
||||
let active_cell = Some(Self::placeholder_session_header_cell(&config));
|
||||
|
||||
let current_cwd = Some(config.cwd.to_path_buf());
|
||||
let effective_service_tier = config.service_tier.clone();
|
||||
let effective_service_tier = crate::service_tier_resolution::effective_service_tier(
|
||||
&config,
|
||||
&header_model,
|
||||
&model_catalog.try_list_models().unwrap_or_default(),
|
||||
);
|
||||
let current_terminal_info = terminal_info();
|
||||
let runtime_keymap = RuntimeKeymap::from_config(&config.tui_keymap).ok();
|
||||
let default_keymap = RuntimeKeymap::defaults();
|
||||
|
||||
@@ -331,11 +331,7 @@ impl ChatWidget {
|
||||
.personality
|
||||
.filter(|_| self.config.features.enabled(Feature::Personality))
|
||||
.filter(|_| self.current_model_supports_personality());
|
||||
let service_tier = match self.config.service_tier.clone() {
|
||||
Some(service_tier) => Some(Some(service_tier)),
|
||||
None if self.config.notices.fast_default_opt_out == Some(true) => Some(None),
|
||||
None => None,
|
||||
};
|
||||
let service_tier = self.service_tier_update_for_core();
|
||||
let active_permission_profile = self.config.permissions.active_permission_profile();
|
||||
let op = AppCommand::user_turn(
|
||||
items,
|
||||
|
||||
@@ -4,14 +4,16 @@ use super::ChatWidget;
|
||||
use crate::app_command::AppCommand;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::bottom_pane::slash_commands::ServiceTierCommand;
|
||||
use crate::service_tier_resolution;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::SPEED_TIER_FAST;
|
||||
|
||||
impl ChatWidget {
|
||||
pub(crate) fn set_service_tier(&mut self, service_tier: Option<String>) {
|
||||
self.config.service_tier = service_tier.clone();
|
||||
self.effective_service_tier = service_tier;
|
||||
self.config.service_tier = service_tier;
|
||||
self.refresh_effective_service_tier();
|
||||
self.refresh_model_dependent_surfaces();
|
||||
}
|
||||
|
||||
@@ -23,8 +25,12 @@ impl ChatWidget {
|
||||
self.config.service_tier.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn fast_default_opt_out(&self) -> Option<bool> {
|
||||
self.config.notices.fast_default_opt_out
|
||||
pub(crate) fn service_tier_update_for_core(&self) -> Option<Option<String>> {
|
||||
service_tier_resolution::service_tier_update_for_core(
|
||||
&self.config,
|
||||
self.current_model(),
|
||||
&self.model_catalog.try_list_models().unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn should_show_fast_status(&self, model: &str, service_tier: Option<&str>) -> bool {
|
||||
@@ -50,7 +56,7 @@ impl ChatWidget {
|
||||
return;
|
||||
};
|
||||
let next_tier = if self.current_service_tier() == Some(fast_tier.id.as_str()) {
|
||||
None
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
} else {
|
||||
Some(fast_tier.id)
|
||||
};
|
||||
@@ -59,7 +65,7 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn toggle_service_tier_from_ui(&mut self, command: ServiceTierCommand) {
|
||||
let next_tier = if self.current_service_tier() == Some(command.id.as_str()) {
|
||||
None
|
||||
Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
} else {
|
||||
Some(command.id)
|
||||
};
|
||||
@@ -98,9 +104,6 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn set_service_tier_selection(&mut self, service_tier: Option<String>) {
|
||||
if service_tier.is_none() {
|
||||
self.config.notices.fast_default_opt_out = Some(true);
|
||||
}
|
||||
self.set_service_tier(service_tier.clone());
|
||||
self.app_event_tx
|
||||
.send(AppEvent::CodexOp(AppCommand::override_turn_context(
|
||||
@@ -144,4 +147,12 @@ impl ChatWidget {
|
||||
.into_iter()
|
||||
.find(|tier| tier.name.eq_ignore_ascii_case(SPEED_TIER_FAST))
|
||||
}
|
||||
|
||||
pub(super) fn refresh_effective_service_tier(&mut self) {
|
||||
self.effective_service_tier = service_tier_resolution::effective_service_tier(
|
||||
&self.config,
|
||||
self.current_model(),
|
||||
&self.model_catalog.try_list_models().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
if feature == Feature::FastMode {
|
||||
self.refresh_effective_service_tier();
|
||||
self.sync_service_tier_commands();
|
||||
}
|
||||
if feature == Feature::Personality {
|
||||
@@ -238,6 +239,7 @@ impl ChatWidget {
|
||||
{
|
||||
mask.model = Some(model.to_string());
|
||||
}
|
||||
self.refresh_effective_service_tier();
|
||||
self.refresh_model_dependent_surfaces();
|
||||
}
|
||||
|
||||
@@ -519,6 +521,7 @@ impl ChatWidget {
|
||||
settings.collaboration_mode.settings.model = settings.model;
|
||||
settings.collaboration_mode.settings.reasoning_effort = settings.effort;
|
||||
self.set_effective_collaboration_mode(settings.collaboration_mode);
|
||||
self.refresh_effective_service_tier();
|
||||
self.refresh_status_surfaces();
|
||||
self.sync_service_tier_commands();
|
||||
self.sync_personality_command_enabled();
|
||||
|
||||
@@ -144,6 +144,7 @@ pub(super) use codex_protocol::approvals::GuardianUserAuthorization;
|
||||
pub(super) use codex_protocol::config_types::CollaborationMode;
|
||||
pub(super) use codex_protocol::config_types::ModeKind;
|
||||
pub(super) use codex_protocol::config_types::Personality;
|
||||
pub(super) use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
pub(super) use codex_protocol::config_types::ServiceTier;
|
||||
pub(super) use codex_protocol::models::ActivePermissionProfile;
|
||||
pub(super) use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
|
||||
@@ -246,15 +246,14 @@ pub(crate) fn set_chatgpt_auth(chat: &mut ChatWidget) {
|
||||
}
|
||||
|
||||
fn test_model_info(slug: &str, priority: i32, supports_fast_mode: bool) -> ModelInfo {
|
||||
let service_tiers = if supports_fast_mode {
|
||||
vec![json!({
|
||||
let mut service_tiers = Vec::new();
|
||||
if supports_fast_mode {
|
||||
service_tiers.push(json!({
|
||||
"id": ServiceTier::Fast.request_value(),
|
||||
"name": "fast",
|
||||
"description": "Fastest inference with increased plan usage"
|
||||
})]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
}));
|
||||
}
|
||||
serde_json::from_value(json!({
|
||||
"slug": slug,
|
||||
"display_name": slug,
|
||||
@@ -267,6 +266,7 @@ fn test_model_info(slug: &str, priority: i32, supports_fast_mode: bool) -> Model
|
||||
"priority": priority,
|
||||
"additional_speed_tiers": [],
|
||||
"service_tiers": service_tiers,
|
||||
"default_service_tier": null,
|
||||
"availability_nux": null,
|
||||
"upgrade": null,
|
||||
"base_instructions": "base instructions",
|
||||
|
||||
@@ -2279,6 +2279,7 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() {
|
||||
supports_personality: false,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker,
|
||||
@@ -2500,6 +2501,7 @@ async fn single_reasoning_option_skips_selection() {
|
||||
supports_personality: false,
|
||||
additional_speed_tiers: Vec::new(),
|
||||
service_tiers: Vec::new(),
|
||||
default_service_tier: None,
|
||||
is_default: false,
|
||||
upgrade: None,
|
||||
show_in_picker: true,
|
||||
|
||||
@@ -91,8 +91,19 @@ fn next_add_to_history_event(rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEv
|
||||
async fn service_tier_commands_lowercase_catalog_names() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
let expected_description = preset.service_tiers[0].description.clone();
|
||||
preset.service_tiers[0].name = "Fast".to_string();
|
||||
let expected_description = preset
|
||||
.service_tiers
|
||||
.iter()
|
||||
.find(|tier| tier.id == ServiceTier::Fast.request_value())
|
||||
.expect("fast tier")
|
||||
.description
|
||||
.clone();
|
||||
preset
|
||||
.service_tiers
|
||||
.iter_mut()
|
||||
.find(|tier| tier.id == ServiceTier::Fast.request_value())
|
||||
.expect("fast tier")
|
||||
.name = "Fast".to_string();
|
||||
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![preset]));
|
||||
|
||||
assert_eq!(
|
||||
@@ -2135,6 +2146,47 @@ async fn user_turn_carries_service_tier_after_fast_toggle() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_switch_recomputes_catalog_default_service_tier() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
set_chatgpt_auth(&mut chat);
|
||||
set_fast_mode_test_catalog(&mut chat);
|
||||
chat.set_feature_enabled(Feature::FastMode, /*enabled*/ true);
|
||||
|
||||
let mut models = chat.model_catalog.try_list_models().expect("test catalog");
|
||||
let default_model = models
|
||||
.iter_mut()
|
||||
.find(|model| model.model == "gpt-5.4")
|
||||
.expect("gpt-5.4 test model");
|
||||
default_model.default_service_tier = Some(ServiceTier::Fast.request_value().to_string());
|
||||
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(models));
|
||||
chat.refresh_effective_service_tier();
|
||||
|
||||
assert_eq!(chat.current_service_tier(), None);
|
||||
|
||||
chat.set_model("gpt-5.4");
|
||||
assert_eq!(
|
||||
chat.current_service_tier(),
|
||||
Some(ServiceTier::Fast.request_value())
|
||||
);
|
||||
|
||||
chat.set_model("gpt-5.3-codex");
|
||||
assert_eq!(chat.current_service_tier(), None);
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("hello".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn {
|
||||
service_tier: Some(Some(service_tier)),
|
||||
..
|
||||
} if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE => {}
|
||||
other => panic!("expected Op::UserTurn with default service tier override, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_fast_slash_applies_before_next_queued_message() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
@@ -2194,18 +2246,20 @@ async fn user_turn_sends_standard_override_after_fast_is_turned_off() {
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::CodexOp(Op::OverrideTurnContext {
|
||||
service_tier: Some(None),
|
||||
service_tier: Some(Some(service_tier)),
|
||||
..
|
||||
})
|
||||
}) if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE
|
||||
)),
|
||||
"expected fast-mode off override app event; events: {events:?}"
|
||||
"expected fast-mode off default service tier app event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::PersistServiceTierSelection { service_tier: None }
|
||||
AppEvent::PersistServiceTierSelection {
|
||||
service_tier: Some(service_tier)
|
||||
} if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE
|
||||
)),
|
||||
"expected fast-mode opt-out persistence app event; events: {events:?}"
|
||||
"expected default service tier persistence app event; events: {events:?}"
|
||||
);
|
||||
|
||||
chat.bottom_pane
|
||||
@@ -2214,10 +2268,10 @@ async fn user_turn_sends_standard_override_after_fast_is_turned_off() {
|
||||
|
||||
match next_submit_op(&mut op_rx) {
|
||||
Op::UserTurn {
|
||||
service_tier: Some(None),
|
||||
service_tier: Some(Some(service_tier)),
|
||||
..
|
||||
} => {}
|
||||
other => panic!("expected Op::UserTurn with standard service tier override, got {other:?}"),
|
||||
} if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE => {}
|
||||
other => panic!("expected Op::UserTurn with default service tier override, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SkillsConfigWriteParams;
|
||||
use codex_app_server_protocol::SkillsConfigWriteResponse;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
@@ -75,26 +76,22 @@ pub(crate) fn build_service_tier_selection_edits(
|
||||
let service_tier_edit = service_tier.map_or_else(
|
||||
|| clear_config_value(profile_scoped_key_path(profile, "service_tier")),
|
||||
|service_tier| {
|
||||
let config_value =
|
||||
let config_value = if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE {
|
||||
SERVICE_TIER_DEFAULT_REQUEST_VALUE
|
||||
} else {
|
||||
match codex_protocol::config_types::ServiceTier::from_request_value(service_tier) {
|
||||
Some(codex_protocol::config_types::ServiceTier::Fast) => "fast",
|
||||
Some(codex_protocol::config_types::ServiceTier::Flex) => "flex",
|
||||
None => service_tier,
|
||||
};
|
||||
}
|
||||
};
|
||||
replace_config_value(
|
||||
profile_scoped_key_path(profile, "service_tier"),
|
||||
serde_json::json!(config_value),
|
||||
)
|
||||
},
|
||||
);
|
||||
let mut edits = vec![service_tier_edit];
|
||||
if service_tier.is_none() {
|
||||
edits.push(replace_config_value(
|
||||
"notice.fast_default_opt_out",
|
||||
serde_json::json!(true),
|
||||
));
|
||||
}
|
||||
edits
|
||||
vec![service_tier_edit]
|
||||
}
|
||||
|
||||
pub(crate) async fn write_config_batch(
|
||||
|
||||
@@ -164,6 +164,7 @@ mod render;
|
||||
mod resize_reflow_cap;
|
||||
mod resume_picker;
|
||||
mod selection_list;
|
||||
mod service_tier_resolution;
|
||||
mod session_log;
|
||||
mod session_resume;
|
||||
mod session_state;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
use crate::legacy_core::config::Config;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
|
||||
pub(crate) fn configured_service_tier(config: &Config) -> Option<String> {
|
||||
config.service_tier.clone().or_else(|| {
|
||||
(config.notices.fast_default_opt_out == Some(true))
|
||||
.then(|| SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn effective_service_tier(
|
||||
config: &Config,
|
||||
model: &str,
|
||||
models: &[ModelPreset],
|
||||
) -> Option<String> {
|
||||
if !config.features.enabled(Feature::FastMode) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let configured = configured_service_tier(config);
|
||||
let Some(preset) = models.iter().find(|preset| preset.model == model) else {
|
||||
return configured;
|
||||
};
|
||||
|
||||
match configured.as_deref() {
|
||||
Some(service_tier) if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE => configured,
|
||||
Some(service_tier) if model_supports_service_tier(preset, service_tier) => configured,
|
||||
Some(_) => None,
|
||||
None => preset
|
||||
.default_service_tier
|
||||
.clone()
|
||||
.filter(|service_tier| model_supports_service_tier(preset, service_tier)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn service_tier_update_for_core(
|
||||
config: &Config,
|
||||
model: &str,
|
||||
models: &[ModelPreset],
|
||||
) -> Option<Option<String>> {
|
||||
if !config.features.enabled(Feature::FastMode) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let effective = effective_service_tier(config, model, models);
|
||||
if let Some(service_tier) = effective {
|
||||
return Some(Some(service_tier));
|
||||
}
|
||||
|
||||
if !models.iter().any(|preset| preset.model == model) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn model_supports_service_tier(model: &ModelPreset, service_tier: &str) -> bool {
|
||||
model
|
||||
.service_tiers
|
||||
.iter()
|
||||
.any(|tier| tier.id == service_tier)
|
||||
}
|
||||
Reference in New Issue
Block a user