use kopitiam_core::{Error, Result};
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct Vocab {
id_to_bytes: Vec<Vec<u8>>,
bytes_to_id: HashMap<Vec<u8>, u32>,
}
impl Vocab {
pub fn new() -> Self {
Self::default()
}
pub fn from_entries(entries: Vec<Vec<u8>>) -> Result<Self> {
let mut vocab = Self::new();
for (id, bytes) in entries.into_iter().enumerate() {
vocab.insert(id as u32, bytes)?;
}
Ok(vocab)
}
pub fn insert(&mut self, id: u32, bytes: Vec<u8>) -> Result<()> {
let idx = id as usize;
if idx < self.id_to_bytes.len() {
let existing = &self.id_to_bytes[idx];
if !existing.is_empty() && existing != &bytes {
return Err(Error::MalformedModel {
format: "bpe-vocab",
reason: format!(
"id {id} is already assigned to a different token \
({existing:?} vs {bytes:?})"
),
});
}
} else {
self.id_to_bytes.resize(idx + 1, Vec::new());
}
self.id_to_bytes[idx] = bytes.clone();
self.bytes_to_id.insert(bytes, id);
Ok(())
}
pub fn len(&self) -> usize {
self.id_to_bytes.len()
}
pub fn is_empty(&self) -> bool {
self.id_to_bytes.is_empty()
}
pub fn bytes_of(&self, id: u32) -> Option<&[u8]> {
self.id_to_bytes
.get(id as usize)
.filter(|b| !b.is_empty())
.map(Vec::as_slice)
}
pub fn id_of(&self, bytes: &[u8]) -> Option<u32> {
self.bytes_to_id.get(bytes).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_entries() {
let vocab = Vocab::from_entries(vec![b"a".to_vec(), b"b".to_vec(), b"ab".to_vec()])
.expect("valid vocab");
assert_eq!(vocab.len(), 3);
assert_eq!(vocab.id_of(b"ab"), Some(2));
assert_eq!(vocab.bytes_of(2), Some(b"ab".as_slice()));
assert_eq!(vocab.id_of(b"missing"), None);
assert_eq!(vocab.bytes_of(99), None);
}
#[test]
fn insert_grows_past_the_end_for_appended_special_tokens() {
let mut vocab = Vocab::from_entries(vec![b"a".to_vec(), b"b".to_vec()]).unwrap();
vocab
.insert(10, b"<|endoftext|>".to_vec())
.expect("growth insert should succeed");
assert_eq!(vocab.len(), 11);
assert_eq!(vocab.id_of(b"<|endoftext|>"), Some(10));
assert_eq!(vocab.bytes_of(5), None);
}
#[test]
fn insert_rejects_conflicting_reassignment() {
let mut vocab = Vocab::from_entries(vec![b"a".to_vec()]).unwrap();
let err = vocab.insert(0, b"z".to_vec()).unwrap_err();
assert!(matches!(err, Error::MalformedModel { .. }));
}
#[test]
fn insert_tolerates_reinserting_the_same_value() {
let mut vocab = Vocab::from_entries(vec![b"a".to_vec()]).unwrap();
vocab.insert(0, b"a".to_vec()).expect("idempotent insert");
assert_eq!(vocab.len(), 1);
}
}