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 62850442f..28a802bb3 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 @@ -10271,6 +10271,16 @@ "string", "null" ] + }, + "upgradeInfo": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ModelUpgradeInfo" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -10373,6 +10383,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 2afa018dc..168a2414a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json @@ -68,6 +68,16 @@ "string", "null" ] + }, + "upgradeInfo": { + "anyOf": [ + { + "$ref": "#/definitions/ModelUpgradeInfo" + }, + { + "type": "null" + } + ] } }, "required": [ @@ -82,6 +92,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 de7b24017..f03e408a0 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 { ModelUpgradeInfo } from "./ModelUpgradeInfo"; 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, upgradeInfo: ModelUpgradeInfo | 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 000000000..82d73e9d0 --- /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 4c638c7d3..ad8642d38 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -111,6 +111,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 f7c4eec7a..225063258 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1396,6 +1396,7 @@ pub struct Model { pub id: String, pub model: String, pub upgrade: Option, + pub upgrade_info: Option, pub display_name: String, pub description: String, pub hidden: bool, @@ -1409,6 +1410,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 20b2dcf2c..2ae54d0b1 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 legacy `upgrade` model ids, and optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`). - `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 1594c6622..a3b3a693e 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(), + }), 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/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs index a71a8a337..ef1dd80d4 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(), + }), display_name: preset.display_name.clone(), description: preset.description.clone(), hidden: !preset.show_in_picker, @@ -127,6 +134,50 @@ async fn list_models_includes_hidden_models() -> Result<()> { Ok(()) } +#[tokio::test] +async fn list_models_returns_upgrade_info_metadata() -> Result<()> { + let codex_home = TempDir::new()?; + write_models_cache(codex_home.path())?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_list_models_request(ModelListParams { + limit: Some(100), + cursor: None, + include_hidden: Some(true), + }) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + + let ModelListResponse { data: items, .. } = to_response::(response)?; + + let item = items + .iter() + .find(|item| item.upgrade_info.is_some()) + .expect("expected at least one model with upgrade info"); + let upgrade_info = item + .upgrade_info + .as_ref() + .expect("expected upgrade info to be populated"); + + assert_eq!(item.upgrade.as_ref(), Some(&upgrade_info.model)); + assert!(!upgrade_info.model.is_empty()); + assert!( + upgrade_info.upgrade_copy.is_some() + || upgrade_info.model_link.is_some() + || upgrade_info.migration_markdown.is_some() + ); + + Ok(()) +} + #[tokio::test] async fn list_models_pagination_works() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/docs/codex_mcp_interface.md b/codex-rs/docs/codex_mcp_interface.md index 3a3abe9a9..763f2602a 100644 --- a/codex-rs/docs/codex_mcp_interface.md +++ b/codex-rs/docs/codex_mcp_interface.md @@ -90,20 +90,26 @@ directory (it returns the restored thread summary). Fetch the catalog of models available in the current Codex build with `model/list`. The request accepts optional pagination inputs: -- `pageSize` – number of models to return (defaults to a server-selected value) +- `limit` – number of models to return (defaults to a server-selected value) - `cursor` – opaque string from the previous response’s `nextCursor` Each response yields: -- `items` – ordered list of models. A model includes: +- `data` – ordered list of models. A model includes: - `id`, `model`, `displayName`, `description` - `supportedReasoningEfforts` – array of objects with: - - `reasoningEffort` – one of `minimal|low|medium|high` + - `reasoningEffort` – one of `none|minimal|low|medium|high|xhigh` - `description` – human-friendly label for the effort - `defaultReasoningEffort` – suggested effort for the UI + - `inputModalities` – accepted input types for the model - `supportsPersonality` – whether the model supports personality-specific instructions - `isDefault` – whether the model is recommended for most users - `upgrade` – optional recommended upgrade model id + - `upgradeInfo` – optional upgrade metadata object with: + - `model` – recommended upgrade model id + - `upgradeCopy` – optional display copy for the upgrade recommendation + - `modelLink` – optional link for the upgrade recommendation + - `migrationMarkdown` – optional markdown shown when presenting the upgrade - `nextCursor` – pass into the next request to continue paging (optional) ## Collaboration modes (experimental)