Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion examples/sglang/multiturn_generate_sglang.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10,<3.14"
# requires-python = ">=3.11,<3.14"
# dependencies = [
# "renderers>=0.1.6",
# "sglang==0.5.10.post1",
Expand Down
2 changes: 1 addition & 1 deletion examples/sglang/online_multiturn_sglang.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10,<3.14"
# requires-python = ">=3.11,<3.14"
# dependencies = [
# "renderers>=0.1.6",
# "transformers>=5.3.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/transformers/multiturn_generate_transformers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10,<3.14"
# requires-python = ">=3.11,<3.14"
# dependencies = [
# "renderers>=0.1.6",
# "transformers>=4.50.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/vllm/multiturn_generate_vllm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10,<3.14"
# requires-python = ">=3.11,<3.14"
# dependencies = [
# "renderers>=0.1.6",
# "vllm>=0.20",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ description = "Chat template renderers — deterministic message-to-token conver
readme = "README.md"
license = "Apache-2.0"
license-files = ["LICENSE"]
requires-python = ">=3.10,<3.15"
requires-python = ">=3.11,<3.15"
dependencies = [
"numpy",
"openai>=1.108.1",
Expand Down
95 changes: 53 additions & 42 deletions renderers/base.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from __future__ import annotations

import enum
import logging
from collections.abc import Mapping
from dataclasses import dataclass, field
from enum import StrEnum
from typing import (
TYPE_CHECKING,
Any,
Callable,
Literal,
Protocol,
Required,
TypedDict,
cast,
runtime_checkable,
Expand Down Expand Up @@ -110,8 +109,8 @@ class Message(TypedDict, total=False):
Optional keys mirror the OpenAI chat format for tool calling.
"""

role: str
content: Content
role: Required[str]
content: Required[Content]
tool_calls: list[ToolCall]
tool_call_id: str
name: str
Expand Down Expand Up @@ -562,7 +561,7 @@ def content_mask_for_roles(self, roles: "set[str] | frozenset[str]") -> list[boo
return mask


class ToolCallParseStatus(str, enum.Enum):
class ToolCallParseStatus(StrEnum):
"""Per-attempt outcome of parsing a single ``<tool_call>`` block.

The renderer parser's job is JSON-syntax → ``dict`` (the parser-level
Expand Down Expand Up @@ -1310,15 +1309,19 @@ def load_tokenizer(model_name_or_path: str):
the requested Meta ID so auto-resolution still selects ``Llama3Renderer``.
Requires the ``renderers[transformers]`` extra.
"""
_require_transformers("Loading a tokenizer")
load_name_or_path = _tokenizer_source_for(model_name_or_path)
kwargs = _tokenizer_load_kwargs(load_name_or_path)
tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
return _preserve_requested_tokenizer_name(
tok,
requested_name_or_path=model_name_or_path,
loaded_name_or_path=load_name_or_path,
)
try:
_require_transformers("Loading a tokenizer")
load_name_or_path = _tokenizer_source_for(model_name_or_path)
kwargs = _tokenizer_load_kwargs(load_name_or_path)
tok = _load_tokenizer_via_auto(load_name_or_path, **kwargs)
return _preserve_requested_tokenizer_name(
tok,
requested_name_or_path=model_name_or_path,
loaded_name_or_path=load_name_or_path,
)
except Exception as exc:
exc.add_note(f"While loading tokenizer {model_name_or_path!r}.")
raise


def _populate_registry():
Expand Down Expand Up @@ -1393,7 +1396,7 @@ def _populate_registry():

def create_renderer(
tokenizer,
config: RendererConfig | None = None,
config: "RendererConfig | None" = None,
*,
chat_template_kwargs: Mapping[str, Any] | None = None,
) -> Renderer:
Expand Down Expand Up @@ -1425,26 +1428,34 @@ def create_renderer(
"""
_populate_registry()

config = _resolve_renderer_config(
tokenizer,
config,
chat_template_kwargs=chat_template_kwargs,
)
from renderers.configs import CustomRendererConfig
from renderers.custom import custom_renderer_config, load_custom_renderer

if isinstance(config, CustomRendererConfig):
renderer_cls = load_custom_renderer(config.import_path)
return renderer_cls(tokenizer, custom_renderer_config(config))
cls = RENDERER_REGISTRY.get(config.name)
if cls is None:
raise ValueError(
f"Unknown renderer {config.name!r}. Available: {', '.join(sorted(RENDERER_REGISTRY))}"
try:
config = _resolve_renderer_config(
tokenizer,
config,
chat_template_kwargs=chat_template_kwargs,
)
from renderers.configs import CustomRendererConfig
from renderers.custom import custom_renderer_config, load_custom_renderer

if isinstance(config, CustomRendererConfig):
renderer_cls = load_custom_renderer(config.import_path)
return renderer_cls(tokenizer, custom_renderer_config(config))
cls = RENDERER_REGISTRY.get(config.name)
if cls is None:
raise ValueError(
f"Unknown renderer {config.name!r}. Available: {', '.join(sorted(RENDERER_REGISTRY))}"
)
return cls(tokenizer, config)
except Exception as exc:
model_name = getattr(tokenizer, "name_or_path", "<unnamed tokenizer>")
renderer_name = config.name if config is not None else "auto"
exc.add_note(
f"While creating renderer {renderer_name!r} for tokenizer {model_name!r}."
)
return cls(tokenizer, config)
raise


def template_field_names(config: RendererConfig) -> frozenset[str]:
def template_field_names(config: "RendererConfig") -> frozenset[str]:
"""Chat-template kwargs that ``config``'s renderer accepts, custom renderers included."""
from renderers.configs import CustomRendererConfig
from renderers.custom import load_custom_renderer
Expand All @@ -1457,9 +1468,9 @@ def template_field_names(config: RendererConfig) -> frozenset[str]:


def merge_chat_template_kwargs(
config: RendererConfig,
config: "RendererConfig",
chat_template_kwargs: Mapping[str, Any] | None,
) -> RendererConfig:
) -> "RendererConfig":
"""Return ``config`` with the template kwargs applied, validated against its allowlist."""
if not chat_template_kwargs:
return config
Expand Down Expand Up @@ -1504,10 +1515,10 @@ def merge_chat_template_kwargs(

def _resolve_renderer_config(
tokenizer,
config: RendererConfig | None,
config: "RendererConfig | None",
*,
chat_template_kwargs: Mapping[str, Any] | None = None,
) -> RendererConfig:
) -> "RendererConfig":
"""Resolve auto/default config and merge chat-template kwargs."""
from renderers.configs import AutoRendererConfig

Expand All @@ -1526,10 +1537,10 @@ def _resolve_renderer_config(

def _resolve_auto_config(
tokenizer,
auto: AutoRendererConfig,
auto: "AutoRendererConfig",
*,
chat_template_kwargs: Mapping[str, Any] | None = None,
) -> RendererConfig:
) -> "RendererConfig":
"""Map ``AutoRendererConfig`` → concrete typed config via the
tokenizer's ``name_or_path``.

Expand Down Expand Up @@ -2117,8 +2128,8 @@ def introduces_user_query(

def resolve_thinking_retention(
config: Any,
implied: ResolvedThinkingRetention,
) -> ResolvedThinkingRetention:
implied: "ResolvedThinkingRetention",
) -> "ResolvedThinkingRetention":
"""Resolve the effective bridge policy for a renderer instance.

``config.thinking_retention is None`` means "derive from template knobs";
Expand All @@ -2132,7 +2143,7 @@ def resolve_thinking_retention(


def should_rerender_for_thinking_retention(
thinking_retention: ResolvedThinkingRetention,
thinking_retention: "ResolvedThinkingRetention",
new_messages: list[Message],
*,
is_user_query: Callable[[Message], bool] = _is_user_message,
Expand Down
32 changes: 16 additions & 16 deletions renderers/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from __future__ import annotations

from typing import Annotated, ClassVar, Literal, Union
from typing import Annotated, ClassVar, Literal, Self, Union

from pydantic import ConfigDict, Field, model_validator
from pydantic_config import BaseConfig
Expand Down Expand Up @@ -159,7 +159,7 @@ class DefaultRendererConfig(BaseRendererConfig):
_allow_opaque_template_kwargs = True

@model_validator(mode="after")
def _reject_legacy_preserve_flags(self):
def _reject_legacy_preserve_flags(self) -> Self:
# ``extra="allow"`` would otherwise swallow the removed ``preserve_*``
# bools into ``model_extra`` and forward them to apply_chat_template,
# silently dropping the user's intent (DefaultRenderer can't
Expand Down Expand Up @@ -261,7 +261,7 @@ class Qwen36RendererConfig(BaseRendererConfig):
_internal_fields = frozenset({"image_cache_max"})

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"preserve_thinking",
Expand Down Expand Up @@ -307,7 +307,7 @@ class Qwen38RendererConfig(BaseRendererConfig):
_internal_fields = frozenset({"image_cache_max"})

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"preserve_thinking",
Expand Down Expand Up @@ -350,7 +350,7 @@ class Gemma4RendererConfig(BaseRendererConfig):
_internal_fields = frozenset({"image_cache_max"})

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"preserve_thinking",
Expand All @@ -377,7 +377,7 @@ class GLM5RendererConfig(BaseRendererConfig):
to ``"all"``."""

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"clear_thinking",
Expand All @@ -401,7 +401,7 @@ class GLM51RendererConfig(BaseRendererConfig):
"""See :class:`GLM5RendererConfig.clear_thinking`."""

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"clear_thinking",
Expand All @@ -424,7 +424,7 @@ class GLM53RendererConfig(BaseRendererConfig):
"""Reasoning-effort system preamble emitted by the canonical template."""

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"clear_thinking",
Expand Down Expand Up @@ -506,7 +506,7 @@ class Hy3RendererConfig(BaseRendererConfig):
and suppresses the generation prompt (``add_generation_prompt=False``)."""

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
if self.preserved_thinking is not None and self.thinking_retention is not None:
implied = "all" if self.preserved_thinking else "tool_cycle"
if self.thinking_retention != implied:
Expand Down Expand Up @@ -566,7 +566,7 @@ class InklingRendererConfig(BaseRendererConfig):
_internal_fields = frozenset({"image_cache_max", "audio_cache_max"})

@model_validator(mode="after")
def _check_reasoning_effort(self):
def _check_reasoning_effort(self) -> Self:
eff = self.reasoning_effort
if isinstance(eff, str):
if eff.strip() not in INKLING_EFFORT_MAP:
Expand Down Expand Up @@ -631,7 +631,7 @@ class GptOssRendererConfig(BaseRendererConfig):
)

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"auto_drop_analysis",
Expand Down Expand Up @@ -836,7 +836,7 @@ class Nemotron3RendererConfig(BaseRendererConfig):
policy to ``"all"``."""

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"truncate_history_thinking",
Expand Down Expand Up @@ -877,7 +877,7 @@ class Nemotron3UltraRendererConfig(BaseRendererConfig):
"""See :class:`Nemotron3RendererConfig.truncate_history_thinking`."""

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"truncate_history_thinking",
Expand Down Expand Up @@ -913,7 +913,7 @@ class Nemotron35RendererConfig(BaseRendererConfig):
"""See :class:`Nemotron3RendererConfig.truncate_history_thinking`."""

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"truncate_history_thinking",
Expand Down Expand Up @@ -984,7 +984,7 @@ class DeepSeekV4RendererConfig(BaseRendererConfig):
"""

@model_validator(mode="after")
def _check_thinking_retention(self):
def _check_thinking_retention(self) -> Self:
_reject_thinking_retention_conflict(
self,
"drop_thinking",
Expand Down Expand Up @@ -1019,7 +1019,7 @@ class CustomRendererConfig(BaseRendererConfig):
_internal_fields = frozenset({"import_path"})

@model_validator(mode="after")
def _validate_renderer_fields(self):
def _validate_renderer_fields(self) -> Self:
# Fail at config load, not at the first render: the import path must
# resolve and the renderer's config must accept the other fields.
from renderers.custom import custom_renderer_config
Expand Down
Loading
Loading