From b1b3eec56e3945bdc9cc4f378a89aebae86bdce9 Mon Sep 17 00:00:00 2001 From: Jesse Du Date: Thu, 25 Jun 2026 20:42:38 -0400 Subject: [PATCH] make generated image directory configurable --- codex-rs/app-server/src/extensions.rs | 2 +- .../tests/suite/v2/imagegen_extension.rs | 20 ++++++- codex-rs/config/src/config_toml.rs | 4 ++ codex-rs/core/config.schema.json | 8 +++ codex-rs/core/src/config/config_tests.rs | 37 ++++++++++++ codex-rs/core/src/config/mod.rs | 9 +++ codex-rs/core/src/session/tests.rs | 6 +- codex-rs/core/src/stream_events_utils.rs | 28 ++++----- .../core/src/stream_events_utils_tests.rs | 59 ++++++++++--------- .../src/tools/handlers/extension_tools.rs | 2 +- .../core/tests/suite/extension_sandbox.rs | 11 +++- codex-rs/core/tests/suite/responses_lite.rs | 2 +- .../ext/image-generation/src/extension.rs | 23 ++++---- codex-rs/ext/image-generation/src/tool.rs | 27 +++++---- .../src/assets/samples/imagegen/SKILL.md | 8 +-- codex-rs/thread-manager-sample/src/main.rs | 1 + 16 files changed, 171 insertions(+), 76 deletions(-) diff --git a/codex-rs/app-server/src/extensions.rs b/codex-rs/app-server/src/extensions.rs index ed040aa8d9..70f597c2cc 100644 --- a/codex-rs/app-server/src/extensions.rs +++ b/codex-rs/app-server/src/extensions.rs @@ -77,7 +77,7 @@ where codex_mcp_extension::install_executor_plugins(&mut builder, environment_manager); codex_web_search_extension::install(&mut builder, auth_manager.clone()); codex_image_generation_extension::install(&mut builder, auth_manager, |config: &Config| { - Some(config.codex_home.clone()) + Some(config.generated_images_dir.clone()) }); let skill_providers = codex_skills_extension::SkillProviders::new() .with_executor_provider(executor_skill_provider) diff --git a/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs index 8c1643e147..388fedc273 100644 --- a/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs +++ b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs @@ -80,7 +80,14 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul .await; let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri(), ImagegenTestMode::Direct)?; + let generated_images_dir = codex_home.path().join("custom-generated-images"); + let generated_images_dir_config = serde_json::to_string(&generated_images_dir)?; + create_config_toml_with_extra_config( + codex_home.path(), + &server.uri(), + ImagegenTestMode::Direct, + &format!("generated_images_dir = {generated_images_dir_config}"), + )?; write_chatgpt_auth( codex_home.path(), ChatGptAuthFixture::new("access-chatgpt"), @@ -116,6 +123,7 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul assert_eq!(status, "completed"); assert_eq!(revised_prompt.as_deref(), Some("paint a blue whale")); assert_eq!(result, RESULT); + assert!(saved_path.starts_with(&generated_images_dir)); assert_eq!(std::fs::read(&saved_path)?, TINY_PNG_BYTES); let requests = response_mock.requests(); @@ -551,6 +559,15 @@ fn create_config_toml( codex_home: &Path, server_uri: &str, mode: ImagegenTestMode, +) -> std::io::Result<()> { + create_config_toml_with_extra_config(codex_home, server_uri, mode, "") +} + +fn create_config_toml_with_extra_config( + codex_home: &Path, + server_uri: &str, + mode: ImagegenTestMode, + extra_config: &str, ) -> std::io::Result<()> { let code_mode_only = match mode { ImagegenTestMode::Direct => "", @@ -565,6 +582,7 @@ approval_policy = "never" sandbox_mode = "read-only" model_provider = "openai-custom" chatgpt_base_url = "{server_uri}" +{extra_config} [features] imagegenext = true diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 823d59c0bc..f7c9ceb138 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -330,6 +330,10 @@ pub struct ConfigToml { /// Defaults to `$CODEX_HOME/log`. pub log_dir: Option, + /// Directory where Codex writes generated images. + /// Defaults to `$CODEX_HOME/generated_images`. + pub generated_images_dir: Option, + /// Debugging and reproducibility settings. pub debug: Option, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 512c050a20..6cce62f5f8 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -5115,6 +5115,14 @@ "default": null, "description": "When set, restricts the login mechanism users may use." }, + "generated_images_dir": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Directory where Codex writes generated images. Defaults to `$CODEX_HOME/generated_images`." + }, "ghost_snapshot": { "allOf": [ { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 6f7114928e..f2d8b50fe4 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5151,6 +5151,43 @@ async fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Re Ok(()) } +#[tokio::test] +async fn generated_images_dir_defaults_to_codex_home() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + + assert_eq!( + config.generated_images_dir, + codex_home.path().join("generated_images").abs() + ); + + Ok(()) +} + +#[tokio::test] +async fn generated_images_dir_uses_configured_path() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let generated_images_dir = codex_home.path().join("custom-images").abs(); + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + generated_images_dir: Some(generated_images_dir.clone()), + ..Default::default() + }, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + + assert_eq!(config.generated_images_dir, generated_images_dir); + + Ok(()) +} + #[tokio::test] async fn workspace_write_includes_configured_writable_root_once_without_memories_root() -> std::io::Result<()> { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index d90d010102..64ce88ecee 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -879,6 +879,10 @@ pub struct Config { /// Directory where Codex writes log files (defaults to `$CODEX_HOME/log`). pub log_dir: PathBuf, + /// Directory where Codex writes generated images (defaults to + /// `$CODEX_HOME/generated_images`). + pub generated_images_dir: AbsolutePathBuf, + /// Directory where Codex writes effective session config lock files. pub config_lock_export_dir: Option, @@ -3635,6 +3639,10 @@ impl Config { .as_ref() .map(AbsolutePathBuf::to_path_buf) .unwrap_or_else(|| codex_home.join("log").to_path_buf()); + let generated_images_dir = cfg + .generated_images_dir + .clone() + .unwrap_or_else(|| codex_home.join("generated_images")); let sqlite_home = cfg .sqlite_home .as_ref() @@ -3834,6 +3842,7 @@ impl Config { codex_home, sqlite_home, log_dir, + generated_images_dir, config_lock_export_dir: cfg .debug .as_ref() diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index d0aac04249..5b4fb13b7f 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -8591,7 +8591,7 @@ async fn handle_output_item_done_records_image_save_history_message() { let turn_context = Arc::new(turn_context); let call_id = "ig_history_records_message"; let expected_saved_path = crate::stream_events_utils::image_generation_artifact_path( - &turn_context.config.codex_home, + &turn_context.config.generated_images_dir, &session.thread_id.to_string(), call_id, ); @@ -8619,7 +8619,7 @@ async fn handle_output_item_done_records_image_save_history_message() { let history = session.clone_history().await; let image_output_path = crate::stream_events_utils::image_generation_artifact_path( - &turn_context.config.codex_home, + &turn_context.config.generated_images_dir, &session.thread_id.to_string(), "", ); @@ -8648,7 +8648,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() { let turn_context = Arc::new(turn_context); let call_id = "ig_history_no_message"; let expected_saved_path = crate::stream_events_utils::image_generation_artifact_path( - &turn_context.config.codex_home, + &turn_context.config.generated_images_dir, &session.thread_id.to_string(), call_id, ); diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index b07c180b16..5f8dd65f6d 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -36,11 +36,9 @@ use tracing::debug; use tracing::instrument; use tracing::warn; -const GENERATED_IMAGE_ARTIFACTS_DIR: &str = "generated_images"; - -/// Returns the host-owned default artifact path for a generated image. +/// Returns the host-owned artifact path for a generated image. pub fn image_generation_artifact_path( - codex_home: &AbsolutePathBuf, + generated_images_dir: &AbsolutePathBuf, session_id: &str, call_id: &str, ) -> AbsolutePathBuf { @@ -61,8 +59,7 @@ pub fn image_generation_artifact_path( sanitized }; - codex_home - .join(GENERATED_IMAGE_ARTIFACTS_DIR) + generated_images_dir .join(sanitize(session_id)) .join(format!("{}.png", sanitize(call_id))) } @@ -109,7 +106,7 @@ pub(crate) fn raw_assistant_output_text_from_item(item: &ResponseItem) -> Option } async fn save_image_generation_result( - codex_home: &AbsolutePathBuf, + generated_images_dir: &AbsolutePathBuf, session_id: &str, call_id: &str, result: &str, @@ -119,7 +116,7 @@ async fn save_image_generation_result( .map_err(|err| { CodexErr::InvalidRequest(format!("invalid image generation payload: {err}")) })?; - let path = image_generation_artifact_path(codex_home, session_id, call_id); + let path = image_generation_artifact_path(generated_images_dir, session_id, call_id); if let Some(parent) = path.parent() { tokio::fs::create_dir_all(parent).await?; } @@ -135,7 +132,7 @@ pub(crate) async fn persist_image_generation_item( image_item.saved_path = None; let session_id = sess.thread_id.to_string(); match save_image_generation_result( - &turn_context.config.codex_home, + &turn_context.config.generated_images_dir, &session_id, &image_item.id, &image_item.result, @@ -148,13 +145,13 @@ pub(crate) async fn persist_image_generation_item( } Err(err) => { let output_path = image_generation_artifact_path( - &turn_context.config.codex_home, + &turn_context.config.generated_images_dir, &session_id, &image_item.id, ); let output_dir = output_path .parent() - .unwrap_or_else(|| turn_context.config.codex_home.clone()); + .unwrap_or_else(|| turn_context.config.generated_images_dir.clone()); tracing::warn!( call_id = %image_item.id, output_dir = %output_dir.display(), @@ -174,11 +171,14 @@ async fn record_image_generation_instructions( return; } let session_id = sess.thread_id.to_string(); - let image_output_path = - image_generation_artifact_path(&turn_context.config.codex_home, &session_id, ""); + let image_output_path = image_generation_artifact_path( + &turn_context.config.generated_images_dir, + &session_id, + "", + ); let image_output_dir = image_output_path .parent() - .unwrap_or_else(|| turn_context.config.codex_home.clone()); + .unwrap_or_else(|| turn_context.config.generated_images_dir.clone()); let message: ResponseItem = ContextualUserFragment::into(ImageGenerationInstructions::new( image_output_dir.display(), image_output_path.display(), diff --git a/codex-rs/core/src/stream_events_utils_tests.rs b/codex-rs/core/src/stream_events_utils_tests.rs index 78a219add6..b167a8a980 100644 --- a/codex-rs/core/src/stream_events_utils_tests.rs +++ b/codex-rs/core/src/stream_events_utils_tests.rs @@ -431,14 +431,15 @@ fn completed_item_defers_mailbox_delivery_for_image_generation_calls() { } #[tokio::test] -async fn save_image_generation_result_saves_base64_to_png_in_codex_home() { - let codex_home = tempfile::tempdir().expect("create codex home"); - let codex_home = codex_home.path().abs(); - let expected_path = image_generation_artifact_path(&codex_home, "session-1", "ig_save_base64"); +async fn save_image_generation_result_saves_base64_to_png_in_generated_images_dir() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let generated_images_dir = temp_dir.path().join("images").abs(); + let expected_path = + image_generation_artifact_path(&generated_images_dir, "session-1", "ig_save_base64"); let _ = std::fs::remove_file(&expected_path); let saved_path = - save_image_generation_result(&codex_home, "session-1", "ig_save_base64", "Zm9v") + save_image_generation_result(&generated_images_dir, "session-1", "ig_save_base64", "Zm9v") .await .expect("image should be saved"); @@ -450,10 +451,10 @@ async fn save_image_generation_result_saves_base64_to_png_in_codex_home() { #[tokio::test] async fn save_image_generation_result_rejects_data_url_payload() { let result = "data:image/jpeg;base64,Zm9v"; - let codex_home = tempfile::tempdir().expect("create codex home"); - let codex_home = codex_home.path().abs(); + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let generated_images_dir = temp_dir.path().join("images").abs(); - let err = save_image_generation_result(&codex_home, "session-1", "ig_456", result) + let err = save_image_generation_result(&generated_images_dir, "session-1", "ig_456", result) .await .expect_err("data url payload should error"); assert!(matches!(err, CodexErr::InvalidRequest(_))); @@ -461,9 +462,10 @@ async fn save_image_generation_result_rejects_data_url_payload() { #[tokio::test] async fn save_image_generation_result_overwrites_existing_file() { - let codex_home = tempfile::tempdir().expect("create codex home"); - let codex_home = codex_home.path().abs(); - let existing_path = image_generation_artifact_path(&codex_home, "session-1", "ig_overwrite"); + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let generated_images_dir = temp_dir.path().join("images").abs(); + let existing_path = + image_generation_artifact_path(&generated_images_dir, "session-1", "ig_overwrite"); std::fs::create_dir_all( existing_path .parent() @@ -472,9 +474,10 @@ async fn save_image_generation_result_overwrites_existing_file() { .expect("create image output dir"); std::fs::write(&existing_path, b"existing").expect("seed existing image"); - let saved_path = save_image_generation_result(&codex_home, "session-1", "ig_overwrite", "Zm9v") - .await - .expect("image should be saved"); + let saved_path = + save_image_generation_result(&generated_images_dir, "session-1", "ig_overwrite", "Zm9v") + .await + .expect("image should be saved"); assert_eq!(saved_path, existing_path); assert_eq!(std::fs::read(&saved_path).expect("saved file"), b"foo"); @@ -482,15 +485,17 @@ async fn save_image_generation_result_overwrites_existing_file() { } #[tokio::test] -async fn save_image_generation_result_sanitizes_call_id_for_codex_home_output_path() { - let codex_home = tempfile::tempdir().expect("create codex home"); - let codex_home = codex_home.path().abs(); - let expected_path = image_generation_artifact_path(&codex_home, "session-1", "../ig/.."); +async fn save_image_generation_result_sanitizes_call_id_for_generated_images_dir() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let generated_images_dir = temp_dir.path().join("images").abs(); + let expected_path = + image_generation_artifact_path(&generated_images_dir, "session-1", "../ig/.."); let _ = std::fs::remove_file(&expected_path); - let saved_path = save_image_generation_result(&codex_home, "session-1", "../ig/..", "Zm9v") - .await - .expect("image should be saved"); + let saved_path = + save_image_generation_result(&generated_images_dir, "session-1", "../ig/..", "Zm9v") + .await + .expect("image should be saved"); assert_eq!(saved_path, expected_path); assert_eq!(std::fs::read(&saved_path).expect("saved file"), b"foo"); @@ -499,9 +504,9 @@ async fn save_image_generation_result_sanitizes_call_id_for_codex_home_output_pa #[tokio::test] async fn save_image_generation_result_rejects_non_standard_base64() { - let codex_home = tempfile::tempdir().expect("create codex home"); - let codex_home = codex_home.path().abs(); - let err = save_image_generation_result(&codex_home, "session-1", "ig_urlsafe", "_-8") + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let generated_images_dir = temp_dir.path().join("images").abs(); + let err = save_image_generation_result(&generated_images_dir, "session-1", "ig_urlsafe", "_-8") .await .expect_err("non-standard base64 should error"); assert!(matches!(err, CodexErr::InvalidRequest(_))); @@ -509,10 +514,10 @@ async fn save_image_generation_result_rejects_non_standard_base64() { #[tokio::test] async fn save_image_generation_result_rejects_non_base64_data_urls() { - let codex_home = tempfile::tempdir().expect("create codex home"); - let codex_home = codex_home.path().abs(); + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let generated_images_dir = temp_dir.path().join("images").abs(); let err = save_image_generation_result( - &codex_home, + &generated_images_dir, "session-1", "ig_svg", "data:image/svg+xml,", diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs index 1aa7754a18..b0ef0c8330 100644 --- a/codex-rs/core/src/tools/handlers/extension_tools.rs +++ b/codex-rs/core/src/tools/handlers/extension_tools.rs @@ -559,7 +559,7 @@ mod tests { let handler = ExtensionToolAdapter::new(Arc::new(ImageGenerationExtensionExecutor)); let expected_path = test_path_buf("/tmp/extension-claimed.png").abs(); let default_path = crate::stream_events_utils::image_generation_artifact_path( - &turn.config.codex_home, + &turn.config.generated_images_dir, &session.thread_id.to_string(), "call-image", ); diff --git a/codex-rs/core/tests/suite/extension_sandbox.rs b/codex-rs/core/tests/suite/extension_sandbox.rs index 6d998a4693..51d32a8056 100644 --- a/codex-rs/core/tests/suite/extension_sandbox.rs +++ b/codex-rs/core/tests/suite/extension_sandbox.rs @@ -51,11 +51,15 @@ const TINY_PNG_DATA_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA fn image_generation_extensions( auth: &CodexAuth, - resolve_save_root: impl Fn(&Config) -> Option + Send + Sync + 'static, + resolve_generated_images_dir: impl Fn(&Config) -> Option + Send + Sync + 'static, ) -> Arc> { let auth_manager = codex_core::test_support::auth_manager_from_auth(auth.clone()); let mut extension_builder = ExtensionRegistryBuilder::::new(); - install_image_generation_extension(&mut extension_builder, auth_manager, resolve_save_root); + install_image_generation_extension( + &mut extension_builder, + auth_manager, + resolve_generated_images_dir, + ); Arc::new(extension_builder.build()) } @@ -65,7 +69,8 @@ async fn extension_tool_receives_turn_environment_sandbox() -> Result<()> { let server = responses::start_mock_server().await; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let extensions = image_generation_extensions(&auth, |config| Some(config.codex_home.clone())); + let extensions = + image_generation_extensions(&auth, |config| Some(config.generated_images_dir.clone())); let mut builder = test_codex() .with_auth(auth) .with_extensions(extensions) diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index 8074344111..aa3f01e560 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -29,7 +29,7 @@ fn responses_extensions(auth: &CodexAuth) -> Arc> { let mut extension_builder = ExtensionRegistryBuilder::::new(); install_web_search_extension(&mut extension_builder, Arc::clone(&auth_manager)); install_image_generation_extension(&mut extension_builder, auth_manager, |config| { - Some(config.codex_home.clone()) + Some(config.generated_images_dir.clone()) }); Arc::new(extension_builder.build()) } diff --git a/codex-rs/ext/image-generation/src/extension.rs b/codex-rs/ext/image-generation/src/extension.rs index 8195c08e61..85ed5a3abc 100644 --- a/codex-rs/ext/image-generation/src/extension.rs +++ b/codex-rs/ext/image-generation/src/extension.rs @@ -21,26 +21,29 @@ use crate::tool::ImageGenerationTool; #[derive(Clone)] struct ImageGenerationExtension { auth_manager: Arc, - resolve_save_root: Arc, + resolve_generated_images_dir: Arc, } -type SaveRootResolver = dyn Fn(&Config) -> Option + Send + Sync; +type GeneratedImagesDirResolver = dyn Fn(&Config) -> Option + Send + Sync; #[derive(Clone)] struct ImageGenerationExtensionConfig { available: bool, provider: ModelProviderInfo, - save_root: Option, + generated_images_dir: Option, } impl ImageGenerationExtensionConfig { /// Resolves whether standalone image generation should be available for a thread. - fn from_config(config: &Config, resolve_save_root: &SaveRootResolver) -> Self { + fn from_config( + config: &Config, + resolve_generated_images_dir: &GeneratedImagesDirResolver, + ) -> Self { Self { // Core selects this executor per turn using the feature flag or model metadata. available: config.model_provider.is_openai(), provider: config.model_provider.clone(), - save_root: resolve_save_root(config), + generated_images_dir: resolve_generated_images_dir(config), } } } @@ -56,7 +59,7 @@ impl ThreadLifecycleContributor for ImageGenerationExtension { .thread_store .insert(ImageGenerationExtensionConfig::from_config( input.config, - self.resolve_save_root.as_ref(), + self.resolve_generated_images_dir.as_ref(), )); }) } @@ -73,7 +76,7 @@ impl ConfigContributor for ImageGenerationExtension { ) { thread_store.insert(ImageGenerationExtensionConfig::from_config( new_config, - self.resolve_save_root.as_ref(), + self.resolve_generated_images_dir.as_ref(), )); } } @@ -97,7 +100,7 @@ impl ToolContributor for ImageGenerationExtension { config.provider.clone(), Some(self.auth_manager.clone()), )), - config.save_root.clone(), + config.generated_images_dir.clone(), thread_store.level_id().to_string(), ))] } @@ -107,11 +110,11 @@ impl ToolContributor for ImageGenerationExtension { pub fn install( registry: &mut ExtensionRegistryBuilder, auth_manager: Arc, - resolve_save_root: impl Fn(&Config) -> Option + Send + Sync + 'static, + resolve_generated_images_dir: impl Fn(&Config) -> Option + Send + Sync + 'static, ) { let extension = Arc::new(ImageGenerationExtension { auth_manager, - resolve_save_root: Arc::new(resolve_save_root), + resolve_generated_images_dir: Arc::new(resolve_generated_images_dir), }); registry.thread_lifecycle_contributor(extension.clone()); registry.config_contributor(extension.clone()); diff --git a/codex-rs/ext/image-generation/src/tool.rs b/codex-rs/ext/image-generation/src/tool.rs index 3bd4dc5348..ed0dd0a3c0 100644 --- a/codex-rs/ext/image-generation/src/tool.rs +++ b/codex-rs/ext/image-generation/src/tool.rs @@ -57,7 +57,7 @@ const IMAGEGEN_DESCRIPTION: &str = include_str!("../imagegen_description.md"); #[derive(Clone)] pub(crate) struct ImageGenerationTool { backend: CodexImagesBackend, - save_root: Option, + generated_images_dir: Option, thread_id: String, } @@ -65,12 +65,12 @@ impl ImageGenerationTool { /// Creates an image-generation tool backed by an image API executor. pub(crate) fn new( backend: CodexImagesBackend, - save_root: Option, + generated_images_dir: Option, thread_id: String, ) -> Self { Self { backend, - save_root, + generated_images_dir, thread_id, } } @@ -151,10 +151,10 @@ impl ImageGenerationTool { return Err(FunctionCallError::RespondToModel(message)); } }; - let saved_path = match self.save_root.as_ref() { - Some(save_root) => match save_image_generation_result( + let saved_path = match self.generated_images_dir.as_ref() { + Some(generated_images_dir) => match save_image_generation_result( LOCAL_FS.as_ref(), - save_root, + generated_images_dir, &self.thread_id, &call.call_id, &result, @@ -163,9 +163,14 @@ impl ImageGenerationTool { { Ok(path) => Some(path), Err(error) => { - let output_path = - image_generation_artifact_path(save_root, &self.thread_id, &call.call_id); - let output_dir = output_path.parent().unwrap_or_else(|| save_root.clone()); + let output_path = image_generation_artifact_path( + generated_images_dir, + &self.thread_id, + &call.call_id, + ); + let output_dir = output_path + .parent() + .unwrap_or_else(|| generated_images_dir.clone()); tracing::warn!( call_id = %call.call_id, output_dir = %output_dir.display(), @@ -198,7 +203,7 @@ impl ImageGenerationTool { async fn save_image_generation_result( fs: &dyn ExecutorFileSystem, - save_root: &AbsolutePathBuf, + generated_images_dir: &AbsolutePathBuf, session_id: &str, call_id: &str, result: &str, @@ -206,7 +211,7 @@ async fn save_image_generation_result( let bytes = BASE64_STANDARD .decode(result.trim().as_bytes()) .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; - let path = image_generation_artifact_path(save_root, session_id, call_id); + let path = image_generation_artifact_path(generated_images_dir, session_id, call_id); if let Some(parent) = path.parent() { fs.create_directory( &PathUri::from_abs_path(&parent), diff --git a/codex-rs/skills/src/assets/samples/imagegen/SKILL.md b/codex-rs/skills/src/assets/samples/imagegen/SKILL.md index 4285e5e641..36b0d17868 100644 --- a/codex-rs/skills/src/assets/samples/imagegen/SKILL.md +++ b/codex-rs/skills/src/assets/samples/imagegen/SKILL.md @@ -34,7 +34,7 @@ Rules: Built-in save-path policy: - In built-in tool mode, Codex saves generated images under `$CODEX_HOME/*` by default. - Do not describe or rely on OS temp as the default built-in destination. -- Do not describe or rely on a destination-path argument (if any) on the built-in `image_gen` tool. If a specific location is needed, generate first and then move or copy the selected output from `$CODEX_HOME/generated_images/...`. +- Do not describe or rely on a destination-path argument (if any) on the built-in `image_gen` tool. If a specific location is needed, generate first and then move or copy the selected output from the path returned by the tool (by default `$CODEX_HOME/generated_images/...`). - Save-path precedence in built-in mode: 1. If the user names a destination, move or copy the selected output there. 2. If the image is meant for the current project, move or copy the final selected image into the workspace before finishing. @@ -110,8 +110,8 @@ Assume the user wants a new image unless they clearly ask to change an existing 11. For transparent-output requests, follow the transparent image guidance below: generate with built-in `image_gen` on a flat chroma-key background, copy the selected output into the workspace or `tmp/imagegen/`, run the installed `$CODEX_HOME/skills/.system/imagegen/scripts/remove_chroma_key.py` helper, and validate the alpha result before using it. If this path looks unsuitable or fails, ask before switching to CLI `gpt-image-1.5`. 12. Inspect outputs and validate: subject, style, composition, text accuracy, and invariants/avoid items. 13. Iterate with a single targeted change, then re-check. -14. For preview-only work, render the image inline; the underlying file may remain at the default `$CODEX_HOME/generated_images/...` path. -15. For project-bound work, move or copy the selected artifact into the workspace and update any consuming code or references. Never leave a project-referenced asset only at the default `$CODEX_HOME/generated_images/...` path. +14. For preview-only work, render the image inline; the underlying file may remain at the path returned by the tool. +15. For project-bound work, move or copy the selected artifact from the path returned by the tool into the workspace and update any consuming code or references. Never leave a project-referenced asset only at the generated-image output path. 16. For batches or multi-asset requests, persist every requested deliverable final in the workspace unless the user explicitly asked to keep outputs preview-only. Discarded variants do not need to be kept unless requested. 17. If the user explicitly chooses or confirms the CLI fallback, then use the fallback-only docs for model, quality, size, `input_fidelity`, masks, output format, output paths, and network setup. 18. Always report the final saved path(s) for any workspace-bound asset(s), plus the final prompt or prompt set and whether the built-in tool or fallback CLI mode was used. @@ -123,7 +123,7 @@ Transparent-image requests still use built-in `image_gen` first. Because the bui Default sequence: 1. Use built-in `image_gen` to generate the requested subject on a perfectly flat solid chroma-key background. 2. Choose a key color that is unlikely to appear in the subject: default `#00ff00`, use `#ff00ff` for green subjects, and avoid `#0000ff` for blue subjects. -3. After generation, move or copy the selected source image from `$CODEX_HOME/generated_images/...` into the workspace or `tmp/imagegen/`. +3. After generation, move or copy the selected source image from the path returned by the tool into the workspace or `tmp/imagegen/`. 4. Run the installed helper path, not a project-relative script path: ```bash python "${CODEX_HOME:-$HOME/.codex}/skills/.system/imagegen/scripts/remove_chroma_key.py" \ diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 4237a33fef..d5259a509e 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -239,6 +239,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R memories: MemoriesConfig::default(), sqlite_home: codex_home.to_path_buf(), log_dir: codex_home.join("log").to_path_buf(), + generated_images_dir: codex_home.join("generated_images"), config_lock_export_dir: None, config_lock_allow_codex_version_mismatch: false, config_lock_save_fields_resolved_from_model_catalog: true,