1use std::{
2 collections::HashMap,
3 fs::File,
4 io::{BufReader, BufWriter, Read as _, Write as _},
5 path::{Path, PathBuf},
6 sync::OnceLock,
7};
8
9use base64::{prelude::BASE64_STANDARD, Engine as _};
10
11use crate::tiktoken::{CoreBPE, Rank};
12use sha1::Sha1;
13use sha2::{Digest as _, Sha256};
14
15#[derive(Debug, thiserror::Error)]
16pub enum LoadError {
17 #[error("the env var TIKTOKEN_ENCODINGS_BASE is not set, or invalid")]
18 InvalidEncodingBaseDirEnvVar,
19
20 #[error("unknown encoding name: {0}")]
21 UnknownEncodingName(String),
22
23 #[error("invalid tiktoken vocab file: {0}")]
24 InvalidTiktokenVocabFile(#[source] std::io::Error),
25
26 #[error("failed to create CoreBPE: {0}")]
27 CoreBPECreationFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
28
29 #[error("error downloading or loading vocab file: {0}")]
30 DownloadOrLoadVocabFile(
31 #[source]
32 #[from]
33 RemoteVocabFileError,
34 ),
35
36 #[error("failed to extend encoding")]
37 FailedToExtendEncoding(#[source] Box<dyn std::error::Error + Send + Sync>),
38}
39
40#[derive(Debug, thiserror::Error)]
41pub enum RemoteVocabFileError {
42 #[error("failed to download or load vocab file")]
43 FailedToDownloadOrLoadVocabFile(#[source] Box<dyn std::error::Error + Send + Sync>),
44
45 #[error("an underlying IO error occurred while {0}: {1}")]
46 IOError(String, #[source] std::io::Error),
47
48 #[error("hash mismatch for remote file {file_url}")]
49 HashMismatch {
50 file_url: String,
51 expected_hash: String,
52 computed_hash: String,
53 },
54}
55
56const TIKTOKEN_ENCODINGS_BASE_VAR: &str = "TIKTOKEN_ENCODINGS_BASE";
57const DEFAULT_TIKTOKEN_BASE_URL: &str = "https://openaipublic.blob.core.windows.net/encodings/";
58
59static TIKTOKEN_BASE_URL_OVERRIDE: OnceLock<String> = OnceLock::new();
60
61pub fn set_tiktoken_base_url(base_url: impl Into<String>) {
62 let mut base = base_url.into();
63 if !base.ends_with('/') {
64 base.push('/');
65 }
66 let _ = TIKTOKEN_BASE_URL_OVERRIDE.set(base);
68}
69
70fn tiktoken_base_url() -> &'static str {
71 TIKTOKEN_BASE_URL_OVERRIDE
72 .get()
73 .map(|s| s.as_str())
74 .unwrap_or(DEFAULT_TIKTOKEN_BASE_URL)
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum Encoding {
79 O200kBase,
80 O200kHarmony,
81 Cl100kBase,
82}
83
84impl Encoding {
85 pub fn all() -> &'static [Self] {
86 &[Self::O200kBase, Self::O200kHarmony, Self::Cl100kBase]
87 }
88
89 pub fn from_name(name: impl AsRef<str>) -> Option<Self> {
90 let name_str = name.as_ref();
91 for encoding in Self::all() {
92 if encoding.name() == name_str {
93 return Some(*encoding);
94 }
95 }
96 None
97 }
98
99 #[cfg(not(target_arch = "wasm32"))]
100 pub fn load_from_name(name: impl AsRef<str>) -> Result<CoreBPE, LoadError> {
101 let name = name.as_ref();
102 Self::from_name(name)
103 .ok_or_else(|| LoadError::UnknownEncodingName(name.to_string()))?
104 .load()
105 }
106
107 #[cfg(target_arch = "wasm32")]
108 pub async fn load_from_name(name: impl AsRef<str>) -> Result<CoreBPE, LoadError> {
109 let name = name.as_ref();
110 Self::from_name(name)
111 .ok_or_else(|| LoadError::UnknownEncodingName(name.to_string()))?
112 .load()
113 .await
114 }
115
116 pub fn name(&self) -> &'static str {
117 match self {
118 Self::O200kBase => "o200k_base",
119 Self::O200kHarmony => "o200k_harmony",
120 Self::Cl100kBase => "cl100k_base",
121 }
122 }
123
124 #[cfg(not(target_arch = "wasm32"))]
125 pub fn load(&self) -> Result<CoreBPE, LoadError> {
126 #[cfg(not(target_arch = "wasm32"))]
127 let (vocab_file_path, check_hash) =
128 if let Ok(base_dir) = std::env::var(TIKTOKEN_ENCODINGS_BASE_VAR) {
129 (PathBuf::from(base_dir).join(self.vocab_file_name()), true)
130 } else {
131 let url = self.public_vocab_file_url();
132 (
133 download_or_find_cached_file(&url, Some(self.expected_hash()))
134 .map_err(LoadError::DownloadOrLoadVocabFile)?,
135 false,
136 )
137 };
138
139 match self {
140 Self::O200kHarmony => {
141 let mut specials: Vec<(String, Rank)> = self
142 .special_tokens()
143 .iter()
144 .map(|(s, r)| ((*s).to_string(), *r))
145 .collect();
146 specials.extend((200014..=201088).map(|id| (format!("<|reserved_{id}|>"), id)));
147 #[cfg(not(target_arch = "wasm32"))]
148 {
149 load_encoding_from_file(
150 vocab_file_path,
151 check_hash.then(|| self.expected_hash()),
152 specials,
153 &self.pattern(),
154 )
155 }
156 #[cfg(target_arch = "wasm32")]
157 {
158 load_encoding_from_bytes(&vocab_bytes, None, specials, &self.pattern())
159 }
160 }
161 Self::O200kBase => {
162 let mut specials: Vec<(String, Rank)> = self
163 .special_tokens()
164 .iter()
165 .map(|(s, r)| ((*s).to_string(), *r))
166 .collect();
167 specials.extend((199998..=201088).map(|id| (format!("<|reserved_{id}|>"), id)));
168 #[cfg(not(target_arch = "wasm32"))]
169 {
170 load_encoding_from_file(
171 vocab_file_path,
172 check_hash.then(|| self.expected_hash()),
173 specials,
174 &self.pattern(),
175 )
176 }
177 #[cfg(target_arch = "wasm32")]
178 {
179 load_encoding_from_bytes(&vocab_bytes, None, specials, &self.pattern())
180 }
181 }
182 _ => {
183 #[cfg(not(target_arch = "wasm32"))]
184 {
185 load_encoding_from_file(
186 vocab_file_path,
187 check_hash.then(|| self.expected_hash()),
188 self.special_tokens().iter().cloned(),
189 &self.pattern(),
190 )
191 }
192 #[cfg(target_arch = "wasm32")]
193 {
194 load_encoding_from_bytes(
195 &vocab_bytes,
196 None,
197 self.special_tokens().iter().cloned(),
198 &self.pattern(),
199 )
200 }
201 }
202 }
203 }
204
205 #[cfg(target_arch = "wasm32")]
206 pub async fn load(&self) -> Result<CoreBPE, LoadError> {
207 let url = self.public_vocab_file_url();
208 let vocab_bytes = download_or_find_cached_file_bytes(&url, Some(self.expected_hash()))
209 .await
210 .map_err(LoadError::DownloadOrLoadVocabFile)?;
211
212 match self {
213 Self::O200kHarmony => {
214 let mut specials: Vec<(String, Rank)> = self
215 .special_tokens()
216 .iter()
217 .map(|(s, r)| ((*s).to_string(), *r))
218 .collect();
219 specials.extend((200014..=201088).map(|id| (format!("<|reserved_{id}|>"), id)));
220 load_encoding_from_bytes(&vocab_bytes, None, specials, &self.pattern())
221 }
222 Self::O200kBase => {
223 let mut specials: Vec<(String, Rank)> = self
224 .special_tokens()
225 .iter()
226 .map(|(s, r)| ((*s).to_string(), *r))
227 .collect();
228 specials.extend((199998..=201088).map(|id| (format!("<|reserved_{id}|>"), id)));
229 load_encoding_from_bytes(&vocab_bytes, None, specials, &self.pattern())
230 }
231 _ => load_encoding_from_bytes(
232 &vocab_bytes,
233 None,
234 self.special_tokens().iter().cloned(),
235 &self.pattern(),
236 ),
237 }
238 }
239
240 fn public_vocab_file_url(&self) -> String {
241 let base = tiktoken_base_url();
242 match self {
243 Self::O200kBase => format!("{base}o200k_base.tiktoken"),
244 Self::O200kHarmony => format!("{base}o200k_base.tiktoken"),
245 Self::Cl100kBase => format!("{base}cl100k_base.tiktoken"),
246 }
247 }
248
249 fn vocab_file_name(&self) -> &'static str {
250 match self {
251 Self::O200kBase => "o200k_base.tiktoken",
252 Self::O200kHarmony => "o200k_base.tiktoken",
253 Self::Cl100kBase => "cl100k_base.tiktoken",
254 }
255 }
256
257 fn expected_hash(&self) -> &'static str {
258 match self {
259 Self::O200kBase => "446a9538cb6c348e3516120d7c08b09f57c36495e2acfffe59a5bf8b0cfb1a2d",
260 Self::O200kHarmony => {
261 "446a9538cb6c348e3516120d7c08b09f57c36495e2acfffe59a5bf8b0cfb1a2d"
262 }
263 Self::Cl100kBase => "223921b76ee99bde995b7ff738513eef100fb51d18c93597a113bcffe865b2a7",
264 }
265 }
266
267 fn special_tokens(&self) -> &'static [(&'static str, Rank)] {
268 match self {
269 Self::O200kBase => &[],
270 Self::O200kHarmony => &[
271 ("<|startoftext|>", 199998),
272 ("<|endoftext|>", 199999),
273 ("<|reserved_200000|>", 200000),
274 ("<|reserved_200001|>", 200001),
275 ("<|return|>", 200002),
276 ("<|constrain|>", 200003),
277 ("<|reserved_200004|>", 200004),
278 ("<|channel|>", 200005),
279 ("<|start|>", 200006),
280 ("<|end|>", 200007),
281 ("<|message|>", 200008),
282 ("<|reserved_200009|>", 200009),
283 ("<|reserved_200010|>", 200010),
284 ("<|reserved_200011|>", 200011),
285 ("<|call|>", 200012),
286 ("<|reserved_200013|>", 200013),
287 ],
288 Self::Cl100kBase => &[
289 ("<|endoftext|>", 100257),
290 ("<|fim_prefix|>", 100258),
291 ("<|fim_middle|>", 100259),
292 ("<|fim_suffix|>", 100260),
293 ("<|endofprompt|>", 100276),
294 ],
295 }
296 }
297
298 fn pattern(&self) -> String {
299 match self {
300 Self::O200kBase => {
301 [
302 "[^\\r\\n\\p{L}\\p{N}]?[\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}\\p{M}]*[\\p{Ll}\\p{Lm}\\p{Lo}\\p{M}]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
303 "[^\\r\\n\\p{L}\\p{N}]?[\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}\\p{M}]+[\\p{Ll}\\p{Lm}\\p{Lo}\\p{M}]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
304 "\\p{N}{1,3}",
305 " ?[^\\s\\p{L}\\p{N}]+[\\r\\n/]*",
306 "\\s*[\\r\\n]+",
307 "\\s+(?!\\S)",
308 "\\s+",
309 ].join("|")
310 }
311 Self::O200kHarmony => {
312 [
313 "[^\\r\\n\\p{L}\\p{N}]?[\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}\\p{M}]*[\\p{Ll}\\p{Lm}\\p{Lo}\\p{M}]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
314 "[^\\r\\n\\p{L}\\p{N}]?[\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}\\p{M}]+[\\p{Ll}\\p{Lm}\\p{Lo}\\p{M}]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?",
315 "\\p{N}{1,3}",
316 " ?[^\\s\\p{L}\\p{N}]+[\\r\\n/]*",
317 "\\s*[\\r\\n]+",
318 "\\s+(?!\\S)",
319 "\\s+",
320 ].join("|")
321 }
322 Self::Cl100kBase => {
323 "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+".to_string()
324 }
325 }
326 }
327}
328
329fn load_tiktoken_vocab<R>(
330 mut reader: R,
331 expected_hash: Option<&str>,
332) -> std::result::Result<HashMap<Vec<u8>, Rank>, std::io::Error>
333where
334 R: std::io::BufRead,
335{
336 let mut hasher = expected_hash.map(|_| Sha256::new());
337 let mut bpe_ranks = HashMap::new();
338 let mut lin_no = 0;
340 let mut line_buffer = String::new();
341 while reader.read_line(&mut line_buffer)? > 0 {
342 lin_no += 1;
343 if let Some(hasher) = hasher.as_mut() {
344 hasher.update(line_buffer.as_bytes());
345 }
346 let line = line_buffer.trim_end();
347 let (token, rank) = line.split_once(' ').ok_or_else(|| {
348 std::io::Error::new(
349 std::io::ErrorKind::InvalidData,
350 format!("expected token and rank, could not split on ' ' at line {lin_no}"),
351 )
352 })?;
353 let bytes = BASE64_STANDARD.decode(token).map_err(|e| {
354 std::io::Error::new(
355 std::io::ErrorKind::InvalidData,
356 format!("failed to decode base64 token at line {lin_no}: {e}",),
357 )
358 })?;
359 let rank = rank.parse().map_err(|e| {
360 std::io::Error::new(
361 std::io::ErrorKind::InvalidData,
362 format!("failed to parse rank at line {lin_no}: {e}"),
363 )
364 })?;
365 bpe_ranks.insert(bytes, rank);
366 line_buffer.clear();
367 }
368 if let Some(hasher) = hasher {
369 let expected_hash = expected_hash.unwrap();
370 let computed_hash = format!("{:x}", hasher.finalize());
371 if computed_hash != expected_hash {
372 return Err(std::io::Error::new(
373 std::io::ErrorKind::InvalidData,
374 format!("hash mismatch: computed={computed_hash}, expected={expected_hash}"),
375 ));
376 }
377 }
378 Ok(bpe_ranks)
379}
380
381pub fn load_tiktoken_vocab_file<P>(
382 path: P,
383 expected_hash: Option<&str>,
384) -> std::result::Result<HashMap<Vec<u8>, Rank>, std::io::Error>
385where
386 P: AsRef<Path>,
387{
388 let file = std::fs::File::open(path)?;
389 let reader = std::io::BufReader::new(file);
390 load_tiktoken_vocab(reader, expected_hash)
391}
392
393pub fn load_encoding_from_file<P, S, TS>(
394 file_path: P,
395 expected_hash: Option<&str>,
396 special_tokens: S,
397 pattern: &str,
398) -> Result<CoreBPE, LoadError>
399where
400 P: AsRef<Path>,
401 S: IntoIterator<Item = (TS, Rank)>,
402 TS: Into<String>,
403{
404 let encoder = load_tiktoken_vocab_file(file_path, expected_hash)
405 .map_err(LoadError::InvalidTiktokenVocabFile)?;
406 CoreBPE::new(
407 encoder,
408 special_tokens.into_iter().map(|(k, v)| (k.into(), v)),
409 pattern,
410 )
411 .map_err(LoadError::CoreBPECreationFailed)
412}
413
414#[cfg(not(target_arch = "wasm32"))]
417fn download_or_find_cached_file(
418 url: &str,
419 expected_hash: Option<&str>,
420) -> Result<PathBuf, RemoteVocabFileError> {
421 let cache_dir = resolve_cache_dir()?;
422 let cache_path = resolve_cache_path(&cache_dir, url);
423 if cache_path.exists() {
424 if verify_file_hash(&cache_path, expected_hash)? {
425 return Ok(cache_path);
426 }
427 let _ = std::fs::remove_file(&cache_path);
428 }
429 let hash = load_remote_file(url, &cache_path)?;
430 if let Some(expected_hash) = expected_hash {
431 if hash != expected_hash {
432 let _ = std::fs::remove_file(&cache_path);
433 return Err(RemoteVocabFileError::HashMismatch {
434 file_url: url.to_string(),
435 expected_hash: expected_hash.to_string(),
436 computed_hash: hash,
437 });
438 }
439 }
440 Ok(cache_path)
441}
442
443#[cfg(target_arch = "wasm32")]
444async fn download_or_find_cached_file_bytes(
445 url: &str,
446 expected_hash: Option<&str>,
447) -> Result<Vec<u8>, RemoteVocabFileError> {
448 let bytes = load_remote_file_bytes(url).await?;
449 if let Some(expected_hash) = expected_hash {
450 let computed_hash = format!("{:x}", Sha256::digest(&bytes));
451 if computed_hash != expected_hash {
452 return Err(RemoteVocabFileError::HashMismatch {
453 file_url: url.to_string(),
454 expected_hash: expected_hash.to_string(),
455 computed_hash,
456 });
457 }
458 }
459 Ok(bytes)
460}
461
462fn resolve_cache_dir() -> Result<PathBuf, RemoteVocabFileError> {
463 let cache_dir_override = std::env::var("TIKTOKEN_RS_CACHE_DIR").ok();
470 if let Some(cache_dir_override) = cache_dir_override {
471 Ok(PathBuf::from(cache_dir_override))
472 } else {
473 let cache_dir = std::env::temp_dir().join("tiktoken-rs-cache");
474 std::fs::create_dir_all(&cache_dir).map_err(|e| {
475 RemoteVocabFileError::IOError(format!("creating cache dir {cache_dir:?}"), e)
476 })?;
477 Ok(cache_dir)
478 }
479}
480
481fn resolve_cache_path(cache_dir: &Path, url: &str) -> PathBuf {
482 let mut hasher = Sha1::new();
483 hasher.update(url.as_bytes());
484 let cache_key = format!("{:x}", hasher.finalize());
485 cache_dir.join(cache_key)
486}
487
488fn verify_file_hash(
489 file_path: &Path,
490 expected_hash: Option<&str>,
491) -> Result<bool, RemoteVocabFileError> {
492 let Some(expected_hash) = expected_hash else {
493 return Ok(true);
494 };
495 let file = File::open(file_path)
496 .map_err(|e| RemoteVocabFileError::IOError(format!("opening file {file_path:?}"), e))?;
497 let mut reader = BufReader::new(file);
498 let mut hasher = Sha256::new();
499 std::io::copy(&mut reader, &mut hasher).map_err(|e| {
500 RemoteVocabFileError::IOError(format!("copying file {file_path:?} contents to hasher"), e)
501 })?;
502 let computed_hash = format!("{:x}", hasher.finalize());
503 Ok(computed_hash == expected_hash)
504}
505
506#[cfg(not(target_arch = "wasm32"))]
509fn load_remote_file(url: &str, destination: &Path) -> Result<String, RemoteVocabFileError> {
510 let client = reqwest::blocking::Client::new();
511 let mut response = client
512 .get(url)
513 .send()
514 .and_then(|r| r.error_for_status())
515 .map_err(|e| RemoteVocabFileError::FailedToDownloadOrLoadVocabFile(Box::new(e)))?;
516
517 let file = File::create(destination)
518 .map_err(|e| RemoteVocabFileError::IOError(format!("creating file {destination:?}"), e))?;
519 let mut dest = BufWriter::new(file);
520 let mut hasher = Sha256::new();
521 let mut buffer = [0u8; 8192];
522 loop {
523 let bytes_read = response.read(&mut buffer).map_err(|e| {
524 RemoteVocabFileError::IOError(format!("reading from response {url}"), e)
525 })?;
526 if bytes_read == 0 {
527 break;
528 }
529 dest.write_all(&buffer[..bytes_read]).map_err(|e| {
530 RemoteVocabFileError::IOError(format!("writing to file {destination:?}"), e)
531 })?;
532 hasher.update(&buffer[..bytes_read]);
533 }
534 Ok(format!("{:x}", hasher.finalize()))
535}
536
537#[cfg(target_arch = "wasm32")]
538fn load_remote_file(_url: &str, _destination: &Path) -> Result<String, RemoteVocabFileError> {
539 Err(RemoteVocabFileError::FailedToDownloadOrLoadVocabFile(
540 Box::new(std::io::Error::new(
541 std::io::ErrorKind::Other,
542 "Downloading files is not supported in wasm32",
543 )),
544 ))
545}
546
547#[cfg(target_arch = "wasm32")]
548async fn load_remote_file_bytes(url: &str) -> Result<Vec<u8>, RemoteVocabFileError> {
549 use reqwest::Client;
550
551 let client = Client::new();
552 let response = client
553 .get(url)
554 .send()
555 .await
556 .and_then(|r| r.error_for_status())
557 .map_err(|e| RemoteVocabFileError::FailedToDownloadOrLoadVocabFile(Box::new(e)))?;
558 let bytes = response
559 .bytes()
560 .await
561 .map_err(|e| RemoteVocabFileError::FailedToDownloadOrLoadVocabFile(Box::new(e)))?;
562 Ok(bytes.to_vec())
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn test_load_encodings() {
571 for encoding in Encoding::all() {
572 let _ = encoding.load().unwrap();
573 }
574 }
575}