Skip to content

Commit a2b06c5

Browse files
committed
cli: hint when install-runner options are ignored
1 parent 2f1609b commit a2b06c5

3 files changed

Lines changed: 177 additions & 7 deletions

File tree

.github/workflows/cli-validate.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,11 @@ jobs:
2929
outputs:
3030
matrix: ${{ steps.generate.outputs.matrix }}
3131
steps:
32-
-
33-
name: Checkout
34-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
3532
-
3633
name: Generate matrix
3734
id: generate
38-
uses: docker/bake-action/subaction/matrix@a66e1c87e2eca0503c343edf1d208c716d54b8a8
39-
with:
40-
files: ./cmd/cli/docker-bake.hcl
41-
target: validate
35+
run: |
36+
echo 'matrix={"include":[{"target":"validate-docs"},{"target":"validate-tests"}]}' >> "$GITHUB_OUTPUT"
4237
4338
validate:
4439
runs-on: ubuntu-24.04

cmd/cli/commands/install-runner.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,39 @@ type runnerOptions struct {
245245
tlsKey string
246246
}
247247

248+
func commandFlagChanged(cmd *cobra.Command, name string) bool {
249+
if cmd == nil {
250+
return false
251+
}
252+
flag := cmd.Flags().Lookup(name)
253+
return flag != nil && flag.Changed
254+
}
255+
256+
func existingRunnerOptionsHint(cmd *cobra.Command, opts runnerOptions) string {
257+
backendChanged := commandFlagChanged(cmd, "backend")
258+
gpuChanged := commandFlagChanged(cmd, "gpu")
259+
260+
if !backendChanged && !gpuChanged {
261+
return ""
262+
}
263+
264+
reinstallArgs := []string{"docker", "model", "reinstall-runner"}
265+
266+
if backendChanged && opts.backend != "" {
267+
reinstallArgs = append(reinstallArgs, "--backend", fmt.Sprintf("%q", opts.backend))
268+
}
269+
if gpuChanged && opts.gpuMode != "" {
270+
reinstallArgs = append(reinstallArgs, "--gpu", fmt.Sprintf("%q", opts.gpuMode))
271+
}
272+
273+
return fmt.Sprintf(
274+
"\nThe requested runner options were not applied because the Model Runner container is already running.\n"+
275+
"To recreate the runner with the requested options, run:\n\n"+
276+
" %s\n",
277+
strings.Join(reinstallArgs, " "),
278+
)
279+
}
280+
248281
// runInstallOrStart is shared logic for install-runner and start-runner commands
249282
func runInstallOrStart(cmd *cobra.Command, opts runnerOptions, debug bool) error {
250283
// On macOS ARM64, the vllm backend requires deferred installation
@@ -343,6 +376,9 @@ func runInstallOrStart(cmd *cobra.Command, opts runnerOptions, debug bool) error
343376
} else {
344377
cmd.Printf("Model Runner container %s is already running\n", ctrID[:12])
345378
}
379+
380+
cmd.Print(existingRunnerOptionsHint(cmd, opts))
381+
346382
return nil
347383
}
348384
}

