diff --git a/cmd/cli/commands/launch.go b/cmd/cli/commands/launch.go index 1570c31db..9df4aa5e2 100644 --- a/cmd/cli/commands/launch.go +++ b/cmd/cli/commands/launch.go @@ -48,7 +48,16 @@ var containerApps = map[string]containerApp{ envFn: anythingllmEnv, extraDockerArgs: []string{"-v", "anythingllm_storage:/app/server/storage"}, }, - "openwebui": {defaultImage: "ghcr.io/open-webui/open-webui:latest", defaultHostPort: 3000, containerPort: 8080, envFn: openwebuiEnv}, + "openwebui": { + defaultImage: "ghcr.io/open-webui/open-webui:latest", + defaultHostPort: 3000, + containerPort: 8080, + envFn: openwebuiEnv, + }, + "llmfit": { + defaultImage: "ghcr.io/alexsjones/llmfit", + envFn: llmfitEnv, + }, } // hostApp describes a native CLI app launched on the host. @@ -86,6 +95,7 @@ var appDescriptions = map[string]string{ "openclaw": "Open Claw AI assistant", "opencode": "Open Code AI code editor", "openwebui": "Open WebUI for models", + "llmfit": "Recommend models that run on your system", } func newLaunchCmd() *cobra.Command { @@ -109,9 +119,11 @@ Supported apps: %s Examples: docker model launch docker model launch opencode + docker model launch llmfit docker model launch claude -- --help docker model launch openwebui --port 3000 - docker model launch claude --config`, strings.Join(supportedApps, ", ")), + docker model launch claude --config + docker model launch llmfit -- recommend -n 5`, strings.Join(supportedApps, ", ")), ValidArgs: supportedApps, RunE: func(cmd *cobra.Command, args []string) error { // No args - list supported apps @@ -205,8 +217,10 @@ func printAppConfig(cmd *cobra.Command, app string, ep engineEndpoints, imageOve } cmd.Printf("Configuration for %s (container app):\n", app) cmd.Printf(" Image: %s\n", img) - cmd.Printf(" Container port: %d\n", ca.containerPort) - cmd.Printf(" Host port: %d\n", hostPort) + if ca.containerPort > 0 { + cmd.Printf(" Container port: %d\n", ca.containerPort) + cmd.Printf(" Host port: %d\n", hostPort) + } if ca.envFn != nil { cmd.Printf(" Environment:\n") for _, e := range ca.envFn(ep.container) { @@ -295,9 +309,13 @@ func launchContainerApp(cmd *cobra.Command, ca containerApp, baseURL string, ima if detach { dockerArgs = append(dockerArgs, "-d") } - dockerArgs = append(dockerArgs, - "-p", fmt.Sprintf("%d:%d", hostPort, ca.containerPort), - ) + + if ca.containerPort > 0 { + dockerArgs = append(dockerArgs, + "-p", fmt.Sprintf("%d:%d", hostPort, ca.containerPort), + ) + } + dockerArgs = append(dockerArgs, ca.extraDockerArgs...) if ca.envFn == nil { return fmt.Errorf("container app requires envFn to be set") @@ -419,6 +437,12 @@ func anthropicEnv(baseURL string) []string { } } +func llmfitEnv(baseURL string) []string { + return []string{ + "DOCKER_MODEL_RUNNER_HOST=" + baseURL, + } +} + // withEnv returns the current process environment extended with extra vars. func withEnv(extra ...string) []string { return append(os.Environ(), extra...) diff --git a/cmd/cli/commands/launch_test.go b/cmd/cli/commands/launch_test.go index 069c2426e..fa65f11b0 100644 --- a/cmd/cli/commands/launch_test.go +++ b/cmd/cli/commands/launch_test.go @@ -549,6 +549,7 @@ func TestListSupportedApps(t *testing.T) { require.Contains(t, output, "claude") require.Contains(t, output, "opencode") require.Contains(t, output, "openwebui") + require.Contains(t, output, "llmfit") } func TestOpenWebuiEnvIncludesWebuiAuth(t *testing.T) { @@ -570,6 +571,8 @@ func TestPrintAppConfigContainerApp(t *testing.T) { require.Contains(t, output, "Configuration for openwebui") require.Contains(t, output, "container app") require.Contains(t, output, "ghcr.io/open-webui/open-webui:latest") + require.Contains(t, output, "Container port") + require.Contains(t, output, "Host port") require.Contains(t, output, "OPENAI_API_BASE") require.Contains(t, output, "WEBUI_AUTH=false") } @@ -588,6 +591,22 @@ func TestPrintAppConfigContainerAppOverrides(t *testing.T) { require.Contains(t, output, "9999") } +func TestPrintAppConfigContainerAppNoPorts(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newTestCmd(buf) + + ep := engineEndpoints{container: testBaseURL, host: testBaseURL} + err := printAppConfig(cmd, "llmfit", ep, "", 0) + require.NoError(t, err) + output := buf.String() + require.Contains(t, output, "Configuration for llmfit") + require.Contains(t, output, "container app") + require.Contains(t, output, "ghcr.io/alexsjones/llmfit") + require.Contains(t, output, "DOCKER_MODEL_RUNNER_HOST="+testBaseURL) + require.NotContains(t, output, "Container port") + require.NotContains(t, output, "Host port") +} + func TestPrintAppConfigHostApp(t *testing.T) { buf := new(bytes.Buffer) cmd := newTestCmd(buf) @@ -611,3 +630,42 @@ func TestPrintAppConfigUnsupported(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "unsupported app") } + +func TestLaunchContainerAppNoPort(t *testing.T) { + ca := containerApp{ + defaultImage: "ghcr.io/alexsjones/llmfit", + envFn: llmfitEnv, + } + buf := new(bytes.Buffer) + cmd := newTestCmd(buf) + + err := launchContainerApp(cmd, ca, testBaseURL, "", 0, false, nil, true) + require.NoError(t, err) + + output := buf.String() + require.Contains(t, output, "Would run: docker") + require.Contains(t, output, "run --rm") + require.NotContains(t, output, "-p") + require.Contains(t, output, "DOCKER_MODEL_RUNNER_HOST="+testBaseURL) + require.Contains(t, output, "ghcr.io/alexsjones/llmfit") +} + +func TestLaunchContainerAppNoPortWithArgs(t *testing.T) { + ca := containerApp{ + defaultImage: "ghcr.io/alexsjones/llmfit", + envFn: llmfitEnv, + } + buf := new(bytes.Buffer) + cmd := newTestCmd(buf) + + err := launchContainerApp(cmd, ca, testBaseURL, "", 0, false, []string{"recommend", "-n", "3"}, true) + require.NoError(t, err) + + output := buf.String() + require.Contains(t, output, "Would run: docker") + require.Contains(t, output, "run --rm") + require.NotContains(t, output, "-p") + require.Contains(t, output, "recommend -n 3") + require.Contains(t, output, "DOCKER_MODEL_RUNNER_HOST="+testBaseURL) + require.Contains(t, output, "ghcr.io/alexsjones/llmfit") +}