@@ -138,10 +138,132 @@ func NewClient(opts ...Option) (*Client, error) {
138138 }, nil
139139}
140140
141+ // normalizeModelName adds the default organization prefix (ai/) and tag (:latest) if missing.
142+ // It also converts Hugging Face model names to lowercase and resolves IDs to full IDs.
143+ // This is a private method used internally by the Client.
144+ func (c * Client ) normalizeModelName (model string ) string {
145+ const (
146+ defaultOrg = "ai"
147+ defaultTag = "latest"
148+ )
149+
150+ model = strings .TrimSpace (model )
151+
152+ // If the model is empty, return as-is
153+ if model == "" {
154+ return model
155+ }
156+
157+ // If it looks like an ID or digest, try to resolve it to full ID
158+ if c .looksLikeID (model ) || c .looksLikeDigest (model ) {
159+ if fullID := c .resolveID (model ); fullID != "" {
160+ return fullID
161+ }
162+ // If not found, return as-is
163+ return model
164+ }
165+
166+ // Normalize HuggingFace model names (lowercase path)
167+ if strings .HasPrefix (model , "hf.co/" ) {
168+ // Replace hf.co with huggingface.co to avoid losing the Authorization header on redirect.
169+ model = "huggingface.co" + strings .ToLower (strings .TrimPrefix (model , "hf.co" ))
170+ }
171+
172+ // Check if model contains a registry (domain with dot before first slash)
173+ firstSlash := strings .Index (model , "/" )
174+ if firstSlash > 0 && strings .Contains (model [:firstSlash ], "." ) {
175+ // Has a registry, just ensure tag
176+ if ! strings .Contains (model , ":" ) {
177+ return model + ":" + defaultTag
178+ }
179+ return model
180+ }
181+
182+ // Split by colon to check for tag
183+ parts := strings .SplitN (model , ":" , 2 )
184+ nameWithOrg := parts [0 ]
185+ tag := defaultTag
186+ if len (parts ) == 2 && parts [1 ] != "" {
187+ tag = parts [1 ]
188+ }
189+
190+ // If name doesn't contain a slash, add the default org
191+ if ! strings .Contains (nameWithOrg , "/" ) {
192+ nameWithOrg = defaultOrg + "/" + nameWithOrg
193+ }
194+
195+ return nameWithOrg + ":" + tag
196+ }
197+
198+ // looksLikeID returns true for short & long hex IDs (12 or 64 chars)
199+ func (c * Client ) looksLikeID (s string ) bool {
200+ n := len (s )
201+ if n != 12 && n != 64 {
202+ return false
203+ }
204+ for i := 0 ; i < n ; i ++ {
205+ ch := s [i ]
206+ if ! ((ch >= '0' && ch <= '9' ) || (ch >= 'a' && ch <= 'f' )) {
207+ return false
208+ }
209+ }
210+ return true
211+ }
212+
213+ // looksLikeDigest returns true for e.g. "sha256:<64-hex>"
214+ func (c * Client ) looksLikeDigest (s string ) bool {
215+ const prefix = "sha256:"
216+ if ! strings .HasPrefix (s , prefix ) {
217+ return false
218+ }
219+ hashPart := s [len (prefix ):]
220+ // SHA256 digests must be exactly 64 hex characters
221+ if len (hashPart ) != 64 {
222+ return false
223+ }
224+ for i := 0 ; i < 64 ; i ++ {
225+ ch := hashPart [i ]
226+ if ! ((ch >= '0' && ch <= '9' ) || (ch >= 'a' && ch <= 'f' )) {
227+ return false
228+ }
229+ }
230+ return true
231+ }
232+
233+ // resolveID attempts to resolve a short ID or digest to a full model ID
234+ // by checking all models in the store. Returns empty string if not found.
235+ func (c * Client ) resolveID (id string ) string {
236+ models , err := c .ListModels ()
237+ if err != nil {
238+ return ""
239+ }
240+
241+ for _ , m := range models {
242+ fullID , err := m .ID ()
243+ if err != nil {
244+ continue
245+ }
246+
247+ // Check short ID (12 chars) - match against the hex part after "sha256:"
248+ if len (id ) == 12 && strings .HasPrefix (fullID , "sha256:" ) {
249+ if len (fullID ) >= 19 && fullID [7 :19 ] == id {
250+ return fullID
251+ }
252+ }
253+
254+ // Check full ID match (with or without sha256: prefix)
255+ if fullID == id || strings .TrimPrefix (fullID , "sha256:" ) == id {
256+ return fullID
257+ }
258+ }
259+
260+ return ""
261+ }
262+
141263// PullModel pulls a model from a registry and returns the local file path
142264func (c * Client ) PullModel (ctx context.Context , reference string , progressWriter io.Writer , bearerToken ... string ) error {
143265 // Normalize the model reference
144- reference = NormalizeModelName (reference )
266+ reference = c . normalizeModelName (reference )
145267 c .log .Infoln ("Starting model pull:" , utils .SanitizeForLog (reference ))
146268
147269 // Use the client's registry, or create a temporary one if bearer token is provided
@@ -327,7 +449,7 @@ func (c *Client) ListModels() ([]types.Model, error) {
327449// GetModel returns a model by reference
328450func (c * Client ) GetModel (reference string ) (types.Model , error ) {
329451 c .log .Infoln ("Getting model by reference:" , utils .SanitizeForLog (reference ))
330- normalizedRef := NormalizeModelName (reference )
452+ normalizedRef := c . normalizeModelName (reference )
331453 model , err := c .store .Read (normalizedRef )
332454 if err != nil {
333455 c .log .Errorln ("Failed to get model:" , err , "reference:" , utils .SanitizeForLog (reference ))
@@ -340,7 +462,7 @@ func (c *Client) GetModel(reference string) (types.Model, error) {
340462// IsModelInStore checks if a model with the given reference is in the local store
341463func (c * Client ) IsModelInStore (reference string ) (bool , error ) {
342464 c .log .Infoln ("Checking model by reference:" , utils .SanitizeForLog (reference ))
343- normalizedRef := NormalizeModelName (reference )
465+ normalizedRef := c . normalizeModelName (reference )
344466 if _ , err := c .store .Read (normalizedRef ); errors .Is (err , ErrModelNotFound ) {
345467 return false , nil
346468 } else if err != nil {
@@ -358,7 +480,7 @@ type DeleteModelResponse []DeleteModelAction
358480
359481// DeleteModel deletes a model
360482func (c * Client ) DeleteModel (reference string , force bool ) (* DeleteModelResponse , error ) {
361- normalizedRef := NormalizeModelName (reference )
483+ normalizedRef := c . normalizeModelName (reference )
362484 mdl , err := c .store .Read (normalizedRef )
363485 if err != nil {
364486 return & DeleteModelResponse {}, err
@@ -371,13 +493,13 @@ func (c *Client) DeleteModel(reference string, force bool) (*DeleteModelResponse
371493 // Check if this is a digest reference (contains @)
372494 // Digest references like "name@sha256:..." should be treated as ID references, not tags
373495 isDigestReference := strings .Contains (reference , "@" )
374- isTag := id != reference && ! isDigestReference
496+ isTag := id != normalizedRef && ! isDigestReference
375497
376498 resp := DeleteModelResponse {}
377499
378500 if isTag {
379501 c .log .Infoln ("Untagging model:" , reference )
380- tags , err := c .store .RemoveTags ([]string {reference })
502+ tags , err := c .store .RemoveTags ([]string {normalizedRef })
381503 if err != nil {
382504 c .log .Errorln ("Failed to untag model:" , err , "tag:" , reference )
383505 return & DeleteModelResponse {}, fmt .Errorf ("untagging model: %w" , err )
@@ -415,8 +537,9 @@ func (c *Client) DeleteModel(reference string, force bool) (*DeleteModelResponse
415537// Tag adds a tag to a model
416538func (c * Client ) Tag (source string , target string ) error {
417539 c .log .Infoln ("Tagging model, source:" , source , "target:" , utils .SanitizeForLog (target ))
418- normalizedRef := NormalizeModelName (source )
419- return c .store .AddTags (normalizedRef , []string {target })
540+ normalizedSource := c .normalizeModelName (source )
541+ normalizedTarget := c .normalizeModelName (target )
542+ return c .store .AddTags (normalizedSource , []string {normalizedTarget })
420543}
421544
422545// PushModel pushes a tagged model from the content store to the registry.
@@ -428,7 +551,7 @@ func (c *Client) PushModel(ctx context.Context, tag string, progressWriter io.Wr
428551 }
429552
430553 // Get the model from the store
431- normalizedRef := NormalizeModelName (tag )
554+ normalizedRef := c . normalizeModelName (tag )
432555 mdl , err := c .store .Read (normalizedRef )
433556 if err != nil {
434557 return fmt .Errorf ("reading model: %w" , err )
@@ -471,7 +594,7 @@ func (c *Client) ResetStore() error {
471594
472595// GetBundle returns a types.Bundle containing the model, creating one as necessary
473596func (c * Client ) GetBundle (ref string ) (types.ModelBundle , error ) {
474- normalizedRef := NormalizeModelName (ref )
597+ normalizedRef := c . normalizeModelName (ref )
475598 return c .store .BundleForModel (normalizedRef )
476599}
477600
0 commit comments