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.
Files changed (70) hide show
  1. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/PKG-INFO +1 -2
  2. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/pyproject.toml +0 -12
  3. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/_version.py +2 -2
  4. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/base.py +28 -215
  5. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_load_tokenizer.py +14 -32
  6. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/uv.lock +1 -33
  7. renderers-0.1.8.dev50/tests/test_load_tokenizer_fastokens.py +0 -213
  8. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.github/workflows/publish-dev.yml +0 -0
  9. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.github/workflows/publish.yml +0 -0
  10. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.github/workflows/style.yml +0 -0
  11. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.github/workflows/test.yml +0 -0
  12. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.gitignore +0 -0
  13. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/.pre-commit-config.yaml +0 -0
  14. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/LICENSE +0 -0
  15. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/README.md +0 -0
  16. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/docs/renderer-config.md +0 -0
  17. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/README.md +0 -0
  18. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/sglang/multiturn_generate_sglang.py +0 -0
  19. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/sglang/online_multiturn_sglang.py +0 -0
  20. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/tinker/multiturn_generate_tinker.py +0 -0
  21. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/transformers/multiturn_generate_transformers.py +0 -0
  22. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/examples/vllm/multiturn_generate_vllm.py +0 -0
  23. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/__init__.py +0 -0
  24. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/client.py +0 -0
  25. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/configs.py +0 -0
  26. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/deepseek_r1.py +0 -0
  27. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/deepseek_v3.py +0 -0
  28. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/default.py +0 -0
  29. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/glm45.py +0 -0
  30. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/glm5.py +0 -0
  31. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/gpt_oss.py +0 -0
  32. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/kimi_k2.py +0 -0
  33. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/kimi_k25.py +0 -0
  34. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/laguna_xs2.py +0 -0
  35. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/llama_3.py +0 -0
  36. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/minimax_m2.py +0 -0
  37. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/nemotron3.py +0 -0
  38. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/parsers.py +0 -0
  39. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/parsing.py +0 -0
  40. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/qwen3.py +0 -0
  41. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/qwen35.py +0 -0
  42. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/qwen36.py +0 -0
  43. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/renderers/qwen3_vl.py +0 -0
  44. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/conftest.py +0 -0
  45. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_bridge.py +0 -0
  46. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_build_helpers.py +0 -0
  47. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_client.py +0 -0
  48. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_deepseek_r1.py +0 -0
  49. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_gpt_oss_harmony_parity.py +0 -0
  50. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_incremental.py +0 -0
  51. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_is_content.py +0 -0
  52. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_kimi_k25_tool_schema.py +0 -0
  53. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_llama_3.py +0 -0
  54. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_message_indices.py +0 -0
  55. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_message_tool_names.py +0 -0
  56. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_multimodal.py +0 -0
  57. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_nemotron3_parity.py +0 -0
  58. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_nemotron3_ultra.py +0 -0
  59. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_parse_response.py +0 -0
  60. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_parse_response_robustness.py +0 -0
  61. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_parsers.py +0 -0
  62. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_preserve_thinking.py +0 -0
  63. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_qwen35_size_coverage.py +0 -0
  64. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_render_ids.py +0 -0
  65. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_renderer_config.py +0 -0
  66. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_renderer_config_parity.py +0 -0
  67. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_roundtrip.py +0 -0
  68. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_sampled_mask.py +0 -0
  69. {renderers-0.1.8.dev50 → renderers-0.1.8.dev51}/tests/test_tokens_per_message.py +0 -0
  70. {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.dev50
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.dev50'
22
- __version_tuple__ = version_tuple = (0, 1, 8, 'dev50')
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
- model_name_or_path: str,
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
- **Security** — default ``trust_remote_code=False``. Models listed in
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
- """Return a tokenizer that supports ``return_offsets_mapping=True``.
1737
-
1738
- If ``tokenizer`` itself supports offsets, returns it unchanged.
1739
- Otherwise loads a vanilla (non-fastokens) tokenizer from
1740
- ``tokenizer.name_or_path`` and caches it. Raises if the tokenizer
1741
- has no usable ``name_or_path`` hand-coded renderers always pass
1742
- a tokenizer loaded via ``load_tokenizer`` which does set it.
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
- return tokenizer
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
- "Cannot construct an offset-aware tokenizer: the supplied "
1755
- "tokenizer has no ``name_or_path`` to fall back on. Pass a "
1756
- "tokenizer loaded via ``renderers.base.load_tokenizer``."
1757
- )
1758
-
1759
- with _offset_tokenizers_lock:
1760
- cached = _offset_tokenizers.get(name_or_path)
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. The
1843
- ``fastokens`` patch ``load_tokenizer`` applies by default does
1844
- **not** track offsets when that's the case we transparently load
1845
- a vanilla offset-capable tokenizer for the same model and cache it
1846
- (see :func:`_get_offset_tokenizer`). Hand-coded renderers are only
1847
- registered for model families that ship a fast tokenizer, so a
1848
- silent slow-tokenizer fallback isn't supported — BPE drift at the
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, use_fastokens=False)
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 test_offset_tokenizer_uses_unsloth_mirror_for_meta_llama(monkeypatch):
124
- """Offset-tokenizer reloads must use the same unrestricted source
125
- override, otherwise Llama rendering can hit the gated Meta repo after
126
- the initial tokenizer load succeeds."""
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 = "meta-llama/Llama-3.2-1B-Instruct"
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
- return {"offset_mapping": [(0, 1)]}
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
- assert calls == [("unsloth/Llama-3.2-1B-Instruct", {"trust_remote_code": False})]
158
- assert tok.name_or_path == "meta-llama/Llama-3.2-1B-Instruct"
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:55:59.889910839Z"
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