Compare commits

...

2 Commits

Author SHA1 Message Date
Ahmed Ibrahim
9286c891c0 enable search config change 2025-08-28 13:04:59 -07:00
Ahmed Ibrahim
1a92267a40 enable search config change 2025-08-28 13:01:37 -07:00
9 changed files with 197 additions and 5 deletions

View File

@@ -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 perturn 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,

View File

@@ -10,3 +10,4 @@ mod prompt_caching;
mod seatbelt;
mod stream_error_allows_next_turn;
mod stream_no_completed;
mod tools_web_search;

View File

@@ -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();

View 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",
);
}

View File

@@ -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;

View File

@@ -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");

View File

@@ -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)]

View File

@@ -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

View File

@@ -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()));