use crate::qwen2::config::Qwen2Config;
use crate::qwen2::model::Qwen2ForCausalLM;
use trustformers_core::errors::Result;
use trustformers_core::tensor::Tensor;
pub fn format_qwen2_chat(system: &str, messages: &[(String, String)]) -> String {
const IM_START: &str = "<|im_start|>";
const IM_END: &str = "<|im_end|>";
let mut buf = String::new();
if !system.is_empty() {
buf.push_str(IM_START);
buf.push_str("system\n");
buf.push_str(system);
buf.push('\n');
buf.push_str(IM_END);
buf.push('\n');
}
for (role, content) in messages {
buf.push_str(IM_START);
buf.push_str(role.as_str());
buf.push('\n');
buf.push_str(content.as_str());
buf.push_str(IM_END);
buf.push('\n');
}
buf.push_str(IM_START);
buf.push_str("assistant\n");
buf
}
pub struct Qwen2ChatModel {
inner: Qwen2ForCausalLM,
}
impl Qwen2ChatModel {
pub fn new(config: Qwen2Config) -> Result<Self> {
let inner = Qwen2ForCausalLM::new(config)?;
Ok(Self { inner })
}
pub fn config(&self) -> &Qwen2Config {
self.inner.config()
}
pub fn parameter_count(&self) -> usize {
self.inner.parameter_count()
}
pub fn forward(&self, input_ids: Vec<u32>) -> Result<Tensor> {
self.inner.forward(input_ids)
}
pub fn build_prompt(
&self,
system_prompt: &str,
messages: &[(String, String)],
) -> String {
format_qwen2_chat(system_prompt, messages)
}
pub fn greedy_next_token(&self, logits: &Tensor) -> Result<u32> {
match logits {
Tensor::F32(arr) => {
let flat: Vec<f32> = arr.iter().copied().collect();
let best = flat
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(idx, _)| idx as u32)
.unwrap_or(0);
Ok(best)
},
_ => Ok(0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::qwen2::config::Qwen2Config;
use trustformers_core::tensor::Tensor;
fn small_config() -> Qwen2Config {
Qwen2Config {
vocab_size: 256,
hidden_size: 64,
intermediate_size: 128,
num_hidden_layers: 2,
num_attention_heads: 4,
num_key_value_heads: 2,
max_position_embeddings: 64,
rope_theta: 10000.0,
rms_norm_eps: 1e-6,
sliding_window: None,
use_sliding_window: false,
qkv_bias: false,
}
}
#[test]
fn test_format_qwen2_no_system_when_empty() {
let out = format_qwen2_chat("", &[]);
assert!(!out.contains("system"), "no system block for empty system");
}
#[test]
fn test_format_qwen2_system_present() {
let out = format_qwen2_chat("You are helpful.", &[]);
assert!(out.contains("system"), "system role must appear");
assert!(out.contains("You are helpful."), "system content must appear");
}
#[test]
fn test_format_qwen2_chatml_tokens() {
let out = format_qwen2_chat("sys", &[]);
assert!(out.contains("<|im_start|>"), "must use im_start");
assert!(out.contains("<|im_end|>"), "must use im_end");
}
#[test]
fn test_format_qwen2_ends_with_assistant() {
let out = format_qwen2_chat("sys", &[("user".to_string(), "hi".to_string())]);
assert!(out.ends_with("assistant\n"), "must end with open assistant turn");
}
#[test]
fn test_format_qwen2_user_message_present() {
let msgs = vec![("user".to_string(), "Hello Qwen".to_string())];
let out = format_qwen2_chat("", &msgs);
assert!(out.contains("Hello Qwen"), "user message must appear");
}
#[test]
fn test_format_qwen2_multiple_roles() {
let msgs = vec![
("user".to_string(), "question".to_string()),
("assistant".to_string(), "answer".to_string()),
];
let out = format_qwen2_chat("", &msgs);
assert!(out.contains("question"), "user content present");
assert!(out.contains("answer"), "assistant content present");
}
#[test]
fn test_format_qwen2_im_start_count() {
let msgs = vec![("user".to_string(), "hi".to_string())];
let out = format_qwen2_chat("sys", &msgs);
let count = out.matches("<|im_start|>").count();
assert_eq!(count, 3, "expected 3 im_start tokens, got {count}");
}
#[test]
fn test_format_qwen2_im_end_count() {
let msgs = vec![("user".to_string(), "hi".to_string())];
let out = format_qwen2_chat("sys", &msgs);
let count = out.matches("<|im_end|>").count();
assert_eq!(count, 2, "expected 2 im_end tokens, got {count}");
}
#[test]
fn test_format_qwen2_deterministic() {
let msgs = vec![("user".to_string(), "test".to_string())];
let a = format_qwen2_chat("sys", &msgs);
let b = format_qwen2_chat("sys", &msgs);
assert_eq!(a, b, "format must be deterministic");
}
#[test]
fn test_qwen2_chat_model_construction() {
let result = Qwen2ChatModel::new(small_config());
assert!(result.is_ok(), "Qwen2ChatModel must construct successfully");
}
#[test]
fn test_qwen2_parameter_count_nonzero() {
let model = Qwen2ChatModel::new(small_config()).unwrap_or_else(|_| panic!("init failed"));
assert!(model.parameter_count() > 0, "parameter count must be > 0");
}
#[test]
fn test_qwen2_config_accessor() {
let cfg = small_config();
let vocab = cfg.vocab_size;
let model = Qwen2ChatModel::new(cfg).unwrap_or_else(|_| panic!("init failed"));
assert_eq!(model.config().vocab_size, vocab);
}
#[test]
fn test_qwen2_forward_nonempty_logits() {
let model = Qwen2ChatModel::new(small_config()).unwrap_or_else(|_| panic!("init failed"));
let result = model.forward(vec![1u32, 2, 3]);
assert!(result.is_ok(), "forward must succeed");
}
#[test]
fn test_build_prompt_matches_format_fn() {
let model = Qwen2ChatModel::new(small_config()).unwrap_or_else(|_| panic!("init failed"));
let msgs = vec![("user".to_string(), "test".to_string())];
let via_model = model.build_prompt("sys", &msgs);
let direct = format_qwen2_chat("sys", &msgs);
assert_eq!(via_model, direct, "build_prompt must match format fn");
}
#[test]
fn test_qwen2_greedy_picks_argmax() {
let model = Qwen2ChatModel::new(small_config()).unwrap_or_else(|_| panic!("init failed"));
let t = Tensor::from_vec(vec![0.1f32, 0.3, 0.9, 0.2], &[4])
.unwrap_or_else(|_| panic!("tensor failed"));
let tok = model.greedy_next_token(&t).unwrap_or(99);
assert_eq!(tok, 2u32, "argmax of [0.1, 0.3, 0.9, 0.2] must be 2");
}
#[test]
fn test_qwen2_greedy_first_max() {
let model = Qwen2ChatModel::new(small_config()).unwrap_or_else(|_| panic!("init failed"));
let t = Tensor::from_vec(vec![9.0f32, 1.0, 2.0], &[3])
.unwrap_or_else(|_| panic!("tensor failed"));
let tok = model.greedy_next_token(&t).unwrap_or(99);
assert_eq!(tok, 0u32, "argmax of [9, 1, 2] must be 0");
}
#[test]
fn test_qwen2_forward_finite() {
let model = Qwen2ChatModel::new(small_config()).unwrap_or_else(|_| panic!("init failed"));
if let Ok(tensor) = model.forward(vec![0u32]) {
if let Tensor::F32(arr) = &tensor {
for &v in arr.iter() {
assert!(v.is_finite(), "logit {v} must be finite");
}
}
}
}
#[test]
fn test_format_qwen2_only_assistant_when_all_empty() {
let out = format_qwen2_chat("", &[]);
assert!(out.contains("<|im_start|>assistant"), "must have open assistant");
}
}