Skip to content

Commit 4b57feb

Browse files
committed
fix(web): 适配 Moonshot K3 原生搜索协议
按官方 Formula API 完成 K3 工具声明、fiber 执行和结果回填链路,并保留 K2.6 旧协议兼容。\n\n复用 Kimi Code 端点常量,记录未实网验证的降级决策及智谱中国站存量配置影响。 Signed-off-by: hexin <372726039@qq.com>
1 parent 7c1963c commit 4b57feb

5 files changed

Lines changed: 287 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Z.AI configurations using `https://open.bigmodel.cn/api/paas/v4` are now
13+
treated as official Zhipu routes for model-alias normalization, credential
14+
scoping, and native web search. Existing unknown model IDs continue to pass
15+
through unchanged.
16+
1017
## [0.9.5] - 2026-08-08
1118

1219
Codewhale v0.9.5 consolidates the terminal application into one compiled

crates/config/src/route/capabilities.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
88
use serde::{Deserialize, Serialize};
99

10-
use crate::{DEFAULT_MOONSHOT_BASE_URL, ProviderKind, provider_base_url_is_official};
10+
use crate::{
11+
DEFAULT_KIMI_CODE_BASE_URL, DEFAULT_MOONSHOT_BASE_URL, ProviderKind,
12+
provider_base_url_is_official,
13+
};
1114

