[codex] allow image generation with provider auth (#29513)

## Summary

- allow the native Responses API `image_generation` tool when the active
provider carries CCA's non-empty `x-openai-actor-authorization` header
- preserve the Codex-managed ChatGPT auth path, scoped to providers that
actually require OpenAI auth
- keep generic custom providers excluded, including when unrelated
ChatGPT credentials are cached
- retain the existing feature, provider-capability, and
image-input-modality gates

## Why

CCA authenticates its inference requests through the active provider's
`x-openai-actor-authorization` and `ChatGPT-Account-ID` headers, so it
does not have a Codex-managed login session. The previous gate therefore
hid the native hosted image-generation tool despite an authenticated
codex-backend path.

This change is intentionally limited to the native hosted tool. It adds
no extension, MCP, plugin-service, session-source, token plumbing, or
new provider configuration surface.

## Tests

- `cargo test -p codex-core
hosted_tools_follow_provider_auth_model_and_config_gates`
- `cargo fmt --all -- --check`
- `git diff --check origin/main`
This commit is contained in:
richardopenai
2026-06-23 12:40:54 -07:00
committed by GitHub
Unverified
parent 18fe1d9fe3
commit c660e2b644
2 changed files with 145 additions and 4 deletions
+17 -4
View File
@@ -96,6 +96,7 @@ use tracing::warn;
const MULTI_AGENT_V2_NAMESPACE_DESCRIPTION: &str = "Tools for spawning and managing sub-agents.";
const IMAGE_GEN_NAMESPACE: &str = "image_gen";
const IMAGEGEN_TOOL_NAME: &str = "imagegen";
const ACTOR_AUTHORIZATION_HEADER: &str = "x-openai-actor-authorization";
type PlannedRuntime = Arc<dyn CoreToolRuntime>;
@@ -383,10 +384,12 @@ fn image_generation_tool_enabled(turn_context: &TurnContext) -> bool {
}
fn image_generation_runtime_enabled(turn_context: &TurnContext) -> bool {
turn_context
.auth_manager
.as_deref()
.is_some_and(AuthManager::current_auth_uses_codex_backend)
(provider_uses_actor_authorization(turn_context)
|| (turn_context.provider.info().requires_openai_auth
&& turn_context
.auth_manager
.as_deref()
.is_some_and(AuthManager::current_auth_uses_codex_backend)))
&& turn_context.provider.capabilities().image_generation
&& turn_context
.model_info
@@ -394,6 +397,16 @@ fn image_generation_runtime_enabled(turn_context: &TurnContext) -> bool {
.contains(&InputModality::Image)
}
fn provider_uses_actor_authorization(turn_context: &TurnContext) -> bool {
let provider_info = turn_context.provider.info();
!provider_info.requires_openai_auth
&& provider_info.http_headers.as_ref().is_some_and(|headers| {
headers.iter().any(|(name, value)| {
name.eq_ignore_ascii_case(ACTOR_AUTHORIZATION_HEADER) && !value.trim().is_empty()
})
})
}
fn standalone_image_generation_model_visible(turn_context: &TurnContext) -> bool {
if !image_generation_runtime_enabled(turn_context) || !namespace_tools_enabled(turn_context) {
return false;
+128
View File
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
use codex_features::Feature;
@@ -270,6 +271,29 @@ fn use_bedrock_provider(turn: &mut TurnContext) {
turn.provider = create_model_provider(provider_info, turn.auth_manager.clone());
}
fn use_provider_auth(
turn: &mut TurnContext,
requires_openai_auth: bool,
actor_header: Option<(&str, &str)>,
) {
let mut provider_info = turn.config.model_provider.clone();
provider_info.requires_openai_auth = requires_openai_auth;
provider_info.http_headers = actor_header.map(|(name, value)| {
HashMap::from([
(name.to_string(), value.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 WebRunExtensionTool;
impl ToolExecutor<ExtensionToolCall> for WebRunExtensionTool {
@@ -1455,6 +1479,110 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() {
.await;
api_key_auth.assert_visible_lacks(&["image_generation"]);
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];
})
.await;
unrelated_chatgpt_auth.assert_visible_lacks(&["image_generation"]);
let provider_without_actor_auth = probe(|turn| {
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
use_provider_auth(
turn, /*requires_openai_auth*/ false, /*actor_header*/ None,
);
turn.model_info.input_modalities = vec![InputModality::Image];
})
.await;
provider_without_actor_auth.assert_visible_lacks(&["image_generation"]);
let provider_authenticated = probe(|turn| {
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
use_provider_auth(
turn,
/*requires_openai_auth*/ false,
Some(("X-OpenAI-Actor-Authorization", "test-actor-authorization")),
);
turn.model_info.input_modalities = vec![InputModality::Image];
})
.await;
provider_authenticated.assert_visible_contains(&["image_generation"]);
let empty_actor_auth = probe(|turn| {
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
use_provider_auth(
turn,
/*requires_openai_auth*/ false,
Some(("x-openai-actor-authorization", " ")),
);
turn.model_info.input_modalities = vec![InputModality::Image];
})
.await;
empty_actor_auth.assert_visible_lacks(&["image_generation"]);
let feature_disabled = probe(|turn| {
set_feature(turn, Feature::ImageGeneration, /*enabled*/ false);
use_provider_auth(
turn,
/*requires_openai_auth*/ false,
Some(("x-openai-actor-authorization", "test-actor-authorization")),
);
turn.model_info.input_modalities = vec![InputModality::Image];
})
.await;
feature_disabled.assert_visible_lacks(&["image_generation"]);
let text_only_model = probe(|turn| {
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
use_provider_auth(
turn,
/*requires_openai_auth*/ false,
Some(("x-openai-actor-authorization", "test-actor-authorization")),
);
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);
let mut provider_info = turn.provider.info().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(),
)]));
turn.auth_manager = None;
update_config(turn, |config| {
config.model_provider = provider_info.clone();
});
turn.provider = create_model_provider(provider_info, /*auth_manager*/ None);
turn.model_info.input_modalities = vec![InputModality::Image];
})
.await;
unsupported_image_generation_provider.assert_visible_lacks(&["image_generation"]);
let codex_managed_auth_provider = probe(|turn| {
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);
use_provider_auth(
turn,
/*requires_openai_auth*/ true,
Some(("x-openai-actor-authorization", "test-actor-authorization")),
);
turn.model_info.input_modalities = vec![InputModality::Image];
})
.await;
codex_managed_auth_provider.assert_visible_lacks(&["image_generation"]);
let image_generation = probe(|turn| {
use_chatgpt_auth(turn);
set_feature(turn, Feature::ImageGeneration, /*enabled*/ true);