diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 56f9b9026..3a2faadf3 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -319,6 +319,48 @@ mod tests { assert_eq!(models[8].model, "nvidia/nemotron-3-ultra-550b-a55b"); } + #[test] + fn zai_fallback_models_expose_glm_5_2_reasoning_efforts() { + let models = fallback_supported_models_for_provider("zai", /*include_hidden*/ false); + + assert_eq!(models[0].model, "glm-5.2"); + assert_eq!(models[0].display_name, "GLM-5.2"); + assert!(models[0].is_default); + assert!(!models[0].hidden); + // Direct Z.ai docs default GLM-5.2 to `reasoning_effort = max`, represented via + // `Custom("max")` since the enum has no first-class `Max` variant. + assert_eq!( + models[0].default_reasoning_effort, + ReasoningEffort::Custom("max".to_string()) + ); + assert_eq!( + models[0] + .supported_reasoning_efforts + .iter() + .map(|effort| effort.reasoning_effort.clone()) + .collect::>(), + vec![ + ReasoningEffort::None, + ReasoningEffort::Minimal, + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ReasoningEffort::XHigh, + ReasoningEffort::Custom("max".to_string()), + ] + ); + + // Other GLM fallbacks use Z.ai's provider-specific `thinking` parameter rather than + // OpenAI-compatible `reasoning_effort`, so the fallback list should not advertise + // selectable reasoning efforts for them. + let glm_5_1 = models + .iter() + .find(|model| model.model == "glm-5.1") + .expect("glm-5.1 fallback should exist"); + assert_eq!(glm_5_1.default_reasoning_effort, ReasoningEffort::None); + assert_eq!(glm_5_1.supported_reasoning_efforts, Vec::new()); + } + #[test] fn xiaomi_fallback_models_include_current_default_and_legacy_alias() { let models = diff --git a/codex-rs/app-server/tests/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs index b84541e7b..3a0199663 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -634,6 +634,36 @@ wire_api = "responses" ); assert!(data.iter().any(|model| model.is_default)); assert!(next_cursor.is_none()); + + if provider_id == "zai" { + // Direct Z.ai GLM-5.2 defaults to `reasoning_effort = max` (represented via + // `Custom("max")`) and exposes the full granular scale through fallback listing. + let glm_5_2 = data + .first() + .expect("zai fallback listing should include glm-5.2"); + assert_eq!( + glm_5_2.default_reasoning_effort, + "max".parse().map_err(Error::msg)?, + "zai glm-5.2 should default to max reasoning effort" + ); + assert_eq!( + glm_5_2 + .supported_reasoning_efforts + .iter() + .map(|effort| effort.reasoning_effort.clone()) + .collect::>(), + vec![ + "none".parse().map_err(Error::msg)?, + "minimal".parse().map_err(Error::msg)?, + "low".parse().map_err(Error::msg)?, + "medium".parse().map_err(Error::msg)?, + "high".parse().map_err(Error::msg)?, + "xhigh".parse().map_err(Error::msg)?, + "max".parse().map_err(Error::msg)?, + ], + "zai glm-5.2 should expose the corrected reasoning-effort scale" + ); + } } Ok(()) } diff --git a/codex-rs/known-provider-models/src/lib.rs b/codex-rs/known-provider-models/src/lib.rs index cb06d03f0..72daa4154 100644 --- a/codex-rs/known-provider-models/src/lib.rs +++ b/codex-rs/known-provider-models/src/lib.rs @@ -869,26 +869,68 @@ mod tests { ) ) ); + } + #[test] + fn zai_glm_5_2_exposes_source_backed_reasoning_scale() { + // Direct Z.ai docs show GLM-5.2 defaults to `reasoning_effort = max` and supports the full + // granular scale, unlike the other GLM models which use Z.ai's provider-specific + // `thinking` parameter instead of OpenAI-compatible reasoning effort. let (default_effort, presets) = reasoning_levels_for_local_fallback(Some(ZAI_PROVIDER_ID), "glm-5.2"); + assert_eq!( - (default_effort, presets), - ( - Some(ReasoningEffort::Custom("max".to_string())), - vec![ - reasoning_preset(ReasoningEffort::None, "Reasoning disabled"), - reasoning_preset(ReasoningEffort::High, "High reasoning"), - reasoning_preset(ReasoningEffort::Custom("max".to_string()), "Max reasoning"), - ], - ) + default_effort, + Some(ReasoningEffort::Custom("max".to_string())) ); assert_eq!( - reasoning_levels_for_local_fallback(Some(ZAI_PROVIDER_ID), "glm-5.1"), - (None, Vec::new()) + presets + .iter() + .map(|preset| preset.effort.clone()) + .collect::>(), + vec![ + ReasoningEffort::None, + ReasoningEffort::Minimal, + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ReasoningEffort::XHigh, + ReasoningEffort::Custom("max".to_string()), + ] + ); + + // The 1M-context variant shares the same direct metadata. + assert_eq!( + reasoning_levels_for_local_fallback(Some(ZAI_PROVIDER_ID), "glm-5.2[1m]"), + reasoning_levels_for_local_fallback(Some(ZAI_PROVIDER_ID), "glm-5.2") ); } + #[test] + fn zai_non_glm_5_2_models_do_not_expose_reasoning_effort() { + for slug in [ + "glm-5.1", + "glm-5", + "glm-5-turbo", + "glm-4.7", + "glm-4.7-flashx", + "glm-4.7-flash", + ] { + let (default_effort, presets) = + reasoning_levels_for_local_fallback(Some(ZAI_PROVIDER_ID), slug); + + assert_eq!( + default_effort, None, + "{slug} should not expose an OpenAI-compatible reasoning default" + ); + assert_eq!( + presets, + Vec::new(), + "{slug} should not expose OpenAI-compatible reasoning presets" + ); + } + } + #[test] fn provider_for_fallback_model_finds_unique_configured_provider() { assert_eq!( diff --git a/codex-rs/known-provider-models/src/zai.rs b/codex-rs/known-provider-models/src/zai.rs index 8d23b3b7d..1cc620835 100644 --- a/codex-rs/known-provider-models/src/zai.rs +++ b/codex-rs/known-provider-models/src/zai.rs @@ -50,17 +50,31 @@ pub(crate) fn reasoning_levels( slug: &str, ) -> (Option, Vec) { match slug { + // Direct Z.ai docs (https://docs.z.ai/guides/llm/glm-5.2 and + // https://docs.z.ai/api-reference/llm/chat-completion, accessed 2026-07-09) show that only + // GLM-5.2 exposes the granular `reasoning_effort` scale (max, xhigh, high, medium, low, + // minimal, none) with a default of `max`. Other GLM models use Z.ai's provider-specific + // `thinking` toggle, so Codewith should not expose those as OpenAI-compatible + // `reasoning_effort` options. + // + // Codewith's `ReasoningEffort` enum has no first-class `max` variant, so `max` is + // represented via `Custom("max")`, which serializes to the exact wire value `max`. If a + // first-class `Max` variant is ever needed, that is a separate protocol follow-up. "glm-5.2" | "glm-5.2[1m]" => ( Some(ReasoningEffort::Custom("max".to_string())), vec![ reasoning_preset(ReasoningEffort::None, "Reasoning disabled"), + reasoning_preset(ReasoningEffort::Minimal, "Minimal reasoning"), + reasoning_preset(ReasoningEffort::Low, "Low reasoning"), + reasoning_preset(ReasoningEffort::Medium, "Moderate reasoning"), reasoning_preset(ReasoningEffort::High, "High reasoning"), - reasoning_preset(ReasoningEffort::Custom("max".to_string()), "Max reasoning"), + reasoning_preset(ReasoningEffort::XHigh, "Extended reasoning"), + reasoning_preset( + ReasoningEffort::Custom("max".to_string()), + "Maximum reasoning", + ), ], ), - "glm-5.1" | "glm-5" | "glm-5-turbo" | "glm-4.7" | "glm-4.7-flashx" | "glm-4.7-flash" => { - (None, Vec::new()) - } _ => (None, Vec::new()), } } diff --git a/codex-rs/models-manager/src/model_info_tests.rs b/codex-rs/models-manager/src/model_info_tests.rs index f56d38406..5b4aea827 100644 --- a/codex-rs/models-manager/src/model_info_tests.rs +++ b/codex-rs/models-manager/src/model_info_tests.rs @@ -382,13 +382,29 @@ fn known_zai_model_uses_local_metadata_with_reasoning_and_search_support() { effort: ReasoningEffort::None, description: "Reasoning disabled".to_string(), }, + ReasoningEffortPreset { + effort: ReasoningEffort::Minimal, + description: "Minimal reasoning".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Low reasoning".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Moderate reasoning".to_string(), + }, ReasoningEffortPreset { effort: ReasoningEffort::High, description: "High reasoning".to_string(), }, + ReasoningEffortPreset { + effort: ReasoningEffort::XHigh, + description: "Extended reasoning".to_string(), + }, ReasoningEffortPreset { effort: ReasoningEffort::Custom("max".to_string()), - description: "Max reasoning".to_string(), + description: "Maximum reasoning".to_string(), }, ] );