agent_engine/runtime/openai/catalog/
kimi.rs1use agent_core::reasoning::ReasoningLevel;
23use serde_json::{json, Map, Value};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum KimiReasoningCapability {
28 Effort {
30 supported: &'static [ReasoningLevel],
31 default_level: Option<ReasoningLevel>,
32 can_disable: bool,
35 },
36 AlwaysThinking,
39 ToggleableThinking,
42}
43
44pub fn kimi_static_capability(model_id: &str) -> Option<KimiReasoningCapability> {
47 use ReasoningLevel::*;
48 match model_id {
49 "kimi-k3" => Some(KimiReasoningCapability::Effort {
50 supported: &[Low, High, Max],
51 default_level: Some(Max),
52 can_disable: false,
53 }),
54 "kimi-k2.7-code" | "kimi-k2.7-code-highspeed" => {
55 Some(KimiReasoningCapability::AlwaysThinking)
56 }
57 "kimi-k2.6" | "kimi-k2.5" => Some(KimiReasoningCapability::ToggleableThinking),
58 _ => None,
59 }
60}
61
62fn kimi_effort_label(level: ReasoningLevel) -> Option<&'static str> {
68 match level {
69 ReasoningLevel::Low => Some("low"),
70 ReasoningLevel::High => Some("high"),
71 ReasoningLevel::Max => Some("max"),
72 _ => None,
73 }
74}
75
76pub fn apply_kimi_reasoning_params(
83 body: &mut Map<String, Value>,
84 model_id: &str,
85 level: ReasoningLevel,
86) {
87 match kimi_static_capability(model_id) {
88 Some(KimiReasoningCapability::Effort { supported, .. }) => {
89 if supported.contains(&level) {
91 if let Some(label) = kimi_effort_label(level) {
92 body.insert("reasoning_effort".to_string(), json!(label));
93 }
94 }
95 }
96 Some(KimiReasoningCapability::ToggleableThinking) => {
97 if level == ReasoningLevel::Off {
98 body.insert("thinking".to_string(), json!({ "type": "disabled" }));
99 }
100 }
102 Some(KimiReasoningCapability::AlwaysThinking) | None => {}
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use ReasoningLevel::*;
112
113 #[test]
114 fn k3_capability_is_exact_low_high_max_default_max_no_disable() {
115 let Some(KimiReasoningCapability::Effort {
116 supported,
117 default_level,
118 can_disable,
119 }) = kimi_static_capability("kimi-k3")
120 else {
121 panic!("kimi-k3 must have documented effort capability");
122 };
123 assert_eq!(supported, &[Low, High, Max]);
124 assert_eq!(default_level, Some(Max));
125 assert!(!can_disable);
126 }
127
128 #[test]
129 fn k27_code_family_is_always_thinking() {
130 for id in ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"] {
131 assert_eq!(
132 kimi_static_capability(id),
133 Some(KimiReasoningCapability::AlwaysThinking),
134 "{id}"
135 );
136 }
137 }
138
139 #[test]
140 fn k26_and_k25_are_toggleable() {
141 for id in ["kimi-k2.6", "kimi-k2.5"] {
142 assert_eq!(
143 kimi_static_capability(id),
144 Some(KimiReasoningCapability::ToggleableThinking),
145 "{id}"
146 );
147 }
148 }
149
150 #[test]
151 fn unknown_ids_fail_closed() {
152 assert_eq!(kimi_static_capability("kimi-k9"), None);
153 assert_eq!(kimi_static_capability(""), None);
154 assert_eq!(kimi_static_capability("kimi-k3-preview"), None);
156 }
157
158 #[test]
159 fn k3_body_carries_exact_reasoning_effort() {
160 for (level, label) in [(Low, "low"), (High, "high"), (Max, "max")] {
161 let mut body = Map::new();
162 apply_kimi_reasoning_params(&mut body, "kimi-k3", level);
163 assert_eq!(body.get("reasoning_effort"), Some(&json!(label)));
164 assert!(!body.contains_key("thinking"));
165 }
166 }
167
168 #[test]
169 fn k3_adaptive_omits_the_field() {
170 let mut body = Map::new();
171 apply_kimi_reasoning_params(&mut body, "kimi-k3", Adaptive);
172 assert!(body.is_empty());
173 }
174
175 #[test]
176 fn k3_unsupported_levels_are_omitted_never_guessed() {
177 for level in [Off, Medium, XHigh, Ultra, UltraCode] {
180 let mut body = Map::new();
181 apply_kimi_reasoning_params(&mut body, "kimi-k3", level);
182 assert!(body.is_empty(), "{level:?} must not serialize");
183 }
184 }
185
186 #[test]
187 fn k26_off_disables_thinking_and_other_levels_omit() {
188 let mut body = Map::new();
189 apply_kimi_reasoning_params(&mut body, "kimi-k2.6", Off);
190 assert_eq!(body.get("thinking"), Some(&json!({"type": "disabled"})));
191 assert!(!body.contains_key("reasoning_effort"));
192 for level in [Adaptive, Low, Medium, High, Max] {
193 let mut body = Map::new();
194 apply_kimi_reasoning_params(&mut body, "kimi-k2.6", level);
195 assert!(body.is_empty(), "{level:?} must omit all fields");
196 }
197 }
198
199 #[test]
200 fn always_thinking_and_unknown_send_nothing() {
201 for id in ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k9"] {
202 for level in [Off, Adaptive, Low, High, Max] {
203 let mut body = Map::new();
204 apply_kimi_reasoning_params(&mut body, id, level);
205 assert!(body.is_empty(), "{id} {level:?} must send nothing");
206 }
207 }
208 }
209}