AAI Agentic Handbook
Problem 102 · Tokenization & Sampling

Byte-Fallback Detokenizer with Streaming

MediumAI engineering
01Problem

A SentencePiece-style vocab maps ids to pieces. Normal pieces use `▁` for a leading space; byte-fallback pieces look like `<0xE2>` and stand for one raw byte. Write `decode(ids, vocab)` that returns the text (dropping the single leading space the first piece introduces), and a `StreamDecoder` whose `push(id)` returns only the text that is now complete, holding back partial UTF-8 sequences, plus `flush()` for the end of stream.

02Key insight

Tokens are not characters. An emoji or a CJK character can arrive as three or four separate byte tokens, so decoding each token independently and concatenating strings prints replacement characters mid-stream. The fix is to concatenate **bytes** and decode once, or, for streaming, use an incremental UTF-8 decoder that buffers an incomplete trailing sequence. `flush()` must still surface a truncated sequence as U+FFFD instead of silently dropping it, so a cut-off generation is visible.

03Solution

Try it first.

TimeO(total bytes)
SpaceO(output) for decode; O(1) buffered bytes (at most 3) for streaming
tests.py · 8 passing
tests.pypython12 lines
vocab = {0: "▁Hello", 1: "▁wor", 2: "ld", 3: "<0xE2>", 4: "<0x82>", 5: "<0xAC>", 6: "!", 7: "<0xF0>", 8: "<0x41>"}
assert decode([0, 1, 2, 3, 4, 5, 6], vocab) == "Hello world€!"
assert decode([8, 6], vocab) == "A!"
assert decode([0, 7], vocab) == "Hello�"
s = StreamDecoder(vocab)
outs = [s.push(i) for i in [0, 1, 2, 3, 4, 5, 6]]
assert outs == ["Hello", " wor", "ld", "", "", "€", "!"], outs
assert s.flush() == ""
s2 = StreamDecoder(vocab)
assert s2.push(0) == "Hello" and s2.push(7) == ""
assert s2.flush() == "�"
assert "".join(outs) == decode([0, 1, 2, 3, 4, 5, 6], vocab)
04Staff-level follow-up

Stop sequences interact with this: you must match stop strings on decoded text, but a stop string can straddle tokens, so hold back the longest suffix that is a prefix of any stop string before emitting. Also, many tokenizers are not round-trip stable (normalisation, added prefix spaces), so never re-tokenise decoded text to count tokens for billing; keep the ids.