Skip to content

Commit 0e0221b

Browse files
localai-botmudler
andauthored
fix(vision): probe the media marker for pinned llama.cpp backend variants (#10955)
llama.cpp picks a random per-process media marker (ggml-org/llama.cpp#21962), so LocalAI renders the prompt with a "<__media__>" sentinel and swaps in the backend's real marker after probing ModelMetadata. That probe was gated on an exact match against "llama-cpp", the gallery's meta backend name. A model config pinning a concrete build ("vulkan-llama-cpp", "cuda12-llama-cpp", "rocm-llama-cpp", ... and their -development counterparts) runs the same llama.cpp gRPC server but skipped the probe, so MediaMarker stayed empty, no substitution happened, and the prompt reached mtmd still carrying the sentinel. mtmd_tokenize then counted zero markers against one bitmap and every image request failed with "Failed to tokenize prompt". The same early return also skipped thinking-mode detection and tool-format marker extraction, so a pinned variant silently lost reasoning and native tool-call parsing too. Add IsLlamaCppBackend, which recognises the whole variant family (plus the empty auto-detect name, which resolves to llama.cpp) while excluding ik-llama.cpp, a separate engine that merely shares the suffix. Fixes #10945 Assisted-by: Claude Code:claude-opus-4-8[1m] [Read] [Edit] [Bash] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent fb4c61d commit 0e0221b

4 files changed

Lines changed: 153 additions & 1 deletion

File tree

core/config/backend_capabilities.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,40 @@ func NormalizeBackendName(backend string) string {
621621
return strings.ReplaceAll(backend, ".", "-")
622622
}
623623

624+
// llamaCppChannelSuffixes are the release-channel suffixes appended to a
625+
// llama.cpp backend name in the gallery ("llama-cpp" vs
626+
// "llama-cpp-development"). They carry no engine information, so they are
627+
// stripped before the family check below.
628+
var llamaCppChannelSuffixes = []string{"-development", "-quantization"}
629+
630+
// IsLlamaCppBackend reports whether a backend name refers to a build of the
631+
// llama.cpp gRPC server. The gallery ships one concrete backend per hardware
632+
// capability ("vulkan-llama-cpp", "cuda12-llama-cpp", "metal-llama-cpp", ...)
633+
// behind the "llama-cpp" meta name, and an operator may pin any of them in a
634+
// model config. They all run the same server, so anything gated on "is this
635+
// llama.cpp" must accept the whole family: an exact match against "llama-cpp"
636+
// silently skips every pinned variant (see #10945, where skipping the media
637+
// marker probe broke all vision requests).
638+
//
639+
// The empty name matches too: it is the GGUF auto-detect path, which resolves
640+
// to llama.cpp.
641+
//
642+
// ik-llama.cpp is deliberately excluded. It is a separate engine with its own
643+
// gRPC server that happens to share the "-llama-cpp" suffix.
644+
func IsLlamaCppBackend(backend string) bool {
645+
name := NormalizeBackendName(backend)
646+
if name == "" {
647+
return true
648+
}
649+
for _, suffix := range llamaCppChannelSuffixes {
650+
name = strings.TrimSuffix(name, suffix)
651+
}
652+
if strings.HasSuffix(name, "ik-llama-cpp") {
653+
return false
654+
}
655+
return name == "llama-cpp" || strings.HasSuffix(name, "-llama-cpp")
656+
}
657+
624658
// nonLlamaSamplerBackends lists backends whose native sampler defaults differ
625659
// from llama.cpp's, so LocalAI must NOT inject llama.cpp's top_k=40 default for
626660
// them (issue #6632). mlx_lm's intended default is top_k=0 (disabled) and mlx

core/config/backend_capabilities_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,39 @@ var _ = Describe("IsValidUsecaseForBackend", func() {
109109
})
110110
})
111111

