Files
LocalAI/core/http/endpoints/ollama/capabilities.go
LocalAI [bot] b0959d4756 feat(api): add GET /v1/models/capabilities endpoint (#10687)
Additive superset of /v1/models that enriches each model entry with the
capabilities it supports plus its input/output modalities
(text / image / audio / video). Clients that only understand /v1/models
are unaffected -- they simply never call the new route.

Audio and video *input* are derived from the model's multimodal limits
(vLLM limit_mm_per_prompt), which no single usecase FLAG expresses. That
gap is exactly why a plain capability list is insufficient and this
enriched endpoint exists: an attachment router can now decide whether an
image/audio/video file can go to the active model directly, or must be
converted/transcribed first.

Capability derivation lives in core/config as the single source of truth
(ModelConfig.Capabilities / InputModalities / OutputModalities /
VisionSupported / ...); the Ollama capability surface now delegates to
it instead of keeping a parallel copy. Vision is gated on
chat/completion capability so a MediaMarker hydrated onto a non-chat
model (e.g. a pure ASR/TTS backend) no longer reports a false vision
capability.

Read-only listing: no new FLAG_* flag, reuses the existing `models`
swagger tag, and intentionally exposes no MCP admin tool (there is
nothing to manage conversationally).

Assisted-by: Claude:claude-opus-4-8 [Claude Code]

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-05 08:51:55 +02:00

104 lines
3.2 KiB
Go

package ollama
import (
"regexp"
"strings"
"github.com/mudler/LocalAI/core/config"
)
// modelCapabilities maps a LocalAI ModelConfig to the Ollama capability strings
// (https://github.com/ollama/ollama/blob/main/docs/api.md#show-model-information).
//
// Ollama clients use these to decide which models are eligible for a given task
// (e.g. only allow embedding models in an "embedding model" picker). Returning
// an empty list makes clients assume "completion" everywhere, which is wrong
// for embedding/rerank/audio backends — see issue #9760.
func modelCapabilities(cfg *config.ModelConfig) []string {
if cfg == nil {
return nil
}
var caps []string
if cfg.HasUsecases(config.FLAG_EMBEDDINGS) {
caps = append(caps, "embedding")
}
chatCapable := cfg.HasUsecases(config.FLAG_CHAT) || cfg.HasUsecases(config.FLAG_COMPLETION)
if chatCapable {
caps = append(caps, "completion")
}
if chatCapable && hasVisionSupport(cfg) {
caps = append(caps, "vision")
}
if chatCapable && hasToolSupport(cfg) {
caps = append(caps, "tools")
}
if chatCapable && hasThinkingSupport(cfg) {
caps = append(caps, "thinking")
}
if chatCapable && cfg.TemplateConfig.Completion != "" {
caps = append(caps, "insert")
}
return caps
}
// hasVisionSupport reports whether the model can accept image inputs.
// The detection heuristic is the canonical config.ModelConfig.VisionSupported —
// kept as a thin wrapper here so the Ollama capability mapping reads cleanly.
func hasVisionSupport(cfg *config.ModelConfig) bool {
return cfg.VisionSupported()
}
// hasToolSupport reports whether the model is wired up for tool / function
// calling. Delegates to the canonical config.ModelConfig.ToolSupported.
func hasToolSupport(cfg *config.ModelConfig) bool {
return cfg.ToolSupported()
}
// hasThinkingSupport reports whether the model has reasoning / thinking enabled.
// Delegates to the canonical config.ModelConfig.ThinkingSupported.
func hasThinkingSupport(cfg *config.ModelConfig) bool {
return cfg.ThinkingSupported()
}
// quantRegex matches GGUF-style quantization suffixes (Q4_K_M, Q8_0, IQ3_XS, F16, ...).
// Matches the convention used by GGUF tooling and what ggml-org/llama.cpp report.
var quantRegex = regexp.MustCompile(`(?i)(IQ\d+(?:_[A-Z0-9]+)*|Q\d+(?:_[A-Z0-9]+)*|F16|F32|BF16)`)
// paramSizeRegex matches a parameter-size token surrounded by separators
// (e.g. "-7B-", "_3b.", ".70B-"). Avoids matching the "7" inside "Qwen3".
var paramSizeRegex = regexp.MustCompile(`(?i)(?:^|[-_.])(\d+(?:\.\d+)?[BM])(?:[-_.]|$)`)
// extractQuantizationLevel pulls the quantization tag from the model filename.
// Returns the uppercased token (e.g. "Q4_K_M") or "" when not present.
func extractQuantizationLevel(modelFile string) string {
if modelFile == "" {
return ""
}
base := strings.TrimSuffix(modelFile, ".gguf")
if m := quantRegex.FindString(base); m != "" {
return strings.ToUpper(m)
}
return ""
}
// extractParameterSize pulls the parameter count from the model filename.
// Returns "" when no recognizable token is present.
func extractParameterSize(modelFile string) string {
if modelFile == "" {
return ""
}
base := strings.TrimSuffix(modelFile, ".gguf")
if m := paramSizeRegex.FindStringSubmatch(base); len(m) > 1 {
return strings.ToUpper(m[1])
}
return ""
}