Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ mod thread_processor_behavior_tests {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: true,
supports_standalone_web_search: false,
};
let config_manager = ConfigManager::new(
temp_dir.path().to_path_buf(),
Expand Down
105 changes: 85 additions & 20 deletions codex-rs/app-server/tests/suite/v2/web_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);

#[tokio::test]
async fn standalone_web_search_round_trips_output() -> Result<()> {
assert_standalone_web_search_round_trips_output(WebSearchProvider::ChatGpt).await
}

#[tokio::test]
async fn standalone_web_search_round_trips_output_for_custom_provider() -> Result<()> {
assert_standalone_web_search_round_trips_output(WebSearchProvider::CustomResponses).await
}

#[derive(Clone, Copy)]
enum WebSearchProvider {
ChatGpt,
CustomResponses,
}

async fn assert_standalone_web_search_round_trips_output(
provider: WebSearchProvider,
) -> Result<()> {
let call_id = "web-run-1";
let expected_model_id = "model-id-from-search-context";
let search_context = json!({
Expand All @@ -57,7 +74,11 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
json!({ "openai/search_context": search_context }).to_string(),
)]);
let server = responses::start_mock_server().await;
mount_search_response(&server).await;
let search_path = match provider {
WebSearchProvider::ChatGpt => "/api/codex/alpha/search",
WebSearchProvider::CustomResponses => "/v1/alpha/search",
};
mount_search_response(&server, search_path).await;

let response_mock = responses::mount_sse_sequence(
&server,
Expand All @@ -84,24 +105,47 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
.await;

let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.with_model_provider("openai-custom")
.with_provider_name("OpenAI")
.with_provider_base_url(&format!("{}/api/codex", server.uri()))
let config = MockResponsesConfig::new(&server.uri())
.with_root_config(&format!("chatgpt_base_url = \"{}\"", server.uri()))
.enable_feature(Feature::StandaloneWebSearch)
.with_provider_config("supports_websockets = false")
.with_provider_config("requires_openai_auth = true")
.write(codex_home.path())?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("access-chatgpt"),
AuthCredentialsStoreMode::File,
)?;
.with_provider_config("supports_websockets = false");
let config = match provider {
WebSearchProvider::ChatGpt => config
.with_model_provider("openai-custom")
.with_provider_name("OpenAI")
.with_provider_base_url(&format!("{}/api/codex", server.uri()))
.with_provider_config("requires_openai_auth = true"),
WebSearchProvider::CustomResponses => config
.with_model_provider("custom-responses")
.with_provider_name("Custom Responses")
.with_provider_base_url(&format!("{}/v1", server.uri()))
.with_provider_config("env_key = \"CUSTOM_RESPONSES_API_KEY\"")
.with_provider_config("supports_standalone_web_search = true")
.with_provider_config("requires_openai_auth = false"),
};
config.write(codex_home.path())?;

if matches!(provider, WebSearchProvider::ChatGpt) {
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("access-chatgpt"),
AuthCredentialsStoreMode::File,
)?;
}

let env_overrides = match provider {
WebSearchProvider::ChatGpt => {
vec![("OPENAI_API_KEY", None), ("CUSTOM_RESPONSES_API_KEY", None)]
}
WebSearchProvider::CustomResponses => vec![
("OPENAI_API_KEY", None),
("CUSTOM_RESPONSES_API_KEY", Some("test-api-key")),
],
};

let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.with_env_overrides(&[("OPENAI_API_KEY", None)])
.with_env_overrides(&env_overrides)
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
.await?;

Expand Down Expand Up @@ -159,7 +203,28 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
"standalone web search should replace hosted web search"
);

let search_request = search_request(&server).await?;
let search_request = search_request(&server, search_path).await?;
let expected_authorization = match provider {
WebSearchProvider::ChatGpt => "Bearer access-chatgpt",
WebSearchProvider::CustomResponses => "Bearer test-api-key",
};
assert_eq!(
search_request
.headers
.get("authorization")
.context("standalone search should include provider authorization")?
.to_str()
.context("standalone search authorization should be valid ASCII")?,
expected_authorization
);
if matches!(provider, WebSearchProvider::CustomResponses) {
assert!(
search_request
.headers
.get("x-openai-actor-authorization")
.is_none()
);
}
assert_eq!(
search_request
.headers
Expand Down Expand Up @@ -268,7 +333,7 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
drop(mcp);
let mut reloaded_mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.with_env_overrides(&[("OPENAI_API_KEY", None)])
.with_env_overrides(&env_overrides)
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
.await?;
let read_req = reloaded_mcp
Expand Down Expand Up @@ -310,9 +375,9 @@ async fn wait_for_web_search_completed(
}
}

async fn mount_search_response(server: &MockServer) {
async fn mount_search_response(server: &MockServer, search_path: &str) {
Mock::given(method("POST"))
.and(path("/api/codex/alpha/search"))
.and(path(search_path))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"encrypted_output": "ciphertext",
"output": "Search result",
Expand Down Expand Up @@ -340,13 +405,13 @@ fn has_hosted_web_search(body: &Value) -> bool {
})
}

