feat: expose provider capability bounds to app server clients (#20049)

follow up of #19442. The app server now exposes provider-derived bounds
through a new v2 `modelProvider/read` method. The response reports the
configured provider map key as `modelProvider` and returns the effective
capability booleans so clients can align their UI with the same
provider-owned limits used by core.
This commit is contained in:
Celia Chen
2026-04-28 18:36:19 -07:00
committed by GitHub
Unverified
parent 4c39ad33cb
commit 8c47e36504
17 changed files with 317 additions and 1 deletions
@@ -1896,6 +1896,9 @@
},
"type": "object"
},
"ModelProviderCapabilitiesReadParams": {
"type": "object"
},
"NetworkAccess": {
"enum": [
"restricted",
@@ -5478,6 +5481,30 @@
"title": "Model/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"modelProvider/capabilities/read"
],
"title": "ModelProvider/capabilities/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ModelProviderCapabilitiesReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ModelProvider/capabilities/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -1266,6 +1266,30 @@
"title": "Model/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"modelProvider/capabilities/read"
],
"title": "ModelProvider/capabilities/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ModelProviderCapabilitiesReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ModelProvider/capabilities/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -11035,6 +11059,32 @@
"title": "ModelListResponse",
"type": "object"
},
"ModelProviderCapabilitiesReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ModelProviderCapabilitiesReadParams",
"type": "object"
},
"ModelProviderCapabilitiesReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"imageGeneration": {
"type": "boolean"
},
"namespaceTools": {
"type": "boolean"
},
"webSearch": {
"type": "boolean"
}
},
"required": [
"imageGeneration",
"namespaceTools",
"webSearch"
],
"title": "ModelProviderCapabilitiesReadResponse",
"type": "object"
},
"ModelRerouteReason": {
"enum": [
"highRiskCyberActivity"
@@ -1972,6 +1972,30 @@
"title": "Model/listRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"modelProvider/capabilities/read"
],
"title": "ModelProvider/capabilities/readRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ModelProviderCapabilitiesReadParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "ModelProvider/capabilities/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -7689,6 +7713,32 @@
"title": "ModelListResponse",
"type": "object"
},
"ModelProviderCapabilitiesReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ModelProviderCapabilitiesReadParams",
"type": "object"
},
"ModelProviderCapabilitiesReadResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"imageGeneration": {
"type": "boolean"
},
"namespaceTools": {
"type": "boolean"
},
"webSearch": {
"type": "boolean"
}
},
"required": [
"imageGeneration",
"namespaceTools",
"webSearch"
],
"title": "ModelProviderCapabilitiesReadResponse",
"type": "object"
},
"ModelRerouteReason": {
"enum": [
"highRiskCyberActivity"
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ModelProviderCapabilitiesReadParams",
"type": "object"
}
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"imageGeneration": {
"type": "boolean"
},
"namespaceTools": {
"type": "boolean"
},
"webSearch": {
"type": "boolean"
}
},
"required": [
"imageGeneration",
"namespaceTools",
"webSearch"
],
"title": "ModelProviderCapabilitiesReadResponse",
"type": "object"
}
File diff suppressed because one or more lines are too long
@@ -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 ModelProviderCapabilitiesReadParams = Record<string, never>;
@@ -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 ModelProviderCapabilitiesReadResponse = { namespaceTools: boolean, imageGeneration: boolean, webSearch: boolean, };
@@ -234,6 +234,8 @@ export type { Model } from "./Model";
export type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
export type { ModelListParams } from "./ModelListParams";
export type { ModelListResponse } from "./ModelListResponse";
export type { ModelProviderCapabilitiesReadParams } from "./ModelProviderCapabilitiesReadParams";
export type { ModelProviderCapabilitiesReadResponse } from "./ModelProviderCapabilitiesReadResponse";
export type { ModelRerouteReason } from "./ModelRerouteReason";
export type { ModelReroutedNotification } from "./ModelReroutedNotification";
export type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
@@ -642,6 +642,11 @@ client_request_definitions! {
serialization: None,
response: v2::ModelListResponse,
},
ModelProviderCapabilitiesRead => "modelProvider/capabilities/read" {
params: v2::ModelProviderCapabilitiesReadParams,
serialization: None,
response: v2::ModelProviderCapabilitiesReadResponse,
},
ExperimentalFeatureList => "experimentalFeature/list" {
params: v2::ExperimentalFeatureListParams,
serialization: global("config"),
@@ -2243,6 +2248,23 @@ mod tests {
Ok(())
}
#[test]
fn serialize_model_provider_capabilities_read() -> Result<()> {
let request = ClientRequest::ModelProviderCapabilitiesRead {
request_id: RequestId::Integer(7),
params: v2::ModelProviderCapabilitiesReadParams {},
};
assert_eq!(
json!({
"method": "modelProvider/capabilities/read",
"id": 7,
"params": {}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_list_collaboration_modes() -> Result<()> {
let request = ClientRequest::CollaborationModeList {
@@ -2355,6 +2355,20 @@ pub struct GetAccountResponse {
pub requires_openai_auth: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ModelProviderCapabilitiesReadParams {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ModelProviderCapabilitiesReadResponse {
pub namespace_tools: bool,
pub image_generation: bool,
pub web_search: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
+1
View File
@@ -191,6 +191,7 @@ Example with notification opt-out:
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `remote_control`, `tool_search`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
- `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.
@@ -1275,6 +1275,11 @@ impl CodexMessageProcessor {
ClientRequest::ConfigRequirementsRead { .. } => {
warn!("ConfigRequirementsRead request reached CodexMessageProcessor unexpectedly");
}
ClientRequest::ModelProviderCapabilitiesRead { .. } => {
warn!(
"ModelProviderCapabilitiesRead request reached CodexMessageProcessor unexpectedly"
);
}
ClientRequest::ExternalAgentConfigDetect { .. }
| ClientRequest::ExternalAgentConfigImport { .. } => {
warn!("ExternalAgentConfig request reached CodexMessageProcessor unexpectedly");
@@ -56,6 +56,7 @@ use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequestPayload;
use codex_app_server_protocol::experimental_required_message;
@@ -76,6 +77,7 @@ use codex_login::default_client::USER_AGENT_SUFFIX;
use codex_login::default_client::get_codex_user_agent;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_login::default_client::set_default_originator;
use codex_model_provider::create_model_provider;
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
@@ -938,6 +940,13 @@ impl MessageProcessor {
)
.await;
}
ClientRequest::ModelProviderCapabilitiesRead {
request_id,
params: _,
} => {
self.handle_model_provider_capabilities_read(request_id_for_connection(request_id))
.await;
}
other => {
// Box the delegated future so this wrapper's async state machine does not
// inline the full `CodexMessageProcessor::process_request` future, which
@@ -957,6 +966,24 @@ impl MessageProcessor {
Ok(())
}
async fn handle_model_provider_capabilities_read(&self, request_id: ConnectionRequestId) {
let result = async {
let config = self
.config_api
.load_latest_config(/*fallback_cwd*/ None)
.await?;
let provider = create_model_provider(config.model_provider, /*auth_manager*/ None);
let capabilities = provider.capabilities();
Ok::<_, JSONRPCErrorError>(ModelProviderCapabilitiesReadResponse {
namespace_tools: capabilities.namespace_tools,
image_generation: capabilities.image_generation,
web_search: capabilities.web_search,
})
}
.await;
self.outgoing.send_result(request_id, result).await;
}
async fn handle_config_value_write(
&self,
request_id: ConnectionRequestId,
@@ -54,6 +54,7 @@ use codex_app_server_protocol::McpResourceReadParams;
use codex_app_server_protocol::McpServerToolCallParams;
use codex_app_server_protocol::MockExperimentalMethodParams;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelProviderCapabilitiesReadParams;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginListParams;
use codex_app_server_protocol::PluginReadParams;
@@ -517,6 +518,16 @@ impl McpProcess {
self.send_request("model/list", params).await
}
/// Send a `modelProvider/capabilities/read` JSON-RPC request.
pub async fn send_model_provider_capabilities_read_request(
&mut self,
params: ModelProviderCapabilitiesReadParams,
) -> anyhow::Result<i64> {
let params = Some(serde_json::to_value(params)?);
self.send_request("modelProvider/capabilities/read", params)
.await
}
/// Send an `experimentalFeature/list` JSON-RPC request.
pub async fn send_experimental_feature_list_request(
&mut self,
@@ -26,6 +26,7 @@ mod mcp_server_status;
mod mcp_tool;
mod memory_reset;
mod model_list;
mod model_provider_capabilities_read;
mod output_schema;
mod plan_item;
mod plugin_install;
@@ -0,0 +1,69 @@
use std::time::Duration;
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ModelProviderCapabilitiesReadParams;
use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse;
use codex_app_server_protocol::RequestId;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[tokio::test]
async fn read_default_provider_capabilities() -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_model_provider_capabilities_read_request(ModelProviderCapabilitiesReadParams {})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: ModelProviderCapabilitiesReadResponse = to_response(response)?;
let expected = ModelProviderCapabilitiesReadResponse {
namespace_tools: true,
image_generation: true,
web_search: true,
};
assert_eq!(received, expected);
Ok(())
}
#[tokio::test]
async fn read_amazon_bedrock_provider_capabilities() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"model_provider = "amazon-bedrock"
"#,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_model_provider_capabilities_read_request(ModelProviderCapabilitiesReadParams {})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let received: ModelProviderCapabilitiesReadResponse = to_response(response)?;
let expected = ModelProviderCapabilitiesReadResponse {
namespace_tools: false,
image_generation: false,
web_search: false,
};
assert_eq!(received, expected);
Ok(())
}