Skip to content

Commit 1546dfc

Browse files
authored
Merge pull request #190 from docker/monorepo-model-cli
Consolidate model-cli into model-runner monorepo
2 parents 48bc7e2 + e23bc1e commit 1546dfc

File tree

96 files changed

+7307
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+7307
-2
lines changed

.dockerignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Version control
2-
.git/
32
.gitignore
43

54
# IDE and editor files
@@ -73,4 +72,4 @@ Dockerfile*
7372
.Spotlight-V100
7473
.Trashes
7574

76-
# Exclude nothing else; keep source code and necessary files for build
75+
# Exclude nothing else; keep source code and necessary files for build

.github/workflows/cli-build.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Build model-cli
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
paths:
7+
- 'cmd/cli/**'
8+
- '.github/workflows/cli-build.yml'
9+
pull_request:
10+
branches: [ "main" ]
11+
paths:
12+
- 'cmd/cli/**'
13+
- '.github/workflows/cli-build.yml'
14+
workflow_dispatch:
15+
inputs:
16+
branch:
17+
description: "Branch"
18+
required: true
19+
default: "main"
20+
21+
jobs:
22+
build:
23+
runs-on: macos-latest
24+
permissions:
25+
id-token: write
26+
contents: read
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: actions/setup-go@v5
30+
with:
31+
go-version-file: cmd/cli/go.mod
32+
cache: true
33+
cache-dependency-path: cmd/cli/go.sum
34+
- name: Build model-cli
35+
working-directory: cmd/cli
36+
run: |
37+
make release VERSION=${{ github.sha }}
38+
- uses: actions/upload-artifact@v4
39+
with:
40+
name: dist
41+
path: |
42+
cmd/cli/dist/
43+
retention-days: 2
44+
if-no-files-found: error

.github/workflows/cli-validate.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Validate model-cli
2+
3+
permissions:
4+
contents: read
5+
6+
concurrency:
7+
group: ${{ github.workflow }}-${{ github.ref }}
8+
cancel-in-progress: true
9+
10+
on:
11+
workflow_dispatch:
12+
push:
13+
branches:
14+
- 'main'
15+
- 'v[0-9]*'
16+
tags:
17+
- 'v*'
18+
paths:
19+
- 'cmd/cli/**'
20+
- '.github/workflows/cli-validate.yml'
21+
pull_request:
22+
paths:
23+
- 'cmd/cli/**'
24+
- '.github/workflows/cli-validate.yml'
25+
26+
jobs:
27+
prepare:
28+
runs-on: ubuntu-24.04
29+
outputs:
30+
targets: ${{ steps.generate.outputs.targets }}
31+
steps:
32+
-
33+
name: Checkout
34+
uses: actions/checkout@v4
35+
-
36+
name: List targets
37+
id: generate
38+
uses: docker/bake-action/subaction/list-targets@v6
39+
with:
40+
files: ./cmd/cli/docker-bake.hcl
41+
target: validate
42+
43+
validate:
44+
runs-on: ubuntu-24.04
45+
needs:
46+
- prepare
47+
strategy:
48+
fail-fast: false
49+
matrix:
50+
target: ${{ fromJson(needs.prepare.outputs.targets) }}
51+
steps:
52+
-
53+
name: Checkout
54+
uses: actions/checkout@v4
55+
-
56+
name: Set up Docker Buildx
57+
uses: docker/setup-buildx-action@v3
58+
with:
59+
buildkitd-flags: --debug
60+
-
61+
name: Validate
62+
uses: docker/bake-action@v6
63+
with:
64+
files: ./cmd/cli/docker-bake.hcl
65+
workdir: ./cmd/cli
66+
targets: ${{ matrix.target }}

cmd/cli/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
model-cli
2+
.idea/
3+
dist/

