@@ -6,9 +6,11 @@ import (
66 "fmt"
77 "io"
88 "net/http"
9+ "os"
910 "slices"
1011 "strings"
1112
13+ "github.com/docker/model-runner/pkg/distribution/huggingface"
1214 "github.com/docker/model-runner/pkg/distribution/internal/progress"
1315 "github.com/docker/model-runner/pkg/distribution/internal/store"
1416 "github.com/docker/model-runner/pkg/distribution/registry"
@@ -162,10 +164,11 @@ func (c *Client) normalizeModelName(model string) string {
162164 return model
163165 }
164166
165- // Normalize HuggingFace model names (lowercase path)
167+ // Normalize HuggingFace model names
166168 if strings .HasPrefix (model , "hf.co/" ) {
167169 // Replace hf.co with huggingface.co to avoid losing the Authorization header on redirect.
168- model = "huggingface.co" + strings .ToLower (strings .TrimPrefix (model , "hf.co" ))
170+ // Note: We preserve case since HuggingFace's native API is case-sensitive
171+ model = "huggingface.co" + strings .TrimPrefix (model , "hf.co" )
169172 }
170173
171174 // Check if model contains a registry (domain with dot before first slash)
@@ -267,15 +270,22 @@ func (c *Client) PullModel(ctx context.Context, reference string, progressWriter
267270
268271 // Use the client's registry, or create a temporary one if bearer token is provided
269272 registryClient := c .registry
273+ var token string
270274 if len (bearerToken ) > 0 && bearerToken [0 ] != "" {
275+ token = bearerToken [0 ]
271276 // Create a temporary registry client with bearer token authentication
272- auth := & authn.Bearer {Token : bearerToken [ 0 ] }
277+ auth := & authn.Bearer {Token : token }
273278 registryClient = registry .FromClient (c .registry , registry .WithAuth (auth ))
274279 }
275280
276281 // First, fetch the remote model to get the manifest
277282 remoteModel , err := registryClient .Model (ctx , reference )
278283 if err != nil {
284+ // Check if this is a HuggingFace reference and the error indicates no OCI manifest
285+ if isHuggingFaceReference (reference ) && isNotOCIError (err ) {
286+ c .log .Infoln ("No OCI manifest found, attempting native HuggingFace pull" )
287+ return c .pullNativeHuggingFace (ctx , reference , progressWriter , token )
288+ }
279289 return fmt .Errorf ("reading model from registry: %w" , err )
280290 }
281291
@@ -637,3 +647,116 @@ func checkCompat(image types.ModelArtifact, log *logrus.Entry, reference string,
637647
638648 return nil
639649}
650+
651+ // isHuggingFaceReference checks if a reference is a HuggingFace model reference
652+ func isHuggingFaceReference (reference string ) bool {
653+ return strings .HasPrefix (reference , "huggingface.co/" )
654+ }
655+
656+ // isNotOCIError checks if the error indicates the model is not OCI-formatted
657+ // This happens when the HuggingFace repository doesn't have an OCI manifest
658+ func isNotOCIError (err error ) bool {
659+ if err == nil {
660+ return false
661+ }
662+
663+ // Check for registry errors indicating no manifest
664+ var regErr * registry.Error
665+ if errors .As (err , & regErr ) {
666+ if regErr .Code == "MANIFEST_UNKNOWN" || regErr .Code == "NAME_UNKNOWN" {
667+ return true
668+ }
669+ }
670+
671+ // Check for invalid reference error (e.g., uppercase letters not allowed in OCI)
672+ // This happens with HuggingFace model names like "Qwen/Qwen3-0.6B"
673+ if errors .Is (err , registry .ErrInvalidReference ) {
674+ return true
675+ }
676+
677+ // Also check error message for common patterns
678+ errStr := err .Error ()
679+ return strings .Contains (errStr , "MANIFEST_UNKNOWN" ) ||
680+ strings .Contains (errStr , "NAME_UNKNOWN" ) ||
681+ strings .Contains (errStr , "manifest unknown" ) ||
682+ // HuggingFace returns this error for non-GGUF repositories
683+ strings .Contains (errStr , "Repository is not GGUF" ) ||
684+ strings .Contains (errStr , "not compatible with llama.cpp" )
685+ }
686+
687+ // parseHFReference extracts repo and revision from a normalized HF reference
688+ // e.g., "huggingface.co/org/model:revision" -> ("org/model", "revision")
689+ // e.g., "huggingface.co/org/model:latest" -> ("org/model", "main")
690+ func parseHFReference (reference string ) (repo , revision string ) {
691+ // Remove registry prefix
692+ ref := strings .TrimPrefix (reference , "huggingface.co/" )
693+
694+ // Split by colon to get tag
695+ parts := strings .SplitN (ref , ":" , 2 )
696+ repo = parts [0 ]
697+
698+ revision = "main"
699+ if len (parts ) == 2 && parts [1 ] != "" && parts [1 ] != "latest" {
700+ revision = parts [1 ]
701+ }
702+
703+ return repo , revision
704+ }
705+
706+ // pullNativeHuggingFace pulls a native HuggingFace repository (non-OCI format)
707+ // This is used when the model is stored as raw files (safetensors) on HuggingFace Hub
708+ func (c * Client ) pullNativeHuggingFace (ctx context.Context , reference string , progressWriter io.Writer , token string ) error {
709+ repo , revision := parseHFReference (reference )
710+ c .log .Infof ("Pulling native HuggingFace model: repo=%s, revision=%s" , repo , revision )
711+
712+ // Create HuggingFace client
713+ hfOpts := []huggingface.ClientOption {
714+ huggingface .WithUserAgent (registry .DefaultUserAgent ),
715+ }
716+ if token != "" {
717+ hfOpts = append (hfOpts , huggingface .WithToken (token ))
718+ }
719+ hfClient := huggingface .NewClient (hfOpts ... )
720+
721+ // Create temp directory for downloads
722+ tempDir , err := os .MkdirTemp ("" , "hf-model-*" )
723+ if err != nil {
724+ return fmt .Errorf ("create temp dir: %w" , err )
725+ }
726+ defer os .RemoveAll (tempDir )
727+
728+ // Build model from HuggingFace repository
729+ model , err := huggingface .BuildModel (ctx , hfClient , repo , revision , tempDir , progressWriter )
730+ if err != nil {
731+ // Convert HuggingFace errors to registry errors for consistent handling
732+ var authErr * huggingface.AuthError
733+ var notFoundErr * huggingface.NotFoundError
734+ if errors .As (err , & authErr ) {
735+ return registry .ErrUnauthorized
736+ }
737+ if errors .As (err , & notFoundErr ) {
738+ return registry .ErrModelNotFound
739+ }
740+ if writeErr := progress .WriteError (progressWriter , fmt .Sprintf ("Error: %s" , err .Error ())); writeErr != nil {
741+ c .log .Warnf ("Failed to write error message: %v" , writeErr )
742+ }
743+ return fmt .Errorf ("build model from HuggingFace: %w" , err )
744+ }
745+
746+ // Write model to store
747+ // Lowercase the reference for storage since OCI tags don't allow uppercase
748+ storageTag := strings .ToLower (reference )
749+ c .log .Infof ("Writing model to store with tag: %s" , storageTag )
750+ if err := c .store .Write (model , []string {storageTag }, progressWriter ); err != nil {
751+ if writeErr := progress .WriteError (progressWriter , fmt .Sprintf ("Error: %s" , err .Error ())); writeErr != nil {
752+ c .log .Warnf ("Failed to write error message: %v" , writeErr )
753+ }
754+ return fmt .Errorf ("writing model to store: %w" , err )
755+ }
756+
757+ if err := progress .WriteSuccess (progressWriter , "Model pulled successfully" ); err != nil {
758+ c .log .Warnf ("Failed to write success message: %v" , err )
759+ }
760+
761+ return nil
762+ }
0 commit comments