ollama/server/capabilities_test.go
Daniel Hiltgen 3b1999d1d9
server: report only decision capability for decision models (#18737)
Show and list now return just "decision" for models declaring it, so clients don't offer them for general chat, tools, or thinking. The show response also omits the thinking field for these models. Scheduling and serving still check the full capability set.
2026-10-01 13:49:46 -07:00

113 lines
3.8 KiB
Go

package server
import (
"net/http"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/parser"
"github.com/ollama/ollama/types/model"
)
func TestCreateCapabilities(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, format := range []string{"gguf", "safetensors"} {
t.Run(format, func(t *testing.T) {
t.Setenv("OLLAMA_MODELS", t.TempDir())
var s Server
baseCaps := []string{"completion", "vision"}
if format == "safetensors" {
createSafetensorsTestModel(t, "base", model.ConfigV2{
ModelFormat: format, Renderer: "qwen3.5", Capabilities: baseCaps,
}, nil)
} else {
_, digest := createBinFile(t, map[string]any{"general.architecture": "qwen35"}, nil)
w := createRequest(t, s.CreateHandler, api.CreateRequest{
Model: "base", Files: map[string]string{"model.gguf": digest},
Capabilities: baseCaps, Stream: &stream,
})
if w.Code != http.StatusOK {
t.Fatalf("create base: %d %s", w.Code, w.Body)
}
}
modelfile := "FROM base\nCAPABILITY decision\nCAPABILITY completion\nCAPABILITY decision\n"
for _, name := range []string{"declared", "roundtrip", "inherited"} {
mf, err := parser.ParseFile(strings.NewReader(modelfile))
if err != nil {
t.Fatal(err)
}
req, err := mf.CreateRequest(t.TempDir())
if err != nil {
t.Fatal(err)
}
req.Model, req.Stream = name, &stream
// The CLI uploads local files under their base names.
files := make(map[string]string, len(req.Files))
for path, digest := range req.Files {
files[filepath.Base(path)] = digest
}
req.Files = files
w := createRequest(t, s.CreateHandler, req)
if w.Code != http.StatusOK {
t.Fatalf("create %s: %d %s", name, w.Code, w.Body)
}
cfg := readCreatedModelConfig(t, name)
if want := []string{"completion", "vision", "decision"}; !slices.Equal(cfg.Capabilities, want) {
t.Fatalf("%s capabilities = %v, want %v", name, cfg.Capabilities, want)
}
shown, err := GetModelInfo(api.ShowRequest{Model: name})
if err != nil {
t.Fatal(err)
}
if !slices.Contains(shown.Capabilities, model.CapabilityDecision) || !strings.Contains(shown.Modelfile, "CAPABILITY decision\n") {
t.Fatalf("show %s lost capability: %+v", name, shown)
}
if want := []model.Capability{model.CapabilityDecision}; !slices.Equal(shown.Capabilities, want) || shown.Thinking != nil {
t.Fatalf("show %s capabilities = %v thinking = %v, want only %v", name, shown.Capabilities, shown.Thinking, want)
}
m, err := GetModel(name)
if err != nil {
t.Fatal(err)
}
if err := m.CheckCapabilities(model.CapabilityCompletion, model.CapabilityDecision); err != nil {
t.Fatalf("%s serving capabilities: %v", name, err)
}
modelfile = shown.Modelfile
if name == "roundtrip" {
modelfile = "FROM roundtrip\n"
}
}
listed, err := listModels(t.Context())
if err != nil {
t.Fatal(err)
}
for _, m := range listed {
want := []model.Capability{model.CapabilityDecision}
if m.Name == "base:latest" {
want = []model.Capability{model.CapabilityCompletion, model.CapabilityVision}
}
if !slices.Equal(m.Capabilities, want) {
t.Fatalf("list %s capabilities = %v, want %v", m.Name, m.Capabilities, want)
}
}
})
}
}
func TestCreateRejectsUnknownCapability(t *testing.T) {
gin.SetMode(gin.TestMode)
var s Server
for _, capability := range []string{"", "system-one", "decision tools"} {
w := createRequest(t, s.CreateHandler, api.CreateRequest{
Model: "invalid", From: "missing", Capabilities: []string{capability}, Stream: &stream,
})
if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "unknown capability") {
t.Fatalf("capability %q: %d %s", capability, w.Code, w.Body)
}
}
}