async fn search_request(server: &MockServer) -> Result<wiremock::Request> {
async fn search_request(server: &MockServer, search_path: &str) -> Result<wiremock::Request> {
let requests = server
.received_requests()
.await
.context("failed to fetch received requests")?;
requests
.into_iter()
.find(|request| request.url.path() == "/api/codex/alpha/search")
.find(|request| request.url.path() == search_path)
.context("expected standalone search request")
}
2 changes: 2 additions & 0 deletions codex-rs/config/src/thread_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ mod tests {
wire_api = "responses"
requires_openai_auth = false
supports_websockets = true
supports_standalone_web_search = true

[features]
plugins = false
Expand Down Expand Up @@ -312,6 +313,7 @@ mod tests {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: true,
supports_standalone_web_search: true,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ message ModelProvider {
optional uint64 websocket_connect_timeout_ms = 15;
bool requires_openai_auth = 16;
bool supports_websockets = 17;
bool supports_standalone_web_search = 18;
}

message StringMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ pub struct ModelProvider {
pub requires_openai_auth: bool,
#[prost(bool, tag = "17")]
pub supports_websockets: bool,
#[prost(bool, tag = "18")]
pub supports_standalone_web_search: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct StringMap {
Expand Down
20 changes: 20 additions & 0 deletions codex-rs/config/src/thread_config/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ fn model_provider_from_proto(
websocket_connect_timeout_ms: provider.websocket_connect_timeout_ms,
requires_openai_auth: provider.requires_openai_auth,
supports_websockets: provider.supports_websockets,
supports_standalone_web_search: provider.supports_standalone_web_search,
};
Ok((id, info))
}
Expand Down Expand Up @@ -217,6 +218,7 @@ fn model_provider_to_proto(
websocket_connect_timeout_ms,
requires_openai_auth,
supports_websockets,
supports_standalone_web_search,
} = provider;

proto::ModelProvider {
Expand All @@ -237,6 +239,7 @@ fn model_provider_to_proto(
websocket_connect_timeout_ms,
requires_openai_auth,
supports_websockets,
supports_standalone_web_search,
}
}

Expand Down Expand Up @@ -421,6 +424,21 @@ mod tests {
fn model_provider_proto_roundtrips_through_domain_type() {
let expected = expected_provider();
let proto = model_provider_to_proto("local", expected.clone());
assert!(proto.supports_standalone_web_search);
let (id, actual) = model_provider_from_proto(proto).expect("model provider from proto");

assert_eq!(id, "local");
assert_eq!(actual, expected);
}

#[test]
fn model_provider_proto_defaults_standalone_web_search_to_false() {
let expected = ModelProviderInfo {
supports_standalone_web_search: false,
..expected_provider()
};
let proto = model_provider_to_proto("local", expected.clone());
assert!(!proto.supports_standalone_web_search);
let (id, actual) = model_provider_from_proto(proto).expect("model provider from proto");

assert_eq!(id, "local");
Expand Down Expand Up @@ -473,6 +491,7 @@ mod tests {
websocket_connect_timeout_ms: Some(10_000),
requires_openai_auth: false,
supports_websockets: true,
supports_standalone_web_search: true,
}],
features: HashMap::from([
("plugins".to_string(), false),
Expand Down Expand Up @@ -536,6 +555,7 @@ mod tests {
websocket_connect_timeout_ms: Some(10_000),
requires_openai_auth: false,
supports_websockets: true,
supports_standalone_web_search: true,
aws: None,
}
}
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,11 @@
"minimum": 0.0,
"type": "integer"
},
"supports_standalone_web_search": {
"default": false,
"description": "Whether this provider supports the standalone web-search endpoint.",
"type": "boolean"
},
"supports_websockets": {
"default": false,
"description": "Whether this provider supports the Responses API WebSocket transport.",
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/compact_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ fn should_use_remote_compact_task_for_azure_provider() {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
supports_standalone_web_search: false,
};

assert!(should_use_remote_compact_task(&provider));
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/tools/spec_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ fn add_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut Plann

fn standalone_web_search_enabled(turn_context: &TurnContext) -> bool {
namespace_tools_enabled(turn_context)
&& turn_context.provider.capabilities().web_search
&& (turn_context.model_info.use_responses_lite
|| turn_context
.config
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/tests/responses_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
supports_standalone_web_search: false,
};

let codex_home = TempDir::new().expect("failed to create TempDir");
Expand Down Expand Up @@ -223,6 +224,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
supports_standalone_web_search: false,
};

let codex_home = TempDir::new().expect("failed to create TempDir");
Expand Down Expand Up @@ -339,6 +341,7 @@ async fn responses_respects_model_info_overrides_from_config() {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
supports_standalone_web_search: false,
};

let codex_home = TempDir::new().expect("failed to create TempDir");
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/tests/suite/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
supports_standalone_web_search: false,
};

send_request_with_provider(provider).await;
Expand Down Expand Up @@ -3186,6 +3187,7 @@ async fn azure_responses_request_includes_store_and_prefixed_item_ids() {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
supports_standalone_web_search: false,
};

let codex_home = TempDir::new().unwrap();
Expand Down Expand Up @@ -3843,6 +3845,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
supports_standalone_web_search: false,
};

// Init session
Expand Down Expand Up @@ -3932,6 +3935,7 @@ async fn env_var_overrides_loaded_auth() {
websocket_connect_timeout_ms: None,
requires_openai_auth: false,
supports_websockets: false,
supports_standalone_web_search: false,
};

// Init session
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/tests/suite/client_websockets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,7 @@ fn websocket_provider_with_connect_timeout(
websocket_connect_timeout_ms,
requires_openai_auth: false,
supports_websockets: true,
supports_standalone_web_search: false,
}
}

Expand Down
Loading
Loading