Skip to content

Commit 631eb5a

Browse files
committed
test ok
1 parent 8cee153 commit 631eb5a

File tree

2 files changed

+106
-9
lines changed

2 files changed

+106
-9
lines changed

packages/ollama-utils/src/chat-template.spec.ts

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { describe, expect, it } from "vitest";
2-
import { mapGGUFTemplateToOllama } from "./chat-template";
2+
import { convertGGUFTemplateToOllama } from "./chat-template";
3+
4+
interface UnknownCase {
5+
desc: string;
6+
jinjaTmpl: string;
7+
ollamaTmpl: string;
8+
stop?: string;
9+
}
310

411
describe("chat-template", () => {
512
it("should format a pre-existing template", async () => {
613
// example with chatml template
7-
const ollamaTmpl = mapGGUFTemplateToOllama({
14+
const ollamaTmpl = convertGGUFTemplateToOllama({
815
chat_template:
916
"{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}",
1017
});
@@ -18,7 +25,7 @@ describe("chat-template", () => {
1825

1926
it("should format by matching tokens", async () => {
2027
// example with chatml template
21-
const ollamaTmpl = mapGGUFTemplateToOllama({
28+
const ollamaTmpl = convertGGUFTemplateToOllama({
2229
chat_template: "something something <|im_start|> something something <|im_end|>",
2330
});
2431
expect(ollamaTmpl && ollamaTmpl.ollama);
@@ -30,15 +37,17 @@ describe("chat-template", () => {
3037

3138
it("should format using custom map", async () => {
3239
// example with THUDM/glm-edge-v-2b-gguf
33-
const ollamaTmpl = mapGGUFTemplateToOllama({
40+
const ollamaTmpl = convertGGUFTemplateToOllama({
3441
chat_template: "<|{{ item['role'] }}|>something<|begin_of_image|>",
3542
});
3643
expect(ollamaTmpl && ollamaTmpl.ollama);
37-
expect(ollamaTmpl?.ollama.template).toEqual("{{ if .System }}<|system|>\n{{ .System }}{{ end }}{{ if .Prompt }}<|user|>\n{{ .Prompt }}{{ end }}<|assistant|>\n{{ .Response }}");
44+
expect(ollamaTmpl?.ollama.template).toEqual(
45+
"{{ if .System }}<|system|>\n{{ .System }}{{ end }}{{ if .Prompt }}<|user|>\n{{ .Prompt }}{{ end }}<|assistant|>\n{{ .Response }}"
46+
);
3847
});
3948

4049
it("should format using @huggingface/jinja", async () => {
41-
const ollamaTmpl = mapGGUFTemplateToOllama({
50+
const ollamaTmpl = convertGGUFTemplateToOllama({
4251
chat_template:
4352
"{% for message in messages %}{{'<|MY_CUSTOM_TOKEN_START|>' + message['role'] + '\n' + message['content'] + '<|MY_CUSTOM_TOKEN_END|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|MY_CUSTOM_TOKEN_START|>assistant\n' }}{% endif %}",
4453
bos_token: "<bos>",
@@ -55,4 +64,93 @@ describe("chat-template", () => {
5564
]);
5665
});
5766

67+
it.each([
68+
{
69+
desc: "This template has system message baked inside it",
70+
jinjaTmpl:
71+
"{{ '以下是描述一项任务的指令。请输出合适的内容回应指令。\n### Input:\n大象和猎豹的奔跑速度谁更快,简单说明原因.\n\n### Response:\n猎豹的奔跑速度比大象快得多。\n\n猎豹:是世界上奔跑速度最快的陆地动物之一,短距离内可以达到约 100-120 公里/小时(约 60-75 英里/小时)。\n大象:虽然大象体型巨大,但它们的速度较慢,奔跑速度最高约为 40 公里/小时(约 25 英里/小时)。\n因此,猎豹在速度上远远超过了大象。\n\n### Input:\n鱼为什么能在水里呼吸。\n\n### Response:\n鱼能够在水中呼吸,主要是因为它们有一种特殊的呼吸器官——鳃。鳃能够从水中提取氧气,并排出二氧化碳,这个过程使鱼能够在水中生存。\n' }}{% for message in messages %}{% if message['role'] == 'user' %}{{ '\n\n### 指令:\n' + message['content'] + '\n\n' }}{% elif message['role'] == 'assistant' %}{{ '### 回应:\n' + message['content'] + '<|end_of_text|>' }}{% else %}{{ raise_exception('Only user and assistant roles are supported!') }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '### 回应:\n' }}{% endif %}",
72+
ollamaTmpl:
73+
'以下是描述一项任务的指令。请输出合适的内容回应指令。\n### Input:\n大象和猎豹的奔跑速度谁更快,简单说明原因.\n\n### Response:\n猎豹的奔跑速度比大象快得多。\n\n猎豹:是世界上奔跑速度最快的陆地动物之一,短距离内可以达到约 100-120 公里/小时(约 60-75 英里/小时)。\n大象:虽然大象体型巨大,但它们的速度较慢,奔跑速度最高约为 40 公里/小时(约 25 英里/小时)。\n因此,猎豹在速度上远远超过了大象。\n\n### Input:\n鱼为什么能在水里呼吸。\n\n### Response:\n鱼能够在水中呼吸,主要是因为它们有一种特殊的呼吸器官——鳃。鳃能够从水中提取氧气,并排出二氧化碳,这个过程使鱼能够在水中生存。\n{{- range .Messages }}{{- if eq .Role "user" }}\n\n### 指令:\n{{ .Content }}\n\n{{- else if eq .Role "assistant" }}### 回应:\n{{ .Content }}<|end_of_text|>{{- end }}{{- end }}### 回应:\n',
74+
stop: "###",
75+
},
76+
{
77+
desc: "Another template with system message baked inside it",
78+
jinjaTmpl:
79+
"{{ bos_token }}{%- if messages[0]['role'] == 'system' -%}{% set loop_messages = messages[1:] %}{%- else -%}{% set loop_messages = messages %}{% endif %}System: This is a chat between a user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions based on the context. The assistant should also indicate when the answer cannot be found in the context.\n\n{% for message in loop_messages %}{%- if message['role'] == 'user' -%}User: {{ message['content'].strip() + '\n\n' }}{%- else -%}Assistant: {{ message['content'].strip() + '\n\n' }}{%- endif %}{% if loop.last and message['role'] == 'user' %}Assistant:{% endif %}{% endfor %}",
80+
ollamaTmpl:
81+
'<bos>System: This is a chat between a user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user\'s questions based on the context. The assistant should also indicate when the answer cannot be found in the context.\n\n{{- range .Messages }}{{- if eq .Role "user" }}User: {{ .Content }}\n\nAssistant:{{- else if eq .Role "assistant" }} {{ .Content }}\n\n{{- end }}{{- end }} ',
82+
stop: "User:",
83+
},
84+
{
85+
desc: "Template formatted via jinja - 1",
86+
jinjaTmpl:
87+
"{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|prompt|>' + message['content'] + eos_token }}{% elif message['role'] == 'system' %}{{ '<|system|>' + message['content'] + eos_token }}{% elif message['role'] == 'assistant' %}{{ '<|answer|>' + message['content'] + eos_token }}{% endif %}{% if loop.last and add_generation_prompt %}{{ '<|answer|>' }}{% endif %}{% endfor %}",
88+
ollamaTmpl:
89+
"{{ if .System }}<|system|>{{ .System }}<eos>{{ end }}{{ if .Prompt }}<|prompt|>{{ .Prompt }}<eos>{{ end }}<|answer|>{{ .Response }}<eos>",
90+
stop: "<eos>",
91+
},
92+
{
93+
desc: "Template formatted via jinja - 2",
94+
jinjaTmpl:
95+
"{{ '<s>' }}{% if messages[0]['role'] == 'system' %}{% set system_message = messages[0]['content'] %}{% endif %}{% if system_message is defined %}{{ '<|system|>\n' + system_message + '<|end|>\n' }}{% endif %}{% for message in messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|user|>\n' + content + '<|end|>\n<|assistant|>\n' }}{% elif message['role'] == 'assistant' %}{{ content + '<|end|>' + '\n' }}{% endif %}{% endfor %}",
96+
ollamaTmpl:
97+
"{{ if .System }}<|system|>\n{{ .System }}<|end|>\n{{ end }}{{ if .Prompt }}<|user|>\n{{ .Prompt }}<|end|>\n{{ end }}<|assistant|>\n{{ .Response }}<|end|>",
98+
stop: "<|end|>",
99+
},
100+
{
101+
desc: "Template formatted via jinja - 3",
102+
jinjaTmpl:
103+
"{% for message in messages %}{% if loop.first %}[gMASK]<sop><|{{ message['role'] }}|>\n {{ message['content'] }}{% else %}<|{{ message['role'] }}|>\n {{ message['content'] }}{% endif %}{% endfor %}{% if add_generation_prompt %}<|assistant|>{% endif %}",
104+
ollamaTmpl:
105+
"{{ if .System }}[gMASK]<sop><|system|>\n {{ .System }}{{ end }}{{ if .Prompt }}<|user|>\n {{ .Prompt }}{{ end }}<|assistant|>\n {{ .Response }}",
106+
stop: "<|assistant|>",
107+
},
108+
{
109+
desc: "Template formatted via jinja - 4",
110+
jinjaTmpl:
111+
"{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{{ messages[0]['content'].strip() }}{% else %}{% set loop_messages = messages %}{{ 'A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user\\'s questions.' }}{% endif %}{% for message in loop_messages %}{% if loop.index0 == 0 %}{% if message['role'] == 'system' or message['role'] == 'user' %}{{ ' USER: ' + message['content'].strip() }}{% else %}{{ ' ASSISTANT: ' + message['content'].strip() + eos_token }}{% endif %}{% else %}{% if message['role'] == 'system' or message['role'] == 'user' %}{{ '\nUSER: ' + message['content'].strip() }}{% else %}{{ ' ASSISTANT: ' + message['content'].strip() + eos_token }}{% endif %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ ' ASSISTANT:' }}{% endif %}",
112+
ollamaTmpl:
113+
"{{ if .System }}{{ .System }}{{ end }}{{ if .Prompt }}\nUSER: {{ .Prompt }}{{ end }} ASSISTANT: {{ .Response }}<eos>",
114+
stop: "USER:",
115+
},
116+
{
117+
desc: "granite-3.0-8b-instruct-GGUF - officially supported by ollama",
118+
jinjaTmpl:
119+
"{%- if tools %}\n {{- '<|start_of_role|>available_tools<|end_of_role|>\n' }}\n {%- for tool in tools %}\n {{- tool | tojson(indent=4) }}\n {%- if not loop.last %}\n {{- '\n\n' }}\n {%- endif %}\n {%- endfor %}\n {{- '<|end_of_text|>\n' }}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'system' %}\n {{- '<|start_of_role|>system<|end_of_role|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- elif message['role'] == 'user' %}\n {{- '<|start_of_role|>user<|end_of_role|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- elif message['role'] == 'assistant' %}\n {{- '<|start_of_role|>assistant<|end_of_role|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- elif message['role'] == 'assistant_tool_call' %}\n {{- '<|start_of_role|>assistant<|end_of_role|><|tool_call|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- elif message['role'] == 'tool_response' %}\n {{- '<|start_of_role|>tool_response<|end_of_role|>' + message['content'] + '<|end_of_text|>\n' }}\n {%- endif %}\n {%- if loop.last and add_generation_prompt %}\n {{- '<|start_of_role|>assistant<|end_of_role|>' }}\n {%- endif %}\n{%- endfor %}",
120+
ollamaTmpl:
121+
'{{- if .Tools }}<|start_of_role|>available_tools<|end_of_role|>\n{{- range .Tools }}\n{{ . }}\n{{ end }}<|end_of_text|>\n{{ end }}\n{{- range $index, $_ := .Messages }}<|start_of_role|>\n{{- if eq .Role "tool" }}tool_response\n{{- else }}{{ .Role }}\n{{- end }}<|end_of_role|>\n{{- if .Content }}{{ .Content }}\n{{- else if .ToolCalls }}<|tool_call|>\n{{- range .ToolCalls }}{"name": "{{ .Function.Name }}", "arguments": {{ .Function.Arguments }}}\n{{- end }}\n{{- end }}\n{{- if eq (len (slice $.Messages $index)) 1 }}\n{{- if eq .Role "assistant" }}\n{{- else }}<|end_of_text|>\n<|start_of_role|>assistant<|end_of_role|>\n{{- end }}\n{{- else }}<|end_of_text|>\n{{ end }}\n{{- end }}',
122+
// stop token is EOS, auto detected by ollama
123+
},
124+
{
125+
desc: "chatglm4",
126+
jinjaTmpl:
127+
"{% for message in messages %}{% if loop.first %}[gMASK]sop<|{{ message['role'] }}|>\n {{ message['content'] }}{% else %}<|{{ message['role'] }}|>\n {{ message['content'] }}{% endif %}{% endfor %}{% if add_generation_prompt %}<|assistant|>{% endif %}",
128+
ollamaTmpl:
129+
"{{ if .System }}[gMASK]sop<|system|>\n {{ .System }}{{ end }}{{ if .Prompt }}<|user|>\n {{ .Prompt }}{{ end }}<|assistant|>\n {{ .Response }}",
130+
stop: "<|assistant|>",
131+
},
132+
{
133+
// this case is currently without CUSTOM_TEMPLATE_MAPPING, because the jinja template does not produce "incremental" format (i.e. it adds eos_token to the end)
134+
desc: "non-incremental format",
135+
jinjaTmpl:
136+
"{{ bos_token }}{% for message in messages %}{{'<|' + message['role'] + '|>' + '\n' + message['content'] + '<|end|>\n' }}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}",
137+
ollamaTmpl:
138+
"{{ if .System }}<|system|>\n{{ .System }}<|end|>\n{{ end }}{{ if .Prompt }}<|user|>\n{{ .Prompt }}<|end|>\n{{ end }}<|assistant|>\n{{ .Response }}<|end|>",
139+
stop: "<|end|>",
140+
},
141+
] satisfies UnknownCase[])("should format known cases ($desc)", async (currCase: UnknownCase) => {
142+
// some known cases that we observed on the hub
143+
const ollamaTmpl = convertGGUFTemplateToOllama({
144+
chat_template: currCase.jinjaTmpl,
145+
bos_token: "<bos>",
146+
eos_token: "<eos>",
147+
});
148+
expect(ollamaTmpl && ollamaTmpl.ollama);
149+
expect(ollamaTmpl?.ollama.template).toEqual(currCase.ollamaTmpl);
150+
if (currCase.stop) {
151+
expect(ollamaTmpl?.ollama.params?.stop).toContain(currCase.stop);
152+
}
153+
});
154+
155+
// TODO: add test with "ollama/template" module compiled to wasm
58156
});

packages/ollama-utils/src/chat-template.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ const CUSTOM_TEMPLATE_MAPPING: ((ggufTmpl: string) => OllamaCustomMappedTemplate
8383
: undefined,
8484
];
8585

86-
// chat template mapping
87-
export function mapGGUFTemplateToOllama(
86+
export function convertGGUFTemplateToOllama(
8887
gguf: NonNullable<GGUFParsedInfo>,
8988
options?: {
9089
// for error tracking purpose
@@ -170,7 +169,7 @@ export function mapGGUFTemplateToOllama(
170169

171170
// try formatting the chat template into Go format
172171
// function is exported to be used in test
173-
export function convertJinjaToGoTemplate(gguf: NonNullable<GGUFParsedInfo>):
172+
function convertJinjaToGoTemplate(gguf: NonNullable<GGUFParsedInfo>):
174173
| {
175174
tmpl: string;
176175
stop?: string;

0 commit comments

Comments
 (0)