renderers 0.1.12.dev17__tar.gz → 0.1.12.dev18__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 (90) hide show
  1. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/.github/workflows/test.yml +1 -1
  2. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/PKG-INFO +2 -2
  3. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/examples/sglang/multiturn_generate_sglang.py +1 -1
  4. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/examples/sglang/online_multiturn_sglang.py +1 -1
  5. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/examples/transformers/multiturn_generate_transformers.py +1 -1
  6. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/examples/vllm/multiturn_generate_vllm.py +1 -1
  7. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/pyproject.toml +1 -1
  8. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/_version.py +2 -2
  9. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/base.py +53 -42
  10. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/configs.py +16 -16
  11. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_parsers.py +10 -0
  12. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_renderer_config.py +6 -0
  13. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/uv.lock +5 -288
  14. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/.github/workflows/publish-dev.yml +0 -0
  15. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/.github/workflows/publish.yml +0 -0
  16. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/.github/workflows/style.yml +0 -0
  17. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/.gitignore +0 -0
  18. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/.pre-commit-config.yaml +0 -0
  19. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/LICENSE +0 -0
  20. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/README.md +0 -0
  21. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/docs/renderer-config.md +0 -0
  22. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/examples/README.md +0 -0
  23. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/examples/tinker/multiturn_generate_tinker.py +0 -0
  24. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/__init__.py +0 -0
  25. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/client.py +0 -0
  26. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/custom.py +0 -0
  27. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/deepseek_r1.py +0 -0
  28. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/deepseek_v3.py +0 -0
  29. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/deepseek_v4.py +0 -0
  30. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/default.py +0 -0
  31. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/gemma4.py +0 -0
  32. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/glm45.py +0 -0
  33. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/glm5.py +0 -0
  34. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/gpt_oss.py +0 -0
  35. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/hy3.py +0 -0
  36. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/inkling.py +0 -0
  37. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/kimi_k2.py +0 -0
  38. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/kimi_k25.py +0 -0
  39. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/laguna_s21.py +0 -0
  40. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/laguna_xs2.py +0 -0
  41. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/llama_3.py +0 -0
  42. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/minimax_m2.py +0 -0
  43. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/nemotron3.py +0 -0
  44. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/parsers.py +0 -0
  45. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/parsing.py +0 -0
  46. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/prime_qwen3.py +0 -0
  47. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/qwen3.py +0 -0
  48. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/qwen35.py +0 -0
  49. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/qwen36.py +0 -0
  50. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/qwen38.py +0 -0
  51. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/qwen3_vl.py +0 -0
  52. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/renderers/reasoning.py +0 -0
  53. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/conftest.py +0 -0
  54. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/parity.py +0 -0
  55. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/reference_rendering.py +0 -0
  56. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_bridge.py +0 -0
  57. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_build_helpers.py +0 -0
  58. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_client.py +0 -0
  59. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_custom.py +0 -0
  60. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_deepseek_r1.py +0 -0
  61. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_deepseek_v4.py +0 -0
  62. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_disabled_thinking_stability.py +0 -0
  63. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_gemma4.py +0 -0
  64. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_glm_tool_name_validation.py +0 -0
  65. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_hy3.py +0 -0
  66. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_incremental.py +0 -0
  67. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_inkling.py +0 -0
  68. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_is_content.py +0 -0
  69. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_kimi_k25_tool_schema.py +0 -0
  70. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_laguna_m1.py +0 -0
  71. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_laguna_xs21.py +0 -0
  72. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_llama_3.py +0 -0
  73. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_load_tokenizer.py +0 -0
  74. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_message_indices.py +0 -0
  75. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_message_tool_names.py +0 -0
  76. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_multimodal.py +0 -0
  77. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_nemotron35_vlm.py +0 -0
  78. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_nemotron3_ultra.py +0 -0
  79. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_offsetless_tokenizers.py +0 -0
  80. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_parity.py +0 -0
  81. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_parse_response.py +0 -0
  82. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_parse_response_robustness.py +0 -0
  83. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_preserve_thinking.py +0 -0
  84. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_qwen35_size_coverage.py +0 -0
  85. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_qwen38.py +0 -0
  86. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_reasoning_boundaries.py +0 -0
  87. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_roundtrip.py +0 -0
  88. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_sampled_mask.py +0 -0
  89. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_tokens_per_message.py +0 -0
  90. {renderers-0.1.12.dev17 → renderers-0.1.12.dev18}/tests/test_tool_arg_type_preservation.py +0 -0