1215
/// Whether a resolved provider/model offering supports one capability.
1316
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
@@ -86,6 +89,8 @@ pub(crate) fn documented_server_side_web_search(
8689
wire_model_id.as_str(),
8790
"deepseek-v4-flash" | "deepseek-v4-pro" | "deepseek-v4-flash-vision-exp"
8891
),
92+
// K3 uses the Formula API official-tools channel; K2.6 retains the
93+
// earlier `$web_search` contract with thinking disabled.
8994
"moonshot" => matches!(wire_model_id.as_str(), "kimi-k3" | "kimi-k2.6"),
9095
"modelstudio-token-plan" => matches!(
9196
wire_model_id.as_str(),
@@ -121,7 +126,7 @@ pub(crate) fn documented_server_side_web_search_for_route(
121126
}
122127

123128
if provider == ProviderKind::Moonshot
124-
&& normalized == "https://api.kimi.com/coding/v1"
129+
&& normalized == DEFAULT_KIMI_CODE_BASE_URL
125130
&& matches!(
126131
wire_model_id.trim().to_ascii_lowercase().as_str(),
127132
"k3" | "k3-256k" | "kimi-for-coding" | "kimi-for-coding-highspeed"
@@ -143,8 +148,8 @@ pub(crate) fn documented_server_side_web_search_for_route(
143148
| ProviderKind::Anthropic
144149
| ProviderKind::Xai
145150
| ProviderKind::XiaomiMimo => true,
146-
// Moonshot's built-in `$web_search` contract belongs to the exact
147-
// direct API product. Kimi Code's exact `/coding/v1` product is
151+
// Moonshot's Formula/legacy `$web_search` contracts belong to the
152+
// exact direct API product. Kimi Code's exact `/coding/v1` product is
148153
// handled above; adjacent coding paths must remain fail-closed.
149154
ProviderKind::Moonshot => normalized == DEFAULT_MOONSHOT_BASE_URL,
150155
// Token Plan exposes Harness web_search only on its Responses API.
@@ -245,6 +250,14 @@ mod tests {
245250
documented_server_side_web_search("deepseek", "deepseek-v4-flash"),
246251
CapabilityState::Supported
247252
);
253+
assert_eq!(
254+
documented_server_side_web_search("moonshot", "kimi-k2.6"),
255+
CapabilityState::Supported
256+
);
257+
assert_eq!(
258+
documented_server_side_web_search("moonshot", "kimi-k3"),
259+
CapabilityState::Supported
260+
);
248261
assert_eq!(
249262
documented_server_side_web_search("modelstudio-token-plan", "qwen3.8-max"),
250263
CapabilityState::Supported
@@ -282,6 +295,14 @@ mod tests {
282295
),
283296
CapabilityState::Supported
284297
);
298+
assert_eq!(
299+
documented_server_side_web_search_for_route(
300+
ProviderKind::Moonshot,
301+
"kimi-k2.6",
302+
"https://api.moonshot.ai/v1",
303+
),
304+
CapabilityState::Supported
305+
);
285306
assert_eq!(
286307
documented_server_side_web_search_for_route(
287308
ProviderKind::ModelstudioTokenPlan,

crates/tui/src/client/provider_native_search.rs

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,23 @@ impl ProviderNativeSearchClient {
182182
.await
183183
.context("provider-native web search returned invalid JSON")
184184
}
185+
186+
pub(super) async fn get_json(&self, url: &str) -> Result<Value> {
187+
let response = self
188+
.inner
189+
.send_with_retry(|| {
190+
self.inner
191+
.http_client
192+
.get(url)
193+
.header("Accept", "application/json")
194+
})
195+
.await
196+
.context("provider-native web search request failed")?;
197+
response
198+
.json::<Value>()
199+
.await
200+
.context("provider-native web search returned invalid JSON")
201+
}
185202
}
186203

187204
#[derive(Clone, Copy)]
@@ -510,7 +527,7 @@ fn bounded_answer(parts: Vec<String>) -> Option<String> {
510527
mod tests {
511528
use super::*;
512529
use crate::config::{Config, ProviderConfig, ProvidersConfig};
513-
use wiremock::matchers::{body_partial_json, header, method, path};
530+
use wiremock::matchers::{body_partial_json, body_string_contains, header, method, path};
514531
use wiremock::{Mock, MockServer, ResponseTemplate};
515532

516533
fn request() -> ProviderNativeSearchRequest {
@@ -809,18 +826,82 @@ mod tests {
809826
}
810827

811828
#[tokio::test]
812-
async fn moonshot_adapter_declares_builtin_search_and_recovers_citations() {
829+
async fn moonshot_k3_adapter_uses_formula_search_and_recovers_citations() {
813830
let server = MockServer::start().await;
831+
Mock::given(method("GET"))
832+
.and(path("/v1/formulas/moonshot/web-search:latest/tools"))
833+
.and(header("authorization", "Bearer moonshot-test-key"))
834+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
835+
"object": "list",
836+
"tools": [{
837+
"type": "function",
838+
"function": {
839+
"name": "web_search",
840+
"description": "Search the web",
841+
"parameters": {
842+
"type": "object",
843+
"properties": { "query": { "type": "string" } },
844+
"required": ["query"]
845+
}
846+
}
847+
}]
848+
})))
849+
.expect(1)
850+
.mount(&server)
851+
.await;
814852
Mock::given(method("POST"))
815853
.and(path("/v1/chat/completions"))
816854
.and(header("authorization", "Bearer moonshot-test-key"))
817855
.and(body_partial_json(json!({
818856
"model": "kimi-k3",
819857
"tools": [{
820-
"type": "builtin_function",
821-
"function": { "name": "$web_search" }
858+
"type": "function",
859+
"function": { "name": "web_search" }
822860
}]
823861
})))
862+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
863+
"choices": [{
864+
"finish_reason": "tool_calls",
865+
"message": {
866+
"role": "assistant",
867+
"content": "",
868+
"tool_calls": [{
869+
"id": "web_search:0",
870+
"type": "function",
871+
"function": {
872+
"name": "web_search",
873+
"arguments": "{\"query\":\"current release\"}"
874+
}
875+
}]
876+
}
877+
}]
878+
})))
879+
.up_to_n_times(1)
880+
.expect(1)
881+
.mount(&server)
882+
.await;
883+
Mock::given(method("POST"))
884+
.and(path(
885+
"/v1/formulas/moonshot/web-search:latest/fibers",
886+
))
887+
.and(header("authorization", "Bearer moonshot-test-key"))
888+
.and(body_partial_json(json!({
889+
"name": "web_search",
890+
"arguments": "{\"query\":\"current release\"}"
891+
})))
892+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
893+
"status": "succeeded",
894+
"context": {
895+
"encrypted_output": "----MOONSHOT ENCRYPTED BEGIN----result----MOONSHOT ENCRYPTED END----"
896+
}
897+
})))
898+
.expect(1)
899+
.mount(&server)
900+
.await;
901+
Mock::given(method("POST"))
902+
.and(path("/v1/chat/completions"))
903+
.and(header("authorization", "Bearer moonshot-test-key"))
904+
.and(body_string_contains("MOONSHOT ENCRYPTED BEGIN"))
824905
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
825906
"choices": [{
826907
"finish_reason": "stop",

0 commit comments

Comments
 (0)