cmd/cli/commands/install-runner_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package commands
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/docker/model-runner/pkg/inference/backends/llamacpp"
@@ -154,3 +155,141 @@ func TestInstallRunnerValidArgsFunction(t *testing.T) {
154155
t.Error("Expected ValidArgsFunction to be set")
155156
}
156157
}
158+
159+
func TestExistingRunnerOptionsHintNoExplicitOptions(t *testing.T) {
160+
cmd := newInstallRunner()
161+
162+
// Default install-runner options should not print a reinstall hint.
163+
got := existingRunnerOptionsHint(cmd, runnerOptions{
164+
backend: "",
165+
gpuMode: "auto",
166+
})
167+
168+
if got != "" {
169+
t.Fatalf("expected no hint when backend/gpu flags are not explicitly changed, got %q", got)
170+
}
171+
}
172+
173+
func TestExistingRunnerOptionsHintWithBackendOnly(t *testing.T) {
174+
cmd := newInstallRunner()
175+
176+
if err := cmd.Flags().Set("backend", vllm.Name); err != nil {
177+
t.Fatal(err)
178+
}
179+
180+
// A backend-only request should preserve only the explicit backend flag.
181+
got := existingRunnerOptionsHint(cmd, runnerOptions{
182+
backend: vllm.Name,
183+
gpuMode: "auto",
184+
})
185+
186+
if !strings.Contains(got, `docker model reinstall-runner --backend "vllm"`) {
187+
t.Fatalf("expected backend-only reinstall hint, got %q", got)
188+
}
189+
if strings.Contains(got, "--gpu") {
190+
t.Fatalf("did not expect gpu flag in backend-only hint, got %q", got)
191+
}
192+
}
193+
194+
func TestExistingRunnerOptionsHintWithCUDA(t *testing.T) {
195+
cmd := newInstallRunner()
196+
197+
if err := cmd.Flags().Set("gpu", "cuda"); err != nil {
198+
t.Fatal(err)
199+
}
200+
201+
// This fakes a user explicitly requesting CUDA without requiring local GPU hardware.
202+
got := existingRunnerOptionsHint(cmd, runnerOptions{
203+
gpuMode: "cuda",
204+
})
205+
206+
if !strings.Contains(got, `docker model reinstall-runner --gpu "cuda"`) {
207+
t.Fatalf("expected cuda reinstall hint, got %q", got)
208+
}
209+
if strings.Contains(got, "--backend") {
210+
t.Fatalf("did not expect backend flag in cuda-only hint, got %q", got)
211+
}
212+
}
213+
214+
func TestExistingRunnerOptionsHintWithBackendAndCUDA(t *testing.T) {
215+
cmd := newInstallRunner()
216+
217+
if err := cmd.Flags().Set("backend", vllm.Name); err != nil {
218+
t.Fatal(err)
219+
}
220+
if err := cmd.Flags().Set("gpu", "cuda"); err != nil {
221+
t.Fatal(err)
222+
}
223+
224+
// This covers the WSL2/vLLM issue path: the existing runner needs reinstall-runner.
225+
got := existingRunnerOptionsHint(cmd, runnerOptions{
226+
backend: vllm.Name,
227+
gpuMode: "cuda",
228+
})
229+
230+
expectedFragments := []string{
231+
"The requested runner options were not applied",
232+
`docker model reinstall-runner --backend "vllm" --gpu "cuda"`,
233+
}
234+
235+
for _, fragment := range expectedFragments {
236+
if !strings.Contains(got, fragment) {
237+
t.Fatalf("expected hint to contain %q, got %q", fragment, got)
238+
}
239+
}
240+
}
241+
242+
func TestExistingRunnerOptionsHintWithNoGPU(t *testing.T) {
243+
cmd := newInstallRunner()
244+
245+
if err := cmd.Flags().Set("gpu", "none"); err != nil {
246+
t.Fatal(err)
247+
}
248+
249+
// An explicit CPU/no-GPU request should be preserved in the reinstall command.
250+
got := existingRunnerOptionsHint(cmd, runnerOptions{
251+
gpuMode: "none",
252+
})
253+
254+
if !strings.Contains(got, `docker model reinstall-runner --gpu "none"`) {
255+
t.Fatalf("expected no-gpu reinstall hint, got %q", got)
256+
}
257+
if strings.Contains(got, "--backend") {
258+
t.Fatalf("did not expect backend flag in no-gpu hint, got %q", got)
259+
}
260+
}
261+
262+
func TestExistingRunnerOptionsHintQuotesFlagValues(t *testing.T) {
263+
cmd := newInstallRunner()
264+
265+
if err := cmd.Flags().Set("gpu", "cuda; echo bad"); err != nil {
266+
t.Fatal(err)
267+
}
268+
269+
// Suggested command values should be quoted before being shown to the user.
270+
got := existingRunnerOptionsHint(cmd, runnerOptions{
271+
gpuMode: "cuda; echo bad",
272+
})
273+
274+
if !strings.Contains(got, `docker model reinstall-runner --gpu "cuda; echo bad"`) {
275+
t.Fatalf("expected quoted gpu reinstall hint, got %q", got)
276+
}
277+
if strings.Contains(got, "--gpu cuda;") {
278+
t.Fatalf("expected gpu value to be quoted in reinstall hint, got %q", got)
279+
}
280+
}
281+
282+
func TestCommandFlagChangedDefensiveCases(t *testing.T) {
283+
cmd := newInstallRunner()
284+
285+
// Missing commands and flags should be treated as unchanged.
286+
if commandFlagChanged(nil, "gpu") {
287+
t.Fatal("expected nil command to report unchanged flag")
288+
}
289+
if commandFlagChanged(cmd, "missing") {
290+
t.Fatal("expected missing flag to report unchanged")
291+
}
292+
if commandFlagChanged(cmd, "gpu") {
293+
t.Fatal("expected default gpu flag to report unchanged")
294+
}
295+
}

0 commit comments

Comments
 (0)