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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add provider-native web search for documented Xiaomi MiMo 2.5 Pro and 2.5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the human contributor’s commit credit

This rescued change is authored and committed solely by Codex <codex@openai.com>, has no Co-authored-by trailer for h3c-hexin, and the original human-authored commit cf8f4b74 is not in this commit’s ancestry. Mentioning the contributor only in the commit prose does not preserve durable Git authorship; retain the original commit or add the canonical identity from .github/AUTHOR_MAP.

AGENTS.md reference: AGENTS.md:L102-L106

Useful? React with 👍 / 👎.

chat routes while keeping neighboring models and custom gateways fail-closed.
- Add structured provider-native web search for exact Z.AI global and Zhipu
China general API routes, using each site's documented search engine value.
Existing `open.bigmodel.cn` configurations now share Z.AI's official model
Expand Down
7 changes: 7 additions & 0 deletions crates/config/src/route/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl CapabilityState {
/// - OpenAI Responses web search: <https://developers.openai.com/api/docs/guides/tools-web-search>
/// - Anthropic web search tool: <https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool>
/// - xAI web search tool: <https://docs.x.ai/developers/tools/web-search>
/// - Xiaomi MiMo web search: <https://mimo.mi.com/docs/en-US/usage-guide/tool-calling/web-search>
/// - Z.AI Web Search API: <https://docs.z.ai/api-reference/tools/web-search>
/// - Zhipu Web Search API: <https://docs.bigmodel.cn/api-reference/工具-api/网络搜索>
/// - Alibaba Model Studio Token Plan Harness tools: <https://help.aliyun.com/en/model-studio/token-plan-harness-tool>
Expand Down Expand Up @@ -80,6 +81,7 @@ pub(crate) fn documented_server_side_web_search(
| "claude-sonnet-4-6"
),
"xai" => matches!(wire_model_id.as_str(), "grok-4.6" | "grok-4.5"),
"xiaomi-mimo" => matches!(wire_model_id.as_str(), "mimo-v2.5-pro" | "mimo-v2.5"),
"zai" => matches!(
wire_model_id.as_str(),
"glm-5.3" | "glm-5.3-flash" | "glm-5.2" | "glm-5.1" | "glm-5-turbo"
Expand Down Expand Up @@ -198,6 +200,10 @@ mod tests {
documented_server_side_web_search("anthropic", "claude-sonnet-4-6"),
CapabilityState::Supported
);
assert_eq!(
documented_server_side_web_search("xiaomi-mimo", "mimo-v2.5-pro"),
CapabilityState::Supported
);
assert_eq!(
documented_server_side_web_search("zai", "GLM-5.3"),
CapabilityState::Supported
Expand All @@ -219,6 +225,7 @@ mod tests {
("xai", "grok-4.6-latest"),
("xai", "grok-4.5-fast"),
("anthropic", "claude-haiku-4-5"),
("xiaomi-mimo", "mimo-v2.5-pro-ultraspeed"),
("zai", "glm-5.3-preview"),
("modelstudio-coding-plan", "qwen3.8-max"),
("modelstudio-token-plan", "qwen3.8-max-preview"),
Expand Down
41 changes: 41 additions & 0 deletions crates/config/src/route/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1433,6 +1433,47 @@ fn provider_native_web_search_requires_exact_direct_endpoint_offering() {
);
}

#[test]
fn mimo_native_search_is_exact_to_documented_chat_models() {
use crate::route::CapabilityState;

let resolver = RouteResolver::new();
for model in ["mimo-v2.5-pro", "mimo-v2.5"] {
let direct = resolver
.resolve(&req(Some(ProviderKind::XiaomiMimo), Some(model)))
.expect("official MiMo chat route resolves");
assert_eq!(
direct.capabilities().server_side_web_search,
CapabilityState::Supported
);
}

let neighboring = resolver
.resolve(&req(
Some(ProviderKind::XiaomiMimo),
Some("mimo-v2.5-pro-ultraspeed"),
))
.expect("neighboring MiMo model resolves");
assert_eq!(
neighboring.capabilities().server_side_web_search,
CapabilityState::Unknown
);

let custom = resolver
.resolve(&RouteRequest {
explicit_provider: Some(ProviderKind::XiaomiMimo),
model_selector: Some(LogicalModelRef::from("mimo-v2.5-pro")),
saved_provider_model: None,
base_url_override: Some("https://compatible.example.test/v1".to_string()),
limit_overrides: Vec::new(),
})
.expect("custom MiMo-compatible route resolves");
assert_eq!(
custom.capabilities().server_side_web_search,
CapabilityState::Unknown
);
}

#[test]
fn zai_native_search_requires_exact_general_api_product() {
use crate::route::CapabilityState;
Expand Down
2 changes: 2 additions & 0 deletions crates/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add provider-native web search for documented Xiaomi MiMo 2.5 Pro and 2.5
chat routes while keeping neighboring models and custom gateways fail-closed.
- Add structured provider-native web search for exact Z.AI global and Zhipu
China general API routes, using each site's documented search engine value.
Existing `open.bigmodel.cn` configurations now share Z.AI's official model
Expand Down
95 changes: 95 additions & 0 deletions crates/tui/src/client/provider_native_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ impl ProviderNativeSearchClient {
ApiProvider::Openai
| ApiProvider::Anthropic
| ApiProvider::Xai
| ApiProvider::XiaomiMimo
| ApiProvider::Zai
| ApiProvider::ModelstudioTokenPlan
| ApiProvider::Deepseek
Expand Down Expand Up @@ -141,6 +142,7 @@ impl ProviderNativeSearchClient {
2_048_u32.min(route_cap),
)
}
ApiProvider::XiaomiMimo => build_mimo_search_body(&self.inner.default_model, request),
ApiProvider::Zai => zai::build_body(request, &self.inner.base_url)?,
_ => bail!("active provider has no native web-search adapter"),
};
Expand All @@ -152,6 +154,7 @@ impl ProviderNativeSearchClient {
responses_api_url(&self.inner.base_url, self.inner.api_provider)
}
ApiProvider::Anthropic => anthropic_messages_url(&self.inner.base_url),
ApiProvider::XiaomiMimo => api_url(&self.inner.base_url, "chat/completions"),
ApiProvider::Zai => api_url(&self.inner.base_url, "web_search"),
_ => unreachable!("provider checked above"),
};
Expand Down Expand Up @@ -179,6 +182,7 @@ impl ProviderNativeSearchClient {
| ApiProvider::Deepseek
| ApiProvider::DeepseekCN => parse_responses_search(&payload),
ApiProvider::Anthropic => parse_anthropic_search(&payload),
ApiProvider::XiaomiMimo => parse_mimo_search(&payload),
ApiProvider::Zai => zai::parse(&payload),
_ => unreachable!("provider checked above"),
};
Expand Down Expand Up @@ -262,6 +266,23 @@ fn build_anthropic_search_body(
})
}

