mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
make generated image directory configurable
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -330,6 +330,10 @@ pub struct ConfigToml {
|
||||
/// Defaults to `$CODEX_HOME/log`.
|
||||
pub log_dir: Option<AbsolutePathBuf>,
|
||||
|
||||
/// Directory where Codex writes generated images.
|
||||
/// Defaults to `$CODEX_HOME/generated_images`.
|
||||
pub generated_images_dir: Option<AbsolutePathBuf>,
|
||||
|
||||
/// Debugging and reproducibility settings.
|
||||
pub debug: Option<DebugToml>,
|
||||
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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<AbsolutePathBuf>,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
"<image_id>",
|
||||
);
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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, "<image_id>");
|
||||
let image_output_path = image_generation_artifact_path(
|
||||
&turn_context.config.generated_images_dir,
|
||||
&session_id,
|
||||
"<image_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(),
|
||||
|
||||
@@ -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,<svg/>",
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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<AbsolutePathBuf> + Send + Sync + 'static,
|
||||
resolve_generated_images_dir: impl Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync + 'static,
|
||||
) -> Arc<ExtensionRegistry<Config>> {
|
||||
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth.clone());
|
||||
let mut extension_builder = ExtensionRegistryBuilder::<Config>::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)
|
||||
|
||||
@@ -29,7 +29,7 @@ fn responses_extensions(auth: &CodexAuth) -> Arc<ExtensionRegistry<Config>> {
|
||||
let mut extension_builder = ExtensionRegistryBuilder::<Config>::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())
|
||||
}
|
||||
|
||||
@@ -21,26 +21,29 @@ use crate::tool::ImageGenerationTool;
|
||||
#[derive(Clone)]
|
||||
struct ImageGenerationExtension {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
resolve_save_root: Arc<SaveRootResolver>,
|
||||
resolve_generated_images_dir: Arc<GeneratedImagesDirResolver>,
|
||||
}
|
||||
|
||||
type SaveRootResolver = dyn Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync;
|
||||
type GeneratedImagesDirResolver = dyn Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ImageGenerationExtensionConfig {
|
||||
available: bool,
|
||||
provider: ModelProviderInfo,
|
||||
save_root: Option<AbsolutePathBuf>,
|
||||
generated_images_dir: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
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<Config> 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<Config> 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<Config>,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
resolve_save_root: impl Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync + 'static,
|
||||
resolve_generated_images_dir: impl Fn(&Config) -> Option<AbsolutePathBuf> + 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());
|
||||
|
||||
@@ -57,7 +57,7 @@ const IMAGEGEN_DESCRIPTION: &str = include_str!("../imagegen_description.md");
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ImageGenerationTool {
|
||||
backend: CodexImagesBackend,
|
||||
save_root: Option<AbsolutePathBuf>,
|
||||
generated_images_dir: Option<AbsolutePathBuf>,
|
||||
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<AbsolutePathBuf>,
|
||||
generated_images_dir: Option<AbsolutePathBuf>,
|
||||
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),
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -239,6 +239,7 @@ fn new_config(model: Option<String>, 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,
|
||||
|
||||
Reference in New Issue
Block a user