tokenlib.py
Loads the shipped browser bundle under QuickJS
82 lines. This is the file the build actually runs, copied verbatim at build time.
"""Run gpt-tokenizer's browser bundle under QuickJS so the build step and the
browser agree exactly on token boundaries. QuickJS has no TextDecoder/TextEncoder,
so we polyfill both before loading the bundle."""
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
VENDOR = os.path.join(HERE, "..", "vendor")
sys.path.insert(0, os.path.join(HERE, "pylib"))
import quickjs # noqa: E402
POLYFILL = r"""
var globalThis = this;
function TextDecoder(enc){}
TextDecoder.prototype.decode = function(bytes){
var out = "", i = 0, n = bytes.length;
while (i < n) {
var c = bytes[i++];
if (c < 0x80) out += String.fromCharCode(c);
else if (c < 0xE0) out += String.fromCharCode(((c & 0x1F) << 6) | (bytes[i++] & 0x3F));
else if (c < 0xF0) out += String.fromCharCode(((c & 0x0F) << 12) | ((bytes[i++] & 0x3F) << 6) | (bytes[i++] & 0x3F));
else {
var cp = ((c & 0x07) << 18) | ((bytes[i++] & 0x3F) << 12) | ((bytes[i++] & 0x3F) << 6) | (bytes[i++] & 0x3F);
cp -= 0x10000;
out += String.fromCharCode(0xD800 + (cp >> 10), 0xDC00 + (cp & 0x3FF));
}
}
return out;
};
function TextEncoder(){}
TextEncoder.prototype.encode = function(str){
var out = [], i = 0;
while (i < str.length) {
var cp = str.codePointAt(i);
i += cp > 0xFFFF ? 2 : 1;
if (cp < 0x80) out.push(cp);
else if (cp < 0x800) out.push(0xC0 | (cp >> 6), 0x80 | (cp & 0x3F));
else if (cp < 0x10000) out.push(0xE0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F));
else out.push(0xF0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3F), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F));
}
return new Uint8Array(out);
};
"""
class Tokenizer:
def __init__(self, encoding="o200k_base"):
self.encoding = encoding
self.ctx = quickjs.Context()
self.ctx.set_memory_limit(1 << 30)
self.ctx.set_max_stack_size(1 << 22)
self.ctx.eval(POLYFILL)
path = os.path.join(VENDOR, "gpt-tokenizer", f"{encoding}.js")
with open(path, encoding="utf-8") as fh:
self.ctx.eval(fh.read())
self.ctx.eval(f"var T = globalThis.GPTTokenizer_{encoding};")
self.ctx.eval("""
function tokenize(s){
var ids = T.encode(s);
var out = [];
for (var i = 0; i < ids.length; i++) out.push([ids[i], T.decode([ids[i]])]);
return JSON.stringify(out);
}
""")
def tokens(self, text):
"""-> list of {id, text} in order."""
self.ctx.set("_input", text)
raw = self.ctx.eval("tokenize(_input)")
return [{"id": i, "text": t} for i, t in json.loads(raw)]
def count(self, text):
self.ctx.set("_input", text)
return int(self.ctx.eval("T.encode(_input).length"))
@property
def vocab_size(self):
return int(self.ctx.eval("T.vocabularySize"))