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(), + }) + ); } }