1use agent_core::reasoning::ReasoningLevel;
30use serde_json::{json, Map, Value};
31
32use super::{
33 CatalogModel, CatalogProviderKind, CatalogSource, Modality, PricingSummary, ReasoningSupport,
34};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum KimiReasoningCapability {
39 Effort {
41 supported: &'static [ReasoningLevel],
42 default_level: Option<ReasoningLevel>,
43 can_disable: bool,
46 },
47 AlwaysThinking,
50 ToggleableThinking,
53}
54
55pub fn kimi_static_capability(model_id: &str) -> Option<KimiReasoningCapability> {
58 use ReasoningLevel::*;
59 match model_id {
60 "kimi-k3" | "kimi-k3.1-pro" | "kimi-k3.1-pro-highspeed" => {
61 Some(KimiReasoningCapability::Effort {
62 supported: &[Low, High, Max],
63 default_level: Some(Max),
64 can_disable: false,
65 })
66 }
67 "kimi-k2.7-code" | "kimi-k2.7-code-highspeed" => {
68 Some(KimiReasoningCapability::AlwaysThinking)
69 }
70 "kimi-k2.6" | "kimi-k2.5" => Some(KimiReasoningCapability::ToggleableThinking),
71 _ => None,
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct KimiCodeModelDescriptor {
85 pub id: &'static str,
86 pub label: &'static str,
87 pub context_tokens: Option<u64>,
88}
89
90pub const KIMI_CODE_TEXT_MODELS: &[KimiCodeModelDescriptor] = &[
91 KimiCodeModelDescriptor {
92 id: "k3",
93 label: "Kimi K3",
94 context_tokens: Some(1_048_576),
95 },
96 KimiCodeModelDescriptor {
97 id: "k3-256k",
98 label: "Kimi K3 256k",
99 context_tokens: Some(262_144),
100 },
101 KimiCodeModelDescriptor {
102 id: "kimi-for-coding",
103 label: "Kimi K2.7 Coding",
104 context_tokens: Some(262_144),
105 },
106 KimiCodeModelDescriptor {
107 id: "kimi-for-coding-highspeed",
108 label: "Kimi K2.7 Coding Highspeed",
109 context_tokens: Some(262_144),
110 },
111];
112
113pub fn kimi_code_model(id: &str) -> Option<&'static KimiCodeModelDescriptor> {
116 KIMI_CODE_TEXT_MODELS.iter().find(|model| model.id == id)
117}
118
119pub fn kimi_code_capability(model_id: &str) -> Option<KimiReasoningCapability> {
126 use ReasoningLevel::*;
127 match model_id {
128 "k3" | "k3-256k" => Some(KimiReasoningCapability::Effort {
129 supported: &[Low, High, Max],
130 default_level: Some(High),
131 can_disable: false,
132 }),
133 "kimi-for-coding" | "kimi-for-coding-highspeed" => {
134 Some(KimiReasoningCapability::AlwaysThinking)
135 }
136 _ => None,
137 }
138}
139
140pub fn kimi_code_static_catalog_models() -> Vec<CatalogModel> {
143 KIMI_CODE_TEXT_MODELS
144 .iter()
145 .map(|descriptor| CatalogModel {
146 provider_key: "kimi-code".into(),
147 provider_name: "Kimi Code".into(),
148 provider_kind: CatalogProviderKind::Generic {
149 key: "kimi-code".into(),
150 },
151 id: descriptor.id.into(),
152 label: Some(descriptor.label.into()),
153 context_tokens: descriptor.context_tokens,
154 max_output_tokens: None,
155 input_modalities: vec![Modality::Text],
156 pricing: PricingSummary::default(),
157 reasoning: ReasoningSupport::GenericOpenAi,
158 source: CatalogSource::StaticFallback,
159 })
160 .collect()
161}
162
163fn kimi_effort_label(level: ReasoningLevel) -> Option<&'static str> {
169 match level {
170 ReasoningLevel::Low => Some("low"),
171 ReasoningLevel::High => Some("high"),
172 ReasoningLevel::Max => Some("max"),
173 _ => None,
174 }
175}
176
177pub fn apply_kimi_reasoning_params(
184 body: &mut Map<String, Value>,
185 model_id: &str,
186 level: ReasoningLevel,
187) {
188 match kimi_static_capability(model_id).or_else(|| kimi_code_capability(model_id)) {
192 Some(KimiReasoningCapability::Effort { supported, .. }) => {
193 if supported.contains(&level) {
195 if let Some(label) = kimi_effort_label(level) {
196 body.insert("reasoning_effort".to_string(), json!(label));
197 }
198 }
199 }
200 Some(KimiReasoningCapability::ToggleableThinking) => {
201 if level == ReasoningLevel::Off {
202 body.insert("thinking".to_string(), json!({ "type": "disabled" }));
203 }
204 }
206 Some(KimiReasoningCapability::AlwaysThinking) | None => {}
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use ReasoningLevel::*;
216
217 const K3_EFFORT_IDS: [&str; 3] = ["kimi-k3", "kimi-k3.1-pro", "kimi-k3.1-pro-highspeed"];
220
221 #[test]
222 fn k3_capability_is_exact_low_high_max_default_max_no_disable() {
223 for id in K3_EFFORT_IDS {
224 let Some(KimiReasoningCapability::Effort {
225 supported,
226 default_level,
227 can_disable,
228 }) = kimi_static_capability(id)
229 else {
230 panic!("{id} must have documented effort capability");
231 };
232 assert_eq!(supported, &[Low, High, Max], "{id}");
233 assert_eq!(default_level, Some(Max), "{id}");
234 assert!(!can_disable, "{id} reasoning must not be disableable");
235 }
236 }
237
238 #[test]
239 fn k27_code_family_is_always_thinking() {
240 for id in ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"] {
241 assert_eq!(
242 kimi_static_capability(id),
243 Some(KimiReasoningCapability::AlwaysThinking),
244 "{id}"
245 );
246 }
247 }
248
249 #[test]
250 fn k26_and_k25_are_toggleable() {
251 for id in ["kimi-k2.6", "kimi-k2.5"] {
252 assert_eq!(
253 kimi_static_capability(id),
254 Some(KimiReasoningCapability::ToggleableThinking),
255 "{id}"
256 );
257 }
258 }
259
260 #[test]
261 fn unknown_ids_fail_closed() {
262 assert_eq!(kimi_static_capability("kimi-k9"), None);
263 assert_eq!(kimi_static_capability(""), None);
264 assert_eq!(kimi_static_capability("kimi-k3-preview"), None);
266 assert_eq!(kimi_static_capability("kimi-k3.1"), None);
267 assert_eq!(kimi_static_capability("kimi-k3.1-pro-preview"), None);
268 assert_eq!(
269 kimi_static_capability("kimi-k3.1-pro-highspeed-preview"),
270 None
271 );
272 }
273
274 #[test]
275 fn k3_body_carries_exact_reasoning_effort() {
276 for id in K3_EFFORT_IDS {
277 for (level, label) in [(Low, "low"), (High, "high"), (Max, "max")] {
278 let mut body = Map::new();
279 apply_kimi_reasoning_params(&mut body, id, level);
280 assert_eq!(body.get("reasoning_effort"), Some(&json!(label)), "{id}");
281 assert!(!body.contains_key("thinking"), "{id}");
282 }
283 }
284 }
285
286 #[test]
287 fn k3_adaptive_omits_the_field() {
288 for id in K3_EFFORT_IDS {
289 let mut body = Map::new();
290 apply_kimi_reasoning_params(&mut body, id, Adaptive);
291 assert!(body.is_empty(), "{id}");
292 }
293 }
294
295 #[test]
296 fn k3_unsupported_levels_are_omitted_never_guessed() {
297 for id in K3_EFFORT_IDS {
300 for level in [Off, Medium, XHigh, Ultra, UltraCode] {
301 let mut body = Map::new();
302 apply_kimi_reasoning_params(&mut body, id, level);
303 assert!(body.is_empty(), "{id} {level:?} must not serialize");
304 }
305 }
306 }
307
308 #[test]
309 fn k26_off_disables_thinking_and_other_levels_omit() {
310 let mut body = Map::new();
311 apply_kimi_reasoning_params(&mut body, "kimi-k2.6", Off);
312 assert_eq!(body.get("thinking"), Some(&json!({"type": "disabled"})));
313 assert!(!body.contains_key("reasoning_effort"));
314 for level in [Adaptive, Low, Medium, High, Max] {
315 let mut body = Map::new();
316 apply_kimi_reasoning_params(&mut body, "kimi-k2.6", level);
317 assert!(body.is_empty(), "{level:?} must omit all fields");
318 }
319 }
320
321 #[test]
322 fn always_thinking_and_unknown_send_nothing() {
323 for id in ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k9"] {
324 for level in [Off, Adaptive, Low, High, Max] {
325 let mut body = Map::new();
326 apply_kimi_reasoning_params(&mut body, id, level);
327 assert!(body.is_empty(), "{id} {level:?} must send nothing");
328 }
329 }
330 }
331
332 #[test]
335 fn kimi_code_k3_family_has_effort_low_high_max_default_high_no_disable() {
336 for id in ["k3", "k3-256k"] {
337 let Some(KimiReasoningCapability::Effort {
338 supported,
339 default_level,
340 can_disable,
341 }) = kimi_code_capability(id)
342 else {
343 panic!("{id} must have managed effort capability");
344 };
345 assert_eq!(supported, &[Low, High, Max]);
346 assert_eq!(default_level, Some(High));
347 assert!(!can_disable, "{id} reasoning must not be disableable");
348 }
349 }
350
351 #[test]
352 fn kimi_code_coding_family_is_always_thinking_and_unknown_fails_closed() {
353 for id in ["kimi-for-coding", "kimi-for-coding-highspeed"] {
354 assert_eq!(
355 kimi_code_capability(id),
356 Some(KimiReasoningCapability::AlwaysThinking),
357 "{id}"
358 );
359 }
360 assert_eq!(kimi_code_capability("k3-preview"), None);
361 assert_eq!(kimi_code_capability("kimi-k3"), None);
363 assert_eq!(kimi_code_capability("kimi-k3.1-pro"), None);
364 assert_eq!(kimi_code_capability("kimi-k3.1-pro-highspeed"), None);
365 assert_eq!(kimi_code_capability(""), None);
366 }
367
368 #[test]
369 fn managed_and_static_id_namespaces_are_disjoint() {
370 for descriptor in KIMI_CODE_TEXT_MODELS {
371 assert!(
372 kimi_static_capability(descriptor.id).is_none(),
373 "{} must not collide with the static Moonshot table",
374 descriptor.id
375 );
376 assert!(kimi_code_model(descriptor.id).is_some());
377 }
378 }
379
380 #[test]
381 fn managed_k3_body_carries_exact_reasoning_effort_via_shared_apply() {
382 for (level, label) in [(Low, "low"), (High, "high"), (Max, "max")] {
383 let mut body = Map::new();
384 apply_kimi_reasoning_params(&mut body, "k3", level);
385 assert_eq!(body.get("reasoning_effort"), Some(&json!(label)));
386 assert!(!body.contains_key("thinking"));
387 }
388 for level in [Adaptive, Off, Medium, XHigh] {
390 let mut body = Map::new();
391 apply_kimi_reasoning_params(&mut body, "k3-256k", level);
392 assert!(body.is_empty(), "{level:?} must not serialize");
393 }
394 }
395
396 #[test]
397 fn managed_always_thinking_sends_nothing() {
398 for id in ["kimi-for-coding", "kimi-for-coding-highspeed"] {
399 for level in [Off, Adaptive, Low, High, Max] {
400 let mut body = Map::new();
401 apply_kimi_reasoning_params(&mut body, id, level);
402 assert!(body.is_empty(), "{id} {level:?} must send nothing");
403 }
404 }
405 }
406
407 #[test]
408 fn kimi_code_catalog_rows_are_managed_and_prefixed_consistently() {
409 let models = kimi_code_static_catalog_models();
410 assert_eq!(models.len(), KIMI_CODE_TEXT_MODELS.len());
411 for model in &models {
412 assert_eq!(model.provider_key, "kimi-code");
413 assert!(model.context_tokens.is_some());
414 assert!(model.label.is_some());
415 }
416 }
417}