From 7111a580b166dbc80df9f5ad95ef4ec7003e7c3b Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 26 Feb 2026 17:39:57 -0800 Subject: [PATCH 1/5] codex: tweak show_nux_new wording (#12972) --- codex-rs/protocol/src/openai_models.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index fbfc9f0000..a4cd8e6b67 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -123,7 +123,7 @@ pub struct ModelPreset { pub upgrade: Option, /// Whether this preset should appear in the picker UI. pub show_in_picker: bool, - /// Whether the TUI should surface the new-model startup tip for this preset. + /// Whether to show NUX for this preset. #[serde(default)] pub show_nux_new: bool, /// whether this model is supported in the api From f16ad6b82f6c291658394b6c583112266b0783d1 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 26 Feb 2026 17:42:53 -0800 Subject: [PATCH 2/5] Rename show_nux_new to show_nux --- .../app-server/tests/common/models_cache.rs | 2 +- codex-rs/codex-api/tests/models_integration.rs | 2 +- codex-rs/core/src/models_manager/model_info.rs | 2 +- codex-rs/core/tests/suite/model_switching.rs | 4 ++-- codex-rs/core/tests/suite/models_cache_ttl.rs | 2 +- codex-rs/core/tests/suite/personality.rs | 4 ++-- codex-rs/core/tests/suite/remote_models.rs | 6 +++--- codex-rs/core/tests/suite/rmcp_client.rs | 2 +- codex-rs/core/tests/suite/view_image.rs | 2 +- codex-rs/protocol/src/openai_models.rs | 18 +++++++++--------- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 90f81bf5b2..685aae0b11 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -34,7 +34,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index e940e8afbd..59daec2c28 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -82,7 +82,7 @@ async fn models_client_hits_models_endpoint() { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index 455bd5fcf2..20c7eff16a 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -69,7 +69,7 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { visibility: ModelVisibility::None, supported_in_api: true, priority: 99, - show_nux_new: false, + show_nux: false, upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), model_messages: local_personality_messages_for_slug(slug), diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index bffaa248de..7d355b03cf 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -237,7 +237,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -396,7 +396,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 563c0bffcd..3f2bff822d 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -339,7 +339,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 0000cd725f..642ce348a4 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -602,7 +602,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -711,7 +711,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index a36822db9d..ec8be2efd8 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -297,7 +297,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -535,7 +535,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -997,7 +997,7 @@ fn test_remote_model_with_policy( default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy, supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index cea528348f..addd764676 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -401,7 +401,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 4d341dd6b7..0231539e49 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -678,7 +678,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux_new: false, + show_nux: false, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index a4cd8e6b67..3554cfd7de 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -125,7 +125,7 @@ pub struct ModelPreset { pub show_in_picker: bool, /// Whether to show NUX for this preset. #[serde(default)] - pub show_nux_new: bool, + pub show_nux: bool, /// whether this model is supported in the api pub supported_in_api: bool, /// Input modalities accepted when composing user turns for this preset. @@ -229,7 +229,7 @@ pub struct ModelInfo { pub supported_in_api: bool, pub priority: i32, #[serde(default)] - pub show_nux_new: bool, + pub show_nux: bool, pub upgrade: Option, pub base_instructions: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -415,7 +415,7 @@ impl From for ModelPreset { migration_markdown: Some(upgrade.migration_markdown.clone()), }), show_in_picker: info.visibility == ModelVisibility::List, - show_nux_new: info.show_nux_new, + show_nux: info.show_nux, supported_in_api: info.supported_in_api, input_modalities: info.input_modalities, } @@ -501,7 +501,7 @@ mod tests { visibility: ModelVisibility::List, supported_in_api: true, priority: 1, - show_nux_new: false, + show_nux: false, upgrade: None, base_instructions: "base".to_string(), model_messages: spec, @@ -677,7 +677,7 @@ mod tests { } #[test] - fn model_info_defaults_show_nux_new_to_false_when_omitted() { + fn model_info_defaults_show_nux_to_false_when_omitted() { let model: ModelInfo = serde_json::from_value(serde_json::json!({ "slug": "test-model", "display_name": "Test Model", @@ -709,16 +709,16 @@ mod tests { })) .expect("deserialize model info"); - assert!(!model.show_nux_new); + assert!(!model.show_nux); } #[test] - fn model_preset_preserves_show_nux_new() { + fn model_preset_preserves_show_nux() { let preset = ModelPreset::from(ModelInfo { - show_nux_new: true, + show_nux: true, ..test_model(None) }); - assert!(preset.show_nux_new); + assert!(preset.show_nux); } } From d47407a2e5c2dac2b9dae5a978b15a135ef4024e Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 26 Feb 2026 17:56:32 -0800 Subject: [PATCH 3/5] codex: fix show_nux test initializers --- codex-rs/tui/src/chatwidget/tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index cbbd40ec93..515b000b93 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -6019,6 +6019,7 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() { is_default: false, upgrade: None, show_in_picker, + show_nux: false, supported_in_api: true, input_modalities: default_input_modalities(), }; @@ -6287,6 +6288,7 @@ async fn single_reasoning_option_skips_selection() { is_default: false, upgrade: None, show_in_picker: true, + show_nux: false, supported_in_api: true, input_modalities: default_input_modalities(), }; From 3b0251757ab186c54146cd9df12f8c502eb33abb Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 26 Feb 2026 18:48:05 -0800 Subject: [PATCH 4/5] Add model availability NUX metadata --- .../codex_app_server_protocol.schemas.json | 25 +++++++++++++ .../schema/json/v2/ModelListResponse.json | 25 +++++++++++++ .../schema/typescript/v2/Model.ts | 3 +- .../typescript/v2/ModelAvailabilityNux.ts | 5 +++ .../schema/typescript/v2/index.ts | 1 + .../app-server-protocol/src/protocol/v2.rs | 19 ++++++++++ codex-rs/app-server/README.md | 2 +- codex-rs/app-server/src/models.rs | 1 + .../app-server/tests/common/models_cache.rs | 2 +- .../app-server/tests/suite/v2/model_list.rs | 1 + .../codex-api/tests/models_integration.rs | 2 +- .../core/src/models_manager/model_info.rs | 2 +- codex-rs/core/tests/suite/model_switching.rs | 4 +- codex-rs/core/tests/suite/models_cache_ttl.rs | 2 +- codex-rs/core/tests/suite/personality.rs | 4 +- codex-rs/core/tests/suite/remote_models.rs | 6 +-- codex-rs/core/tests/suite/rmcp_client.rs | 2 +- codex-rs/core/tests/suite/view_image.rs | 2 +- codex-rs/protocol/src/openai_models.rs | 37 +++++++++++++------ 19 files changed, 118 insertions(+), 27 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ModelAvailabilityNux.ts 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 62850442fc..dfcbcca389 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 @@ -10225,6 +10225,16 @@ }, "Model": { "properties": { + "availabilityNux": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ModelAvailabilityNux" + }, + { + "type": "null" + } + ] + }, "defaultReasoningEffort": { "$ref": "#/definitions/v2/ReasoningEffort" }, @@ -10285,6 +10295,21 @@ ], "type": "object" }, + "ModelAvailabilityNux": { + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "id", + "message" + ], + "type": "object" + }, "ModelListParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json index 2afa018dc5..9bb5ecd4f0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json @@ -22,6 +22,16 @@ }, "Model": { "properties": { + "availabilityNux": { + "anyOf": [ + { + "$ref": "#/definitions/ModelAvailabilityNux" + }, + { + "type": "null" + } + ] + }, "defaultReasoningEffort": { "$ref": "#/definitions/ReasoningEffort" }, @@ -82,6 +92,21 @@ ], "type": "object" }, + "ModelAvailabilityNux": { + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "id", + "message" + ], + "type": "object" + }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts index de7b240173..b2403cd8ea 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts @@ -3,6 +3,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { InputModality } from "../InputModality"; import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; import type { ReasoningEffortOption } from "./ReasoningEffortOption"; -export type Model = { id: string, model: string, upgrade: string | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, }; +export type Model = { id: string, model: string, upgrade: string | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelAvailabilityNux.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelAvailabilityNux.ts new file mode 100644 index 0000000000..b0254a68b5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelAvailabilityNux.ts @@ -0,0 +1,5 @@ +// 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 ModelAvailabilityNux = { id: string, message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 4c638c7d30..7bd3ecfcfb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -107,6 +107,7 @@ export type { McpToolCallResult } from "./McpToolCallResult"; export type { McpToolCallStatus } from "./McpToolCallStatus"; export type { MergeStrategy } from "./MergeStrategy"; export type { Model } from "./Model"; +export type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; export type { ModelListParams } from "./ModelListParams"; export type { ModelListResponse } from "./ModelListResponse"; export type { ModelRerouteReason } from "./ModelRerouteReason"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index f7c4eec7a7..a739070ab4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -31,6 +31,7 @@ use codex_protocol::models::MessagePhase; use codex_protocol::models::PermissionProfile as CorePermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::InputModality; +use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailabilityNux; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::default_input_modalities; use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand; @@ -1389,6 +1390,23 @@ pub struct ModelListParams { pub include_hidden: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelAvailabilityNux { + pub id: String, + pub message: String, +} + +impl From for ModelAvailabilityNux { + fn from(value: CoreModelAvailabilityNux) -> Self { + Self { + id: value.id, + message: value.message, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1396,6 +1414,7 @@ pub struct Model { pub id: String, pub model: String, pub upgrade: Option, + pub availability_nux: Option, pub display_name: String, pub description: String, pub hidden: bool, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 20b2dcf2c7..3227ea0421 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -142,7 +142,7 @@ Example with notification opt-out: - `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`. - `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review. - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). -- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options and optional `upgrade` model ids. +- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional `upgrade` model ids, and optional `availabilityNux` metadata. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 1594c66229..38cf25ca8a 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -25,6 +25,7 @@ fn model_from_preset(preset: ModelPreset) -> Model { id: preset.id.to_string(), model: preset.model.to_string(), upgrade: preset.upgrade.map(|upgrade| upgrade.id), + availability_nux: preset.availability_nux.map(Into::into), display_name: preset.display_name.to_string(), description: preset.description.to_string(), hidden: !preset.show_in_picker, diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 685aae0b11..0de8fda5f2 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -34,7 +34,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/app-server/tests/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs index a71a8a3377..04a4b8d9bb 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -24,6 +24,7 @@ fn model_from_preset(preset: &ModelPreset) -> Model { id: preset.id.clone(), model: preset.model.clone(), upgrade: preset.upgrade.as_ref().map(|upgrade| upgrade.id.clone()), + availability_nux: preset.availability_nux.clone().map(Into::into), display_name: preset.display_name.clone(), description: preset.description.clone(), hidden: !preset.show_in_picker, diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 59daec2c28..2b61f0de60 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -82,7 +82,7 @@ async fn models_client_hits_models_endpoint() { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index 20c7eff16a..4824e4cd11 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -69,7 +69,7 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { visibility: ModelVisibility::None, supported_in_api: true, priority: 99, - show_nux: false, + availability_nux: None, upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), model_messages: local_personality_messages_for_slug(slug), diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 7d355b03cf..62d0545462 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -237,7 +237,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -396,7 +396,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 3f2bff822d..9948eb4e21 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -339,7 +339,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 642ce348a4..97ce2360ba 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -602,7 +602,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -711,7 +711,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index ec8be2efd8..c304cc25fa 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -297,7 +297,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -535,7 +535,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, @@ -997,7 +997,7 @@ fn test_remote_model_with_policy( default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy, supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index addd764676..3c7e50230c 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -401,7 +401,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 0231539e49..73502241e3 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -678,7 +678,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an default_reasoning_summary: ReasoningSummary::Auto, support_verbosity: false, default_verbosity: None, - show_nux: false, + availability_nux: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 3554cfd7de..85b79939d1 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -99,6 +99,12 @@ pub struct ModelUpgrade { pub migration_markdown: Option, } +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +pub struct ModelAvailabilityNux { + pub id: String, + pub message: String, +} + /// Metadata describing a Codex-supported model. #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct ModelPreset { @@ -123,9 +129,8 @@ pub struct ModelPreset { pub upgrade: Option, /// Whether this preset should appear in the picker UI. pub show_in_picker: bool, - /// Whether to show NUX for this preset. - #[serde(default)] - pub show_nux: bool, + /// Availability NUX shown when this preset becomes accessible to the user. + pub availability_nux: Option, /// whether this model is supported in the api pub supported_in_api: bool, /// Input modalities accepted when composing user turns for this preset. @@ -228,8 +233,7 @@ pub struct ModelInfo { pub visibility: ModelVisibility, pub supported_in_api: bool, pub priority: i32, - #[serde(default)] - pub show_nux: bool, + pub availability_nux: Option, pub upgrade: Option, pub base_instructions: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -415,7 +419,7 @@ impl From for ModelPreset { migration_markdown: Some(upgrade.migration_markdown.clone()), }), show_in_picker: info.visibility == ModelVisibility::List, - show_nux: info.show_nux, + availability_nux: info.availability_nux, supported_in_api: info.supported_in_api, input_modalities: info.input_modalities, } @@ -501,7 +505,7 @@ mod tests { visibility: ModelVisibility::List, supported_in_api: true, priority: 1, - show_nux: false, + availability_nux: None, upgrade: None, base_instructions: "base".to_string(), model_messages: spec, @@ -677,7 +681,7 @@ mod tests { } #[test] - fn model_info_defaults_show_nux_to_false_when_omitted() { + fn model_info_defaults_availability_nux_to_none_when_omitted() { let model: ModelInfo = serde_json::from_value(serde_json::json!({ "slug": "test-model", "display_name": "Test Model", @@ -709,16 +713,25 @@ mod tests { })) .expect("deserialize model info"); - assert!(!model.show_nux); + assert_eq!(model.availability_nux, None); } #[test] - fn model_preset_preserves_show_nux() { + fn model_preset_preserves_availability_nux() { let preset = ModelPreset::from(ModelInfo { - show_nux: true, + availability_nux: Some(ModelAvailabilityNux { + id: "try_spark".to_string(), + message: "Try Spark.".to_string(), + }), ..test_model(None) }); - assert!(preset.show_nux); + assert_eq!( + preset.availability_nux, + Some(ModelAvailabilityNux { + id: "try_spark".to_string(), + message: "Try Spark.".to_string(), + }) + ); } } From c5fa78fa8133632dab6e1d7bc3dbfc40b29c1eda Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 26 Feb 2026 18:56:02 -0800 Subject: [PATCH 5/5] Resolve app-server model payload merge drift --- .../codex_app_server_protocol.schemas.json | 39 +++++++++++++++++++ .../schema/json/v2/ModelListResponse.json | 39 +++++++++++++++++++ .../schema/typescript/v2/Model.ts | 3 +- .../schema/typescript/v2/ModelUpgradeInfo.ts | 5 +++ .../schema/typescript/v2/index.ts | 1 + .../app-server-protocol/src/protocol/v2.rs | 11 ++++++ codex-rs/app-server/README.md | 2 +- codex-rs/app-server/src/models.rs | 9 ++++- .../app-server/tests/suite/v2/model_list.rs | 7 ++++ 9 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ModelUpgradeInfo.ts 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 dfcbcca389..ee6ba28ff3 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 @@ -10281,6 +10281,16 @@ "string", "null" ] + }, + "upgradeInfo": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ModelUpgradeInfo" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -10398,6 +10408,35 @@ "title": "ModelReroutedNotification", "type": "object" }, + "ModelUpgradeInfo": { + "properties": { + "migrationMarkdown": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "modelLink": { + "type": [ + "string", + "null" + ] + }, + "upgradeCopy": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "model" + ], + "type": "object" + }, "NetworkAccess": { "enum": [ "restricted", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json index 9bb5ecd4f0..992e966faf 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json @@ -78,6 +78,16 @@ "string", "null" ] + }, + "upgradeInfo": { + "anyOf": [ + { + "$ref": "#/definitions/ModelUpgradeInfo" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -107,6 +117,35 @@ ], "type": "object" }, + "ModelUpgradeInfo": { + "properties": { + "migrationMarkdown": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "modelLink": { + "type": [ + "string", + "null" + ] + }, + "upgradeCopy": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "model" + ], + "type": "object" + }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts index b2403cd8ea..cd99108190 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/Model.ts @@ -4,6 +4,7 @@ import type { InputModality } from "../InputModality"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ModelAvailabilityNux } from "./ModelAvailabilityNux"; +import type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; import type { ReasoningEffortOption } from "./ReasoningEffortOption"; -export type Model = { id: string, model: string, upgrade: string | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, }; +export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array, defaultReasoningEffort: ReasoningEffort, inputModalities: Array, supportsPersonality: boolean, isDefault: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ModelUpgradeInfo.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ModelUpgradeInfo.ts new file mode 100644 index 0000000000..82d73e9d06 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ModelUpgradeInfo.ts @@ -0,0 +1,5 @@ +// 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 ModelUpgradeInfo = { model: string, upgradeCopy: string | null, modelLink: string | null, migrationMarkdown: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 7bd3ecfcfb..4a881427d9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -112,6 +112,7 @@ export type { ModelListParams } from "./ModelListParams"; export type { ModelListResponse } from "./ModelListResponse"; export type { ModelRerouteReason } from "./ModelRerouteReason"; export type { ModelReroutedNotification } from "./ModelReroutedNotification"; +export type { ModelUpgradeInfo } from "./ModelUpgradeInfo"; export type { NetworkAccess } from "./NetworkAccess"; export type { NetworkApprovalContext } from "./NetworkApprovalContext"; export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a739070ab4..ad6c26f8a4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1414,6 +1414,7 @@ pub struct Model { pub id: String, pub model: String, pub upgrade: Option, + pub upgrade_info: Option, pub availability_nux: Option, pub display_name: String, pub description: String, @@ -1428,6 +1429,16 @@ pub struct Model { pub is_default: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelUpgradeInfo { + pub model: String, + pub upgrade_copy: Option, + pub model_link: Option, + pub migration_markdown: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 3227ea0421..60cf6d6107 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -142,7 +142,7 @@ Example with notification opt-out: - `thread/realtime/stop` — stop the active realtime session for the thread (experimental); returns `{}`. - `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits `item/started`/`item/completed` notifications with `enteredReviewMode` and `exitedReviewMode` items, plus a final assistant `agentMessage` containing the review. - `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation). -- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional `upgrade` model ids, and optional `availabilityNux` metadata. +- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 38cf25ca8a..6dbe77455f 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use codex_app_server_protocol::Model; +use codex_app_server_protocol::ModelUpgradeInfo; use codex_app_server_protocol::ReasoningEffortOption; use codex_core::ThreadManager; use codex_core::models_manager::manager::RefreshStrategy; @@ -24,7 +25,13 @@ fn model_from_preset(preset: ModelPreset) -> Model { Model { id: preset.id.to_string(), model: preset.model.to_string(), - upgrade: preset.upgrade.map(|upgrade| upgrade.id), + upgrade: preset.upgrade.as_ref().map(|upgrade| upgrade.id.clone()), + upgrade_info: preset.upgrade.as_ref().map(|upgrade| ModelUpgradeInfo { + model: upgrade.id.clone(), + upgrade_copy: upgrade.upgrade_copy.clone(), + model_link: upgrade.model_link.clone(), + migration_markdown: upgrade.migration_markdown.clone(), + }), availability_nux: preset.availability_nux.map(Into::into), display_name: preset.display_name.to_string(), description: preset.description.to_string(), diff --git a/codex-rs/app-server/tests/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs index 04a4b8d9bb..eba8905a77 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -9,6 +9,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::Model; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; +use codex_app_server_protocol::ModelUpgradeInfo; use codex_app_server_protocol::ReasoningEffortOption; use codex_app_server_protocol::RequestId; use codex_protocol::openai_models::ModelPreset; @@ -24,6 +25,12 @@ fn model_from_preset(preset: &ModelPreset) -> Model { id: preset.id.clone(), model: preset.model.clone(), upgrade: preset.upgrade.as_ref().map(|upgrade| upgrade.id.clone()), + upgrade_info: preset.upgrade.as_ref().map(|upgrade| ModelUpgradeInfo { + model: upgrade.id.clone(), + upgrade_copy: upgrade.upgrade_copy.clone(), + model_link: upgrade.model_link.clone(), + migration_markdown: upgrade.migration_markdown.clone(), + }), availability_nux: preset.availability_nux.clone().map(Into::into), display_name: preset.display_name.clone(), description: preset.description.clone(),