mirror of
https://github.com/mudler/LocalAI.git
synced 2026-08-01 19:09:42 -04:00
fix(vllm): apply Options[] engine flags before engine init (#11130) CLI-style flags in a model's `options:` array (`--quantization:gptq_marlin`, `--enable-prefix-caching`, `--kv-cache-dtype:fp8_e5m2`) were discarded: the backend only ever read `tool_parser`/`reasoning_parser` out of Options[], and did so *after* `AsyncLLMEngine.from_engine_args()`, where nothing it set could still reach the engine. Map `--` prefixed options onto the AsyncEngineArgs dataclass before the engine is constructed. Names are normalized the way vLLM's CLI spells them (`--enable-prefix-caching` -> `enable_prefix_caching`), values are coerced to the target field's type (bare flag -> True for booleans), and unknown or uncoercible flags warn and are skipped instead of failing the load, since Options[] is a bag shared with backend-level settings. Field types come from the annotation's base so `Literal["auto", "float16"]` (vLLM's dtype) is not mistaken for a float. Precedence is typed proto fields -> `options:` -> `engine_args:`. The production engine_args defaults seeded in hooks_vllm.go therefore skip any key the user already set as an option, otherwise the later engine_args pass would silently override it. Parser lookups now accept both spellings, so `--reasoning-parser:qwen3` selects LocalAI's parser as well. The helper's tests are stdlib-only and run in the lint workflow's dependency-light job via `make test-python-helpers`. Assisted-by: Claude:claude-opus-5 golangci-lint Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: localai-org-maint-bot <bot-opensource@localaisrl.com>
140 lines
3.9 KiB
Go
140 lines
3.9 KiB
Go
package config
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/mudler/xlog"
|
|
)
|
|
|
|
//go:embed parser_defaults.json
|
|
var parserDefaultsJSON []byte
|
|
|
|
type parserDefaultsData struct {
|
|
Families map[string]map[string]string `json:"families"`
|
|
Patterns []string `json:"patterns"`
|
|
}
|
|
|
|
var parsersData *parserDefaultsData
|
|
|
|
func init() {
|
|
parsersData = &parserDefaultsData{}
|
|
if err := json.Unmarshal(parserDefaultsJSON, parsersData); err != nil {
|
|
xlog.Warn("failed to parse parser_defaults.json", "error", err)
|
|
}
|
|
|
|
RegisterBackendHook("vllm", vllmDefaults)
|
|
RegisterBackendHook("vllm-omni", vllmDefaults)
|
|
}
|
|
|
|
// MatchParserDefaults returns parser defaults for the best-matching model family.
|
|
// Returns nil if no family matches. Used both at load time (via hook) and at import time.
|
|
func MatchParserDefaults(modelID string) map[string]string {
|
|
if parsersData == nil || len(parsersData.Patterns) == 0 {
|
|
return nil
|
|
}
|
|
normalized := normalizeModelID(modelID)
|
|
for _, pattern := range parsersData.Patterns {
|
|
if strings.Contains(normalized, pattern) {
|
|
if family, ok := parsersData.Families[pattern]; ok {
|
|
return family
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// productionEngineArgsDefaults are vLLM ≥ 0.6 features that production deployments
|
|
// almost always want. Applied at load time when the user hasn't set the key in
|
|
// engine_args. Anything user-supplied wins; we never silently override.
|
|
var productionEngineArgsDefaults = map[string]any{
|
|
"enable_prefix_caching": true,
|
|
"enable_chunked_prefill": true,
|
|
}
|
|
|
|
func vllmDefaults(cfg *ModelConfig, modelPath string) {
|
|
applyEngineArgDefaults(cfg)
|
|
applyParserDefaults(cfg)
|
|
}
|
|
|
|
// applyEngineArgDefaults seeds production-friendly engine_args without overwriting
|
|
// anything the user already set, in engine_args or as a CLI-style option.
|
|
func applyEngineArgDefaults(cfg *ModelConfig) {
|
|
if cfg.EngineArgs == nil {
|
|
cfg.EngineArgs = map[string]any{}
|
|
}
|
|
fromOptions := engineOptionKeys(cfg.Options)
|
|
for k, v := range productionEngineArgsDefaults {
|
|
if _, set := cfg.EngineArgs[k]; set {
|
|
continue
|
|
}
|
|
// The backend applies options: before engine_args:, so seeding a key
|
|
// the user wrote as an option would silently override it.
|
|
if _, set := fromOptions[k]; set {
|
|
continue
|
|
}
|
|
cfg.EngineArgs[k] = v
|
|
}
|
|
}
|
|
|
|
// engineOptionKeys returns the engine-arg field names carried by CLI-style
|
|
// options (`--enable-prefix-caching:false` -> `enable_prefix_caching`). Only
|
|
// `--` prefixed entries are engine flags; the rest of Options[] is
|
|
// backend-level (tool_parser:, reasoning_parser:, ...).
|
|
func engineOptionKeys(options []string) map[string]struct{} {
|
|
keys := map[string]struct{}{}
|
|
for _, opt := range options {
|
|
opt = strings.TrimSpace(opt)
|
|
if !strings.HasPrefix(opt, "--") {
|
|
continue
|
|
}
|
|
name := opt
|
|
if i := strings.IndexAny(opt, ":="); i != -1 {
|
|
name = opt[:i]
|
|
}
|
|
name = strings.ReplaceAll(strings.TrimLeft(name, "-"), "-", "_")
|
|
if name != "" {
|
|
keys[name] = struct{}{}
|
|
}
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func applyParserDefaults(cfg *ModelConfig) {
|
|
hasToolParser := false
|
|
hasReasoningParser := false
|
|
for _, opt := range cfg.Options {
|
|
if strings.HasPrefix(opt, "tool_parser:") {
|
|
hasToolParser = true
|
|
}
|
|
if strings.HasPrefix(opt, "reasoning_parser:") {
|
|
hasReasoningParser = true
|
|
}
|
|
}
|
|
if hasToolParser && hasReasoningParser {
|
|
return
|
|
}
|
|
|
|
parsers := MatchParserDefaults(cfg.Model)
|
|
if parsers == nil {
|
|
parsers = MatchParserDefaults(cfg.Name)
|
|
}
|
|
if parsers == nil {
|
|
return
|
|
}
|
|
|
|
if !hasToolParser {
|
|
if tp, ok := parsers["tool_parser"]; ok {
|
|
cfg.Options = append(cfg.Options, "tool_parser:"+tp)
|
|
xlog.Debug("[parser_defaults] auto-set tool_parser", "parser", tp, "model", cfg.Model)
|
|
}
|
|
}
|
|
if !hasReasoningParser {
|
|
if rp, ok := parsers["reasoning_parser"]; ok {
|
|
cfg.Options = append(cfg.Options, "reasoning_parser:"+rp)
|
|
xlog.Debug("[parser_defaults] auto-set reasoning_parser", "parser", rp, "model", cfg.Model)
|
|
}
|
|
}
|
|
}
|