[content] crates/cli/src/main.rs (rust)
        
        // Try to load tokenizer_config.json for chat template
        let config = Self::load_tokenizer_config(model_name).unwrap_or_default();
        
        Ok(Self { 
            inner,
            chat_template: config.chat_template,
            bos_token: config.bos_token.map(|t| t.as_str().to_string()),
            eos_token: config.eos_token.map(|t| t.as_str().to_string()),
            unk_token: config.unk_token.map(|t| t.as_str().to_string()),
            pad_token: config.pad_token.map(|t| t.as_str().to_string()),
        })
    }

    /// Load tokenizer_config.json from HuggingFace Hub or local path.
    fn load_tokenizer_config(model_name: &str) -> Result<TokenizerConfig, Box<dyn std::error::Error>> {
        // Check if it's a local path
        let local_config_path = PathBuf::from(model_name).join("tokenizer_config.json");
        if local_config_path.exists() {
            let content = std::fs::read_to_string(&local_config_path)?;
            let config: TokenizerConfig = serde_json::from_str(&content)?;
            return Ok(config);
        }

        // Otherwise, fetch from HuggingFace Hub
        let url = format!(
            "https://huggingface.co/{}/resolve/main/tokenizer_config.json",
            model_name
        );
        
        // Use a simple blocking HTTP request
        let response = ureq::get(&url).call()?;
        let content = response.into_string()?;
        let config: TokenizerConfig = serde_json::from_str(&content)?;
        
        Ok(config)
    }

    /// Apply chat template to render a conversation into a string.
    /// Returns None if no chat template is configured.
    fn apply_chat_template(&self, messages: &[ChatMessage]) -> Option<String> {
        let template_str = self.chat_template.as_ref()?;
        
        // Preprocess template to convert Python method syntax to minijinja filter/test syntax
        let preprocessed_template = preprocess_chat_template(template_str);
        
        let mut env = Environment::new();
        
        // Add Python-compatible string methods as filters and tests
        add_python_string_methods(&mut env);
        
        // Add startswith/endswith as tests (for `is` syntax)
        env.add_test("startswith", |s: &str, prefix: &str| -> bool {
            s.starts_with(prefix)
        });
        env.add_test("endswith", |s: &str, suffix: &str| -> bool {
            s.ends_with(suffix)
        });
        
        if let Err(e) = env.add_template("chat", &preprocessed_template) {
            eprintln!("Warning: Failed to compile chat template: {}", e);
            return None;
        }
        
        let tmpl = env.get_template("chat").ok()?;
        
        // Convert messages to a format minijinja can iterate over
        let messages_for_template: Vec<Value> = messages
            .iter()
            .map(|m| {
                Value::from_iter([
                    ("role", Value::from(m.role.as_str())),
                    ("content", Value::from(m.content.as_str())),
                ])
            })
            .collect();
        
        let rendered = tmpl.render(context! {
            messages => messages_for_template,
            add_generation_prompt => false,
            bos_token => self.bos_token.as_deref().unwrap_or(""),
            eos_token => self.eos_token.as_deref().unwrap_or(""),
            unk_token => self.unk_token.as_deref().unwrap_or(""),
            pad_token => self.pad_token.as_deref().unwrap_or(""),
        });
        
        match rendered {
            Ok(s) => Some(s),
            Err(e) => {
                eprintln!("Warning: Failed to render chat template: {}", e);
                None
            }
        }