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
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
were previously unpriced: `mimo-v2.5-pro` / `xiaomi/mimo-v2.5-pro` reuse the
DeepSeek V4-Pro rate table and `mimo-v2.5` / `xiaomi/mimo-v2.5` reuse the
DeepSeek V4-Flash rates. Existing DeepSeek pricing is unchanged (#2731, #2750).
- Added optional `[search].base_url` / `CODEWHALE_SEARCH_BASE_URL` support for
DuckDuckGo-compatible private search endpoints, while keeping
`DEEPSEEK_SEARCH_BASE_URL` as a legacy alias. Custom endpoints are gated by
their configured host, do not fall back to public Bing, and report the custom
host as the result source for diagnostics (#2436, #2510).

### Changed

Expand Down Expand Up @@ -154,8 +159,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Community

Thanks to **@sximelon** for reporting and fixing the saved-session resume
footer hint (#2758, #2760), **@cyq1017** for the restore-listing implementation
(#2513) and pending-input delivery-mode label work (#2532, #2054),
footer hint (#2758, #2760), **@cyq1017** for the custom
DuckDuckGo-compatible search endpoint, restore-listing implementation, and
pending-input delivery-mode label work (#2510, #2513, #2532, #2054),
**@Artenx** for the private-search endpoint report (#2436),
**@wywsoor** for the broader macOS/iTerm rollback UX report (#2494),
**@HUQIANTAO** for the `web_run` lock-splitting work (#2502), turn-metadata
prefix-cache stability work (#2517), and project-context cache direction
Expand Down
3 changes: 3 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -409,13 +409,16 @@ max_subagents = 10 # optional (1-20)
# # baidu: 百度 AI Search via qianfan.baidubce.com,需 api_key
# # volcengine: 火山引擎 Ark web_search (免费 2 万次/月), 需 api_key
# # 也回退到 VOLCENGINE_API_KEY / VOLCENGINE_ARK_API_KEY / ARK_API_KEY 环境变量
# base_url = "https://search.example/html/" # optional DuckDuckGo-compatible HTML endpoint
# api_key = "YOUR_SEARCH_KEY" # required for tavily, bocha, and baidu; optional for metaso
# # WARNING: treat config.toml like a secret file when
# # storing API keys. Prefer env vars for local smoke tests.
#
# Env-var overrides:
# DEEPSEEK_SEARCH_PROVIDER → search.provider
# DEEPSEEK_SEARCH_API_KEY → search.api_key
# CODEWHALE_SEARCH_BASE_URL → search.base_url
# DEEPSEEK_SEARCH_BASE_URL → search.base_url (legacy alias)
# METASO_API_KEY → metaso key fallback
# BAIDU_SEARCH_API_KEY → baidu key fallback

Expand Down
11 changes: 9 additions & 2 deletions crates/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
were previously unpriced: `mimo-v2.5-pro` / `xiaomi/mimo-v2.5-pro` reuse the
DeepSeek V4-Pro rate table and `mimo-v2.5` / `xiaomi/mimo-v2.5` reuse the
DeepSeek V4-Flash rates. Existing DeepSeek pricing is unchanged (#2731, #2750).
- Added optional `[search].base_url` / `CODEWHALE_SEARCH_BASE_URL` support for
DuckDuckGo-compatible private search endpoints, while keeping
`DEEPSEEK_SEARCH_BASE_URL` as a legacy alias. Custom endpoints are gated by
their configured host, do not fall back to public Bing, and report the custom
host as the result source for diagnostics (#2436, #2510).

### Changed

Expand Down Expand Up @@ -154,8 +159,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Community

Thanks to **@sximelon** for reporting and fixing the saved-session resume
footer hint (#2758, #2760), **@cyq1017** for the restore-listing implementation
(#2513) and pending-input delivery-mode label work (#2532, #2054),
footer hint (#2758, #2760), **@cyq1017** for the custom
DuckDuckGo-compatible search endpoint, restore-listing implementation, and
pending-input delivery-mode label work (#2510, #2513, #2532, #2054),
**@Artenx** for the private-search endpoint report (#2436),
**@wywsoor** for the broader macOS/iTerm rollback UX report (#2494),
**@HUQIANTAO** for the `web_run` lock-splitting work (#2502), turn-metadata
prefix-cache stability work (#2517), and project-context cache direction
Expand Down
85 changes: 85 additions & 0 deletions crates/tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,11 @@ pub struct SearchConfig {
/// Search provider: `bing` | `duckduckgo` | `tavily` | `bocha` | `metaso` | `baidu` | `volcengine`. Default: `duckduckgo`.
#[serde(default)]
pub provider: Option<SearchProvider>,
/// Optional DuckDuckGo-compatible HTML endpoint. When set with the
/// DuckDuckGo provider, `web_search` appends the `q` query parameter to
/// this URL instead of using `https://html.duckduckgo.com/html/`.
#[serde(default)]
pub base_url: Option<String>,
/// API key for Tavily, Bocha, Metaso, Baidu, or Volcengine. Not required for Bing or DuckDuckGo.
/// Metaso also falls back to `METASO_API_KEY` env var, then a built-in default.
/// Baidu also falls back to `BAIDU_SEARCH_API_KEY` env var.
Expand Down Expand Up @@ -3803,6 +3808,12 @@ fn apply_env_overrides(config: &mut Config) {
.get_or_insert_with(SearchConfig::default)
.api_key = Some(value);
}
if let Ok(value) = codewhale_env_var("CODEWHALE_SEARCH_BASE_URL", "DEEPSEEK_SEARCH_BASE_URL") {
config
.search
.get_or_insert_with(SearchConfig::default)
.base_url = Some(value);
}
if let Ok(value) = std::env::var("DEEPSEEK_REQUIREMENTS_PATH") {
config.requirements_path = Some(value);
}
Expand Down Expand Up @@ -5524,6 +5535,25 @@ mod tests {
);
}

#[test]
fn search_config_preserves_custom_base_url() {
let config: Config = toml::from_str(
r#"
[search]
provider = "duckduckgo"
base_url = "https://search.internal.example/html/"
"#,
)
.expect("search config");

let search = config.search.expect("search table");
assert_eq!(search.provider, Some(SearchProvider::DuckDuckGo));
assert_eq!(
search.base_url.as_deref(),
Some("https://search.internal.example/html/")
);
}

#[test]
fn explicit_baidu_search_provider_is_preserved() {
let config: Config = toml::from_str(
Expand Down Expand Up @@ -5667,6 +5697,61 @@ mod tests {
);
}

#[test]
fn apply_env_overrides_sets_search_base_url() {
let _guard = lock_test_env();
let prev_codewhale = env::var_os("CODEWHALE_SEARCH_BASE_URL");
let prev_deepseek = env::var_os("DEEPSEEK_SEARCH_BASE_URL");
unsafe {
env::remove_var("CODEWHALE_SEARCH_BASE_URL");
env::set_var(
"DEEPSEEK_SEARCH_BASE_URL",
"https://search.internal.example/html/",
)
};
let mut config = Config::default();

apply_env_overrides(&mut config);

unsafe {
EnvGuard::restore_var("CODEWHALE_SEARCH_BASE_URL", prev_codewhale);
EnvGuard::restore_var("DEEPSEEK_SEARCH_BASE_URL", prev_deepseek);
}
assert_eq!(
config.search.and_then(|search| search.base_url),
Some("https://search.internal.example/html/".to_string())
);
}

#[test]
fn codewhale_search_base_url_env_wins_over_legacy_alias() {
let _guard = lock_test_env();
let prev_codewhale = env::var_os("CODEWHALE_SEARCH_BASE_URL");
let prev_deepseek = env::var_os("DEEPSEEK_SEARCH_BASE_URL");
unsafe {
env::set_var(
"CODEWHALE_SEARCH_BASE_URL",
"https://codewhale-search.example/html/",
);
env::set_var(
"DEEPSEEK_SEARCH_BASE_URL",
"https://legacy-search.example/html/",
);
}
let mut config = Config::default();

apply_env_overrides(&mut config);

unsafe {
EnvGuard::restore_var("CODEWHALE_SEARCH_BASE_URL", prev_codewhale);
EnvGuard::restore_var("DEEPSEEK_SEARCH_BASE_URL", prev_deepseek);
}
assert_eq!(
config.search.and_then(|search| search.base_url),
Some("https://codewhale-search.example/html/".to_string())
);
}

#[test]
fn search_provider_resolution_ignores_invalid_env_override() {
let _guard = lock_test_env();
Expand Down
4 changes: 4 additions & 0 deletions crates/tui/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ pub struct EngineConfig {
/// Metaso also falls back to `METASO_API_KEY` env var, then a built-in key.
/// Baidu also falls back to `BAIDU_SEARCH_API_KEY`.
pub search_api_key: Option<String>,
/// Optional DuckDuckGo-compatible HTML endpoint override.
pub search_base_url: Option<String>,
/// Per-step DeepSeek API timeout for sub-agent `create_message` requests.
/// Resolved from `[subagents] api_timeout_secs` (clamped to 1..=1800)
/// once at engine construction, then threaded onto every
Expand Down Expand Up @@ -408,6 +410,7 @@ impl Default for EngineConfig {
workshop: None,
search_provider: crate::config::SearchProvider::default(),
search_api_key: None,
search_base_url: None,
subagent_api_timeout: Duration::from_secs(
crate::config::DEFAULT_SUBAGENT_API_TIMEOUT_SECS,
),
Expand Down Expand Up @@ -2251,6 +2254,7 @@ In {new} mode: {policy}\n\n\
// Wire search provider config.
ctx.search_provider = self.config.search_provider;
ctx.search_api_key = self.config.search_api_key.clone();
ctx.search_base_url = self.config.search_base_url.clone();

let policy = sandbox_policy_for_mode(mode, &self.session.workspace);
let mut ctx = ctx.with_elevated_sandbox_policy(policy);
Expand Down
3 changes: 3 additions & 0 deletions crates/tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5745,6 +5745,7 @@ async fn run_exec_agent(
workshop: config.workshop.clone(),
search_provider: config.search_provider(),
search_api_key: config.search.as_ref().and_then(|s| s.api_key.clone()),
search_base_url: config.search.as_ref().and_then(|s| s.base_url.clone()),
tools_always_load: config.tools_always_load(),
tools: config.tools.clone(),
};
Expand Down Expand Up @@ -6317,6 +6318,7 @@ mod doctor_endpoint_tests {
let config = Config {
search: Some(crate::config::SearchConfig {
provider: Some(crate::config::SearchProvider::DuckDuckGo),
base_url: None,
api_key: None,
}),
..Default::default()
Expand Down Expand Up @@ -6356,6 +6358,7 @@ mod doctor_endpoint_tests {
let config = Config {
search: Some(crate::config::SearchConfig {
provider: Some(crate::config::SearchProvider::Bing),
base_url: None,
api_key: None,
}),
..Default::default()
Expand Down
1 change: 1 addition & 0 deletions crates/tui/src/runtime_threads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2083,6 +2083,7 @@ impl RuntimeThreadManager {
workshop: self.config.workshop.clone(),
search_provider: self.config.search_provider(),
search_api_key: self.config.search.as_ref().and_then(|s| s.api_key.clone()),
search_base_url: self.config.search.as_ref().and_then(|s| s.base_url.clone()),
tools_always_load: self.config.tools_always_load(),
tools: self.config.tools.clone(),
};
Expand Down
5 changes: 5 additions & 0 deletions crates/tui/src/tools/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ pub struct ToolContext {
/// Metaso also falls back to `METASO_API_KEY` env var, then a built-in key.
/// Baidu also falls back to `BAIDU_SEARCH_API_KEY`.
pub search_api_key: Option<String>,
/// Optional DuckDuckGo-compatible HTML endpoint override for `web_search`.
pub search_base_url: Option<String>,

/// Per-session workshop variable store (#548). Holds the raw content of
/// the most recent large-tool routing event so the parent can call
Expand Down Expand Up @@ -210,6 +212,7 @@ impl ToolContext {
large_output_router: None,
search_provider: crate::config::SearchProvider::default(),
search_api_key: None,
search_base_url: None,
workshop_vars: None,
}
}
Expand Down Expand Up @@ -247,6 +250,7 @@ impl ToolContext {
large_output_router: None,
search_provider: crate::config::SearchProvider::default(),
search_api_key: None,
search_base_url: None,
workshop_vars: None,
}
}
Expand Down Expand Up @@ -284,6 +288,7 @@ impl ToolContext {
large_output_router: None,
search_provider: crate::config::SearchProvider::default(),
search_api_key: None,
search_base_url: None,
workshop_vars: None,
}
}
Expand Down
Loading
Loading