From be1d3cff9300c7d72d4adab2b31c19e8a8bb27ca Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 6 May 2026 18:00:21 +0300 Subject: [PATCH] 2- Use string service tiers in session protocol (#20971) ## Summary - break service tier session/op/app-server protocol fields from the closed enum to string tier ids - send the service tier string directly through model requests, prewarm, compaction, memories, and TUI/app-server turn starts - regenerate app-server protocol JSON/TypeScript schemas, removing the standalone ServiceTier TS enum ## Verification - just fmt - cargo check -p codex-core -p codex-app-server -p codex-tui - just write-app-server-schema --------- Co-authored-by: Codex --- .../schema/json/ClientRequest.json | 79 ++--------- .../codex_app_server_protocol.schemas.json | 129 ++++-------------- .../codex_app_server_protocol.v2.schemas.json | 129 ++++-------------- .../schema/json/v2/ConfigReadResponse.json | 27 +--- .../schema/json/v2/ThreadForkParams.json | 24 +--- .../schema/json/v2/ThreadForkResponse.json | 17 +-- .../schema/json/v2/ThreadResumeParams.json | 24 +--- .../schema/json/v2/ThreadResumeResponse.json | 17 +-- .../schema/json/v2/ThreadStartParams.json | 24 +--- .../schema/json/v2/ThreadStartResponse.json | 17 +-- .../schema/json/v2/TurnStartParams.json | 28 +--- .../schema/typescript/ServiceTier.ts | 5 - .../schema/typescript/index.ts | 1 - .../schema/typescript/v2/Config.ts | 3 +- .../schema/typescript/v2/ProfileV2.ts | 3 +- .../schema/typescript/v2/ThreadForkParams.ts | 3 +- .../typescript/v2/ThreadForkResponse.ts | 3 +- .../typescript/v2/ThreadResumeParams.ts | 3 +- .../typescript/v2/ThreadResumeResponse.ts | 3 +- .../schema/typescript/v2/ThreadStartParams.ts | 3 +- .../typescript/v2/ThreadStartResponse.ts | 3 +- .../schema/typescript/v2/TurnStartParams.ts | 3 +- .../src/protocol/v2/config.rs | 5 +- .../src/protocol/v2/thread.rs | 13 +- .../src/protocol/v2/turn.rs | 3 +- .../request_processors/thread_processor.rs | 2 +- .../thread_processor_tests.rs | 6 +- .../src/request_processors/turn_processor.rs | 2 +- .../app-server/tests/suite/v2/thread_start.rs | 8 +- .../app-server/tests/suite/v2/turn_start.rs | 65 +++++++++ codex-rs/core/src/client.rs | 25 ++-- codex-rs/core/src/codex_thread.rs | 5 +- codex-rs/core/src/compact.rs | 2 +- codex-rs/core/src/compact_remote.rs | 2 +- codex-rs/core/src/compact_remote_v2.rs | 2 +- codex-rs/core/src/config/config_tests.rs | 23 ++++ codex-rs/core/src/config/mod.rs | 26 ++-- codex-rs/core/src/guardian/tests.rs | 2 +- codex-rs/core/src/session/config_lock.rs | 7 +- codex-rs/core/src/session/mod.rs | 8 +- codex-rs/core/src/session/session.rs | 20 ++- codex-rs/core/src/session/tests.rs | 27 +++- codex-rs/core/src/session/turn.rs | 9 +- codex-rs/core/src/session/turn_context.rs | 2 +- codex-rs/core/src/session_startup_prewarm.rs | 2 +- codex-rs/core/tests/common/test_codex.rs | 6 +- codex-rs/core/tests/suite/agent_websocket.rs | 2 +- .../core/tests/suite/client_websockets.rs | 2 +- codex-rs/core/tests/suite/compact_remote.rs | 2 +- codex-rs/exec/src/lib.rs | 6 +- codex-rs/memories/write/src/runtime.rs | 5 +- codex-rs/memories/write/src/startup_tests.rs | 17 ++- codex-rs/protocol/src/config_types.rs | 17 +++ codex-rs/protocol/src/protocol.rs | 11 +- codex-rs/tui/src/app/event_dispatch.rs | 3 +- codex-rs/tui/src/app/session_lifecycle.rs | 5 +- codex-rs/tui/src/app/tests.rs | 6 +- codex-rs/tui/src/app/thread_routing.rs | 2 +- codex-rs/tui/src/app/thread_session_state.rs | 5 +- codex-rs/tui/src/app_command.rs | 9 +- codex-rs/tui/src/app_server_session.rs | 10 +- codex-rs/tui/src/chatwidget.rs | 24 +++- codex-rs/tui/src/chatwidget/tests/helpers.rs | 5 +- .../src/chatwidget/tests/slash_commands.rs | 20 +-- codex-rs/tui/src/session_state.rs | 2 +- 65 files changed, 403 insertions(+), 570 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/typescript/ServiceTier.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 35efe8695b..36596807ba 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -3324,13 +3324,6 @@ ], "type": "object" }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "SessionMigration": { "properties": { "cwd": { @@ -3608,20 +3601,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "threadId": { @@ -4030,20 +4012,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "threadId": { @@ -4227,20 +4198,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "sessionStartSource": { @@ -4407,22 +4367,11 @@ "description": "Override the sandbox policy for this turn and subsequent turns." }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } - ], - "description": "Override the service tier for this turn and subsequent turns." + "description": "Override the service tier for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] }, "summary": { "anyOf": [ 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 c4f69f47ea..acb4b16d93 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 @@ -7224,13 +7224,9 @@ ] }, "service_tier": { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "tools": { @@ -13089,13 +13085,9 @@ ] }, "service_tier": { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "tools": { @@ -14624,13 +14616,6 @@ "title": "ServerRequestResolvedNotification", "type": "object" }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "SessionMigration": { "properties": { "cwd": { @@ -15573,20 +15558,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "threadId": { @@ -15660,13 +15634,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { @@ -17091,20 +17061,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "threadId": { @@ -17167,13 +17126,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { @@ -17399,20 +17354,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "sessionStartSource": { @@ -17490,13 +17434,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { @@ -18163,22 +18103,11 @@ "description": "Override the sandbox policy for this turn and subsequent turns." }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/v2/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } - ], - "description": "Override the service tier for this turn and subsequent turns." + "description": "Override the service tier for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] }, "summary": { "anyOf": [ 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 fdd5ba9084..ded3dc2b87 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 @@ -3680,13 +3680,9 @@ ] }, "service_tier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "tools": { @@ -9700,13 +9696,9 @@ ] }, "service_tier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "tools": { @@ -12510,13 +12502,6 @@ "title": "ServerRequestResolvedNotification", "type": "object" }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "SessionMigration": { "properties": { "cwd": { @@ -13459,20 +13444,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "threadId": { @@ -13546,13 +13520,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { @@ -14977,20 +14947,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "threadId": { @@ -15053,13 +15012,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { @@ -15285,20 +15240,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "sessionStartSource": { @@ -15376,13 +15320,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { @@ -16049,22 +15989,11 @@ "description": "Override the sandbox policy for this turn and subsequent turns." }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } - ], - "description": "Override the service tier for this turn and subsequent turns." + "description": "Override the service tier for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] }, "summary": { "anyOf": [ 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 c348e7d955..87a826e5af 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -352,13 +352,9 @@ ] }, "service_tier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "tools": { @@ -658,13 +654,9 @@ ] }, "service_tier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "tools": { @@ -754,13 +746,6 @@ }, "type": "object" }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "ToolsV2": { "properties": { "view_image": { 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 6419bc9422..29d67403cd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -131,13 +131,6 @@ ], "type": "string" }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "ThreadSource": { "enum": [ "user", @@ -222,20 +215,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "threadId": { 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 93f5ee18b1..6e74ab4ac8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1177,13 +1177,6 @@ } ] }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "SessionSource": { "oneOf": [ { @@ -2615,13 +2608,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { 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 9fe5c7f47f..5f07fe0149 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -1010,13 +1010,6 @@ "danger-full-access" ], "type": "string" - }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" } }, "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nThe precedence is: history > path > thread_id. If using history or path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", @@ -1101,20 +1094,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "threadId": { 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 bb1290ecbe..727b7a3fb2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1177,13 +1177,6 @@ } ] }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "SessionSource": { "oneOf": [ { @@ -2615,13 +2608,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { 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 aa5029afd9..9a60049a61 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -165,13 +165,6 @@ ], "type": "string" }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "ThreadSource": { "enum": [ "user", @@ -295,20 +288,9 @@ ] }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "sessionStartSource": { 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 f1354d70b1..bf03f0fb55 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1177,13 +1177,6 @@ } ] }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "SessionSource": { "oneOf": [ { @@ -2615,13 +2608,9 @@ "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." }, "serviceTier": { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } + "type": [ + "string", + "null" ] }, "thread": { 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 da1320a796..1ef33d4301 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -312,13 +312,6 @@ } ] }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, "Settings": { "description": "Settings for a collaboration mode.", "properties": { @@ -586,22 +579,11 @@ "description": "Override the sandbox policy for this turn and subsequent turns." }, "serviceTier": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/definitions/ServiceTier" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } - ], - "description": "Override the service tier for this turn and subsequent turns." + "description": "Override the service tier for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] }, "summary": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/ServiceTier.ts b/codex-rs/app-server-protocol/schema/typescript/ServiceTier.ts deleted file mode 100644 index ce11286dbd..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/ServiceTier.ts +++ /dev/null @@ -1,5 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ServiceTier = "fast" | "flex"; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index a082e045fa..97ea435601 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -64,7 +64,6 @@ export type { ResponseItem } from "./ResponseItem"; export type { ReviewDecision } from "./ReviewDecision"; export type { ServerNotification } from "./ServerNotification"; export type { ServerRequest } from "./ServerRequest"; -export type { ServiceTier } from "./ServiceTier"; export type { SessionSource } from "./SessionSource"; export type { Settings } from "./Settings"; export type { SubAgentSource } from "./SubAgentSource"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts index 508fe84e92..cc7e340ea3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Config.ts @@ -4,7 +4,6 @@ import type { ForcedLoginMethod } from "../ForcedLoginMethod"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ReasoningSummary } from "../ReasoningSummary"; -import type { ServiceTier } from "../ServiceTier"; import type { Verbosity } from "../Verbosity"; import type { WebSearchMode } from "../WebSearchMode"; import type { JsonValue } from "../serde_json/JsonValue"; @@ -20,4 +19,4 @@ export type Config = {model: string | null, review_model: string | null, model_c * [UNSTABLE] Optional default for where approval requests are routed for * review. */ -approvals_reviewer: ApprovalsReviewer | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: string | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, profile: string | null, profiles: { [key in string]?: ProfileV2 }, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, service_tier: ServiceTier | null, analytics: AnalyticsConfig | null} & ({ [key in string]?: number | string | boolean | Array | { [key in string]?: JsonValue } | null }); +approvals_reviewer: ApprovalsReviewer | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: string | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, profile: string | null, profiles: { [key in string]?: ProfileV2 }, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, service_tier: string | null, analytics: AnalyticsConfig | null} & ({ [key in string]?: number | string | boolean | Array | { [key in string]?: JsonValue } | null }); diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ProfileV2.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ProfileV2.ts index 7afe3e0c54..d05038701c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ProfileV2.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ProfileV2.ts @@ -3,7 +3,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ReasoningEffort } from "../ReasoningEffort"; import type { ReasoningSummary } from "../ReasoningSummary"; -import type { ServiceTier } from "../ServiceTier"; import type { Verbosity } from "../Verbosity"; import type { WebSearchMode } from "../WebSearchMode"; import type { JsonValue } from "../serde_json/JsonValue"; @@ -16,4 +15,4 @@ export type ProfileV2 = {model: string | null, model_provider: string | null, ap * are routed for review. If omitted, the enclosing config default is * used. */ -approvals_reviewer: ApprovalsReviewer | null, service_tier: ServiceTier | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, chatgpt_base_url: string | null} & ({ [key in string]?: number | string | boolean | Array | { [key in string]?: JsonValue } | null }); +approvals_reviewer: ApprovalsReviewer | null, service_tier: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, chatgpt_base_url: string | null} & ({ [key in string]?: number | string | boolean | Array | { [key in string]?: JsonValue } | null }); diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index ea67b491ad..6076a4bb14 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -1,7 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -20,7 +19,7 @@ import type { ThreadSource } from "./ThreadSource"; export type ThreadForkParams = {threadId: string, /** * Configuration overrides for the forked thread, if any. */ -model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** +model?: string | null, modelProvider?: string | null, serviceTier?: string | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** * Override where approval requests are routed for review on this thread * and subsequent turns. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts index ddcef104e9..c44533ec1a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -3,13 +3,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** +export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ instructionSources: Array, approvalPolicy: AskForApproval, /** diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index ac8b1e293b..6d1dbdca4f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -2,7 +2,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Personality } from "../Personality"; -import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -22,7 +21,7 @@ import type { SandboxMode } from "./SandboxMode"; export type ThreadResumeParams = {threadId: string, /** * Configuration overrides for the resumed thread, if any. */ -model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** +model?: string | null, modelProvider?: string | null, serviceTier?: string | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** * Override where approval requests are routed for review on this thread * and subsequent turns. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index f7627c07ae..f91756c7c6 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -3,13 +3,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** +export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ instructionSources: Array, approvalPolicy: AskForApproval, /** diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index cecc183f92..30509ef6cb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -2,7 +2,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Personality } from "../Personality"; -import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -10,7 +9,7 @@ import type { SandboxMode } from "./SandboxMode"; import type { ThreadSource } from "./ThreadSource"; import type { ThreadStartSource } from "./ThreadStartSource"; -export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** +export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: string | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** * Override where approval requests are routed for review on this thread * and subsequent turns. */ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index ce28a4a1d7..9573bd7dee 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -3,13 +3,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { ReasoningEffort } from "../ReasoningEffort"; -import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** +export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ instructionSources: Array, approvalPolicy: AskForApproval, /** diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index 4af17115c8..b04919d86b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -4,7 +4,6 @@ import type { Personality } from "../Personality"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ReasoningSummary } from "../ReasoningSummary"; -import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -30,7 +29,7 @@ sandboxPolicy?: SandboxPolicy | null, /** model?: string | null, /** * Override the service tier for this turn and subsequent turns. */ -serviceTier?: ServiceTier | null | null, /** +serviceTier?: string | null | null, /** * Override the reasoning effort for this turn and subsequent turns. */ effort?: ReasoningEffort | null, /** diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index 1c1ef35d62..da4f493d77 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -5,7 +5,6 @@ use super::shared::default_enabled; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningSummary; -use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; @@ -137,7 +136,7 @@ pub struct ProfileV2 { /// used. #[experimental("config/read.approvalsReviewer")] pub approvals_reviewer: Option, - pub service_tier: Option, + pub service_tier: Option, pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, pub model_verbosity: Option, @@ -248,7 +247,7 @@ pub struct Config { pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, pub model_verbosity: Option, - pub service_tier: Option, + pub service_tier: Option, pub analytics: Option, #[experimental("config/read.apps")] #[serde(default)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index 578ef9193f..5f293ff8f6 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -12,7 +12,6 @@ use super::TurnEnvironmentParams; use super::shared::v2_enum_from_core; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::Personality; -use codex_protocol::config_types::ServiceTier; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus; @@ -103,7 +102,7 @@ pub struct ThreadStartParams { skip_serializing_if = "Option::is_none" )] #[ts(optional = nullable)] - pub service_tier: Option>, + pub service_tier: Option>, #[ts(optional = nullable)] pub cwd: Option, #[experimental(nested)] @@ -192,7 +191,7 @@ pub struct ThreadStartResponse { pub thread: Thread, pub model: String, pub model_provider: String, - pub service_tier: Option, + pub service_tier: Option, pub cwd: AbsolutePathBuf, /// Instruction source files currently loaded for this thread. #[serde(default)] @@ -260,7 +259,7 @@ pub struct ThreadResumeParams { skip_serializing_if = "Option::is_none" )] #[ts(optional = nullable)] - pub service_tier: Option>, + pub service_tier: Option>, #[ts(optional = nullable)] pub cwd: Option, #[experimental(nested)] @@ -307,7 +306,7 @@ pub struct ThreadResumeResponse { pub thread: Thread, pub model: String, pub model_provider: String, - pub service_tier: Option, + pub service_tier: Option, pub cwd: AbsolutePathBuf, /// Instruction source files currently loaded for this thread. #[serde(default)] @@ -366,7 +365,7 @@ pub struct ThreadForkParams { skip_serializing_if = "Option::is_none" )] #[ts(optional = nullable)] - pub service_tier: Option>, + pub service_tier: Option>, #[ts(optional = nullable)] pub cwd: Option, #[experimental(nested)] @@ -416,7 +415,7 @@ pub struct ThreadForkResponse { pub thread: Thread, pub model: String, pub model_provider: String, - pub service_tier: Option, + pub service_tier: Option, pub cwd: AbsolutePathBuf, /// Instruction source files currently loaded for this thread. #[serde(default)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index 38118ab15e..61a09bfbf5 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -7,7 +7,6 @@ use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; -use codex_protocol::config_types::ServiceTier; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg; use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus; @@ -94,7 +93,7 @@ pub struct TurnStartParams { skip_serializing_if = "Option::is_none" )] #[ts(optional = nullable)] - pub service_tier: Option>, + pub service_tier: Option>, /// Override the reasoning effort for this turn and subsequent turns. #[ts(optional = nullable)] pub effort: Option, diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index aaf7fb1de1..783e53ccb0 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -1188,7 +1188,7 @@ impl ThreadRequestProcessor { &self, model: Option, model_provider: Option, - service_tier: Option>, + service_tier: Option>, cwd: Option, approval_policy: Option, approvals_reviewer: Option, diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 01c09de43b..5a18c09db9 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -629,7 +629,7 @@ mod thread_processor_behavior_tests { path: None, model: None, model_provider: None, - service_tier: Some(Some(codex_protocol::config_types::ServiceTier::Fast)), + service_tier: Some(Some("priority".to_string())), cwd: None, approval_policy: None, approvals_reviewer: None, @@ -645,7 +645,7 @@ mod thread_processor_behavior_tests { let config_snapshot = ThreadConfigSnapshot { model: "gpt-5".to_string(), model_provider_id: "openai".to_string(), - service_tier: Some(codex_protocol::config_types::ServiceTier::Flex), + service_tier: Some("flex".to_string()), approval_policy: codex_protocol::protocol::AskForApproval::OnRequest, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, permission_profile: codex_protocol::models::PermissionProfile::Disabled, @@ -660,7 +660,7 @@ mod thread_processor_behavior_tests { assert_eq!( collect_resume_override_mismatches(&request, &config_snapshot), - vec!["service_tier requested=Some(Fast) active=Some(Flex)".to_string()] + vec!["service_tier requested=Some(\"priority\") active=Some(\"flex\")".to_string()] ); } diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index ad084f16c3..b207fde940 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -438,7 +438,7 @@ impl TurnRequestProcessor { model: model.clone(), effort, summary, - service_tier, + service_tier: service_tier.clone(), collaboration_mode: collaboration_mode.clone(), personality, }) 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 4da6b54ed1..78155d8c9a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -28,7 +28,6 @@ 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::ServiceTier; use codex_protocol::config_types::TrustLevel; use codex_protocol::openai_models::ReasoningEffort; use pretty_assertions::assert_eq; @@ -402,7 +401,7 @@ model_reasoning_effort = "high" } #[tokio::test] -async fn thread_start_accepts_flex_service_tier() -> Result<()> { +async fn thread_start_accepts_arbitrary_service_tier_id() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; let codex_home = TempDir::new()?; @@ -411,9 +410,10 @@ async fn thread_start_accepts_flex_service_tier() -> Result<()> { let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + let service_tier_id = "experimental-tier-id".to_string(); let req_id = mcp .send_thread_start_request(ThreadStartParams { - service_tier: Some(Some(ServiceTier::Flex)), + service_tier: Some(Some(service_tier_id.clone())), ..Default::default() }) .await?; @@ -425,7 +425,7 @@ async fn thread_start_accepts_flex_service_tier() -> Result<()> { .await??; let ThreadStartResponse { service_tier, .. } = to_response::(resp)?; - assert_eq!(service_tier, Some(ServiceTier::Flex)); + assert_eq!(service_tier, Some(service_tier_id)); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 971b081ae5..e5c5c5adbb 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -358,6 +358,71 @@ async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills( Ok(()) } +#[tokio::test] +async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()> { + let server = responses::start_mock_server().await; + let body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-1"), + ]); + let response_mock = responses::mount_sse_once(&server, body).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::default(), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let service_tier_id = "experimental-tier-id".to_string(); + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + service_tier: Some(Some(service_tier_id.clone())), + input: vec![V2UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + assert_eq!( + response_mock.single_request().body_json()["service_tier"], + json!(service_tier_id) + ); + + Ok(()) +} + #[tokio::test] async fn thread_start_omits_empty_instruction_overrides_from_model_request() -> Result<()> { let server = responses::start_mock_server().await; diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 6449237dd6..4806633a3f 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -73,7 +73,6 @@ use codex_otel::current_span_w3c_trace_context; use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; -use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::Verbosity as VerbosityConfig; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelInfo; @@ -150,7 +149,7 @@ pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration = pub(crate) struct CompactConversationRequestSettings { pub(crate) effort: Option, pub(crate) summary: ReasoningSummaryConfig, - pub(crate) service_tier: Option, + pub(crate) service_tier: Option, } /// Session-scoped state shared by all [`ModelClient`] clones. @@ -683,7 +682,7 @@ impl ModelClient { model_info: &ModelInfo, effort: Option, summary: ReasoningSummaryConfig, - service_tier: Option, + service_tier: Option, ) -> Result { let instructions = &prompt.base_instructions.text; let input = prompt.get_formatted_input(); @@ -722,11 +721,7 @@ impl ModelClient { store: provider.is_azure_responses_endpoint(), stream: true, include, - service_tier: match service_tier { - Some(ServiceTier::Fast) => Some("priority".to_string()), - Some(service_tier) => Some(service_tier.to_string()), - None => None, - }, + service_tier, prompt_cache_key, text, client_metadata: Some(HashMap::from([( @@ -1168,7 +1163,7 @@ impl ModelClientSession { session_telemetry: &SessionTelemetry, effort: Option, summary: ReasoningSummaryConfig, - service_tier: Option, + service_tier: Option, turn_metadata_header: Option<&str>, inference_trace: &InferenceTraceContext, ) -> Result { @@ -1215,7 +1210,7 @@ impl ModelClientSession { model_info, effort, summary, - service_tier, + service_tier.clone(), )?; let inference_trace_attempt = inference_trace.start_attempt(); inference_trace_attempt.record_started(&request); @@ -1293,7 +1288,7 @@ impl ModelClientSession { session_telemetry: &SessionTelemetry, effort: Option, summary: ReasoningSummaryConfig, - service_tier: Option, + service_tier: Option, turn_metadata_header: Option<&str>, warmup: bool, request_trace: Option, @@ -1321,7 +1316,7 @@ impl ModelClientSession { model_info, effort, summary, - service_tier, + service_tier.clone(), )?; let mut ws_payload = ResponseCreateWsRequest { client_metadata: response_create_client_metadata( @@ -1453,7 +1448,7 @@ impl ModelClientSession { session_telemetry: &SessionTelemetry, effort: Option, summary: ReasoningSummaryConfig, - service_tier: Option, + service_tier: Option, turn_metadata_header: Option<&str>, ) -> Result<()> { if !self.client.responses_websocket_enabled() { @@ -1514,7 +1509,7 @@ impl ModelClientSession { session_telemetry: &SessionTelemetry, effort: Option, summary: ReasoningSummaryConfig, - service_tier: Option, + service_tier: Option, turn_metadata_header: Option<&str>, inference_trace: &InferenceTraceContext, ) -> Result { @@ -1530,7 +1525,7 @@ impl ModelClientSession { session_telemetry, effort, summary, - service_tier, + service_tier.clone(), turn_metadata_header, /*warmup*/ false, request_trace, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 508e8c2fac..24a976637b 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -11,7 +11,6 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; -use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; @@ -53,7 +52,7 @@ use codex_rollout::state_db::StateDbHandle; pub struct ThreadConfigSnapshot { pub model: String, pub model_provider_id: String, - pub service_tier: Option, + pub service_tier: Option, pub approval_policy: AskForApproval, pub approvals_reviewer: ApprovalsReviewer, pub permission_profile: PermissionProfile, @@ -91,7 +90,7 @@ pub struct CodexThreadTurnContextOverrides { pub model: Option, pub effort: Option>, pub summary: Option, - pub service_tier: Option>, + pub service_tier: Option>, pub collaboration_mode: Option, pub personality: Option, } diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index a4197acccd..38eb19f1ba 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -524,7 +524,7 @@ async fn drain_to_completed( &turn_context.session_telemetry, turn_context.reasoning_effort, turn_context.reasoning_summary, - turn_context.config.service_tier, + turn_context.config.service_tier.clone(), turn_metadata_header, // Rollout tracing currently models remote compaction only; local compaction streams // are left untraced until the reducer has a first-class local compaction lifecycle. diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index cf30a623b3..bc0516ce01 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -174,7 +174,7 @@ async fn run_remote_compact_task_inner_impl( CompactConversationRequestSettings { effort: turn_context.reasoning_effort, summary: turn_context.reasoning_summary, - service_tier: turn_context.config.service_tier, + service_tier: turn_context.config.service_tier.clone(), }, &turn_context.session_telemetry, &compaction_trace, diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index f461dd9ce1..7705616d37 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -252,7 +252,7 @@ async fn run_remote_compaction_request_v2( &turn_context.session_telemetry, turn_context.reasoning_effort, turn_context.reasoning_summary, - turn_context.config.service_tier, + turn_context.config.service_tier.clone(), turn_metadata_header, &InferenceTraceContext::disabled(), ) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index f2ae82810a..4c6d1a87e0 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -62,6 +62,7 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; +use codex_protocol::config_types::ServiceTier; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfileModification; use codex_protocol::models::ManagedFileSystemPermissions; @@ -7132,6 +7133,28 @@ async fn explicit_null_service_tier_override_sets_fast_default_opt_out() -> std: Ok(()) } +#[tokio::test] +async fn legacy_fast_service_tier_override_uses_priority_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("fast".to_string())), + ..Default::default() + }, + fixture.codex_home(), + ) + .await?; + + assert_eq!( + config.service_tier, + Some(ServiceTier::Fast.request_value().to_string()) + ); + Ok(()) +} + #[tokio::test] async fn fast_default_opt_out_notice_config_is_respected() -> std::io::Result<()> { let fixture = create_test_fixture()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index d868e8474c..6dbc9519a9 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -402,8 +402,8 @@ pub struct Config { /// Optional override of model selection. pub model: Option, - /// Effective service tier preference for new turns (`fast` or `flex`). - pub service_tier: Option, + /// Effective service tier request id preference for new turns. + pub service_tier: Option, /// Model used specifically for review sessions. pub review_model: Option, @@ -1867,7 +1867,7 @@ pub struct ConfigOverrides { pub permission_profile: Option, pub default_permissions: Option, pub model_provider: Option, - pub service_tier: Option>, + pub service_tier: Option>, pub config_profile: Option, pub codex_self_exe: Option, pub codex_linux_sandbox_exe: Option, @@ -2735,16 +2735,20 @@ impl Config { notices.fast_default_opt_out = Some(true); None } - None => config_profile.service_tier.or(cfg.service_tier), + None => config_profile + .service_tier + .or(cfg.service_tier) + .map(|service_tier| service_tier.request_value().to_string()), }; - let service_tier = match service_tier { - Some(ServiceTier::Fast) if features.enabled(Feature::FastMode) => { - Some(ServiceTier::Fast) + let service_tier = service_tier.and_then(|service_tier| { + match ServiceTier::from_request_value(&service_tier) { + Some(ServiceTier::Fast) => features + .enabled(Feature::FastMode) + .then(|| ServiceTier::Fast.request_value().to_string()), + Some(ServiceTier::Flex) => Some(ServiceTier::Flex.request_value().to_string()), + None => Some(service_tier), } - Some(ServiceTier::Fast) => None, - Some(ServiceTier::Flex) => Some(ServiceTier::Flex), - None => None, - }; + }); let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| { let trimmed = value.trim(); diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 78362b6f89..6d03dfa813 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -1894,7 +1894,7 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() -> #[tokio::test] async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> { - const TEST_STACK_SIZE_BYTES: usize = 2 * 1024 * 1024; + const TEST_STACK_SIZE_BYTES: usize = 4 * 1024 * 1024; let handle = std::thread::Builder::new() diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs index d1f190510a..10f2242647 100644 --- a/codex-rs/core/src/session/config_lock.rs +++ b/codex-rs/core/src/session/config_lock.rs @@ -109,7 +109,10 @@ fn save_session_resolved_fields(sc: &SessionConfiguration, lock_config: &mut Con lock_config.model = Some(sc.collaboration_mode.model().to_string()); lock_config.model_reasoning_effort = sc.collaboration_mode.reasoning_effort(); lock_config.model_reasoning_summary = sc.model_reasoning_summary; - lock_config.service_tier = sc.service_tier; + lock_config.service_tier = sc + .service_tier + .as_deref() + .and_then(codex_protocol::config_types::ServiceTier::from_request_value); lock_config.instructions = Some(sc.base_instructions.clone()); lock_config.developer_instructions = sc.developer_instructions.clone(); lock_config.compact_prompt = sc.compact_prompt.clone(); @@ -280,7 +283,7 @@ mod tests { sc.base_instructions = "catalog instructions".to_string(); sc.developer_instructions = Some("catalog developer instructions".to_string()); sc.compact_prompt = Some("catalog compact prompt".to_string()); - sc.service_tier = Some(codex_protocol::config_types::ServiceTier::Flex); + sc.service_tier = Some("flex".to_string()); let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize"); let lock = &lockfile.config; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 9d83ec2ff0..bacd1708eb 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -588,7 +588,7 @@ impl Codex { .auth_cached() .and_then(|auth| auth.account_plan_type()); let service_tier = get_service_tier( - config.service_tier, + config.service_tier.clone(), config.notices.fast_default_opt_out.unwrap_or(false), account_plan_type, config.features.enabled(Feature::FastMode), @@ -787,18 +787,18 @@ impl Codex { } fn get_service_tier( - configured_service_tier: Option, + configured_service_tier: Option, fast_default_opt_out: bool, account_plan_type: Option, fast_mode_enabled: bool, -) -> Option { +) -> Option { if configured_service_tier.is_some() || fast_default_opt_out || !fast_mode_enabled { return configured_service_tier; } account_plan_type .is_some_and(is_enterprise_default_service_tier_plan) - .then_some(ServiceTier::Fast) + .then_some(ServiceTier::Fast.request_value().to_string()) } fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool { diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 124734a254..3c6acd31e3 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1,6 +1,7 @@ use super::*; use crate::goals::GoalRuntimeState; use codex_protocol::SessionId; +use codex_protocol::config_types::ServiceTier; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::ThreadSource; @@ -42,7 +43,7 @@ pub(crate) struct SessionConfiguration { pub(super) collaboration_mode: CollaborationMode, pub(super) model_reasoning_summary: Option, - pub(super) service_tier: Option, + pub(super) service_tier: Option, /// Developer instructions that supplement the base instructions. pub(super) developer_instructions: Option, @@ -136,7 +137,7 @@ impl SessionConfiguration { ThreadConfigSnapshot { model: self.collaboration_mode.model().to_string(), model_provider_id: self.original_config_do_not_use.model_provider_id.clone(), - service_tier: self.service_tier, + service_tier: self.service_tier.clone(), approval_policy: self.approval_policy.value(), approvals_reviewer: self.approvals_reviewer, permission_profile: self.permission_profile(), @@ -182,8 +183,15 @@ impl SessionConfiguration { if let Some(summary) = updates.reasoning_summary { next_configuration.model_reasoning_summary = Some(summary); } - if let Some(service_tier) = updates.service_tier { - next_configuration.service_tier = service_tier; + 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() + }) + }); } if let Some(personality) = updates.personality { next_configuration.personality = Some(personality); @@ -309,7 +317,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) windows_sandbox_level: Option, pub(crate) collaboration_mode: Option, pub(crate) reasoning_summary: Option, - pub(crate) service_tier: Option>, + pub(crate) service_tier: Option>, pub(crate) final_output_json_schema: Option>, /// Turn-local environment override. `None` inherits the sticky thread /// environments stored on `SessionConfiguration`; `Some([])` explicitly @@ -905,7 +913,7 @@ impl Session { thread_name: session_configuration.thread_name.clone(), model: session_configuration.collaboration_mode.model().to_string(), model_provider_id: config.model_provider_id.clone(), - service_tier: session_configuration.service_tier, + service_tier: session_configuration.service_tier.clone(), approval_policy: session_configuration.approval_policy.value(), approvals_reviewer: session_configuration.approvals_reviewer, permission_profile: session_configuration.permission_profile(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index a20f5489c6..a5c5ec0ca4 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2891,7 +2891,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::Enterprise), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::Fast.request_value().to_string()) ); assert_eq!( get_service_tier( @@ -2900,7 +2900,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::EnterpriseCbpUsageBased), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::Fast.request_value().to_string()) ); assert_eq!( get_service_tier( @@ -2909,7 +2909,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::Business), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::Fast.request_value().to_string()) ); assert_eq!( get_service_tier( @@ -2918,7 +2918,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::Team), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::Fast.request_value().to_string()) ); assert_eq!( get_service_tier( @@ -2927,7 +2927,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::SelfServeBusinessUsageBased), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::Fast.request_value().to_string()) ); } @@ -2980,6 +2980,23 @@ async fn session_settings_null_service_tier_update_clears_service_tier() { assert_eq!(updated.service_tier, None); } +#[tokio::test] +async fn session_settings_legacy_fast_service_tier_update_uses_priority_request_value() { + let session_configuration = make_session_configuration_for_tests().await; + + let updated = session_configuration + .apply(&SessionSettingsUpdate { + service_tier: Some(Some("fast".to_string())), + ..Default::default() + }) + .expect("legacy fast service tier update should apply"); + + assert_eq!( + updated.service_tier, + Some(ServiceTier::Fast.request_value().to_string()) + ); +} + pub(crate) async fn make_session_configuration_for_tests() -> SessionConfiguration { let codex_home = tempfile::tempdir().expect("create temp dir"); let config = build_test_config(codex_home.path()).await; diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index d74ddd5fe9..7126dfd27a 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -73,6 +73,7 @@ use codex_hooks::HookEventAfterAgent; use codex_hooks::HookPayload; use codex_hooks::HookResult; use codex_protocol::config_types::ModeKind; +use codex_protocol::config_types::ServiceTier; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::items::PlanItem; @@ -693,7 +694,11 @@ async fn track_turn_resolved_config_analytics( permission_profile_cwd: turn_context.cwd.to_path_buf(), reasoning_effort: turn_context.reasoning_effort, reasoning_summary: Some(turn_context.reasoning_summary), - service_tier: turn_context.config.service_tier, + service_tier: turn_context + .config + .service_tier + .as_deref() + .and_then(ServiceTier::from_request_value), approval_policy: turn_context.approval_policy.value(), approvals_reviewer: turn_context.config.approvals_reviewer, sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(), @@ -1858,7 +1863,7 @@ async fn try_run_sampling_request( &turn_context.session_telemetry, turn_context.reasoning_effort, turn_context.reasoning_summary, - turn_context.config.service_tier, + turn_context.config.service_tier.clone(), turn_metadata_header, &inference_trace, ) diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 8e78a0c9cd..6d8443e933 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -415,7 +415,7 @@ impl Session { per_turn_config.model_reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; - per_turn_config.service_tier = session_configuration.service_tier; + per_turn_config.service_tier = session_configuration.service_tier.clone(); per_turn_config.personality = session_configuration.personality; per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer; per_turn_config.permissions.permission_profile = diff --git a/codex-rs/core/src/session_startup_prewarm.rs b/codex-rs/core/src/session_startup_prewarm.rs index 71afdba6f9..93d14ff7d6 100644 --- a/codex-rs/core/src/session_startup_prewarm.rs +++ b/codex-rs/core/src/session_startup_prewarm.rs @@ -232,7 +232,7 @@ async fn schedule_startup_prewarm_inner( &startup_turn_context.session_telemetry, startup_turn_context.reasoning_effort, startup_turn_context.reasoning_summary, - startup_turn_context.config.service_tier, + startup_turn_context.config.service_tier.clone(), startup_turn_metadata_header.as_deref(), ) .await?; diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index ac118615bd..1728f5bc5c 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -654,7 +654,7 @@ impl TestCodex { prompt, AskForApproval::Never, PermissionProfile::Disabled, - Some(service_tier), + Some(service_tier.map(|service_tier| service_tier.request_value().to_string())), /*environments*/ None, ) .await @@ -716,7 +716,7 @@ impl TestCodex { prompt: &str, approval_policy: AskForApproval, permission_profile: PermissionProfile, - service_tier: Option>, + service_tier: Option>, environments: Option>, ) -> Result<()> { self.submit_turn_with_context( @@ -734,7 +734,7 @@ impl TestCodex { prompt: &str, approval_policy: AskForApproval, permission_profile: PermissionProfile, - service_tier: Option>, + service_tier: Option>, environments: Option>, ) -> Result<()> { let (sandbox_policy, permission_profile) = diff --git a/codex-rs/core/tests/suite/agent_websocket.rs b/codex-rs/core/tests/suite/agent_websocket.rs index 305346afac..6e985eebe0 100644 --- a/codex-rs/core/tests/suite/agent_websocket.rs +++ b/codex-rs/core/tests/suite/agent_websocket.rs @@ -313,7 +313,7 @@ async fn websocket_v2_first_turn_drops_fast_tier_after_startup_prewarm() -> Resu .features .enable(Feature::ResponsesWebsocketsV2) .expect("test config should allow feature update"); - config.service_tier = Some(ServiceTier::Fast); + config.service_tier = Some(ServiceTier::Fast.request_value().to_string()); }); let test = builder.build_with_websocket_server(&server).await?; diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 7ef571af37..8680ee530a 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -1948,7 +1948,7 @@ async fn stream_until_complete_with_request_metadata( &harness.session_telemetry, harness.effort, harness.summary, - service_tier, + service_tier.map(|service_tier| service_tier.request_value().to_string()), turn_metadata_header, &codex_rollout_trace::InferenceTraceContext::disabled(), ) diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 5e15b54b77..3da841ba79 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -445,7 +445,7 @@ async fn assert_remote_manual_compact_request_parity( let mut builder = test_codex().with_auth(auth); if let Some(service_tier) = configured_service_tier { builder = builder.with_config(move |config| { - config.service_tier = Some(service_tier); + config.service_tier = Some(service_tier.request_value().to_string()); }); } let harness = TestCodexHarness::with_builder(builder).await?; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index b4bbb2c1d5..6e232f7297 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -1066,7 +1066,7 @@ fn session_configured_from_thread_start_response( response.thread.path.clone(), response.model.clone(), response.model_provider.clone(), - response.service_tier, + response.service_tier.clone(), response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response @@ -1091,7 +1091,7 @@ fn session_configured_from_thread_resume_response( response.thread.path.clone(), response.model.clone(), response.model_provider.clone(), - response.service_tier, + response.service_tier.clone(), response.approval_policy.to_core(), response.approvals_reviewer.to_core(), response @@ -1125,7 +1125,7 @@ fn session_configured_from_thread_response( rollout_path: Option, model: String, model_provider_id: String, - service_tier: Option, + service_tier: Option, approval_policy: AskForApproval, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, permission_profile: PermissionProfile, diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 2e2c664e64..53a10934a5 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -18,7 +18,6 @@ use codex_otel::TelemetryAuthMode; use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary; -use codex_protocol::config_types::ServiceTier; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::InitialHistory; @@ -46,7 +45,7 @@ pub(crate) struct StageOneRequestContext { pub(crate) session_telemetry: SessionTelemetry, pub(crate) reasoning_effort: Option, pub(crate) reasoning_summary: ReasoningSummary, - pub(crate) service_tier: Option, + pub(crate) service_tier: Option, pub(crate) turn_metadata_header: Option, } @@ -194,7 +193,7 @@ impl MemoryStartupContext { &context.session_telemetry, context.reasoning_effort, context.reasoning_summary, - context.service_tier, + context.service_tier.clone(), context.turn_metadata_header.as_deref(), &InferenceTraceContext::disabled(), ) diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs index f80b891ddc..4fcb1d409b 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -253,14 +253,18 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu model: None, effort: None, summary: None, - service_tier: Some(Some(ServiceTier::Fast)), + service_tier: Some(Some(ServiceTier::Fast.request_value().to_string())), collaboration_mode: None, personality: None, }) .await?; - let config_snapshot = wait_for_service_tier(&test, Some(ServiceTier::Fast)).await?; - assert_eq!(config_snapshot.service_tier, Some(ServiceTier::Fast)); + let config_snapshot = + wait_for_service_tier(&test, Some(ServiceTier::Fast.request_value().to_string())).await?; + assert_eq!( + config_snapshot.service_tier, + Some(ServiceTier::Fast.request_value().to_string()) + ); let context = crate::runtime::MemoryStartupContext::new( Arc::clone(&test.thread_manager), @@ -277,7 +281,10 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu ReasoningEffort::Low, ) .await; - assert_eq!(request_context.service_tier, Some(ServiceTier::Fast)); + assert_eq!( + request_context.service_tier, + Some(ServiceTier::Fast.request_value().to_string()) + ); shutdown_test_codex(&test).await?; Ok(()) @@ -394,7 +401,7 @@ async fn wait_for_request(mock: &ResponseMock, expected_count: usize) -> Vec, + expected_service_tier: Option, ) -> anyhow::Result { let deadline = Instant::now() + Duration::from_secs(10); loop { diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index da83ee858a..47dc15f183 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -355,6 +355,23 @@ pub enum ServiceTier { Flex, } +impl ServiceTier { + pub const fn request_value(self) -> &'static str { + match self { + Self::Fast => "priority", + Self::Flex => "flex", + } + } + + pub fn from_request_value(value: &str) -> Option { + match value { + "fast" | "priority" => Some(Self::Fast), + "flex" => Some(Self::Flex), + _ => None, + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 017eca8c59..4689bbae20 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -22,7 +22,6 @@ use crate::config_types::CollaborationMode; use crate::config_types::ModeKind; use crate::config_types::Personality; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; -use crate::config_types::ServiceTier; use crate::config_types::WindowsSandboxLevel; use crate::dynamic_tools::DynamicToolCallOutputContentItem; use crate::dynamic_tools::DynamicToolCallRequest; @@ -514,7 +513,7 @@ pub enum Op { /// Use `Some(Some(_))` to set a specific tier, `Some(None)` to clear the /// preference, or `None` to leave the existing value unchanged. #[serde(skip_serializing_if = "Option::is_none")] - service_tier: Option>, + service_tier: Option>, /// EXPERIMENTAL - set a pre-set collaboration mode. /// Takes precedence over model, effort, and developer instructions if set. @@ -575,7 +574,7 @@ pub enum Op { /// explicitly clear the tier for this turn, or `None` to keep the existing /// session preference. #[serde(default, skip_serializing_if = "Option::is_none")] - service_tier: Option>, + service_tier: Option>, // The JSON schema to use for the final assistant message final_output_json_schema: Option, @@ -652,7 +651,7 @@ pub enum Op { /// Use `Some(Some(_))` to set a specific tier, `Some(None)` to clear the /// preference, or `None` to leave the existing value unchanged. #[serde(skip_serializing_if = "Option::is_none")] - service_tier: Option>, + service_tier: Option>, /// EXPERIMENTAL - set a pre-set collaboration mode. /// Takes precedence over model, effort, and developer instructions if set. @@ -3476,7 +3475,7 @@ pub struct SessionConfiguredEvent { pub model_provider_id: String, #[serde(skip_serializing_if = "Option::is_none")] - pub service_tier: Option, + pub service_tier: Option, /// When to escalate for approval for execution pub approval_policy: AskForApproval, @@ -3542,7 +3541,7 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent { thread_name: Option, model: String, model_provider_id: String, - service_tier: Option, + service_tier: Option, approval_policy: AskForApproval, #[serde(default)] approvals_reviewer: ApprovalsReviewer, diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 37fcdb4251..ad174127b9 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1263,7 +1263,8 @@ impl App { AppEvent::PersistServiceTierSelection { service_tier } => { self.refresh_status_line(); let profile = self.active_profile.as_deref(); - self.config.service_tier = service_tier; + self.config.service_tier = + service_tier.map(|service_tier| service_tier.request_value().to_string()); let mut edits = ConfigEditsBuilder::new(&self.config.codex_home) .with_profile(profile) .set_service_tier(service_tier); diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 05aba144c8..b114d08be9 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -617,7 +617,10 @@ 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.service_tier = self + .chat_widget + .configured_service_tier() + .map(|service_tier| service_tier.request_value().to_string()); config.notices.fast_default_opt_out = self.chat_widget.fast_default_opt_out(); config } diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 8598c035a9..e926da7ecc 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -4496,7 +4496,11 @@ async fn fresh_session_config_uses_current_service_tier() { assert_eq!( config.service_tier, - Some(codex_protocol::config_types::ServiceTier::Fast) + Some( + codex_protocol::config_types::ServiceTier::Fast + .request_value() + .to_string() + ) ); } diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 1915cc7683..37712857c5 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -623,7 +623,7 @@ impl App { model.to_string(), *effort, *summary, - *service_tier, + service_tier.clone(), collaboration_mode.clone(), *personality, final_output_json_schema.clone(), diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 15d034cc3d..33837ea12f 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -63,7 +63,10 @@ impl App { thread_name: None, model: self.chat_widget.current_model().to_string(), model_provider_id: self.config.model_provider_id.clone(), - service_tier: self.chat_widget.current_service_tier(), + service_tier: self + .chat_widget + .current_service_tier() + .map(|service_tier| service_tier.request_value().to_string()), approval_policy: AskForApproval::from( self.config.permissions.approval_policy.value(), ), diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 8633da04bf..cfce49ce28 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -15,7 +15,6 @@ use codex_protocol::approvals::GuardianAssessmentEvent; 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::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -46,7 +45,7 @@ pub(crate) enum AppCommand { model: String, effort: Option, summary: Option, - service_tier: Option>, + service_tier: Option>, final_output_json_schema: Option, collaboration_mode: Option, personality: Option, @@ -60,7 +59,7 @@ pub(crate) enum AppCommand { model: Option, effort: Option>, summary: Option, - service_tier: Option>, + service_tier: Option>, collaboration_mode: Option, personality: Option, }, @@ -154,7 +153,7 @@ impl AppCommand { model: String, effort: Option, summary: Option, - service_tier: Option>, + service_tier: Option>, final_output_json_schema: Option, collaboration_mode: Option, personality: Option, @@ -185,7 +184,7 @@ impl AppCommand { model: Option, effort: Option>, summary: Option, - service_tier: Option>, + service_tier: Option>, collaboration_mode: Option, personality: Option, ) -> Self { diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 16f2f05d0c..7d5e06451c 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -531,7 +531,7 @@ impl AppServerSession { model: String, effort: Option, summary: Option, - service_tier: Option>, + service_tier: Option>, collaboration_mode: Option, personality: Option, output_schema: Option, @@ -1344,7 +1344,7 @@ async fn thread_session_state_from_thread_start_response( response.thread.path.clone(), response.model.clone(), response.model_provider.clone(), - response.service_tier, + response.service_tier.clone(), response.approval_policy, response.approvals_reviewer.to_core(), permission_profile, @@ -1376,7 +1376,7 @@ async fn thread_session_state_from_thread_resume_response( response.thread.path.clone(), response.model.clone(), response.model_provider.clone(), - response.service_tier, + response.service_tier.clone(), response.approval_policy, response.approvals_reviewer.to_core(), permission_profile, @@ -1408,7 +1408,7 @@ async fn thread_session_state_from_thread_fork_response( response.thread.path.clone(), response.model.clone(), response.model_provider.clone(), - response.service_tier, + response.service_tier.clone(), response.approval_policy, response.approvals_reviewer.to_core(), permission_profile, @@ -1450,7 +1450,7 @@ async fn thread_session_state_from_thread_response( rollout_path: Option, model: String, model_provider_id: String, - service_tier: Option, + service_tier: Option, approval_policy: AskForApproval, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, permission_profile: PermissionProfile, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 377ebf3a9e..3cee9d2152 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2066,7 +2066,10 @@ impl ChatWidget { self.current_rollout_path = session.rollout_path.clone(); self.current_cwd = Some(session.cwd.to_path_buf()); self.config.cwd = session.cwd.clone(); - self.effective_service_tier = session.service_tier; + self.effective_service_tier = session + .service_tier + .as_deref() + .and_then(ServiceTier::from_request_value); if let Err(err) = self .config .permissions @@ -2115,7 +2118,7 @@ impl ChatWidget { if display == SessionConfiguredDisplay::Normal { let startup_tooltip_override = self.startup_tooltip_override.take(); let show_fast_status = - self.should_show_fast_status(&model_for_header, session.service_tier); + self.should_show_fast_status(&model_for_header, self.effective_service_tier); let session_info_cell = history_cell::new_session_info( &self.config, &model_for_header, @@ -4880,7 +4883,10 @@ 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; + let effective_service_tier = config + .service_tier + .as_deref() + .and_then(ServiceTier::from_request_value); let current_terminal_info = terminal_info(); let runtime_keymap = RuntimeKeymap::from_config(&config.tui_keymap).ok(); let default_keymap = RuntimeKeymap::defaults(); @@ -5865,7 +5871,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 { + 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, @@ -9277,7 +9283,8 @@ impl ChatWidget { /// Set Fast mode in the widget's config copy. pub(crate) fn set_service_tier(&mut self, service_tier: Option) { - self.config.service_tier = service_tier; + self.config.service_tier = + service_tier.map(|service_tier| service_tier.request_value().to_string()); self.effective_service_tier = service_tier; } @@ -9286,7 +9293,10 @@ impl ChatWidget { } pub(crate) fn configured_service_tier(&self) -> Option { - self.config.service_tier + self.config + .service_tier + .as_deref() + .and_then(ServiceTier::from_request_value) } pub(crate) fn fast_default_opt_out(&self) -> Option { @@ -9393,7 +9403,7 @@ impl ChatWidget { /*model*/ None, /*effort*/ None, /*summary*/ None, - Some(service_tier), + Some(service_tier.map(|service_tier| service_tier.request_value().to_string())), /*collaboration_mode*/ None, /*personality*/ None, ))); diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 1db968f7e0..0d781a6a38 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -182,7 +182,10 @@ pub(super) async fn make_chatwidget_manual( }; let current_collaboration_mode = base_mode; let active_collaboration_mask = collaboration_modes::default_mask(model_catalog.as_ref()); - let effective_service_tier = cfg.service_tier; + let effective_service_tier = cfg + .service_tier + .as_deref() + .and_then(ServiceTier::from_request_value); let mut widget = ChatWidget { app_event_tx, codex_op_target: super::CodexOpTarget::Direct(op_tx), diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 3b6b0e7ff2..1f03835511 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1803,9 +1803,9 @@ async fn fast_slash_command_updates_and_persists_local_service_tier() { events.iter().any(|event| matches!( event, AppEvent::CodexOp(Op::OverrideTurnContext { - service_tier: Some(Some(ServiceTier::Fast)), + service_tier: Some(Some(service_tier)), .. - }) + }) if service_tier == ServiceTier::Fast.request_value() )), "expected fast-mode override app event; events: {events:?}" ); @@ -1834,9 +1834,9 @@ async fn fast_keybinding_toggle_uses_same_events_as_fast_slash_command() { events.iter().any(|event| matches!( event, AppEvent::CodexOp(Op::OverrideTurnContext { - service_tier: Some(Some(ServiceTier::Fast)), + service_tier: Some(Some(service_tier)), .. - }) + }) if service_tier == ServiceTier::Fast.request_value() )), "expected fast-mode override app event; events: {events:?}" ); @@ -1884,9 +1884,9 @@ async fn user_turn_carries_service_tier_after_fast_toggle() { match next_submit_op(&mut op_rx) { Op::UserTurn { - service_tier: Some(Some(ServiceTier::Fast)), + service_tier: Some(Some(service_tier)), .. - } => {} + } if service_tier == ServiceTier::Fast.request_value() => {} other => panic!("expected Op::UserTurn with fast service tier, got {other:?}"), } } @@ -1909,9 +1909,9 @@ async fn queued_fast_slash_applies_before_next_queued_message() { events.iter().any(|event| matches!( event, AppEvent::CodexOp(Op::OverrideTurnContext { - service_tier: Some(Some(ServiceTier::Fast)), + service_tier: Some(Some(service_tier)), .. - }) + }) if service_tier == ServiceTier::Fast.request_value() )), "expected queued /fast to update service tier before next turn; events: {events:?}" ); @@ -1919,9 +1919,9 @@ async fn queued_fast_slash_applies_before_next_queued_message() { match next_submit_op(&mut op_rx) { Op::UserTurn { items, - service_tier: Some(Some(ServiceTier::Fast)), + service_tier: Some(Some(service_tier)), .. - } => assert_eq!( + } if service_tier == ServiceTier::Fast.request_value() => assert_eq!( items, vec![UserInput::Text { text: "hello after fast".to_string(), diff --git a/codex-rs/tui/src/session_state.rs b/codex-rs/tui/src/session_state.rs index ec0f7789d7..684a43e5f2 100644 --- a/codex-rs/tui/src/session_state.rs +++ b/codex-rs/tui/src/session_state.rs @@ -25,7 +25,7 @@ pub(crate) struct ThreadSessionState { pub(crate) thread_name: Option, pub(crate) model: String, pub(crate) model_provider_id: String, - pub(crate) service_tier: Option, + pub(crate) service_tier: Option, pub(crate) approval_policy: AskForApproval, pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, /// Canonical active permissions for this session. Legacy app-server