mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Use the image generation extension by default (#31596)
This commit is contained in:
Generated
+2
@@ -2760,6 +2760,7 @@ dependencies = [
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-home",
|
||||
"codex-image-generation-extension",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
@@ -3443,6 +3444,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-home",
|
||||
"codex-image-generation-extension",
|
||||
"codex-login",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
|
||||
@@ -589,7 +589,6 @@ model_provider = "openai-custom"
|
||||
chatgpt_base_url = "{server_uri}"
|
||||
|
||||
[features]
|
||||
imagegenext = true
|
||||
{code_mode_only}
|
||||
|
||||
[model_providers.openai-custom]
|
||||
|
||||
@@ -21,6 +21,7 @@ codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-image-generation-extension = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
|
||||
@@ -61,6 +61,7 @@ pub use codex_exec_server::NoiseChannelIdentity;
|
||||
pub use codex_exec_server::NoiseChannelPublicKey;
|
||||
pub use codex_exec_server::NoiseRendezvousConnectBundle;
|
||||
pub use codex_exec_server::NoiseRendezvousConnectProvider;
|
||||
pub use codex_extension_api::ExtensionRegistryBuilder;
|
||||
pub use codex_extension_api::LoadUserInstructionsFuture;
|
||||
pub use codex_extension_api::LoadedUserInstructions;
|
||||
pub use codex_extension_api::UserInstructions;
|
||||
@@ -69,6 +70,7 @@ pub use codex_extension_api::empty_extension_registry;
|
||||
pub use codex_features::Feature;
|
||||
pub use codex_features::Features;
|
||||
pub use codex_home::CodexHomeUserInstructionsProvider;
|
||||
pub use codex_image_generation_extension::install as install_image_generation_extension;
|
||||
pub use codex_login::AuthHeaders;
|
||||
pub use codex_login::AuthManager;
|
||||
pub use codex_login::CodexAuth;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::ContextualUserFragment;
|
||||
use std::fmt::Display;
|
||||
|
||||
/// Maximum size of the extension's model-facing generated-image path hint.
|
||||
@@ -21,36 +20,3 @@ fn image_generation_hint(
|
||||
"Generated images are saved to {image_output_dir} as {image_output_path} by default.\nIf you need to use a generated image at another path, copy it and leave the original in place unless the user explicitly asks you to delete it."
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ImageGenerationInstructions {
|
||||
image_output_dir: String,
|
||||
image_output_path: String,
|
||||
}
|
||||
|
||||
impl ImageGenerationInstructions {
|
||||
pub(crate) fn new(image_output_dir: impl Display, image_output_path: impl Display) -> Self {
|
||||
Self {
|
||||
image_output_dir: image_output_dir.to_string(),
|
||||
image_output_path: image_output_path.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for ImageGenerationInstructions {
|
||||
fn role(&self) -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
("", "")
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
image_generation_hint(&self.image_output_dir, &self.image_output_path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
|
||||
pub(crate) use current_time_reminder::CurrentTimeReminder;
|
||||
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
|
||||
pub(crate) use hook_additional_context::HookAdditionalContext;
|
||||
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
|
||||
pub use image_generation_instructions::extension_image_generation_output_hint;
|
||||
pub(crate) use inter_agent_completion_message::InterAgentCompletionMessage;
|
||||
pub use internal_model_context::InternalContextSource;
|
||||
|
||||
@@ -8572,49 +8572,6 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_is_emp
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_initial_context_omits_default_image_save_location_with_image_history() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
session
|
||||
.replace_history(
|
||||
vec![ResponseItem::ImageGenerationCall {
|
||||
id: Some("ig-test".to_string()),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("a tiny blue square".to_string()),
|
||||
result: "Zm9v".to_string(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}],
|
||||
/*reference_context_item*/ None,
|
||||
)
|
||||
.await;
|
||||
let turn_context = Arc::new(turn_context);
|
||||
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
assert!(
|
||||
!developer_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("Generated images are saved to")),
|
||||
"expected initial context to omit image save instructions even with image history, got {developer_texts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_initial_context_omits_default_image_save_location_without_image_history() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let turn_context = Arc::new(turn_context);
|
||||
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
|
||||
assert!(
|
||||
!developer_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("Generated images are saved to")),
|
||||
"expected initial context to omit image save instructions without image history, got {developer_texts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_initial_context_trims_skill_metadata_from_context_window_budget() {
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
@@ -8822,102 +8779,6 @@ async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_buil
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_output_item_done_records_image_save_history_message() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let session = Arc::new(session);
|
||||
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,
|
||||
&session.thread_id.to_string(),
|
||||
call_id,
|
||||
);
|
||||
let _ = std::fs::remove_file(&expected_saved_path);
|
||||
let item = ResponseItem::ImageGenerationCall {
|
||||
id: Some(call_id.to_string()),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("a tiny blue square".to_string()),
|
||||
result: "Zm9v".to_string(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
|
||||
let mut ctx = HandleOutputCtx {
|
||||
sess: Arc::clone(&session),
|
||||
turn_context: Arc::clone(&turn_context),
|
||||
turn_store: Arc::new(codex_extension_api::ExtensionData::new(
|
||||
turn_context.sub_id.clone(),
|
||||
)),
|
||||
tool_runtime: test_tool_runtime(Arc::clone(&session), Arc::clone(&turn_context)),
|
||||
cancellation_token: CancellationToken::new(),
|
||||
};
|
||||
handle_output_item_done(&mut ctx, item.clone(), /*previously_active_item*/ None)
|
||||
.await
|
||||
.expect("image generation item should succeed");
|
||||
|
||||
let history = session.clone_history().await;
|
||||
let image_output_path = crate::stream_events_utils::image_generation_artifact_path(
|
||||
&turn_context.config.codex_home,
|
||||
&session.thread_id.to_string(),
|
||||
"<image_id>",
|
||||
);
|
||||
let image_output_dir = image_output_path
|
||||
.parent()
|
||||
.expect("generated image path should have a parent");
|
||||
let image_message: ResponseItem = crate::context::ContextualUserFragment::into(
|
||||
crate::context::ImageGenerationInstructions::new(
|
||||
image_output_dir.display(),
|
||||
image_output_path.display(),
|
||||
),
|
||||
);
|
||||
let expected = vec![image_message, item];
|
||||
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
|
||||
assert_eq!(
|
||||
std::fs::read(&expected_saved_path).expect("saved file"),
|
||||
b"foo"
|
||||
);
|
||||
let _ = std::fs::remove_file(&expected_saved_path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let session = Arc::new(session);
|
||||
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,
|
||||
&session.thread_id.to_string(),
|
||||
call_id,
|
||||
);
|
||||
let _ = std::fs::remove_file(&expected_saved_path);
|
||||
let item = ResponseItem::ImageGenerationCall {
|
||||
id: Some(call_id.to_string()),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("broken payload".to_string()),
|
||||
result: "_-8".to_string(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
|
||||
let mut ctx = HandleOutputCtx {
|
||||
sess: Arc::clone(&session),
|
||||
turn_context: Arc::clone(&turn_context),
|
||||
turn_store: Arc::new(codex_extension_api::ExtensionData::new(
|
||||
turn_context.sub_id.clone(),
|
||||
)),
|
||||
tool_runtime: test_tool_runtime(Arc::clone(&session), Arc::clone(&turn_context)),
|
||||
cancellation_token: CancellationToken::new(),
|
||||
};
|
||||
handle_output_item_done(&mut ctx, item.clone(), /*previously_active_item*/ None)
|
||||
.await
|
||||
.expect("image generation item should still complete");
|
||||
|
||||
let history = session.clone_history().await;
|
||||
let expected = vec![item];
|
||||
assert_eq!(strip_metadata_from_items(history.raw_items()), expected);
|
||||
assert!(!expected_saved_path.exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_initial_context_uses_previous_turn_settings_for_realtime_end() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
|
||||
@@ -1853,7 +1853,6 @@ async fn handle_assistant_item_done_in_plan_mode(
|
||||
let mut finalized_facts = None;
|
||||
if let Some(finalized_turn_item) = finalize_non_tool_response_item(
|
||||
sess,
|
||||
turn_context,
|
||||
TurnItemContributorPolicy::Run(turn_store),
|
||||
item,
|
||||
/*plan_mode*/ true,
|
||||
@@ -2162,7 +2161,6 @@ async fn try_run_sampling_request(
|
||||
}
|
||||
if let Some(turn_item) = handle_non_tool_response_item(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
TurnItemContributorPolicy::Skip,
|
||||
&item,
|
||||
plan_mode,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::items::ImageGenerationItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_utils_stream_parser::strip_citations;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::ImageGenerationInstructions;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::parse_turn_item;
|
||||
use crate::session::session::Session;
|
||||
@@ -108,85 +103,6 @@ pub(crate) fn raw_assistant_output_text_from_item(item: &ResponseItem) -> Option
|
||||
None
|
||||
}
|
||||
|
||||
async fn save_image_generation_result(
|
||||
codex_home: &AbsolutePathBuf,
|
||||
session_id: &str,
|
||||
call_id: &str,
|
||||
result: &str,
|
||||
) -> Result<AbsolutePathBuf> {
|
||||
let bytes = BASE64_STANDARD
|
||||
.decode(result.trim().as_bytes())
|
||||
.map_err(|err| {
|
||||
CodexErr::InvalidRequest(format!("invalid image generation payload: {err}"))
|
||||
})?;
|
||||
let path = image_generation_artifact_path(codex_home, session_id, call_id);
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
tokio::fs::write(&path, bytes).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub(crate) async fn persist_image_generation_item(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
image_item: &mut ImageGenerationItem,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
image_item.saved_path = None;
|
||||
let session_id = sess.thread_id.to_string();
|
||||
match save_image_generation_result(
|
||||
&turn_context.config.codex_home,
|
||||
&session_id,
|
||||
&image_item.id,
|
||||
&image_item.result,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(path) => {
|
||||
image_item.saved_path = Some(path.clone());
|
||||
Some(path)
|
||||
}
|
||||
Err(err) => {
|
||||
let output_path = image_generation_artifact_path(
|
||||
&turn_context.config.codex_home,
|
||||
&session_id,
|
||||
&image_item.id,
|
||||
);
|
||||
let output_dir = output_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| turn_context.config.codex_home.clone());
|
||||
tracing::warn!(
|
||||
call_id = %image_item.id,
|
||||
output_dir = %output_dir.display(),
|
||||
"failed to save generated image: {err}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn record_image_generation_instructions(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
image_item: &ImageGenerationItem,
|
||||
) {
|
||||
if image_item.saved_path.is_none() {
|
||||
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_dir = image_output_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| turn_context.config.codex_home.clone());
|
||||
let message: ResponseItem = ContextualUserFragment::into(ImageGenerationInstructions::new(
|
||||
image_output_dir.display(),
|
||||
image_output_path.display(),
|
||||
));
|
||||
sess.record_conversation_items(turn_context, &[message])
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Persist a completed model response item and record any cited memory usage.
|
||||
pub(crate) async fn record_completed_response_item(
|
||||
sess: &Session,
|
||||
@@ -356,14 +272,12 @@ pub(crate) struct FinalizedTurnItemFacts {
|
||||
|
||||
pub(crate) async fn finalize_non_tool_response_item(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
contributor_policy: TurnItemContributorPolicy<'_>,
|
||||
item: &ResponseItem,
|
||||
plan_mode: bool,
|
||||
) -> Option<FinalizedTurnItem> {
|
||||
let turn_item =
|
||||
handle_non_tool_response_item(sess, turn_context, contributor_policy, item, plan_mode)
|
||||
.await?;
|
||||
handle_non_tool_response_item(sess, contributor_policy, item, plan_mode).await?;
|
||||
let (memory_citation, last_agent_message, defers_mailbox_delivery_to_next_turn) =
|
||||
match &turn_item {
|
||||
TurnItem::AgentMessage(agent_message) => {
|
||||
@@ -388,7 +302,6 @@ pub(crate) async fn finalize_non_tool_response_item(
|
||||
defers_mailbox_delivery_to_next_turn,
|
||||
)
|
||||
}
|
||||
TurnItem::ImageGeneration(_) => (None, None, true),
|
||||
_ => (None, None, false),
|
||||
};
|
||||
Some(FinalizedTurnItem {
|
||||
@@ -446,7 +359,6 @@ pub(crate) async fn handle_output_item_done(
|
||||
Ok(None) => {
|
||||
let finalized_turn_item = finalize_non_tool_response_item(
|
||||
ctx.sess.as_ref(),
|
||||
ctx.turn_context.as_ref(),
|
||||
TurnItemContributorPolicy::Run(ctx.turn_store.as_ref()),
|
||||
&item,
|
||||
plan_mode,
|
||||
@@ -457,15 +369,8 @@ pub(crate) async fn handle_output_item_done(
|
||||
.map(|finalized| finalized.facts.clone());
|
||||
if let Some(finalized_turn_item) = finalized_turn_item {
|
||||
if previously_active_item.is_none() {
|
||||
let mut started_item = finalized_turn_item.turn_item.clone();
|
||||
if let TurnItem::ImageGeneration(item) = &mut started_item {
|
||||
item.status = "in_progress".to_string();
|
||||
item.revised_prompt = None;
|
||||
item.result.clear();
|
||||
item.saved_path = None;
|
||||
}
|
||||
ctx.sess
|
||||
.emit_turn_item_started(&ctx.turn_context, &started_item)
|
||||
.emit_turn_item_started(&ctx.turn_context, &finalized_turn_item.turn_item)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -516,7 +421,6 @@ pub(crate) async fn handle_output_item_done(
|
||||
|
||||
pub(crate) async fn handle_non_tool_response_item(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
contributor_policy: TurnItemContributorPolicy<'_>,
|
||||
item: &ResponseItem,
|
||||
plan_mode: bool,
|
||||
@@ -526,20 +430,9 @@ pub(crate) async fn handle_non_tool_response_item(
|
||||
match item {
|
||||
ResponseItem::Message { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::WebSearchCall { .. }
|
||||
| ResponseItem::ImageGenerationCall { .. } => {
|
||||
| ResponseItem::WebSearchCall { .. } => {
|
||||
let mut turn_item = parse_turn_item(item)?;
|
||||
finalize_turn_item(
|
||||
sess,
|
||||
turn_context,
|
||||
contributor_policy,
|
||||
&mut turn_item,
|
||||
plan_mode,
|
||||
)
|
||||
.await;
|
||||
if let TurnItem::ImageGeneration(image_item) = &turn_item {
|
||||
record_image_generation_instructions(sess, turn_context, image_item).await;
|
||||
}
|
||||
finalize_turn_item(sess, contributor_policy, &mut turn_item, plan_mode).await;
|
||||
Some(turn_item)
|
||||
}
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
@@ -554,7 +447,6 @@ pub(crate) async fn handle_non_tool_response_item(
|
||||
|
||||
pub(crate) async fn finalize_turn_item(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
contributor_policy: TurnItemContributorPolicy<'_>,
|
||||
turn_item: &mut TurnItem,
|
||||
plan_mode: bool,
|
||||
@@ -578,11 +470,6 @@ pub(crate) async fn finalize_turn_item(
|
||||
agent_message.memory_citation = memory_citation;
|
||||
}
|
||||
}
|
||||
if let TurnItem::ImageGeneration(image_item) = &mut *turn_item
|
||||
&& !image_item.result.is_empty()
|
||||
{
|
||||
persist_image_generation_item(sess, turn_context, image_item).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn last_assistant_message_from_item(
|
||||
@@ -615,7 +502,6 @@ fn completed_item_defers_mailbox_delivery_to_next_turn(
|
||||
// to the safer "defer mailbox mail" behavior.
|
||||
last_assistant_message_from_item(item, plan_mode).is_some()
|
||||
}
|
||||
ResponseItem::ImageGenerationCall { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ use super::completed_item_defers_mailbox_delivery_to_next_turn;
|
||||
use super::finalize_non_tool_response_item;
|
||||
use super::handle_non_tool_response_item;
|
||||
use super::handle_output_item_done;
|
||||
use super::image_generation_artifact_path;
|
||||
use super::last_assistant_message_from_item;
|
||||
use super::response_item_may_include_external_context;
|
||||
use super::save_image_generation_result;
|
||||
use crate::session::step_context::StepContext;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::tools::ToolRouter;
|
||||
@@ -15,7 +13,6 @@ use crate::tools::parallel::ToolCallRuntime;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_extension_api::TurnItemContributor;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::memory_citation::MemoryCitation;
|
||||
@@ -26,7 +23,6 @@ use codex_protocol::models::LocalShellExecAction;
|
||||
use codex_protocol::models::LocalShellStatus;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -139,14 +135,13 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_non_tool_response_item_strips_citations_from_assistant_message() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let (session, _) = make_session_and_context().await;
|
||||
let item = assistant_output_text(
|
||||
"hello<oai-mem-citation><citation_entries>\nMEMORY.md:1-2|note=[x]\n</citation_entries>\n<rollout_ids>\n019cc2ea-1dff-7902-8d40-c8f6e5d83cc4\n</rollout_ids></oai-mem-citation> world",
|
||||
);
|
||||
|
||||
let turn_item = handle_non_tool_response_item(
|
||||
&session,
|
||||
&turn_context,
|
||||
TurnItemContributorPolicy::Skip,
|
||||
&item,
|
||||
/*plan_mode*/ false,
|
||||
@@ -234,7 +229,6 @@ async fn handle_non_tool_response_item_runs_turn_item_contributors_only_when_req
|
||||
|
||||
let provisional_turn_item = handle_non_tool_response_item(
|
||||
&session,
|
||||
&turn_context,
|
||||
TurnItemContributorPolicy::Skip,
|
||||
&item,
|
||||
/*plan_mode*/ false,
|
||||
@@ -250,7 +244,6 @@ async fn handle_non_tool_response_item_runs_turn_item_contributors_only_when_req
|
||||
|
||||
let turn_item = handle_non_tool_response_item(
|
||||
&session,
|
||||
&turn_context,
|
||||
TurnItemContributorPolicy::Run(&turn_store),
|
||||
&item,
|
||||
/*plan_mode*/ false,
|
||||
@@ -325,7 +318,6 @@ async fn finalized_turn_item_defers_mailbox_for_contributed_visible_text() {
|
||||
|
||||
let finalized = finalize_non_tool_response_item(
|
||||
&session,
|
||||
&turn_context,
|
||||
TurnItemContributorPolicy::Run(&turn_store),
|
||||
&item,
|
||||
/*plan_mode*/ false,
|
||||
@@ -351,7 +343,6 @@ async fn finalized_turn_item_keeps_mailbox_open_for_commentary_text() {
|
||||
|
||||
let finalized = finalize_non_tool_response_item(
|
||||
&session,
|
||||
&turn_context,
|
||||
TurnItemContributorPolicy::Run(&turn_store),
|
||||
&item,
|
||||
/*plan_mode*/ false,
|
||||
@@ -415,110 +406,3 @@ fn completed_item_keeps_mailbox_delivery_open_for_commentary_messages() {
|
||||
&item, /*plan_mode*/ false,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_item_defers_mailbox_delivery_for_image_generation_calls() {
|
||||
let item = ResponseItem::ImageGenerationCall {
|
||||
id: Some("ig-1".to_string()),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: None,
|
||||
result: "Zm9v".to_string(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
|
||||
assert!(completed_item_defers_mailbox_delivery_to_next_turn(
|
||||
&item, /*plan_mode*/ false,
|
||||
));
|
||||
}
|
||||
|
||||
#[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");
|
||||
let _ = std::fs::remove_file(&expected_path);
|
||||
|
||||
let saved_path =
|
||||
save_image_generation_result(&codex_home, "session-1", "ig_save_base64", "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");
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
}
|
||||
|
||||
#[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 err = save_image_generation_result(&codex_home, "session-1", "ig_456", result)
|
||||
.await
|
||||
.expect_err("data url payload should error");
|
||||
assert!(matches!(err, CodexErr::InvalidRequest(_)));
|
||||
}
|
||||
|
||||
#[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");
|
||||
std::fs::create_dir_all(
|
||||
existing_path
|
||||
.parent()
|
||||
.expect("generated image path should have a parent"),
|
||||
)
|
||||
.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");
|
||||
|
||||
assert_eq!(saved_path, existing_path);
|
||||
assert_eq!(std::fs::read(&saved_path).expect("saved file"), b"foo");
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
}
|
||||
|
||||
#[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/..");
|
||||
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");
|
||||
|
||||
assert_eq!(saved_path, expected_path);
|
||||
assert_eq!(std::fs::read(&saved_path).expect("saved file"), b"foo");
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
}
|
||||
|
||||
#[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")
|
||||
.await
|
||||
.expect_err("non-standard base64 should error");
|
||||
assert!(matches!(err, CodexErr::InvalidRequest(_)));
|
||||
}
|
||||
|
||||
#[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 err = save_image_generation_result(
|
||||
&codex_home,
|
||||
"session-1",
|
||||
"ig_svg",
|
||||
"data:image/svg+xml,<svg/>",
|
||||
)
|
||||
.await
|
||||
.expect_err("non-base64 data url should error");
|
||||
assert!(matches!(err, CodexErr::InvalidRequest(_)));
|
||||
}
|
||||
|
||||
@@ -11,12 +11,6 @@ pub struct WebSearchToolOptions<'a> {
|
||||
pub web_search_tool_type: WebSearchToolType,
|
||||
}
|
||||
|
||||
pub fn create_image_generation_tool(output_format: &str) -> ToolSpec {
|
||||
ToolSpec::ImageGeneration {
|
||||
output_format: output_format.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_web_search_tool(options: WebSearchToolOptions<'_>) -> Option<ToolSpec> {
|
||||
let (external_web_access, indexed_web_access) = match options.web_search_mode {
|
||||
Some(WebSearchMode::Cached) => (false, None),
|
||||
|
||||
@@ -7,16 +7,6 @@ use codex_tools::ResponsesApiWebSearchFilters;
|
||||
use codex_tools::ResponsesApiWebSearchUserLocation;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn image_generation_tool_matches_expected_spec() {
|
||||
assert_eq!(
|
||||
create_image_generation_tool("png"),
|
||||
ToolSpec::ImageGeneration {
|
||||
output_format: "png".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_tool_preserves_configured_options() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -473,7 +473,6 @@ fn namespace_function_names(specs: &[ToolSpec], namespace_name: &str) -> Vec<Str
|
||||
ToolSpec::Function(_)
|
||||
| ToolSpec::Freeform(_)
|
||||
| ToolSpec::ToolSearch { .. }
|
||||
| ToolSpec::ImageGeneration { .. }
|
||||
| ToolSpec::WebSearch { .. }
|
||||
| ToolSpec::Namespace(_) => None,
|
||||
})
|
||||
|
||||
@@ -52,7 +52,6 @@ use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHand
|
||||
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
|
||||
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
|
||||
use crate::tools::hosted_spec::WebSearchToolOptions;
|
||||
use crate::tools::hosted_spec::create_image_generation_tool;
|
||||
use crate::tools::hosted_spec::create_web_search_tool;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::ToolExposure;
|
||||
@@ -319,12 +318,6 @@ fn hosted_model_tool_specs(context: &CoreToolPlanContext<'_>) -> Vec<ToolSpec> {
|
||||
}) {
|
||||
specs.push(hosted_web_search_tool);
|
||||
}
|
||||
// TODO: Remove hosted image generation once the standalone extension is ready.
|
||||
if image_generation_tool_enabled(turn_context)
|
||||
&& !standalone_image_generation_available(turn_context, context.extension_tool_executors)
|
||||
{
|
||||
specs.push(create_image_generation_tool("png"));
|
||||
}
|
||||
specs
|
||||
}
|
||||
|
||||
@@ -376,15 +369,6 @@ fn agent_jobs_worker_tools_enabled(turn_context: &TurnContext) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn image_generation_tool_enabled(turn_context: &TurnContext) -> bool {
|
||||
image_generation_runtime_enabled(turn_context)
|
||||
&& turn_context
|
||||
.config
|
||||
.features
|
||||
.get()
|
||||
.enabled(Feature::ImageGeneration)
|
||||
}
|
||||
|
||||
fn image_generation_runtime_enabled(turn_context: &TurnContext) -> bool {
|
||||
(turn_context
|
||||
.provider
|
||||
@@ -407,25 +391,11 @@ fn standalone_image_generation_model_visible(turn_context: &TurnContext) -> bool
|
||||
return false;
|
||||
}
|
||||
|
||||
if turn_context.model_info.use_responses_lite {
|
||||
return true;
|
||||
}
|
||||
|
||||
turn_context
|
||||
.config
|
||||
.features
|
||||
.get()
|
||||
.enabled(Feature::ImageGenExt)
|
||||
}
|
||||
|
||||
fn standalone_image_generation_available(
|
||||
turn_context: &TurnContext,
|
||||
extension_tools: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],
|
||||
) -> bool {
|
||||
standalone_image_generation_model_visible(turn_context)
|
||||
&& extension_tools.iter().any(|executor| {
|
||||
executor.tool_name() == ToolName::namespaced(IMAGE_GEN_NAMESPACE, IMAGEGEN_TOOL_NAME)
|
||||
})
|
||||
.enabled(Feature::ImageGeneration)
|
||||
}
|
||||
|
||||
fn wait_agent_timeout_options(turn_context: &TurnContext) -> WaitAgentTimeoutOptions {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_features::Feature;
|
||||
@@ -83,7 +82,6 @@ impl ToolPlanProbe {
|
||||
)),
|
||||
ToolSpec::Function(_)
|
||||
| ToolSpec::ToolSearch { .. }
|
||||
| ToolSpec::ImageGeneration { .. }
|
||||
| ToolSpec::WebSearch { .. }
|
||||
| ToolSpec::Freeform(_) => None,
|
||||
})
|
||||
@@ -275,40 +273,23 @@ fn use_bedrock_provider(turn: &mut TurnContext) {
|
||||
turn.provider = create_model_provider(provider_info, turn.auth_manager.clone());
|
||||
}
|
||||
|
||||
fn use_actor_authorized_provider(turn: &mut TurnContext) {
|
||||
let mut provider_info = turn.config.model_provider.clone();
|
||||
provider_info.requires_openai_auth = false;
|
||||
provider_info.http_headers = Some(HashMap::from([
|
||||
(
|
||||
"x-openai-actor-authorization".to_string(),
|
||||
"test-actor-authorization".to_string(),
|
||||
),
|
||||
(
|
||||
"ChatGPT-Account-ID".to_string(),
|
||||
"test-account-id".to_string(),
|
||||
),
|
||||
]));
|
||||
turn.auth_manager = None;
|
||||
update_config(turn, |config| {
|
||||
config.model_provider = provider_info.clone();
|
||||
});
|
||||
turn.provider = create_model_provider(provider_info, /*auth_manager*/ None);
|
||||
struct TestNamespaceExtensionTool {
|
||||
namespace: &'static str,
|
||||
tool_name: &'static str,
|
||||
}
|
||||
|
||||
struct WebRunExtensionTool;
|
||||
|
||||
impl ToolExecutor<ExtensionToolCall> for WebRunExtensionTool {
|
||||
impl ToolExecutor<ExtensionToolCall> for TestNamespaceExtensionTool {
|
||||
fn tool_name(&self) -> ToolName {
|
||||
ToolName::namespaced("web", "run")
|
||||
ToolName::namespaced(self.namespace, self.tool_name)
|
||||
}
|
||||
|
||||
fn spec(&self) -> ToolSpec {
|
||||
ToolSpec::Namespace(codex_tools::ResponsesApiNamespace {
|
||||
name: "web".to_string(),
|
||||
description: "Test web namespace.".to_string(),
|
||||
name: self.namespace.to_string(),
|
||||
description: "Test namespace.".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "run".to_string(),
|
||||
description: "Test standalone web search tool.".to_string(),
|
||||
name: self.tool_name.to_string(),
|
||||
description: "Test namespace tool.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: codex_tools::JsonSchema::default(),
|
||||
@@ -1497,7 +1478,7 @@ async fn code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools() {
|
||||
"wait",
|
||||
"request_user_input",
|
||||
"agents",
|
||||
// Hosted Responses tools.
|
||||
// Hosted Responses tool.
|
||||
"web_search",
|
||||
]
|
||||
);
|
||||
@@ -1526,79 +1507,63 @@ async fn code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hosted_tools_follow_provider_auth_model_and_config_gates() {
|
||||
let api_key_auth = probe(|turn| {
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
})
|
||||
async fn hosted_web_search_and_standalone_image_generation_follow_runtime_gates() {
|
||||
let image_generation_tool = Arc::new(TestNamespaceExtensionTool {
|
||||
namespace: "image_gen",
|
||||
tool_name: "imagegen",
|
||||
});
|
||||
let image_generation = probe_with(
|
||||
|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![image_generation_tool.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
api_key_auth.assert_visible_lacks(&["image_generation"]);
|
||||
image_generation.assert_visible_contains(&["image_gen"]);
|
||||
|
||||
let unrelated_chatgpt_auth = probe(|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
|
||||
let mut provider_info = turn.provider.info().clone();
|
||||
provider_info.requires_openai_auth = false;
|
||||
provider_info.http_headers = None;
|
||||
update_config(turn, |config| {
|
||||
config.model_provider = provider_info.clone();
|
||||
});
|
||||
turn.provider = create_model_provider(provider_info, turn.auth_manager.clone());
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
})
|
||||
let extension_disabled = probe_with(
|
||||
|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ false);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![image_generation_tool.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
unrelated_chatgpt_auth.assert_visible_lacks(&["image_generation"]);
|
||||
extension_disabled.assert_visible_lacks(&["image_gen"]);
|
||||
|
||||
let actor_authorized_provider = probe(|turn| {
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
|
||||
use_actor_authorized_provider(turn);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
})
|
||||
let text_only_model = probe_with(
|
||||
|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
turn.model_info.input_modalities = vec![];
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![image_generation_tool.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
actor_authorized_provider.assert_visible_contains(&["image_generation"]);
|
||||
text_only_model.assert_visible_lacks(&["image_gen"]);
|
||||
|
||||
let feature_disabled = probe(|turn| {
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ false);
|
||||
use_actor_authorized_provider(turn);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
})
|
||||
let unsupported_provider = probe_with(
|
||||
|turn| {
|
||||
use_bedrock_provider(turn);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![image_generation_tool],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
feature_disabled.assert_visible_lacks(&["image_generation"]);
|
||||
|
||||
let text_only_model = probe(|turn| {
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
|
||||
use_actor_authorized_provider(turn);
|
||||
turn.model_info.input_modalities = vec![];
|
||||
})
|
||||
.await;
|
||||
text_only_model.assert_visible_lacks(&["image_generation"]);
|
||||
|
||||
let unsupported_image_generation_provider = probe(|turn| {
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
|
||||
use_bedrock_provider(turn);
|
||||
use_actor_authorized_provider(turn);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
})
|
||||
.await;
|
||||
unsupported_image_generation_provider.assert_visible_lacks(&["image_generation"]);
|
||||
|
||||
let image_generation = probe(|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
})
|
||||
.await;
|
||||
image_generation.assert_visible_contains(&["image_generation"]);
|
||||
|
||||
let extension_flag_without_imagegen_tool = probe(|turn| {
|
||||
use_chatgpt_auth(turn);
|
||||
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
|
||||
set_feature(turn, Feature::ImageGenExt, /*enabled*/ true);
|
||||
turn.model_info.input_modalities = vec![InputModality::Image];
|
||||
})
|
||||
.await;
|
||||
extension_flag_without_imagegen_tool.assert_visible_contains(&["image_generation"]);
|
||||
extension_flag_without_imagegen_tool.assert_visible_lacks(&["image_gen"]);
|
||||
unsupported_provider.assert_visible_lacks(&["image_gen"]);
|
||||
|
||||
let live_web_search = probe(|turn| {
|
||||
set_web_search_mode(turn, WebSearchMode::Live);
|
||||
@@ -1635,7 +1600,6 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() {
|
||||
MULTI_AGENT_V2_NAMESPACE,
|
||||
// Hosted Responses tools.
|
||||
"web_search",
|
||||
"image_generation",
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1652,7 +1616,10 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() {
|
||||
set_web_search_mode(turn, WebSearchMode::Live);
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![Arc::new(WebRunExtensionTool)],
|
||||
extension_tool_executors: vec![Arc::new(TestNamespaceExtensionTool {
|
||||
namespace: "web",
|
||||
tool_name: "run",
|
||||
})],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -720,8 +720,7 @@ if (!tool) {
|
||||
"exec".to_string(),
|
||||
"wait".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"web_search".to_string(),
|
||||
"image_generation".to_string()
|
||||
"web_search".to_string()
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -75,8 +75,6 @@ async fn extension_tool_receives_turn_environment_sandbox() -> Result<()> {
|
||||
})
|
||||
.with_config(|config| {
|
||||
assert!(config.web_search_mode.set(WebSearchMode::Live).is_ok());
|
||||
assert!(config.features.enable(Feature::ImageGeneration).is_ok());
|
||||
assert!(config.features.disable(Feature::ImageGenExt).is_ok());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let denied_path = test.config.cwd.join("denied.png");
|
||||
@@ -183,8 +181,6 @@ async fn extension_tool_uses_granted_turn_permissions_without_local_persistence(
|
||||
.set_permission_profile(permission_profile_for_config)
|
||||
.expect("set permission profile");
|
||||
assert!(config.web_search_mode.set(WebSearchMode::Live).is_ok());
|
||||
assert!(config.features.enable(Feature::ImageGeneration).is_ok());
|
||||
assert!(config.features.disable(Feature::ImageGenExt).is_ok());
|
||||
assert!(
|
||||
config
|
||||
.features
|
||||
|
||||
@@ -17,11 +17,9 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::user_input::ByteRange;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_image_generation_call;
|
||||
use core_test_support::responses::ev_message_item_added;
|
||||
use core_test_support::responses::ev_output_text_delta;
|
||||
use core_test_support::responses::ev_reasoning_item;
|
||||
@@ -42,8 +40,6 @@ use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn disabled_plan_turn(
|
||||
text: &str,
|
||||
@@ -72,30 +68,6 @@ fn disabled_plan_turn(
|
||||
})
|
||||
}
|
||||
|
||||
fn image_generation_artifact_path(codex_home: &Path, session_id: &str, call_id: &str) -> PathBuf {
|
||||
fn sanitize(value: &str) -> String {
|
||||
let mut sanitized: String = value
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
|
||||
ch
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if sanitized.is_empty() {
|
||||
sanitized = "generated_image".to_string();
|
||||
}
|
||||
sanitized
|
||||
}
|
||||
|
||||
codex_home
|
||||
.join("generated_images")
|
||||
.join(sanitize(session_id))
|
||||
.join(format!("{}.png", sanitize(call_id)))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn user_message_item_is_emitted() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -418,155 +390,6 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn builtin_image_generation_call_persisted() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let TestCodex {
|
||||
codex,
|
||||
config,
|
||||
session_configured,
|
||||
..
|
||||
} = test_codex().build(&server).await?;
|
||||
let call_id = "ig_image_saved_to_temp_dir_default";
|
||||
let expected_saved_path = image_generation_artifact_path(
|
||||
config.codex_home.as_path(),
|
||||
&session_configured.thread_id.to_string(),
|
||||
call_id,
|
||||
);
|
||||
let _ = std::fs::remove_file(&expected_saved_path);
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_image_generation_call(call_id, "generating", "A tiny blue square", "Zm9v"),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
mount_sse_once(&server, first_response).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "generate a tiny blue square".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let started = wait_for_event_match(&codex, |ev| match ev {
|
||||
EventMsg::ItemStarted(ItemStartedEvent {
|
||||
item: TurnItem::ImageGeneration(item),
|
||||
started_at_ms,
|
||||
..
|
||||
}) => Some((item.clone(), *started_at_ms)),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
let begin = wait_for_event_match(&codex, |ev| match ev {
|
||||
EventMsg::ImageGenerationBegin(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
let completed = wait_for_event_match(&codex, |ev| match ev {
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
item: TurnItem::ImageGeneration(item),
|
||||
completed_at_ms,
|
||||
..
|
||||
}) => Some((item.clone(), *completed_at_ms)),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
let end = wait_for_event_match(&codex, |ev| match ev {
|
||||
EventMsg::ImageGenerationEnd(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(begin.call_id, call_id);
|
||||
assert_eq!(started.0.id, call_id);
|
||||
assert!(started.1 > 0);
|
||||
assert_eq!(completed.0.id, call_id);
|
||||
assert!(completed.1 > 0);
|
||||
assert_eq!(end.call_id, call_id);
|
||||
assert_eq!(end.status, "generating");
|
||||
assert_eq!(end.revised_prompt, Some("A tiny blue square".to_string()));
|
||||
assert_eq!(end.result, "Zm9v");
|
||||
assert_eq!(
|
||||
end.saved_path.as_ref().map(AbsolutePathBuf::as_path),
|
||||
Some(expected_saved_path.as_path())
|
||||
);
|
||||
assert_eq!(std::fs::read(&expected_saved_path)?, b"foo");
|
||||
let _ = std::fs::remove_file(&expected_saved_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let TestCodex {
|
||||
codex,
|
||||
config,
|
||||
session_configured,
|
||||
..
|
||||
} = test_codex().build(&server).await?;
|
||||
let expected_saved_path = image_generation_artifact_path(
|
||||
config.codex_home.as_path(),
|
||||
&session_configured.thread_id.to_string(),
|
||||
"ig_invalid",
|
||||
);
|
||||
let _ = std::fs::remove_file(&expected_saved_path);
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_image_generation_call("ig_invalid", "completed", "broken payload", "_-8"),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
mount_sse_once(&server, first_response).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "generate an image".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let begin = wait_for_event_match(&codex, |ev| match ev {
|
||||
EventMsg::ImageGenerationBegin(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
let end = wait_for_event_match(&codex, |ev| match ev {
|
||||
EventMsg::ImageGenerationEnd(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(begin.call_id, "ig_invalid");
|
||||
assert_eq!(end.call_id, "ig_invalid");
|
||||
assert_eq!(end.status, "completed");
|
||||
assert_eq!(end.revised_prompt, Some("broken payload".to_string()));
|
||||
assert_eq!(end.result, "_-8");
|
||||
assert_eq!(end.saved_path, None);
|
||||
assert!(!expected_saved_path.exists());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -201,9 +201,8 @@ async fn remote_tool_mode_selector_overrides_feature_flags() -> Result<()> {
|
||||
codex_code_mode::PUBLIC_TOOL_NAME.to_string(),
|
||||
codex_code_mode::WAIT_TOOL_NAME.to_string(),
|
||||
"request_user_input".to_string(),
|
||||
// Hosted Responses tools.
|
||||
// Hosted Responses tool.
|
||||
"web_search".to_string(),
|
||||
"image_generation".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -31,15 +31,12 @@ use core_test_support::responses::sse;
|
||||
use core_test_support::responses::sse_completed;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::local_selections;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use wiremock::MockServer;
|
||||
|
||||
fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -> Op {
|
||||
@@ -68,30 +65,6 @@ fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -
|
||||
}
|
||||
}
|
||||
|
||||
fn image_generation_artifact_path(codex_home: &Path, session_id: &str, call_id: &str) -> PathBuf {
|
||||
fn sanitize(value: &str) -> String {
|
||||
let mut sanitized: String = value
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
|
||||
ch
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if sanitized.is_empty() {
|
||||
sanitized = "generated_image".to_string();
|
||||
}
|
||||
sanitized
|
||||
}
|
||||
|
||||
codex_home
|
||||
.join("generated_images")
|
||||
.join(sanitize(session_id))
|
||||
.join(format!("{}.png", sanitize(call_id)))
|
||||
}
|
||||
|
||||
fn test_model_info(
|
||||
slug: &str,
|
||||
display_name: &str,
|
||||
@@ -645,12 +618,6 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
|
||||
config.model = Some(image_model_slug.to_string());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let saved_path = image_generation_artifact_path(
|
||||
test.codex_home_path(),
|
||||
&test.session_configured.thread_id.to_string(),
|
||||
"ig_123",
|
||||
);
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
let models_manager = test.thread_manager.get_models_manager();
|
||||
let _ = models_manager
|
||||
.list_models(
|
||||
@@ -703,15 +670,6 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
|
||||
Some("Zm9v"),
|
||||
"expected the original generated image payload to be preserved"
|
||||
);
|
||||
assert!(
|
||||
second_request
|
||||
.message_input_texts("developer")
|
||||
.iter()
|
||||
.any(|text| text.contains("Generated images are saved to")),
|
||||
"second request should include the saved-path note in model-visible history"
|
||||
);
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -762,12 +720,6 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
|
||||
config.model = Some(image_model_slug.to_string());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let saved_path = image_generation_artifact_path(
|
||||
test.codex_home_path(),
|
||||
&test.session_configured.thread_id.to_string(),
|
||||
"ig_123",
|
||||
);
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
let models_manager = test.thread_manager.get_models_manager();
|
||||
let _ = models_manager
|
||||
.list_models(
|
||||
@@ -830,22 +782,11 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
|
||||
.all(|text| text != "image content omitted because you do not support image input"),
|
||||
"second request should not inject the image-omitted placeholder text"
|
||||
);
|
||||
assert!(
|
||||
second_request
|
||||
.message_input_texts("developer")
|
||||
.iter()
|
||||
.any(|text| text.contains("Generated images are saved to")),
|
||||
"second request should include the saved-path note in model-visible history"
|
||||
);
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() -> Result<()> {
|
||||
// TODO(anp): Remove after generated-image artifacts use target-native paths.
|
||||
skip_if_wine_exec!(Ok(()), "uses host-native generated-image artifact paths");
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = MockServer::start().await;
|
||||
@@ -883,12 +824,6 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
|
||||
config.model = Some(image_model_slug.to_string());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let saved_path = image_generation_artifact_path(
|
||||
test.codex_home_path(),
|
||||
&test.session_configured.thread_id.to_string(),
|
||||
"ig_rollback",
|
||||
);
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
let models_manager = test.thread_manager.get_models_manager();
|
||||
let _ = models_manager
|
||||
.list_models(
|
||||
@@ -940,21 +875,12 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
|
||||
.any(|text| text == "generate a lobster"),
|
||||
"rollback should remove the rolled-back image-generation user turn"
|
||||
);
|
||||
assert!(
|
||||
!second_request
|
||||
.message_input_texts("developer")
|
||||
.iter()
|
||||
.any(|text| text.contains("Generated images are saved to")),
|
||||
"rollback should remove the generated-image save note with the rolled-back turn"
|
||||
);
|
||||
assert!(
|
||||
second_request
|
||||
.inputs_of_type("image_generation_call")
|
||||
.is_empty(),
|
||||
"rollback should remove the generated image call with the rolled-back turn"
|
||||
);
|
||||
let _ = std::fs::remove_file(&saved_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ fn configure_responses_tools(config: &mut Config) {
|
||||
.disable(Feature::StandaloneWebSearch)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(config.features.enable(Feature::ImageGeneration).is_ok());
|
||||
assert!(config.features.disable(Feature::ImageGenExt).is_ok());
|
||||
}
|
||||
|
||||
fn configure_image_capable_model(model_info: &mut codex_protocol::openai_models::ModelInfo) {
|
||||
@@ -382,7 +380,7 @@ async fn responses_lite_omits_hosted_tools_without_standalone_extensions() -> Re
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn non_lite_uses_hosted_tools_when_standalone_features_are_disabled() -> Result<()> {
|
||||
async fn non_lite_uses_standalone_image_generation_by_default() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -404,18 +402,18 @@ async fn non_lite_uses_hosted_tools_when_standalone_features_are_disabled() -> R
|
||||
.with_config(configure_responses_tools);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("Use hosted tools").await?;
|
||||
test.submit_turn("Use image generation").await?;
|
||||
|
||||
let request = response_mock.single_request();
|
||||
assert_eq!(request.header(RESPONSES_LITE_HEADER), None);
|
||||
assert!(request.tool_by_name("web", "run").is_none());
|
||||
assert!(request.tool_by_name("image_gen", "imagegen").is_none());
|
||||
assert!(request.tool_by_name("image_gen", "imagegen").is_some());
|
||||
let body = request.body_json();
|
||||
let tools = body["tools"]
|
||||
.as_array()
|
||||
.context("Responses request tools should be an array")?;
|
||||
assert!(has_hosted_tool(tools, "web_search"));
|
||||
assert!(has_hosted_tool(tools, "image_generation"));
|
||||
assert!(!has_hosted_tool(tools, "image_generation"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ impl ImageGenerationExtensionConfig {
|
||||
fn from_config(config: &Config, resolve_save_root: &SaveRootResolver) -> Self {
|
||||
Self {
|
||||
available: config.model_provider.is_openai()
|
||||
|| config.model_provider.requires_openai_auth
|
||||
|| config.model_provider.uses_openai_actor_authorization(),
|
||||
provider: config.model_provider.clone(),
|
||||
save_root: resolve_save_root(config),
|
||||
|
||||
@@ -29,6 +29,10 @@ const ALIASES: &[Alias] = &[
|
||||
legacy_key: "web_search",
|
||||
feature: Feature::WebSearchRequest,
|
||||
},
|
||||
Alias {
|
||||
legacy_key: "imagegenext",
|
||||
feature: Feature::ImageGeneration,
|
||||
},
|
||||
Alias {
|
||||
legacy_key: "collab",
|
||||
feature: Feature::Collab,
|
||||
|
||||
@@ -196,10 +196,8 @@ pub enum Feature {
|
||||
PluginSharing,
|
||||
/// Removed compatibility flag retained as a no-op.
|
||||
ExternalMigration,
|
||||
/// Allow the model to invoke the built-in image generation tool.
|
||||
/// Enable extension-backed image generation.
|
||||
ImageGeneration,
|
||||
/// Replace hosted image generation with the standalone image-generation extension.
|
||||
ImageGenExt,
|
||||
/// Removed compatibility flag for always-on centralized image preparation.
|
||||
ResizeAllImages,
|
||||
/// Generate Responses API item IDs for client-created history items.
|
||||
@@ -497,6 +495,10 @@ impl Features {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if k == "imagegenext" && m.contains_key(Feature::ImageGeneration.key()) {
|
||||
self.record_legacy_usage(k, Feature::ImageGeneration);
|
||||
continue;
|
||||
}
|
||||
match feature_for_key(k) {
|
||||
Some(feat) => {
|
||||
if matches!(feat, Feature::TuiAppServer) {
|
||||
@@ -1168,12 +1170,6 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ImageGenExt,
|
||||
key: "imagegenext",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ResizeAllImages,
|
||||
key: "resize_all_images",
|
||||
|
||||
@@ -263,16 +263,44 @@ fn use_linux_sandbox_bwrap_is_a_removed_feature_key() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_generation_is_stable_and_enabled_by_default() {
|
||||
fn image_generation_is_stable_and_extension_alias_is_supported() {
|
||||
assert_eq!(Feature::ImageGeneration.stage(), Stage::Stable);
|
||||
assert_eq!(Feature::ImageGeneration.default_enabled(), true);
|
||||
assert_eq!(
|
||||
feature_for_key("image_generation"),
|
||||
Some(Feature::ImageGeneration)
|
||||
);
|
||||
assert_eq!(
|
||||
feature_for_key("imagegenext"),
|
||||
Some(Feature::ImageGeneration)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_generation_extension_is_under_development_and_disabled_by_default() {
|
||||
assert_eq!(Feature::ImageGenExt.stage(), Stage::UnderDevelopment);
|
||||
assert_eq!(Feature::ImageGenExt.default_enabled(), false);
|
||||
assert_eq!(feature_for_key("imagegenext"), Some(Feature::ImageGenExt));
|
||||
fn image_generation_toggle_controls_extension_backed_generation() {
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert("image_generation".to_string(), false);
|
||||
let mut features = Features::with_defaults();
|
||||
features.apply_map(&entries);
|
||||
assert!(!features.enabled(Feature::ImageGeneration));
|
||||
|
||||
entries.insert("image_generation".to_string(), true);
|
||||
features.disable(Feature::ImageGeneration);
|
||||
features.apply_map(&entries);
|
||||
assert!(features.enabled(Feature::ImageGeneration));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_image_generation_toggle_wins_over_extension_alias() {
|
||||
for (canonical, alias) in [(false, true), (true, false)] {
|
||||
let entries = BTreeMap::from([
|
||||
("image_generation".to_string(), canonical),
|
||||
("imagegenext".to_string(), alias),
|
||||
]);
|
||||
let mut features = Features::with_defaults();
|
||||
features.apply_map(&entries);
|
||||
assert_eq!(features.enabled(Feature::ImageGeneration), canonical);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,6 +22,7 @@ codex-arg0 = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-image-generation-extension = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
|
||||
@@ -6,7 +6,7 @@ use codex_core::StateDbHandle;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_home::CodexHomeUserInstructionsProvider;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::default_client::USER_AGENT_SUFFIX;
|
||||
@@ -66,12 +66,18 @@ impl MessageProcessor {
|
||||
let user_instructions_provider = Arc::new(CodexHomeUserInstructionsProvider::new(
|
||||
config.codex_home.clone(),
|
||||
));
|
||||
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
|
||||
codex_image_generation_extension::install(
|
||||
&mut extensions,
|
||||
auth_manager.clone(),
|
||||
|config: &Config| Some(config.codex_home.clone()),
|
||||
);
|
||||
let thread_manager = Arc::new(ThreadManager::new(
|
||||
config.as_ref(),
|
||||
auth_manager,
|
||||
SessionSource::Mcp,
|
||||
environment_manager,
|
||||
empty_extension_registry(),
|
||||
Arc::new(extensions.build()),
|
||||
user_instructions_provider,
|
||||
/*analytics_events_client*/ None,
|
||||
codex_core::thread_store_from_config(config.as_ref(), state_db.clone()),
|
||||
|
||||
@@ -264,7 +264,7 @@ fn dispatched_tool_kind(tool_name: &str, _payload: &ToolDispatchPayload) -> Tool
|
||||
"write_stdin" => ToolCallKind::WriteStdin,
|
||||
"apply_patch" => ToolCallKind::ApplyPatch,
|
||||
"web_search" | "web_search_preview" => ToolCallKind::Web,
|
||||
"image_generation" | "image_query" => ToolCallKind::ImageGeneration,
|
||||
"image_generation" | "image_query" | "imagegen" => ToolCallKind::ImageGeneration,
|
||||
"spawn_agent" => ToolCallKind::SpawnAgent,
|
||||
"send_message" => ToolCallKind::SendMessage,
|
||||
"followup_task" | "assign_task" => ToolCallKind::AssignAgentTask,
|
||||
@@ -438,6 +438,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_imagegen_as_image_generation() {
|
||||
assert_eq!(
|
||||
dispatched_tool_kind(
|
||||
"imagegen",
|
||||
&ToolDispatchPayload::Function {
|
||||
arguments: String::new(),
|
||||
},
|
||||
),
|
||||
ToolCallKind::ImageGeneration
|
||||
);
|
||||
}
|
||||
|
||||
fn invocation(
|
||||
tool_name: &str,
|
||||
tool_namespace: Option<String>,
|
||||
|
||||
@@ -24,6 +24,7 @@ use codex_core_api::Constrained;
|
||||
use codex_core_api::EnvironmentManager;
|
||||
use codex_core_api::EventMsg;
|
||||
use codex_core_api::ExecServerRuntimePaths;
|
||||
use codex_core_api::ExtensionRegistryBuilder;
|
||||
use codex_core_api::Features;
|
||||
use codex_core_api::GhostSnapshotConfig;
|
||||
use codex_core_api::History;
|
||||
@@ -55,9 +56,9 @@ use codex_core_api::UserInput;
|
||||
use codex_core_api::WebSearchMode;
|
||||
use codex_core_api::arg0_dispatch_or_else;
|
||||
use codex_core_api::built_in_model_providers;
|
||||
use codex_core_api::empty_extension_registry;
|
||||
use codex_core_api::find_codex_home;
|
||||
use codex_core_api::init_state_db;
|
||||
use codex_core_api::install_image_generation_extension;
|
||||
use codex_core_api::item_event_to_server_notification;
|
||||
use codex_core_api::local_agent_graph_store_from_state_db;
|
||||
use codex_core_api::resolve_installation_id;
|
||||
@@ -125,12 +126,16 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
let user_instructions_provider = Arc::new(CodexHomeUserInstructionsProvider::new(
|
||||
config.codex_home.clone(),
|
||||
));
|
||||
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
|
||||
install_image_generation_extension(&mut extensions, auth_manager.clone(), |config: &Config| {
|
||||
Some(config.codex_home.clone())
|
||||
});
|
||||
let thread_manager = ThreadManager::new(
|
||||
&config,
|
||||
auth_manager,
|
||||
SessionSource::Exec,
|
||||
environment_manager,
|
||||
empty_extension_registry(),
|
||||
Arc::new(extensions.build()),
|
||||
user_instructions_provider,
|
||||
/*analytics_events_client*/ None,
|
||||
Arc::clone(&thread_store),
|
||||
|
||||
@@ -148,9 +148,7 @@ fn code_mode_tool_definitions_for_spec(spec: &ToolSpec) -> Vec<CodeModeToolDefin
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
ToolSpec::ImageGeneration { .. }
|
||||
| ToolSpec::ToolSearch { .. }
|
||||
| ToolSpec::WebSearch { .. } => Vec::new(),
|
||||
ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,10 +49,9 @@ impl ToolSearchInfo {
|
||||
}
|
||||
LoadableToolSpec::Namespace(namespace)
|
||||
}
|
||||
ToolSpec::ToolSearch { .. }
|
||||
| ToolSpec::ImageGeneration { .. }
|
||||
| ToolSpec::WebSearch { .. }
|
||||
| ToolSpec::Freeform(_) => return None,
|
||||
ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } | ToolSpec::Freeform(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
@@ -81,9 +80,6 @@ fn default_tool_search_text(spec: &ToolSpec) -> String {
|
||||
ToolSpec::ToolSearch { description, .. } => {
|
||||
push_search_part(&mut parts, description.clone());
|
||||
}
|
||||
ToolSpec::ImageGeneration { .. } => {
|
||||
push_search_part(&mut parts, "image generation".to_string());
|
||||
}
|
||||
ToolSpec::WebSearch { .. } => {
|
||||
push_search_part(&mut parts, "web search".to_string());
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ pub enum ToolSpec {
|
||||
description: String,
|
||||
parameters: JsonSchema,
|
||||
},
|
||||
#[serde(rename = "image_generation")]
|
||||
ImageGeneration { output_format: String },
|
||||
// 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
|
||||
@@ -58,7 +56,6 @@ impl ToolSpec {
|
||||
ToolSpec::Function(tool) => tool.name.as_str(),
|
||||
ToolSpec::Namespace(namespace) => namespace.name.as_str(),
|
||||
ToolSpec::ToolSearch { .. } => "tool_search",
|
||||
ToolSpec::ImageGeneration { .. } => "image_generation",
|
||||
ToolSpec::WebSearch { .. } => "web_search",
|
||||
ToolSpec::Freeform(tool) => tool.name.as_str(),
|
||||
}
|
||||
|
||||
@@ -57,13 +57,6 @@ fn tool_spec_name_covers_all_variants() {
|
||||
.name(),
|
||||
"tool_search"
|
||||
);
|
||||
assert_eq!(
|
||||
ToolSpec::ImageGeneration {
|
||||
output_format: "png".to_string(),
|
||||
}
|
||||
.name(),
|
||||
"image_generation"
|
||||
);
|
||||
assert_eq!(
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(true),
|
||||
|
||||
Reference in New Issue
Block a user