@@ -21,7 +21,7 @@ jobs:
21
21
  strategy:
22
22
  fail-fast: false
23
23
  matrix:
24
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
24
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
25
25
 
26
26
  steps:
27
27
  - uses: actions/checkout@v4
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: renderers
3
- Version: 0.1.12.dev17
3
+ Version: 0.1.12.dev18
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
- Requires-Python: <3.15,>=3.10
7
+ Requires-Python: <3.15,>=3.11
8
8
  Requires-Dist: jinja2
9
9
  Requires-Dist: numpy
10
10
  Requires-Dist: openai-harmony>=0.0.4
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env -S uv run --script
2
2
  # /// script
3
- # requires-python = ">=3.10,<3.14"
3
+ # requires-python = ">=3.11,<3.14"
4
4
  # dependencies = [
5
5
  # "renderers>=0.1.6",
6
6
  # "sglang==0.5.10.post1",
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env -S uv run --script
2
2
  # /// script
3
- # requires-python = ">=3.10,<3.14"
3
+ # requires-python = ">=3.11,<3.14"
4
4
  # dependencies = [
5
5
  # "renderers>=0.1.6",
6
6
  # "transformers>=5.3.0",
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env -S uv run --script
2
2
  # /// script
3
- # requires-python = ">=3.10,<3.14"
3
+ # requires-python = ">=3.11,<3.14"
4
4
  # dependencies = [
5
5
  # "renderers>=0.1.6",
6
6
  # "transformers>=4.50.0",
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env -S uv run --script
2
2
  # /// script
3
- # requires-python = ">=3.10,<3.14"
3
+ # requires-python = ">=3.11,<3.14"
4
4
  # dependencies = [
5
5
  # "renderers>=0.1.6",
6
6
  # "vllm>=0.20",
@@ -13,7 +13,7 @@ description = "Chat template renderers — deterministic message-to-token conver
13
13
  readme = "README.md"
14
14
  license = "Apache-2.0"
15
15
  license-files = ["LICENSE"]
