Files
codex/codex-rs/ext/image-generation/src/extension.rs
T
Won Park 0d4351c1b8 [codex] allow CCA image generation and web search extensions (#29909)
## Summary

- allow the standalone image-generation and web-search extensions for
the actor-authorized provider shape used by CCA
- preserve builtin `image_generation` and `web_search` for older models
and existing flows
- keep ordinary non-OpenAI providers excluded from both extensions
- remove only the image extension local managed-AuthManager requirement
that CCA cannot satisfy
- share actor-authorization detection through `ModelProviderInfo`
- keep Core tests focused on routing behavior and cover header-shape
edge cases in `model-provider-info`
- add a Responses Lite regression that verifies both
`image_gen.imagegen` and `web.run`

## Why

CCA uses a provider named `local` with `requires_openai_auth: false` and
a non-empty `x-openai-actor-authorization` header. Core accepts that
provider shape, but both extension provider-name gates rejected it;
image generation additionally required a Codex-managed login.

The standalone paths must coexist with existing builtin tools. New
Responses Lite models can receive `image_gen.imagegen` and `web.run`,
while older models continue using builtin tools.

## Impact

This enables both standalone extensions for CCA once installed
downstream, without removing or changing builtin-tool compatibility for
older models.

## Validation

- `just test -p codex-core
responses_lite_exposes_standalone_tools_for_actor_authorized_provider`
- `just test -p codex-core
responses_lite_uses_standalone_web_search_and_image_generation`
- `just test -p codex-core
hosted_tools_follow_provider_auth_model_and_config_gates`
- `just test -p codex-image-generation-extension`
- `just test -p codex-web-search-extension`
- `just test -p codex-model-provider-info`
- `just fmt`
- `git diff --check`
2026-06-25 18:34:35 -07:00

120 lines
3.9 KiB
Rust

use std::sync::Arc;
use codex_core::config::Config;
use codex_extension_api::ConfigContributor;
use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionFuture;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::ThreadLifecycleContributor;
use codex_extension_api::ThreadStartInput;
use codex_extension_api::ToolCall;
use codex_extension_api::ToolContributor;
use codex_extension_api::ToolExecutor;
use codex_login::AuthManager;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::backend::CodexImagesBackend;
use crate::tool::ImageGenerationTool;
#[derive(Clone)]
struct ImageGenerationExtension {
auth_manager: Arc<AuthManager>,
resolve_save_root: Arc<SaveRootResolver>,
}
type SaveRootResolver = dyn Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync;
#[derive(Clone)]
struct ImageGenerationExtensionConfig {
available: bool,
provider: ModelProviderInfo,
save_root: Option<AbsolutePathBuf>,
}
impl ImageGenerationExtensionConfig {
/// Resolves the image provider and save root for a thread.
fn from_config(config: &Config, resolve_save_root: &SaveRootResolver) -> Self {
Self {
available: config.model_provider.is_openai()
|| config.model_provider.uses_openai_actor_authorization(),
provider: config.model_provider.clone(),
save_root: resolve_save_root(config),
}
}
}
impl ThreadLifecycleContributor<Config> for ImageGenerationExtension {
/// Seeds image-generation configuration when a thread begins.
fn on_thread_start<'a>(
&'a self,
input: ThreadStartInput<'a, Config>,
) -> ExtensionFuture<'a, ()> {
Box::pin(async move {
input
.thread_store
.insert(ImageGenerationExtensionConfig::from_config(
input.config,
self.resolve_save_root.as_ref(),
));
})
}
}
impl ConfigContributor<Config> for ImageGenerationExtension {
/// Refreshes image-generation configuration after thread configuration changes.
fn on_config_changed(
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
_previous_config: &Config,
new_config: &Config,
) {
thread_store.insert(ImageGenerationExtensionConfig::from_config(
new_config,
self.resolve_save_root.as_ref(),
));
}
}
impl ToolContributor for ImageGenerationExtension {
/// Creates the image-generation tool exposed by this installed extension.
fn tools(
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
let Some(config) = thread_store.get::<ImageGenerationExtensionConfig>() else {
return Vec::new();
};
if !config.available {
return Vec::new();
}
vec![Arc::new(ImageGenerationTool::new(
CodexImagesBackend::new(create_model_provider(
config.provider.clone(),
Some(self.auth_manager.clone()),
)),
config.save_root.clone(),
thread_store.level_id().to_string(),
))]
}
}
/// Installs the standalone image-generation extension contributors.
pub fn install(
registry: &mut ExtensionRegistryBuilder<Config>,
auth_manager: Arc<AuthManager>,
resolve_save_root: impl Fn(&Config) -> Option<AbsolutePathBuf> + Send + Sync + 'static,
) {
let extension = Arc::new(ImageGenerationExtension {
auth_manager,
resolve_save_root: Arc::new(resolve_save_root),
});
registry.thread_lifecycle_contributor(extension.clone());
registry.config_contributor(extension.clone());
registry.tool_contributor(extension);
}