1use agent_core::reasoning::ReasoningLevel;
23use serde_json::{json, Map, Value};
24
25use super::{
26 CatalogModel, CatalogProviderKind, CatalogSource, Modality, PricingSummary, ReasoningSupport,
27};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum KimiReasoningCapability {
32 Effort {
34 supported: &'static [ReasoningLevel],
35 default_level: Option<ReasoningLevel>,
36 can_disable: bool,
39 },
40 AlwaysThinking,
43 ToggleableThinking,
46}
47
48pub fn kimi_static_capability(model_id: &str) -> Option<KimiReasoningCapability> {
51 use ReasoningLevel::*;
52 match model_id {
53 "kimi-k3" => Some(KimiReasoningCapability::Effort {
54 supported: &[Low, High, Max],
55 default_level: Some(Max),
56 can_disable: false,
57 }),
58 "kimi-k2.7-code" | "kimi-k2.7-code-highspeed" => {
59 Some(KimiReasoningCapability::AlwaysThinking)
60 }
61 "kimi-k2.6" | "kimi-k2.5" => Some(KimiReasoningCapability::ToggleableThinking),
62 _ => None,
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct KimiCodeModelDescriptor {
76 pub id: &'static str,
77 pub label: &'static str,
78 pub context_tokens: Option<u64>,
79}
80
81pub const KIMI_CODE_TEXT_MODELS: &[KimiCodeModelDescriptor] = &[
82 KimiCodeModelDescriptor {
83 id: "k3",
84 label: "Kimi K3",
85 context_tokens: Some(1_048_576),
86 },
87 KimiCodeModelDescriptor {
88 id: "k3-256k",
89 label: "Kimi K3 256k",
90 context_tokens: Some(262_144),
91 },
92 KimiCodeModelDescriptor {
93 id: "kimi-for-coding",
94 label: "Kimi K2.7 Coding",
95 context_tokens: Some(262_144),
96 },
97 KimiCodeModelDescriptor {
98 id: "kimi-for-coding-highspeed",
99 label: "Kimi K2.7 Coding Highspeed",
100 context_tokens: Some(262_144),
101 },
102];
103
104pub fn kimi_code_model(id: &str) -> Option<&'static KimiCodeModelDescriptor> {
107 KIMI_CODE_TEXT_MODELS.iter().find(|model| model.id == id)
108}
109
110pub fn kimi_code_capability(model_id: &str) -> Option<KimiReasoningCapability> {
117 use ReasoningLevel::*;
118 match model_id {
119 "k3" | "k3-256k" => Some(KimiReasoningCapability::Effort {
120 supported: &[Low, High, Max],
121 default_level: Some(High),
122 can_disable: false,
123 }),
124 "kimi-for-coding" | "kimi-for-coding-highspeed" => {
125 Some(KimiReasoningCapability::AlwaysThinking)
126 }
127 _ => None,
128 }
129}
130
131pub fn kimi_code_static_catalog_models() -> Vec<CatalogModel> {
134 KIMI_CODE_TEXT_MODELS
135 .iter()
136 .map(|descriptor| CatalogModel {
137 provider_key: "kimi-code".into(),
138 provider_name: "Kimi Code".into(),
139 provider_kind: CatalogProviderKind::Generic {
140 key: "kimi-code".into(),
141 },
142 id: descriptor.id.into(),
143 label: Some(descriptor.label.into()),
144 context_tokens: descriptor.context_tokens,
145 max_output_tokens: None,
146 input_modalities: vec![Modality::Text],
147 pricing: PricingSummary::default(),
148 reasoning: ReasoningSupport::GenericOpenAi,
149 source: CatalogSource::StaticFallback,
150 })
151 .collect()
152}
153
154fn kimi_effort_label(level: ReasoningLevel) -> Option<&'static str> {
160 match level {
161 ReasoningLevel::Low => Some("low"),
162 ReasoningLevel::High => Some("high"),
163 ReasoningLevel::Max => Some("max"),
164 _ => None,
165 }
166}
167
168pub fn apply_kimi_reasoning_params(
175 body: &mut Map<String, Value>,
176 model_id: &str,
177 level: ReasoningLevel,
178) {
179 match kimi_static_capability(model_id).or_else(|| kimi_code_capability(model_id)) {
183 Some(KimiReasoningCapability::Effort { supported, .. }) => {
184 if supported.contains(&level) {
186 if let Some(label) = kimi_effort_label(level) {
187 body.insert("reasoning_effort".to_string(), json!(label));
188 }
189 }
190 }
191 Some(KimiReasoningCapability::ToggleableThinking) => {
192 if level == ReasoningLevel::Off {
193 body.insert("thinking".to_string(), json!({ "type": "disabled" }));
194 }
195 }
197 Some(KimiReasoningCapability::AlwaysThinking) | None => {}
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use ReasoningLevel::*;
207
208 #[test]
209 fn k3_capability_is_exact_low_high_max_default_max_no_disable() {
210 let Some(KimiReasoningCapability::Effort {
211 supported,
212 default_level,
213 can_disable,
214 }) = kimi_static_capability("kimi-k3")
215 else {
216 panic!("kimi-k3 must have documented effort capability");
217 };
218 assert_eq!(supported, &[Low, High, Max]);
219 assert_eq!(default_level, Some(Max));
220 assert!(!can_disable);
221 }
222
223 #[test]
224 fn k27_code_family_is_always_thinking() {
225 for id in ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"] {
226 assert_eq!(
227 kimi_static_capability(id),
228 Some(KimiReasoningCapability::AlwaysThinking),
229 "{id}"
230 );
231 }
232 }
233
234 #[test]
235 fn k26_and_k25_are_toggleable() {
236 for id in ["kimi-k2.6", "kimi-k2.5"] {
237 assert_eq!(
238 kimi_static_capability(id),
239 Some(KimiReasoningCapability::ToggleableThinking),
240 "{id}"
241 );
242 }
243 }
244
245 #[test]
246 fn unknown_ids_fail_closed() {
247 assert_eq!(kimi_static_capability("kimi-k9"), None);
248 assert_eq!(kimi_static_capability(""), None);
249 assert_eq!(kimi_static_capability("kimi-k3-preview"), None);
251 }
252
253 #[test]
254 fn k3_body_carries_exact_reasoning_effort() {
255 for (level, label) in [(Low, "low"), (High, "high"), (Max, "max")] {
256 let mut body = Map::new();
257 apply_kimi_reasoning_params(&mut body, "kimi-k3", level);
258 assert_eq!(body.get("reasoning_effort"), Some(&json!(label)));
259 assert!(!body.contains_key("thinking"));
260 }
261 }
262
263 #[test]
264 fn k3_adaptive_omits_the_field() {
265 let mut body = Map::new();
266 apply_kimi_reasoning_params(&mut body, "kimi-k3", Adaptive);
267 assert!(body.is_empty());
268 }
269
270 #[test]
271 fn k3_unsupported_levels_are_omitted_never_guessed() {
272 for level in [Off, Medium, XHigh, Ultra, UltraCode] {
275 let mut body = Map::new();
276 apply_kimi_reasoning_params(&mut body, "kimi-k3", level);
277 assert!(body.is_empty(), "{level:?} must not serialize");
278 }
279 }
280
281 #[test]
282 fn k26_off_disables_thinking_and_other_levels_omit() {
283 let mut body = Map::new();
284 apply_kimi_reasoning_params(&mut body, "kimi-k2.6", Off);
285 assert_eq!(body.get("thinking"), Some(&json!({"type": "disabled"})));
286 assert!(!body.contains_key("reasoning_effort"));
287 for level in [Adaptive, Low, Medium, High, Max] {
288 let mut body = Map::new();
289 apply_kimi_reasoning_params(&mut body, "kimi-k2.6", level);
290 assert!(body.is_empty(), "{level:?} must omit all fields");
291 }
292 }
293
294 #[test]
295 fn always_thinking_and_unknown_send_nothing() {
296 for id in ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k9"] {
297 for level in [Off, Adaptive, Low, High, Max] {
298 let mut body = Map::new();
299 apply_kimi_reasoning_params(&mut body, id, level);
300 assert!(body.is_empty(), "{id} {level:?} must send nothing");
301 }
302 }
303 }
304
305 #[test]
308 fn kimi_code_k3_family_has_effort_low_high_max_default_high_no_disable() {
309 for id in ["k3", "k3-256k"] {
310 let Some(KimiReasoningCapability::Effort {
311 supported,
312 default_level,
313 can_disable,
314 }) = kimi_code_capability(id)
315 else {
316 panic!("{id} must have managed effort capability");
317 };
318 assert_eq!(supported, &[Low, High, Max]);
319 assert_eq!(default_level, Some(High));
320 assert!(!can_disable, "{id} reasoning must not be disableable");
321 }
322 }
323
324 #[test]
325 fn kimi_code_coding_family_is_always_thinking_and_unknown_fails_closed() {
326 for id in ["kimi-for-coding", "kimi-for-coding-highspeed"] {
327 assert_eq!(
328 kimi_code_capability(id),
329 Some(KimiReasoningCapability::AlwaysThinking),
330 "{id}"
331 );
332 }
333 assert_eq!(kimi_code_capability("k3-preview"), None);
334 assert_eq!(kimi_code_capability("kimi-k3"), None);
336 assert_eq!(kimi_code_capability(""), None);
337 }
338
339 #[test]
340 fn managed_and_static_id_namespaces_are_disjoint() {
341 for descriptor in KIMI_CODE_TEXT_MODELS {
342 assert!(
343 kimi_static_capability(descriptor.id).is_none(),
344 "{} must not collide with the static Moonshot table",
345 descriptor.id
346 );
347 assert!(kimi_code_model(descriptor.id).is_some());
348 }
349 }
350
351 #[test]
352 fn managed_k3_body_carries_exact_reasoning_effort_via_shared_apply() {
353 for (level, label) in [(Low, "low"), (High, "high"), (Max, "max")] {
354 let mut body = Map::new();
355 apply_kimi_reasoning_params(&mut body, "k3", level);
356 assert_eq!(body.get("reasoning_effort"), Some(&json!(label)));
357 assert!(!body.contains_key("thinking"));
358 }
359 for level in [Adaptive, Off, Medium, XHigh] {
361 let mut body = Map::new();
362 apply_kimi_reasoning_params(&mut body, "k3-256k", level);
363 assert!(body.is_empty(), "{level:?} must not serialize");
364 }
365 }
366
367 #[test]
368 fn managed_always_thinking_sends_nothing() {
369 for id in ["kimi-for-coding", "kimi-for-coding-highspeed"] {
370 for level in [Off, Adaptive, Low, High, Max] {
371 let mut body = Map::new();
372 apply_kimi_reasoning_params(&mut body, id, level);
373 assert!(body.is_empty(), "{id} {level:?} must send nothing");
374 }
375 }
376 }
377
378 #[test]
379 fn kimi_code_catalog_rows_are_managed_and_prefixed_consistently() {
380 let models = kimi_code_static_catalog_models();
381 assert_eq!(models.len(), KIMI_CODE_TEXT_MODELS.len());
382 for model in &models {
383 assert_eq!(model.provider_key, "kimi-code");
384 assert!(model.context_tokens.is_some());
385 assert!(model.label.is_some());
386 }
387 }
388}