259 lines
7.5 KiB
Go
259 lines
7.5 KiB
Go
package meta_test
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
. "github.com/onsi/gomega"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/config/meta"
|
|
)
|
|
|
|
// TestAllFieldsHaveRegistryEntries fails when a NEW ModelConfig field
|
|
// is added without either a registry entry in DefaultRegistry() or an
|
|
// entry in the grandfatheredUnregistered baseline below.
|
|
//
|
|
// Why this matters: fields without a registry entry render in the UI
|
|
// with no description, the default `input` (single-line) component,
|
|
// and land in the catch-all "other" section — which is what we just
|
|
// hit for router.classifier_system_template. The reflection-based
|
|
// fallback produces something that works mechanically but is hostile
|
|
// to operators.
|
|
//
|
|
// How to fix when this test fails:
|
|
//
|
|
// 1. Preferred — add a registry entry to DefaultRegistry() in
|
|
// registry.go with Section, Label, Description, and Component.
|
|
// See e.g. "router.classifier" or "template.chat" for the pattern.
|
|
//
|
|
// 2. Escape hatch — append the field path to
|
|
// grandfatheredUnregistered with a one-line comment justifying
|
|
// why it has no UI surface (internal, deprecated, legacy
|
|
// compatibility shim, etc.). The expectation is that this list
|
|
// shrinks over time as fields get proper registry entries; it
|
|
// should never grow without good reason.
|
|
//
|
|
// The grandfathered list was seeded from a one-time audit. Migrating
|
|
// the existing 150+ entries to proper registry metadata is out of
|
|
// scope for any single PR; the lock just stops the list from growing.
|
|
func TestAllFieldsHaveRegistryEntries(t *testing.T) {
|
|
g := NewWithT(t)
|
|
md := meta.BuildConfigMetadata(reflect.TypeOf(config.ModelConfig{}))
|
|
reg := meta.DefaultRegistry()
|
|
|
|
grand := make(map[string]struct{}, len(grandfatheredUnregistered))
|
|
for _, p := range grandfatheredUnregistered {
|
|
grand[p] = struct{}{}
|
|
}
|
|
|
|
var missing []string
|
|
for _, f := range md.Fields {
|
|
if _, ok := reg[f.Path]; ok {
|
|
continue
|
|
}
|
|
if _, ok := grand[f.Path]; ok {
|
|
continue
|
|
}
|
|
missing = append(missing, f.Path)
|
|
}
|
|
|
|
sort.Strings(missing)
|
|
g.Expect(missing).To(BeEmpty(),
|
|
"%d config field(s) have no registry entry and are not on the grandfathered list.\n"+
|
|
"Add a registry entry to core/config/meta/registry.go OR append to grandfatheredUnregistered in this file with a justification:\n %s",
|
|
len(missing), strings.Join(missing, "\n "),
|
|
)
|
|
|
|
// Inverse drift check: catch dead entries on the grandfathered
|
|
// list (field was renamed/removed, or someone wrote a registry
|
|
// entry without trimming the grandfathered duplicate).
|
|
known := make(map[string]struct{}, len(md.Fields))
|
|
for _, f := range md.Fields {
|
|
known[f.Path] = struct{}{}
|
|
}
|
|
var stale, duplicated []string
|
|
for _, p := range grandfatheredUnregistered {
|
|
if _, ok := known[p]; !ok {
|
|
stale = append(stale, p)
|
|
}
|
|
if _, ok := reg[p]; ok {
|
|
duplicated = append(duplicated, p)
|
|
}
|
|
}
|
|
sort.Strings(stale)
|
|
g.Expect(stale).To(BeEmpty(),
|
|
"grandfatheredUnregistered references fields that no longer exist in ModelConfig — remove them:\n %s",
|
|
strings.Join(stale, "\n "))
|
|
sort.Strings(duplicated)
|
|
g.Expect(duplicated).To(BeEmpty(),
|
|
"grandfatheredUnregistered references fields that now HAVE a registry entry — remove them so the test stays meaningful:\n %s",
|
|
strings.Join(duplicated, "\n "))
|
|
}
|
|
|
|
// grandfatheredUnregistered is the baseline of config fields that
|
|
// pre-date the registry-coverage test and have no UI metadata yet.
|
|
// Adding new entries here should be a deliberate, justified decision
|
|
// — prefer adding a registry entry in registry.go instead.
|
|
//
|
|
// Keep the list sorted (one-line-per-entry) so the diff is minimal
|
|
// when an entry is removed or (rarely) added.
|
|
var grandfatheredUnregistered = []string{
|
|
"agent.disable_sink_state",
|
|
"agent.enable_mcp_prompts",
|
|
"agent.enable_plan_re_evaluator",
|
|
"agent.enable_planning",
|
|
"agent.enable_reasoning",
|
|
"agent.force_reasoning_tool",
|
|
"agent.loop_detection",
|
|
"agent.max_adjustment_attempts",
|
|
"agent.max_attempts",
|
|
"agent.max_iterations",
|
|
"cfg_scale",
|
|
"chat_template_kwargs",
|
|
"concurrency_groups",
|
|
"cutstrings",
|
|
"debug",
|
|
"diffusers.clip_model",
|
|
"diffusers.clip_skip",
|
|
"diffusers.clip_subfolder",
|
|
"diffusers.control_net",
|
|
"diffusers.enable_parameters",
|
|
"diffusers.img2img",
|
|
"disable_log_stats",
|
|
"disabled",
|
|
"download_files",
|
|
"draft_model",
|
|
"dtype",
|
|
"enforce_eager",
|
|
"engine_args",
|
|
"extract_regex",
|
|
"feature_flags",
|
|
"function.argument_regex",
|
|
"function.argument_regex_key_name",
|
|
"function.argument_regex_value_name",
|
|
"function.automatic_tool_parsing_fallback",
|
|
"function.capture_llm_results",
|
|
"function.disable_no_action",
|
|
"function.disable_peg_parser",
|
|
"function.function_arguments_key",
|
|
"function.function_name_key",
|
|
"function.grammar.disable_parallel_new_lines",
|
|
"function.grammar.expect_strings_after_json",
|
|
"function.grammar.no_mixed_free_string",
|
|
"function.grammar.prefix",
|
|
"function.grammar.properties_order",
|
|
"function.grammar.schema_type",
|
|
"function.grammar.triggers",
|
|
"function.json_regex_match",
|
|
"function.no_action_description_name",
|
|
"function.no_action_function_name",
|
|
"function.replace_function_results",
|
|
"function.replace_llm_results",
|
|
"function.response_regex",
|
|
"function.xml_format.allow_toolcall_in_think",
|
|
"function.xml_format.key_start",
|
|
"function.xml_format.key_val_sep",
|
|
"function.xml_format.key_val_sep2",
|
|
"function.xml_format.last_tool_end",
|
|
"function.xml_format.last_val_end",
|
|
"function.xml_format.raw_argval",
|
|
"function.xml_format.scope_end",
|
|
"function.xml_format.scope_start",
|
|
"function.xml_format.tool_end",
|
|
"function.xml_format.tool_sep",
|
|
"function.xml_format.tool_start",
|
|
"function.xml_format.trim_raw_argval",
|
|
"function.xml_format.val_end",
|
|
"function.xml_format_preset",
|
|
"gpu_memory_utilization",
|
|
"grammar",
|
|
"grpc.attempts",
|
|
"grpc.attempts_sleep_time",
|
|
"limit_mm_per_prompt.audio",
|
|
"limit_mm_per_prompt.image",
|
|
"limit_mm_per_prompt.video",
|
|
"limits.max_concurrent",
|
|
"limits.retry_after_seconds",
|
|
"load_format",
|
|
"lora_adapter",
|
|
"lora_adapters",
|
|
"lora_base",
|
|
"lora_scale",
|
|
"lora_scales",
|
|
"main_gpu",
|
|
"max_model_len",
|
|
"mcp.remote",
|
|
"mcp.stdio",
|
|
"mirostat",
|
|
"mirostat_eta",
|
|
"mirostat_tau",
|
|
"mmproj",
|
|
"n_draft",
|
|
"ngqa",
|
|
"no_kv_offloading",
|
|
"no_mulmatq",
|
|
"numa",
|
|
"options",
|
|
"overrides",
|
|
"parameters.batch",
|
|
"parameters.clip_skip",
|
|
"parameters.echo",
|
|
"parameters.encoding_format",
|
|
"parameters.frequency_penalty",
|
|
"parameters.ignore_eos",
|
|
"parameters.language",
|
|
"parameters.logit_bias",
|
|
"parameters.logprobs",
|
|
"parameters.min_p",
|
|
"parameters.model",
|
|
"parameters.n",
|
|
"parameters.n_keep",
|
|
"parameters.negative_prompt",
|
|
"parameters.negative_prompt_scale",
|
|
"parameters.presence_penalty",
|
|
"parameters.repeat_last_n",
|
|
"parameters.rope_freq_base",
|
|
"parameters.rope_freq_scale",
|
|
"parameters.tfz",
|
|
"parameters.tokenizer",
|
|
"parameters.top_logprobs",
|
|
"parameters.translate",
|
|
"parameters.typical_p",
|
|
"pinned",
|
|
"prompt_cache_all",
|
|
"prompt_cache_path",
|
|
"prompt_cache_ro",
|
|
"proxy.api_key_file",
|
|
"reasoning.disable",
|
|
"reasoning.disable_reasoning_tag_prefill",
|
|
"reasoning.strip_reasoning_only",
|
|
"reasoning.tag_pairs",
|
|
"reasoning.thinking_start_tokens",
|
|
"reranking",
|
|
"rms_norm_eps",
|
|
"roles",
|
|
"rope_scaling",
|
|
"step",
|
|
"stopwords",
|
|
"swap_space",
|
|
"system_prompt",
|
|
"template.edit",
|
|
"template.join_chat_messages_by_character",
|
|
"template.multimodal",
|
|
"template.reply_prefix",
|
|
"tensor_parallel_size",
|
|
"tensor_split",
|
|
"trimspace",
|
|
"trimsuffix",
|
|
"trust_remote_code",
|
|
"tts.audio_path",
|
|
"type",
|
|
"yarn_attn_factor",
|
|
"yarn_beta_fast",
|
|
"yarn_beta_slow",
|
|
"yarn_ext_factor",
|
|
}
|