forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchat_templates.py
More file actions
163 lines (138 loc) · 5.49 KB
/
Copy pathchat_templates.py
File metadata and controls
163 lines (138 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Chat-template registry + override (v0.36.0 Part C).
Replaces the silent ``f"{role}: {content}"`` fallback in
``trainer/sft.py`` with an explicit registry of named chat templates plus a
``DataConfig.chat_template`` override field that accepts either a registered
name or a raw Jinja string.
Mirrors LlamaFactory and Axolotl behaviour: tokenizer without a chat template
+ no override = hard error. Silent garbage labels are no longer possible.
"""
from __future__ import annotations
from types import MappingProxyType
from typing import Any, Optional
# Jinja templates for popular chat formats. Kept minimal — full upstream
# templates ship with the model tokenizer; these are conservative fallbacks
# for users explicitly opting in via DataConfig.chat_template = "<name>".
#
# All templates assume ``messages`` is a list of ``{"role", "content"}``
# dicts and tolerate an optional leading ``system`` turn.
_CHATML = (
"{% for message in messages %}"
"<|im_start|>{{ message['role'] }}\n"
"{{ message['content'] }}<|im_end|>\n"
"{% endfor %}"
"{% if add_generation_prompt %}<|im_start|>assistant\n{% endif %}"
)
_LLAMA3 = (
"{% for message in messages %}"
"<|start_header_id|>{{ message['role'] }}<|end_header_id|>\n\n"
"{{ message['content'] }}<|eot_id|>"
"{% endfor %}"
"{% if add_generation_prompt %}"
"<|start_header_id|>assistant<|end_header_id|>\n\n"
"{% endif %}"
)
# Mistral's official template injects the system prompt INSIDE the first
# [INST] block, not as a freestanding turn. We track whether we've emitted
# the leading [INST] yet and prepend the system content to the next user
# turn's content.
_MISTRAL = (
"{% set system = namespace(content='') %}"
"{% for message in messages %}"
"{% if message['role'] == 'system' %}"
"{% set system.content = message['content'] %}"
"{% elif message['role'] == 'user' %}"
"{% if system.content %}"
"[INST] {{ system.content }}\n\n{{ message['content'] }} [/INST]"
"{% set system.content = '' %}"
"{% else %}"
"[INST] {{ message['content'] }} [/INST]"
"{% endif %}"
"{% elif message['role'] == 'assistant' %}"
"{{ message['content'] }}</s>"
"{% endif %}"
"{% endfor %}"
)
_GEMMA3 = (
"{% for message in messages %}"
"<start_of_turn>{{ 'user' if message['role'] == 'user' else 'model' }}\n"
"{{ message['content'] }}<end_of_turn>\n"
"{% endfor %}"
"{% if add_generation_prompt %}<start_of_turn>model\n{% endif %}"
)
_DEEPSEEK_R1 = (
"{% for message in messages %}"
"{% if message['role'] == 'user' %}"
"<|User|>{{ message['content'] }}"
"{% elif message['role'] == 'assistant' %}"
"<|Assistant|>{{ message['content'] }}<|end▁of▁sentence|>"
"{% endif %}"
"{% endfor %}"
"{% if add_generation_prompt %}<|Assistant|>{% endif %}"
)
# Phi-4 and Qwen2.5 both use a ChatML variant — re-use the ChatML template.
# Wrap in MappingProxyType so callers cannot mutate the registry at runtime.
_REGISTRY: "MappingProxyType[str, str]" = MappingProxyType({
"chatml": _CHATML,
"qwen2.5": _CHATML,
"qwen": _CHATML,
"phi4": _CHATML,
"phi-4": _CHATML,
"llama3": _LLAMA3,
"llama-3": _LLAMA3,
"mistral": _MISTRAL,
"gemma3": _GEMMA3,
"gemma-3": _GEMMA3,
"deepseek-r1": _DEEPSEEK_R1,
})
# Treat anything containing Jinja control tokens (`{%` / `{{`) as a raw
# Jinja string instead of a registry key.
_JINJA_MARKERS = ("{%", "{{")
def list_template_names() -> list[str]:
"""Return the canonical (sorted) list of registered template names."""
return sorted(_REGISTRY.keys())
def get_template(name: str) -> str:
"""Look up a registered template by name. Raises KeyError if unknown."""
if name not in _REGISTRY:
raise KeyError(
f"chat_template '{name}' is not registered. "
f"Known: {', '.join(list_template_names())}"
)
return _REGISTRY[name]
def _looks_like_jinja(value: str) -> bool:
return any(marker in value for marker in _JINJA_MARKERS)
def resolve_chat_template(value: Optional[str]) -> Optional[str]:
"""Resolve a ``DataConfig.chat_template`` value to a Jinja string.
- ``None`` / empty → ``None``
- Looks-like-Jinja → returned unchanged
- Registered name → registry lookup
- Otherwise → ``KeyError`` (typo in the name)
"""
if not value:
return None
if _looks_like_jinja(value):
return value
return get_template(value)
def apply_chat_template_override(
tokenizer: Any, value: Optional[str], console: Any | None = None
) -> bool:
"""Set ``tokenizer.chat_template`` from a name or Jinja string.
No-op when ``value`` is ``None`` / empty. Mutates the tokenizer in-place
so downstream calls (HF ``apply_chat_template`` and the tokenizer's
``.save_pretrained``) pick up the override.
Returns ``True`` when an override was applied. When ``console`` is
supplied and an override fires, prints a yellow advisory so the user
knows that ``soup push`` will persist the override into
``tokenizer_config.json``.
"""
resolved = resolve_chat_template(value)
if resolved is None:
return False
tokenizer.chat_template = resolved
if console is not None:
console.print(
"[yellow]chat_template override applied.[/] Subsequent "
"tokenizer.save_pretrained() / soup push will persist this "
"Jinja string into tokenizer_config.json — replacing whatever "
"the model originally shipped."
)
return True