cmd/cli/Dockerfile

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# syntax=docker/dockerfile:1
2+
3+
ARG GO_VERSION=1.24
4+
ARG ALPINE_VERSION=3.21
5+
6+
ARG DOCS_FORMATS="md,yaml"
7+
8+
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
9+
RUN apk add --no-cache rsync git
10+
ENV CGO_ENABLED=0
11+
WORKDIR /src
12+
13+
FROM base AS docs-gen
14+
RUN --mount=target=. \
15+
--mount=target=/root/.cache,type=cache \
16+
go build -C cmd/cli -o /out/docsgen ./docs/generate.go
17+
18+
FROM base AS docs-build
19+
COPY --from=docs-gen /out/docsgen /usr/bin
20+
ENV DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND="model"
21+
ARG DOCS_FORMATS
22+
RUN --mount=target=/context \
23+
--mount=target=.,type=tmpfs <<EOT
24+
set -e
25+
rsync -a --exclude='models-store' /context/. .
26+
docsgen --formats "$DOCS_FORMATS" --source "cmd/cli/docs/reference"
27+
mkdir /out
28+
cp -r cmd/cli/docs/reference/* /out/
29+
EOT
30+
31+
FROM scratch AS docs-update
32+
COPY --from=docs-build /out /
33+
34+
FROM docs-build AS docs-validate
35+
RUN --mount=target=/context \
36+
--mount=target=.,type=tmpfs <<EOT
37+
set -e
38+
rsync -a --exclude='models-store' /context/. .
39+
git add -A
40+
rm -rf cmd/cli/docs/reference/*
41+
cp -rf /out/* ./cmd/cli/docs/reference/
42+
if [ -n "$(git status --porcelain -- docs/reference)" ]; then
43+
echo >&2 'ERROR: Docs result differs. Please update with "make docs"'
44+
git status --porcelain -- cmd/cli/docs/reference
45+
exit 1
46+
fi
47+
EOT
48+
49+
FROM base AS test
50+
RUN apk add --no-cache make gcc musl-dev
51+
WORKDIR /src
52+
RUN --mount=target=. \
53+
--mount=target=/root/.cache,type=cache \
54+
CGO_ENABLED=1 make unit-tests

cmd/cli/Makefile

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
.PHONY: all build clean link mock unit-tests docs
2+
3+
BINARY_NAME=model-cli
4+
5+
PLUGIN_DIR=$(HOME)/.docker/cli-plugins
6+
PLUGIN_NAME=docker-model
7+
8+
VERSION ?=
9+
10+
MACOS_MIN_VERSION := 14.0
11+
MACOS_MIN_VERSION_LDFLAG := -mmacosx-version-min=$(MACOS_MIN_VERSION)
12+
13+
all: build
14+
15+
build:
16+
@echo "Building $(BINARY_NAME)..."
17+
go build -ldflags="-s -w" -o $(BINARY_NAME) .
18+
19+
link:
20+
@if [ ! -f $(BINARY_NAME) ]; then \
21+
echo "Binary not found, building first..."; \
22+
$(MAKE) build; \
23+
else \
24+
echo "Using existing binary $(BINARY_NAME)"; \
25+
fi
26+
@echo "Linking $(BINARY_NAME) to Docker CLI plugins directory..."
27+
@mkdir -p $(PLUGIN_DIR)
28+
@ln -sf $(shell pwd)/$(BINARY_NAME) $(PLUGIN_DIR)/$(PLUGIN_NAME)
29+
@echo "Link created: $(PLUGIN_DIR)/$(PLUGIN_NAME)"
30+
31+
install: build link
32+
33+
release:
34+
@if [ -z "$(VERSION)" ]; then \
35+
echo "Error: VERSION parameter is required. Use: make release VERSION=x.y.z"; \
36+
exit 1; \
37+
fi
38+
@echo "Building release version '$(VERSION)'..."
39+
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CGO_CFLAGS="$(MACOS_MIN_VERSION_LDFLAG)" CGO_LDFLAGS="$(MACOS_MIN_VERSION_LDFLAG)" go build -trimpath -ldflags="-s -w -X github.com/docker/model-cli/desktop.Version=$(VERSION)" -o dist/darwin-arm64/$(PLUGIN_NAME) .
40+
GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w -X github.com/docker/model-cli/desktop.Version=$(VERSION)" -o dist/windows-amd64/$(PLUGIN_NAME).exe .
41+
GOOS=windows GOARCH=arm64 go build -trimpath -ldflags="-s -w -X github.com/docker/model-cli/desktop.Version=$(VERSION)" -o dist/windows-arm64/$(PLUGIN_NAME).exe .
42+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w -X github.com/docker/model-cli/desktop.Version=$(VERSION)" -o dist/linux-amd64/$(PLUGIN_NAME) .
43+
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags="-s -w -X github.com/docker/model-cli/desktop.Version=$(VERSION)" -o dist/linux-arm64/$(PLUGIN_NAME) .
44+
@echo "Release build complete: $(PLUGIN_NAME) version '$(VERSION)'"
45+
46+
ce-release:
47+
@if [ -z "$(VERSION)" ]; then \
48+
echo "Error: VERSION parameter is required. Use: make release VERSION=x.y.z"; \
49+
exit 1; \
50+
fi
51+
@if [ "$(uname -s)" != "Linux" ]; then \
52+
echo "Warning: This release target is designed for Linux"; \
53+
fi
54+
@echo "Building local release version '$(VERSION)'..."
55+
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w -X github.com/docker/model-cli/desktop.Version=$(VERSION)" -o dist/$(PLUGIN_NAME) .
56+
@echo "Local release build complete: $(PLUGIN_NAME) version '$(VERSION)'"
57+
58+
mock:
59+
@echo "Generating mocks..."
60+
@mkdir -p mocks
61+
@go generate ./...
62+
@echo "Mocks generated!"
63+
64+
unit-tests:
65+
@echo "Running unit tests..."
66+
@go test -race -v ./...
67+
@echo "Unit tests completed!"
68+
69+
clean:
70+
@echo "Cleaning up..."
71+
@rm -f $(BINARY_NAME)
72+
@echo "Cleaned!"
73+
74+
docs:
75+
$(eval $@_TMP_OUT := $(shell mktemp -d -t model-cli-output.XXXXXXXXXX))
76+
docker buildx bake --allow=fs.read=$(shell cd ../.. && pwd) --set "*.output=type=local,dest=$($@_TMP_OUT)" update-docs
77+
rm -rf ./docs/reference/*
78+
cp -R "$($@_TMP_OUT)"/* ./docs/reference/
79+
rm -rf "$($@_TMP_OUT)"/*

cmd/cli/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Docker Model CLI
2+
3+
A powerful command-line interface for managing, running, packaging, and deploying AI/ML models using Docker. This CLI lets you install and control the Docker Model Runner, interact with models, manage model artifacts, and integrate with OpenAI and other backends—all from your terminal.
4+
5+
## Features
6+
- **Install Model Runner**: Easily set up the Docker Model Runner for local or cloud environments with GPU support.
7+
- **Run Models**: Execute models with prompts or in interactive chat mode, supporting multiline input and OpenAI-style backends.
8+
- **List Models**: View all models available locally or via OpenAI, with options for JSON and quiet output.
9+
- **Package Models**: Convert GGUF files into Docker model OCI artifacts and push them to registries, including license and context size options.
10+
- **Configure Models**: Set runtime flags and context sizes for models.
11+
- **Logs & Status**: Stream logs and check the status of the Model Runner and individual models.
12+
- **Tag, Pull, Push, Remove, Unload**: Full lifecycle management for model artifacts.
13+
- **Compose & Desktop Integration**: Advanced orchestration and desktop support for model backends.
14+
15+
## Building
16+
1. **Clone the repo:**
17+
```bash
18+
git clone https://github.com/docker/model-cli.git
19+
cd model-cli
20+
```
21+
2. **Build the CLI:**
22+
```bash
23+
make build
24+
```
25+
3. **Install Model Runner:**
26+
```bash
27+
./model install-runner
28+
```
29+
Use `--gpu cuda` for GPU support, or `--gpu auto` for automatic detection.
30+
31+
## Usage
32+
Run `./model --help` to see all commands and options.
33+
34+
### Common Commands
35+
- `model install-runner` — Install the Docker Model Runner
36+
- `model run MODEL [PROMPT]` — Run a model with a prompt or enter chat mode
37+
- `model list` — List available models
38+
- `model package --gguf <path> --push <target>` — Package and push a model
39+
- `model logs` — View logs
40+
- `model status` — Check runner status
41+
- `model configure MODEL [flags]` — Configure model runtime
42+
- `model unload MODEL` — Unload a model
43+
- `model tag SOURCE TARGET` — Tag a model
44+
- `model pull MODEL` — Pull a model
45+
- `model push MODEL` — Push a model
46+
- `model rm MODEL` — Remove a model
47+
48+
## Example: Interactive Chat
49+
```bash
50+
./model run llama.cpp "What is the capital of France?"
51+
```
52+
Or enter chat mode:
53+
```bash
54+
./model run llama.cpp
55+
Interactive chat mode started. Type '/bye' to exit.
56+
> """
57+
Tell me a joke.
58+
"""
59+
```
60+
61+
## Advanced
62+
- **Packaging:**
63+
Add licenses and set context size when packaging models for distribution.
64+
65+
## Development
66+
- **Run unit tests:**
67+
```bash
68+
make unit-tests
69+
```
70+
- **Generate docs:**
71+
```bash
72+
make docs
73+
```
74+
75+
## License
76+
[Apache 2.0](LICENSE)
77+

cmd/cli/commands/backend.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package commands
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"maps"
7+
"os"
8+
"slices"
9+
"strings"
10+
)
11+
12+
// ValidBackends is a map of valid backends
13+
var ValidBackends = map[string]bool{
14+
"llama.cpp": true,
15+
"openai": true,
16+
}
17+
18+
// validateBackend checks if the provided backend is valid
19+
func validateBackend(backend string) error {
20+
if !ValidBackends[backend] {
21+
return fmt.Errorf("invalid backend '%s'. Valid backends are: %s",
22+
backend, ValidBackendsKeys())
23+
}
24+
return nil
25+
}
26+
27+
// ensureAPIKey retrieves the API key if needed
28+
func ensureAPIKey(backend string) (string, error) {
29+
if backend == "openai" {
30+
apiKey := os.Getenv("OPENAI_API_KEY")
31+
if apiKey == "" {
32+
return "", errors.New("OPENAI_API_KEY environment variable is required when using --backend=openai")
33+
}
34+
return apiKey, nil
35+
}
36+
return "", nil
37+
}
38+
39+
func ValidBackendsKeys() string {
40+
keys := slices.Collect(maps.Keys(ValidBackends))
41+
slices.Sort(keys)
42+
return strings.Join(keys, ", ")
43+
}

0 commit comments

Comments
 (0)