mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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`
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -56,6 +57,18 @@ fn has_hosted_tool(tools: &[Value], tool_type: &str) -> bool {
|
||||
.any(|tool| tool.get("type").and_then(Value::as_str) == Some(tool_type))
|
||||
}
|
||||
|
||||
fn has_namespaced_tool(tools: &[Value], namespace: &str, tool_name: &str) -> bool {
|
||||
tools.iter().any(|tool| {
|
||||
tool.get("type").and_then(Value::as_str) == Some("namespace")
|
||||
&& tool.get("name").and_then(Value::as_str) == Some(namespace)
|
||||
&& tool["tools"].as_array().is_some_and(|tools| {
|
||||
tools
|
||||
.iter()
|
||||
.any(|tool| tool.get("name").and_then(Value::as_str) == Some(tool_name))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn additional_tools(body: &Value) -> Result<&[Value]> {
|
||||
body["input"]
|
||||
.as_array()
|
||||
@@ -227,13 +240,58 @@ async fn responses_lite_uses_standalone_web_search_and_image_generation() -> Res
|
||||
let body = request.body_json();
|
||||
assert!(body.get("tools").is_none());
|
||||
let tools = additional_tools(&body)?;
|
||||
assert!(!tools.is_empty());
|
||||
assert!(has_namespaced_tool(tools, "web", "run"));
|
||||
assert!(has_namespaced_tool(tools, "image_gen", "imagegen"));
|
||||
assert!(!has_hosted_tool(tools, "web_search"));
|
||||
assert!(!has_hosted_tool(tools, "image_generation"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_lite_exposes_standalone_tools_for_actor_authorized_provider() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let auth = CodexAuth::from_api_key("dummy");
|
||||
let extensions = responses_extensions(&auth);
|
||||
let mut builder = test_codex()
|
||||
.with_auth(auth)
|
||||
.with_extensions(extensions)
|
||||
.with_model_info_override("gpt-5.4", |model_info| {
|
||||
model_info.use_responses_lite = true;
|
||||
configure_image_capable_model(model_info);
|
||||
})
|
||||
.with_config(|config| {
|
||||
configure_responses_tools(config);
|
||||
config.model_provider.name = "local".to_string();
|
||||
config.model_provider.requires_openai_auth = false;
|
||||
config.model_provider.http_headers = Some(HashMap::from([(
|
||||
"x-openai-actor-authorization".to_string(),
|
||||
"test-actor-authorization".to_string(),
|
||||
)]));
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("Use standalone tools").await?;
|
||||
|
||||
let body = response_mock.single_request().body_json();
|
||||
let tools = additional_tools(&body)?;
|
||||
assert!(has_namespaced_tool(tools, "web", "run"));
|
||||
assert!(has_namespaced_tool(tools, "image_gen", "imagegen"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_lite_compact_request_uses_lite_transport_contract() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user