F5: web search/fetch on the Kimi services (kimi-cli parity)
web_search now speaks the Kimi search service (kimi-cli tools/web/search.py,
wire-verified against api.kimi.com):
- POST {coding_base}/search with {text_query, limit 1-20 (default 5),
enable_page_crawling, timeout_seconds: 30}, OAuth bearer +
X-Msh-Tool-Call-Id; results render in kimi-cli's Title/Date/URL/Summary
schema with result URLs as citations.
- The old implementation called the xAI Responses API with a search model;
that client is fully replaced and the entire model-based config surface
is excised root-and-branch: web_search_model config keys/env/CLI plumbing,
resolve_web_search_sampling_config, toolset web_search SamplerConfig,
RemoteSettings.web_search_model, default_web_search_model.
- Enablement is now purely structural: the service exists only on the Kimi
Code subscription channel, so OAuth sessions get Enabled and API-key-only
sessions get Disabled (tool absent) — per PRD F5.
web_fetch gains the Kimi fetch service as its primary path (kimi-cli
tools/web/fetch.py): POST {coding_base}/fetch with {url}, Accept:
text/markdown, OAuth bearer + X-Msh-Tool-Call-Id; the 200 body is the
extracted markdown (still overflow-budgeted). Any service failure falls
back to the existing local pipeline (SSRF guards, cache, extraction).
The tool gate defaults ON now (kimi-cli always offers FetchURL) and the
egress User-Agent no longer claims grok-agent/x.ai.
Verified end-to-end against the scripted mock service: a headless session
drove web_search (limit/crawling/call-id observed on the wire) then
web_fetch (Accept + call-id observed) to completion.
This commit is contained in:
@@ -153,9 +153,9 @@ pub(crate) fn atomic_write_string(path: &std::path::Path, content: &str) -> std:
|
||||
}
|
||||
|
||||
/// Merge `[toolset.ask_user_question]` into the root table. `[toolset]` is
|
||||
/// deliberately NOT merged wholesale — it carries runtime-only structs
|
||||
/// (`web_search` sampler etc.) whose serialized defaults must never land in
|
||||
/// the user file — so only this settings-writable sub-table round-trips.
|
||||
/// deliberately NOT merged wholesale — its other sub-tables (`bash` etc.)
|
||||
/// carry serialized defaults that must never land in the user file — so only
|
||||
/// this settings-writable sub-table round-trips.
|
||||
fn merge_ask_user_question_section(
|
||||
table: &mut TomlMap<String, TomlValue>,
|
||||
ask: &crate::tools::config::AskUserQuestionToolConfig,
|
||||
@@ -245,7 +245,7 @@ mod tests {
|
||||
|
||||
/// The `[toolset.ask_user_question]` settings write merges only that
|
||||
/// sub-table: the toggled field lands, hand-written sibling keys survive,
|
||||
/// and no other `[toolset]` defaults (bash/web_search) are splatted into
|
||||
/// and no other `[toolset]` defaults (bash etc.) are splatted into
|
||||
/// the user file. All-None leaves the file untouched.
|
||||
#[test]
|
||||
fn ask_user_question_merge_writes_subtable_without_splatting_toolset() {
|
||||
@@ -830,7 +830,6 @@ auto_update = true
|
||||
if let TomlValue::Table(t) = v {
|
||||
assert_eq!(t.len(), 1);
|
||||
assert!(t.contains_key("default"));
|
||||
assert!(!t.contains_key("web_search"));
|
||||
assert!(!t.contains_key("session_summary"));
|
||||
assert!(!t.contains_key("image_description"));
|
||||
assert!(!t.contains_key("hidden_models"));
|
||||
@@ -935,7 +934,10 @@ auto_update = true
|
||||
fn merge_section_models_only_updates_set_fields_preserves_others() {
|
||||
let mut table = TomlMap::new();
|
||||
let mut models = TomlMap::new();
|
||||
models.insert("web_search".into(), TomlValue::String("old-search".into()));
|
||||
models.insert(
|
||||
"session_summary".into(),
|
||||
TomlValue::String("old-title".into()),
|
||||
);
|
||||
models.insert("unmodeled_foo".into(), TomlValue::String("keep-me".into()));
|
||||
table.insert("models".into(), TomlValue::Table(models));
|
||||
let cfg = crate::agent::config::ModelsConfig {
|
||||
@@ -946,19 +948,19 @@ auto_update = true
|
||||
let m = table.get("models").unwrap().as_table().unwrap();
|
||||
assert_eq!(m.get("default").and_then(|v| v.as_str()), Some("grok-new"));
|
||||
assert_eq!(
|
||||
m.get("web_search").and_then(|v| v.as_str()),
|
||||
Some("old-search")
|
||||
m.get("session_summary").and_then(|v| v.as_str()),
|
||||
Some("old-title")
|
||||
);
|
||||
assert_eq!(
|
||||
m.get("unmodeled_foo").and_then(|v| v.as_str()),
|
||||
Some("keep-me")
|
||||
);
|
||||
assert!(!m.contains_key("session_summary"));
|
||||
assert!(!m.contains_key("image_description"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persist_preferred_model_flow_roundtrips_via_load_and_new_from_toml_cfg() {
|
||||
let original = "[models]\ndefault = \"grok-old\"\nweb_search = \"some-search\"\n";
|
||||
let original = "[models]\ndefault = \"grok-old\"\n";
|
||||
let root: TomlValue = toml::from_str(original).unwrap();
|
||||
let mut cfg = load_config_from_toml(&root);
|
||||
cfg.models.default = Some("grok-persisted".to_string());
|
||||
|
||||
Reference in New Issue
Block a user