codex_image_generation_extension/
extension.rs1use std::sync::Arc;
2
3use codex_core::config::Config;
4use codex_extension_api::ConfigContributor;
5use codex_extension_api::ExtensionData;
6use codex_extension_api::ExtensionFuture;
7use codex_extension_api::ExtensionRegistryBuilder;
8use codex_extension_api::ThreadLifecycleContributor;
9use codex_extension_api::ThreadOriginator;
10use codex_extension_api::ThreadStartInput;
11use codex_extension_api::ToolCall;
12use codex_extension_api::ToolContributor;
13use codex_extension_api::ToolExecutor;
14use codex_login::AuthManager;
15use codex_model_provider::create_model_provider;
16use codex_model_provider_info::ModelProviderInfo;
17use codex_utils_absolute_path::AbsolutePathBuf;
18
19use crate::backend::CodexImagesBackend;
20use crate::tool::ImageGenerationTool;
21
22#[derive(Clone)]
23struct ImageGenerationExtension {
24 auth_manager: Arc<AuthManager>,
25 resolve_save_root: Arc<SaveRootResolver>,
26}
27
28type SaveRootResolver = dyn Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync;
29
30#[derive(Clone)]
31struct ImageGenerationExtensionConfig {
32 available: bool,
33 provider: ModelProviderInfo,
34 save_root: Option<AbsolutePathBuf>,
35}
36
37impl ImageGenerationExtensionConfig {
38 fn from_config(config: &Config, resolve_save_root: &SaveRootResolver) -> Self {
40 Self {
41 available: config.model_provider.is_openai()
42 || config.model_provider.requires_openai_auth
43 || config.model_provider.uses_openai_actor_authorization(),
44 provider: config.model_provider.clone(),
45 save_root: resolve_save_root(config),
46 }
47 }
48}
49
50impl ThreadLifecycleContributor<Config> for ImageGenerationExtension {
51 fn on_thread_start<'a>(
53 &'a self,
54 input: ThreadStartInput<'a, Config>,
55 ) -> ExtensionFuture<'a, ()> {
56 Box::pin(async move {
57 input
58 .thread_store
59 .insert(ImageGenerationExtensionConfig::from_config(
60 input.config,
61 self.resolve_save_root.as_ref(),
62 ));
63 })
64 }
65}
66
67impl ConfigContributor<Config> for ImageGenerationExtension {
68 fn on_config_changed(
70 &self,
71 _session_store: &ExtensionData,
72 thread_store: &ExtensionData,
73 _previous_config: &Config,
74 new_config: &Config,
75 ) {
76 thread_store.insert(ImageGenerationExtensionConfig::from_config(
77 new_config,
78 self.resolve_save_root.as_ref(),
79 ));
80 }
81}
82
83impl ToolContributor for ImageGenerationExtension {
84 fn tools(
86 &self,
87 _session_store: &ExtensionData,
88 thread_store: &ExtensionData,
89 _step_store: &ExtensionData,
90 ) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
91 let Some(config) = thread_store.get::<ImageGenerationExtensionConfig>() else {
92 return Vec::new();
93 };
94 if !config.available {
95 return Vec::new();
96 }
97
98 vec![Arc::new(ImageGenerationTool::new(
99 CodexImagesBackend::new(
100 create_model_provider(config.provider.clone(), Some(self.auth_manager.clone())),
101 thread_store
102 .get::<ThreadOriginator>()
103 .map(|originator| originator.0.clone()),
104 ),
105 config.save_root.clone(),
106 thread_store.level_id().to_string(),
107 ))]
108 }
109}
110
111pub fn install(
113 registry: &mut ExtensionRegistryBuilder<Config>,
114 auth_manager: Arc<AuthManager>,
115 resolve_save_root: impl Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync + 'static,
116) {
117 let extension = Arc::new(ImageGenerationExtension {
118 auth_manager,
119 resolve_save_root: Arc::new(resolve_save_root),
120 });
121 registry.thread_lifecycle_contributor(extension.clone());
122 registry.config_contributor(extension.clone());
123 registry.tool_contributor(extension);
124}