16
- requires-python = ">=3.10,<3.15"
16
+ requires-python = ">=3.11,<3.15"
17
17
  dependencies = [
18
18
  "numpy",
19
19
  "openai>=1.108.1",
@@ -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.12.dev17'
22
- __version_tuple__ = version_tuple = (0, 1, 12, 'dev17')
21
+ __version__ = version = '0.1.12.dev18'
22
+ __version_tuple__ = version_tuple = (0, 1, 12, 'dev18')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1,15 +1,14 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
1
  import logging
5
2
  from collections.abc import Mapping
6
3
  from dataclasses import dataclass, field
4
+ from enum import StrEnum
7
5
  from typing import (
8
6
  TYPE_CHECKING,
9
7
  Any,
10
8
  Callable,
11
9
  Literal,
12
10
  Protocol,
11
+ Required,
13
12
  TypedDict,
14
13
  cast,
15
14
  runtime_checkable,
@@ -110,8 +109,8 @@ class Message(TypedDict, total=False):
110
109
  Optional keys mirror the OpenAI chat format for tool calling.
111
110
  """
112
111
 
113
- role: str
114
- content: Content
112
+ role: Required[str]
113
+ content: Required[Content]
115
114
  tool_calls: list[ToolCall]
116
115
  tool_call_id: str
117
116
  name: str
@@ -562,7 +561,7 @@ class RenderedTokens:
562
561
  return mask
563
562
 
564
563
 
565
- class ToolCallParseStatus(str, enum.Enum):
564
+ class ToolCallParseStatus(StrEnum):
566
565
  """Per-attempt outcome of parsing a single ``<tool_call>`` block.
567
566
 
568
567
  The renderer parser's job is JSON-syntax → ``dict`` (the parser-level
@@ -1310,15 +1309,19 @@ def load_tokenizer(model_name_or_path: str):
1310
1309
  the requested Meta ID so auto-resolution still selects ``Llama3Renderer``.
1311
1310
  Requires the ``renderers[transformers]`` extra.
1312
1311
  """
1313
- _require_transformers("Loading a tokenizer")
1314
- load_name_or_path = _tokenizer_source_for(model_name_or_path)
1315
- kwargs = _tokenizer_load_kwargs(load_name_or_path)
1316
- tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
1317
- return _preserve_requested_tokenizer_name(
1318
- tok,
1319
- requested_name_or_path=model_name_or_path,
1320
- loaded_name_or_path=load_name_or_path,
1321
- )
1312
+ try:
1313
+ _require_transformers("Loading a tokenizer")
1314
+ load_name_or_path = _tokenizer_source_for(model_name_or_path)
1315
+ kwargs = _tokenizer_load_kwargs(load_name_or_path)
1316
+ tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
1317
+ return _preserve_requested_tokenizer_name(
1318
+ tok,
1319
+ requested_name_or_path=model_name_or_path,
1320
+ loaded_name_or_path=load_name_or_path,
1321
+ )
1322
+ except Exception as exc:
1323
+ exc.add_note(f"While loading tokenizer {model_name_or_path!r}.")
1324
+ raise
1322
1325
 
1323
1326
 
1324
1327
  def _populate_registry():
@@ -1393,7 +1396,7 @@ def _populate_registry():
1393
1396
 
1394
1397
  def create_renderer(
1395
1398
  tokenizer,
1396
- config: RendererConfig | None = None,
1399
+ config: "RendererConfig | None" = None,
1397
1400
  *,
1398
1401
  chat_template_kwargs: Mapping[str, Any] | None = None,
1399
1402
  ) -> Renderer:
@@ -1425,26 +1428,34 @@ def create_renderer(
1425
1428
  """
1426
1429
  _populate_registry()
1427
1430
 
1428
- config = _resolve_renderer_config(
1429
- tokenizer,
1430
- config,
1431
- chat_template_kwargs=chat_template_kwargs,
1432
- )
1433
- from renderers.configs import CustomRendererConfig
1434
- from renderers.custom import custom_renderer_config, load_custom_renderer
1435
-
1436
- if isinstance(config, CustomRendererConfig):
1437
- renderer_cls = load_custom_renderer(config.import_path)
1438
- return renderer_cls(tokenizer, custom_renderer_config(config))
1439
- cls = RENDERER_REGISTRY.get(config.name)
1440
- if cls is None:
1441
- raise ValueError(
1442
- f"Unknown renderer {config.name!r}. Available: {', '.join(sorted(RENDERER_REGISTRY))}"
1431
+ try:
1432
+ config = _resolve_renderer_config(
1433
+ tokenizer,
1434
+ config,
1435
+ chat_template_kwargs=chat_template_kwargs,
1436
+ )
1437
+ from renderers.configs import CustomRendererConfig
1438
+ from renderers.custom import custom_renderer_config, load_custom_renderer
1439
+
1440
+ if isinstance(config, CustomRendererConfig):
1441
+ renderer_cls = load_custom_renderer(config.import_path)
1442
+ return renderer_cls(tokenizer, custom_renderer_config(config))
1443
+ cls = RENDERER_REGISTRY.get(config.name)
1444
+ if cls is None:
1445
+ raise ValueError(
1446
+ f"Unknown renderer {config.name!r}. Available: {', '.join(sorted(RENDERER_REGISTRY))}"
1447
+ )
1448
+ return cls(tokenizer, config)
1449
+ except Exception as exc:
1450
+ model_name = getattr(tokenizer, "name_or_path", "<unnamed tokenizer>")
1451
+ renderer_name = config.name if config is not None else "auto"
1452
+ exc.add_note(
1453
+ f"While creating renderer {renderer_name!r} for tokenizer {model_name!r}."
1443
1454
  )
1444
- return cls(tokenizer, config)
1455
+ raise
1445
1456
 
1446
1457
 
1447
- def template_field_names(config: RendererConfig) -> frozenset[str]:
1458
+ def template_field_names(config: "RendererConfig") -> frozenset[str]:
1448
1459
  """Chat-template kwargs that ``config``'s renderer accepts, custom renderers included."""
1449
1460
  from renderers.configs import CustomRendererConfig
1450
1461
  from renderers.custom import load_custom_renderer
@@ -1457,9 +1468,9 @@ def template_field_names(config: RendererConfig) -> frozenset[str]:
1457
1468
 
1458
1469
 
1459
1470
  def merge_chat_template_kwargs(
1460
- config: RendererConfig,
1471
+ config: "RendererConfig",
1461
1472
  chat_template_kwargs: Mapping[str, Any] | None,
1462
- ) -> RendererConfig:
1473
+ ) -> "RendererConfig":
1463
1474
  """Return ``config`` with the template kwargs applied, validated against its allowlist."""
1464
1475
  if not chat_template_kwargs:
1465
1476
  return config
@@ -1504,10 +1515,10 @@ def merge_chat_template_kwargs(
1504
1515
 
1505
1516
  def _resolve_renderer_config(
1506
1517
  tokenizer,
1507
- config: RendererConfig | None,
1518
+ config: "RendererConfig | None",
1508
1519
  *,
1509
1520
  chat_template_kwargs: Mapping[str, Any] | None = None,
1510
- ) -> RendererConfig:
1521
+ ) -> "RendererConfig":
1511
1522
  """Resolve auto/default config and merge chat-template kwargs."""
1512
1523
  from renderers.configs import AutoRendererConfig
1513
1524
 
@@ -1526,10 +1537,10 @@ def _resolve_renderer_config(
1526
1537
 
1527
1538
  def _resolve_auto_config(
1528
1539
  tokenizer,
1529
- auto: AutoRendererConfig,
1540
+ auto: "AutoRendererConfig",
1530
1541
  *,
1531
1542
  chat_template_kwargs: Mapping[str, Any] | None = None,
1532
- ) -> RendererConfig:
1543
+ ) -> "RendererConfig":
1533
1544
  """Map ``AutoRendererConfig`` → concrete typed config via the
1534
1545
  tokenizer's ``name_or_path``.
1535
1546
 
@@ -2117,8 +2128,8 @@ def introduces_user_query(
2117
2128
 
2118
2129
  def resolve_thinking_retention(
2119
2130
  config: Any,
2120
- implied: ResolvedThinkingRetention,
2121
- ) -> ResolvedThinkingRetention:
2131
+ implied: "ResolvedThinkingRetention",
2132
+ ) -> "ResolvedThinkingRetention":
2122
2133
  """Resolve the effective bridge policy for a renderer instance.
2123
2134
 
2124
2135
  ``config.thinking_retention is None`` means "derive from template knobs";
@@ -2132,7 +2143,7 @@ def resolve_thinking_retention(
2132
2143
 
2133
2144
 
2134
2145
  def should_rerender_for_thinking_retention(
2135
- thinking_retention: ResolvedThinkingRetention,
2146
+ thinking_retention: "ResolvedThinkingRetention",
2136
2147
  new_messages: list[Message],
2137
2148
  *,
2138
2149
  is_user_query: Callable[[Message], bool] = _is_user_message,
@@ -21,7 +21,7 @@ keys its tokenizer's template will honour, so it can't enumerate them.
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
- from typing import Annotated, ClassVar, Literal, Union
24
+ from typing import Annotated, ClassVar, Literal, Self, Union
25
25
 
26
26
  from pydantic import ConfigDict, Field, model_validator
27
27
  from pydantic_config import BaseConfig
@@ -159,7 +159,7 @@ class DefaultRendererConfig(BaseRendererConfig):
159
159
  _allow_opaque_template_kwargs = True
160
160
 
161
161
  @model_validator(mode="after")
162
- def _reject_legacy_preserve_flags(self):
162
+ def _reject_legacy_preserve_flags(self) -> Self:
163
163
  # ``extra="allow"`` would otherwise swallow the removed ``preserve_*``
164
164
  # bools into ``model_extra`` and forward them to apply_chat_template,
165
165
  # silently dropping the user's intent (DefaultRenderer can't
@@ -261,7 +261,7 @@ class Qwen36RendererConfig(BaseRendererConfig):
261
261
  _internal_fields = frozenset({"image_cache_max"})
262
262
 
263
263
  @model_validator(mode="after")
264
- def _check_thinking_retention(self):
264
+ def _check_thinking_retention(self) -> Self:
265
265
  _reject_thinking_retention_conflict(
266
266
  self,
267
267
  "preserve_thinking",
@@ -307,7 +307,7 @@ class Qwen38RendererConfig(BaseRendererConfig):
307
307
  _internal_fields = frozenset({"image_cache_max"})
308
308
 
309
309
  @model_validator(mode="after")
310
- def _check_thinking_retention(self):
310
+ def _check_thinking_retention(self) -> Self:
311
311
  _reject_thinking_retention_conflict(
312
312
  self,
313
313
  "preserve_thinking",
@@ -350,7 +350,7 @@ class Gemma4RendererConfig(BaseRendererConfig):
350
350
  _internal_fields = frozenset({"image_cache_max"})
351
351
 
352
352
  @model_validator(mode="after")
353
- def _check_thinking_retention(self):
353
+ def _check_thinking_retention(self) -> Self:
354
354
  _reject_thinking_retention_conflict(
355
355
  self,
356
356
  "preserve_thinking",
@@ -377,7 +377,7 @@ class GLM5RendererConfig(BaseRendererConfig):
377
377
  to ``"all"``."""
378
378
 
379
379
  @model_validator(mode="after")
380
- def _check_thinking_retention(self):
380
+ def _check_thinking_retention(self) -> Self:
381
381
  _reject_thinking_retention_conflict(
382
382
  self,
383
383
  "clear_thinking",
@@ -401,7 +401,7 @@ class GLM51RendererConfig(BaseRendererConfig):
401
401
  """See :class:`GLM5RendererConfig.clear_thinking`."""
402
402
 
403
403
  @model_validator(mode="after")
404
- def _check_thinking_retention(self):
404
+ def _check_thinking_retention(self) -> Self:
405
405
  _reject_thinking_retention_conflict(
406
406
  self,
407
407
  "clear_thinking",
@@ -424,7 +424,7 @@ class GLM53RendererConfig(BaseRendererConfig):
424
424
  """Reasoning-effort system preamble emitted by the canonical template."""
425
425
 
426
426
  @model_validator(mode="after")
427
- def _check_thinking_retention(self):
427
+ def _check_thinking_retention(self) -> Self:
428
428
  _reject_thinking_retention_conflict(
429
429
  self,
430
430
  "clear_thinking",
@@ -506,7 +506,7 @@ class Hy3RendererConfig(BaseRendererConfig):
506
506
  and suppresses the generation prompt (``add_generation_prompt=False``)."""
507
507
 
508
508
  @model_validator(mode="after")
509
- def _check_thinking_retention(self):
509
+ def _check_thinking_retention(self) -> Self:
510
510
  if self.preserved_thinking is not None and self.thinking_retention is not None:
511
511
  implied = "all" if self.preserved_thinking else "tool_cycle"
512
512
  if self.thinking_retention != implied:
@@ -566,7 +566,7 @@ class InklingRendererConfig(BaseRendererConfig):
566
566
  _internal_fields = frozenset({"image_cache_max", "audio_cache_max"})
567
567
 
568
568
  @model_validator(mode="after")
569
- def _check_reasoning_effort(self):
569
+ def _check_reasoning_effort(self) -> Self:
570
570
  eff = self.reasoning_effort
571
571
  if isinstance(eff, str):
572
572
  if eff.strip() not in INKLING_EFFORT_MAP:
@@ -631,7 +631,7 @@ class GptOssRendererConfig(BaseRendererConfig):
631
631
  )
632
632
 
633
633
  @model_validator(mode="after")
634
- def _check_thinking_retention(self):
634
+ def _check_thinking_retention(self) -> Self:
635
635
  _reject_thinking_retention_conflict(
636
636
  self,
637
637
  "auto_drop_analysis",
@@ -836,7 +836,7 @@ class Nemotron3RendererConfig(BaseRendererConfig):
836
836
  policy to ``"all"``."""
837
837
 
838
838
  @model_validator(mode="after")
839
- def _check_thinking_retention(self):
839
+ def _check_thinking_retention(self) -> Self:
840
840
  _reject_thinking_retention_conflict(
841
841
  self,
842
842
  "truncate_history_thinking",
@@ -877,7 +877,7 @@ class Nemotron3UltraRendererConfig(BaseRendererConfig):
877
877
  """See :class:`Nemotron3RendererConfig.truncate_history_thinking`."""
878
878
 
879
879
  @model_validator(mode="after")
880
- def _check_thinking_retention(self):
880
+ def _check_thinking_retention(self) -> Self:
881
881
  _reject_thinking_retention_conflict(
882
882
  self,
883
883
  "truncate_history_thinking",
@@ -913,7 +913,7 @@ class Nemotron35RendererConfig(BaseRendererConfig):
913
913
  """See :class:`Nemotron3RendererConfig.truncate_history_thinking`."""
914
914
 
915
915
  @model_validator(mode="after")
916
- def _check_thinking_retention(self):
916
+ def _check_thinking_retention(self) -> Self:
917
917
  _reject_thinking_retention_conflict(
918
918
  self,
919
919
  "truncate_history_thinking",
@@ -984,7 +984,7 @@ class DeepSeekV4RendererConfig(BaseRendererConfig):
984
984
  """
985
985
 
986
986
  @model_validator(mode="after")
987
- def _check_thinking_retention(self):
987
+ def _check_thinking_retention(self) -> Self:
988
988
  _reject_thinking_retention_conflict(
989
989
  self,
990
990
  "drop_thinking",
@@ -1019,7 +1019,7 @@ class CustomRendererConfig(BaseRendererConfig):
1019
1019
  _internal_fields = frozenset({"import_path"})
1020
1020
 
1021
1021
  @model_validator(mode="after")
1022
- def _validate_renderer_fields(self):
1022
+ def _validate_renderer_fields(self) -> Self:
1023
1023
  # Fail at config load, not at the first render: the import path must
1024
1024
  # resolve and the renderer's config must accept the other fields.
1025
1025
  from renderers.custom import custom_renderer_config
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+
5
7
  import pytest
6
8
 
7
9
  from renderers.base import ToolCallParseStatus, load_tokenizer
@@ -160,3 +162,11 @@ def test_default_renderer_without_parsers_is_backward_compatible():
160
162
  def test_think_parser_requires_initial_opener(text):
161
163
  parser = get_reasoning_parser("think", None)
162
164
  assert parser.extract(text) == (None, text)
165
+
166
+
167
+ def test_parse_status_uses_wire_value_as_string():
168
+ for status in ToolCallParseStatus:
169
+ assert str(status) == status.value
170
+ assert f"{status}" == status.value
171
+ assert json.loads(json.dumps({"status": status})) == {"status": status.value}
172
+ assert ToolCallParseStatus(status.value) is status
@@ -349,3 +349,9 @@ def test_default_renderer_config_rejects_legacy_preserve_flags():
349
349
 
350
350
  cfg = DefaultRendererConfig(some_jinja_kwarg=True)
351
351
  assert cfg.model_extra["some_jinja_kwarg"] is True
352
+
353
+
354
+ def test_message_required_keys():
355
+ assert base.Message.__required_keys__ == {"role", "content"}
356
+ assert "tool_calls" in base.Message.__optional_keys__
357
+ assert "reasoning_content" in base.Message.__optional_keys__