From a8b35060ae52e52de0aac803b72de89de171101f Mon Sep 17 00:00:00 2001 From: jatmn Date: Sat, 11 Jul 2026 22:11:58 -0700 Subject: [PATCH] feat: first-class reasoning/thinking + tool translation for GLM-5.x, Kimi-K2.6/2.7-code, DeepSeek-V4, grok-4.5 - Add missing grok-4.5 catalog entry (500k context, reasoning_effort with none->low mapping, web search, image modality). - Fix DeepSeek-v4-flash context window (256k -> 1M) and enable preserve_reasoning_content_history for DeepSeek-v4-flash/-pro so tool-use turns replay reasoning_content (provider 400s otherwise); forward reasoning_effort alongside thinking.type. - Forward reasoning_effort for GLM-5.x so GLM-5.2 honors its native effort control (GLM-5/5.1 ignore it harmlessly). - Port reasoning_effort_none_value capability into config schema + request morph so models that cannot disable reasoning map none to a valid level. - Verify Kimi-K2.6/-2.7-code are already correct (thinking.type=enabled, thinking.keep=all, history preserved, sampling overrides dropped). - Update model-family-catalogs.md and README coverage tables; add/extend tests. All providers only expose OpenAI /chat/completions with function tools, so "first class special tool calls" = correct custom->function coercion plus faithful reconstruction, already implemented; this makes reasoning/thinking, context, and history translation complete for the listed models. Validated: cargo fmt --check, cargo test --locked (126 passed), cargo build --locked, git diff --check. --- README.md | 2 +- configs/model-families/deepseek.toml | 14 ++++- configs/model-families/x-ai.toml | 27 +++++++++ configs/model-families/z-ai.toml | 5 ++ docs/model-family-catalogs.md | 9 +-- src/config.rs | 6 ++ src/config_tests.rs | 87 ++++++++++++++++++++++++++++ src/models_tests.rs | 2 +- src/transform.rs | 4 +- src/transform_morph.rs | 18 ++++++ 10 files changed, 166 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e2f64fa..cb23fcd 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ for deployment examples. | MiniMax | [`configs/model-families/minimax.toml`](configs/model-families/minimax.toml) | `minimax-m2.5`, `minimax-m2.7`, `minimax-m3` | | Moonshot AI | [`configs/model-families/moonshot-ai.toml`](configs/model-families/moonshot-ai.toml) | `kimi-k2`, `kimi-k2-0905`, `kimi-k2.5`, `kimi-k2.6`, `kimi-k2.6-code`, `kimi-k2.7-code`, `kimi-k2.7-code-highspeed`, `kimi-for-coding` | | Alibaba Cloud | [`configs/model-families/qwen.toml`](configs/model-families/qwen.toml) | `qwen3.6-35b-a3b`; conservative broad defaults for `qwen3.6*` and `qwen3.7*` | -| xAI | [`configs/model-families/x-ai.toml`](configs/model-families/x-ai.toml) | `grok-4.3`, `grok-build-0.1` | +| xAI | [`configs/model-families/x-ai.toml`](configs/model-families/x-ai.toml) | `grok-4.3`, `grok-4.5`, `grok-build-0.1` | | Xiaomi | [`configs/model-families/xiaomi.toml`](configs/model-families/xiaomi.toml) | `mimo-v2.5`, `mimo-v2.5-pro` | | Z.ai | [`configs/model-families/z-ai.toml`](configs/model-families/z-ai.toml) | `glm-5`, `glm-5.1`, `glm-5.2` | diff --git a/configs/model-families/deepseek.toml b/configs/model-families/deepseek.toml index 2dabd4b..3c8f356 100644 --- a/configs/model-families/deepseek.toml +++ b/configs/model-families/deepseek.toml @@ -64,7 +64,7 @@ patterns = [ ] [model_families.deepseek_v4_flash.model_metadata] -context_window = 256000 +context_window = 1000000 default_reasoning_level = "medium" supported_reasoning_levels = ["low", "medium", "high"] supports_parallel_tool_calls = false @@ -72,12 +72,18 @@ auto_review_model_override = "deepseek-v4-flash" [model_families.deepseek_v4_flash.transform] force_parallel_tool_calls = false +preserve_reasoning_content_history = true [[model_families.deepseek_v4_flash.transform.remove_chat_request_morphs]] from = "reasoning.effort" to = "reasoning_effort" kind = "rename" +[[model_families.deepseek_v4_flash.transform.append_chat_request_morphs]] +from = "reasoning.effort" +to = "reasoning_effort" +kind = "rename" + [[model_families.deepseek_v4_flash.transform.append_chat_request_morphs]] from = "reasoning.effort" to = "thinking.type" @@ -100,12 +106,18 @@ auto_review_model_override = "deepseek-v4-flash" [model_families.deepseek_v4_pro.transform] force_parallel_tool_calls = false +preserve_reasoning_content_history = true [[model_families.deepseek_v4_pro.transform.remove_chat_request_morphs]] from = "reasoning.effort" to = "reasoning_effort" kind = "rename" +[[model_families.deepseek_v4_pro.transform.append_chat_request_morphs]] +from = "reasoning.effort" +to = "reasoning_effort" +kind = "rename" + [[model_families.deepseek_v4_pro.transform.append_chat_request_morphs]] from = "reasoning.effort" to = "thinking.type" diff --git a/configs/model-families/x-ai.toml b/configs/model-families/x-ai.toml index b842002..0c5d80d 100644 --- a/configs/model-families/x-ai.toml +++ b/configs/model-families/x-ai.toml @@ -66,3 +66,30 @@ supports_search_tool = true [model_families.x_ai_grok_build_0_1.transform] force_parallel_tool_calls = false + +[model_families.x_ai_grok_4_5] +priority = 10 +patterns = [ + "grok-4.5", + "grok_4.5", + "grok-4.5-latest", + "xai/grok-4.5", + "x-ai/grok-4.5", +] + +[model_families.x_ai_grok_4_5.model_metadata] +context_window = 500000 +default_reasoning_level = "high" +supported_reasoning_levels = ["low", "medium", "high"] +input_modalities = ["text", "image"] +supports_parallel_tool_calls = true +supports_search_tool = true + +[model_families.x_ai_grok_4_5.transform] +force_parallel_tool_calls = false +reasoning_effort_none_value = "low" + +[[model_families.x_ai_grok_4_5.transform.append_chat_request_morphs]] +from = "reasoning.effort" +to = "reasoning_effort" +kind = "rename" diff --git a/configs/model-families/z-ai.toml b/configs/model-families/z-ai.toml index 5352ee3..46ba551 100644 --- a/configs/model-families/z-ai.toml +++ b/configs/model-families/z-ai.toml @@ -35,6 +35,11 @@ from = "reasoning.effort" to = "thinking.type" kind = "thinking_type" +[[model_families.z_ai_glm_5.transform.append_chat_request_morphs]] +from = "reasoning.effort" +to = "reasoning_effort" +kind = "rename" + [model_families.z_ai_glm_5_base] priority = 10 patterns = [ diff --git a/docs/model-family-catalogs.md b/docs/model-family-catalogs.md index 697b1ad..3602b94 100644 --- a/docs/model-family-catalogs.md +++ b/docs/model-family-catalogs.md @@ -29,7 +29,7 @@ Provider/gateway behavior belongs in | MiniMax | [`minimax.toml`](../configs/model-families/minimax.toml) | `minimax-m2.5`, `minimax-m2.7`, `minimax-m3` | | Moonshot AI | [`moonshot-ai.toml`](../configs/model-families/moonshot-ai.toml) | `kimi-k2`, `kimi-k2-0905`, `kimi-k2.5`, `kimi-k2.6`, `kimi-k2.6-code`, `kimi-k2.7-code`, `kimi-k2.7-code-highspeed`, `kimi-for-coding` | | Alibaba Cloud | [`qwen.toml`](../configs/model-families/qwen.toml) | `qwen3.6-35b-a3b`; conservative broad defaults for `qwen3.6*` and `qwen3.7*` | -| xAI | [`x-ai.toml`](../configs/model-families/x-ai.toml) | `grok-4.3`, `grok-build-0.1` | +| xAI | [`x-ai.toml`](../configs/model-families/x-ai.toml) | `grok-4.3`, `grok-4.5`, `grok-build-0.1` | | Xiaomi | [`xiaomi.toml`](../configs/model-families/xiaomi.toml) | `mimo-v2.5`, `mimo-v2.5-pro` | | Z.ai | [`z-ai.toml`](../configs/model-families/z-ai.toml) | `glm-5`, `glm-5.1`, `glm-5.2` | @@ -48,8 +48,8 @@ transform behavior. | --- | --- | --- | --- | --- | --- | --- | | DeepSeek | `deepseek-v3.2` | 128k | provider/default | low, medium, high; default medium | provider/default | Parallel tool metadata is true, but requests force `parallel_tool_calls = false`. | | DeepSeek | `deepseek-v3.2-speciale` | 128k | provider/default | low, medium, high; default high | provider/default | Forces `parallel_tool_calls = false`. | -| DeepSeek | `deepseek-v4-flash` | 256k | provider/default | low, medium, high; default medium | provider/default | Converts `reasoning.effort` to `thinking.type`; forces `parallel_tool_calls = false`. | -| DeepSeek | `deepseek-v4-pro` | 1,000k | provider/default | low, medium, high; default high | provider/default | Converts `reasoning.effort` to `thinking.type`; forces `parallel_tool_calls = false`. | +| DeepSeek | `deepseek-v4-flash` | 1,000k | provider/default | low, medium, high; default medium | provider/default | Converts `reasoning.effort` to `thinking.type` and forwards `reasoning_effort`; preserves reasoning history (`preserve_reasoning_content_history`); forces `parallel_tool_calls = false`. | +| DeepSeek | `deepseek-v4-pro` | 1,000k | provider/default | low, medium, high; default high | provider/default | Converts `reasoning.effort` to `thinking.type` and forwards `reasoning_effort`; preserves reasoning history (`preserve_reasoning_content_history`); forces `parallel_tool_calls = false`. | | MiniMax | `minimax-m2.5` | 192k | text | none | true | Non-reasoning variant; forces `parallel_tool_calls = false`. | | MiniMax | `minimax-m2.7` | 200k | text | low, medium, high; default high | true | Converts `reasoning.effort` to `thinking.type`; forces `parallel_tool_calls = false`. | | MiniMax | `minimax-m3` | 1,000k | text, image | low, medium, high; default high | false | Converts `reasoning.effort` to `thinking.type`; forces `parallel_tool_calls = false`. | @@ -61,11 +61,12 @@ transform behavior. | Moonshot AI | `kimi-k2.7-code`, `kimi-k2.7-code-highspeed`, `kimi-for-coding` | usable 220k, max 262,144 | text, image | high | provider/default | Uses a conservative Codex planning window below the provider 256K cap to leave output/tokenizer headroom; sets `thinking.type = enabled` and `thinking.keep = all`; drops K2.7-rejected sampling overrides; forces `parallel_tool_calls = false`. | | Alibaba Cloud | `qwen3.6-35b-a3b` | 262k, max 1,010k | text, image, video | high | false | Removes inherited OpenAI `reasoning_effort`; broad `qwen3.6*` and `qwen3.7*` entries also drop it until a gateway documents support. | | xAI | `grok-4.3` | 1,000k | text, image | low, medium, high; default medium | true | Uses `web_search`; parallel tool metadata is true, but requests force `parallel_tool_calls = false`. | +| xAI | `grok-4.5` | 500k | text, image | low, medium, high; default high | true | Uses `web_search`; reasoning cannot be disabled (`none` mapped to `low` via `reasoning_effort_none_value`); parallel tool metadata is true, but requests force `parallel_tool_calls = false`. | | xAI | `grok-build-0.1` | 256k | text | low, medium, high; default medium | true | Uses `web_search`; parallel tool metadata is true, but requests force `parallel_tool_calls = false`. | | Xiaomi | `mimo-v2.5` | 1,000k | text, image | low, medium, high; default medium | provider/default | Gateway profile does not carry model-specific overrides. | | Xiaomi | `mimo-v2.5-pro` | 1,000k | text | low, medium, high; default medium | provider/default | Gateway profile does not carry model-specific overrides. | | Z.ai | `glm-5`, `glm-5.1` family | 200k | text | low, medium, high; default medium | provider/default | Broad GLM-5 transform converts `reasoning.effort` to `thinking.type`; forces `parallel_tool_calls = false`. | -| Z.ai | `glm-5.2` | 1,000k | text | low, medium, high; default medium | provider/default | Inherits broad GLM-5 transform. | +| Z.ai | `glm-5.2` | 1,000k | text | low, medium, high; default medium | provider/default | Inherits broad GLM-5 transform; forwards `reasoning_effort` alongside `thinking.type` (GLM-5.2 honors it natively). | ## Matching And Priority diff --git a/src/config.rs b/src/config.rs index 63c5b58..0a3fe3b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -343,6 +343,7 @@ pub struct TransformConfig { pub responses_request_morphs: Vec, pub unsupported_tool_types: Vec, pub unsupported_tool_strategy: UnsupportedToolStrategy, + pub reasoning_effort_none_value: Option, pub drop_empty_tool_choice: bool, pub force_parallel_tool_calls: Option, pub request_stream_options_include_usage: bool, @@ -357,6 +358,7 @@ impl Default for TransformConfig { responses_request_morphs: Vec::new(), unsupported_tool_types: vec!["custom".to_string()], unsupported_tool_strategy: UnsupportedToolStrategy::AsFunction, + reasoning_effort_none_value: None, drop_empty_tool_choice: true, force_parallel_tool_calls: None, request_stream_options_include_usage: false, @@ -377,6 +379,7 @@ pub struct TransformConfigPatch { pub append_responses_request_morphs: Vec, pub unsupported_tool_types: Option>, pub unsupported_tool_strategy: Option, + pub reasoning_effort_none_value: Option, pub drop_empty_tool_choice: Option, pub force_parallel_tool_calls: Option, pub request_stream_options_include_usage: Option, @@ -414,6 +417,9 @@ impl TransformConfigPatch { if let Some(strategy) = self.unsupported_tool_strategy { transform.unsupported_tool_strategy = strategy; } + if let Some(value) = &self.reasoning_effort_none_value { + transform.reasoning_effort_none_value = Some(value.clone()); + } if let Some(drop_empty_tool_choice) = self.drop_empty_tool_choice { transform.drop_empty_tool_choice = drop_empty_tool_choice; } diff --git a/src/config_tests.rs b/src/config_tests.rs index 53e1017..bce3811 100644 --- a/src/config_tests.rs +++ b/src/config_tests.rs @@ -51,6 +51,7 @@ fn example_configs_parse_request_morphs() { assert!(default_config.model_families.contains_key("mimo_v2_5_pro")); assert!(default_config.model_families.contains_key("x_ai_grok")); assert!(default_config.model_families.contains_key("x_ai_grok_4_3")); + assert!(default_config.model_families.contains_key("x_ai_grok_4_5")); assert!( default_config .model_families @@ -506,6 +507,92 @@ fn model_family_matches_are_sorted_by_priority() { assert_eq!(matches[1].priority, 10); } +#[test] +fn first_class_reasoning_and_tool_translation_for_target_models() { + let config = load_config_layers(&[]).expect("default config loads"); + + // grok-4.5: new model with correct metadata and `none` -> `low` mapping. + let grok45 = config + .model_families + .get("x_ai_grok_4_5") + .expect("grok-4.5 family exists"); + assert_eq!(grok45.model_metadata.context_window, Some(500_000)); + assert_eq!( + grok45.model_metadata.default_reasoning_level.as_deref(), + Some("high") + ); + assert_eq!( + grok45.model_metadata.supported_reasoning_levels, + Some(vec![ + "low".to_string(), + "medium".to_string(), + "high".to_string() + ]) + ); + assert_eq!( + grok45.transform.reasoning_effort_none_value.as_deref(), + Some("low") + ); + assert!( + grok45 + .transform + .append_chat_request_morphs + .iter() + .any(|m| m.from == "reasoning.effort" && m.to.as_deref() == Some("reasoning_effort")) + ); + assert!( + matching_model_families(&config, "grok-4.5") + .iter() + .any(|family| family.priority == 10) + ); + + // DeepSeek V4: 1M context + reasoning-history preservation + effort forwarding. + let ds_flash = config + .model_families + .get("deepseek_v4_flash") + .expect("deepseek-v4-flash family exists"); + assert_eq!(ds_flash.model_metadata.context_window, Some(1_000_000)); + assert_eq!( + ds_flash.transform.preserve_reasoning_content_history, + Some(true) + ); + assert!( + ds_flash + .transform + .append_chat_request_morphs + .iter() + .any(|m| m.from == "reasoning.effort" && m.to.as_deref() == Some("reasoning_effort")) + ); + + let ds_pro = config + .model_families + .get("deepseek_v4_pro") + .expect("deepseek-v4-pro family exists"); + assert_eq!( + ds_pro.transform.preserve_reasoning_content_history, + Some(true) + ); + assert!( + ds_pro + .transform + .append_chat_request_morphs + .iter() + .any(|m| m.from == "reasoning.effort" && m.to.as_deref() == Some("reasoning_effort")) + ); + + // GLM-5.2: broad family forwards reasoning_effort alongside thinking.type. + let glm5 = config + .model_families + .get("z_ai_glm_5") + .expect("glm-5 broad family exists"); + assert!( + glm5.transform + .append_chat_request_morphs + .iter() + .any(|m| m.from == "reasoning.effort" && m.to.as_deref() == Some("reasoning_effort")) + ); +} + #[test] fn transform_patch_can_remove_inherited_morphs_before_appending() { let mut transform = TransformConfig::default(); diff --git a/src/models_tests.rs b/src/models_tests.rs index 7d7aaa3..4e1f603 100644 --- a/src/models_tests.rs +++ b/src/models_tests.rs @@ -411,7 +411,7 @@ fn deepseek_family_metadata_is_variant_specific() { assert_eq!(models[0]["context_window"], 128_000); assert_eq!(models[0]["supports_parallel_tool_calls"], true); - assert_eq!(models[1]["context_window"], 256_000); + assert_eq!(models[1]["context_window"], 1_000_000); assert_eq!(models[1]["supports_parallel_tool_calls"], false); assert_eq!(models[2]["context_window"], 1_000_000); assert_eq!(models[2]["default_reasoning_level"], "high"); diff --git a/src/transform.rs b/src/transform.rs index c31f4e9..ada31ba 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -9,6 +9,7 @@ use crate::config::TransformConfig; use crate::config::UnsupportedToolStrategy; use crate::ids::generated_id; use crate::transform_morph::apply_native_request_morphs; +use crate::transform_morph::apply_reasoning_effort_none_value; use crate::transform_morph::apply_request_morphs; #[derive(Debug, Clone)] @@ -138,7 +139,8 @@ pub fn responses_to_chat(request: Value, transform: &TransformConfig) -> ChatTra } apply_request_morphs(&request, &mut out, transform); - let body = Value::Object(out); + let mut body = Value::Object(out); + apply_reasoning_effort_none_value(&mut body, transform); let diagnostics = transform_diagnostics(&request, &body, input_tools.len(), tool_diagnostics); ChatTransform { diff --git a/src/transform_morph.rs b/src/transform_morph.rs index 05b3ab8..07e1679 100644 --- a/src/transform_morph.rs +++ b/src/transform_morph.rs @@ -59,6 +59,24 @@ pub(crate) fn apply_request_morphs( } } +pub(crate) fn apply_reasoning_effort_none_value(body: &mut Value, transform: &TransformConfig) { + let Some(none_value) = &transform.reasoning_effort_none_value else { + return; + }; + if let Some(Value::String(effort)) = body.get_mut("reasoning_effort") { + if effort.eq_ignore_ascii_case("none") { + *effort = none_value.clone(); + } + } + if let Some(reasoning) = body.get_mut("reasoning").and_then(Value::as_object_mut) { + if let Some(Value::String(effort)) = reasoning.get_mut("effort") { + if effort.eq_ignore_ascii_case("none") { + *effort = none_value.clone(); + } + } + } +} + pub(crate) fn apply_native_request_morphs(request: &mut Value, transform: &TransformConfig) { let original = request.clone(); for morph in &transform.responses_request_morphs {