From a4b9dc5f528f95b9dac6995609b8825cab89fcfa Mon Sep 17 00:00:00 2001 From: ummon-v <252456890+ummon-v@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:01:26 +0000 Subject: [PATCH] mistral: Add Z.ai GLM (latest version) and show thinking toggle for supported models (#63535) # Objective - Add Z.ai GLM, now hosted by Mistral as a third-party open model, to the built-in Mistral model list. - Fix the thinking selector never appearing for Mistral models that support reasoning (mistral-medium-latest, mistral-small-latest). ## Solution - Added a `ZaiGlmLatest` variant to `mistral::Model` that uses Mistral's `zai-glm-latest` model id: 1M token context window, tool calling enabled, image input disabled, thinking enabled. The provider builds its model list by iterating this enum, so no further wiring was needed. - This PR initially targeted GLM 5.2 (`zai-glm-5-2`), but Mistral released 5.3 while it was open. Switching to the `zai-glm-latest` alias keeps Zed on the newest GLM release without requiring a new PR each time Mistral bumps the version, mirroring how Mistral's own model ids work for their flagship models. - Implemented the missing `LanguageModel::supports_thinking()` override in the Mistral provider, delegating to `mistral::Model::supports_thinking()`. The request-building code already sent `reasoning_effort` and serialized thinking parts, but the UI toggle was hidden because the trait method fell back to the default `false`. ## Testing - `./script/clippy -p mistral` and `cargo check -p language_models` pass. - Existing unit tests in the provider cover the `reasoning_effort` request mapping; no new behavior beyond the enum variant and the trait delegation. - Reviewers can test with a Mistral API key: `zai-glm-latest` appears in the model dropdown, and the thinking toggle now shows for zai-glm-latest, mistral-medium-latest, and mistral-small-latest. - Tested on macOS; changes are platform-independent. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Added Z.ai GLM to the Mistral provider and fixed the thinking toggle for Mistral Small and Medium. --------- Co-authored-by: dino --- .../language_models/src/provider/mistral.rs | 75 +++++++++++++++---- crates/mistral/src/mistral.rs | 20 ++++- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 78841f097c7..3884896cb8b 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -319,6 +319,14 @@ impl LanguageModel for MistralLanguageModel { self.model.supports_images() } + fn supports_thinking(&self) -> bool { + self.model.supports_thinking() + } + + fn supports_disabling_thinking(&self) -> bool { + self.model.supports_disabling_thinking() + } + fn telemetry_id(&self) -> String { format!("mistral/{}", self.model.id()) } @@ -583,9 +591,18 @@ pub fn into_mistral( }) }) .collect::>()?, - reasoning_effort: if model.supports_thinking() && request.thinking_allowed { + reasoning_effort: if !model.supports_thinking() { + None + } else if request.thinking_allowed { Some(mistral::ReasoningEffort::High) + } else if model.supports_disabling_thinking() { + // Explicitly disable thinking rather than relying on the API's + // default. + Some(mistral::ReasoningEffort::None) } else { + // Models for which thinking can't be disabled will reject + // "none" as the `reasoning_effort` value. Omitting the field is + // the only way to request the API's default effort. None }, }, @@ -937,20 +954,52 @@ mod tests { max_output_tokens: None, }; - let (mistral_request, _) = - into_mistral(request(true), mistral::Model::MistralMediumLatest, None).unwrap(); - assert_eq!( - mistral_request.reasoning_effort, - Some(mistral::ReasoningEffort::High) - ); + let cases = vec![ + ( + mistral::Model::MistralSmallLatest, + true, + Some(mistral::ReasoningEffort::High), + ), + ( + mistral::Model::MistralSmallLatest, + false, + Some(mistral::ReasoningEffort::None), + ), + ( + mistral::Model::MistralMediumLatest, + true, + Some(mistral::ReasoningEffort::High), + ), + ( + mistral::Model::MistralMediumLatest, + false, + Some(mistral::ReasoningEffort::None), + ), + ( + mistral::Model::ZaiGlmLatest, + true, + Some(mistral::ReasoningEffort::High), + ), + // Z.ai GLM always thinks and rejects "none", so the field is + // omitted when the toggle is off instead of sending an explicit + // value. + (mistral::Model::ZaiGlmLatest, false, None), + // Ensure that, for non-thinking models, `reasoning_effort` is + // always omitted. + (mistral::Model::CodestralLatest, true, None), + (mistral::Model::CodestralLatest, false, None), + ]; - let (mistral_request, _) = - into_mistral(request(false), mistral::Model::MistralMediumLatest, None).unwrap(); - assert_eq!(mistral_request.reasoning_effort, None); + for (model, thinking_allowed, reasoning_effort) in cases { + let (mistral_request, _) = into_mistral(request(thinking_allowed), model, None) + .expect("should be able to convert request"); - let (mistral_request, _) = - into_mistral(request(true), mistral::Model::CodestralLatest, None).unwrap(); - assert_eq!(mistral_request.reasoning_effort, None); + assert_eq!( + mistral_request.reasoning_effort, reasoning_effort, + "reasoning_effort should match, expected {:?}, got {:?}", + reasoning_effort, mistral_request.reasoning_effort + ) + } } #[test] diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index 718207ebb2c..861979548d7 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -66,6 +66,9 @@ pub enum Model { #[serde(rename = "ministral-14b-latest", alias = "ministral-14b-latest")] Ministral14bLatest, + #[serde(rename = "zai-glm-latest", alias = "zai-glm-latest")] + ZaiGlmLatest, + #[serde(rename = "custom")] Custom { name: String, @@ -94,6 +97,7 @@ impl Model { "ministral-3b-latest" => Ok(Self::Ministral3bLatest), "ministral-8b-latest" => Ok(Self::Ministral8bLatest), "ministral-14b-latest" => Ok(Self::Ministral14bLatest), + "zai-glm-latest" => Ok(Self::ZaiGlmLatest), invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), } } @@ -107,6 +111,7 @@ impl Model { Self::Ministral3bLatest => "ministral-3b-latest", Self::Ministral8bLatest => "ministral-8b-latest", Self::Ministral14bLatest => "ministral-14b-latest", + Self::ZaiGlmLatest => "zai-glm-latest", Self::Custom { name, .. } => name, } } @@ -120,6 +125,7 @@ impl Model { Self::Ministral3bLatest => "ministral-3b-latest", Self::Ministral8bLatest => "ministral-8b-latest", Self::Ministral14bLatest => "ministral-14b-latest", + Self::ZaiGlmLatest => "zai-glm-latest", Self::Custom { name, display_name, .. } => display_name.as_ref().unwrap_or(name), @@ -135,6 +141,7 @@ impl Model { Self::Ministral3bLatest => 256000, Self::Ministral8bLatest => 256000, Self::Ministral14bLatest => 256000, + Self::ZaiGlmLatest => 1_048_576, Self::Custom { max_tokens, .. } => *max_tokens, } } @@ -156,7 +163,8 @@ impl Model { | Self::MistralSmallLatest | Self::Ministral3bLatest | Self::Ministral8bLatest - | Self::Ministral14bLatest => true, + | Self::Ministral14bLatest + | Self::ZaiGlmLatest => true, Self::Custom { supports_tools, .. } => supports_tools.unwrap_or(false), } } @@ -169,7 +177,7 @@ impl Model { | Self::Ministral3bLatest | Self::Ministral8bLatest | Self::Ministral14bLatest => true, - Self::CodestralLatest => false, + Self::CodestralLatest | Self::ZaiGlmLatest => false, Self::Custom { supports_images, .. } => supports_images.unwrap_or(false), @@ -178,13 +186,19 @@ impl Model { pub fn supports_thinking(&self) -> bool { match self { - Self::MistralMediumLatest | Self::MistralSmallLatest => true, + Self::MistralMediumLatest | Self::MistralSmallLatest | Self::ZaiGlmLatest => true, Self::Custom { supports_thinking, .. } => supports_thinking.unwrap_or(false), _ => false, } } + + /// Whether thinking can be turned off. Z.ai GLM models always think and + /// reject `reasoning_effort: "none"`. + pub fn supports_disabling_thinking(&self) -> bool { + !matches!(self, Self::ZaiGlmLatest) + } } #[derive(Debug, Serialize, Deserialize)]