mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Allow full web search tool config (#13675)
Previously, we could only configure whether web search was on/off. This PR enables sending along a web search config, which includes all the stuff responsesapi supports: filters, location, etc.
This commit is contained in:
committed by
GitHub
Unverified
parent
8b81284975
commit
61098c7f51
@@ -12108,6 +12108,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ToolsV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"web_search": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -15848,9 +15858,13 @@
|
||||
]
|
||||
},
|
||||
"web_search": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/WebSearchToolConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -16449,6 +16463,44 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"WebSearchContextSize": {
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchLocation": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"country": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timezone": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"WebSearchMode": {
|
||||
"enum": [
|
||||
"disabled",
|
||||
@@ -16457,6 +16509,41 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchToolConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allowed_domains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"context_size": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/WebSearchContextSize"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"location": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/WebSearchLocation"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"WindowsSandboxSetupCompletedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -8765,6 +8765,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolsV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"web_search": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -13880,9 +13890,13 @@
|
||||
]
|
||||
},
|
||||
"web_search": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WebSearchToolConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -14705,6 +14719,44 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"WebSearchContextSize": {
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchLocation": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"country": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timezone": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"WebSearchMode": {
|
||||
"enum": [
|
||||
"disabled",
|
||||
@@ -14713,6 +14765,41 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchToolConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allowed_domains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"context_size": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WebSearchContextSize"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"location": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WebSearchLocation"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"WindowsSandboxSetupCompletedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -628,6 +628,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolsV2"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"web_search": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -721,9 +731,13 @@
|
||||
]
|
||||
},
|
||||
"web_search": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WebSearchToolConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -738,6 +752,44 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchContextSize": {
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchLocation": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"country": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timezone": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"WebSearchMode": {
|
||||
"enum": [
|
||||
"disabled",
|
||||
@@ -745,6 +797,41 @@
|
||||
"live"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchToolConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allowed_domains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"context_size": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WebSearchContextSize"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"location": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WebSearchLocation"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type WebSearchContextSize = "low" | "medium" | "high";
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type WebSearchLocation = { country: string | null, region: string | null, city: string | null, timezone: string | null, };
|
||||
@@ -0,0 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { WebSearchContextSize } from "./WebSearchContextSize";
|
||||
import type { WebSearchLocation } from "./WebSearchLocation";
|
||||
|
||||
export type WebSearchToolConfig = { context_size: WebSearchContextSize | null, allowed_domains: Array<string> | null, location: WebSearchLocation | null, };
|
||||
@@ -211,7 +211,10 @@ export type { ViewImageToolCallEvent } from "./ViewImageToolCallEvent";
|
||||
export type { WarningEvent } from "./WarningEvent";
|
||||
export type { WebSearchAction } from "./WebSearchAction";
|
||||
export type { WebSearchBeginEvent } from "./WebSearchBeginEvent";
|
||||
export type { WebSearchContextSize } from "./WebSearchContextSize";
|
||||
export type { WebSearchEndEvent } from "./WebSearchEndEvent";
|
||||
export type { WebSearchItem } from "./WebSearchItem";
|
||||
export type { WebSearchLocation } from "./WebSearchLocation";
|
||||
export type { WebSearchMode } from "./WebSearchMode";
|
||||
export type { WebSearchToolConfig } from "./WebSearchToolConfig";
|
||||
export * as v2 from "./v2";
|
||||
|
||||
@@ -8,5 +8,6 @@ import type { Verbosity } from "../Verbosity";
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { ToolsV2 } from "./ToolsV2";
|
||||
|
||||
export type ProfileV2 = { model: string | null, model_provider: string | null, approval_policy: AskForApproval | null, service_tier: ServiceTier | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, web_search: WebSearchMode | null, chatgpt_base_url: string | null, } & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
export type ProfileV2 = { model: string | null, model_provider: string | null, approval_policy: AskForApproval | null, service_tier: ServiceTier | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, chatgpt_base_url: string | null, } & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { WebSearchToolConfig } from "../WebSearchToolConfig";
|
||||
|
||||
export type ToolsV2 = { web_search: boolean | null, view_image: boolean | null, };
|
||||
export type ToolsV2 = { web_search: WebSearchToolConfig | null, view_image: boolean | null, };
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_protocol::config_types::SandboxMode as CoreSandboxMode;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WebSearchToolConfig;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::mcp::Resource as McpResource;
|
||||
@@ -375,8 +376,7 @@ pub struct SandboxWorkspaceWrite {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ToolsV2 {
|
||||
#[serde(alias = "web_search_request")]
|
||||
pub web_search: Option<bool>,
|
||||
pub web_search: Option<WebSearchToolConfig>,
|
||||
pub view_image: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -401,6 +401,7 @@ pub struct ProfileV2 {
|
||||
pub model_reasoning_summary: Option<ReasoningSummary>,
|
||||
pub model_verbosity: Option<Verbosity>,
|
||||
pub web_search: Option<WebSearchMode>,
|
||||
pub tools: Option<ToolsV2>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
#[serde(default, flatten)]
|
||||
pub additional: HashMap<String, JsonValue>,
|
||||
|
||||
@@ -23,6 +23,9 @@ use codex_app_server_protocol::ToolsV2;
|
||||
use codex_app_server_protocol::WriteStatus;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchLocation;
|
||||
use codex_protocol::config_types::WebSearchToolConfig;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -93,8 +96,11 @@ async fn config_read_includes_tools() -> Result<()> {
|
||||
r#"
|
||||
model = "gpt-user"
|
||||
|
||||
[tools.web_search]
|
||||
context_size = "low"
|
||||
allowed_domains = ["example.com"]
|
||||
|
||||
[tools]
|
||||
web_search = true
|
||||
view_image = false
|
||||
"#,
|
||||
)?;
|
||||
@@ -125,12 +131,28 @@ view_image = false
|
||||
assert_eq!(
|
||||
tools,
|
||||
ToolsV2 {
|
||||
web_search: Some(true),
|
||||
web_search: Some(WebSearchToolConfig {
|
||||
context_size: Some(WebSearchContextSize::Low),
|
||||
allowed_domains: Some(vec!["example.com".to_string()]),
|
||||
location: None,
|
||||
}),
|
||||
view_image: Some(false),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
origins.get("tools.web_search").expect("origin").name,
|
||||
origins
|
||||
.get("tools.web_search.context_size")
|
||||
.expect("origin")
|
||||
.name,
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
origins
|
||||
.get("tools.web_search.allowed_domains.0")
|
||||
.expect("origin")
|
||||
.name,
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
}
|
||||
@@ -148,6 +170,54 @@ view_image = false
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_read_includes_nested_web_search_tool_config() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config(
|
||||
&codex_home,
|
||||
r#"
|
||||
web_search = "live"
|
||||
|
||||
[tools.web_search]
|
||||
context_size = "high"
|
||||
allowed_domains = ["example.com"]
|
||||
location = { country = "US", city = "New York", timezone = "America/New_York" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ConfigReadResponse { config, .. } = to_response(resp)?;
|
||||
|
||||
assert_eq!(
|
||||
config.tools.expect("tools present").web_search,
|
||||
Some(WebSearchToolConfig {
|
||||
context_size: Some(WebSearchContextSize::High),
|
||||
allowed_domains: Some(vec!["example.com".to_string()]),
|
||||
location: Some(WebSearchLocation {
|
||||
country: Some("US".to_string()),
|
||||
region: None,
|
||||
city: Some("New York".to_string()),
|
||||
timezone: Some("America/New_York".to_string()),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_read_includes_apps() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -530,10 +530,10 @@
|
||||
"service_tier": {
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
"tools_view_image": {
|
||||
"type": "boolean"
|
||||
"tools": {
|
||||
"$ref": "#/definitions/ToolsToml"
|
||||
},
|
||||
"tools_web_search": {
|
||||
"tools_view_image": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"web_search": {
|
||||
@@ -1464,8 +1464,12 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"web_search": {
|
||||
"default": null,
|
||||
"type": "boolean"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/WebSearchToolConfig"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -1573,6 +1577,32 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchContextSize": {
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchLocation": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
},
|
||||
"region": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"WebSearchMode": {
|
||||
"enum": [
|
||||
"disabled",
|
||||
@@ -1581,6 +1611,24 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchToolConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allowed_domains": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"context_size": {
|
||||
"$ref": "#/definitions/WebSearchContextSize"
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/definitions/WebSearchLocation"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"WindowsSandboxModeToml": {
|
||||
"enum": [
|
||||
"elevated",
|
||||
|
||||
@@ -154,8 +154,13 @@ fn strip_total_output_header(output: &str) -> Option<(&str, u32)> {
|
||||
|
||||
pub(crate) mod tools {
|
||||
use crate::tools::spec::JsonSchema;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchFilters;
|
||||
use codex_protocol::config_types::WebSearchUserLocation;
|
||||
use codex_protocol::config_types::WebSearchUserLocationType;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
|
||||
/// When serialized as JSON, this produces a valid "Tool" in the OpenAI
|
||||
/// Responses API.
|
||||
@@ -176,6 +181,18 @@ pub(crate) mod tools {
|
||||
WebSearch {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
external_web_access: Option<bool>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_web_search_filters"
|
||||
)]
|
||||
filters: Option<WebSearchFilters>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_web_search_user_location"
|
||||
)]
|
||||
user_location: Option<WebSearchUserLocation>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
search_context_size: Option<WebSearchContextSize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
search_content_types: Option<Vec<String>>,
|
||||
},
|
||||
@@ -195,6 +212,66 @@ pub(crate) mod tools {
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_web_search_filters<S>(
|
||||
filters: &Option<WebSearchFilters>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match filters {
|
||||
Some(filters) => {
|
||||
#[derive(Serialize)]
|
||||
struct SerializableWebSearchFilters<'a> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
allowed_domains: Option<&'a Vec<String>>,
|
||||
}
|
||||
|
||||
SerializableWebSearchFilters {
|
||||
allowed_domains: filters.allowed_domains.as_ref(),
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_web_search_user_location<S>(
|
||||
user_location: &Option<WebSearchUserLocation>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match user_location {
|
||||
Some(user_location) => {
|
||||
#[derive(Serialize)]
|
||||
struct SerializableWebSearchUserLocation<'a> {
|
||||
#[serde(rename = "type")]
|
||||
r#type: WebSearchUserLocationType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
country: Option<&'a String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
region: Option<&'a String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
city: Option<&'a String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
timezone: Option<&'a String>,
|
||||
}
|
||||
|
||||
SerializableWebSearchUserLocation {
|
||||
r#type: user_location.r#type,
|
||||
country: user_location.country.as_ref(),
|
||||
region: user_location.region.as_ref(),
|
||||
city: user_location.city.as_ref(),
|
||||
timezone: user_location.timezone.as_ref(),
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FreeformTool {
|
||||
pub(crate) name: String,
|
||||
|
||||
@@ -744,6 +744,7 @@ impl TurnContext {
|
||||
web_search_mode: self.tools_config.web_search_mode,
|
||||
session_source: self.session_source.clone(),
|
||||
})
|
||||
.with_web_search_config(self.tools_config.web_search_config.clone())
|
||||
.with_allow_login_shell(self.tools_config.allow_login_shell)
|
||||
.with_agent_roles(config.agent_roles.clone());
|
||||
|
||||
@@ -1119,6 +1120,7 @@ impl Session {
|
||||
web_search_mode: Some(per_turn_config.web_search_mode.value()),
|
||||
session_source: session_source.clone(),
|
||||
})
|
||||
.with_web_search_config(per_turn_config.web_search_config.clone())
|
||||
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
|
||||
.with_agent_roles(per_turn_config.agent_roles.clone());
|
||||
|
||||
@@ -4911,6 +4913,7 @@ async fn spawn_review_thread(
|
||||
web_search_mode: Some(review_web_search_mode),
|
||||
session_source: parent_turn_context.session_source.clone(),
|
||||
})
|
||||
.with_web_search_config(None)
|
||||
.with_allow_login_shell(config.permissions.allow_login_shell)
|
||||
.with_agent_roles(config.agent_roles.clone());
|
||||
|
||||
|
||||
@@ -1250,34 +1250,6 @@ fn web_search_mode_for_turn_falls_back_when_live_is_disallowed() -> anyhow::Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_legacy_toggles_override_base() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut profiles = HashMap::new();
|
||||
profiles.insert(
|
||||
"work".to_string(),
|
||||
ConfigProfile {
|
||||
tools_web_search: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let cfg = ConfigToml {
|
||||
profiles,
|
||||
profile: Some("work".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
assert!(!config.features.enabled(Feature::WebSearchRequest));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn project_profile_overrides_user_profile() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -3100,6 +3072,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
web_search_config: None,
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
@@ -3234,6 +3207,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
web_search_config: None,
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
@@ -3366,6 +3340,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
web_search_config: None,
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
@@ -3484,6 +3459,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
web_search_config: None,
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
|
||||
@@ -218,14 +218,6 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature
|
||||
enabled,
|
||||
));
|
||||
}
|
||||
if let Some(enabled) = cfg.tools.as_ref().and_then(|tools| tools.web_search) {
|
||||
explicit_settings.push((
|
||||
"tools.web_search".to_string(),
|
||||
Feature::WebSearchRequest,
|
||||
enabled,
|
||||
));
|
||||
}
|
||||
|
||||
for (profile_name, profile) in &cfg.profiles {
|
||||
if let Some(features) = profile.features.as_ref() {
|
||||
for (key, enabled) in &features.entries {
|
||||
@@ -259,13 +251,6 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature
|
||||
enabled,
|
||||
));
|
||||
}
|
||||
if let Some(enabled) = profile.tools_web_search {
|
||||
explicit_settings.push((
|
||||
format!("profiles.{profile_name}.tools_web_search"),
|
||||
Feature::WebSearchRequest,
|
||||
enabled,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
explicit_settings
|
||||
|
||||
@@ -68,7 +68,9 @@ use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::config_types::WebSearchConfig;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WebSearchToolConfig;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
@@ -482,6 +484,9 @@ pub struct Config {
|
||||
/// Explicit or feature-derived web search mode.
|
||||
pub web_search_mode: Constrained<WebSearchMode>,
|
||||
|
||||
/// Additional parameters for the web search tool when it is enabled.
|
||||
pub web_search_config: Option<WebSearchConfig>,
|
||||
|
||||
/// If set to `true`, used only the experimental unified exec tool.
|
||||
pub use_experimental_unified_exec_tool: bool,
|
||||
|
||||
@@ -1379,8 +1384,8 @@ pub struct RealtimeAudioToml {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolsToml {
|
||||
#[serde(default, alias = "web_search_request")]
|
||||
pub web_search: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub web_search: Option<WebSearchToolConfig>,
|
||||
|
||||
/// Enable the `view_image` tool that lets the agent attach local images.
|
||||
#[serde(default)]
|
||||
@@ -1442,7 +1447,7 @@ pub struct AgentRoleToml {
|
||||
impl From<ToolsToml> for Tools {
|
||||
fn from(tools_toml: ToolsToml) -> Self {
|
||||
Self {
|
||||
web_search: tools_toml.web_search,
|
||||
web_search: tools_toml.web_search.is_some().then_some(true),
|
||||
view_image: tools_toml.view_image,
|
||||
}
|
||||
}
|
||||
@@ -1734,6 +1739,27 @@ fn resolve_web_search_mode(
|
||||
None
|
||||
}
|
||||
|
||||
fn resolve_web_search_config(
|
||||
config_toml: &ConfigToml,
|
||||
config_profile: &ConfigProfile,
|
||||
) -> Option<WebSearchConfig> {
|
||||
let base = config_toml
|
||||
.tools
|
||||
.as_ref()
|
||||
.and_then(|tools| tools.web_search.as_ref());
|
||||
let profile = config_profile
|
||||
.tools
|
||||
.as_ref()
|
||||
.and_then(|tools| tools.web_search.as_ref());
|
||||
|
||||
match (base, profile) {
|
||||
(None, None) => None,
|
||||
(Some(base), None) => Some(base.clone().into()),
|
||||
(None, Some(profile)) => Some(profile.clone().into()),
|
||||
(Some(base), Some(profile)) => Some(base.merge(profile).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
web_search_mode: &Constrained<WebSearchMode>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
@@ -2013,6 +2039,7 @@ impl Config {
|
||||
}
|
||||
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
|
||||
.unwrap_or(WebSearchMode::Cached);
|
||||
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
|
||||
|
||||
let mut model_providers = built_in_model_providers();
|
||||
// Merge user-defined providers into the built-in list.
|
||||
@@ -2418,6 +2445,7 @@ impl Config {
|
||||
forced_login_method,
|
||||
include_apply_patch_tool: include_apply_patch_tool_flag,
|
||||
web_search_mode: constrained_web_search_mode.value,
|
||||
web_search_config,
|
||||
use_experimental_unified_exec_tool,
|
||||
background_terminal_max_timeout,
|
||||
ghost_snapshot,
|
||||
|
||||
@@ -3,6 +3,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::config::ToolsToml;
|
||||
use crate::config::types::Personality;
|
||||
use crate::config::types::WindowsToml;
|
||||
use crate::protocol::AskForApproval;
|
||||
@@ -47,8 +48,8 @@ pub struct ConfigProfile {
|
||||
pub include_apply_patch_tool: Option<bool>,
|
||||
pub experimental_use_unified_exec_tool: Option<bool>,
|
||||
pub experimental_use_freeform_apply_patch: Option<bool>,
|
||||
pub tools_web_search: Option<bool>,
|
||||
pub tools_view_image: Option<bool>,
|
||||
pub tools: Option<ToolsToml>,
|
||||
pub web_search: Option<WebSearchMode>,
|
||||
pub analytics: Option<crate::config::types::AnalyticsConfigToml>,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -213,10 +213,17 @@ impl FeatureOverrides {
|
||||
fn apply(self, features: &mut Features) {
|
||||
LegacyFeatureToggles {
|
||||
include_apply_patch_tool: self.include_apply_patch_tool,
|
||||
tools_web_search: self.web_search_request,
|
||||
..Default::default()
|
||||
}
|
||||
.apply(features);
|
||||
if let Some(enabled) = self.web_search_request {
|
||||
if enabled {
|
||||
features.enable(Feature::WebSearchRequest);
|
||||
} else {
|
||||
features.disable(Feature::WebSearchRequest);
|
||||
}
|
||||
features.record_legacy_usage("web_search_request", Feature::WebSearchRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +349,6 @@ impl Features {
|
||||
let base_legacy = LegacyFeatureToggles {
|
||||
experimental_use_freeform_apply_patch: cfg.experimental_use_freeform_apply_patch,
|
||||
experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool,
|
||||
tools_web_search: cfg.tools.as_ref().and_then(|t| t.web_search),
|
||||
..Default::default()
|
||||
};
|
||||
base_legacy.apply(&mut features);
|
||||
@@ -357,7 +363,6 @@ impl Features {
|
||||
.experimental_use_freeform_apply_patch,
|
||||
|
||||
experimental_use_unified_exec_tool: config_profile.experimental_use_unified_exec_tool,
|
||||
tools_web_search: config_profile.tools_web_search,
|
||||
};
|
||||
profile_legacy.apply(&mut features);
|
||||
if let Some(profile_features) = config_profile.features.as_ref() {
|
||||
@@ -388,7 +393,6 @@ fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option<String>
|
||||
Feature::WebSearchRequest | Feature::WebSearchCached => {
|
||||
let label = match alias {
|
||||
"web_search" => "[features].web_search",
|
||||
"tools.web_search" => "[tools].web_search",
|
||||
"features.web_search_request" | "web_search_request" => {
|
||||
"[features].web_search_request"
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ pub struct LegacyFeatureToggles {
|
||||
pub include_apply_patch_tool: Option<bool>,
|
||||
pub experimental_use_freeform_apply_patch: Option<bool>,
|
||||
pub experimental_use_unified_exec_tool: Option<bool>,
|
||||
pub tools_web_search: Option<bool>,
|
||||
}
|
||||
|
||||
impl LegacyFeatureToggles {
|
||||
@@ -85,12 +84,6 @@ impl LegacyFeatureToggles {
|
||||
self.experimental_use_unified_exec_tool,
|
||||
"experimental_use_unified_exec_tool",
|
||||
);
|
||||
set_if_some(
|
||||
features,
|
||||
Feature::WebSearchRequest,
|
||||
self.tools_web_search,
|
||||
"tools.web_search",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::tools::handlers::multi_agents::MAX_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents::MIN_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::request_user_input_tool_description;
|
||||
use crate::tools::registry::ToolRegistryBuilder;
|
||||
use codex_protocol::config_types::WebSearchConfig;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
|
||||
@@ -58,6 +59,7 @@ pub(crate) struct ToolsConfig {
|
||||
pub allow_login_shell: bool,
|
||||
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
||||
pub web_search_mode: Option<WebSearchMode>,
|
||||
pub web_search_config: Option<WebSearchConfig>,
|
||||
pub web_search_tool_type: WebSearchToolType,
|
||||
pub image_gen_tool: bool,
|
||||
pub agent_roles: BTreeMap<String, AgentRoleConfig>,
|
||||
@@ -158,6 +160,7 @@ impl ToolsConfig {
|
||||
allow_login_shell: true,
|
||||
apply_patch_tool_type,
|
||||
web_search_mode: *web_search_mode,
|
||||
web_search_config: None,
|
||||
web_search_tool_type: model_info.web_search_tool_type,
|
||||
image_gen_tool: include_image_gen_tool,
|
||||
agent_roles: BTreeMap::new(),
|
||||
@@ -184,6 +187,11 @@ impl ToolsConfig {
|
||||
self.allow_login_shell = allow_login_shell;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_web_search_config(mut self, web_search_config: Option<WebSearchConfig>) -> Self {
|
||||
self.web_search_config = web_search_config;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_image_generation(model_info: &ModelInfo) -> bool {
|
||||
@@ -1979,6 +1987,18 @@ pub(crate) fn build_specs(
|
||||
|
||||
builder.push_spec(ToolSpec::WebSearch {
|
||||
external_web_access: Some(external_web_access),
|
||||
filters: config
|
||||
.web_search_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.filters.clone()),
|
||||
user_location: config
|
||||
.web_search_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.user_location.clone()),
|
||||
search_context_size: config
|
||||
.web_search_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.search_context_size),
|
||||
search_content_types,
|
||||
});
|
||||
}
|
||||
@@ -2266,6 +2286,9 @@ mod tests {
|
||||
create_apply_patch_freeform_tool(),
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(true),
|
||||
filters: None,
|
||||
user_location: None,
|
||||
search_context_size: None,
|
||||
search_content_types: None,
|
||||
},
|
||||
create_view_image_tool(),
|
||||
@@ -2592,6 +2615,9 @@ mod tests {
|
||||
tool.spec,
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(false),
|
||||
filters: None,
|
||||
user_location: None,
|
||||
search_context_size: None,
|
||||
search_content_types: None,
|
||||
}
|
||||
);
|
||||
@@ -2617,6 +2643,51 @@ mod tests {
|
||||
tool.spec,
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(true),
|
||||
filters: None,
|
||||
user_location: None,
|
||||
search_context_size: None,
|
||||
search_content_types: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_config_is_forwarded_to_tool_spec() {
|
||||
let config = test_config();
|
||||
let model_info =
|
||||
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
|
||||
let features = Features::with_defaults();
|
||||
let web_search_config = WebSearchConfig {
|
||||
filters: Some(codex_protocol::config_types::WebSearchFilters {
|
||||
allowed_domains: Some(vec!["example.com".to_string()]),
|
||||
}),
|
||||
user_location: Some(codex_protocol::config_types::WebSearchUserLocation {
|
||||
r#type: codex_protocol::config_types::WebSearchUserLocationType::Approximate,
|
||||
country: Some("US".to_string()),
|
||||
region: Some("California".to_string()),
|
||||
city: Some("San Francisco".to_string()),
|
||||
timezone: Some("America/Los_Angeles".to_string()),
|
||||
}),
|
||||
search_context_size: Some(codex_protocol::config_types::WebSearchContextSize::High),
|
||||
};
|
||||
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Live),
|
||||
session_source: SessionSource::Cli,
|
||||
})
|
||||
.with_web_search_config(Some(web_search_config.clone()));
|
||||
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
||||
|
||||
let tool = find_tool(&tools, "web_search");
|
||||
assert_eq!(
|
||||
tool.spec,
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(true),
|
||||
filters: web_search_config.filters,
|
||||
user_location: web_search_config.user_location,
|
||||
search_context_size: web_search_config.search_context_size,
|
||||
search_content_types: None,
|
||||
}
|
||||
);
|
||||
@@ -2643,6 +2714,9 @@ mod tests {
|
||||
tool.spec,
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(true),
|
||||
filters: None,
|
||||
user_location: None,
|
||||
search_context_size: None,
|
||||
search_content_types: Some(
|
||||
WEB_SEARCH_CONTENT_TYPES
|
||||
.into_iter()
|
||||
|
||||
@@ -9,6 +9,8 @@ use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
fn find_web_search_tool(body: &Value) -> &Value {
|
||||
@@ -223,3 +225,61 @@ async fn web_search_mode_updates_between_turns_with_sandbox_policy() {
|
||||
"danger-full-access policy should default web_search to live"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn web_search_tool_config_from_config_toml_is_forwarded_to_request() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let sse = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let resp_mock = responses::mount_sse_once(&server, sse).await;
|
||||
|
||||
let home = Arc::new(tempfile::TempDir::new().expect("create codex home"));
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
r#"web_search = "live"
|
||||
|
||||
[tools.web_search]
|
||||
context_size = "high"
|
||||
allowed_domains = ["example.com"]
|
||||
location = { country = "US", city = "New York", timezone = "America/New_York" }
|
||||
"#,
|
||||
)
|
||||
.expect("write config.toml");
|
||||
|
||||
let mut builder = test_codex().with_model("gpt-5-codex").with_home(home);
|
||||
let test = builder
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("create test Codex conversation");
|
||||
|
||||
test.submit_turn_with_policy(
|
||||
"hello configured web search",
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.await
|
||||
.expect("submit turn");
|
||||
|
||||
let body = resp_mock.single_request().body_json();
|
||||
let tool = find_web_search_tool(&body);
|
||||
assert_eq!(
|
||||
tool,
|
||||
&json!({
|
||||
"type": "web_search",
|
||||
"external_web_access": true,
|
||||
"search_context_size": "high",
|
||||
"filters": {
|
||||
"allowed_domains": ["example.com"],
|
||||
},
|
||||
"user_location": {
|
||||
"type": "approximate",
|
||||
"country": "US",
|
||||
"city": "New York",
|
||||
"timezone": "America/New_York",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,122 @@ pub enum WebSearchMode {
|
||||
Live,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum WebSearchContextSize {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct WebSearchLocation {
|
||||
pub country: Option<String>,
|
||||
pub region: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
impl WebSearchLocation {
|
||||
pub fn merge(&self, other: &Self) -> Self {
|
||||
Self {
|
||||
country: other.country.clone().or_else(|| self.country.clone()),
|
||||
region: other.region.clone().or_else(|| self.region.clone()),
|
||||
city: other.city.clone().or_else(|| self.city.clone()),
|
||||
timezone: other.timezone.clone().or_else(|| self.timezone.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct WebSearchToolConfig {
|
||||
pub context_size: Option<WebSearchContextSize>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub location: Option<WebSearchLocation>,
|
||||
}
|
||||
|
||||
impl WebSearchToolConfig {
|
||||
pub fn merge(&self, other: &Self) -> Self {
|
||||
Self {
|
||||
context_size: other.context_size.or(self.context_size),
|
||||
allowed_domains: other
|
||||
.allowed_domains
|
||||
.clone()
|
||||
.or_else(|| self.allowed_domains.clone()),
|
||||
location: match (&self.location, &other.location) {
|
||||
(Some(location), Some(other_location)) => Some(location.merge(other_location)),
|
||||
(Some(location), None) => Some(location.clone()),
|
||||
(None, Some(other_location)) => Some(other_location.clone()),
|
||||
(None, None) => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct WebSearchFilters {
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Display, JsonSchema, TS,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum WebSearchUserLocationType {
|
||||
#[default]
|
||||
Approximate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct WebSearchUserLocation {
|
||||
#[serde(default)]
|
||||
pub r#type: WebSearchUserLocationType,
|
||||
pub country: Option<String>,
|
||||
pub region: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct WebSearchConfig {
|
||||
pub filters: Option<WebSearchFilters>,
|
||||
pub user_location: Option<WebSearchUserLocation>,
|
||||
pub search_context_size: Option<WebSearchContextSize>,
|
||||
}
|
||||
|
||||
impl From<WebSearchLocation> for WebSearchUserLocation {
|
||||
fn from(location: WebSearchLocation) -> Self {
|
||||
Self {
|
||||
r#type: WebSearchUserLocationType::Approximate,
|
||||
country: location.country,
|
||||
region: location.region,
|
||||
city: location.city,
|
||||
timezone: location.timezone,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WebSearchToolConfig> for WebSearchConfig {
|
||||
fn from(config: WebSearchToolConfig) -> Self {
|
||||
Self {
|
||||
filters: config
|
||||
.allowed_domains
|
||||
.map(|allowed_domains| WebSearchFilters {
|
||||
allowed_domains: Some(allowed_domains),
|
||||
}),
|
||||
user_location: config.location.map(Into::into),
|
||||
search_context_size: config.context_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
@@ -366,4 +482,66 @@ mod tests {
|
||||
assert!(!ModeKind::PairProgramming.is_tui_visible());
|
||||
assert!(!ModeKind::Execute.is_tui_visible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_location_merge_prefers_overlay_values() {
|
||||
let base = WebSearchLocation {
|
||||
country: Some("US".to_string()),
|
||||
region: Some("CA".to_string()),
|
||||
city: None,
|
||||
timezone: Some("America/Los_Angeles".to_string()),
|
||||
};
|
||||
let overlay = WebSearchLocation {
|
||||
country: None,
|
||||
region: Some("WA".to_string()),
|
||||
city: Some("Seattle".to_string()),
|
||||
timezone: None,
|
||||
};
|
||||
|
||||
let expected = WebSearchLocation {
|
||||
country: Some("US".to_string()),
|
||||
region: Some("WA".to_string()),
|
||||
city: Some("Seattle".to_string()),
|
||||
timezone: Some("America/Los_Angeles".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(expected, base.merge(&overlay));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_tool_config_merge_prefers_overlay_values() {
|
||||
let base = WebSearchToolConfig {
|
||||
context_size: Some(WebSearchContextSize::Low),
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
location: Some(WebSearchLocation {
|
||||
country: Some("US".to_string()),
|
||||
region: Some("CA".to_string()),
|
||||
city: None,
|
||||
timezone: Some("America/Los_Angeles".to_string()),
|
||||
}),
|
||||
};
|
||||
let overlay = WebSearchToolConfig {
|
||||
context_size: Some(WebSearchContextSize::High),
|
||||
allowed_domains: None,
|
||||
location: Some(WebSearchLocation {
|
||||
country: None,
|
||||
region: Some("WA".to_string()),
|
||||
city: Some("Seattle".to_string()),
|
||||
timezone: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let expected = WebSearchToolConfig {
|
||||
context_size: Some(WebSearchContextSize::High),
|
||||
allowed_domains: Some(vec!["openai.com".to_string()]),
|
||||
location: Some(WebSearchLocation {
|
||||
country: Some("US".to_string()),
|
||||
region: Some("WA".to_string()),
|
||||
city: Some("Seattle".to_string()),
|
||||
timezone: Some("America/Los_Angeles".to_string()),
|
||||
}),
|
||||
};
|
||||
|
||||
assert_eq!(expected, base.merge(&overlay));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user