Files
ollama/server/create.go
Jesse Gross 2e036e7cdf mlx, mlxrunner: move the MLX engine out of x/
The MLX runner is the only Go inference runner left and is no longer
experimental, so its packages leave x/. The bindings become a top-level
mlx package beside the carried patches in mlx/compat, mirroring how
llama/ holds the llama.cpp integration, and the runner becomes mlxrunner
with the architectures nested under the package they implement.
Subpackages move with their parent unless listed.

  x/mlxrunner/mlx            mlx
  x/internal/mlxthread       mlx/mlxthread
  x/internal/mlxthreadtest   mlx/mlxthread/mlxthreadtest
  x/internal/mlxtest         mlx/mlxtest
  x/quant                    mlx/quant
  mlx/compat/*.patch         mlx/compat/mlx-c   (MLX patches go in mlx/compat/mlx)
  x/mlxrunner                mlxrunner
  x/models/nn                mlxrunner/nn
  x/models/<arch>            mlxrunner/model/<arch>
  x/mlxrunner/imports.go     mlxrunner/model/architectures   (new package)
  x/create                   create
  x/safetensors              fs/safetensors
  x/tokenizer                mlxrunner/tokenizer

Every package keeps its name, so the Go changes are the import path
rewrites the moves force, and the CMake, Dockerfile, CI cache keys, drift
check and Darwin payload script follow the new paths. Four edits are not
paths: the runner's blank architecture imports become the package
mlxrunner/model/architectures, so the list to extend for a new model sits
beside the architecture directories; a depguard rule keeps the two test
harnesses out of non-test code, as the x/internal placement used to; the
CI change filter's two entries for the long-deleted x/imagegen/mlx now
name the bindings' CMake project and the carried patches, so a change to
either builds the payload; and the tokenizer parity test reads its
fixtures from its own testdata instead of walking out of x/.

x/server and x/imagegen/manifest stay for the next two commits.
2026-09-16 14:06:08 -07:00

946 lines
27 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server
import (
"bytes"
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"math"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"runtime/debug"
"slices"
"strings"
"github.com/gin-gonic/gin"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/create"
"github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/format"
"github.com/ollama/ollama/fs/gguf"
"github.com/ollama/ollama/manifest"
"github.com/ollama/ollama/mlx/quant"
"github.com/ollama/ollama/mlxrunner"
"github.com/ollama/ollama/types/errtypes"
"github.com/ollama/ollama/types/model"
)
var (
errNoFilesProvided = errors.New("no files provided to convert")
errAdaptersUnsupported = errors.New("LoRA adapters are no longer supported")
errOnlyGGUFSupported = errors.New("supplied file was not in GGUF format")
errUnknownType = errors.New("unknown type")
errNeitherFromOrFiles = errors.New("neither 'from' or 'files' was specified")
errFilePath = errors.New("file path must be relative")
errRemoteDraftUnsupported = errors.New("DRAFT cannot be used with remote models")
errSafetensorsFrom = errors.New("safetensors imports do not support FROM model overlays")
errInvalidSplitGGUF = errors.New("invalid split GGUF")
errMixedModelTypes = errors.New("mixed model file types")
errInvalidCreateInfo = errors.New("invalid create info")
)
const (
maxSafetensorsMetadataSize = 64 << 20
maxCreateFiles = 1024
)
func (s *Server) CreateHandler(c *gin.Context) {
config := new(model.ConfigV2)
var r api.CreateRequest
if err := c.ShouldBindJSON(&r); errors.Is(err, io.EOF) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "missing request body"})
return
} else if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if r.Parameters["typical_p"] != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": errTypicalPUnsupported.Error()})
return
}
config.Renderer = r.Renderer
config.Parser = r.Parser
config.Requires = r.Requires
if err := validateCreateFiles(r.Files); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validateCreateFiles(r.DraftFiles); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(r.Adapters) > 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": errAdaptersUnsupported.Error()})
return
}
if _, err := create.LicenseStrings(r.License); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if r.DraftQuantize != "" && len(r.DraftFiles) == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "--draft-quantize requires a DRAFT model"})
return
}
name := model.ParseName(cmp.Or(r.Model, r.Name))
if !name.IsValid() {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": errtypes.InvalidModelNameErrMsg})
return
}
name, err := getExistingName(name)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fileType, err := detectModelTypeFromFiles(r.Files)
if err != nil {
// Mixed types and blobs the client never uploaded are request errors;
// anything else is the server failing to read its own blob store.
status := http.StatusInternalServerError
if errors.Is(err, errMixedModelTypes) || errors.Is(err, fs.ErrNotExist) || errors.Is(err, manifest.ErrInvalidDigestFormat) {
status = http.StatusBadRequest
}
c.AbortWithStatusJSON(status, gin.H{"error": err.Error()})
return
}
if err := validateCreateOptions(r, fileType); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
reqCtx := c.Request.Context()
ch := make(chan any)
go func() {
send := func(resp any) bool {
select {
case ch <- resp:
return true
case <-reqCtx.Done():
return false
}
}
defer close(ch)
defer recoverCreatePanic(send)
fn := func(resp api.ProgressResponse) {
send(resp)
}
oldManifest, _ := manifest.ParseNamedManifest(name)
if fileType == "safetensors" {
if err := createSafetensorsModel(reqCtx, r, name, fn); err != nil {
send(createSafetensorsErrorResponse(err))
return
}
if err := pruneOldManifestLayers(oldManifest); err != nil {
send(gin.H{"error": err.Error()})
return
}
send(api.ProgressResponse{Status: "success"})
return
}
var baseLayers []*modelLayer
var err error
var remote bool
if r.From != "" {
slog.Debug("create model from model name", "from", r.From)
fromRef, err := parseAndValidateModelRef(r.From)
if err != nil {
send(gin.H{"error": errtypes.InvalidModelNameErrMsg, "status": http.StatusBadRequest})
return
}
fromName := fromRef.Name
remoteHost := r.RemoteHost
if fromRef.Source == modelSourceCloud && remoteHost == "" {
remoteHost = cloudProxyBaseURL
}
if remoteHost != "" {
ru, err := remoteURL(remoteHost)
if err != nil {
send(gin.H{"error": "bad remote", "status": http.StatusBadRequest})
return
}
config.RemoteModel = fromRef.Base
config.RemoteHost = ru
remote = true
} else {
ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel()
var baseConfig model.ConfigV2
baseLayers, baseConfig, err = parseFromModel(ctx, fromName, fn)
if err != nil {
send(gin.H{"error": err.Error()})
return
}
requestConfig := *config
*config = baseConfig
if requestConfig.Renderer != "" {
config.Renderer = requestConfig.Renderer
}
if requestConfig.Parser != "" {
config.Parser = requestConfig.Parser
}
if requestConfig.Requires != "" {
config.Requires = requestConfig.Requires
}
}
} else if r.Files != nil {
baseLayers, err = convertModelFromFiles(reqCtx, r.Files, fn)
if err != nil {
for _, badReq := range []error{errNoFilesProvided, errOnlyGGUFSupported, errUnknownType, errInvalidSplitGGUF, errMixedModelTypes, errAdaptersUnsupported} {
if errors.Is(err, badReq) {
send(gin.H{"error": err.Error(), "status": http.StatusBadRequest})
return
}
}
send(gin.H{"error": err.Error()})
return
}
} else {
send(gin.H{"error": errNeitherFromOrFiles.Error(), "status": http.StatusBadRequest})
return
}
if remote && len(r.DraftFiles) > 0 {
send(gin.H{"error": errRemoteDraftUnsupported.Error(), "status": http.StatusBadRequest})
return
}
var draftLayers []*modelLayer
if !remote && r.DraftFiles != nil {
draftLayers, err = convertDraftModelFromFiles(reqCtx, r.DraftFiles, fn)
if err != nil {
for _, badReq := range []error{errNoFilesProvided, errOnlyGGUFSupported, errUnknownType, errFilePath, errInvalidSplitGGUF, errMixedModelTypes, errAdaptersUnsupported} {
if errors.Is(err, badReq) {
send(gin.H{"error": err.Error(), "status": http.StatusBadRequest})
return
}
}
send(gin.H{"error": err.Error(), "status": http.StatusBadRequest})
return
}
}
if len(draftLayers) > 0 {
baseLayers = append(baseLayers, draftLayers...)
}
// Info is not currently exposed by Modelfiles, but allows overriding various
// config values.
if err := applyCreateInfo(config, r.Info); err != nil {
send(gin.H{"error": err.Error(), "status": http.StatusBadRequest})
return
}
if err := createModel(reqCtx, r, name, baseLayers, config, fn); err != nil {
if errors.Is(err, create.ErrBadTemplate) || errors.Is(err, create.ErrInvalidRequires) || errors.Is(err, create.ErrInvalidLicense) || errors.Is(err, errInvalidSplitGGUF) {
send(gin.H{"error": err.Error(), "status": http.StatusBadRequest})
return
}
send(gin.H{"error": err.Error()})
return
}
if err := pruneOldManifestLayers(oldManifest); err != nil {
send(gin.H{"error": err.Error()})
return
}
send(api.ProgressResponse{Status: "success"})
}()
if r.Stream != nil && !*r.Stream {
waitForStream(c, ch)
return
}
streamResponse(c, ch)
}
func pruneOldManifestLayers(oldManifest *manifest.Manifest) error {
if envconfig.NoPrune() || oldManifest == nil {
return nil
}
removed, err := oldManifest.RemoveLayers()
removeGGUFMetadata(removed...)
return err
}
func recoverCreatePanic(send func(any) bool) {
if r := recover(); r != nil {
slog.Error("panic in create background goroutine", "panic", r, "stack", string(debug.Stack()))
send(gin.H{"error": "internal server error"})
}
}
// createSafetensorsModel imports uploaded raw safetensors source files by
// staging them as a normal model directory and running the shared create
// pipeline on the server.
func createSafetensorsModel(ctx context.Context, r api.CreateRequest, name model.Name, fn func(resp api.ProgressResponse)) error {
if len(r.Files) == 0 {
return errNoFilesProvided
}
if r.From != "" {
return errSafetensorsFrom
}
if err := ctx.Err(); err != nil {
return err
}
// Validate Info before staging or writing blobs. writeSafetensorsManifest
// applies the same overrides to the inferred model config at commit time.
if err := applyCreateInfo(new(model.ConfigV2), r.Info); err != nil {
return fmt.Errorf("%w: %v", errInvalidCreateInfo, err)
}
modelDir, cleanup, err := stageSafetensorsSourceFiles(ctx, r.Files)
if err != nil {
return err
}
defer cleanup()
if err := ctx.Err(); err != nil {
return err
}
progressFn := func(status string) {
fn(api.ProgressResponse{Status: status})
}
store := create.ManifestBlobStore{}
var draftDir string
var draftCleanup func()
if len(r.DraftFiles) > 0 {
if err := ctx.Err(); err != nil {
return err
}
draftDir, draftCleanup, err = stageSafetensorsSourceFiles(ctx, r.DraftFiles)
if err != nil {
return err
}
defer draftCleanup()
}
return create.Create(ctx, name.String(), modelDir, create.PipelineOptions{
Quantize: cmp.Or(r.Quantize, r.Quantization),
Parser: r.Parser,
Renderer: r.Renderer,
Requires: r.Requires,
DraftDir: draftDir,
DraftQuantize: r.DraftQuantize,
}, store, writeSafetensorsManifest(r, draftDir, fn), progressFn)
}
func createSafetensorsErrorResponse(err error) gin.H {
if errors.Is(err, mlxrunner.ErrRuntimeUnavailable) {
slog.Warn("MLX runtime unavailable during safetensors create", "error", err)
return gin.H{"error": mlxrunner.ErrRuntimeUnavailable.Error(), "status": http.StatusServiceUnavailable}
}
status := http.StatusInternalServerError
for _, badReq := range []error{errNoFilesProvided, errFilePath, errSafetensorsFrom, errInvalidCreateInfo, manifest.ErrInvalidDigestFormat, create.ErrBadTemplate, create.ErrInvalidRequires, create.ErrInvalidLicense, create.ErrUnsupportedMLXArchitecture, os.ErrNotExist} {
if errors.Is(err, badReq) {
status = http.StatusBadRequest
break
}
}
return gin.H{"error": err.Error(), "status": status}
}
func writeSafetensorsManifest(r api.CreateRequest, draftDir string, fn func(resp api.ProgressResponse)) create.ManifestWriter {
next := create.NewSafetensorsManifestWriter(create.SafetensorsManifestOptions{
MinVersion: create.SafetensorsMinOllamaVersion,
DraftDir: draftDir,
Template: r.Template,
System: r.System,
License: r.License,
Parameters: r.Parameters,
Messages: r.Messages,
BeforeWriteManifest: func() { fn(api.ProgressResponse{Status: "writing manifest"}) },
})
return func(ctx context.Context, modelName string, info create.ManifestInfo) error {
if len(info.ModelConfig.Capabilities) == 0 {
info.ModelConfig.Capabilities = []string{"completion"}
}
if err := applyCreateInfo(&info.ModelConfig, r.Info); err != nil {
return fmt.Errorf("%w: %v", errInvalidCreateInfo, err)
}
return next(ctx, modelName, info)
}
}
func stageSafetensorsSourceFiles(ctx context.Context, files map[string]string) (string, func(), error) {
dir, err := os.MkdirTemp("", "ollama-create-safetensors-*")
if err != nil {
return "", nil, err
}
cleanup := func() {
if err := os.RemoveAll(dir); err != nil {
slog.Warn("failed to remove staged safetensors source", "dir", dir, "error", err)
}
}
for filePath, digest := range files {
if err := ctx.Err(); err != nil {
cleanup()
return "", nil, err
}
if err := validateCreateFilePath(filePath); err != nil {
cleanup()
return "", nil, err
}
blobPath, err := manifest.BlobsPath(digest)
if err != nil {
cleanup()
return "", nil, fmt.Errorf("invalid digest for %s: %w", filePath, err)
}
info, err := os.Stat(blobPath)
if err != nil {
cleanup()
return "", nil, fmt.Errorf("blob not found for %s (digest %s): %w", filePath, digest, err)
}
if !info.Mode().IsRegular() {
cleanup()
return "", nil, fmt.Errorf("blob for %s is not a regular file", filePath)
}
if isSafetensorsMetadataFile(filePath) && info.Size() > maxSafetensorsMetadataSize {
cleanup()
return "", nil, fmt.Errorf("metadata file %s is %d bytes, exceeds maximum %d", filePath, info.Size(), maxSafetensorsMetadataSize)
}
dst := filepath.Join(dir, filepath.FromSlash(filePath))
if err := linkOrCopyFile(ctx, blobPath, dst); err != nil {
cleanup()
return "", nil, fmt.Errorf("stage %s: %w", filePath, err)
}
}
return dir, cleanup, nil
}
func isSafetensorsMetadataFile(filePath string) bool {
switch path.Base(filePath) {
case "config.json", "generation_config.json", "model.safetensors.index.json", "tokenizer_config.json", "chat_template.jinja":
return true
default:
return false
}
}
func linkOrCopyFile(ctx context.Context, src, dst string) error {
if err := ctx.Err(); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
if err := os.Link(src, dst); err == nil {
return nil
}
if err := os.Symlink(src, dst); err == nil {
return nil
}
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
if err != nil {
return err
}
_, copyErr := io.Copy(out, create.ReaderWithContext(ctx, in))
closeErr := out.Close()
if copyErr != nil {
return copyErr
}
return closeErr
}
func applyCreateInfo(config *model.ConfigV2, info map[string]any) error {
if info == nil {
return nil
}
if caps, ok := info["capabilities"]; ok {
parsed, err := capabilitiesFromInfo(caps)
if err != nil {
return err
}
config.Capabilities = parsed
}
setStringFromInfo := func(k string, dst *string) error {
v, ok := info[k]
if !ok {
return nil
}
val, ok := v.(string)
if !ok {
return fmt.Errorf("info field %q must be a string", k)
}
*dst = val
return nil
}
setIntFromInfo := func(k string, dst *int) error {
v, ok := info[k]
if !ok {
return nil
}
val, ok := v.(float64)
if !ok {
return fmt.Errorf("info field %q must be a number", k)
}
if val < 0 || math.Trunc(val) != val || val > float64(maxCreateInfoInt()) {
return fmt.Errorf("info field %q must be a non-negative integer", k)
}
*dst = int(val)
return nil
}
if err := setStringFromInfo("model_family", &config.ModelFamily); err != nil {
return err
}
if _, ok := info["model_family"]; ok {
config.ModelFamilies = nil
if config.ModelFamily != "" {
config.ModelFamilies = []string{config.ModelFamily}
}
}
if err := setStringFromInfo("base_name", &config.BaseName); err != nil {
return err
}
if err := setStringFromInfo("quantization_level", &config.FileType); err != nil {
return err
}
if err := setStringFromInfo("parameter_size", &config.ModelType); err != nil {
return err
}
if err := setIntFromInfo("context_length", &config.ContextLen); err != nil {
return err
}
if err := setIntFromInfo("embedding_length", &config.EmbedLen); err != nil {
return err
}
return nil
}
func capabilitiesFromInfo(v any) ([]string, error) {
switch caps := v.(type) {
case []string:
return append([]string(nil), caps...), nil
case []any:
out := make([]string, len(caps))
for i, c := range caps {
str, ok := c.(string)
if !ok {
return nil, fmt.Errorf("info field %q element %d must be a string", "capabilities", i)
}
out[i] = str
}
return out, nil
default:
return nil, fmt.Errorf("info field %q must be an array of strings", "capabilities")
}
}
func remoteURL(raw string) (string, error) {
// Specialcase: user supplied only a path ("/foo/bar").
if strings.HasPrefix(raw, "/") {
return (&url.URL{
Scheme: "http",
Host: net.JoinHostPort("localhost", "11434"),
Path: path.Clean(raw),
}).String(), nil
}
if !strings.Contains(raw, "://") {
raw = "http://" + raw
}
if raw == "ollama.com" || raw == "http://ollama.com" {
raw = "https://ollama.com:443"
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse error: %w", err)
}
if u.Host == "" {
u.Host = "localhost"
}
hostPart, portPart, err := net.SplitHostPort(u.Host)
if err == nil {
u.Host = net.JoinHostPort(hostPart, portPart)
} else {
u.Host = net.JoinHostPort(u.Host, "11434")
}
if u.Path != "" {
u.Path = path.Clean(u.Path)
}
if u.Path == "/" {
u.Path = ""
}
return u.String(), nil
}
func convertModelFromFiles(ctx context.Context, files map[string]string, fn func(resp api.ProgressResponse)) ([]*modelLayer, error) {
return convertModelFromFilesWithMediaType(ctx, files, "", true, fn)
}
func convertDraftModelFromFiles(ctx context.Context, files map[string]string, fn func(resp api.ProgressResponse)) ([]*modelLayer, error) {
return convertModelFromFilesWithMediaType(ctx, files, manifest.MediaTypeImageDraft, false, fn)
}
func convertModelFromFilesWithMediaType(ctx context.Context, files map[string]string, mediaType string, detectTemplate bool, fn func(resp api.ProgressResponse)) ([]*modelLayer, error) {
modelType, err := detectModelTypeFromFiles(files)
if err != nil {
return nil, err
}
switch modelType {
case "safetensors":
return nil, errOnlyGGUFSupported
case "gguf":
if len(files) == 0 {
return nil, errNoFilesProvided
}
filePaths := make([]string, 0, len(files))
for filePath := range files {
filePaths = append(filePaths, filePath)
}
slices.Sort(filePaths)
splitCollector := newSplitGGUFCollector()
for _, filePath := range filePaths {
if err := ctx.Err(); err != nil {
return nil, err
}
layers, err := ggufLayersWithMediaType(files[filePath], filePath, mediaType, fn)
if err != nil {
return nil, err
}
for _, layer := range layers {
if err := splitCollector.Add(layer); err != nil {
return nil, err
}
}
}
allLayers, err := splitCollector.Layers()
if err != nil {
return nil, err
}
if detectTemplate {
return detectChatTemplate(allLayers)
}
return allLayers, nil
default:
return nil, errUnknownType
}
}
func validateCreateFiles(files map[string]string) error {
if len(files) > maxCreateFiles {
return fmt.Errorf("too many files: %d exceeds maximum %d", len(files), maxCreateFiles)
}
for filePath, digest := range files {
if err := validateCreateFilePath(filePath); err != nil {
return err
}
if digest == "" {
return manifest.ErrInvalidDigestFormat
}
if _, err := manifest.BlobsPath(digest); err != nil {
return fmt.Errorf("invalid digest for %s: %w", filePath, err)
}
}
return nil
}
func validateCreateFilePath(filePath string) error {
if filePath == "." || !fs.ValidPath(filePath) || strings.ContainsAny(filePath, `\:`) {
return fmt.Errorf("%w: %s", errFilePath, filePath)
}
return nil
}
func validateCreateOptions(r api.CreateRequest, modelType string) error {
quantize := cmp.Or(r.Quantize, r.Quantization)
if modelType == "gguf" || (modelType == "" && r.From != "") {
if quantize != "" {
return fmt.Errorf("create-time quantization is only supported for safetensors imports; quantize GGUF models with llama.cpp tools before importing")
}
if r.DraftQuantize != "" {
return fmt.Errorf("draft quantization during create is only supported for safetensors imports; quantize GGUF draft models with llama.cpp tools before importing")
}
return nil
}
if quantize != "" && quant.Canonical(quantize) == "" {
return fmt.Errorf("unsupported quantize type %q: supported types are int4, int8, nvfp4, mxfp4, mxfp8", quantize)
}
if r.DraftQuantize != "" && quant.Canonical(r.DraftQuantize) == "" {
return fmt.Errorf("unsupported draft quantize type %q: supported types are int4, int8, nvfp4, mxfp4, mxfp8", r.DraftQuantize)
}
return nil
}
func maxCreateInfoInt() int {
return int(^uint(0) >> 1)
}
func detectModelTypeFromFiles(files map[string]string) (string, error) {
filePaths := make([]string, 0, len(files))
for filePath := range files {
filePaths = append(filePaths, filePath)
}
slices.Sort(filePaths)
var modelType string
for _, filePath := range filePaths {
t, err := detectModelTypeFromFile(filePath, files[filePath])
if err != nil {
return "", err
}
if t == "" {
continue
}
if modelType != "" && modelType != t {
return "", fmt.Errorf("%w: found both %s and %s inputs", errMixedModelTypes, modelType, t)
}
modelType = t
}
return modelType, nil
}
func detectModelTypeFromFile(filePath, digest string) (string, error) {
if strings.HasSuffix(filePath, ".safetensors") {
return "safetensors", nil
}
if strings.HasSuffix(filePath, ".gguf") {
return "gguf", nil
}
// Try to detect GGUF files even when the source name has no extension.
blobPath, err := manifest.BlobsPath(digest)
if err != nil {
return "", fmt.Errorf("blob path for %s: %w", filePath, err)
}
f, err := os.Open(blobPath)
if errors.Is(err, fs.ErrNotExist) {
return "", fmt.Errorf("blob not found for %s (digest %s): %w", filePath, digest, err)
} else if err != nil {
return "", fmt.Errorf("read %s: %w", filePath, err)
}
defer f.Close()
buf := make([]byte, 4)
if _, err := io.ReadFull(f, buf); err != nil {
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
return "", fmt.Errorf("read %s: %w", filePath, err)
}
return "", nil
}
if bytes.Equal(buf, []byte("GGUF")) || bytes.Equal(buf, []byte("FUGG")) {
return "gguf", nil
}
return "", nil
}
func createModel(ctx context.Context, r api.CreateRequest, name model.Name, baseLayers []*modelLayer, config *model.ConfigV2, fn func(resp api.ProgressResponse)) (err error) {
if err := ctx.Err(); err != nil {
return err
}
if quantize := cmp.Or(r.Quantize, r.Quantization); quantize != "" {
return fmt.Errorf("create-time quantization is only supported for safetensors imports; quantize GGUF models with llama.cpp tools before importing")
}
if r.DraftQuantize != "" {
return fmt.Errorf("draft quantization during create is only supported for safetensors imports; quantize GGUF draft models with llama.cpp tools before importing")
}
var layers []manifest.Layer
hasSplitGGUF := false
for _, layer := range baseLayers {
if len(layer.splitLayers) > 0 {
hasSplitGGUF = true
layers = append(layers, layer.splitLayers...)
} else {
layers = append(layers, layer.Layer)
}
if layer.GGUF != nil {
switch layer.MediaType {
case "application/vnd.ollama.image.model":
config.ModelFormat = cmp.Or(config.ModelFormat, "gguf")
config.ModelFamily = cmp.Or(config.ModelFamily, layer.GGUF.Architecture())
config.ModelType = cmp.Or(config.ModelType, format.HumanNumber(layer.parameterCount))
config.FileType = cmp.Or(config.FileType, layer.GGUF.FileType().String())
architecture := layer.GGUF.Architecture()
if !slices.Contains(config.ModelFamilies, architecture) {
config.ModelFamilies = append(config.ModelFamilies, architecture)
}
// Auto-detect renderer, parser, and stop tokens from GGUF architecture.
if config.Renderer == "" || config.Parser == "" {
arch := layer.GGUF.Architecture()
switch arch {
case "gemma4":
config.Renderer = cmp.Or(config.Renderer, gemma4RendererLegacy)
config.Parser = cmp.Or(config.Parser, "gemma4")
if _, ok := r.Parameters["stop"]; !ok {
if r.Parameters == nil {
r.Parameters = make(map[string]any)
}
r.Parameters["stop"] = []string{"<turn|>"}
}
case "laguna":
config.Renderer = cmp.Or(config.Renderer, "laguna")
config.Parser = cmp.Or(config.Parser, "laguna")
case "nemotron_h", "nemotron_h_moe", "nemotron_h_omni":
config.Renderer = cmp.Or(config.Renderer, "nemotron-3-nano")
config.Parser = cmp.Or(config.Parser, "nemotron-3-nano")
}
}
case manifest.MediaTypeImageDraft:
config.Draft = &model.Draft{
ModelFormat: "gguf",
Architecture: layer.GGUF.Architecture(),
}
}
}
}
if hasSplitGGUF {
if config.Requires == "" {
config.Requires = splitGGUFMinOllamaVersion
}
}
layers, err = create.ApplyModelfileLayers(layers, create.ModelfileLayerOptions{
Template: r.Template,
System: r.System,
License: r.License,
Parameters: r.Parameters,
Messages: r.Messages,
})
if err != nil {
return err
}
configLayer, err := createConfigLayer(*config)
if err != nil {
return err
}
for _, layer := range layers {
if layer.Status != "" {
fn(api.ProgressResponse{Status: layer.Status})
}
}
fn(api.ProgressResponse{Status: "writing manifest"})
if err := ctx.Err(); err != nil {
return err
}
if err := manifest.WriteManifest(name, *configLayer, layers); err != nil {
return err
}
return nil
}
func ggufLayersWithMediaType(digest, sourceName, mediaType string, fn func(resp api.ProgressResponse)) ([]*modelLayer, error) {
var layers []*modelLayer
fn(api.ProgressResponse{Status: "parsing GGUF"})
blobPath, err := manifest.BlobsPath(digest)
if err != nil {
return nil, err
}
metadata, err := gguf.ReadFileMetadata(blobPath, 1)
if err != nil {
return nil, err
}
if metadata.Kind() == "adapter" {
return nil, fmt.Errorf("%w: %s is a LoRA adapter", errAdaptersUnsupported, sourceName)
}
if mediaType == "" {
mediaType = "application/vnd.ollama.image.model"
if isProjectorGGUF(metadata) {
mediaType = "application/vnd.ollama.image.projector"
}
}
layer, err := manifest.NewLayerFromLayer(digest, mediaType, sourceName)
if err != nil {
slog.Debug("could not create new layer from layer", "error", err)
return nil, err
}
layers = append(layers, &modelLayer{
Layer: layer,
GGUF: metadata,
parameterCount: metadata.ParameterCount(),
splitFile: sourceName,
})
return layers, nil
}
func isProjectorGGUF(metadata *gguf.Metadata) bool {
switch metadata.Kind() {
case "projector", "mmproj":
return true
}
// If a model has vision.block_count but not block_count, it is a standalone vision model.
if metadata.BlockCount() == 0 && metadata.Uint("vision.block_count") > 0 {
return true
}
return metadata.Architecture() == "clip" && metadata.BlockCount() == 0 &&
(metadata.Bool("has_vision_encoder") || metadata.Bool("has_audio_encoder"))
}
func createConfigLayer(config model.ConfigV2) (*manifest.Layer, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(config); err != nil {
return nil, err
}
layer, err := manifest.NewLayer(&b, "application/vnd.docker.container.image.v1+json")
if err != nil {
return nil, err
}
return &layer, nil
}