Skip to content
Open
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
68 changes: 67 additions & 1 deletion pkg/cmd/dev/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -170,6 +192,50 @@ 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) {
Comment thread
rishi-jat marked this conversation as resolved.
stat, err := os.Lstat(absPath)
if err != nil {
return "", err
}

// Case 1: direct file
if stat.Mode().IsRegular() {
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
}

// Case 2: directory → search for .gguf
if stat.IsDir() {
entries, err := os.ReadDir(absPath)
if err != nil {
return "", fmt.Errorf("error searching for lora adapter in %s: %w", absPath, err)
}

var found string
for _, entry := range entries {
if entry.Type().IsRegular() && strings.HasSuffix(strings.ToLower(entry.Name()), ".gguf") {
path := filepath.Join(absPath, entry.Name())
if found != "" {
return "", fmt.Errorf("multiple lora adapter files found: %s and %s", found, path)
}
found = path
}
}
if found == "" {
return "", fmt.Errorf("no .gguf lora adapter found in %s", absPath)
}
output.Debugf("Found lora adapter path in directory %s at %s", absPath, found)
return found, nil
}
Comment on lines +212 to +234
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know me and @gorkem are giving conflicting review here, but I'm still of the opinion that if you have a model part with type lora-adapter, we should not support directories for this part. Given that a LoRA adapter is a specific thing, what's the use case for allowing it to be a directory?


return "", fmt.Errorf("lora adapter path %s is not a regular file or directory", absPath)
}

// extractModelKitToCache extracts a ModelKit reference to a cache directory
// using the unpack library with model filter
func extractModelKitToCache(ctx context.Context, options *DevStartOptions) error {
Expand Down
24 changes: 18 additions & 6 deletions pkg/lib/harness/llm-harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -85,10 +85,13 @@ 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

if runtime.GOOS == "windows" {
cmd = exec.Command(
"./llamafile.exe",
args := []string{
"--server",
"--model", modelPath,
"--host", harness.Host,
Expand All @@ -97,11 +100,20 @@ func (harness *LLMHarness) Start(modelPath string) (err error) {
"--gpu", "AUTO",
"--nobrowser",
"--unsecure",
)
}
for _, loraPath := range loraPaths {
args = append(args, "--lora", loraPath)
}
cmd = exec.Command("./llamafile.exe", args...)
} else {
// Build command string for sh -c (required for APE binaries on Linux)
loraArgs := ""
for _, loraPath := range loraPaths {
loraArgs += fmt.Sprintf(" --lora %s", loraPath)
}
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),
fmt.Sprintf("./llamafile --server --model %s --host %s --port %d --path %s --gpu AUTO --nobrowser --unsecure%s",
modelPath, harness.Host, harness.Port, uiHome, loraArgs),
Comment on lines 114 to +116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I understand reverting to using sh -c in 87f4adf, but why also undo the change of using one args slice for both windows and unix? If we want the shell-injectable form here, we can still reuse the args slice rather than duplicating the arguments in this format:

	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", args...)
	} else {
		// Note: running llamafile with sh -c is required as it is an APE binary (is this still true?)
		llamaCmd := "./llamafile " + strings.Join(args, " ")
		cmd = exec.Command("sh", "-c", llamaCmd)
	}

)
}

Expand Down