model: export capabillity check from model type

This information will be repeated anywhere information from a model is read, so it should be a shared exported function
This commit is contained in:
Bruce MacDonald
2026-08-25 14:09:46 -07:00
parent 377ef091dc
commit 9764ed75a9
4 changed files with 84 additions and 37 deletions
+3 -20
View File
@@ -189,22 +189,18 @@ func chatTemplateCapabilities(capabilities []model.Capability, chatTemplate stri
return capabilities
}
if chatTemplateHasToolSupport(chatTemplate) {
if model.ChatTemplateHasToolSupport(chatTemplate) {
capabilities = appendCapability(capabilities, model.CapabilityTools)
}
if chatTemplateHasThinkingSupport(chatTemplate) {
if model.ChatTemplateHasThinkingSupport(chatTemplate) {
capabilities = appendCapability(capabilities, model.CapabilityThinking)
}
return capabilities
}
func chatTemplateHasToolSupport(chatTemplate string) bool {
return strings.Contains(chatTemplate, "tools") || strings.Contains(chatTemplate, "tool_call")
}
func chatTemplateHasToolRoundTrip(chatTemplate string) bool {
if !chatTemplateHasToolSupport(chatTemplate) {
if !model.ChatTemplateHasToolSupport(chatTemplate) {
return false
}
@@ -220,19 +216,6 @@ func chatTemplateHasToolRoundTrip(chatTemplate string) bool {
strings.Contains(chatTemplate, "ipython"))
}
func chatTemplateHasThinkingSupport(chatTemplate string) bool {
if strings.Contains(chatTemplate, "<think>") && strings.Contains(chatTemplate, "</think>") {
return true
}
// Some Qwen/DeepSeek templates strip prior reasoning by splitting assistant
// content at </think>; llama.cpp can still extract reasoning from them.
return (strings.Contains(chatTemplate, "content.split('</think>')") ||
strings.Contains(chatTemplate, `content.split("</think>")`)) &&
!strings.Contains(chatTemplate, "reasoning_content") &&
!strings.Contains(chatTemplate, "<SPECIAL_12>")
}
func goTemplateCapabilities(t *template.Template) []model.Capability {
if t == nil {
return nil
+23
View File
@@ -1,5 +1,7 @@
package model
import "strings"
type Capability string
const (
@@ -16,3 +18,24 @@ const (
func (c Capability) String() string {
return string(c)
}
// ChatTemplateHasToolSupport reports whether a Jinja chat template references
// tools or tool calls.
func ChatTemplateHasToolSupport(chatTemplate string) bool {
return strings.Contains(chatTemplate, "tools") || strings.Contains(chatTemplate, "tool_call")
}
// ChatTemplateHasThinkingSupport reports whether a Jinja chat template emits
// thinking blocks.
func ChatTemplateHasThinkingSupport(chatTemplate string) bool {
if strings.Contains(chatTemplate, "<think>") && strings.Contains(chatTemplate, "</think>") {
return true
}
// Some Qwen/DeepSeek templates strip prior reasoning by splitting assistant
// content at </think>; llama.cpp can still extract reasoning from them.
return (strings.Contains(chatTemplate, "content.split('</think>')") ||
strings.Contains(chatTemplate, `content.split("</think>")`)) &&
!strings.Contains(chatTemplate, "reasoning_content") &&
!strings.Contains(chatTemplate, "<SPECIAL_12>")
}
+57
View File
@@ -0,0 +1,57 @@
package model
import "testing"
func TestChatTemplateCapabilities(t *testing.T) {
tests := []struct {
name string
chatTemplate string
wantTools bool
wantThinking bool
}{
{
name: "tools variable",
chatTemplate: "{% if tools %}{{ tools }}{% endif %}",
wantTools: true,
},
{
name: "tool call",
chatTemplate: "{% for tool_call in message.tool_calls %}{{ tool_call }}{% endfor %}",
wantTools: true,
},
{
name: "thinking tags",
chatTemplate: "<think>{{ content }}</think>",
wantThinking: true,
},
{
name: "thinking split",
chatTemplate: "{% set content = content.split('</think>')[-1] %}",
wantThinking: true,
},
{
name: "double quoted thinking split",
chatTemplate: `{% set content = content.split("</think>")[-1] %}`,
wantThinking: true,
},
{
name: "reasoning content exclusion",
chatTemplate: "{% set content = content.split('</think>')[-1] %}{{ reasoning_content }}",
},
{
name: "special token exclusion",
chatTemplate: "{% set content = content.split('</think>')[-1] %}<SPECIAL_12>",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ChatTemplateHasToolSupport(tt.chatTemplate); got != tt.wantTools {
t.Errorf("ChatTemplateHasToolSupport() = %v, want %v", got, tt.wantTools)
}
if got := ChatTemplateHasThinkingSupport(tt.chatTemplate); got != tt.wantThinking {
t.Errorf("ChatTemplateHasThinkingSupport() = %v, want %v", got, tt.wantThinking)
}
})
}
}
+1 -17
View File
@@ -528,7 +528,7 @@ func detectCapabilities(modelDir string) modelCapabilities {
return modelCapabilities{
vision: cfg.VisionConfig != nil || cfg.HasVision,
audio: cfg.AudioConfig != nil || cfg.SoundConfig != nil,
thinking: chatTemplateHasThinkingSupport(readChatTemplate(modelDir)) ||
thinking: model.ChatTemplateHasThinkingSupport(readChatTemplate(modelDir)) ||
alwaysSupportsThinking(cfg.Architectures, cfg.ModelType),
}
}
@@ -551,22 +551,6 @@ func readChatTemplate(modelDir string) string {
return ""
}
// chatTemplateHasThinkingSupport reports whether a chat template emits thinking
// blocks. Copied from server.chatTemplateHasThinkingSupport so this package need
// not depend on the server package for an eight-line string check.
func chatTemplateHasThinkingSupport(chatTemplate string) bool {
if strings.Contains(chatTemplate, "<think>") && strings.Contains(chatTemplate, "</think>") {
return true
}
// Some Qwen/DeepSeek templates strip prior reasoning by splitting assistant
// content at </think>; llama.cpp can still extract reasoning from them.
return (strings.Contains(chatTemplate, "content.split('</think>')") ||
strings.Contains(chatTemplate, `content.split("</think>")`)) &&
!strings.Contains(chatTemplate, "reasoning_content") &&
!strings.Contains(chatTemplate, "<SPECIAL_12>")
}
func alwaysSupportsThinking(architectures []string, modelType string) bool {
if isQwen35Family(modelType) {
return true