renderers 0.1.8.dev50__tar.gz → 0.1.8.dev51__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/PKG-INFO +1 -2
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/pyproject.toml +0 -12
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/_version.py +2 -2
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/base.py +28 -215
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_load_tokenizer.py +14 -32
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/uv.lock +1 -33
- renderers-0.1.8.dev50/tests/test_load_tokenizer_fastokens.py +0 -213
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.github/workflows/style.yml +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.github/workflows/test.yml +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.gitignore +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/LICENSE +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/README.md +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/docs/renderer-config.md +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/README.md +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/__init__.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/client.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/configs.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/default.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/glm45.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/glm5.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/llama_3.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/nemotron3.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/parsers.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/parsing.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/qwen3.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/qwen35.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/qwen36.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/conftest.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_bridge.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_client.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_incremental.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_is_content.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_llama_3.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_message_indices.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_multimodal.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_nemotron3_parity.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_parse_response.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_parsers.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_render_ids.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_renderer_config_parity.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_tool_arg_type_preservation.py +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: renderers
|
|
3
|
-
Version: 0.1.8.
|
|
3
|
+
Version: 0.1.8.dev51
|
|
4
4
|
Summary: Chat template renderers — deterministic message-to-token conversion for LLM training
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: <3.14,>=3.10
|
|
8
|
-
Requires-Dist: fastokens>=0.2.0
|
|
9
8
|
Requires-Dist: jinja2
|
|
10
9
|
Requires-Dist: numpy
|
|
11
10
|
Requires-Dist: openai-harmony>=0.0.4
|
|
@@ -31,12 +31,6 @@ dependencies = [
|
|
|
31
31
|
# against 0.0.8) and ``tests/test_gpt_oss_harmony_parity.py`` passes on it,
|
|
32
32
|
# so the older harmony is safe.
|
|
33
33
|
"openai-harmony>=0.0.4",
|
|
34
|
-
# Crusoe's Rust BPE tokenizer; ~10x faster encode vs HF's tokenizers.
|
|
35
|
-
# ``load_tokenizer`` patches it in by default for every supported model
|
|
36
|
-
# except a small denylist (DeepSeek-V3 family). The patch is bracketed
|
|
37
|
-
# around ``from_pretrained``, so subsequent ``AutoTokenizer`` calls
|
|
38
|
-
# outside the renderers package stay vanilla.
|
|
39
|
-
"fastokens>=0.2.0",
|
|
40
34
|
# ``BaseRendererConfig`` inherits from ``pydantic_config.BaseConfig`` so
|
|
41
35
|
# the typed-config surface stays uniform with prime-rl / verifiers config
|
|
42
36
|
# bases. Transitively brings pydantic, which ``renderers.configs`` also
|
|
@@ -103,12 +97,6 @@ required-version = ">=0.11.1"
|
|
|
103
97
|
exclude-newer = "7 days"
|
|
104
98
|
|
|
105
99
|
[tool.uv.exclude-newer-package]
|
|
106
|
-
# fastokens 0.2.0 was published on 2026-05-17 and contains the
|
|
107
|
-
# ``unpatch_transformers`` fix (crusoecloud/fastokens#32) needed for
|
|
108
|
-
# MiniMax-M2's slow→fast tokenizer conversion path. Exempting it from
|
|
109
|
-
# the project-wide 7-day cutoff lets the lockfile pick it up immediately
|
|
110
|
-
# while the rest of the dependency graph stays gated.
|
|
111
|
-
fastokens = false
|
|
112
100
|
# PrimeIntellect-published packages in this project's dependency closure —
|
|
113
101
|
# fast-track so first-party releases can land same-day. Only packages that
|
|
114
102
|
# appear in `uv tree` are listed.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.8.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 8, '
|
|
21
|
+
__version__ = version = '0.1.8.dev51'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 8, 'dev51')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import contextlib
|
|
4
3
|
import enum
|
|
5
|
-
import io
|
|
6
4
|
import logging
|
|
7
5
|
import queue
|
|
8
6
|
import threading
|
|
@@ -1163,29 +1161,6 @@ TOKENIZER_SOURCE_OVERRIDES: dict[str, str] = {
|
|
|
1163
1161
|
}
|
|
1164
1162
|
|
|
1165
1163
|
|
|
1166
|
-
# Models for which ``fastokens`` is known to diverge from vanilla
|
|
1167
|
-
# ``transformers.AutoTokenizer`` and therefore must NOT be patched.
|
|
1168
|
-
# Empirical audit ran each entry of ``MODEL_RENDERER_MAP`` through both
|
|
1169
|
-
# backends. The entries below fail to load under fastokens (DeepSeek-V3
|
|
1170
|
-
# family — Metaspace pretokenizer not yet implemented).
|
|
1171
|
-
FASTOKENS_INCOMPATIBLE: frozenset[str] = frozenset(
|
|
1172
|
-
{
|
|
1173
|
-
# fastokens: ``ValueError: pre-tokenizer error: unsupported
|
|
1174
|
-
# pre-tokenizer type: Metaspace`` — DeepSeek's tokenizer uses
|
|
1175
|
-
# SentencePiece-style Metaspace pretokenization which fastokens
|
|
1176
|
-
# doesn't yet implement.
|
|
1177
|
-
"deepseek-ai/DeepSeek-V3",
|
|
1178
|
-
"deepseek-ai/DeepSeek-V3-Base",
|
|
1179
|
-
"deepseek-ai/DeepSeek-R1",
|
|
1180
|
-
"deepseek-ai/DeepSeek-R1-0528",
|
|
1181
|
-
}
|
|
1182
|
-
)
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
_FASTOKENS_PATCH_LOCK = threading.Lock()
|
|
1186
|
-
_FASTOKENS_ANNOUNCED = False
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
1164
|
def _tokenizer_source_for(model_name_or_path: str) -> str:
|
|
1190
1165
|
return TOKENIZER_SOURCE_OVERRIDES.get(model_name_or_path, model_name_or_path)
|
|
1191
1166
|
|
|
@@ -1222,48 +1197,6 @@ def _preserve_requested_tokenizer_name(
|
|
|
1222
1197
|
return tokenizer
|
|
1223
1198
|
|
|
1224
1199
|
|
|
1225
|
-
def _patched_load(model_name_or_path: str, **kwargs):
|
|
1226
|
-
"""Run ``AutoTokenizer.from_pretrained`` with fastokens patched in
|
|
1227
|
-
process-locally — patch around the load, unpatch right after.
|
|
1228
|
-
|
|
1229
|
-
fastokens captures the loaded backend on a per-tokenizer basis, so
|
|
1230
|
-
after we unpatch the returned tokenizer object continues to use
|
|
1231
|
-
fastokens for ``encode``/``decode`` while subsequent
|
|
1232
|
-
``AutoTokenizer.from_pretrained`` calls (outside our control) go
|
|
1233
|
-
back to vanilla. This keeps the global side effect minimal.
|
|
1234
|
-
|
|
1235
|
-
fastokens itself prints ``[fastokens] patch_transformers: ...`` to
|
|
1236
|
-
stdout on every patch/unpatch call. Building a pool of size N would
|
|
1237
|
-
therefore emit ~N lines (more under thread contention, where some
|
|
1238
|
-
threads see ``already patched``). We swallow those prints under a
|
|
1239
|
-
lock — ``contextlib.redirect_stdout`` swaps ``sys.stdout``
|
|
1240
|
-
process-wide, so the lock keeps unrelated stdout writes from other
|
|
1241
|
-
threads from disappearing into our buffer. The patch/unpatch calls
|
|
1242
|
-
are cheap; only the brief patch+unpatch is serialized, the actual
|
|
1243
|
-
``from_pretrained`` still runs concurrently across pool slots. A
|
|
1244
|
-
single ``logger.info`` is emitted on the first patch so the fast
|
|
1245
|
-
path is still discoverable in logs.
|
|
1246
|
-
"""
|
|
1247
|
-
import fastokens
|
|
1248
|
-
|
|
1249
|
-
global _FASTOKENS_ANNOUNCED
|
|
1250
|
-
|
|
1251
|
-
with _FASTOKENS_PATCH_LOCK:
|
|
1252
|
-
with contextlib.redirect_stdout(io.StringIO()):
|
|
1253
|
-
fastokens.patch_transformers()
|
|
1254
|
-
if not _FASTOKENS_ANNOUNCED:
|
|
1255
|
-
logger.info(
|
|
1256
|
-
"fastokens enabled — tokenizers load through the Rust BPE fast path (~10x encode speedup)."
|
|
1257
|
-
)
|
|
1258
|
-
_FASTOKENS_ANNOUNCED = True
|
|
1259
|
-
try:
|
|
1260
|
-
return _load_tokenizer_via_auto(model_name_or_path, **kwargs)
|
|
1261
|
-
finally:
|
|
1262
|
-
with _FASTOKENS_PATCH_LOCK:
|
|
1263
|
-
with contextlib.redirect_stdout(io.StringIO()):
|
|
1264
|
-
fastokens.unpatch_transformers()
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
1200
|
def _load_fast_tokenizer_directly(
|
|
1268
1201
|
model_name_or_path: str, revision: str | None
|
|
1269
1202
|
) -> Any | None:
|
|
@@ -1323,36 +1256,14 @@ def _load_tokenizer_via_auto(model_name_or_path: str, **kwargs) -> Any:
|
|
|
1323
1256
|
return tok
|
|
1324
1257
|
|
|
1325
1258
|
|
|
1326
|
-
def load_tokenizer(
|
|
1327
|
-
|
|
1328
|
-
*,
|
|
1329
|
-
use_fastokens: bool = True,
|
|
1330
|
-
):
|
|
1331
|
-
"""Load a tokenizer with the renderers-package security + perf policy.
|
|
1259
|
+
def load_tokenizer(model_name_or_path: str):
|
|
1260
|
+
"""Load a tokenizer with the renderers-package security policy.
|
|
1332
1261
|
|
|
1333
|
-
|
|
1262
|
+
Default ``trust_remote_code=False``. Models listed in
|
|
1334
1263
|
``TRUSTED_REVISIONS`` (Moonshot Kimi-K2 family) load with
|
|
1335
1264
|
``trust_remote_code=True`` AND a pinned ``revision=<sha>`` so
|
|
1336
1265
|
transformers only executes the reviewed commit's tokenizer Python.
|
|
1337
1266
|
|
|
1338
|
-
**Performance** — ``use_fastokens=True`` (default) routes the load
|
|
1339
|
-
through ``fastokens.patch_transformers()`` so the resulting tokenizer
|
|
1340
|
-
encodes ~10x faster than vanilla ``tokenizers``. The patch is
|
|
1341
|
-
bracketed: it's applied before ``from_pretrained`` and removed
|
|
1342
|
-
immediately after, so global ``AutoTokenizer.from_pretrained`` calls
|
|
1343
|
-
elsewhere in the user's process are not affected.
|
|
1344
|
-
|
|
1345
|
-
Models in ``FASTOKENS_INCOMPATIBLE`` (DeepSeek-V3 family) skip the
|
|
1346
|
-
patch — fastokens currently fails to load them. Pass
|
|
1347
|
-
``use_fastokens=False`` to force the vanilla backend for any other
|
|
1348
|
-
model.
|
|
1349
|
-
|
|
1350
|
-
Unknown / fine-tuned model paths fall through to
|
|
1351
|
-
``trust_remote_code=False`` and the patched-load fast path. If
|
|
1352
|
-
fastokens raises during the patched load (e.g. an unknown
|
|
1353
|
-
pre-tokenizer type), we automatically retry with the vanilla
|
|
1354
|
-
backend and emit an INFO log.
|
|
1355
|
-
|
|
1356
1267
|
``AutoTokenizer.from_pretrained`` eagerly builds the model config to
|
|
1357
1268
|
resolve the tokenizer class. If that construction raises on a
|
|
1358
1269
|
modeling-only concern the tokenizer doesn't need (e.g. RoPE
|
|
@@ -1367,28 +1278,7 @@ def load_tokenizer(
|
|
|
1367
1278
|
"""
|
|
1368
1279
|
load_name_or_path = _tokenizer_source_for(model_name_or_path)
|
|
1369
1280
|
kwargs = _tokenizer_load_kwargs(load_name_or_path)
|
|
1370
|
-
|
|
1371
|
-
if not use_fastokens or load_name_or_path in FASTOKENS_INCOMPATIBLE:
|
|
1372
|
-
tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
|
|
1373
|
-
return _preserve_requested_tokenizer_name(
|
|
1374
|
-
tok,
|
|
1375
|
-
requested_name_or_path=model_name_or_path,
|
|
1376
|
-
loaded_name_or_path=load_name_or_path,
|
|
1377
|
-
)
|
|
1378
|
-
|
|
1379
|
-
try:
|
|
1380
|
-
tok = _patched_load(load_name_or_path, **kwargs)
|
|
1381
|
-
except Exception as exc:
|
|
1382
|
-
logger.info(
|
|
1383
|
-
"fastokens could not load %r (%s: %s); falling back to vanilla "
|
|
1384
|
-
"AutoTokenizer. Add this model to FASTOKENS_INCOMPATIBLE in "
|
|
1385
|
-
"renderers.base to suppress the retry.",
|
|
1386
|
-
load_name_or_path,
|
|
1387
|
-
type(exc).__name__,
|
|
1388
|
-
str(exc)[:160],
|
|
1389
|
-
)
|
|
1390
|
-
tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
|
|
1391
|
-
|
|
1281
|
+
tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
|
|
1392
1282
|
return _preserve_requested_tokenizer_name(
|
|
1393
1283
|
tok,
|
|
1394
1284
|
requested_name_or_path=model_name_or_path,
|
|
@@ -1718,104 +1608,28 @@ def trim_to_turn_close(
|
|
|
1718
1608
|
return previous_ids
|
|
1719
1609
|
|
|
1720
1610
|
|
|
1721
|
-
# Per-model offset-aware tokenizer cache. ``attribute_text_segments``
|
|
1722
|
-
# uses the fast HuggingFace tokenizer's ``offset_mapping`` to attribute
|
|
1723
|
-
# each token to its source text segment under one BPE pass. Fastokens
|
|
1724
|
-
# (the Rust BPE we patch in by default for ~10x faster encode) does not
|
|
1725
|
-
# track character offsets — the patched tokenizer's
|
|
1726
|
-
# ``return_offsets_mapping=True`` raises ``NotImplementedError``. So we
|
|
1727
|
-
# keep a parallel vanilla tokenizer per model purely for offset queries.
|
|
1728
|
-
# Memory cost is one extra tokenizer per *unique* model name across all
|
|
1729
|
-
# pools / renderers (the cache is process-global), independent of pool
|
|
1730
|
-
# size.
|
|
1731
|
-
_offset_tokenizers: dict[str, Any] = {}
|
|
1732
|
-
_offset_tokenizers_lock = threading.Lock()
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
1611
|
def _get_offset_tokenizer(tokenizer):
|
|
1736
|
-
"""
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1612
|
+
"""Assert ``tokenizer`` supports ``return_offsets_mapping=True``.
|
|
1613
|
+
|
|
1614
|
+
Hand-coded renderers concatenate scaffold + body in one BPE pass to
|
|
1615
|
+
preserve cross-boundary merges, then attribute each resulting token
|
|
1616
|
+
back to its source segment via the fast tokenizer's
|
|
1617
|
+
``offset_mapping`` (see :func:`attribute_text_segments`). The
|
|
1618
|
+
contract: every BYO tokenizer must be a fast tokenizer with offset
|
|
1619
|
+
support. Tokenizers loaded via :func:`load_tokenizer` are
|
|
1620
|
+
``PreTrainedTokenizerFast`` instances that satisfy this trivially.
|
|
1743
1621
|
"""
|
|
1744
|
-
# Cheap probe: does this tokenizer already provide offsets?
|
|
1745
1622
|
try:
|
|
1746
1623
|
tokenizer("a", add_special_tokens=False, return_offsets_mapping=True)
|
|
1747
|
-
|
|
1748
|
-
except (NotImplementedError, ValueError, TypeError):
|
|
1749
|
-
pass
|
|
1750
|
-
|
|
1751
|
-
name_or_path = getattr(tokenizer, "name_or_path", "")
|
|
1752
|
-
if not name_or_path:
|
|
1624
|
+
except (NotImplementedError, ValueError, TypeError) as exc:
|
|
1753
1625
|
raise RuntimeError(
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1756
|
-
"tokenizer loaded via
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
if cached is not None:
|
|
1762
|
-
return cached
|
|
1763
|
-
|
|
1764
|
-
load_name_or_path = _tokenizer_source_for(name_or_path)
|
|
1765
|
-
kwargs = _tokenizer_load_kwargs(load_name_or_path)
|
|
1766
|
-
|
|
1767
|
-
def _has_offsets(tok) -> bool:
|
|
1768
|
-
if not getattr(tok, "is_fast", False):
|
|
1769
|
-
return False
|
|
1770
|
-
try:
|
|
1771
|
-
tok("a", add_special_tokens=False, return_offsets_mapping=True)
|
|
1772
|
-
return True
|
|
1773
|
-
except (NotImplementedError, ValueError, TypeError):
|
|
1774
|
-
return False
|
|
1775
|
-
|
|
1776
|
-
# We want HF's Rust tokenizer with offset tracking, not the fastokens
|
|
1777
|
-
# shim. The shim is installed by a *process-global* monkeypatch that
|
|
1778
|
-
# ``load_tokenizer`` toggles per pool-slot load, so a plain reload here
|
|
1779
|
-
# can race a concurrent slot's open patch window and silently pick up
|
|
1780
|
-
# the offset-less shim (then get cached, poisoning the process). So:
|
|
1781
|
-
# load, verify offsets, and if missing, reload with the patch forced
|
|
1782
|
-
# off — serialized against pool patch/unpatch via ``_FASTOKENS_PATCH_LOCK``
|
|
1783
|
-
# so no concurrent window can swap the shim back in mid-load — then
|
|
1784
|
-
# restore the prior patch state. Never cache a non-offset tokenizer.
|
|
1785
|
-
offset_tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
|
|
1786
|
-
offset_tok = _preserve_requested_tokenizer_name(
|
|
1787
|
-
offset_tok,
|
|
1788
|
-
requested_name_or_path=name_or_path,
|
|
1789
|
-
loaded_name_or_path=load_name_or_path,
|
|
1790
|
-
)
|
|
1791
|
-
if not _has_offsets(offset_tok):
|
|
1792
|
-
import fastokens
|
|
1793
|
-
|
|
1794
|
-
with _FASTOKENS_PATCH_LOCK:
|
|
1795
|
-
was_patched = bool(getattr(fastokens, "_patched", False))
|
|
1796
|
-
if was_patched:
|
|
1797
|
-
with contextlib.redirect_stdout(io.StringIO()):
|
|
1798
|
-
fastokens.unpatch_transformers()
|
|
1799
|
-
try:
|
|
1800
|
-
offset_tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
|
|
1801
|
-
offset_tok = _preserve_requested_tokenizer_name(
|
|
1802
|
-
offset_tok,
|
|
1803
|
-
requested_name_or_path=name_or_path,
|
|
1804
|
-
loaded_name_or_path=load_name_or_path,
|
|
1805
|
-
)
|
|
1806
|
-
finally:
|
|
1807
|
-
if was_patched:
|
|
1808
|
-
with contextlib.redirect_stdout(io.StringIO()):
|
|
1809
|
-
fastokens.patch_transformers()
|
|
1810
|
-
if not _has_offsets(offset_tok):
|
|
1811
|
-
raise RuntimeError(
|
|
1812
|
-
f"Could not load an offset-capable tokenizer for {name_or_path!r}: "
|
|
1813
|
-
"offset_mapping is unavailable even with the fastokens patch off. "
|
|
1814
|
-
"Hand-coded renderers require a fast tokenizer for body/scaffold "
|
|
1815
|
-
"attribution."
|
|
1816
|
-
)
|
|
1817
|
-
_offset_tokenizers[name_or_path] = offset_tok
|
|
1818
|
-
return offset_tok
|
|
1626
|
+
"Hand-coded renderers require a fast tokenizer with "
|
|
1627
|
+
"``return_offsets_mapping=True`` support for body/scaffold "
|
|
1628
|
+
"attribution. Pass a tokenizer loaded via "
|
|
1629
|
+
"``renderers.base.load_tokenizer``, or any "
|
|
1630
|
+
"``transformers.PreTrainedTokenizerFast`` instance."
|
|
1631
|
+
) from exc
|
|
1632
|
+
return tokenizer
|
|
1819
1633
|
|
|
1820
1634
|
|
|
1821
1635
|
def attribute_text_segments(
|
|
@@ -1839,14 +1653,13 @@ def attribute_text_segments(
|
|
|
1839
1653
|
tokens (rare; usually pre-tokenizer artefacts) are attributed to
|
|
1840
1654
|
the most recently entered segment.
|
|
1841
1655
|
|
|
1842
|
-
Requires a HuggingFace fast tokenizer with offset tracking.
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
wrap/body boundary would defeat the whole point.
|
|
1656
|
+
Requires a HuggingFace fast tokenizer with offset tracking. Every
|
|
1657
|
+
model in ``MODEL_RENDERER_MAP`` ships one, so the offset lookup
|
|
1658
|
+
always succeeds for tokenizers obtained via :func:`load_tokenizer`.
|
|
1659
|
+
BYO tokenizers must be a ``PreTrainedTokenizerFast`` (or anything
|
|
1660
|
+
else exposing ``return_offsets_mapping=True``); slow tokenizers
|
|
1661
|
+
aren't supported — BPE drift at the wrap/body boundary would
|
|
1662
|
+
defeat the whole point.
|
|
1850
1663
|
|
|
1851
1664
|
Empty input or empty joined text returns an empty list.
|
|
1852
1665
|
"""
|
|
@@ -12,6 +12,8 @@ import re
|
|
|
12
12
|
from types import SimpleNamespace
|
|
13
13
|
from unittest.mock import patch
|
|
14
14
|
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
15
17
|
from renderers import base
|
|
16
18
|
from renderers.base import TOKENIZER_SOURCE_OVERRIDES, TRUSTED_REVISIONS, load_tokenizer
|
|
17
19
|
|
|
@@ -81,7 +83,7 @@ def test_meta_llama_loads_tokenizer_from_unsloth_mirror(mock_from_pretrained):
|
|
|
81
83
|
mirror = "unsloth/Llama-3.2-1B-Instruct"
|
|
82
84
|
mock_from_pretrained.return_value = SimpleNamespace(name_or_path=mirror)
|
|
83
85
|
|
|
84
|
-
tok = load_tokenizer(canonical
|
|
86
|
+
tok = load_tokenizer(canonical)
|
|
85
87
|
|
|
86
88
|
args, kwargs = mock_from_pretrained.call_args
|
|
87
89
|
assert args == (mirror,)
|
|
@@ -120,42 +122,22 @@ def test_tokenizer_source_overrides_are_exact_llama_mirrors():
|
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
|
|
123
|
-
def
|
|
124
|
-
"""
|
|
125
|
-
|
|
126
|
-
the
|
|
125
|
+
def test_get_offset_tokenizer_rejects_offsetless_byo():
|
|
126
|
+
"""BYO tokenizers without ``return_offsets_mapping`` support raise a
|
|
127
|
+
clear error. Hand-coded renderers concatenate scaffold + body in one
|
|
128
|
+
BPE pass and attribute tokens via the fast tokenizer's offset map;
|
|
129
|
+
no transparent reload-from-name_or_path fallback exists. The
|
|
130
|
+
contract is: pass a fast tokenizer or get a loud error at construct
|
|
131
|
+
time, not silent BPE drift at the wrap/body boundary."""
|
|
127
132
|
|
|
128
133
|
class _NoOffsets:
|
|
129
|
-
name_or_path = "
|
|
130
|
-
|
|
131
|
-
def __call__(self, *args, **kwargs):
|
|
132
|
-
raise NotImplementedError("fastokens shim has no offsets")
|
|
133
|
-
|
|
134
|
-
class _OffsetTokenizer:
|
|
135
|
-
is_fast = True
|
|
136
|
-
|
|
137
|
-
def __init__(self, name_or_path: str):
|
|
138
|
-
self.name_or_path = name_or_path
|
|
134
|
+
name_or_path = "anywhere/anything"
|
|
139
135
|
|
|
140
136
|
def __call__(self, *args, **kwargs):
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
calls = []
|
|
144
|
-
|
|
145
|
-
def _fake_load(name_or_path, **kwargs):
|
|
146
|
-
calls.append((name_or_path, kwargs))
|
|
147
|
-
return _OffsetTokenizer(name_or_path)
|
|
148
|
-
|
|
149
|
-
base._offset_tokenizers.clear()
|
|
150
|
-
monkeypatch.setattr(base, "_load_tokenizer_via_auto", _fake_load)
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
tok = base._get_offset_tokenizer(_NoOffsets())
|
|
154
|
-
finally:
|
|
155
|
-
base._offset_tokenizers.clear()
|
|
137
|
+
raise NotImplementedError("BYO tokenizer has no offsets")
|
|
156
138
|
|
|
157
|
-
|
|
158
|
-
|
|
139
|
+
with pytest.raises(RuntimeError, match="fast tokenizer.*offsets"):
|
|
140
|
+
base._get_offset_tokenizer(_NoOffsets())
|
|
159
141
|
|
|
160
142
|
|
|
161
143
|
# ---------------------------------------------------------------------------
|
|
@@ -9,12 +9,11 @@ resolution-markers = [
|
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
[options]
|
|
12
|
-
exclude-newer = "2026-06-19T02:
|
|
12
|
+
exclude-newer = "2026-06-19T02:36:32.208558271Z"
|
|
13
13
|
exclude-newer-span = "P7D"
|
|
14
14
|
|
|
15
15
|
[options.exclude-newer-package]
|
|
16
16
|
prime-pydantic-config = false
|
|
17
|
-
fastokens = false
|
|
18
17
|
|
|
19
18
|
[[package]]
|
|
20
19
|
name = "annotated-doc"
|
|
@@ -268,35 +267,6 @@ wheels = [
|
|
|
268
267
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
|
269
268
|
]
|
|
270
269
|
|
|
271
|
-
[[package]]
|
|
272
|
-
name = "fastokens"
|
|
273
|
-
version = "0.2.0"
|
|
274
|
-
source = { registry = "https://pypi.org/simple" }
|
|
275
|
-
sdist = { url = "https://files.pythonhosted.org/packages/14/8e/7e88ec1d48db5a6e8d8d44318ce285e38c04b81508bdc2a60e17045a116f/fastokens-0.2.0.tar.gz", hash = "sha256:ef0e175de5c8cb1b616b3210d75dce1fab78e35fc02f77f03f7847d4678be686", size = 675822, upload-time = "2026-05-17T10:32:55.642Z" }
|
|
276
|
-
wheels = [
|
|
277
|
-
{ url = "https://files.pythonhosted.org/packages/e8/14/3d640cbe3c866ee6c113ea4ca37c16c5aa44be6412918928bbd3f3b739ef/fastokens-0.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:e7db86c2785a502e3cd993c7d3a91c46e2751f94d1f446caa9600e9cf3dafbd1", size = 3078350, upload-time = "2026-05-17T10:32:41.313Z" },
|
|
278
|
-
{ url = "https://files.pythonhosted.org/packages/bc/bf/33ca3798842fb8bdacf03a97c874454d50fe328d44e0d3c3ba7a633fd3ab/fastokens-0.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa8dcbc6ad3f7a7e9f5bf1ef3cfc9f0c04f0c8779be9e38864815dc567b27de4", size = 2983397, upload-time = "2026-05-17T10:32:38.823Z" },
|
|
279
|
-
{ url = "https://files.pythonhosted.org/packages/c4/0a/1bfd13fb855bce3ce50faa644d6e0e19343035706c2628e17da1247bbf50/fastokens-0.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b399600ae5beaa34e45afd05c6835b8569861955dfdc3e05afba25fe414e5a", size = 3309835, upload-time = "2026-05-17T10:32:33.427Z" },
|
|
280
|
-
{ url = "https://files.pythonhosted.org/packages/32/ce/33292977a81011ffc59cee1c20b6f8d0dbd9cb39209c327dc74dc542178d/fastokens-0.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f363b1e89fe4ed979224c326b25b33b28e1172134f3e7959238276170f12328e", size = 3615173, upload-time = "2026-05-17T10:32:30.898Z" },
|
|
281
|
-
{ url = "https://files.pythonhosted.org/packages/07/1c/1bef8b4831bf9220ae182fcee3e250273b7f537c81aec96041f6eb4bc990/fastokens-0.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d79358faa11658d8908fbdfab321b6ae6a0ff52ae3cdce7a0fda5c30629a276", size = 3295485, upload-time = "2026-05-17T10:32:36.109Z" },
|
|
282
|
-
{ url = "https://files.pythonhosted.org/packages/92/11/49afb739800b6a82af04fa1e991b68669c789ebeaa3bcbc96c2544c8ff9c/fastokens-0.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cf6ae284b70d65989548a8143e1f463f31126f5650ad45ce6af05b83e4afb3e7", size = 3329524, upload-time = "2026-05-17T10:32:44.607Z" },
|
|
283
|
-
{ url = "https://files.pythonhosted.org/packages/f0/d2/f9b1c01535a0ce994d30e86cf49616023246134d2a406b74c5dd49df7825/fastokens-0.2.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:40fa091ff12aa8ac00fa8e2133c7256b96209827b88de5120eb1361600d7f68f", size = 3149194, upload-time = "2026-05-17T10:32:47.596Z" },
|
|
284
|
-
{ url = "https://files.pythonhosted.org/packages/bd/0b/95b2e5d25efc684988918658c90445e0e5f1dc4fee7dae5edd586b4feaba/fastokens-0.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f643f4739b20c86ed21e2c46e7fe203e6a621e21124074f138819423882e3aba", size = 3401011, upload-time = "2026-05-17T10:32:50.13Z" },
|
|
285
|
-
{ url = "https://files.pythonhosted.org/packages/9b/31/80b74196524d7a9576fa96685a39ee1f333ffd12eb29dbeb5e65c6e3dcb2/fastokens-0.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3cd4effdc6610cbee0be717032652a1997976f1a2f21949435d925c7982cf6f3", size = 3593240, upload-time = "2026-05-17T10:32:52.661Z" },
|
|
286
|
-
{ url = "https://files.pythonhosted.org/packages/59/96/12814aa955b7277adb07a8b36176181f247f3b9cf973d05b34294df6a72c/fastokens-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d29eb1d608977d63fc4679d6ce2b360fdb8b0ea8d66a0eaf804f7ac31ba52a3a", size = 3089011, upload-time = "2026-05-17T10:32:43.16Z" },
|
|
287
|
-
{ url = "https://files.pythonhosted.org/packages/c0/05/553b59c6a8542ad7d443306fc1fd7bb012e4aae459ccf9ab422ea4c681b7/fastokens-0.2.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:f8ec7a20322aafe245201727c7a507b87333c51dc580940b9ee5456dbfe3963c", size = 2992964, upload-time = "2026-05-17T10:32:39.992Z" },
|
|
288
|
-
{ url = "https://files.pythonhosted.org/packages/13/66/a53c2309003510de7c189ba8a9d2ea5a1833e9b447d9f69b95449c3dfe34/fastokens-0.2.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc4c0a25726620b1cc4cfb7e9dc1a53b17bbe3f2f9adbaa57b8375b5f36ac5d4", size = 3316289, upload-time = "2026-05-17T10:32:34.557Z" },
|
|
289
|
-
{ url = "https://files.pythonhosted.org/packages/b4/54/e0e4318ee1ad0b5196df72cf93615bba0b81f7869d659a44ccc475969151/fastokens-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:160253f8d30747cf66e7ed895c513e16f7b173dd9e644fa641e2eecbd43a616a", size = 3303534, upload-time = "2026-05-17T10:32:37.462Z" },
|
|
290
|
-
{ url = "https://files.pythonhosted.org/packages/64/44/bfff90e4b1a43c17edf7305dafbd56dc992bbe832cc08da78f1f50104c2d/fastokens-0.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b61b9fe5b41e0bb36ad86e7551dc53293c9833909ef07b1cdbaa2055b06c3b3e", size = 3254096, upload-time = "2026-05-17T10:32:28.489Z" },
|
|
291
|
-
{ url = "https://files.pythonhosted.org/packages/df/89/bec1a0368100c5f1134ab1ce588d71f88697fadbbad5f26bb130eda00fe8/fastokens-0.2.0-cp39-abi3-manylinux_2_28_armv7l.whl", hash = "sha256:8f138fc64e355589be43068e6996ac0bfcfd872cd75fb5793d11d06cb9c0ac9b", size = 3000177, upload-time = "2026-05-17T10:32:29.745Z" },
|
|
292
|
-
{ url = "https://files.pythonhosted.org/packages/f2/fd/cbe8a033e2ef565ce5aef4e02316054b04adad96ee9805cba74a50a84ed9/fastokens-0.2.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:c1a6cd93d16e880d81deb12519cc2eb20149d7423321cf00a34ce25ea9c9ef15", size = 3626561, upload-time = "2026-05-17T10:32:32.087Z" },
|
|
293
|
-
{ url = "https://files.pythonhosted.org/packages/05/bf/1cad7f0e8d03f5f5b2b417cda8859e4d968d2eebdca0cd336b23d7dbbdbb/fastokens-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:01b9bdba818d7b2c67d57d9917faf7a1dad32ece0734440130de94ad768b819f", size = 3336689, upload-time = "2026-05-17T10:32:46.21Z" },
|
|
294
|
-
{ url = "https://files.pythonhosted.org/packages/05/56/0030c6e67c60ee88b74336d1bb03eb556b2afa60445268921ad02d9cb3b3/fastokens-0.2.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9aec70b85a06bf2ac95dd76e07e404040dbcfb06eb165f29bc61ef2ab68dc87a", size = 3154666, upload-time = "2026-05-17T10:32:48.835Z" },
|
|
295
|
-
{ url = "https://files.pythonhosted.org/packages/c1/5b/02cc5d501ccc2fbd4b068a7aa34a35347bdb3a4189b96af647ef0407af87/fastokens-0.2.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:8c41f5278ac53853249c1170d653ee0afaa0906c9bc32c8d25c9443b6d667298", size = 3406657, upload-time = "2026-05-17T10:32:51.392Z" },
|
|
296
|
-
{ url = "https://files.pythonhosted.org/packages/97/d7/f5fb2564e16b1f5733e05c41b090f95a3fe767f6b888ba7d864193bc5447/fastokens-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d068bc50082ad67d5d542847075f1f7b8d10f703274e56e241312f18b4d9e772", size = 3598064, upload-time = "2026-05-17T10:32:54.109Z" },
|
|
297
|
-
{ url = "https://files.pythonhosted.org/packages/0a/39/2098de2aa01c3e2ef62b066926d70015f88845e3ac1a976ba1cc3a363c05/fastokens-0.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:5729c44ce1d60cb03e506731dcb17d8c2d69c267098c3ff35ca8be37d618714d", size = 2767905, upload-time = "2026-05-17T10:32:56.759Z" },
|
|
298
|
-
]
|
|
299
|
-
|
|
300
270
|
[[package]]
|
|
301
271
|
name = "filelock"
|
|
302
272
|
version = "3.29.0"
|
|
@@ -1380,7 +1350,6 @@ wheels = [
|
|
|
1380
1350
|
name = "renderers"
|
|
1381
1351
|
source = { editable = "." }
|
|
1382
1352
|
dependencies = [
|
|
1383
|
-
{ name = "fastokens" },
|
|
1384
1353
|
{ name = "jinja2" },
|
|
1385
1354
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
1386
1355
|
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
@@ -1405,7 +1374,6 @@ dev = [
|
|
|
1405
1374
|
|
|
1406
1375
|
[package.metadata]
|
|
1407
1376
|
requires-dist = [
|
|
1408
|
-
{ name = "fastokens", specifier = ">=0.2.0" },
|
|
1409
1377
|
{ name = "jinja2" },
|
|
1410
1378
|
{ name = "numpy" },
|
|
1411
1379
|
{ name = "openai", specifier = ">=1.108.1" },
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
"""Coverage for the fastokens fast-path in ``renderers.base.load_tokenizer``.
|
|
2
|
-
|
|
3
|
-
``load_tokenizer`` defaults to routing every supported model through
|
|
4
|
-
``fastokens.patch_transformers()`` for ~10x faster encode. Models in
|
|
5
|
-
``FASTOKENS_INCOMPATIBLE`` skip the patch (DeepSeek's Metaspace
|
|
6
|
-
pretokenizer isn't supported). Callers can opt out per-call with
|
|
7
|
-
``use_fastokens=False``.
|
|
8
|
-
|
|
9
|
-
These tests pin the policy:
|
|
10
|
-
|
|
11
|
-
1. The denylist contains the empirically-verified incompat models —
|
|
12
|
-
adding to it should be a deliberate review action.
|
|
13
|
-
2. With ``use_fastokens=True`` (the default) on a compatible model, the
|
|
14
|
-
resulting tokenizer's backend is the fastokens shim. Encode output
|
|
15
|
-
stays byte-identical to vanilla.
|
|
16
|
-
3. With ``use_fastokens=False``, the resulting tokenizer is vanilla.
|
|
17
|
-
4. For incompat models, the fast path is silently skipped and the
|
|
18
|
-
tokenizer still loads + encodes correctly.
|
|
19
|
-
5. The fastokens patch is removed immediately after the load so it
|
|
20
|
-
doesn't leak into the caller's process — subsequent
|
|
21
|
-
``AutoTokenizer.from_pretrained`` calls outside ``load_tokenizer``
|
|
22
|
-
use vanilla.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
from __future__ import annotations
|
|
26
|
-
|
|
27
|
-
import pytest
|
|
28
|
-
from transformers import AutoTokenizer
|
|
29
|
-
|
|
30
|
-
from renderers.base import (
|
|
31
|
-
FASTOKENS_INCOMPATIBLE,
|
|
32
|
-
load_tokenizer,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# ---------------------------------------------------------------------------
|
|
37
|
-
# Denylist shape
|
|
38
|
-
# ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_fastokens_incompatible_is_explicit_set():
|
|
42
|
-
"""The denylist is small and audited — pinning the exact contents
|
|
43
|
-
catches accidental drift. Adding/removing entries should be a
|
|
44
|
-
deliberate action with a parity probe."""
|
|
45
|
-
assert FASTOKENS_INCOMPATIBLE == frozenset(
|
|
46
|
-
{
|
|
47
|
-
"deepseek-ai/DeepSeek-V3",
|
|
48
|
-
"deepseek-ai/DeepSeek-V3-Base",
|
|
49
|
-
"deepseek-ai/DeepSeek-R1",
|
|
50
|
-
"deepseek-ai/DeepSeek-R1-0528",
|
|
51
|
-
}
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# ---------------------------------------------------------------------------
|
|
56
|
-
# Fast path (compatible model — Qwen3.5-9B as representative)
|
|
57
|
-
# ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
_FAST_MODEL = "Qwen/Qwen3.5-9B"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _backend_class_name(tok) -> str:
|
|
64
|
-
"""Return the class name of the underlying backend object so tests
|
|
65
|
-
can tell vanilla from fastokens-shimmed tokenizers."""
|
|
66
|
-
backend = getattr(tok, "_tokenizer", None)
|
|
67
|
-
return type(backend).__name__ if backend is not None else type(tok).__name__
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_default_uses_fastokens_on_compatible_model():
|
|
71
|
-
tok = load_tokenizer(_FAST_MODEL)
|
|
72
|
-
# The shim type is named ``_TokenizerShim`` (see fastokens._compat);
|
|
73
|
-
# match by name so we don't import private fastokens internals.
|
|
74
|
-
assert "Shim" in _backend_class_name(tok), (
|
|
75
|
-
f"Expected fastokens shim backend, got {_backend_class_name(tok)!r}"
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_explicit_off_returns_vanilla_backend():
|
|
80
|
-
tok = load_tokenizer(_FAST_MODEL, use_fastokens=False)
|
|
81
|
-
assert "Shim" not in _backend_class_name(tok), (
|
|
82
|
-
f"Expected vanilla backend, got {_backend_class_name(tok)!r}"
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def test_fast_and_vanilla_encode_identically_on_compatible_model():
|
|
87
|
-
fast = load_tokenizer(_FAST_MODEL)
|
|
88
|
-
vanilla = load_tokenizer(_FAST_MODEL, use_fastokens=False)
|
|
89
|
-
samples = [
|
|
90
|
-
"Hello, world!",
|
|
91
|
-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
|
92
|
-
"🌍 emoji + 中文 + tabs\there",
|
|
93
|
-
" ".join([f"word_{i}" for i in range(50)]),
|
|
94
|
-
]
|
|
95
|
-
for s in samples:
|
|
96
|
-
assert fast.encode(s, add_special_tokens=False) == vanilla.encode(
|
|
97
|
-
s, add_special_tokens=False
|
|
98
|
-
), f"encode diverged on {s!r}"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# ---------------------------------------------------------------------------
|
|
102
|
-
# Denylist: incompat models silently skip the patch and still load.
|
|
103
|
-
# ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@pytest.mark.parametrize("model", sorted(FASTOKENS_INCOMPATIBLE))
|
|
107
|
-
def test_incompat_model_loads_via_vanilla_backend(model):
|
|
108
|
-
"""For models we know diverge / fail under fastokens, the fast path
|
|
109
|
-
must be skipped so the load still succeeds with a vanilla backend."""
|
|
110
|
-
if "DeepSeek" in model:
|
|
111
|
-
# Skip if upstream gating / size makes the load impractical here.
|
|
112
|
-
# We only care that the path doesn't try fastokens. Probe the
|
|
113
|
-
# tokenizer_config to make sure the repo is reachable; if not,
|
|
114
|
-
# skip rather than fail (CI without HF auth, network issues).
|
|
115
|
-
from huggingface_hub import HfApi
|
|
116
|
-
|
|
117
|
-
try:
|
|
118
|
-
HfApi().repo_info(model)
|
|
119
|
-
except Exception as e:
|
|
120
|
-
pytest.skip(f"{model}: repo unreachable in this env ({e})")
|
|
121
|
-
tok = load_tokenizer(model)
|
|
122
|
-
assert "Shim" not in _backend_class_name(tok), (
|
|
123
|
-
f"{model}: should NOT have been patched; got {_backend_class_name(tok)!r}"
|
|
124
|
-
)
|
|
125
|
-
# And it still encodes.
|
|
126
|
-
ids = tok.encode("hello", add_special_tokens=False)
|
|
127
|
-
assert len(ids) > 0
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# ---------------------------------------------------------------------------
|
|
131
|
-
# Patch must not leak: AutoTokenizer.from_pretrained calls OUTSIDE
|
|
132
|
-
# load_tokenizer should still produce a vanilla tokenizer.
|
|
133
|
-
# ---------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def test_patch_is_unloaded_after_call():
|
|
137
|
-
"""``load_tokenizer`` brackets the fastokens patch. After it returns
|
|
138
|
-
a fastokens-shimmed tokenizer, a fresh ``AutoTokenizer.from_pretrained``
|
|
139
|
-
call must NOT pick up the patch — the user's process stays clean."""
|
|
140
|
-
fast = load_tokenizer(_FAST_MODEL)
|
|
141
|
-
assert "Shim" in _backend_class_name(fast), "preconditions: fast path active"
|
|
142
|
-
|
|
143
|
-
# Now call AutoTokenizer.from_pretrained directly. It MUST be vanilla.
|
|
144
|
-
direct = AutoTokenizer.from_pretrained(_FAST_MODEL, trust_remote_code=False)
|
|
145
|
-
assert "Shim" not in _backend_class_name(direct), (
|
|
146
|
-
f"fastokens patch leaked into user-side AutoTokenizer call: "
|
|
147
|
-
f"got {_backend_class_name(direct)!r}"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
# ---------------------------------------------------------------------------
|
|
152
|
-
# Failure-mode fallback: if fastokens raises during the patched load,
|
|
153
|
-
# load_tokenizer falls back to vanilla without surfacing the error.
|
|
154
|
-
# ---------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def test_fallback_on_fastokens_load_error(monkeypatch):
|
|
158
|
-
"""Simulate fastokens raising during patched load — load_tokenizer
|
|
159
|
-
should fall back to vanilla and return a working tokenizer."""
|
|
160
|
-
import renderers.base as rb
|
|
161
|
-
|
|
162
|
-
def _boom(*args, **kwargs):
|
|
163
|
-
raise ValueError("simulated fastokens failure: unsupported pre-tokenizer")
|
|
164
|
-
|
|
165
|
-
monkeypatch.setattr(rb, "_patched_load", _boom)
|
|
166
|
-
|
|
167
|
-
tok = load_tokenizer(_FAST_MODEL) # default use_fastokens=True
|
|
168
|
-
# The vanilla fallback ran — backend is not a fastokens shim.
|
|
169
|
-
assert "Shim" not in _backend_class_name(tok)
|
|
170
|
-
# Still works.
|
|
171
|
-
assert len(tok.encode("hi", add_special_tokens=False)) > 0
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# ---------------------------------------------------------------------------
|
|
175
|
-
# Print suppression: fastokens itself prints "[fastokens]
|
|
176
|
-
# patch_transformers: ..." on every patch/unpatch call. Building a
|
|
177
|
-
# RendererPool of size N would emit ~N lines (the pool factory calls
|
|
178
|
-
# load_tokenizer once per slot). load_tokenizer swallows that stdout
|
|
179
|
-
# chatter and emits a single INFO log on the first patch instead.
|
|
180
|
-
# ---------------------------------------------------------------------------
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def test_no_fastokens_stdout_chatter(capsys, caplog):
|
|
184
|
-
"""``load_tokenizer`` must not leak ``[fastokens]`` prints onto
|
|
185
|
-
stdout, and must emit exactly one INFO log per process announcing
|
|
186
|
-
the fast path (not once per call)."""
|
|
187
|
-
import logging
|
|
188
|
-
|
|
189
|
-
import renderers.base as rb
|
|
190
|
-
|
|
191
|
-
# Reset the process-wide "announced" flag so this test sees the
|
|
192
|
-
# first-call log even if another test loaded a tokenizer earlier.
|
|
193
|
-
rb._FASTOKENS_ANNOUNCED = False
|
|
194
|
-
|
|
195
|
-
with caplog.at_level(logging.INFO, logger="renderers.base"):
|
|
196
|
-
load_tokenizer(_FAST_MODEL)
|
|
197
|
-
load_tokenizer(_FAST_MODEL)
|
|
198
|
-
|
|
199
|
-
captured = capsys.readouterr()
|
|
200
|
-
assert "[fastokens]" not in captured.out, (
|
|
201
|
-
f"fastokens print leaked to stdout: {captured.out!r}"
|
|
202
|
-
)
|
|
203
|
-
assert "[fastokens]" not in captured.err, (
|
|
204
|
-
f"fastokens print leaked to stderr: {captured.err!r}"
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
fastokens_info = [
|
|
208
|
-
r for r in caplog.records if "fastokens enabled" in r.getMessage()
|
|
209
|
-
]
|
|
210
|
-
assert len(fastokens_info) == 1, (
|
|
211
|
-
f"expected exactly one fastokens INFO log across two loads, "
|
|
212
|
-
f"got {len(fastokens_info)}"
|
|
213
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/sglang/multiturn_generate_sglang.py
RENAMED
|
File without changes
|
|
File without changes
|
{renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/tinker/multiturn_generate_tinker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|