Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ vendor/
# model-distribution
pkg/distribution/bin/
/parallelget
/cli
3 changes: 2 additions & 1 deletion cmd/cli/commands/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/docker/model-runner/pkg/inference/scheduling"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -33,7 +34,7 @@ func newConfigureCmd() *cobra.Command {
argsBeforeDash)
}
}
opts.Model = args[0]
opts.Model = models.NormalizeModelName(args[0])
opts.RuntimeFlags = args[1:]
return nil
},
Expand Down
4 changes: 3 additions & 1 deletion cmd/cli/commands/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/cmd/cli/commands/formatter"
"github.com/docker/model-runner/cmd/cli/desktop"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -47,7 +48,8 @@ func newInspectCmd() *cobra.Command {
}

func inspectModel(args []string, openai bool, remote bool, desktopClient *desktop.Client) (string, error) {
modelName := args[0]
// Normalize model name to add default org and tag if missing
modelName := models.NormalizeModelName(args[0])
if openai {
model, err := desktopClient.InspectOpenAI(modelName)
if err != nil {
Expand Down
15 changes: 12 additions & 3 deletions cmd/cli/commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"os"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -89,12 +88,20 @@ func listModels(openai bool, backend string, desktopClient *desktop.Client, quie
}

if modelFilter != "" {
// Normalize the filter to match stored model names
normalizedFilter := dmrm.NormalizeModelName(modelFilter)
var filteredModels []dmrm.Model
for _, m := range models {
hasMatchingTag := false
for _, tag := range m.Tags {
if tag == normalizedFilter {
hasMatchingTag = true
break
}
// Also check without the tag part
modelName, _, _ := strings.Cut(tag, ":")
if slices.Contains([]string{modelName, tag + ":latest", tag}, modelFilter) {
filterName, _, _ := strings.Cut(normalizedFilter, ":")
if modelName == filterName {
hasMatchingTag = true
break
}
Expand Down Expand Up @@ -165,8 +172,10 @@ func appendRow(table *tablewriter.Table, tag string, model dmrm.Model) {
fmt.Fprintf(os.Stderr, "invalid model ID for model: %v\n", model)
return
}
// Strip default "ai/" prefix and ":latest" tag for display
displayTag := stripDefaultsFromModelName(tag)
table.Append([]string{
tag,
displayTag,
model.Config.Parameters,
model.Config.Quantization,
model.Config.Architecture,
Expand Down
3 changes: 3 additions & 0 deletions cmd/cli/commands/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func psTable(ps []desktop.BackendStatus) string {
modelName := status.ModelName
if strings.HasPrefix(modelName, "sha256:") {
modelName = modelName[7:19]
} else {
// Strip default "ai/" prefix and ":latest" tag for display
modelName = stripDefaultsFromModelName(modelName)
}
table.Append([]string{
modelName,
Expand Down
3 changes: 3 additions & 0 deletions cmd/cli/commands/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/cmd/cli/desktop"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -41,6 +42,8 @@ func newPullCmd() *cobra.Command {
}

func pullModel(cmd *cobra.Command, desktopClient *desktop.Client, model string, ignoreRuntimeMemoryCheck bool) error {
// Normalize model name to add default org and tag if missing
model = models.NormalizeModelName(model)
var progress func(string)
if isatty.IsTerminal(os.Stdout.Fd()) {
progress = TUIProgress
Expand Down
3 changes: 3 additions & 0 deletions cmd/cli/commands/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/cmd/cli/desktop"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -34,6 +35,8 @@ func newPushCmd() *cobra.Command {
}

func pushModel(cmd *cobra.Command, desktopClient *desktop.Client, model string) error {
// Normalize model name to add default org and tag if missing
model = models.NormalizeModelName(model)
response, progressShown, err := desktopClient.Push(model, TUIProgress)

// Add a newline before any output (success or error) if progress was shown.
Expand Down
8 changes: 7 additions & 1 deletion cmd/cli/commands/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/spf13/cobra"
)

Expand All @@ -27,7 +28,12 @@ func newRemoveCmd() *cobra.Command {
if _, err := ensureStandaloneRunnerAvailable(cmd.Context(), cmd); err != nil {
return fmt.Errorf("unable to initialize standalone model runner: %w", err)
}
response, err := desktopClient.Remove(args, force)
// Normalize model names to add default org and tag if missing
normalizedArgs := make([]string, len(args))
for i, arg := range args {
normalizedArgs[i] = models.NormalizeModelName(arg)
}
response, err := desktopClient.Remove(normalizedArgs, force)
if response != "" {
cmd.Print(response)
}
Expand Down
4 changes: 3 additions & 1 deletion cmd/cli/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/cmd/cli/desktop"
"github.com/docker/model-runner/cmd/cli/readline"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/fatih/color"
"github.com/spf13/cobra"
"golang.org/x/term"
Expand Down Expand Up @@ -561,7 +562,8 @@ func newRunCmd() *cobra.Command {
return err
}

model := args[0]
// Normalize model name to add default org and tag if missing
model := models.NormalizeModelName(args[0])
prompt := ""
argsLen := len(args)
if argsLen > 1 {
Expand Down
3 changes: 3 additions & 0 deletions cmd/cli/commands/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/cmd/cli/desktop"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -36,6 +37,8 @@ func newTagCmd() *cobra.Command {
}

func tagModel(cmd *cobra.Command, desktopClient *desktop.Client, source, target string) error {
// Normalize source model name to add default org and tag if missing
source = models.NormalizeModelName(source)
// Ensure tag is valid
tag, err := name.NewTag(target)
if err != nil {
Expand Down
10 changes: 8 additions & 2 deletions cmd/cli/commands/unload.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/docker/model-runner/cmd/cli/commands/completion"
"github.com/docker/model-runner/cmd/cli/desktop"
"github.com/docker/model-runner/pkg/inference/models"
"github.com/spf13/cobra"
)

Expand All @@ -16,8 +17,13 @@ func newUnloadCmd() *cobra.Command {
c := &cobra.Command{
Use: "unload " + cmdArgs,
Short: "Unload running models",
RunE: func(cmd *cobra.Command, models []string) error {
unloadResp, err := desktopClient.Unload(desktop.UnloadRequest{All: all, Backend: backend, Models: models})
RunE: func(cmd *cobra.Command, modelArgs []string) error {
// Normalize model names
normalizedModels := make([]string, len(modelArgs))
for i, model := range modelArgs {
normalizedModels[i] = models.NormalizeModelName(model)
}
unloadResp, err := desktopClient.Unload(desktop.UnloadRequest{All: all, Backend: backend, Models: normalizedModels})
if err != nil {
err = handleClientError(err, "Failed to unload models")
return handleNotRunningError(err)
Expand Down
27 changes: 27 additions & 0 deletions cmd/cli/commands/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (
"github.com/pkg/errors"
)

const (
defaultOrg = "ai"
defaultTag = "latest"
)

const (
enableViaCLI = "Enable Docker Model Runner via the CLI → docker desktop enable model-runner"
enableViaGUI = "Enable Docker Model Runner via the GUI → Go to Settings->AI->Enable Docker Model Runner"
Expand All @@ -32,3 +37,25 @@ func handleNotRunningError(err error) error {
}
return err
}

// stripDefaultsFromModelName removes the default "ai/" prefix and ":latest" tag for display.
// Examples:
// - "ai/gemma3:latest" -> "gemma3"
// - "ai/gemma3:v1" -> "ai/gemma3:v1"
// - "myorg/gemma3:latest" -> "myorg/gemma3"
// - "gemma3:latest" -> "gemma3"
// - "hf.co/bartowski/model:latest" -> "hf.co/bartowski/model"
func stripDefaultsFromModelName(model string) string {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Maybe this should be implemented in the model manager along with NormalizeModelName? Could be called ExtractModelBaseName().

Copy link
Contributor Author

@ericcurtin ericcurtin Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good consideration, I thought of that at one point, might do it in a follow on PR. The reason it's not is one has wider use than the other. But I get your point they are similar, one does the opposite of the other.

I actually think it's a pity we didn't go lowercase for our dmr quantization sometimes, the :tag . We actually have code here that converts everything to lowercase for huggingface and it's generally what ollama does also.

// Check if model has ai/ prefix without tag (implicitly :latest) - strip just ai/
if strings.HasPrefix(model, defaultOrg+"/") {
model = strings.TrimPrefix(model, defaultOrg+"/")
}

// Check if model has :latest but no slash (no org specified) - strip :latest
if strings.HasSuffix(model, ":"+defaultTag) {
model = strings.TrimSuffix(model, ":"+defaultTag)
}

// For other cases (ai/ with custom tag, custom org with :latest, etc.), keep as-is
return model
}
148 changes: 148 additions & 0 deletions cmd/cli/commands/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package commands

import (
"testing"

"github.com/docker/model-runner/pkg/inference/models"
)

func TestNormalizeModelName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple model name",
input: "gemma3",
expected: "ai/gemma3:latest",
},
{
name: "model name with tag",
input: "gemma3:v1",
expected: "ai/gemma3:v1",
},
{
name: "model name with org",
input: "myorg/gemma3",
expected: "myorg/gemma3:latest",
},
{
name: "model name with org and tag",
input: "myorg/gemma3:v1",
expected: "myorg/gemma3:v1",
},
{
name: "fully qualified model name",
input: "ai/gemma3:latest",
expected: "ai/gemma3:latest",
},
{
name: "huggingface model",
input: "hf.co/bartowski/model",
expected: "hf.co/bartowski/model:latest",
},
{
name: "huggingface model with tag",
input: "hf.co/bartowski/model:Q4_K_S",
expected: "hf.co/bartowski/model:q4_k_s",
},
{
name: "registry with model",
input: "docker.io/library/model",
expected: "docker.io/library/model:latest",
},
{
name: "registry with model and tag",
input: "docker.io/library/model:v1",
expected: "docker.io/library/model:v1",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "ai prefix already present",
input: "ai/gemma3",
expected: "ai/gemma3:latest",
},
{
name: "model name with latest tag already",
input: "gemma3:latest",
expected: "ai/gemma3:latest",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := models.NormalizeModelName(tt.input)
if result != tt.expected {
t.Errorf("NormalizeModelName(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

func TestStripDefaultsFromModelName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "ai prefix and latest tag",
input: "ai/gemma3:latest",
expected: "gemma3",
},
{
name: "ai prefix with custom tag",
input: "ai/gemma3:v1",
expected: "gemma3:v1",
},
{
name: "custom org with latest tag",
input: "myorg/gemma3:latest",
expected: "myorg/gemma3",
},
{
name: "simple model name with latest",
input: "gemma3:latest",
expected: "gemma3",
},
{
name: "simple model name without tag",
input: "gemma3",
expected: "gemma3",
},
{
name: "ai prefix without tag",
input: "ai/gemma3",
expected: "gemma3",
},
{
name: "huggingface model with latest",
input: "hf.co/bartowski/model:latest",
expected: "hf.co/bartowski/model",
},
{
name: "huggingface model with custom tag",
input: "hf.co/bartowski/model:Q4_K_S",
expected: "hf.co/bartowski/model:Q4_K_S",
},
{
name: "empty string",
input: "",
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := stripDefaultsFromModelName(tt.input)
if result != tt.expected {
t.Errorf("stripDefaultsFromModelName(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
Loading
Loading