Enable loading of lora-adapter parts in dev mode#1098
Enable loading of lora-adapter parts in dev mode#1098rishi-jat wants to merge 6 commits intokitops-ml:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for loading LoRA (Low-Rank Adaptation) adapters in dev mode when type: lora-adapter parts are defined in a ModelKit. Dev mode now detects lora-adapter entries, resolves their paths, locates the corresponding .gguf files, and passes them to the LLM harness via --lora flags.
Changes:
- Modified
LLMHarness.Start()to accept a slice of LoRA adapter paths - Added LoRA adapter detection and collection logic in
runDev() - Created
findLoraAdapterFile()helper function to locate.ggufadapter files
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pkg/lib/harness/llm-harness.go | Updated Start() method signature to accept loraPaths parameter; modified command construction for both Windows and non-Windows to include --lora flags for each adapter |
| pkg/cmd/dev/dev.go | Added logic to detect lora-adapter type parts, resolve their paths, and pass them to the harness; added findLoraAdapterFile() helper function mirroring findModelFile() |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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), | ||
| ) |
There was a problem hiding this comment.
The non-Windows shell command construction is vulnerable to shell injection. The loraPath variables are directly interpolated into the shell command string without proper escaping or quoting. If a lora adapter path contains spaces or special shell characters (e.g., semicolons, backticks, dollar signs), it could lead to command injection or malformed commands.
Consider using the same approach as the Windows code path: build an args slice and pass it to exec.Command instead of using shell interpolation. This is safer and more consistent across platforms.
|
/cc @gorkem |
|
@rishi-jat there are some test failures on this PR |
pkg/lib/harness/llm-harness.go
Outdated
| 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) | ||
| } | ||
| cmd = exec.Command("./llamafile", args...) |
There was a problem hiding this comment.
The arguments seem to be the same between the windows and non-windows case. Why not define them once and branch on the exec.Command step?
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 {
cmd = exec.Command("./llamafile", args...)
}
pkg/cmd/dev/dev.go
Outdated
| if err := filepath.WalkDir(absPath, func(path string, d fs.DirEntry, err error) error { | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if strings.HasSuffix(path, ".gguf") && d.Type().IsRegular() { | ||
| if loraPath == "" { | ||
| loraPath = path | ||
| } else { | ||
| return fmt.Errorf("multiple lora adapter files found: %s and %s", loraPath, path) | ||
| } | ||
| } | ||
| return nil | ||
| }); err != nil { | ||
| return "", fmt.Errorf("error searching for lora adapter file in %s: %w", absPath, err) | ||
| } else if loraPath == "" { | ||
| return "", fmt.Errorf("could not find lora adapter file in %s", absPath) | ||
| } | ||
| output.Debugf("Found lora adapter path in directory %s at %s", absPath, loraPath) | ||
| return loraPath, nil |
There was a problem hiding this comment.
Since only one LoRA is supported, I think it makes sense to avoid the walkDir step entirely and error out if the path specified by the modelpart is not a regular file.
|
@amisevsk I have made the suggested changes please have a look. Thanks! |
0eebd5e to
5bff6cf
Compare
amisevsk
left a comment
There was a problem hiding this comment.
Looks good to me, thanks @rishi-jat
gorkem
left a comment
There was a problem hiding this comment.
Thank you for the PR, this was something I was hoping to get to for a long time. PR needs to be rebased and there are a couple of issues that would be nice to address before merging.
5bff6cf to
a2fa09b
Compare
Signed-off-by: Rishi Jat <rishijat098@gmail.com> Avoid shell interpolation when passing LoRA adapter paths Signed-off-by: Rishi Jat <rishijat098@gmail.com> refactor: require direct .gguf file for lora adapter instead of directory walk Signed-off-by: Rishi Jat <rishijat098@gmail.com>
a2fa09b to
25e9f06
Compare
Signed-off-by: Rishi Jat <rishijat098@gmail.com>
|
@rishi-jat the build fails. Please make sure that build passes cleanly |
Signed-off-by: Rishi Jat <rishijat098@gmail.com>
Signed-off-by: Rishi Jat <rishijat098@gmail.com>
|
@gorkem CI pass. please have a look. Thanks! |
amisevsk
left a comment
There was a problem hiding this comment.
I'm not sure why there's mock-related code in the llm-harness.go file
amisevsk
left a comment
There was a problem hiding this comment.
Tests are still failing
Signed-off-by: Rishi Jat <rishijat098@gmail.com>
5a78f85 to
87f4adf
Compare
CI is now green please have a look. Thanks! |
amisevsk
left a comment
There was a problem hiding this comment.
The current version is once again vulnerable to shell injection:
- We run with
--lora <LoRA-Path>andsh -c, a malicious LoRA path can easily lead to shell injection - Since we support LoRA adapters as directories (I still think it's not worth doing), it's additionally easy to obfuscate the presence of injection, as we can just store a directory path and put the malicious file in it to hide its presence from the user
E.g., if my ModelKit contains something like
/adapters/"lora.gguf &; echo "hi" > ~/hi.gguf"
and the Kitfile
model
parts:
- path: adapters/
type: lora
you a) won't be able to notice an issue by looking at the Kitfile, and b) will automatically run arbitrary code when using kit dev.
It seems we're at an impasse here, as it seems sh -c is required for APE binaries used by llamafile, though.
|
According to https://github.com/jart/cosmopolitan?tab=readme-ov-file#shells, the Is the |
| 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), |
There was a problem hiding this comment.
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)
}| // 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 | ||
| } |
There was a problem hiding this comment.
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?
Description
This PR enables dev mode to load LoRA adapters when
type: lora-adapterparts are defined in the ModelKit.Dev mode now:
lora-adapterentries inmodel.parts.ggufadapter file--lora <adapter_path>to the LLM harness during startupExisting behavior remains unchanged when no LoRA adapters are present.
Linked issues
Fixes #306