fn build_mimo_search_body(model: &str, request: &ProviderNativeSearchRequest) -> Value {
json!({
"model": model,
"messages": [{ "role": "user", "content": search_prompt(request) }],
"tools": [{
"type": "web_search",
"max_keyword": 1,
"force_search": true,
"limit": request.max_results,
}],
"tool_choice": "auto",
"max_completion_tokens": 2_048,
"stream": false,
"thinking": { "type": "disabled" },
})
}

fn anthropic_messages_url(base_url: &str) -> String {
let base = base_url.trim_end_matches('/');
if base.ends_with("/v1") {
Expand Down Expand Up @@ -388,6 +409,48 @@ fn parse_anthropic_search(payload: &Value) -> ProviderNativeSearchResponse {
}
}

fn parse_mimo_search(payload: &Value) -> ProviderNativeSearchResponse {
let message = payload.pointer("/choices/0/message");
let answer = message
.and_then(|value| value.get("content"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|text| !text.is_empty())
.map(str::to_string);
let mut citations = Vec::new();
if let Some(annotations) = message
.and_then(|value| value.get("annotations"))
.and_then(Value::as_array)
.or_else(|| payload.get("annotations").and_then(Value::as_array))
{
for annotation in annotations {
let Some(url) = annotation.get("url").and_then(Value::as_str) else {
continue;
};
let title = annotation
.get("title")
.and_then(Value::as_str)
.map(str::to_string);
let snippet = annotation
.get("summary")
.and_then(Value::as_str)
.map(str::to_string);
let published = annotation
.get("publish_time")
.and_then(Value::as_str)
.map(str::to_string);
push_citation(
&mut citations,
citation_from_url(url, title, snippet, published),
);
}
}
ProviderNativeSearchResponse {
answer: bounded_answer(answer.into_iter().collect()),
citations,
}
}

fn citation_from_value(
value: &Value,
snippet: Option<String>,
Expand Down Expand Up @@ -596,6 +659,16 @@ mod tests {
assert_eq!(tiny_route["max_tokens"], 128);
}

#[test]
fn mimo_payload_forces_bounded_web_search_plugin() {
let body = build_mimo_search_body("mimo-v2.5-pro", &request());
assert_eq!(body["tools"][0]["type"], "web_search");
assert_eq!(body["tools"][0]["force_search"], true);
assert_eq!(body["tools"][0]["limit"], 3);
assert_eq!(body["max_completion_tokens"], 2_048);
assert_eq!(body["thinking"]["type"], "disabled");
}

#[test]
fn responses_parser_separates_answer_and_deduplicated_citations() {
let payload = json!({
Expand Down Expand Up @@ -706,6 +779,28 @@ mod tests {
);
}

#[test]
fn mimo_parser_keeps_non_streaming_annotations() {
let parsed = parse_mimo_search(&json!({
"choices": [{
"message": {
"content": "Grounded answer.",
"annotations": [{
"type": "url_citation",
"url": "https://example.com/weather",
"title": "Weather",
"summary": "Forecast",
"publish_time": "2026-08-28"
}]
}
}]
}));
assert_eq!(parsed.answer.as_deref(), Some("Grounded answer."));
assert_eq!(parsed.citations.len(), 1);
assert_eq!(parsed.citations[0].snippet.as_deref(), Some("Forecast"));
assert_eq!(parsed.citations[0].published.as_deref(), Some("2026-08-28"));
}

#[test]
fn non_http_citations_are_rejected() {
let payload = json!({ "citations": ["javascript:alert(1)"] });
Expand Down
2 changes: 1 addition & 1 deletion docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ overlay and lets DSH resolve its own keys.
| `volcengine` | `[providers.volcengine]` | `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, `ARK_API_KEY` | `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, `ARK_BASE_URL`; default `https://ark.cn-beijing.volces.com/api/coding/v3` | `DeepSeek-V4-Pro`, `DeepSeek-V4-Flash` | Volcengine/Volcano Engine Ark OpenAI-compatible coding endpoint. `VOLCENGINE_MODEL` and `VOLCENGINE_ARK_MODEL` are accepted. |
| `openrouter` | `[providers.openrouter]` | `OPENROUTER_API_KEY` | `OPENROUTER_BASE_URL`; default `https://openrouter.ai/api/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`; recent large IDs include `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `google/gemma-4-31b-it`, `z-ai/glm-5.1`, `z-ai/glm-5.2`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6` | Additive open-model routing layer. It does not replace DeepSeek; it lets users route supported model IDs through OpenRouter when they choose it. |
| `orcarouter` | `[providers.orcarouter]` | `ORCAROUTER_API_KEY` | `ORCAROUTER_BASE_URL`; default `https://api.orcarouter.ai/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`; router alias `orcarouter/auto`; recent large IDs mirror the OpenRouter namespaced catalog | [OrcaRouter](https://www.orcarouter.ai) OpenAI-compatible aggregation gateway. Shares the namespaced `vendor/model` wire-model format and DeepSeek model set with OpenRouter, so the OpenRouter base-URL and model-normalization rules apply. `ORCAROUTER_MODEL` is accepted. Provider aliases: `orcarouter`, `orca_router`, `orca`. |
| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`, `XIAOMI_MIMO_MODE`, `MIMO_MODE`; default `https://token-plan-sgp.xiaomimimo.com/v1` | Chat: `mimo-v2.5-pro`, `mimo-v2.5-pro-ultraspeed`, `mimo-v2.5`; speech/TTS: `mimo-v2.5-tts`, `mimo-v2.5-tts-voicedesign`, `mimo-v2.5-tts-voiceclone`, `mimo-v2-tts` | Xiaomi MiMo OpenAI-compatible chat completions route. Token Plan keys (`tp-...`) use `api-key` auth and the token-plan endpoint by default; pay-as-you-go mode uses standard API keys (`sk-...`) and `https://api.xiaomimimo.com/v1`. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. Token Plan cost/usage is credit/quota based; Codewhale shows it as unknown until Xiaomi exposes a reliable balance API. `codewhale speech` / `tts` uses the TTS models. |
| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`, `XIAOMI_MIMO_MODE`, `MIMO_MODE`; default `https://token-plan-sgp.xiaomimimo.com/v1` | Chat: `mimo-v2.5-pro`, `mimo-v2.5-pro-ultraspeed`, `mimo-v2.5`; speech/TTS: `mimo-v2.5-tts`, `mimo-v2.5-tts-voicedesign`, `mimo-v2.5-tts-voiceclone`, `mimo-v2-tts` | Xiaomi MiMo OpenAI-compatible chat completions route. `mimo-v2.5-pro` and `mimo-v2.5` can use the documented provider-native web-search plugin; ultraspeed, speech/TTS, and custom-compatible routes do not inherit it. Token Plan keys (`tp-...`) use `api-key` auth and the token-plan endpoint by default; pay-as-you-go mode uses standard API keys (`sk-...`) and `https://api.xiaomimimo.com/v1`. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. Token Plan cost/usage is credit/quota based; Codewhale shows it as unknown until Xiaomi exposes a reliable balance API. `codewhale speech` / `tts` uses the TTS models. |
| `novita` | `[providers.novita]` | `NOVITA_API_KEY` | `NOVITA_BASE_URL`; default `https://api.novita.ai/openai/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | OpenAI-compatible hosted route for DeepSeek model IDs. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |
| `fireworks` | `[providers.fireworks]` | `FIREWORKS_API_KEY` | `FIREWORKS_BASE_URL`; default `https://api.fireworks.ai/inference/v1` | `accounts/fireworks/models/deepseek-v4-pro` | OpenAI-compatible hosted route. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |
| `siliconflow` | `[providers.siliconflow]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.com/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | OpenAI-compatible hosted route. Official docs use the `.com` endpoint. `SILICONFLOW_MODEL` is accepted. Reasoning aliases `deepseek-reasoner` and `deepseek-r1` map to Pro; `deepseek-chat` and `deepseek-v3` map to Flash. |
Expand Down
Loading