112+
var _ = Describe("IsLlamaCppBackend", func() {
113+
DescribeTable("classifies a backend name",
114+
func(backend string, expected bool) {
115+
Expect(IsLlamaCppBackend(backend)).To(Equal(expected))
116+
},
117+
Entry("meta name", "llama-cpp", true),
118+
Entry("dotted spelling", "llama.cpp", true),
119+
Entry("auto-detect (empty)", "", true),
120+
Entry("development channel", "llama-cpp-development", true),
121+
Entry("quantization channel", "llama-cpp-quantization", true),
122+
Entry("vulkan variant", "vulkan-llama-cpp", true),
123+
Entry("cuda 12 variant", "cuda12-llama-cpp", true),
124+
Entry("cuda 13 variant", "cuda13-llama-cpp", true),
125+
Entry("jetson variant", "cuda13-nvidia-l4t-arm64-llama-cpp", true),
126+
Entry("rocm variant", "rocm-llama-cpp", true),
127+
Entry("metal variant", "metal-llama-cpp", true),
128+
Entry("intel sycl f16 variant", "intel-sycl-f16-llama-cpp", true),
129+
Entry("intel sycl f32 variant", "intel-sycl-f32-llama-cpp", true),
130+
Entry("cpu variant", "cpu-llama-cpp", true),
131+
Entry("variant on the development channel", "rocm-llama-cpp-development", true),
132+
Entry("darwin quantization variant", "metal-darwin-arm64-llama-cpp-quantization", true),
133+
// ik-llama.cpp is a distinct engine that merely shares the suffix.
134+
Entry("ik-llama-cpp", "ik-llama-cpp", false),
135+
Entry("ik-llama-cpp development", "ik-llama-cpp-development", false),
136+
Entry("cpu ik-llama-cpp", "cpu-ik-llama-cpp", false),
137+
Entry("cpu ik-llama-cpp development", "cpu-ik-llama-cpp-development", false),
138+
Entry("vllm", "vllm", false),
139+
Entry("mlx", "mlx", false),
140+
Entry("whisper", "whisper", false),
141+
Entry("bark-cpp", "bark-cpp", false),
142+
)
143+
})
144+
112145
var _ = Describe("AllBackendNames", func() {
113146
It("returns 30+ backends in sorted order", func() {
114147
names := AllBackendNames()

core/config/gguf.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ func DetectThinkingSupportFromBackend(ctx context.Context, cfg *ModelConfig, bac
142142

143143
// Only llama-cpp exposes ModelMetadata today. Other backends will either error
144144
// or return an empty response — both are fine, we just bail before calling.
145-
if cfg.Backend != "llama-cpp" {
145+
// The check must cover every llama.cpp build, not just the "llama-cpp" meta
146+
// name: a config pinning a concrete variant ("vulkan-llama-cpp", ...) runs the
147+
// same server and needs the same probe (#10945).
148+
if !IsLlamaCppBackend(cfg.Backend) {
146149
xlog.Debug("[gguf] DetectThinkingSupportFromBackend: skipping detection", "backend", cfg.Backend)
147150
return
148151
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package config
2+
3+
import (
4+
"context"
5+
6+
"github.com/mudler/LocalAI/pkg/grpc"
7+
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
8+
9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
grpclib "google.golang.org/grpc"
12+
)
13+
14+
// markerBackend answers ModelMetadata with a fixed marker and records whether it
15+
// was called at all. Embedding grpc.Backend keeps the rest of the (large)
16+
// interface unimplemented on purpose: DetectThinkingSupportFromBackend must only
17+
// reach for ModelMetadata, and any other call would panic loudly.
18+
type markerBackend struct {
19+
grpc.Backend
20+
marker string
21+
called bool
22+
}
23+
24+
func (m *markerBackend) ModelMetadata(ctx context.Context, in *pb.ModelOptions, opts ...grpclib.CallOption) (*pb.ModelMetadataResponse, error) {
25+
m.called = true
26+
return &pb.ModelMetadataResponse{MediaMarker: m.marker}, nil
27+
}
28+
29+
var _ = Describe("media marker probing across llama.cpp backend variants", func() {
30+
// llama.cpp picks a random per-process media marker (ggml-org/llama.cpp#21962),
31+
// so the marker MUST come from the backend. A config that pins a concrete
32+
// variant name still runs the same llama.cpp gRPC server and must be probed,
33+
// or the rendered prompt keeps LocalAI's "<__media__>" sentinel and
34+
// mtmd_tokenize reports zero markers against one bitmap (#10945).
35+
const backendMarker = "<__media_9fJqQpVb2hZK3nT7__>"
36+
37+
probe := func(backend string) *ModelConfig {
38+
cfg := &ModelConfig{}
39+
cfg.Backend = backend
40+
client := &markerBackend{marker: backendMarker}
41+
DetectThinkingSupportFromBackend(context.Background(), cfg, client, &pb.ModelOptions{})
42+
return cfg
43+
}
44+
45+
DescribeTable("captures the backend-reported marker",
46+
func(backend string) {
47+
Expect(probe(backend).MediaMarker).To(Equal(backendMarker))
48+
},
49+
Entry("meta backend", "llama-cpp"),
50+
Entry("auto-detected (empty) backend", ""),
51+
Entry("development channel", "llama-cpp-development"),
52+
Entry("vulkan variant", "vulkan-llama-cpp"),
53+
Entry("vulkan development variant", "vulkan-llama-cpp-development"),
54+
Entry("cuda 12 variant", "cuda12-llama-cpp"),
55+
Entry("cuda 13 jetson variant", "cuda13-nvidia-l4t-arm64-llama-cpp"),
56+
Entry("rocm variant", "rocm-llama-cpp"),
57+
Entry("metal variant", "metal-llama-cpp"),
58+
Entry("intel sycl variant", "intel-sycl-f16-llama-cpp"),
59+
Entry("cpu variant", "cpu-llama-cpp"),
60+
Entry("quantization variant", "llama-cpp-quantization"),
61+
)
62+
63+
DescribeTable("leaves the marker untouched for backends that are not llama.cpp",
64+
func(backend string) {
65+
cfg := &ModelConfig{}
66+
cfg.Backend = backend
67+
client := &markerBackend{marker: backendMarker}
68+
DetectThinkingSupportFromBackend(context.Background(), cfg, client, &pb.ModelOptions{})
69+
Expect(client.called).To(BeFalse(), "backend %q must not be probed", backend)
70+
Expect(cfg.MediaMarker).To(BeEmpty())
71+
},
72+
// ik-llama.cpp is a separate engine with its own gRPC server that still
73+
// uses mtmd_default_marker(), so the sentinel already matches and there
74+
// is nothing to probe.
75+
Entry("ik-llama-cpp", "ik-llama-cpp"),
76+
Entry("ik-llama-cpp development", "ik-llama-cpp-development"),
77+
Entry("cpu ik-llama-cpp", "cpu-ik-llama-cpp"),
78+
Entry("vllm", "vllm"),
79+
Entry("mlx", "mlx"),
80+
Entry("whisper", "whisper"),
81+
)
82+
})

0 commit comments

Comments
 (0)