mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
2 Commits
dev/zhao/2
...
search-per
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9286c891c0 | ||
|
|
1a92267a40 |
@@ -1058,6 +1058,7 @@ async fn submission_loop(
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
enable_web_search,
|
||||
} => {
|
||||
// Recalculate the persistent turn context with provided overrides.
|
||||
let prev = Arc::clone(&turn_context);
|
||||
@@ -1082,12 +1083,14 @@ async fn submission_loop(
|
||||
let mut updated_config = (*config).clone();
|
||||
updated_config.model = effective_model.clone();
|
||||
updated_config.model_family = effective_family.clone();
|
||||
updated_config.tools_web_search_request =
|
||||
enable_web_search.unwrap_or(config.tools_web_search_request);
|
||||
if let Some(model_info) = get_model_info(&effective_family) {
|
||||
updated_config.model_context_window = Some(model_info.context_window);
|
||||
}
|
||||
|
||||
let client = ModelClient::new(
|
||||
Arc::new(updated_config),
|
||||
Arc::new(updated_config.clone()),
|
||||
auth_manager,
|
||||
provider,
|
||||
effective_effort,
|
||||
@@ -1100,14 +1103,14 @@ async fn submission_loop(
|
||||
.clone()
|
||||
.unwrap_or(prev.sandbox_policy.clone());
|
||||
let new_cwd = cwd.clone().unwrap_or_else(|| prev.cwd.clone());
|
||||
|
||||
// TODO: we need to not have both updated config and global config
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_family: &effective_family,
|
||||
approval_policy: new_approval_policy,
|
||||
sandbox_policy: new_sandbox_policy.clone(),
|
||||
include_plan_tool: config.include_plan_tool,
|
||||
include_apply_patch_tool: config.include_apply_patch_tool,
|
||||
include_web_search_request: config.tools_web_search_request,
|
||||
include_web_search_request: updated_config.tools_web_search_request,
|
||||
use_streamable_shell_tool: config.use_experimental_streamable_shell_tool,
|
||||
include_view_image_tool: config.include_view_image_tool,
|
||||
});
|
||||
@@ -1154,6 +1157,7 @@ async fn submission_loop(
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
enable_web_search,
|
||||
} => {
|
||||
// attempt to inject input into current task
|
||||
if let Err(items) = sess.inject_input(items) {
|
||||
@@ -1169,6 +1173,7 @@ async fn submission_loop(
|
||||
let mut per_turn_config = (*config).clone();
|
||||
per_turn_config.model = model.clone();
|
||||
per_turn_config.model_family = model_family.clone();
|
||||
per_turn_config.tools_web_search_request = enable_web_search;
|
||||
if let Some(model_info) = get_model_info(&model_family) {
|
||||
per_turn_config.model_context_window = Some(model_info.context_window);
|
||||
}
|
||||
@@ -1176,7 +1181,7 @@ async fn submission_loop(
|
||||
// Build a new client with per‑turn reasoning settings.
|
||||
// Reuse the same provider and session id; auth defaults to env/API key.
|
||||
let client = ModelClient::new(
|
||||
Arc::new(per_turn_config),
|
||||
Arc::new(per_turn_config.clone()),
|
||||
auth_manager,
|
||||
provider,
|
||||
effort,
|
||||
@@ -1184,6 +1189,7 @@ async fn submission_loop(
|
||||
sess.session_id,
|
||||
);
|
||||
|
||||
// TODO: we need to not have both updated config and global config
|
||||
let fresh_turn_context = TurnContext {
|
||||
client,
|
||||
tools_config: ToolsConfig::new(&ToolsConfigParams {
|
||||
@@ -1192,7 +1198,7 @@ async fn submission_loop(
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
include_plan_tool: config.include_plan_tool,
|
||||
include_apply_patch_tool: config.include_apply_patch_tool,
|
||||
include_web_search_request: config.tools_web_search_request,
|
||||
include_web_search_request: per_turn_config.tools_web_search_request,
|
||||
use_streamable_shell_tool: config
|
||||
.use_experimental_streamable_shell_tool,
|
||||
include_view_image_tool: config.include_view_image_tool,
|
||||
|
||||
@@ -10,3 +10,4 @@ mod prompt_caching;
|
||||
mod seatbelt;
|
||||
mod stream_error_allows_next_turn;
|
||||
mod stream_no_completed;
|
||||
mod tools_web_search;
|
||||
|
||||
@@ -393,6 +393,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() {
|
||||
model: Some("o3".to_string()),
|
||||
effort: Some(ReasoningEffort::High),
|
||||
summary: Some(ReasoningSummary::Detailed),
|
||||
enable_web_search: Some(false),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -521,6 +522,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() {
|
||||
model: "o3".to_string(),
|
||||
effort: ReasoningEffort::High,
|
||||
summary: ReasoningSummary::Detailed,
|
||||
enable_web_search: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
170
codex-rs/core/tests/suite/tools_web_search.rs
Normal file
170
codex-rs/core/tests/suite/tools_web_search.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::ModelProviderInfo;
|
||||
use codex_core::built_in_model_providers;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::InputItem;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol_config_types::ReasoningEffort;
|
||||
use codex_core::protocol_config_types::ReasoningSummary;
|
||||
use codex_login::CodexAuth;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::load_sse_fixture_with_id;
|
||||
use core_test_support::wait_for_event;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
/// Build minimal SSE stream with completed marker using the JSON fixture.
|
||||
fn sse_completed(id: &str) -> String {
|
||||
load_sse_fixture_with_id("tests/fixtures/completed_template.json", id)
|
||||
}
|
||||
|
||||
fn tools_include_web_search(body: &serde_json::Value) -> bool {
|
||||
body["tools"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|t| t["type"].as_str() == Some("web_search"))
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn web_search_tool_present_after_override_turn_context() {
|
||||
// Mock server
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let sse = sse_completed("resp");
|
||||
let template = ResponseTemplate::new(200)
|
||||
.insert_header("content-type", "text/event-stream")
|
||||
.set_body_raw(sse, "text/event-stream");
|
||||
|
||||
// Expect one POST to /v1/responses
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/responses"))
|
||||
.respond_with(template)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let model_provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers()["openai"].clone()
|
||||
};
|
||||
|
||||
let cwd = TempDir::new().unwrap();
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let mut config = load_default_config_for_test(&codex_home);
|
||||
config.cwd = cwd.path().to_path_buf();
|
||||
config.model_provider = model_provider;
|
||||
|
||||
let conversation_manager =
|
||||
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||
let codex = conversation_manager
|
||||
.new_conversation(config)
|
||||
.await
|
||||
.expect("create new conversation")
|
||||
.conversation;
|
||||
|
||||
// Enable web search for subsequent turns
|
||||
codex
|
||||
.submit(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
enable_web_search: Some(true),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Trigger a user turn
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![InputItem::Text {
|
||||
text: "hello".into(),
|
||||
}],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
let requests = server.received_requests().await.unwrap();
|
||||
assert_eq!(requests.len(), 1, "expected one POST request");
|
||||
let body = requests[0].body_json::<serde_json::Value>().unwrap();
|
||||
assert!(
|
||||
tools_include_web_search(&body),
|
||||
"tools should include web_search when enable_web_search is true via OverrideTurnContext",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn web_search_tool_present_in_user_turn_when_enabled() {
|
||||
// Mock server
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let sse = sse_completed("resp");
|
||||
let template = ResponseTemplate::new(200)
|
||||
.insert_header("content-type", "text/event-stream")
|
||||
.set_body_raw(sse, "text/event-stream");
|
||||
|
||||
// Expect one POST to /v1/responses
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/responses"))
|
||||
.respond_with(template)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let model_provider = ModelProviderInfo {
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
..built_in_model_providers()["openai"].clone()
|
||||
};
|
||||
|
||||
let cwd = TempDir::new().unwrap();
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let mut config = load_default_config_for_test(&codex_home);
|
||||
config.cwd = cwd.path().to_path_buf();
|
||||
config.model_provider = model_provider;
|
||||
|
||||
let conversation_manager =
|
||||
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||
let codex = conversation_manager
|
||||
.new_conversation(config)
|
||||
.await
|
||||
.expect("create new conversation")
|
||||
.conversation;
|
||||
|
||||
// Submit a per-turn override with enable_web_search = true
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![InputItem::Text {
|
||||
text: "hello".into(),
|
||||
}],
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
model: "o3".to_string(),
|
||||
effort: ReasoningEffort::High,
|
||||
summary: ReasoningSummary::Detailed,
|
||||
enable_web_search: true,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
|
||||
|
||||
let requests = server.received_requests().await.unwrap();
|
||||
assert_eq!(requests.len(), 1, "expected one POST request");
|
||||
let body = requests[0].body_json::<serde_json::Value>().unwrap();
|
||||
assert!(
|
||||
tools_include_web_search(&body),
|
||||
"tools should include web_search when enable_web_search is true in UserTurn",
|
||||
);
|
||||
}
|
||||
@@ -505,6 +505,7 @@ impl CodexMessageProcessor {
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
enable_web_search,
|
||||
} = params;
|
||||
|
||||
let Ok(conversation) = self
|
||||
@@ -539,6 +540,7 @@ impl CodexMessageProcessor {
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
enable_web_search,
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -320,6 +320,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() {
|
||||
model: "mock-model".to_string(),
|
||||
effort: ReasoningEffort::Medium,
|
||||
summary: ReasoningSummary::Auto,
|
||||
enable_web_search: false,
|
||||
})
|
||||
.await
|
||||
.expect("send sendUserTurn");
|
||||
|
||||
@@ -266,6 +266,7 @@ pub struct SendUserTurnParams {
|
||||
pub model: String,
|
||||
pub effort: ReasoningEffort,
|
||||
pub summary: ReasoningSummary,
|
||||
pub enable_web_search: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||||
|
||||
@@ -76,6 +76,9 @@ pub enum Op {
|
||||
|
||||
/// Will only be honored if the model is configured to use reasoning.
|
||||
summary: ReasoningSummaryConfig,
|
||||
|
||||
/// Whether to enable web search.
|
||||
enable_web_search: bool,
|
||||
},
|
||||
|
||||
/// Override parts of the persistent turn context for subsequent turns.
|
||||
@@ -108,6 +111,10 @@ pub enum Op {
|
||||
/// Updated reasoning summary preference (honored only for reasoning-capable models).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
summary: Option<ReasoningSummaryConfig>,
|
||||
|
||||
/// Whether to enable web search.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
enable_web_search: Option<bool>,
|
||||
},
|
||||
|
||||
/// Approve a command execution
|
||||
|
||||
@@ -1058,6 +1058,7 @@ impl ChatWidget {
|
||||
model: Some(model_slug.clone()),
|
||||
effort: Some(effort),
|
||||
summary: None,
|
||||
enable_web_search: None,
|
||||
}));
|
||||
tx.send(AppEvent::UpdateModel(model_slug.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
@@ -1099,6 +1100,7 @@ impl ChatWidget {
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
enable_web_search: None,
|
||||
}));
|
||||
tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));
|
||||
tx.send(AppEvent::UpdateSandboxPolicy(sandbox.clone()));
|
||||
|
||||
Reference in New Issue
Block a user