mirror of
https://github.com/ollama/ollama.git
synced 2025-04-22 14:34:28 +02:00
With support for multimodal models becoming more varied and common it is important for clients to be able to easily see what capabilities a model has. Retuning these from the show endpoint will allow clients to easily see what a model can do.
361 lines
9.9 KiB
Go
361 lines
9.9 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ollama/ollama/template"
|
|
"github.com/ollama/ollama/types/model"
|
|
)
|
|
|
|
// Constants for GGUF magic bytes and version
|
|
var (
|
|
ggufMagic = []byte{0x47, 0x47, 0x55, 0x46} // "GGUF"
|
|
ggufVer = uint32(3) // Version 3
|
|
)
|
|
|
|
// Helper function to create mock GGUF data
|
|
func createMockGGUFData(architecture string, vision bool) []byte {
|
|
var buf bytes.Buffer
|
|
|
|
// Write GGUF header
|
|
buf.Write(ggufMagic)
|
|
binary.Write(&buf, binary.LittleEndian, ggufVer)
|
|
|
|
// Write tensor count (0 for our test)
|
|
var numTensors uint64 = 0
|
|
binary.Write(&buf, binary.LittleEndian, numTensors)
|
|
|
|
// Calculate number of metadata entries
|
|
numMetaEntries := uint64(1) // architecture entry
|
|
if vision {
|
|
numMetaEntries++
|
|
}
|
|
// Add embedding entry if architecture is "bert"
|
|
if architecture == "bert" {
|
|
numMetaEntries++
|
|
}
|
|
binary.Write(&buf, binary.LittleEndian, numMetaEntries)
|
|
|
|
// Write architecture metadata
|
|
archKey := "general.architecture"
|
|
keyLen := uint64(len(archKey))
|
|
binary.Write(&buf, binary.LittleEndian, keyLen)
|
|
buf.WriteString(archKey)
|
|
|
|
// String type (8)
|
|
var strType uint32 = 8
|
|
binary.Write(&buf, binary.LittleEndian, strType)
|
|
|
|
// String length
|
|
strLen := uint64(len(architecture))
|
|
binary.Write(&buf, binary.LittleEndian, strLen)
|
|
buf.WriteString(architecture)
|
|
|
|
if vision {
|
|
visionKey := architecture + ".vision.block_count"
|
|
keyLen = uint64(len(visionKey))
|
|
binary.Write(&buf, binary.LittleEndian, keyLen)
|
|
buf.WriteString(visionKey)
|
|
|
|
// uint32 type (4)
|
|
var uint32Type uint32 = 4
|
|
binary.Write(&buf, binary.LittleEndian, uint32Type)
|
|
|
|
// uint32 value (1)
|
|
var countVal uint32 = 1
|
|
binary.Write(&buf, binary.LittleEndian, countVal)
|
|
}
|
|
// Write embedding metadata if architecture is "bert"
|
|
if architecture == "bert" {
|
|
poolKey := architecture + ".pooling_type"
|
|
keyLen = uint64(len(poolKey))
|
|
binary.Write(&buf, binary.LittleEndian, keyLen)
|
|
buf.WriteString(poolKey)
|
|
|
|
// uint32 type (4)
|
|
var uint32Type uint32 = 4
|
|
binary.Write(&buf, binary.LittleEndian, uint32Type)
|
|
|
|
// uint32 value (1)
|
|
var poolingVal uint32 = 1
|
|
binary.Write(&buf, binary.LittleEndian, poolingVal)
|
|
}
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func TestModelCapabilities(t *testing.T) {
|
|
// Create a temporary directory for test files
|
|
tempDir, err := os.MkdirTemp("", "model_capabilities_test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create different types of mock model files
|
|
completionModelPath := filepath.Join(tempDir, "model.bin")
|
|
visionModelPath := filepath.Join(tempDir, "vision_model.bin")
|
|
embeddingModelPath := filepath.Join(tempDir, "embedding_model.bin")
|
|
// Create a simple model file for tests that don't depend on GGUF content
|
|
simpleModelPath := filepath.Join(tempDir, "simple_model.bin")
|
|
|
|
err = os.WriteFile(completionModelPath, createMockGGUFData("llama", false), 0o644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create completion model file: %v", err)
|
|
}
|
|
err = os.WriteFile(visionModelPath, createMockGGUFData("llama", true), 0o644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create completion model file: %v", err)
|
|
}
|
|
err = os.WriteFile(embeddingModelPath, createMockGGUFData("bert", false), 0o644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create embedding model file: %v", err)
|
|
}
|
|
err = os.WriteFile(simpleModelPath, []byte("dummy model data"), 0o644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create simple model file: %v", err)
|
|
}
|
|
|
|
toolsInsertTemplate, err := template.Parse("{{ .prompt }}{{ if .tools }}{{ .tools }}{{ end }}{{ if .suffix }}{{ .suffix }}{{ end }}")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
chatTemplate, err := template.Parse("{{ .prompt }}")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
toolsTemplate, err := template.Parse("{{ .prompt }}{{ if .tools }}{{ .tools }}{{ end }}")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
|
|
testModels := []struct {
|
|
name string
|
|
model Model
|
|
expectedCaps []model.Capability
|
|
}{
|
|
{
|
|
name: "model with completion capability",
|
|
model: Model{
|
|
ModelPath: completionModelPath,
|
|
Template: chatTemplate,
|
|
},
|
|
expectedCaps: []model.Capability{model.CapabilityCompletion},
|
|
},
|
|
|
|
{
|
|
name: "model with completion, tools, and insert capability",
|
|
model: Model{
|
|
ModelPath: completionModelPath,
|
|
Template: toolsInsertTemplate,
|
|
},
|
|
expectedCaps: []model.Capability{model.CapabilityCompletion, model.CapabilityTools, model.CapabilityInsert},
|
|
},
|
|
{
|
|
name: "model with tools and insert capability",
|
|
model: Model{
|
|
ModelPath: simpleModelPath,
|
|
Template: toolsInsertTemplate,
|
|
},
|
|
expectedCaps: []model.Capability{model.CapabilityTools, model.CapabilityInsert},
|
|
},
|
|
{
|
|
name: "model with tools capability",
|
|
model: Model{
|
|
ModelPath: simpleModelPath,
|
|
Template: toolsTemplate,
|
|
},
|
|
expectedCaps: []model.Capability{model.CapabilityTools},
|
|
},
|
|
{
|
|
name: "model with vision capability",
|
|
model: Model{
|
|
ModelPath: visionModelPath,
|
|
Template: chatTemplate,
|
|
},
|
|
expectedCaps: []model.Capability{model.CapabilityCompletion, model.CapabilityVision},
|
|
},
|
|
{
|
|
name: "model with vision, tools, and insert capability",
|
|
model: Model{
|
|
ModelPath: visionModelPath,
|
|
Template: toolsInsertTemplate,
|
|
},
|
|
expectedCaps: []model.Capability{model.CapabilityCompletion, model.CapabilityVision, model.CapabilityTools, model.CapabilityInsert},
|
|
},
|
|
{
|
|
name: "model with embedding capability",
|
|
model: Model{
|
|
ModelPath: embeddingModelPath,
|
|
Template: chatTemplate,
|
|
},
|
|
expectedCaps: []model.Capability{model.CapabilityEmbedding},
|
|
},
|
|
}
|
|
|
|
// compare two slices of model.Capability regardless of order
|
|
compareCapabilities := func(a, b []model.Capability) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
|
|
aCount := make(map[model.Capability]int)
|
|
for _, cap := range a {
|
|
aCount[cap]++
|
|
}
|
|
|
|
bCount := make(map[model.Capability]int)
|
|
for _, cap := range b {
|
|
bCount[cap]++
|
|
}
|
|
|
|
for cap, count := range aCount {
|
|
if bCount[cap] != count {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
for _, tt := range testModels {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Test Capabilities method
|
|
caps := tt.model.Capabilities()
|
|
if !compareCapabilities(caps, tt.expectedCaps) {
|
|
t.Errorf("Expected capabilities %v, got %v", tt.expectedCaps, caps)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModelCheckCapabilities(t *testing.T) {
|
|
// Create a temporary directory for test files
|
|
tempDir, err := os.MkdirTemp("", "model_check_capabilities_test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
visionModelPath := filepath.Join(tempDir, "vision_model.bin")
|
|
simpleModelPath := filepath.Join(tempDir, "model.bin")
|
|
embeddingModelPath := filepath.Join(tempDir, "embedding_model.bin")
|
|
|
|
err = os.WriteFile(simpleModelPath, []byte("dummy model data"), 0o644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create simple model file: %v", err)
|
|
}
|
|
err = os.WriteFile(visionModelPath, createMockGGUFData("llama", true), 0o644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create vision model file: %v", err)
|
|
}
|
|
err = os.WriteFile(embeddingModelPath, createMockGGUFData("bert", false), 0o644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create embedding model file: %v", err)
|
|
}
|
|
|
|
toolsInsertTemplate, err := template.Parse("{{ .prompt }}{{ if .tools }}{{ .tools }}{{ end }}{{ if .suffix }}{{ .suffix }}{{ end }}")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
chatTemplate, err := template.Parse("{{ .prompt }}")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
toolsTemplate, err := template.Parse("{{ .prompt }}{{ if .tools }}{{ .tools }}{{ end }}")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse template: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
model Model
|
|
checkCaps []model.Capability
|
|
expectedErrMsg string
|
|
}{
|
|
{
|
|
name: "completion model without tools capability",
|
|
model: Model{
|
|
ModelPath: simpleModelPath,
|
|
Template: chatTemplate,
|
|
},
|
|
checkCaps: []model.Capability{model.CapabilityTools},
|
|
expectedErrMsg: "does not support tools",
|
|
},
|
|
{
|
|
name: "model with all needed capabilities",
|
|
model: Model{
|
|
ModelPath: simpleModelPath,
|
|
Template: toolsInsertTemplate,
|
|
},
|
|
checkCaps: []model.Capability{model.CapabilityTools, model.CapabilityInsert},
|
|
},
|
|
{
|
|
name: "model missing insert capability",
|
|
model: Model{
|
|
ModelPath: simpleModelPath,
|
|
Template: toolsTemplate,
|
|
},
|
|
checkCaps: []model.Capability{model.CapabilityInsert},
|
|
expectedErrMsg: "does not support insert",
|
|
},
|
|
{
|
|
name: "model missing vision capability",
|
|
model: Model{
|
|
ModelPath: simpleModelPath,
|
|
Template: toolsTemplate,
|
|
},
|
|
checkCaps: []model.Capability{model.CapabilityVision},
|
|
expectedErrMsg: "does not support vision",
|
|
},
|
|
{
|
|
name: "model with vision capability",
|
|
model: Model{
|
|
ModelPath: visionModelPath,
|
|
Template: chatTemplate,
|
|
},
|
|
checkCaps: []model.Capability{model.CapabilityVision},
|
|
},
|
|
{
|
|
name: "model with embedding capability",
|
|
model: Model{
|
|
ModelPath: embeddingModelPath,
|
|
Template: chatTemplate,
|
|
},
|
|
checkCaps: []model.Capability{model.CapabilityEmbedding},
|
|
},
|
|
{
|
|
name: "unknown capability",
|
|
model: Model{
|
|
ModelPath: simpleModelPath,
|
|
Template: chatTemplate,
|
|
},
|
|
checkCaps: []model.Capability{"unknown"},
|
|
expectedErrMsg: "unknown capability",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Test CheckCapabilities method
|
|
err := tt.model.CheckCapabilities(tt.checkCaps...)
|
|
if tt.expectedErrMsg == "" {
|
|
if err != nil {
|
|
t.Errorf("Expected no error, got: %v", err)
|
|
}
|
|
} else {
|
|
if err == nil {
|
|
t.Errorf("Expected error containing %q, got nil", tt.expectedErrMsg)
|
|
} else if !strings.Contains(err.Error(), tt.expectedErrMsg) {
|
|
t.Errorf("Expected error containing %q, got: %v", tt.expectedErrMsg, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|