|
7 | 7 | "log/slog" |
8 | 8 | "math" |
9 | 9 | "os" |
| 10 | + "path/filepath" |
10 | 11 | "strconv" |
11 | 12 | "strings" |
12 | 13 | "time" |
@@ -83,6 +84,22 @@ func buildDeployDeps(ac *appContext, deps *mcp.ToolDeps, |
83 | 84 | } |
84 | 85 | } |
85 | 86 |
|
| 87 | + // Auto-wire the multimodal projector for llama.cpp VL models. A GGUF vision |
| 88 | + // model ships a co-located mmproj-*.gguf, and llama-server needs --mmproj to |
| 89 | + // accept images. If the caller didn't set it, inject the projector path so |
| 90 | + // vision works zero-config (it flows through configToFlags as --mmproj). |
| 91 | + if resolved.ModelFormat == "gguf" && strings.HasPrefix(strings.ToLower(resolved.Engine), "llamacpp") { |
| 92 | + if _, set := resolved.Config["mmproj"]; !set { |
| 93 | + if mm := findColocatedMMProj(modelPath); mm != "" { |
| 94 | + if resolved.Config == nil { |
| 95 | + resolved.Config = map[string]any{} |
| 96 | + } |
| 97 | + resolved.Config["mmproj"] = mm |
| 98 | + slog.Info("auto-wired multimodal projector for vision", "model", modelName, "mmproj", mm) |
| 99 | + } |
| 100 | + } |
| 101 | + } |
| 102 | + |
86 | 103 | req := &runtime.DeployRequest{ |
87 | 104 | Name: modelName, |
88 | 105 | Engine: resolved.Engine, |
@@ -856,3 +873,38 @@ func normalizeServedModelName(modelName, raw string) string { |
856 | 873 | } |
857 | 874 | return served |
858 | 875 | } |
| 876 | + |
| 877 | +// findColocatedMMProj returns the path of a multimodal projector (mmproj-*.gguf) |
| 878 | +// next to a GGUF model, preferring an f16 projector for quality. Returns "" when |
| 879 | +// none is present (i.e. the model is not multimodal). modelPath may be the model |
| 880 | +// file or its directory; the projector is expected in the same directory. |
| 881 | +func findColocatedMMProj(modelPath string) string { |
| 882 | + dir := modelPath |
| 883 | + if fi, err := os.Stat(modelPath); err == nil && !fi.IsDir() { |
| 884 | + dir = filepath.Dir(modelPath) |
| 885 | + } |
| 886 | + entries, err := os.ReadDir(dir) |
| 887 | + if err != nil { |
| 888 | + return "" |
| 889 | + } |
| 890 | + var f16, other string |
| 891 | + for _, e := range entries { |
| 892 | + if e.IsDir() { |
| 893 | + continue |
| 894 | + } |
| 895 | + lower := strings.ToLower(e.Name()) |
| 896 | + if !strings.HasSuffix(lower, ".gguf") || !strings.Contains(lower, "mmproj") { |
| 897 | + continue |
| 898 | + } |
| 899 | + full := filepath.Join(dir, e.Name()) |
| 900 | + if strings.Contains(lower, "f16") || strings.Contains(lower, "fp16") { |
| 901 | + f16 = full |
| 902 | + } else if other == "" { |
| 903 | + other = full |
| 904 | + } |
| 905 | + } |
| 906 | + if f16 != "" { |
| 907 | + return f16 |
| 908 | + } |
| 909 | + return other |
| 910 | +} |
0 commit comments