feature: derived templates This PR adds a simple hash based method for picking context and instruct templates based on the chat template, when provided by the back end.

0e2fdf37a818f287b93bf4e44bed536f5fbe249e

Karl-Johan Alm <karljohan-alm@garage.co.jp>

Signed
3 files changed, +142 -0Showing whitespace changes
public/script.js+26 -0
@@ -267,6 +267,7 @@ import { applyBrowserFixes } from './scripts/browser-fixes.js';
267267import { initServerHistory } from './scripts/server-history.js';
268268import { initSettingsSearch } from './scripts/setting-search.js';
269269import { initBulkEdit } from './scripts/bulk-edit.js';
270+import { deriveTemplatesFromChatTemplate } from './scripts/chat-cemplates.js';
270271
271272//exporting functions and vars for mods
272273export {
@@ -1235,6 +1236,31 @@ async function getStatusTextgen() {
12351236 const supportsTokenization = response.headers.get('x-supports-tokenization') === 'true';
12361237 supportsTokenization ? sessionStorage.setItem(TOKENIZER_SUPPORTED_KEY, 'true') : sessionStorage.removeItem(TOKENIZER_SUPPORTED_KEY);
12371238
1239+ const supportsChatTemplate = response.headers.get('x-supports-chat-template') === 'true';
1240+
1241+ if (supportsChatTemplate) {
1242+ const response = await fetch('/api/backends/text-completions/chat_template', {
1243+ method: 'POST',
1244+ headers: getRequestHeaders(),
1245+ body: JSON.stringify({
1246+ api_server: endpoint,
1247+ api_type: textgen_settings.type,
1248+ }),
1249+ });
1250+
1251+ const data = await response.json();
1252+ if (data) {
1253+ const chat_template = data.chat_template;
1254+ console.log(`We have chat template ${chat_template.split('\n')[0]}...`);
1255+ const templates = await deriveTemplatesFromChatTemplate(chat_template);
1256+ if (templates) {
1257+ const { context, instruct } = templates;
1258+ selectContextPreset(context, { isAuto: true });
1259+ selectInstructPreset(instruct, { isAuto: true });
1260+ }
1261+ }
1262+ }
1263+
12381264 // We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
12391265 if (online_status === 'no_connection' && data.response) {
12401266 toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
public/scripts/chat-cemplates.js+76 -0
@@ -0,0 +1,76 @@
1+// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
2+async function digestMessage(message) {
3+ const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
4+ const hashBuffer = await window.crypto.subtle.digest('SHA-256', msgUint8); // hash the message
5+ const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
6+ const hashHex = hashArray
7+ .map((b) => b.toString(16).padStart(2, '0'))
8+ .join(''); // convert bytes to hex string
9+ return hashHex;
10+}
11+
12+// the hash can be obtained from command line e.g. via: MODEL=path_to_model; python -c "import json, hashlib, sys; print(hashlib.sha256(json.load(open('"$MODEL"/tokenizer_config.json'))['chat_template'].strip().encode()).hexdigest())"
13+// note that chat templates must be trimmed to match the llama.cpp metadata value
14+const derivations = {
15+ // Meta
16+ '93c0e9aa3629bbd77e68dbc0f5621f6e6b23aa8d74b932595cdb8d64684526d7': {
17+ // Meta-Llama-3.1-8B-Instruct
18+ // Meta-Llama-3.1-70B-Instruct
19+ context: 'Llama 3 Instruct',
20+ instruct: 'Llama 3 Instruct',
21+ },
22+ 'd82792f95932f1c9cef5c4bd992f171225e3bf8c7b609b4557c9e1ec96be819f': {
23+ // Llama-3.2-1B-Instruct
24+ // Llama-3.2-3B-Instruct
25+ context: 'Llama 3 Instruct',
26+ instruct: 'Llama 3 Instruct',
27+ },
28+
29+ // Mistral
30+ // Mistral Reference: https://github.com/mistralai/mistral-common
31+ 'cafb64e0e9e5fd2503054b3479593fae39cbdfd52338ce8af9bb4664a8eb05bd': {
32+ // Mistral-Small-Instruct-2409
33+ // Mistral-Large-Instruct-2407
34+ context: 'Mistral V2 & V3',
35+ instruct: 'Mistral V2 & V3',
36+ },
37+ '3c4ad5fa60dd8c7ccdf82fa4225864c903e107728fcaf859fa6052cb80c92ee9': {
38+ // Mistral-Large-Instruct-2411
39+ context: 'Mistral V7', // https://huggingface.co/mistralai/Mistral-Large-Instruct-2411
40+ instruct: 'Mistral V7',
41+ },
42+ 'e7deee034838db2bfc7487788a3013d8a307ab69f72f3c54a85f06fd76007d4e': {
43+ // Mistral-Nemo-Instruct-2407
44+ context: 'Mistral V3-Tekken',
45+ instruct: 'Mistral V3-Tekken',
46+ },
47+ '26a59556925c987317ce5291811ba3b7f32ec4c647c400c6cc7e3a9993007ba7': {
48+ // Mistral-7B-Instruct-v0.3
49+ context: 'Mistral V2 & V3',
50+ instruct: 'Mistral V2 & V3',
51+ },
52+
53+ // Gemma
54+ 'ecd6ae513fe103f0eb62e8ab5bfa8d0fe45c1074fa398b089c93a7e70c15cfd6': {
55+ // gemma-2-9b-it
56+ // gemma-2-27b-it
57+ context: 'Gemma 2',
58+ instruct: 'Gemma 2',
59+ },
60+
61+ // Cohere
62+ '3b54f5c219ae1caa5c0bb2cdc7c001863ca6807cf888e4240e8739fa7eb9e02e': {
63+ // command-r-08-2024
64+ context: 'Command R',
65+ instruct: 'Command R',
66+ },
67+};
68+
69+export async function deriveTemplatesFromChatTemplate(chat_template) {
70+ const hash = await digestMessage(chat_template);
71+ if (hash in derivations) {
72+ return derivations[hash];
73+ }
74+ console.log(`Unknown chat template hash: ${hash}`);
75+ return null;
76+}
src/endpoints/backends/text-completions.js+40 -0
@@ -218,6 +218,18 @@ router.post('/status', jsonParser, async function (request, response) {
218218 } catch (error) {
219219 console.error(`Failed to get TabbyAPI model info: ${error}`);
220220 }
221+ } else if (apiType == TEXTGEN_TYPES.KOBOLDCPP) {
222+ try {
223+ const chatTemplateUrl = baseUrl + '/api/extra/chat_template';
224+ const chatTemplateReply = await fetch(chatTemplateUrl);
225+ if (chatTemplateReply.ok) {
226+ response.setHeader('x-supports-chat-template', 'true');
227+ } else {
228+ console.log(`ct res = ${JSON.stringify(chatTemplateReply)}`);
229+ }
230+ } catch (error) {
231+ console.error(`Failed to fetch chat template info: ${error}`);
232+ }
221233 }
222234
223235 return response.send({ result, data: data.data });
@@ -227,6 +239,34 @@ router.post('/status', jsonParser, async function (request, response) {
227239 }
228240});
229241
242+router.post('/chat_template', jsonParser, async function (request, response) {
243+ if (!request.body.api_server) return response.sendStatus(400);
244+
245+ try {
246+ const baseUrl = trimV1(request.body.api_server);
247+ const args = {
248+ headers: { 'Content-Type': 'application/json' },
249+ };
250+
251+ setAdditionalHeaders(request, args, baseUrl);
252+
253+ const chatTemplateUrl = baseUrl + '/api/extra/chat_template';
254+ const chatTemplateReply = await fetch(chatTemplateUrl, args);
255+
256+ if (!chatTemplateReply.ok) {
257+ console.log('Chat template endpoint is offline.');
258+ return response.status(400);
259+ }
260+
261+ /** @type {any} */
262+ const chatTemplate = await chatTemplateReply.json();
263+ return response.send(chatTemplate);
264+ } catch (error) {
265+ console.error(error);
266+ return response.status(500);
267+ }
268+});
269+
230270router.post('/generate', jsonParser, async function (request, response) {
231271 if (!request.body) return response.sendStatus(400);
232272