diff --git a/pkg/cmd/dev/dev.go b/pkg/cmd/dev/dev.go index 00349a08..c7283a88 100644 --- a/pkg/cmd/dev/dev.go +++ b/pkg/cmd/dev/dev.go @@ -85,6 +85,28 @@ func runDev(ctx context.Context, options *DevStartOptions) error { return err } + // Detect and collect lora-adapter parts + var loraPaths []string + for _, part := range kitfile.Model.Parts { + if strings.EqualFold(part.Type, "lora-adapter") { + if part.Path == "" { + continue + } + partAbsPath, _, err := filesystem.VerifySubpath(options.contextDir, part.Path) + if err != nil { + output.Debugf("Skipping lora-adapter: %v", err) + continue + } + loraPath, err := findLoraAdapterFile(partAbsPath) + if err != nil { + output.Debugf("Skipping lora-adapter: %v", err) + continue + } + output.Infof("Found lora-adapter: %s", loraPath) + loraPaths = append(loraPaths, loraPath) + } + } + llmHarness := &harness.LLMHarness{} llmHarness.Host = options.host llmHarness.Port = options.port @@ -93,7 +115,7 @@ func runDev(ctx context.Context, options *DevStartOptions) error { return err } - if err := llmHarness.Start(modelPath); err != nil { + if err := llmHarness.Start(modelPath, loraPaths); err != nil { return err } @@ -170,6 +192,26 @@ func findModelFile(absPath string) (string, error) { return modelPath, nil } +// findLoraAdapterFile validates a lora adapter path. +// The path must point to a regular file. +func findLoraAdapterFile(absPath string) (string, error) { + stat, err := os.Lstat(absPath) + if err != nil { + return "", err + } + + if !stat.Mode().IsRegular() { + return "", fmt.Errorf("lora adapter path must be a regular .gguf file: %s", absPath) + } + + if !strings.HasSuffix(strings.ToLower(absPath), ".gguf") { + return "", fmt.Errorf("lora adapter file must be a .gguf file: %s", absPath) + } + + output.Debugf("Found lora adapter path at %s", absPath) + return absPath, nil +} + // extractModelKitToCache extracts a ModelKit reference to a cache directory // using the unpack library with model filter func extractModelKitToCache(ctx context.Context, options *DevStartOptions) error { diff --git a/pkg/lib/harness/llm-harness.go b/pkg/lib/harness/llm-harness.go index ea8f313d..4df40047 100644 --- a/pkg/lib/harness/llm-harness.go +++ b/pkg/lib/harness/llm-harness.go @@ -63,7 +63,7 @@ func (harness *LLMHarness) Init() error { return nil } -func (harness *LLMHarness) Start(modelPath string) (err error) { +func (harness *LLMHarness) Start(modelPath string, loraPaths []string) (err error) { harnessPath := constants.HarnessPath(harness.ConfigHome) pidFile := filepath.Join(harnessPath, constants.HarnessProcessFile) @@ -85,24 +85,31 @@ func (harness *LLMHarness) Start(modelPath string) (err error) { uiHome := filepath.Join(harnessPath, "ui") output.Debugf("model path is %s", modelPath) + for _, loraPath := range loraPaths { + output.Debugf("lora adapter path is %s", loraPath) + } var cmd *exec.Cmd + args := []string{ + "--server", + "--model", modelPath, + "--host", harness.Host, + "--port", fmt.Sprintf("%d", harness.Port), + "--path", uiHome, + "--gpu", "AUTO", + "--nobrowser", + "--unsecure", + } + for _, loraPath := range loraPaths { + args = append(args, "--lora", loraPath) + } + if runtime.GOOS == "windows" { - cmd = exec.Command( - "./llamafile.exe", - "--server", - "--model", modelPath, - "--host", harness.Host, - "--port", fmt.Sprintf("%d", harness.Port), - "--path", uiHome, - "--gpu", "AUTO", - "--nobrowser", - "--unsecure", - ) + cmd = exec.Command("./llamafile.exe", args...) } else { - cmd = exec.Command("sh", "-c", - fmt.Sprintf("./llamafile --server --model %s --host %s --port %d --path %s --gpu AUTO --nobrowser --unsecure", - modelPath, harness.Host, harness.Port, uiHome), - ) + // Run through sh -c for APE compatibility, while passing all user-controlled + // values as positional args to avoid shell interpolation and injection. + shellArgs := append([]string{"-c", "exec ./llamafile \"$@\"", "llamafile"}, args...) + cmd = exec.Command("sh", shellArgs...) } cmd.Dir = harnessPath