1use anyhow::Result;
6use std::path::Path;
7use tokenizers::Tokenizer;
8
9#[path = "chat_impl/vision_pads.rs"]
10mod vision_pads;
11
12use super::{
13 ChatEncoding, ChatTokenizer, StreamingDecoder, autoclose_assistant_think,
14 normalize_tool_call_arguments, remap_developer_role, resolve_think_control,
15};
16
17pub(crate) fn preprocess_for_render(
28 messages: &[serde_json::Value],
29 enable_thinking: bool,
30) -> (Vec<serde_json::Value>, bool) {
31 let prepared = normalize_tool_call_arguments(messages);
33 let mut prepared = remap_developer_role(prepared);
36 autoclose_assistant_think(&mut prepared);
38 let (prepared, control_override) = resolve_think_control(&prepared);
40 let effective_thinking = control_override.unwrap_or(enable_thinking);
41 (prepared, effective_thinking)
42}
43
44impl ChatTokenizer {
45 pub fn from_model_dir(
46 model_dir: &Path,
47 eos_token_id: u32,
48 supports_thinking: bool,
49 model_type: &str,
50 repo_root: Option<&Path>,
51 disable_template_overrides: bool,
52 ) -> Result<Self> {
53 let official_k3 = super::kimi_k3::uses_xtml(model_dir, model_type)?;
54 let tokenizer_path = model_dir.join("tokenizer.json");
55 let mut tokenizer = Tokenizer::from_file(&tokenizer_path)
56 .map_err(|e| anyhow::anyhow!("Failed to load tokenizer: {e}"))?;
57 tokenizer
58 .with_truncation(None)
59 .map_err(|e| anyhow::anyhow!("Failed to disable tokenizer truncation: {e}"))?;
60
61 let override_tmpl = if disable_template_overrides {
93 None
94 } else {
95 super::jinja_helpers::load_override_template(model_type, repo_root)
96 };
97 let (chat_template, checkpoint_template) = if official_k3 {
98 tracing::warn!("Official Kimi K3: raw completions only; XTML chat is unavailable");
99 (String::new(), false)
100 } else if let Some(override_tmpl) = override_tmpl {
101 (override_tmpl, false)
102 } else if let Some(config_tmpl) = super::jinja_helpers::load_config_template(model_dir)? {
103 (config_tmpl, true)
104 } else {
105 tracing::warn!("No chat template found — using default ChatML");
106 (
107 super::jinja_helpers::default_chatml_template(supports_thinking),
108 false,
109 )
110 };
111
112 let jinja_env = super::jinja_helpers::build_jinja_env(&chat_template)?;
113
114 let openai_jinja_env = super::jinja_helpers::load_openai_template(model_type, repo_root)
118 .and_then(|tmpl| {
119 tracing::info!("Loaded OpenAI-variant Jinja template for {model_type}");
120 super::jinja_helpers::build_jinja_env(&tmpl).ok()
121 });
122 let chat_encoding = if official_k3 {
123 ChatEncoding::KimiK3XtmlUnsupported
124 } else if model_type == "deepseek_v4" || model_type == "deepseek_v41" {
125 tracing::info!("Using checkpoint-native DeepSeek-V4 message encoding");
126 ChatEncoding::DeepseekV4
127 } else {
128 ChatEncoding::Jinja
129 };
130
131 let native_qwen_tool_template = checkpoint_owns_qwen_tool_prompt(
132 model_type,
133 checkpoint_template,
134 openai_jinja_env.is_some(),
135 ) && jinja_env
136 .get_template("chat")?
137 .undeclared_variables(false)
138 .contains("tools");
139 tracing::info!("Loaded tokenizer from {}", tokenizer_path.display());
140 Ok(Self {
141 tokenizer,
142 eos_token_id,
143 supports_thinking,
144 chat_encoding,
145 native_qwen_tool_template,
146 chat_template,
147 jinja_env,
148 openai_jinja_env,
149 })
150 }
151
152 pub(crate) fn uses_native_qwen_tool_template(&self) -> bool {
153 self.native_qwen_tool_template
154 }
155
156 pub fn inner(&self) -> &tokenizers::Tokenizer {
159 &self.tokenizer
160 }
161
162 pub fn encode(&self, text: &str) -> Result<Vec<u32>> {
163 let encoding = self
164 .tokenizer
165 .encode(text, false)
166 .map_err(|e| anyhow::anyhow!("Tokenizer encode error: {e}"))?;
167 Ok(encoding.get_ids().to_vec())
168 }
169
170 pub fn decode(&self, ids: &[u32]) -> Result<String> {
171 self.tokenizer
172 .decode(ids, true)
173 .map_err(|e| anyhow::anyhow!("Tokenizer decode error: {e}"))
174 }
175
176 pub fn decode_with_special(&self, ids: &[u32]) -> Result<String> {
180 self.tokenizer
181 .decode(ids, false)
182 .map_err(|e| anyhow::anyhow!("Tokenizer decode error: {e}"))
183 }
184
185 pub fn incremental_decode(
201 &self,
202 toks: &[u32],
203 prefix_offset: &mut usize,
204 read_offset: &mut usize,
205 ) -> String {
206 if *read_offset > toks.len() || *prefix_offset > *read_offset {
208 *prefix_offset = 0;
209 *read_offset = 0;
210 }
211 let prefix_text = self
212 .decode(&toks[*prefix_offset..*read_offset])
213 .unwrap_or_default();
214 let new_text = self.decode(&toks[*prefix_offset..]).unwrap_or_default();
215 if new_text.len() > prefix_text.len()
216 && !new_text.ends_with('\u{FFFD}')
217 && let Some(delta) = new_text.get(prefix_text.len()..)
218 {
219 let delta = delta.to_string();
220 *prefix_offset = *read_offset;
221 *read_offset = toks.len();
222 return delta;
223 }
224 String::new()
227 }
228
229 pub fn streaming_decoder(&self, skip_special_tokens: bool) -> StreamingDecoder<'_> {
233 StreamingDecoder {
234 inner: self.tokenizer.decode_stream(skip_special_tokens),
235 }
236 }
237
238 pub fn apply_chat_template_jinja(
245 &self,
246 messages: &[serde_json::Value],
247 tools: Option<&[serde_json::Value]>,
248 enable_thinking: bool,
249 disable_tool_steering: bool,
250 ) -> Result<Vec<u32>> {
251 self.apply_chat_template_jinja_with_effort(
252 messages,
253 tools,
254 enable_thinking,
255 disable_tool_steering,
256 None,
257 None,
258 )
259 }
260
261 pub fn apply_chat_template_jinja_with_effort(
262 &self,
263 messages: &[serde_json::Value],
264 tools: Option<&[serde_json::Value]>,
265 enable_thinking: bool,
266 disable_tool_steering: bool,
267 reasoning_effort: Option<&str>,
268 preserve_thinking: Option<bool>,
269 ) -> Result<Vec<u32>> {
270 super::kimi_k3::require_chat_support(self.chat_encoding)?;
271 if self.chat_encoding == ChatEncoding::DeepseekV4 {
272 let rendered = super::deepseek_v4::encode_messages(
273 messages,
274 tools,
275 enable_thinking,
276 reasoning_effort,
277 )?;
278 return self.encode(&rendered);
279 }
280
281 let rendered = super::chat_render::render_chat(
282 &self.jinja_env,
283 messages,
284 tools,
285 super::chat_render::RenderFlags {
286 enable_thinking,
287 disable_tool_steering,
288 reasoning_effort,
289 preserve_thinking,
290 allow_continue_final: true,
291 },
292 )?;
293
294 if rendered.len() < 2000 {
297 let tail_start = rendered.floor_char_boundary(rendered.len().saturating_sub(200));
298 tracing::info!(
299 "Jinja rendered ({} chars): {:?}",
300 rendered.len(),
301 &rendered[tail_start..]
302 );
303 }
304
305 self.encode(&rendered)
306 }
307
308 pub fn apply_chat_template_openai(
312 &self,
313 messages: &[serde_json::Value],
314 tools: Option<&[serde_json::Value]>,
315 enable_thinking: bool,
316 disable_tool_steering: bool,
317 ) -> Result<Vec<u32>> {
318 self.apply_chat_template_openai_with_effort(
319 messages,
320 tools,
321 enable_thinking,
322 disable_tool_steering,
323 None,
324 None,
325 )
326 }
327
328 pub fn apply_chat_template_openai_with_effort(
329 &self,
330 messages: &[serde_json::Value],
331 tools: Option<&[serde_json::Value]>,
332 enable_thinking: bool,
333 disable_tool_steering: bool,
334 reasoning_effort: Option<&str>,
335 preserve_thinking: Option<bool>,
336 ) -> Result<Vec<u32>> {
337 super::kimi_k3::require_chat_support(self.chat_encoding)?;
338 if self.chat_encoding == ChatEncoding::DeepseekV4 {
339 return self.apply_chat_template_jinja_with_effort(
340 messages,
341 tools,
342 enable_thinking,
343 disable_tool_steering,
344 reasoning_effort,
345 preserve_thinking,
346 );
347 }
348 if let Some(ref env) = self.openai_jinja_env {
349 let rendered = super::chat_render::render_chat(
353 env,
354 messages,
355 tools,
356 super::chat_render::RenderFlags {
357 enable_thinking,
358 disable_tool_steering,
359 reasoning_effort,
360 preserve_thinking,
361 allow_continue_final: false,
362 },
363 )
364 .map_err(|e| anyhow::anyhow!("Failed to render OpenAI Jinja template: {e}"))?;
365 self.encode(&rendered)
366 } else {
367 self.apply_chat_template_jinja_with_effort(
368 messages,
369 tools,
370 enable_thinking,
371 disable_tool_steering,
372 reasoning_effort,
373 preserve_thinking,
374 )
375 }
376 }
377
378 pub fn apply_chat_template(
381 &self,
382 messages: &[(String, String)],
383 enable_thinking: bool,
384 _image_pad_counts: &[usize],
385 ) -> Result<Vec<u32>> {
386 let json_messages: Vec<serde_json::Value> = messages
387 .iter()
388 .map(|(role, content)| {
389 serde_json::json!({
390 "role": role,
391 "content": content,
392 })
393 })
394 .collect();
395
396 self.apply_chat_template_jinja(&json_messages, None, enable_thinking, false)
397 }
398
399 pub fn eos_token_id(&self) -> u32 {
400 self.eos_token_id
401 }
402
403 pub fn think_end_token_id(&self) -> Option<u32> {
404 if !self.supports_thinking {
405 return None;
406 }
407 match self.encode("</think>") {
408 Ok(ids) if ids.len() == 1 => Some(ids[0]),
409 _ => None,
410 }
411 }
412
413 pub fn supports_thinking(&self) -> bool {
414 self.supports_thinking
415 }
416
417 pub fn uses_deepseek_v4_encoding(&self) -> bool {
418 self.chat_encoding == ChatEncoding::DeepseekV4
419 }
420}
421
422fn checkpoint_owns_qwen_tool_prompt(
425 model_type: &str,
426 checkpoint: bool,
427 openai_override: bool,
428) -> bool {
429 checkpoint
430 && !openai_override
431 && matches!(
432 model_type,
433 "qwen3_5" | "qwen3_5_moe" | "qwen3_6" | "qwen3_6_moe"
434 )
435}