825 lines
20 KiB
Go
825 lines
20 KiB
Go
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"cmp"
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ollama/ollama/api"
|
|
"github.com/ollama/ollama/manifest"
|
|
"github.com/ollama/ollama/model/parsers"
|
|
ollamatemplate "github.com/ollama/ollama/template"
|
|
"github.com/ollama/ollama/thinking"
|
|
"github.com/ollama/ollama/types/model"
|
|
)
|
|
|
|
type modelListSummary struct {
|
|
Model string
|
|
Name string
|
|
RemoteModel string
|
|
RemoteHost string
|
|
Size int64
|
|
Digest string
|
|
ModifiedAt time.Time
|
|
Details api.ModelDetails
|
|
Capabilities []model.Capability
|
|
}
|
|
|
|
type modelListCacheEntry struct {
|
|
Digest string
|
|
Summary modelListSummary
|
|
}
|
|
|
|
type modelListCache struct {
|
|
mu sync.RWMutex
|
|
|
|
entries map[string]modelListCacheEntry
|
|
|
|
once sync.Once
|
|
readyOnce sync.Once
|
|
ready chan struct{}
|
|
hydrateErr error
|
|
build func(model.Name, *manifest.Manifest) (modelListSummary, error)
|
|
}
|
|
|
|
func newModelListCache() *modelListCache {
|
|
return &modelListCache{
|
|
entries: make(map[string]modelListCacheEntry),
|
|
ready: make(chan struct{}),
|
|
build: buildModelListSummary,
|
|
}
|
|
}
|
|
|
|
func (c *modelListCache) Start(ctx context.Context) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
|
|
c.once.Do(func() {
|
|
slog.Debug("starting model list cache")
|
|
go func() {
|
|
err := c.hydrate(ctx)
|
|
c.markReady(err)
|
|
if err != nil {
|
|
if ctx != nil && ctx.Err() != nil {
|
|
return
|
|
}
|
|
slog.Warn("model list cache hydration failed", "error", err)
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
|
|
func (c *modelListCache) hydrate(ctx context.Context) error {
|
|
start := time.Now()
|
|
|
|
manifests, err := manifest.Manifests(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var hydrated, failed int
|
|
for name, mf := range manifests {
|
|
if ctx != nil {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
summary, err := c.build(name, mf)
|
|
if err != nil {
|
|
failed++
|
|
slog.Warn("failed to hydrate model list cache", "model", name.String(), "error", err)
|
|
continue
|
|
}
|
|
|
|
c.set(name, mf.Digest(), summary)
|
|
hydrated++
|
|
}
|
|
|
|
slog.Info("model list cache hydration complete", "models", hydrated, "failures", failed, "elapsed", time.Since(start))
|
|
return nil
|
|
}
|
|
|
|
func (c *modelListCache) markReady(err error) {
|
|
c.mu.Lock()
|
|
c.hydrateErr = err
|
|
c.mu.Unlock()
|
|
|
|
c.readyOnce.Do(func() {
|
|
close(c.ready)
|
|
})
|
|
}
|
|
|
|
func (c *modelListCache) Wait(ctx context.Context) error {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
select {
|
|
case <-c.ready:
|
|
c.mu.RLock()
|
|
err := c.hydrateErr
|
|
c.mu.RUnlock()
|
|
return err
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (c *modelListCache) List(ctx context.Context) ([]api.ListModelResponse, error) {
|
|
if err := c.Wait(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.syncManifests(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.mu.RLock()
|
|
models := make([]api.ListModelResponse, 0, len(c.entries))
|
|
for _, entry := range c.entries {
|
|
models = append(models, entry.Summary.ListModelResponse())
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
sortListModelResponses(models)
|
|
return models, nil
|
|
}
|
|
|
|
func (c *modelListCache) syncManifests(ctx context.Context) error {
|
|
manifests, err := manifest.Manifests(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.mu.RLock()
|
|
current := make(map[string]string, len(c.entries))
|
|
for name, entry := range c.entries {
|
|
current[name] = entry.Digest
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
type update struct {
|
|
name model.Name
|
|
digest string
|
|
summary modelListSummary
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(manifests))
|
|
stale := make(map[string]struct{})
|
|
var updates []update
|
|
for name, mf := range manifests {
|
|
if ctx != nil {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
key := name.String()
|
|
digest := mf.Digest()
|
|
seen[key] = struct{}{}
|
|
if current[key] == digest {
|
|
continue
|
|
}
|
|
|
|
summary, err := c.build(name, mf)
|
|
if err != nil {
|
|
slog.Warn("failed to refresh model list cache", "model", key, "error", err)
|
|
if _, ok := current[key]; ok {
|
|
stale[key] = struct{}{}
|
|
}
|
|
continue
|
|
}
|
|
updates = append(updates, update{name: name, digest: digest, summary: summary})
|
|
}
|
|
|
|
c.mu.Lock()
|
|
for name := range c.entries {
|
|
if _, ok := seen[name]; !ok {
|
|
delete(c.entries, name)
|
|
continue
|
|
}
|
|
if _, ok := stale[name]; ok {
|
|
delete(c.entries, name)
|
|
}
|
|
}
|
|
for _, update := range updates {
|
|
c.entries[update.name.String()] = modelListCacheEntry{
|
|
Digest: update.digest,
|
|
Summary: cloneModelListSummary(update.summary),
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *modelListCache) RefreshModel(name model.Name) error {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
|
|
if !name.IsFullyQualified() {
|
|
var err error
|
|
name, err = getExistingName(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
mf, err := manifest.ParseNamedManifest(name)
|
|
if err != nil {
|
|
c.DeleteModel(name)
|
|
return err
|
|
}
|
|
|
|
summary, err := c.build(name, mf)
|
|
if err != nil {
|
|
c.DeleteModel(name)
|
|
return err
|
|
}
|
|
|
|
c.set(name, mf.Digest(), summary)
|
|
return nil
|
|
}
|
|
|
|
func (c *modelListCache) DeleteModel(name model.Name) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
|
|
c.mu.Lock()
|
|
delete(c.entries, name.String())
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *modelListCache) Get(name model.Name) (modelListSummary, bool) {
|
|
if c == nil {
|
|
return modelListSummary{}, false
|
|
}
|
|
|
|
if !name.IsFullyQualified() {
|
|
if existing, err := getExistingName(name); err == nil {
|
|
name = existing
|
|
}
|
|
}
|
|
|
|
c.mu.RLock()
|
|
entry, ok := c.entries[name.String()]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
return modelListSummary{}, false
|
|
}
|
|
|
|
return cloneModelListSummary(entry.Summary), true
|
|
}
|
|
|
|
func (c *modelListCache) Len() int {
|
|
if c == nil {
|
|
return 0
|
|
}
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return len(c.entries)
|
|
}
|
|
|
|
func (c *modelListCache) set(name model.Name, digest string, summary modelListSummary) {
|
|
c.mu.Lock()
|
|
c.entries[name.String()] = modelListCacheEntry{
|
|
Digest: digest,
|
|
Summary: cloneModelListSummary(summary),
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func buildModelListSummary(name model.Name, mf *manifest.Manifest) (modelListSummary, error) {
|
|
cfg, err := readModelListConfig(mf)
|
|
if err != nil {
|
|
return modelListSummary{}, err
|
|
}
|
|
|
|
var modified time.Time
|
|
if fi := mf.FileInfo(); fi != nil {
|
|
modified = fi.ModTime()
|
|
}
|
|
|
|
summary := modelListSummary{
|
|
Model: name.DisplayShortest(),
|
|
Name: name.DisplayShortest(),
|
|
RemoteModel: cfg.RemoteModel,
|
|
RemoteHost: cfg.RemoteHost,
|
|
Size: mf.Size(),
|
|
Digest: mf.Digest(),
|
|
ModifiedAt: modified,
|
|
Details: api.ModelDetails{
|
|
Format: cfg.ModelFormat,
|
|
Family: cfg.ModelFamily,
|
|
Families: append([]string(nil), cfg.ModelFamilies...),
|
|
ParameterSize: cfg.ModelType,
|
|
QuantizationLevel: cfg.FileType,
|
|
ContextLength: cfg.ContextLen,
|
|
EmbeddingLength: cfg.EmbedLen,
|
|
},
|
|
}
|
|
|
|
modelPath, projectorCount, tmpl, err := readModelListLayers(mf, &summary)
|
|
if err != nil {
|
|
return modelListSummary{}, err
|
|
}
|
|
|
|
if cfg.RemoteHost == "" && cfg.RemoteModel == "" && modelPath != "" {
|
|
info, err := readModelListGGUF(modelPath)
|
|
if err != nil {
|
|
slog.Debug("failed to read gguf model metadata", "model", name.String(), "error", err)
|
|
} else {
|
|
summary.Capabilities = appendModelListCapabilities(summary.Capabilities, info.Capabilities...)
|
|
if summary.Details.ContextLength == 0 {
|
|
summary.Details.ContextLength = info.ContextLength
|
|
}
|
|
if summary.Details.EmbeddingLength == 0 {
|
|
summary.Details.EmbeddingLength = info.EmbeddingLength
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, c := range cfg.Capabilities {
|
|
summary.Capabilities = appendModelListCapability(summary.Capabilities, model.Capability(c))
|
|
}
|
|
|
|
builtinParser := parsers.ParserForName(cfg.Parser)
|
|
if tmpl != nil {
|
|
vars, err := tmpl.Vars()
|
|
if err != nil {
|
|
slog.Warn("model template contains errors", "model", name.String(), "error", err)
|
|
}
|
|
if slices.Contains(vars, "tools") || (builtinParser != nil && builtinParser.HasToolSupport()) {
|
|
summary.Capabilities = appendModelListCapability(summary.Capabilities, model.CapabilityTools)
|
|
}
|
|
if slices.Contains(vars, "suffix") {
|
|
summary.Capabilities = appendModelListCapability(summary.Capabilities, model.CapabilityInsert)
|
|
}
|
|
|
|
openingTag, closingTag := thinking.InferTags(tmpl.Template)
|
|
hasTags := openingTag != "" && closingTag != ""
|
|
isGptoss := slices.Contains([]string{"gptoss", "gpt-oss"}, cfg.ModelFamily)
|
|
if !slices.Contains(summary.Capabilities, model.CapabilityThinking) &&
|
|
(hasTags || isGptoss || (builtinParser != nil && builtinParser.HasThinkingSupport())) {
|
|
summary.Capabilities = appendModelListCapability(summary.Capabilities, model.CapabilityThinking)
|
|
}
|
|
}
|
|
|
|
if projectorCount > 0 {
|
|
summary.Capabilities = appendModelListCapability(summary.Capabilities, model.CapabilityVision)
|
|
}
|
|
|
|
if cfg.ModelFormat == "safetensors" && isGemma4Renderer(cfg.Renderer) {
|
|
summary.Capabilities = slices.DeleteFunc(summary.Capabilities, func(c model.Capability) bool {
|
|
return c == model.CapabilityVision || c == model.CapabilityAudio
|
|
})
|
|
}
|
|
|
|
return summary, nil
|
|
}
|
|
|
|
func readModelListConfig(mf *manifest.Manifest) (model.ConfigV2, error) {
|
|
var cfg model.ConfigV2
|
|
if mf == nil || mf.Config.Digest == "" {
|
|
return cfg, nil
|
|
}
|
|
|
|
f, err := mf.Config.Open()
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
defer f.Close()
|
|
|
|
if err := json.NewDecoder(f).Decode(&cfg); err != nil {
|
|
return cfg, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func readModelListLayers(mf *manifest.Manifest, summary *modelListSummary) (string, int, *ollamatemplate.Template, error) {
|
|
var modelPath string
|
|
var projectorCount int
|
|
tmpl := ollamatemplate.DefaultTemplate
|
|
|
|
for _, layer := range mf.Layers {
|
|
switch layer.MediaType {
|
|
case "application/vnd.ollama.image.model":
|
|
filename, err := manifest.BlobsPath(layer.Digest)
|
|
if err != nil {
|
|
return "", 0, nil, err
|
|
}
|
|
modelPath = filename
|
|
summary.Details.ParentModel = layer.From
|
|
case "application/vnd.ollama.image.projector":
|
|
projectorCount++
|
|
case "application/vnd.ollama.image.prompt",
|
|
"application/vnd.ollama.image.template":
|
|
filename, err := manifest.BlobsPath(layer.Digest)
|
|
if err != nil {
|
|
return "", 0, nil, err
|
|
}
|
|
bts, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return "", 0, nil, err
|
|
}
|
|
|
|
tmpl, err = ollamatemplate.Parse(string(bts))
|
|
if err != nil {
|
|
return "", 0, nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return modelPath, projectorCount, tmpl, nil
|
|
}
|
|
|
|
type modelListGGUF struct {
|
|
Capabilities []model.Capability
|
|
ContextLength int
|
|
EmbeddingLength int
|
|
}
|
|
|
|
const (
|
|
modelListGGUFMagicLE = 0x46554747
|
|
modelListGGUFMagicBE = 0x47475546
|
|
)
|
|
|
|
const (
|
|
modelListGGUFTypeUint8 uint32 = iota
|
|
modelListGGUFTypeInt8
|
|
modelListGGUFTypeUint16
|
|
modelListGGUFTypeInt16
|
|
modelListGGUFTypeUint32
|
|
modelListGGUFTypeInt32
|
|
modelListGGUFTypeFloat32
|
|
modelListGGUFTypeBool
|
|
modelListGGUFTypeString
|
|
modelListGGUFTypeArray
|
|
modelListGGUFTypeUint64
|
|
modelListGGUFTypeInt64
|
|
modelListGGUFTypeFloat64
|
|
)
|
|
|
|
// readModelListGGUF scans only the small GGUF header values launch needs
|
|
// and stops before tokenizer arrays. Using gguf.File.KeyValue for missing keys
|
|
// can otherwise advance through large arrays just to discover absence.
|
|
func readModelListGGUF(path string) (modelListGGUF, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
defer f.Close()
|
|
|
|
r := bufio.NewReaderSize(f, 32<<10)
|
|
var magic uint32
|
|
if err := binary.Read(r, binary.LittleEndian, &magic); err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
|
|
var byteOrder binary.ByteOrder = binary.LittleEndian
|
|
switch magic {
|
|
case modelListGGUFMagicLE:
|
|
case modelListGGUFMagicBE:
|
|
byteOrder = binary.BigEndian
|
|
default:
|
|
return modelListGGUF{}, fmt.Errorf("invalid file magic")
|
|
}
|
|
|
|
var version uint32
|
|
if err := binary.Read(r, byteOrder, &version); err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
|
|
var numKV uint64
|
|
switch version {
|
|
case 1:
|
|
var header struct {
|
|
NumTensor uint32
|
|
NumKV uint32
|
|
}
|
|
if err := binary.Read(r, byteOrder, &header); err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
numKV = uint64(header.NumKV)
|
|
default:
|
|
var header struct {
|
|
NumTensor uint64
|
|
NumKV uint64
|
|
}
|
|
if err := binary.Read(r, byteOrder, &header); err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
numKV = header.NumKV
|
|
}
|
|
|
|
info := modelListGGUF{}
|
|
var architecture string
|
|
var hasPoolingType bool
|
|
|
|
for range numKV {
|
|
key, err := readModelListGGUFString(r, byteOrder, version)
|
|
if err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
|
|
var valueType uint32
|
|
if err := binary.Read(r, byteOrder, &valueType); err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
|
|
if key == "general.architecture" {
|
|
value, err := readModelListGGUFStringValue(r, byteOrder, version, valueType)
|
|
if err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
architecture = value
|
|
continue
|
|
}
|
|
|
|
if architecture != "" && strings.HasPrefix(key, "tokenizer.") {
|
|
break
|
|
}
|
|
|
|
if architecture != "" && strings.HasPrefix(key, architecture+".") {
|
|
switch strings.TrimPrefix(key, architecture+".") {
|
|
case "pooling_type":
|
|
hasPoolingType = true
|
|
case "vision.block_count":
|
|
info.Capabilities = appendModelListCapability(info.Capabilities, model.CapabilityVision)
|
|
case "audio.block_count":
|
|
info.Capabilities = appendModelListCapability(info.Capabilities, model.CapabilityAudio)
|
|
case "context_length":
|
|
value, err := readModelListGGUFIntValue(r, byteOrder, version, valueType)
|
|
if err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
info.ContextLength = value
|
|
continue
|
|
case "embedding_length":
|
|
value, err := readModelListGGUFIntValue(r, byteOrder, version, valueType)
|
|
if err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
info.EmbeddingLength = value
|
|
continue
|
|
}
|
|
}
|
|
|
|
if err := skipModelListGGUFValue(r, byteOrder, version, valueType); err != nil {
|
|
return modelListGGUF{}, err
|
|
}
|
|
}
|
|
|
|
if hasPoolingType {
|
|
info.Capabilities = appendModelListCapability(info.Capabilities, model.CapabilityEmbedding)
|
|
} else {
|
|
info.Capabilities = appendModelListCapability(info.Capabilities, model.CapabilityCompletion)
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func readModelListGGUFStringValue(r io.Reader, byteOrder binary.ByteOrder, version uint32, valueType uint32) (string, error) {
|
|
if valueType != modelListGGUFTypeString {
|
|
if err := skipModelListGGUFValue(r, byteOrder, version, valueType); err != nil {
|
|
return "", err
|
|
}
|
|
return "", fmt.Errorf("unexpected gguf string type %d", valueType)
|
|
}
|
|
return readModelListGGUFString(r, byteOrder, version)
|
|
}
|
|
|
|
func readModelListGGUFIntValue(r io.Reader, byteOrder binary.ByteOrder, version uint32, valueType uint32) (int, error) {
|
|
switch valueType {
|
|
case modelListGGUFTypeUint8:
|
|
var value uint8
|
|
if err := binary.Read(r, byteOrder, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(value), nil
|
|
case modelListGGUFTypeInt8:
|
|
var value int8
|
|
if err := binary.Read(r, byteOrder, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(value), nil
|
|
case modelListGGUFTypeUint16:
|
|
var value uint16
|
|
if err := binary.Read(r, byteOrder, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(value), nil
|
|
case modelListGGUFTypeInt16:
|
|
var value int16
|
|
if err := binary.Read(r, byteOrder, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(value), nil
|
|
case modelListGGUFTypeUint32:
|
|
var value uint32
|
|
if err := binary.Read(r, byteOrder, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(value), nil
|
|
case modelListGGUFTypeInt32:
|
|
var value int32
|
|
if err := binary.Read(r, byteOrder, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(value), nil
|
|
case modelListGGUFTypeUint64:
|
|
var value uint64
|
|
if err := binary.Read(r, byteOrder, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(value), nil
|
|
case modelListGGUFTypeInt64:
|
|
var value int64
|
|
if err := binary.Read(r, byteOrder, &value); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(value), nil
|
|
default:
|
|
if err := skipModelListGGUFValue(r, byteOrder, version, valueType); err != nil {
|
|
return 0, err
|
|
}
|
|
return 0, fmt.Errorf("unexpected gguf integer type %d", valueType)
|
|
}
|
|
}
|
|
|
|
func skipModelListGGUFValue(r io.Reader, byteOrder binary.ByteOrder, version uint32, valueType uint32) error {
|
|
switch valueType {
|
|
case modelListGGUFTypeUint8, modelListGGUFTypeInt8, modelListGGUFTypeBool:
|
|
return discardModelListGGUFBytes(r, 1)
|
|
case modelListGGUFTypeUint16, modelListGGUFTypeInt16:
|
|
return discardModelListGGUFBytes(r, 2)
|
|
case modelListGGUFTypeUint32, modelListGGUFTypeInt32, modelListGGUFTypeFloat32:
|
|
return discardModelListGGUFBytes(r, 4)
|
|
case modelListGGUFTypeUint64, modelListGGUFTypeInt64, modelListGGUFTypeFloat64:
|
|
return discardModelListGGUFBytes(r, 8)
|
|
case modelListGGUFTypeString:
|
|
return skipModelListGGUFString(r, byteOrder, version)
|
|
case modelListGGUFTypeArray:
|
|
var arrayType uint32
|
|
if err := binary.Read(r, byteOrder, &arrayType); err != nil {
|
|
return err
|
|
}
|
|
var count uint64
|
|
if err := binary.Read(r, byteOrder, &count); err != nil {
|
|
return err
|
|
}
|
|
return skipModelListGGUFArray(r, byteOrder, version, arrayType, count)
|
|
default:
|
|
return fmt.Errorf("unsupported gguf value type %d", valueType)
|
|
}
|
|
}
|
|
|
|
func skipModelListGGUFArray(r io.Reader, byteOrder binary.ByteOrder, version uint32, arrayType uint32, count uint64) error {
|
|
var size uint64
|
|
switch arrayType {
|
|
case modelListGGUFTypeUint8, modelListGGUFTypeInt8, modelListGGUFTypeBool:
|
|
size = 1
|
|
case modelListGGUFTypeUint16, modelListGGUFTypeInt16:
|
|
size = 2
|
|
case modelListGGUFTypeUint32, modelListGGUFTypeInt32, modelListGGUFTypeFloat32:
|
|
size = 4
|
|
case modelListGGUFTypeUint64, modelListGGUFTypeInt64, modelListGGUFTypeFloat64:
|
|
size = 8
|
|
case modelListGGUFTypeString:
|
|
for range count {
|
|
if err := skipModelListGGUFString(r, byteOrder, version); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported gguf array type %d", arrayType)
|
|
}
|
|
return discardModelListGGUFBytes(r, int64(count*size))
|
|
}
|
|
|
|
func readModelListGGUFString(r io.Reader, byteOrder binary.ByteOrder, version uint32) (string, error) {
|
|
var length uint64
|
|
if err := binary.Read(r, byteOrder, &length); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if length == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
bts := make([]byte, length)
|
|
if _, err := io.ReadFull(r, bts); err != nil {
|
|
return "", err
|
|
}
|
|
if version == 1 && bts[len(bts)-1] == 0 {
|
|
bts = bts[:len(bts)-1]
|
|
}
|
|
return string(bts), nil
|
|
}
|
|
|
|
func skipModelListGGUFString(r io.Reader, byteOrder binary.ByteOrder, version uint32) error {
|
|
var length uint64
|
|
if err := binary.Read(r, byteOrder, &length); err != nil {
|
|
return err
|
|
}
|
|
return discardModelListGGUFBytes(r, int64(length))
|
|
}
|
|
|
|
func discardModelListGGUFBytes(r io.Reader, n int64) error {
|
|
if n <= 0 {
|
|
return nil
|
|
}
|
|
_, err := io.CopyN(io.Discard, r, n)
|
|
return err
|
|
}
|
|
|
|
func appendModelListCapabilities(capabilities []model.Capability, values ...model.Capability) []model.Capability {
|
|
for _, capability := range values {
|
|
capabilities = appendModelListCapability(capabilities, capability)
|
|
}
|
|
return capabilities
|
|
}
|
|
|
|
func appendModelListCapability(capabilities []model.Capability, capability model.Capability) []model.Capability {
|
|
if capability == "" || slices.Contains(capabilities, capability) {
|
|
return capabilities
|
|
}
|
|
return append(capabilities, capability)
|
|
}
|
|
|
|
func cloneModelListSummary(summary modelListSummary) modelListSummary {
|
|
summary.Details.Families = append([]string(nil), summary.Details.Families...)
|
|
summary.Capabilities = append([]model.Capability(nil), summary.Capabilities...)
|
|
return summary
|
|
}
|
|
|
|
func (s modelListSummary) ListModelResponse() api.ListModelResponse {
|
|
resp := api.ListModelResponse{
|
|
Model: s.Model,
|
|
Name: s.Name,
|
|
RemoteModel: s.RemoteModel,
|
|
RemoteHost: s.RemoteHost,
|
|
Size: s.Size,
|
|
Digest: s.Digest,
|
|
ModifiedAt: s.ModifiedAt,
|
|
Details: api.ModelDetails{
|
|
ParentModel: s.Details.ParentModel,
|
|
Format: s.Details.Format,
|
|
Family: s.Details.Family,
|
|
Families: append([]string(nil), s.Details.Families...),
|
|
ParameterSize: s.Details.ParameterSize,
|
|
QuantizationLevel: s.Details.QuantizationLevel,
|
|
ContextLength: s.Details.ContextLength,
|
|
EmbeddingLength: s.Details.EmbeddingLength,
|
|
},
|
|
}
|
|
|
|
resp.Capabilities = append([]model.Capability(nil), s.Capabilities...)
|
|
|
|
return resp
|
|
}
|
|
|
|
func sortListModelResponses(models []api.ListModelResponse) {
|
|
slices.SortStableFunc(models, func(i, j api.ListModelResponse) int {
|
|
// Preserve the existing /api/tags order: most recently modified first.
|
|
return cmp.Compare(j.ModifiedAt.Unix(), i.ModifiedAt.Unix())
|
|
})
|
|
}
|
|
|
|
func (s *Server) refreshModelListCache(name model.Name) {
|
|
if s == nil || s.modelCaches == nil || s.modelCaches.modelList == nil {
|
|
return
|
|
}
|
|
|
|
if err := s.modelCaches.modelList.RefreshModel(name); err != nil {
|
|
slog.Warn("failed to refresh model list cache", "model", name.String(), "error", err)
|
|
}
|
|
}
|
|
|
|
func (s *Server) deleteModelListCache(name model.Name) {
|
|
if s == nil || s.modelCaches == nil || s.modelCaches.modelList == nil {
|
|
return
|
|
}
|
|
|
|
s.modelCaches.modelList.DeleteModel(name)
|
|
}
|