From e916cb4de24a02683fcc139d60b34a02a55515f0 Mon Sep 17 00:00:00 2001 From: won Date: Tue, 3 Mar 2026 22:28:05 -0800 Subject: [PATCH 01/11] carGOOOOO --- codex-rs/core/config.schema.json | 8 +- codex-rs/core/src/client.rs | 15 +++- codex-rs/core/src/client_common.rs | 84 +++++++++++++++++++ codex-rs/core/src/codex.rs | 2 + codex-rs/core/src/compact_remote.rs | 1 + codex-rs/core/src/context_manager/history.rs | 6 +- .../core/src/context_manager/history_tests.rs | 42 ++++++++++ codex-rs/core/src/features.rs | 12 +++ codex-rs/core/src/rollout/policy.rs | 2 + codex-rs/core/src/tools/spec.rs | 83 +++++++++++++++++- codex-rs/otel/src/traces/otel_manager.rs | 1 + codex-rs/protocol/src/models.rs | 45 ++++++++++ 12 files changed, 295 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 71d624d2c03..a751e5acd24 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -365,6 +365,9 @@ "image_detail_original": { "type": "boolean" }, + "image_generation": { + "type": "boolean" + }, "include_apply_patch_tool": { "type": "boolean" }, @@ -1752,6 +1755,9 @@ "image_detail_original": { "type": "boolean" }, + "image_generation": { + "type": "boolean" + }, "include_apply_patch_tool": { "type": "boolean" }, @@ -2241,4 +2247,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index d81c10f3968..7414cf7fe70 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -89,6 +89,7 @@ use crate::auth::RefreshTokenError; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; +use crate::client_common::rewrite_image_generation_calls_for_stateless_input; use crate::config::Config; use crate::default_client::build_reqwest_client; use crate::error::CodexErr; @@ -497,7 +498,8 @@ impl ModelClientSession { service_tier: Option, ) -> Result { let instructions = &prompt.base_instructions.text; - let input = prompt.get_formatted_input(); + let store = provider.is_azure_responses_endpoint(); + let input = Self::prepare_responses_input(store, prompt.get_formatted_input()); let tools = create_tools_json_for_responses_api(&prompt.tools)?; let default_reasoning_effort = model_info.default_reasoning_level; let reasoning = if model_info.supports_reasoning_summaries { @@ -541,7 +543,7 @@ impl ModelClientSession { tool_choice: "auto".to_string(), parallel_tool_calls: prompt.parallel_tool_calls, reasoning, - store: provider.is_azure_responses_endpoint(), + store, stream: true, include, service_tier: match service_tier { @@ -566,7 +568,6 @@ impl ModelClientSession { ) -> ApiResponsesOptions { let turn_metadata_header = parse_turn_metadata_header(turn_metadata_header); let conversation_id = self.client.state.conversation_id.to_string(); - ApiResponsesOptions { conversation_id: Some(conversation_id), session_source: Some(self.client.state.session_source.clone()), @@ -655,6 +656,14 @@ impl ModelClientSession { }) } + fn prepare_responses_input(store: bool, input: Vec) -> Vec { + if store { + input + } else { + rewrite_image_generation_calls_for_stateless_input(input) + } + } + /// Opportunistically preconnects a websocket for this turn-scoped client session. /// /// This performs only connection setup; it never sends prompt payloads. diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index cddf99d6f53..1564cd58494 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -3,6 +3,7 @@ use crate::config::types::Personality; use crate::error::Result; pub use codex_api::common::ResponseEvent; use codex_protocol::models::BaseInstructions; +use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::ResponseItem; use futures::Stream; @@ -64,6 +65,34 @@ impl Prompt { } } +pub(crate) fn rewrite_image_generation_calls_for_stateless_input( + items: Vec, +) -> Vec { + items + .into_iter() + .filter_map(|item| match item { + ResponseItem::ImageGenerationCall { + result: Some(result), + .. + } => Some(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputImage { + image_url: if result.starts_with("data:") { + result + } else { + format!("data:image/png;base64,{result}") + }, + }], + end_turn: None, + phase: None, + }), + ResponseItem::ImageGenerationCall { .. } => None, + _ => Some(item), + }) + .collect() +} + fn reserialize_shell_outputs(items: &mut [ResponseItem]) { let mut shell_call_ids: HashSet = HashSet::new(); @@ -166,6 +195,8 @@ pub(crate) mod tools { Function(ResponsesApiTool), #[serde(rename = "local_shell")] LocalShell {}, + #[serde(rename = "image_generation")] + ImageGeneration {}, // TODO: Understand why we get an error on web_search although the API docs say it's supported. // https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#:~:text=%7B%20type%3A%20%22web_search%22%20%7D%2C // The `external_web_access` field determines whether the web search is over cached or live content. @@ -184,6 +215,7 @@ pub(crate) mod tools { match self { ToolSpec::Function(tool) => tool.name.as_str(), ToolSpec::LocalShell {} => "local_shell", + ToolSpec::ImageGeneration {} => "image_generation", ToolSpec::WebSearch { .. } => "web_search", ToolSpec::Freeform(tool) => tool.name.as_str(), } @@ -234,11 +266,63 @@ mod tests { use codex_api::common::OpenAiVerbosity; use codex_api::common::TextControls; use codex_api::create_text_param_for_request; + use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputPayload; use pretty_assertions::assert_eq; use super::*; + #[test] + fn rewrites_image_generation_calls_for_stateless_input() { + let input = vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "generate a lobster".to_string(), + }], + end_turn: None, + phase: None, + }, + ResponseItem::ImageGenerationCall { + id: Some("ig_123".to_string()), + status: Some("completed".to_string()), + revised_prompt: Some("lobster".to_string()), + result: Some("Zm9v".to_string()), + }, + ResponseItem::ImageGenerationCall { + id: Some("ig_456".to_string()), + status: Some("completed".to_string()), + revised_prompt: None, + result: None, + }, + ]; + + assert_eq!( + rewrite_image_generation_calls_for_stateless_input(input), + vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "generate a lobster".to_string(), + }], + end_turn: None, + phase: None, + }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputImage { + image_url: "data:image/png;base64,Zm9v".to_string(), + }], + end_turn: None, + phase: None, + }, + ] + ); + } + #[test] fn serializes_text_verbosity_when_set() { let input: Vec = vec![]; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9b9e0214a85..488ccfa485f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -737,6 +737,7 @@ impl TurnContext { session_source: self.session_source.clone(), }) .with_allow_login_shell(self.tools_config.allow_login_shell) + .with_image_generation_enabled(features.enabled(Feature::ImageGeneration)) .with_agent_roles(config.agent_roles.clone()); Self { @@ -1110,6 +1111,7 @@ impl Session { session_source: session_source.clone(), }) .with_allow_login_shell(per_turn_config.permissions.allow_login_shell) + .with_image_generation_enabled(per_turn_config.features.enabled(Feature::ImageGeneration)) .with_agent_roles(per_turn_config.agent_roles.clone()); let cwd = session_configuration.cwd.clone(); diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 6d8368bce45..40c520f659c 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -206,6 +206,7 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool { | ResponseItem::CustomToolCall { .. } | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::WebSearchCall { .. } + | ResponseItem::ImageGenerationCall { .. } | ResponseItem::GhostSnapshot { .. } | ResponseItem::Other => false, } diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 40e0f31e41c..37d9809369e 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -374,6 +374,7 @@ impl ContextManager { | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } | ResponseItem::WebSearchCall { .. } + | ResponseItem::ImageGenerationCall { .. } | ResponseItem::CustomToolCall { .. } | ResponseItem::Compaction { .. } | ResponseItem::GhostSnapshot { .. } @@ -402,7 +403,8 @@ fn truncate_function_output_payload( } /// API messages include every non-system item (user/assistant messages, reasoning, -/// tool calls, tool outputs, shell calls, and web-search calls). +/// tool calls, tool outputs, shell calls, web-search calls, and image-generation +/// calls). fn is_api_message(message: &ResponseItem) -> bool { match message { ResponseItem::Message { role, .. } => role.as_str() != "system", @@ -413,6 +415,7 @@ fn is_api_message(message: &ResponseItem) -> bool { | ResponseItem::LocalShellCall { .. } | ResponseItem::Reasoning { .. } | ResponseItem::WebSearchCall { .. } + | ResponseItem::ImageGenerationCall { .. } | ResponseItem::Compaction { .. } => true, ResponseItem::GhostSnapshot { .. } => false, ResponseItem::Other => false, @@ -600,6 +603,7 @@ fn is_model_generated_item(item: &ResponseItem) -> bool { ResponseItem::Reasoning { .. } | ResponseItem::FunctionCall { .. } | ResponseItem::WebSearchCall { .. } + | ResponseItem::ImageGenerationCall { .. } | ResponseItem::CustomToolCall { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::Compaction { .. } => true, diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 46aa4623220..8f74cb99ba2 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -395,6 +395,48 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { } } +#[test] +fn for_prompt_keeps_image_generation_calls() { + let history = create_history_with_items(vec![ + ResponseItem::ImageGenerationCall { + id: Some("ig_123".to_string()), + status: Some("generating".to_string()), + revised_prompt: Some("lobster".to_string()), + result: Some("Zm9v".to_string()), + }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "hi".to_string(), + }], + end_turn: None, + phase: None, + }, + ]); + + assert_eq!( + history.for_prompt(&default_input_modalities()), + vec![ + ResponseItem::ImageGenerationCall { + id: Some("ig_123".to_string()), + status: Some("generating".to_string()), + revised_prompt: Some("lobster".to_string()), + result: Some("Zm9v".to_string()), + }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "hi".to_string(), + }], + end_turn: None, + phase: None, + } + ] + ); +} + #[test] fn get_history_for_prompt_drops_ghost_commits() { let items = vec![ResponseItem::GhostSnapshot { diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index ef6712fd00b..fd859922dda 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -131,6 +131,8 @@ pub enum Feature { Apps, /// Enable plugins. Plugins, + /// Allow the model to invoke the built-in image generation tool. + ImageGeneration, /// Route apps MCP calls through the configured gateway. AppsMcpGateway, /// Allow prompting and installing missing MCP dependencies. @@ -649,6 +651,16 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::ImageGeneration, + key: "image_generation", + stage: Stage::Experimental { + name: "Image generation", + menu_description: "Enable the built-in image generation tool for supported Responses API models.", + announcement: "NEW: Image generation is available as an experimental built-in tool. Enable it in /experimental to receive image-generation responses from supported models.", + }, + default_enabled: false, + }, FeatureSpec { id: Feature::AppsMcpGateway, key: "apps_mcp_gateway", diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 4c76bc54f62..9af54610e23 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -35,6 +35,7 @@ pub(crate) fn should_persist_response_item(item: &ResponseItem) -> bool { | ResponseItem::CustomToolCall { .. } | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::WebSearchCall { .. } + | ResponseItem::ImageGenerationCall { .. } | ResponseItem::GhostSnapshot { .. } | ResponseItem::Compaction { .. } => true, ResponseItem::Other => false, @@ -53,6 +54,7 @@ pub(crate) fn should_persist_response_item_for_memories(item: &ResponseItem) -> | ResponseItem::CustomToolCallOutput { .. } | ResponseItem::WebSearchCall { .. } => true, ResponseItem::Reasoning { .. } + | ResponseItem::ImageGenerationCall { .. } | ResponseItem::GhostSnapshot { .. } | ResponseItem::Compaction { .. } | ResponseItem::Other => false, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 0e536707500..ac1e937b37b 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -35,6 +35,17 @@ use std::collections::HashMap; const SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE: &str = include_str!("../../templates/search_tool/tool_description.md"); +const IMAGE_GENERATION_SUPPORTED_MODELS: [&str; 9] = [ + "gpt-4o", + "gpt-4o-mini", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "o3", + "gpt-5", + "gpt-5-nano", + "gpt-5.2", +]; #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ShellCommandBackendConfig { @@ -49,6 +60,8 @@ pub(crate) struct ToolsConfig { pub allow_login_shell: bool, pub apply_patch_tool_type: Option, pub web_search_mode: Option, + pub image_generation_enabled: bool, + pub image_generation_supported: bool, pub agent_roles: BTreeMap, pub search_tool: bool, pub request_permission_enabled: bool, @@ -135,6 +148,8 @@ impl ToolsConfig { allow_login_shell: true, apply_patch_tool_type, web_search_mode: *web_search_mode, + image_generation_enabled: false, + image_generation_supported: supports_image_generation(model_info), agent_roles: BTreeMap::new(), search_tool: include_search_tool, request_permission_enabled, @@ -158,6 +173,15 @@ impl ToolsConfig { self.allow_login_shell = allow_login_shell; self } + + pub fn with_image_generation_enabled(mut self, image_generation_enabled: bool) -> Self { + self.image_generation_enabled = image_generation_enabled; + self + } +} + +fn supports_image_generation(model_info: &ModelInfo) -> bool { + IMAGE_GENERATION_SUPPORTED_MODELS.contains(&model_info.slug.as_str()) } /// Generic JSON‑Schema subset needed for our tool definitions @@ -1917,6 +1941,10 @@ pub(crate) fn build_specs( Some(WebSearchMode::Disabled) | None => {} } + if config.image_generation_enabled && config.image_generation_supported { + builder.push_spec(ToolSpec::ImageGeneration {}); + } + builder.push_spec_with_parallel_support(create_view_image_tool(), true); builder.register_handler("view_image", view_image_handler); @@ -2047,6 +2075,7 @@ mod tests { match tool { ToolSpec::Function(ResponsesApiTool { name, .. }) => name, ToolSpec::LocalShell {} => "local_shell", + ToolSpec::ImageGeneration {} => "image_generation", ToolSpec::WebSearch { .. } => "web_search", ToolSpec::Freeform(FreeformTool { name, .. }) => name, } @@ -2125,7 +2154,10 @@ mod tests { ToolSpec::Function(ResponsesApiTool { parameters, .. }) => { strip_descriptions_schema(parameters); } - ToolSpec::Freeform(_) | ToolSpec::LocalShell {} | ToolSpec::WebSearch { .. } => {} + ToolSpec::Freeform(_) + | ToolSpec::LocalShell {} + | ToolSpec::ImageGeneration {} + | ToolSpec::WebSearch { .. } => {} } } @@ -2374,6 +2406,55 @@ mod tests { assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]); } + #[test] + fn image_generation_tools_require_feature_and_supported_model() { + let config = test_config(); + let supported_model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5.2", &config); + let unsupported_model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5.2-codex", &config); + let features = Features::with_defaults(); + + let default_tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &supported_model_info, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + }); + let (default_tools, _) = build_specs(&default_tools_config, None, None, &[]).build(); + assert!( + !default_tools + .iter() + .any(|tool| tool.spec.name() == "image_generation"), + "image_generation should be disabled by default" + ); + + let supported_tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &supported_model_info, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + }) + .with_image_generation_enabled(true); + let (supported_tools, _) = build_specs(&supported_tools_config, None, None, &[]).build(); + assert_contains_tool_names(&supported_tools, &["image_generation"]); + + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &unsupported_model_info, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + }) + .with_image_generation_enabled(true); + let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); + assert!( + !tools + .iter() + .any(|tool| tool.spec.name() == "image_generation"), + "image_generation should be disabled for unsupported models" + ); + } + #[test] fn js_repl_freeform_grammar_blocks_common_non_js_prefixes() { let ToolSpec::Freeform(FreeformTool { format, .. }) = create_js_repl_tool() else { diff --git a/codex-rs/otel/src/traces/otel_manager.rs b/codex-rs/otel/src/traces/otel_manager.rs index 1074b6edd1e..aa11fce3b01 100644 --- a/codex-rs/otel/src/traces/otel_manager.rs +++ b/codex-rs/otel/src/traces/otel_manager.rs @@ -771,6 +771,7 @@ impl OtelManager { ResponseItem::CustomToolCall { .. } => "custom_tool_call".into(), ResponseItem::CustomToolCallOutput { .. } => "custom_tool_call_output".into(), ResponseItem::WebSearchCall { .. } => "web_search_call".into(), + ResponseItem::ImageGenerationCall { .. } => "image_generation_call".into(), ResponseItem::GhostSnapshot { .. } => "ghost_snapshot".into(), ResponseItem::Compaction { .. } => "compaction".into(), ResponseItem::Other => "other".into(), diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index d12eef3b301..c7b5e72178f 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -313,6 +313,29 @@ pub enum ResponseItem { #[ts(optional)] action: Option, }, + // Emitted by the Responses API when the agent triggers image generation. + // Example payload: + // { + // "id":"ig_123", + // "type":"image_generation_call", + // "status":"completed", + // "revised_prompt":"A gray tabby cat hugging an otter...", + // "result":"..." + // } + ImageGenerationCall { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + status: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + revised_prompt: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + result: Option, + }, // Generated by the harness but considered exactly as a model response. GhostSnapshot { ghost_commit: GhostCommit, @@ -1286,6 +1309,28 @@ mod tests { ); } + #[test] + fn response_item_parses_image_generation_call() { + let item = serde_json::from_value::(serde_json::json!({ + "id": "ig_123", + "type": "image_generation_call", + "status": "completed", + "revised_prompt": "A small blue square", + "result": "Zm9v", + })) + .expect("image generation item should deserialize"); + + assert_eq!( + item, + ResponseItem::ImageGenerationCall { + id: Some("ig_123".to_string()), + status: Some("completed".to_string()), + revised_prompt: Some("A small blue square".to_string()), + result: Some("Zm9v".to_string()), + } + ); + } + #[test] fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() { let contents = vec![serde_json::json!({ From c09e9da0c322a6a3dd0cd23d6e1430057fcd0460 Mon Sep 17 00:00:00 2001 From: won Date: Mon, 2 Mar 2026 18:21:12 -0800 Subject: [PATCH 02/11] input modality, swapped to under-dev and added some integration tests --- codex-rs/core/src/client.rs | 14 +- codex-rs/core/src/context_manager/mod.rs | 2 +- codex-rs/core/src/features.rs | 12 +- codex-rs/core/src/tools/spec.rs | 23 +- codex-rs/core/tests/common/responses.rs | 18 ++ codex-rs/core/tests/suite/model_switching.rs | 295 ++++++++++++++++--- 6 files changed, 301 insertions(+), 63 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 7414cf7fe70..47695066fe6 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -91,6 +91,7 @@ use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; use crate::client_common::rewrite_image_generation_calls_for_stateless_input; use crate::config::Config; +use crate::context_manager::normalize; use crate::default_client::build_reqwest_client; use crate::error::CodexErr; use crate::error::Result; @@ -499,7 +500,8 @@ impl ModelClientSession { ) -> Result { let instructions = &prompt.base_instructions.text; let store = provider.is_azure_responses_endpoint(); - let input = Self::prepare_responses_input(store, prompt.get_formatted_input()); + let input = prompt.get_formatted_input(); + let input = Self::prepare_responses_input(store, input, model_info); let tools = create_tools_json_for_responses_api(&prompt.tools)?; let default_reasoning_effort = model_info.default_reasoning_level; let reasoning = if model_info.supports_reasoning_summaries { @@ -656,11 +658,17 @@ impl ModelClientSession { }) } - fn prepare_responses_input(store: bool, input: Vec) -> Vec { + fn prepare_responses_input( + store: bool, + input: Vec, + model_info: &ModelInfo, + ) -> Vec { if store { input } else { - rewrite_image_generation_calls_for_stateless_input(input) + let mut input = rewrite_image_generation_calls_for_stateless_input(input); + normalize::strip_images_when_unsupported(&model_info.input_modalities, &mut input); + input } } diff --git a/codex-rs/core/src/context_manager/mod.rs b/codex-rs/core/src/context_manager/mod.rs index 853f8af5ac0..4c9e23f02e6 100644 --- a/codex-rs/core/src/context_manager/mod.rs +++ b/codex-rs/core/src/context_manager/mod.rs @@ -1,5 +1,5 @@ mod history; -mod normalize; +pub(crate) mod normalize; pub(crate) mod updates; pub(crate) use history::ContextManager; diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index fd859922dda..dbc4e706854 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -654,11 +654,7 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::ImageGeneration, key: "image_generation", - stage: Stage::Experimental { - name: "Image generation", - menu_description: "Enable the built-in image generation tool for supported Responses API models.", - announcement: "NEW: Image generation is available as an experimental built-in tool. Enable it in /experimental to receive image-generation responses from supported models.", - }, + stage: Stage::UnderDevelopment, default_enabled: false, }, FeatureSpec { @@ -880,6 +876,12 @@ mod tests { assert_eq!(Feature::JsRepl.default_enabled(), false); } + #[test] + fn image_generation_is_under_development() { + assert_eq!(Feature::ImageGeneration.stage(), Stage::UnderDevelopment); + assert_eq!(Feature::ImageGeneration.default_enabled(), false); + } + #[test] fn collab_is_legacy_alias_for_multi_agent() { assert_eq!(feature_for_key("multi_agent"), Some(Feature::Collab)); diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index ac1e937b37b..19343d2fe5f 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -23,6 +23,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::VIEW_IMAGE_TOOL_NAME; use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; +use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -35,18 +36,6 @@ use std::collections::HashMap; const SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE: &str = include_str!("../../templates/search_tool/tool_description.md"); -const IMAGE_GENERATION_SUPPORTED_MODELS: [&str; 9] = [ - "gpt-4o", - "gpt-4o-mini", - "gpt-4.1", - "gpt-4.1-mini", - "gpt-4.1-nano", - "o3", - "gpt-5", - "gpt-5-nano", - "gpt-5.2", -]; - #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ShellCommandBackendConfig { Classic, @@ -181,7 +170,7 @@ impl ToolsConfig { } fn supports_image_generation(model_info: &ModelInfo) -> bool { - IMAGE_GENERATION_SUPPORTED_MODELS.contains(&model_info.slug.as_str()) + model_info.input_modalities.contains(&InputModality::Image) } /// Generic JSON‑Schema subset needed for our tool definitions @@ -2023,6 +2012,7 @@ mod tests { use crate::models_manager::manager::ModelsManager; use crate::models_manager::model_info::with_config_overrides; use crate::tools::registry::ConfiguredToolSpec; + use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; use pretty_assertions::assert_eq; @@ -2409,10 +2399,11 @@ mod tests { #[test] fn image_generation_tools_require_feature_and_supported_model() { let config = test_config(); - let supported_model_info = + let mut supported_model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5.2", &config); - let unsupported_model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5.2-codex", &config); + supported_model_info.slug = "custom/gpt-5.2-variant".to_string(); + let mut unsupported_model_info = supported_model_info.clone(); + unsupported_model_info.input_modalities = vec![InputModality::Text]; let features = Features::with_defaults(); let default_tools_config = ToolsConfig::new(&ToolsConfigParams { diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs index 4f2e89636ea..d07b155f612 100644 --- a/codex-rs/core/tests/common/responses.rs +++ b/codex-rs/core/tests/common/responses.rs @@ -739,6 +739,24 @@ pub fn ev_web_search_call_done(id: &str, status: &str, query: &str) -> Value { }) } +pub fn ev_image_generation_call( + id: &str, + status: &str, + revised_prompt: &str, + result: &str, +) -> Value { + serde_json::json!({ + "type": "response.output_item.done", + "item": { + "type": "image_generation_call", + "id": id, + "status": status, + "revised_prompt": revised_prompt, + "result": result, + } + }) +} + pub fn ev_function_call(call_id: &str, name: &str, arguments: &str) -> Value { serde_json::json!({ "type": "response.output_item.done", diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index df02d5e2584..54ecd39d8fc 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -20,6 +20,7 @@ use codex_protocol::protocol::Op; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_completed_with_tokens; +use core_test_support::responses::ev_image_generation_call; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_sse_sequence; @@ -32,6 +33,47 @@ use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use wiremock::MockServer; +fn test_model_info( + slug: &str, + display_name: &str, + description: &str, + input_modalities: Vec, +) -> ModelInfo { + ModelInfo { + slug: slug.to_string(), + display_name: display_name.to_string(), + description: Some(description.to_string()), + default_reasoning_level: Some(ReasoningEffort::Medium), + supported_reasoning_levels: vec![ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: ReasoningEffort::Medium.to_string(), + }], + shell_type: ConfigShellToolType::ShellCommand, + visibility: ModelVisibility::List, + supported_in_api: true, + input_modalities, + prefer_websockets: false, + used_fallback_model_metadata: false, + priority: 1, + upgrade: None, + base_instructions: "base instructions".to_string(), + model_messages: None, + supports_reasoning_summaries: false, + default_reasoning_summary: ReasoningSummary::Auto, + support_verbosity: false, + default_verbosity: None, + availability_nux: None, + apply_patch_tool_type: None, + truncation_policy: TruncationPolicyConfig::bytes(10_000), + supports_parallel_tool_calls: false, + supports_image_detail_original: false, + context_window: Some(272_000), + auto_compact_token_limit: None, + effective_context_window_percent: 95, + experimental_supported_tools: Vec::new(), + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn model_change_appends_model_instructions_developer_message() -> Result<()> { skip_if_no_network!(Ok(())); @@ -254,44 +296,18 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< let server = MockServer::start().await; let image_model_slug = "test-image-model"; let text_model_slug = "test-text-only-model"; - let image_model = ModelInfo { - slug: image_model_slug.to_string(), - display_name: "Test Image Model".to_string(), - description: Some("supports image input".to_string()), - default_reasoning_level: Some(ReasoningEffort::Medium), - supported_reasoning_levels: vec![ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: ReasoningEffort::Medium.to_string(), - }], - shell_type: ConfigShellToolType::ShellCommand, - visibility: ModelVisibility::List, - supported_in_api: true, - input_modalities: default_input_modalities(), - prefer_websockets: false, - used_fallback_model_metadata: false, - priority: 1, - upgrade: None, - base_instructions: "base instructions".to_string(), - model_messages: None, - supports_reasoning_summaries: false, - default_reasoning_summary: ReasoningSummary::Auto, - support_verbosity: false, - default_verbosity: None, - availability_nux: None, - apply_patch_tool_type: None, - truncation_policy: TruncationPolicyConfig::bytes(10_000), - supports_parallel_tool_calls: false, - supports_image_detail_original: false, - context_window: Some(272_000), - auto_compact_token_limit: None, - effective_context_window_percent: 95, - experimental_supported_tools: Vec::new(), - }; - let mut text_model = image_model.clone(); - text_model.slug = text_model_slug.to_string(); - text_model.display_name = "Test Text Model".to_string(); - text_model.description = Some("text only".to_string()); - text_model.input_modalities = vec![InputModality::Text]; + let image_model = test_model_info( + image_model_slug, + "Test Image Model", + "supports image input", + default_input_modalities(), + ); + let text_model = test_model_info( + text_model_slug, + "Test Text Model", + "text only", + vec![InputModality::Text], + ); mount_models_once( &server, ModelsResponse { @@ -401,6 +417,209 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = MockServer::start().await; + let image_model_slug = "test-image-model"; + let image_model = test_model_info( + image_model_slug, + "Test Image Model", + "supports image input", + default_input_modalities(), + ); + mount_models_once( + &server, + ModelsResponse { + models: vec![image_model], + }, + ) + .await; + + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_image_generation_call("ig_123", "completed", "lobster", "Zm9v"), + ev_completed_with_tokens("resp-1", 10), + ]), + sse_completed("resp-2"), + ], + ) + .await; + + let mut builder = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(move |config| { + config.model = Some(image_model_slug.to_string()); + }); + let test = builder.build(&server).await?; + let models_manager = test.thread_manager.get_models_manager(); + let _ = models_manager + .list_models(RefreshStrategy::OnlineIfUncached) + .await; + + test.codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "generate a lobster".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: test.cwd_path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + model: image_model_slug.to_string(), + effort: test.config.model_reasoning_effort, + summary: None, + collaboration_mode: None, + personality: None, + }) + .await?; + wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + test.codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "describe the generated image".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: test.cwd_path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + model: image_model_slug.to_string(), + effort: test.config.model_reasoning_effort, + summary: None, + collaboration_mode: None, + personality: None, + }) + .await?; + wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + let requests = responses.requests(); + assert_eq!(requests.len(), 2, "expected two model requests"); + + let second_request = requests.last().expect("expected second request"); + assert_eq!( + second_request.message_input_image_urls("user"), + vec!["data:image/png;base64,Zm9v".to_string()] + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn model_change_from_generated_image_to_text_strips_prior_generated_image_content() +-> Result<()> { + skip_if_no_network!(Ok(())); + + let server = MockServer::start().await; + let image_model_slug = "test-image-model"; + let text_model_slug = "test-text-only-model"; + let image_model = test_model_info( + image_model_slug, + "Test Image Model", + "supports image input", + default_input_modalities(), + ); + let text_model = test_model_info( + text_model_slug, + "Test Text Model", + "text only", + vec![InputModality::Text], + ); + mount_models_once( + &server, + ModelsResponse { + models: vec![image_model, text_model], + }, + ) + .await; + + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_image_generation_call("ig_123", "completed", "lobster", "Zm9v"), + ev_completed_with_tokens("resp-1", 10), + ]), + sse_completed("resp-2"), + ], + ) + .await; + + let mut builder = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(move |config| { + config.model = Some(image_model_slug.to_string()); + }); + let test = builder.build(&server).await?; + let models_manager = test.thread_manager.get_models_manager(); + let _ = models_manager + .list_models(RefreshStrategy::OnlineIfUncached) + .await; + + test.codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "generate a lobster".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: test.cwd_path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + model: image_model_slug.to_string(), + effort: test.config.model_reasoning_effort, + summary: None, + collaboration_mode: None, + personality: None, + }) + .await?; + wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + test.codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "describe the generated image".to_string(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: test.cwd_path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + model: text_model_slug.to_string(), + effort: test.config.model_reasoning_effort, + summary: None, + collaboration_mode: None, + personality: None, + }) + .await?; + wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + + let requests = responses.requests(); + assert_eq!(requests.len(), 2, "expected two model requests"); + + let second_request = requests.last().expect("expected second request"); + assert!( + second_request.message_input_image_urls("user").is_empty(), + "second request should strip generated image content for text-only models" + ); + assert!( + second_request + .message_input_texts("user") + .iter() + .any(|text| text == "image content omitted because you do not support image input"), + "second request should include the image-omitted placeholder text" + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<()> { skip_if_no_network!(Ok(())); From 3b988cbf2842cde42d21e945e7ef5ea0da70b647 Mon Sep 17 00:00:00 2001 From: won Date: Mon, 2 Mar 2026 19:33:53 -0800 Subject: [PATCH 03/11] schema changes --- .../schema/json/ClientRequest.json | 40 +++++++++++++++++++ .../schema/json/EventMsg.json | 40 +++++++++++++++++++ .../codex_app_server_protocol.schemas.json | 40 +++++++++++++++++++ .../RawResponseItemCompletedNotification.json | 40 +++++++++++++++++++ .../schema/json/v2/ThreadResumeParams.json | 40 +++++++++++++++++++ .../schema/typescript/ResponseItem.ts | 2 +- 6 files changed, 201 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 509d4426785..3ff4807e67b 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1416,6 +1416,46 @@ "title": "WebSearchCallResponseItem", "type": "object" }, + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "result": { + "type": [ + "string", + "null" + ] + }, + "revised_prompt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "image_generation_call" + ], + "title": "ImageGenerationCallResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ImageGenerationCallResponseItem", + "type": "object" + }, { "properties": { "ghost_commit": { diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index cafd8bc5d0b..69b7fc35476 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -4983,6 +4983,46 @@ "title": "WebSearchCallResponseItem", "type": "object" }, + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "result": { + "type": [ + "string", + "null" + ] + }, + "revised_prompt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "image_generation_call" + ], + "title": "ImageGenerationCallResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ImageGenerationCallResponseItem", + "type": "object" + }, { "properties": { "ghost_commit": { 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 94e9f3a50c8..b8c07507308 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 @@ -11761,6 +11761,46 @@ "title": "WebSearchCallResponseItem", "type": "object" }, + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "result": { + "type": [ + "string", + "null" + ] + }, + "revised_prompt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "image_generation_call" + ], + "title": "ImageGenerationCallResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ImageGenerationCallResponseItem", + "type": "object" + }, { "properties": { "ghost_commit": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index cedfdb19c48..9cb51e96338 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -641,6 +641,46 @@ "title": "WebSearchCallResponseItem", "type": "object" }, + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "result": { + "type": [ + "string", + "null" + ] + }, + "revised_prompt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "image_generation_call" + ], + "title": "ImageGenerationCallResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ImageGenerationCallResponseItem", + "type": "object" + }, { "properties": { "ghost_commit": { 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 91850f60e5a..12207bdd25b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -691,6 +691,46 @@ "title": "WebSearchCallResponseItem", "type": "object" }, + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "result": { + "type": [ + "string", + "null" + ] + }, + "revised_prompt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "image_generation_call" + ], + "title": "ImageGenerationCallResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ImageGenerationCallResponseItem", + "type": "object" + }, { "properties": { "ghost_commit": { diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts index dd7621f01d6..62bdaec41af 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts @@ -15,4 +15,4 @@ export type ResponseItem = { "type": "message", role: string, content: Array Date: Tue, 3 Mar 2026 15:12:09 -0800 Subject: [PATCH 04/11] logic changes --- codex-rs/core/src/client.rs | 20 +---- codex-rs/core/src/client_common.rs | 81 ------------------- codex-rs/core/src/codex.rs | 2 - codex-rs/core/src/context_manager/history.rs | 2 + .../core/src/context_manager/history_tests.rs | 61 ++++++++++++-- codex-rs/core/src/context_manager/mod.rs | 2 +- .../core/src/context_manager/normalize.rs | 32 +++++++- codex-rs/core/src/tools/spec.rs | 31 +++---- 8 files changed, 102 insertions(+), 129 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 47695066fe6..7e4b8297e09 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -89,9 +89,7 @@ use crate::auth::RefreshTokenError; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; -use crate::client_common::rewrite_image_generation_calls_for_stateless_input; use crate::config::Config; -use crate::context_manager::normalize; use crate::default_client::build_reqwest_client; use crate::error::CodexErr; use crate::error::Result; @@ -499,9 +497,7 @@ impl ModelClientSession { service_tier: Option, ) -> Result { let instructions = &prompt.base_instructions.text; - let store = provider.is_azure_responses_endpoint(); let input = prompt.get_formatted_input(); - let input = Self::prepare_responses_input(store, input, model_info); let tools = create_tools_json_for_responses_api(&prompt.tools)?; let default_reasoning_effort = model_info.default_reasoning_level; let reasoning = if model_info.supports_reasoning_summaries { @@ -545,7 +541,7 @@ impl ModelClientSession { tool_choice: "auto".to_string(), parallel_tool_calls: prompt.parallel_tool_calls, reasoning, - store, + store: provider.is_azure_responses_endpoint(), stream: true, include, service_tier: match service_tier { @@ -658,20 +654,6 @@ impl ModelClientSession { }) } - fn prepare_responses_input( - store: bool, - input: Vec, - model_info: &ModelInfo, - ) -> Vec { - if store { - input - } else { - let mut input = rewrite_image_generation_calls_for_stateless_input(input); - normalize::strip_images_when_unsupported(&model_info.input_modalities, &mut input); - input - } - } - /// Opportunistically preconnects a websocket for this turn-scoped client session. /// /// This performs only connection setup; it never sends prompt payloads. diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 1564cd58494..f91143e1a6f 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -3,7 +3,6 @@ use crate::config::types::Personality; use crate::error::Result; pub use codex_api::common::ResponseEvent; use codex_protocol::models::BaseInstructions; -use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::ResponseItem; use futures::Stream; @@ -65,34 +64,6 @@ impl Prompt { } } -pub(crate) fn rewrite_image_generation_calls_for_stateless_input( - items: Vec, -) -> Vec { - items - .into_iter() - .filter_map(|item| match item { - ResponseItem::ImageGenerationCall { - result: Some(result), - .. - } => Some(ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputImage { - image_url: if result.starts_with("data:") { - result - } else { - format!("data:image/png;base64,{result}") - }, - }], - end_turn: None, - phase: None, - }), - ResponseItem::ImageGenerationCall { .. } => None, - _ => Some(item), - }) - .collect() -} - fn reserialize_shell_outputs(items: &mut [ResponseItem]) { let mut shell_call_ids: HashSet = HashSet::new(); @@ -266,63 +237,11 @@ mod tests { use codex_api::common::OpenAiVerbosity; use codex_api::common::TextControls; use codex_api::create_text_param_for_request; - use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputPayload; use pretty_assertions::assert_eq; use super::*; - #[test] - fn rewrites_image_generation_calls_for_stateless_input() { - let input = vec![ - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "generate a lobster".to_string(), - }], - end_turn: None, - phase: None, - }, - ResponseItem::ImageGenerationCall { - id: Some("ig_123".to_string()), - status: Some("completed".to_string()), - revised_prompt: Some("lobster".to_string()), - result: Some("Zm9v".to_string()), - }, - ResponseItem::ImageGenerationCall { - id: Some("ig_456".to_string()), - status: Some("completed".to_string()), - revised_prompt: None, - result: None, - }, - ]; - - assert_eq!( - rewrite_image_generation_calls_for_stateless_input(input), - vec![ - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "generate a lobster".to_string(), - }], - end_turn: None, - phase: None, - }, - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputImage { - image_url: "data:image/png;base64,Zm9v".to_string(), - }], - end_turn: None, - phase: None, - }, - ] - ); - } - #[test] fn serializes_text_verbosity_when_set() { let input: Vec = vec![]; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 488ccfa485f..9b9e0214a85 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -737,7 +737,6 @@ impl TurnContext { session_source: self.session_source.clone(), }) .with_allow_login_shell(self.tools_config.allow_login_shell) - .with_image_generation_enabled(features.enabled(Feature::ImageGeneration)) .with_agent_roles(config.agent_roles.clone()); Self { @@ -1111,7 +1110,6 @@ impl Session { session_source: session_source.clone(), }) .with_allow_login_shell(per_turn_config.permissions.allow_login_shell) - .with_image_generation_enabled(per_turn_config.features.enabled(Feature::ImageGeneration)) .with_agent_roles(per_turn_config.agent_roles.clone()); let cwd = session_configuration.cwd.clone(); diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 37d9809369e..53ab412ded9 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -344,6 +344,8 @@ impl ContextManager { // all outputs must have a corresponding function/tool call normalize::remove_orphan_outputs(&mut self.items); + normalize::rewrite_image_generation_calls_for_stateless_input(&mut self.items); + // strip images when model does not support them normalize::strip_images_when_unsupported(input_modalities, &mut self.items); } diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 8f74cb99ba2..21f89e2d9d4 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -396,7 +396,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { } #[test] -fn for_prompt_keeps_image_generation_calls() { +fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() { let history = create_history_with_items(vec![ ResponseItem::ImageGenerationCall { id: Some("ig_123".to_string()), @@ -418,11 +418,14 @@ fn for_prompt_keeps_image_generation_calls() { assert_eq!( history.for_prompt(&default_input_modalities()), vec![ - ResponseItem::ImageGenerationCall { - id: Some("ig_123".to_string()), - status: Some("generating".to_string()), - revised_prompt: Some("lobster".to_string()), - result: Some("Zm9v".to_string()), + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputImage { + image_url: "data:image/png;base64,Zm9v".to_string(), + }], + end_turn: None, + phase: None, }, ResponseItem::Message { id: None, @@ -437,6 +440,52 @@ fn for_prompt_keeps_image_generation_calls() { ); } +#[test] +fn for_prompt_rewrites_image_generation_calls_when_images_are_unsupported() { + let history = create_history_with_items(vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "generate a lobster".to_string(), + }], + end_turn: None, + phase: None, + }, + ResponseItem::ImageGenerationCall { + id: Some("ig_123".to_string()), + status: Some("completed".to_string()), + revised_prompt: Some("lobster".to_string()), + result: Some("Zm9v".to_string()), + }, + ]); + + assert_eq!( + history.for_prompt(&[InputModality::Text]), + vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "generate a lobster".to_string(), + }], + end_turn: None, + phase: None, + }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "image content omitted because you do not support image input" + .to_string(), + }], + end_turn: None, + phase: None, + }, + ] + ); +} + #[test] fn get_history_for_prompt_drops_ghost_commits() { let items = vec![ResponseItem::GhostSnapshot { diff --git a/codex-rs/core/src/context_manager/mod.rs b/codex-rs/core/src/context_manager/mod.rs index 4c9e23f02e6..853f8af5ac0 100644 --- a/codex-rs/core/src/context_manager/mod.rs +++ b/codex-rs/core/src/context_manager/mod.rs @@ -1,5 +1,5 @@ mod history; -pub(crate) mod normalize; +mod normalize; pub(crate) mod updates; pub(crate) use history::ContextManager; diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs index 572ac51fc81..47bf9996d09 100644 --- a/codex-rs/core/src/context_manager/normalize.rs +++ b/codex-rs/core/src/context_manager/normalize.rs @@ -1,10 +1,9 @@ -use std::collections::HashSet; - use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::InputModality; +use std::collections::HashSet; use crate::util::error_or_panic; use tracing::info; @@ -211,6 +210,35 @@ where } } +pub(crate) fn rewrite_image_generation_calls_for_stateless_input(items: &mut Vec) { + let original_items = std::mem::take(items); + *items = original_items + .into_iter() + .filter_map(|item| match item { + ResponseItem::ImageGenerationCall { + result: Some(result), + .. + } => { + let image_url = if result.starts_with("data:") { + result + } else { + format!("data:image/png;base64,{result}") + }; + + Some(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputImage { image_url }], + end_turn: None, + phase: None, + }) + } + ResponseItem::ImageGenerationCall { .. } => None, + _ => Some(item), + }) + .collect(); +} + /// Strip image content from messages and tool outputs when the model does not support images. /// When `input_modalities` contains `InputModality::Image`, no stripping is performed. pub(crate) fn strip_images_when_unsupported( diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 19343d2fe5f..3ff8bc50e1a 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -49,8 +49,7 @@ pub(crate) struct ToolsConfig { pub allow_login_shell: bool, pub apply_patch_tool_type: Option, pub web_search_mode: Option, - pub image_generation_enabled: bool, - pub image_generation_supported: bool, + pub image_gen_tool: bool, pub agent_roles: BTreeMap, pub search_tool: bool, pub request_permission_enabled: bool, @@ -88,6 +87,8 @@ impl ToolsConfig { features.enabled(Feature::DefaultModeRequestUserInput); let include_search_tool = features.enabled(Feature::Apps); let include_artifact_tools = features.enabled(Feature::Artifact); + let include_image_gen_tool = + features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info); let include_agent_jobs = include_collab_tools && features.enabled(Feature::Sqlite); let request_permission_enabled = features.enabled(Feature::RequestPermissions); let shell_command_backend = @@ -137,8 +138,7 @@ impl ToolsConfig { allow_login_shell: true, apply_patch_tool_type, web_search_mode: *web_search_mode, - image_generation_enabled: false, - image_generation_supported: supports_image_generation(model_info), + image_gen_tool: include_image_gen_tool, agent_roles: BTreeMap::new(), search_tool: include_search_tool, request_permission_enabled, @@ -162,11 +162,6 @@ impl ToolsConfig { self.allow_login_shell = allow_login_shell; self } - - pub fn with_image_generation_enabled(mut self, image_generation_enabled: bool) -> Self { - self.image_generation_enabled = image_generation_enabled; - self - } } fn supports_image_generation(model_info: &ModelInfo) -> bool { @@ -1930,7 +1925,7 @@ pub(crate) fn build_specs( Some(WebSearchMode::Disabled) | None => {} } - if config.image_generation_enabled && config.image_generation_supported { + if config.image_gen_tool { builder.push_spec(ToolSpec::ImageGeneration {}); } @@ -2404,11 +2399,13 @@ mod tests { supported_model_info.slug = "custom/gpt-5.2-variant".to_string(); let mut unsupported_model_info = supported_model_info.clone(); unsupported_model_info.input_modalities = vec![InputModality::Text]; - let features = Features::with_defaults(); + let default_features = Features::with_defaults(); + let mut image_generation_features = default_features.clone(); + image_generation_features.enable(Feature::ImageGeneration); let default_tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &supported_model_info, - features: &features, + features: &default_features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, }); @@ -2422,21 +2419,19 @@ mod tests { let supported_tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &supported_model_info, - features: &features, + features: &image_generation_features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - }) - .with_image_generation_enabled(true); + }); let (supported_tools, _) = build_specs(&supported_tools_config, None, None, &[]).build(); assert_contains_tool_names(&supported_tools, &["image_generation"]); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &unsupported_model_info, - features: &features, + features: &image_generation_features, web_search_mode: Some(WebSearchMode::Cached), session_source: SessionSource::Cli, - }) - .with_image_generation_enabled(true); + }); let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); assert!( !tools From c993aaba2f5940d824c5cae9cb829165909f543c Mon Sep 17 00:00:00 2001 From: won Date: Tue, 3 Mar 2026 15:28:40 -0800 Subject: [PATCH 05/11] redacted some serdes --- .../core/src/context_manager/history_tests.rs | 12 +++--- .../core/src/context_manager/normalize.rs | 14 +++---- codex-rs/protocol/src/models.rs | 39 +++++++++++++------ 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 21f89e2d9d4..b678c2e6cb7 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -399,10 +399,10 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() { let history = create_history_with_items(vec![ ResponseItem::ImageGenerationCall { - id: Some("ig_123".to_string()), - status: Some("generating".to_string()), + id: "ig_123".to_string(), + status: "generating".to_string(), revised_prompt: Some("lobster".to_string()), - result: Some("Zm9v".to_string()), + result: "Zm9v".to_string(), }, ResponseItem::Message { id: None, @@ -453,10 +453,10 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_unsupported() { phase: None, }, ResponseItem::ImageGenerationCall { - id: Some("ig_123".to_string()), - status: Some("completed".to_string()), + id: "ig_123".to_string(), + status: "completed".to_string(), revised_prompt: Some("lobster".to_string()), - result: Some("Zm9v".to_string()), + result: "Zm9v".to_string(), }, ]); diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs index 47bf9996d09..465edae41b5 100644 --- a/codex-rs/core/src/context_manager/normalize.rs +++ b/codex-rs/core/src/context_manager/normalize.rs @@ -214,27 +214,23 @@ pub(crate) fn rewrite_image_generation_calls_for_stateless_input(items: &mut Vec let original_items = std::mem::take(items); *items = original_items .into_iter() - .filter_map(|item| match item { - ResponseItem::ImageGenerationCall { - result: Some(result), - .. - } => { + .map(|item| match item { + ResponseItem::ImageGenerationCall { result, .. } => { let image_url = if result.starts_with("data:") { result } else { format!("data:image/png;base64,{result}") }; - Some(ResponseItem::Message { + ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputImage { image_url }], end_turn: None, phase: None, - }) + } } - ResponseItem::ImageGenerationCall { .. } => None, - _ => Some(item), + _ => item, }) .collect(); } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index c7b5e72178f..90d9945b06e 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -323,18 +323,12 @@ pub enum ResponseItem { // "result":"..." // } ImageGenerationCall { - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - status: Option, + id: String, + status: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] revised_prompt: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - result: Option, + result: String, }, // Generated by the harness but considered exactly as a model response. GhostSnapshot { @@ -1323,10 +1317,31 @@ mod tests { assert_eq!( item, ResponseItem::ImageGenerationCall { - id: Some("ig_123".to_string()), - status: Some("completed".to_string()), + id: "ig_123".to_string(), + status: "completed".to_string(), revised_prompt: Some("A small blue square".to_string()), - result: Some("Zm9v".to_string()), + result: "Zm9v".to_string(), + } + ); + } + + #[test] + fn response_item_parses_image_generation_call_without_revised_prompt() { + let item = serde_json::from_value::(serde_json::json!({ + "id": "ig_123", + "type": "image_generation_call", + "status": "completed", + "result": "Zm9v", + })) + .expect("image generation item should deserialize"); + + assert_eq!( + item, + ResponseItem::ImageGenerationCall { + id: "ig_123".to_string(), + status: "completed".to_string(), + revised_prompt: None, + result: "Zm9v".to_string(), } ); } From 4c6c93e9e447cc368dfb5f36e61860d1409c371e Mon Sep 17 00:00:00 2001 From: won Date: Tue, 3 Mar 2026 15:32:01 -0800 Subject: [PATCH 06/11] added comment --- codex-rs/core/src/context_manager/history.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 53ab412ded9..7342c38105f 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -344,6 +344,7 @@ impl ContextManager { // all outputs must have a corresponding function/tool call normalize::remove_orphan_outputs(&mut self.items); + //rewrite image gen calls to messages so that normalize::rewrite_image_generation_calls_for_stateless_input(&mut self.items); // strip images when model does not support them From 288c8cd06daae38b8e6f7b8b33d7139879fcabe1 Mon Sep 17 00:00:00 2001 From: won Date: Tue, 3 Mar 2026 15:33:21 -0800 Subject: [PATCH 07/11] comment --- codex-rs/core/src/context_manager/history.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 7342c38105f..1bafca40828 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -344,7 +344,7 @@ impl ContextManager { // all outputs must have a corresponding function/tool call normalize::remove_orphan_outputs(&mut self.items); - //rewrite image gen calls to messages so that + //rewrite image_gen_calls to messages to support stateless input normalize::rewrite_image_generation_calls_for_stateless_input(&mut self.items); // strip images when model does not support them From 0a29672efb7c9d02bd803bd4735ba30cf9dc7437 Mon Sep 17 00:00:00 2001 From: won Date: Tue, 3 Mar 2026 16:18:14 -0800 Subject: [PATCH 08/11] cargo fix --- codex-rs/core/tests/suite/model_switching.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 54ecd39d8fc..13d91b1ebb0 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -473,6 +473,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, + service_tier: None, summary: None, collaboration_mode: None, personality: None, @@ -492,6 +493,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { sandbox_policy: SandboxPolicy::new_read_only_policy(), model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, + service_tier: None, summary: None, collaboration_mode: None, personality: None, @@ -575,6 +577,7 @@ async fn model_change_from_generated_image_to_text_strips_prior_generated_image_ sandbox_policy: SandboxPolicy::new_read_only_policy(), model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, + service_tier: None, summary: None, collaboration_mode: None, personality: None, @@ -594,6 +597,7 @@ async fn model_change_from_generated_image_to_text_strips_prior_generated_image_ sandbox_policy: SandboxPolicy::new_read_only_policy(), model: text_model_slug.to_string(), effort: test.config.model_reasoning_effort, + service_tier: None, summary: None, collaboration_mode: None, personality: None, From 5c9a99caa2a548f5e40833a0f42db5b38d1d7cbd Mon Sep 17 00:00:00 2001 From: won Date: Tue, 3 Mar 2026 16:58:33 -0800 Subject: [PATCH 09/11] cargo fix --- codex-rs/protocol/src/openai_models.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 089b0026809..5968ae699b7 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -706,6 +706,7 @@ mod tests { "limit": 10000 }, "supports_parallel_tool_calls": false, + "supports_image_detail_original": false, "context_window": null, "auto_compact_token_limit": null, "effective_context_window_percent": 95, From 58b71c14f5556ac782942af8bf32c07af266bc66 Mon Sep 17 00:00:00 2001 From: won Date: Tue, 3 Mar 2026 17:33:35 -0800 Subject: [PATCH 10/11] schema.... --- .../schema/json/ClientRequest.json | 18 ++++------ .../schema/json/EventMsg.json | 18 ++++------ .../codex_app_server_protocol.schemas.json | 18 ++++------ .../codex_app_server_protocol.v2.schemas.json | 34 +++++++++++++++++++ .../RawResponseItemCompletedNotification.json | 18 ++++------ .../schema/json/v2/ThreadResumeParams.json | 18 ++++------ .../schema/typescript/ResponseItem.ts | 2 +- 7 files changed, 65 insertions(+), 61 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 3ff4807e67b..b1d2921d2e8 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1419,16 +1419,10 @@ { "properties": { "id": { - "type": [ - "string", - "null" - ] + "type": "string" }, "result": { - "type": [ - "string", - "null" - ] + "type": "string" }, "revised_prompt": { "type": [ @@ -1437,10 +1431,7 @@ ] }, "status": { - "type": [ - "string", - "null" - ] + "type": "string" }, "type": { "enum": [ @@ -1451,6 +1442,9 @@ } }, "required": [ + "id", + "result", + "status", "type" ], "title": "ImageGenerationCallResponseItem", diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 69b7fc35476..ed7b258f729 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -4986,16 +4986,10 @@ { "properties": { "id": { - "type": [ - "string", - "null" - ] + "type": "string" }, "result": { - "type": [ - "string", - "null" - ] + "type": "string" }, "revised_prompt": { "type": [ @@ -5004,10 +4998,7 @@ ] }, "status": { - "type": [ - "string", - "null" - ] + "type": "string" }, "type": { "enum": [ @@ -5018,6 +5009,9 @@ } }, "required": [ + "id", + "result", + "status", "type" ], "title": "ImageGenerationCallResponseItem", 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 b8c07507308..8f152602ede 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 @@ -11764,16 +11764,10 @@ { "properties": { "id": { - "type": [ - "string", - "null" - ] + "type": "string" }, "result": { - "type": [ - "string", - "null" - ] + "type": "string" }, "revised_prompt": { "type": [ @@ -11782,10 +11776,7 @@ ] }, "status": { - "type": [ - "string", - "null" - ] + "type": "string" }, "type": { "enum": [ @@ -11796,6 +11787,9 @@ } }, "required": [ + "id", + "result", + "status", "type" ], "title": "ImageGenerationCallResponseItem", 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 0d1de74481d..550322e6c7b 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 @@ -9324,6 +9324,40 @@ "title": "WebSearchCallResponseItem", "type": "object" }, + { + "properties": { + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revised_prompt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "string" + }, + "type": { + "enum": [ + "image_generation_call" + ], + "title": "ImageGenerationCallResponseItemType", + "type": "string" + } + }, + "required": [ + "id", + "result", + "status", + "type" + ], + "title": "ImageGenerationCallResponseItem", + "type": "object" + }, { "properties": { "ghost_commit": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index 9cb51e96338..cb145426078 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -644,16 +644,10 @@ { "properties": { "id": { - "type": [ - "string", - "null" - ] + "type": "string" }, "result": { - "type": [ - "string", - "null" - ] + "type": "string" }, "revised_prompt": { "type": [ @@ -662,10 +656,7 @@ ] }, "status": { - "type": [ - "string", - "null" - ] + "type": "string" }, "type": { "enum": [ @@ -676,6 +667,9 @@ } }, "required": [ + "id", + "result", + "status", "type" ], "title": "ImageGenerationCallResponseItem", 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 12207bdd25b..97eae32a83e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -694,16 +694,10 @@ { "properties": { "id": { - "type": [ - "string", - "null" - ] + "type": "string" }, "result": { - "type": [ - "string", - "null" - ] + "type": "string" }, "revised_prompt": { "type": [ @@ -712,10 +706,7 @@ ] }, "status": { - "type": [ - "string", - "null" - ] + "type": "string" }, "type": { "enum": [ @@ -726,6 +717,9 @@ } }, "required": [ + "id", + "result", + "status", "type" ], "title": "ImageGenerationCallResponseItem", diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts index 62bdaec41af..dc42485935b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts @@ -15,4 +15,4 @@ export type ResponseItem = { "type": "message", role: string, content: Array Date: Tue, 3 Mar 2026 22:56:09 -0800 Subject: [PATCH 11/11] schema --- codex-rs/core/config.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index a751e5acd24..40722b33a60 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2247,4 +2247,4 @@ }, "title": "ConfigToml", "type": "object" -} +} \ No newline at end of file