File size: 3,335 Bytes
ef6e870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Detects whether a model's chat template supports thinking/reasoning control.
 *
 * The server "/props" endpoint does NOT expose a supports_thinking flag.
 * It is computed internally by common_chat_templates_support_enable_thinking
 * in common/chat.cpp. A proper server flag would make this unnecessary.
 *
 * Detection order (most reliable first):
 *  1. Thinking-control Jinja2 variables  === pass-through via chat_template_kwargs
 *  2. Thinking-control Jinja2 conditionals  === template-native on/off logic
 *  3. Paired thinking-content tag pairs    === models that output special tags
 */

const THINKING_KWARG_VARS = ['enable_thinking', 'reasoning_effort', 'thinking_budget'];

/**
 * Paired thinking-content tag patterns.
 *
 * Inspected: llama-cpp-deepseek-r1/v3, nim-nemotron-{3,4}-nano, qwen-qwq-32b,
 * qwen-3-32b, google-gemma-4-31b-it, kimikimi-k2-thinking, apertus-8b-instruct,
 * mistralai-Mistral-Small-3.2-24B, ByteDance-Seed-OSS.
 *
 * The self-closing entry is Kimi-K2, Gemma4 fixed-length pair,
 * where both tags always appear adjacent with no content between.
 */
const THINKING_TAG_PATTERNS: Array<[string, string | null]> = [
	['<think>', '</think>'],
	['<|channel>thought', '<|channel|>'],
	['<|think|>', '</|think|>'],
	['<seed:think|>', '</seed:think|>'],
	['<think></think>', null]
];

const JINJA_THINKING_CONDITIONALS: RegExp[] = [
	// Matches: {% if enable thinking %}, {% if enable_thinking %}, {% if (enable_thinking is defined) %}
	// Handles: underscore-separated (enable_thinking), space-separated (enable thinking),
	// and optional parens/brackets before enable (if (enable_thinking )
	/\{%-?\s*if\s+\(?\s*\w*enable[\s_]+\w*(thinking|think|reasoning)/i,
	/\{%-?\s*if\s+\w*(thinking|reasoning)\s*(is not|==|!=)/i,
	/\{%-?\s*if\s+not\s+\w*enable/i,
	/\{%-?\s*if\s+ns\.enable_thinking/i
];

/** Guards against false positives:
 *  - Generic thought keyword (tool descriptions say "chain of thought")
 *  - Qwen vertical-bar token (used for ALL tool calls, not thinking)
 */
export function detectThinkingSupport(t: string): boolean {
	if (!t) return false;
	for (const kwarg of THINKING_KWARG_VARS) {
		const regex = new RegExp(
			`(\\{\\{[^{}]*\\b${kwarg}\\b[^{}]*\\}\\}|\\{%[^{}]*\\b${kwarg}\\b[^{}]*%\\})`,
			'i'
		);
		if (regex.test(t)) return true;
	}
	for (const p of JINJA_THINKING_CONDITIONALS) {
		if (p.test(t)) return true;
	}
	for (const [s, e] of THINKING_TAG_PATTERNS) {
		if (t.includes(s) && (!e || t.includes(e))) return true;
	}
	return false;
}

export function detectThinkingSupportWithReason(t: string): { supported: boolean; reason: string } {
	if (!t) return { supported: false, reason: 'No chat template available' };
	for (const kwarg of THINKING_KWARG_VARS) {
		const regex = new RegExp(
			`(\\{\\{[^{}]*\\b${kwarg}\\b[^{}]*\\}\\}|\\{%[^{}]*\\b${kwarg}\\b[^{}]*%\\})`,
			'i'
		);
		if (regex.test(t)) {
			return { supported: true, reason: 'Found: ' + kwarg };
		}
	}
	for (const p of JINJA_THINKING_CONDITIONALS) {
		if (p.test(t)) return { supported: true, reason: 'Found: thinking conditional' };
	}
	for (const [s, e] of THINKING_TAG_PATTERNS) {
		if (t.includes(s) && (!e || t.includes(e))) {
			return { supported: true, reason: 'Found: ' + s + (e ? ' .. ' + e : ' (self)') };
		}
	}
	return { supported: false, reason: 'No thinking patterns found' };
}