mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-17 21:21:23 -04:00
Ollama-compatible clients (Open WebUI, Enchanted, ollama-grid-search,
etc.) rely on the `capabilities` list and `details.{parameter_size,
quantization_level,families}` fields returned by /api/tags and
/api/show to decide which models are eligible for a given task --
for example to filter the "embedding model" picker. Upstream Ollama
returns these; LocalAI's compat layer was leaving them empty, so
embedding models were silently rejected by clients that only allow
chat models for chat and only allow embedding models for embeddings.
This wires up the existing config signals already present in
ModelConfig:
- modelCapabilities() derives the Ollama capability strings from the
config: "embedding" (FLAG_EMBEDDINGS), "completion" (FLAG_CHAT /
FLAG_COMPLETION), "vision" (explicit KnownUsecases bit or MMProj /
multimodal template / backend media marker), "tools" (auto-detected
ToolFormatMarkers, JSON/Response regex, XML format, grammar
triggers), "thinking" (ReasoningConfig with reasoning not disabled)
and "insert" (presence of a completion template).
- modelDetailsFromModelConfig() now fills families, parameter_size
and quantization_level. The latter two are parsed from the GGUF
filename via regex -- conservative tokens only (Q*/IQ*/F16/F32/BF16
and \d+(\.\d+)?[BM] surrounded by separators) so we don't accidentally
match "Qwen3" as "3B".
- modelInfoFromModelConfig() exposes general.architecture and
general.context_length in the new ShowResponse.model_info map.
Note: HasUsecases(FLAG_VISION) cannot be used directly -- GuessUsecases
has no FLAG_VISION case and returns true at the end for any chat model.
hasVisionSupport() instead reads KnownUsecases explicitly plus MMProj /
template / media-marker signals.
Tests are written first (TDD) using Ginkgo/Gomega -- DescribeTable for
the capability mapping (embedding-only, chat, vision, thinking, tools
via markers, tools via JSON regex, no-capability rerank) plus
integration tests against ShowModelEndpoint that round-trip JSON
through a real ModelConfigLoader populated from a temp YAML file.
Fixes #9760.
Assisted-by: Claude Code:claude-opus-4-7
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
143 lines
4.5 KiB
Go
143 lines
4.5 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. We avoid
|
|
// cfg.HasUsecases(FLAG_VISION) because GuessUsecases has no FLAG_VISION case
|
|
// and returns true for any chat model — see core/config/model_config.go. Instead
|
|
// we look for explicit signals: KnownUsecases bit, multimodal projector, or
|
|
// template/backend-reported multimodal markers.
|
|
func hasVisionSupport(cfg *config.ModelConfig) bool {
|
|
if cfg.KnownUsecases != nil && (*cfg.KnownUsecases&config.FLAG_VISION) == config.FLAG_VISION {
|
|
return true
|
|
}
|
|
if cfg.MMProj != "" {
|
|
return true
|
|
}
|
|
if cfg.TemplateConfig.Multimodal != "" {
|
|
return true
|
|
}
|
|
if cfg.MediaMarker != "" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasToolSupport reports whether the model is wired up for tool / function calling.
|
|
// We look for any of the explicit configuration knobs LocalAI uses to drive
|
|
// function-call extraction (regex match, response regex, grammar triggers, XML
|
|
// format) or for the auto-detected tool-format markers populated by the
|
|
// llama.cpp backend during model load.
|
|
func hasToolSupport(cfg *config.ModelConfig) bool {
|
|
fc := cfg.FunctionsConfig
|
|
if fc.ToolFormatMarkers != nil && fc.ToolFormatMarkers.FormatType != "" {
|
|
return true
|
|
}
|
|
if len(fc.JSONRegexMatch) > 0 || len(fc.ResponseRegex) > 0 {
|
|
return true
|
|
}
|
|
if fc.XMLFormatPreset != "" || fc.XMLFormat != nil {
|
|
return true
|
|
}
|
|
if len(fc.GrammarConfig.GrammarTriggers) > 0 || fc.GrammarConfig.SchemaType != "" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasThinkingSupport reports whether the model has reasoning / thinking enabled.
|
|
// LocalAI sets DisableReasoning=false (or leaves thinking markers configured)
|
|
// when the backend probe reports that the model supports thinking.
|
|
func hasThinkingSupport(cfg *config.ModelConfig) bool {
|
|
rc := cfg.ReasoningConfig
|
|
if rc.DisableReasoning != nil && !*rc.DisableReasoning {
|
|
return true
|
|
}
|
|
if len(rc.ThinkingStartTokens) > 0 || len(rc.TagPairs) > 0 {
|
|
// Explicit thinking markers imply support unless explicitly disabled.
|
|
return rc.DisableReasoning == nil || !*rc.DisableReasoning
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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 ""
|
|
}
|