Refactor model search & reorganize model_providers.yaml

keyboardstaff committed Mar 17, 2026 at 05:48 UTC 1b0c57a3e86dcb165c78e07da7483c5834f3c6f5
4 files changed +120 -90
conf/model_providers.yaml
+51 -53
@@ -16,20 +16,21 @@
16 # kwargs: A dictionary of extra parameters to pass to LiteLLM.
17 # This is useful for `api_base`, `extra_headers`, etc.
18 #
19 -# Optional model search fields (used by the Model Configuration plugin):
20 -# models_endpoint: URL or path for the model listing API.
19 +# Optional model listing fields (used by the Model Configuration plugin):
20 +# models_list:
21 +# endpoint_url: URL or path for the model listing API.
22 # Absolute URL (https://...) is used directly.
23 # Relative path (/path) is appended to api_base or default_base.
23 -# models_format: Response parsing format: "openai" (default), "google", "ollama".
24 -# models_params: Extra query parameters for the listing request.
25 -# static_models: Hardcoded model list for providers without a listing API.
26 -# default_base: Default base URL for local/self-hosted providers.
24 +# format: Response parsing format: "openai" (default), "google", "ollama".
25 +# params: Extra query parameters for the listing request.
26 +# default_base: Default base URL for local/self-hosted providers.
27
28 chat:
29 a0_venice:
30 name: Agent Zero API
31 litellm_provider: openai
32 - models_endpoint: "https://api.venice.ai/api/v1/models"
32 + models_list:
33 + endpoint_url: "https://api.venice.ai/api/v1/models"
34 kwargs:
35 api_base: https://llm.agent-zero.ai/v1
36 venice_parameters:
@@ -37,24 +38,23 @@ chat:
38 anthropic:
39 name: Anthropic
40 litellm_provider: anthropic
40 - models_endpoint: "https://api.anthropic.com/v1/models"
41 - models_params:
42 - limit: "1000"
41 + models_list:
42 + endpoint_url: "https://api.anthropic.com/v1/models"
43 + params:
44 + limit: "1000"
45 cometapi:
46 name: CometAPI
47 litellm_provider: cometapi
48 + models_list:
49 + endpoint_url: "https://api.cometapi.com/v1/models"
50 deepseek:
51 name: DeepSeek
52 litellm_provider: deepseek
49 - models_endpoint: "https://api.deepseek.com/models"
53 + models_list:
54 + endpoint_url: "https://api.deepseek.com/models"
55 github_copilot:
56 name: GitHub Copilot
57 litellm_provider: github_copilot
53 - static_models:
54 - - "gpt-4.1"
55 - - "gpt-4o"
56 - - "gpt-5-mini"
57 - - "oswe-vscode-prime"
58 kwargs:
59 extra_headers:
60 "Editor-Version": "vscode/1.85.1"
@@ -63,54 +63,63 @@ chat:
63 google:
64 name: Google
65 litellm_provider: gemini
66 - models_endpoint: "/v1beta/models"
67 - models_format: "google"
68 - models_params:
69 - pageSize: "1000"
70 - default_base: "https://generativelanguage.googleapis.com"
66 + models_list:
67 + endpoint_url: "/v1beta/models"
68 + format: "google"
69 + params:
70 + pageSize: "1000"
71 + default_base: "https://generativelanguage.googleapis.com"
72 groq:
73 name: Groq
74 litellm_provider: groq
74 - models_endpoint: "https://api.groq.com/openai/v1/models"
75 + models_list:
76 + endpoint_url: "https://api.groq.com/openai/v1/models"
77 huggingface:
78 name: HuggingFace
79 litellm_provider: huggingface
80 lm_studio:
81 name: LM Studio
82 litellm_provider: lm_studio
81 - models_endpoint: "/v1/models"
82 - default_base: "http://host.docker.internal:1234"
83 + models_list:
84 + endpoint_url: "/v1/models"
85 + default_base: "http://host.docker.internal:1234"
86 mistral:
87 name: Mistral AI
88 litellm_provider: mistral
86 - models_endpoint: "https://api.mistral.ai/v1/models"
89 + models_list:
90 + endpoint_url: "https://api.mistral.ai/v1/models"
91 moonshot:
92 name: Moonshot AI
93 litellm_provider: moonshot
90 - models_endpoint: "https://api.moonshot.cn/v1/models"
94 + models_list:
95 + endpoint_url: "https://api.moonshot.cn/v1/models"
96 ollama:
97 name: Ollama
98 litellm_provider: ollama
94 - models_endpoint: "/api/tags"
95 - models_format: "ollama"
96 - default_base: "http://host.docker.internal:11434"
99 + models_list:
100 + endpoint_url: "/api/tags"
101 + format: "ollama"
102 + default_base: "http://host.docker.internal:11434"
103 openai:
104 name: OpenAI
105 litellm_provider: openai
100 - models_endpoint: "https://api.openai.com/v1/models"
106 + models_list:
107 + endpoint_url: "https://api.openai.com/v1/models"
108 azure:
109 name: OpenAI Azure
110 litellm_provider: azure
104 - models_endpoint: "/openai/models"
105 - models_params:
106 - api-version: "2024-10-21"
111 + models_list:
112 + endpoint_url: "/openai/models"
113 + params:
114 + api-version: "2024-10-21"
115 bedrock:
116 name: AWS Bedrock
117 litellm_provider: bedrock
118 openrouter:
119 name: OpenRouter
120 litellm_provider: openrouter
113 - models_endpoint: "https://openrouter.ai/api/v1/models"
121 + models_list:
122 + endpoint_url: "https://openrouter.ai/api/v1/models"
123 kwargs:
124 extra_headers:
125 "HTTP-Referer": "https://agent-zero.ai/"
@@ -121,7 +130,8 @@ chat:
130 sambanova:
131 name: Sambanova
132 litellm_provider: sambanova
124 - models_endpoint: "https://api.sambanova.ai/v1/models"
133 + models_list:
134 + endpoint_url: "https://api.sambanova.ai/v1/models"
135 venice:
136 name: Venice.ai
137 litellm_provider: openai
@@ -132,32 +142,20 @@ chat:
142 xai:
143 name: xAI
144 litellm_provider: xai
135 - models_endpoint: "https://api.x.ai/v1/models"
145 + models_list:
146 + endpoint_url: "https://api.x.ai/v1/models"
147 zai:
148 name: Z.AI
149 litellm_provider: openai
139 - static_models:
140 - - "glm-4-plus"
141 - - "glm-4-air-250414"
142 - - "glm-4-airx"
143 - - "glm-4-long"
144 - - "glm-4-flashx"
145 - - "glm-4-flash-250414"
146 - - "glm-4v-plus"
147 - - "glm-4v"
148 - - "glm-3-turbo"
150 + models_list:
151 + endpoint_url: "/models"
152 kwargs:
153 api_base: https://api.z.ai/api/paas/v4
154 zai_coding:
155 name: Z.AI Coding
156 litellm_provider: openai
154 - static_models:
155 - - "codegeex-4"
156 - - "glm-4-plus"
157 - - "glm-4-air-250414"
158 - - "glm-4-airx"
159 - - "glm-4-flashx"
160 - - "glm-4-flash-250414"
157 + models_list:
158 + endpoint_url: "/models"
159 kwargs:
160 api_base: https://api.z.ai/api/coding/paas/v4
161 other:
plugins/_model_config/api/model_search.py
+27 -31
@@ -13,55 +13,51 @@ _LITELLM_EXCLUDE = frozenset({
13 class ModelSearch(ApiHandler):
14 async def process(self, input: dict, request: Request) -> dict | Response:
15 provider = input.get("provider", "")
16 - query = input.get("query", "").lower()
16 model_type = input.get("model_type", "chat")
17 user_api_base = input.get("api_base", "")
18
19 if not provider:
20 return {"models": []}
21
23 - cfg = self._get_search_config(model_type, provider)
22 + cfg = self._get_provider_cfg(model_type, provider)
23 + ml = self._get_models_list(cfg)
24
25 - static = cfg.get("static_models")
26 - if static:
27 - all_models = list(static)
28 - else:
29 - all_models = await self._fetch_models(provider, cfg, user_api_base) or []
25 + all_models = await self._fetch_models(provider, cfg, ml, user_api_base) or []
26
31 - if not all_models:
32 - litellm_provider = (cfg or {}).get("litellm_provider", provider)
33 - if litellm_provider == provider:
34 - all_models = self._litellm_fallback(provider, cfg)
27 + if not all_models:
28 + litellm_provider = cfg.get("litellm_provider", provider)
29 + if litellm_provider == provider:
30 + all_models = self._litellm_fallback(provider, cfg)
31
36 - if query:
37 - all_models = [m for m in all_models if query in m.lower()]
38 -
39 - return {"models": sorted(all_models)[:50], "provider": provider}
32 + return {"models": sorted(all_models), "provider": provider}
33
34 @staticmethod
42 - def _get_search_config(model_type: str, provider: str) -> dict:
43 - """Get provider config with search metadata, falling back to chat config."""
35 + def _get_provider_cfg(model_type: str, provider: str) -> dict:
36 + """Get provider config, falling back to chat config for models_list."""
37 cfg = get_provider_config(model_type, provider) or {}
45 - if model_type != "chat" and not cfg.get("models_endpoint") and not cfg.get("static_models"):
38 + if model_type != "chat" and not cfg.get("models_list"):
39 chat_cfg = get_provider_config("chat", provider) or {}
47 - merged = dict(cfg)
48 - for field in ("models_endpoint", "models_format", "models_params",
49 - "static_models", "default_base"):
50 - if field in chat_cfg and field not in merged:
51 - merged[field] = chat_cfg[field]
52 - return merged
40 + if chat_cfg.get("models_list"):
41 + merged = dict(cfg)
42 + merged["models_list"] = chat_cfg["models_list"]
43 + return merged
44 return cfg
45
55 - async def _fetch_models(self, provider: str, cfg: dict, user_api_base: str = "") -> list[str] | None:
46 + @staticmethod
47 + def _get_models_list(cfg: dict) -> dict:
48 + """Extract models_list sub-config."""
49 + return cfg.get("models_list") or {}
50 +
51 + async def _fetch_models(self, provider: str, cfg: dict, ml: dict, user_api_base: str = "") -> list[str] | None:
52 api_key = models.get_api_key(provider)
53 api_base = user_api_base or (cfg or {}).get("kwargs", {}).get("api_base", "")
54
59 - url, fmt = self._resolve_url(cfg, api_base)
55 + url, fmt = self._resolve_url(ml, api_base)
56 if not url:
57 return None
58
59 headers = self._build_headers(provider, api_key, cfg)
64 - params = dict(cfg.get("models_params", {}) or {})
60 + params = dict(ml.get("params", {}) or {})
61
62 # Google uses query-param auth
63 if provider == "google" and api_key and api_key != "None":
@@ -80,10 +76,10 @@ class ModelSearch(ApiHandler):
76 return None
77
78 @staticmethod
83 - def _resolve_url(cfg: dict, api_base: str) -> tuple[str | None, str]:
84 - fmt = cfg.get("models_format", "openai")
85 - endpoint = cfg.get("models_endpoint", "")
86 - default_base = cfg.get("default_base", "")
79 + def _resolve_url(ml: dict, api_base: str) -> tuple[str | None, str]:
80 + fmt = ml.get("format", "openai")
81 + endpoint = ml.get("endpoint_url", "")
82 + default_base = ml.get("default_base", "")
83
84 if endpoint.startswith("http"):
85 return endpoint, fmt
plugins/_model_config/webui/config.html
+30 -6
@@ -89,7 +89,8 @@
89 </div>
90 <div class="field-control" style="position:relative;"
91 x-data="{ results: [], open: false, searching: false,
92 - doSearch() { this.searching = true; $store.modelConfig.searchModels(preset.chat.provider, preset.chat.name, 'chat', preset.chat.api_base).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); }
92 + doSearch() { this.searching = true; $store.modelConfig.searchModels(preset.chat.provider, preset.chat.name, 'chat', preset.chat.api_base).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); },
93 + grouped() { return $store.modelConfig.groupResults(this.results, preset.chat.name); }
94 }"
95 @click.outside="open = false">
96 <input type="text" x-model="preset.chat.name" style="padding-right:32px;"
@@ -101,7 +102,11 @@
102 <span class="material-symbols-outlined model-search-spinner" :style="!searching && 'opacity:0'">progress_activity</span>
103 </span>
104 <div class="model-search-results" x-show="open && results.length > 0" x-transition.opacity>
104 - <template x-for="m in results" :key="m">
105 + <template x-for="m in grouped().matched" :key="'m_'+m">
106 + <div class="model-search-item matched" @click="preset.chat.name = m; open = false;" x-text="m"></div>
107 + </template>
108 + <div class="model-search-separator" x-show="grouped().matched.length > 0 && grouped().rest.length > 0"></div>
109 + <template x-for="m in grouped().rest" :key="'r_'+m">
110 <div class="model-search-item" @click="preset.chat.name = m; open = false;" x-text="m"></div>
111 </template>
112 </div>
@@ -163,7 +168,8 @@
168 </div>
169 <div class="field-control" style="position:relative;"
170 x-data="{ results: [], open: false, searching: false,
166 - doSearch() { this.searching = true; $store.modelConfig.searchModels(preset.utility.provider || preset.chat.provider, preset.utility.name, 'chat', preset.utility.api_base || preset.chat.api_base).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); }
171 + doSearch() { this.searching = true; $store.modelConfig.searchModels(preset.utility.provider || preset.chat.provider, preset.utility.name, 'chat', preset.utility.api_base || preset.chat.api_base).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); },
172 + grouped() { return $store.modelConfig.groupResults(this.results, preset.utility.name); }
173 }"
174 @click.outside="open = false">
175 <input type="text" x-model="preset.utility.name" style="padding-right:32px;"
@@ -175,7 +181,11 @@
181 <span class="material-symbols-outlined model-search-spinner" :style="!searching && 'opacity:0'">progress_activity</span>
182 </span>
183 <div class="model-search-results" x-show="open && results.length > 0" x-transition.opacity>
178 - <template x-for="m in results" :key="m">
184 + <template x-for="m in grouped().matched" :key="'m_'+m">
185 + <div class="model-search-item matched" @click="preset.utility.name = m; open = false;" x-text="m"></div>
186 + </template>
187 + <div class="model-search-separator" x-show="grouped().matched.length > 0 && grouped().rest.length > 0"></div>
188 + <template x-for="m in grouped().rest" :key="'r_'+m">
189 <div class="model-search-item" @click="preset.utility.name = m; open = false;" x-text="m"></div>
190 </template>
191 </div>
@@ -262,7 +272,8 @@
272 </div>
273 <div class="field-control" style="position:relative;"
274 x-data="{ results: [], open: false, searching: false,
265 - doSearch() { this.searching = true; $store.modelConfig.searchModels(config[section.key].provider, config[section.key].name, $store.modelConfig.getSearchType(section.key), config[section.key].api_base).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); }
275 + doSearch() { this.searching = true; $store.modelConfig.searchModels(config[section.key].provider, config[section.key].name, $store.modelConfig.getSearchType(section.key), config[section.key].api_base).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); },
276 + grouped() { return $store.modelConfig.groupResults(this.results, config[section.key].name); }
277 }"
278 @click.outside="open = false">
279 <input type="text" x-model="config[section.key].name" style="padding-right:32px;"
@@ -274,7 +285,11 @@
285 <span class="material-symbols-outlined model-search-spinner" :style="!searching && 'opacity:0'">progress_activity</span>
286 </span>
287 <div class="model-search-results" x-show="open && results.length > 0" x-transition.opacity>
277 - <template x-for="m in results" :key="m">
288 + <template x-for="m in grouped().matched" :key="'m_'+m">
289 + <div class="model-search-item matched" @click="config[section.key].name = m; open = false;" x-text="m"></div>
290 + </template>
291 + <div class="model-search-separator" x-show="grouped().matched.length > 0 && grouped().rest.length > 0"></div>
292 + <template x-for="m in grouped().rest" :key="'r_'+m">
293 <div class="model-search-item" @click="config[section.key].name = m; open = false;" x-text="m"></div>
294 </template>
295 </div>
@@ -608,6 +623,15 @@
623 cursor: default;
624 font-style: italic;
625 }
626 + .model-search-item.matched {
627 + font-weight: 500;
628 + }
629 + .model-search-separator {
630 + height: 1px;
631 + margin: 4px 8px;
632 + background: var(--color-border);
633 + opacity: 0.5;
634 + }
635 .model-search-item.disabled:hover {
636 background: transparent;
637 }
plugins/_model_config/webui/model-config-store.js
+12
@@ -168,6 +168,18 @@ export const store = createStore("modelConfig", {
168 }
169 },
170
171 + groupResults(models, query) {
172 + const q = (query || '').trim().toLowerCase();
173 + if (!q) return { matched: [], rest: models };
174 + const matched = [];
175 + const rest = [];
176 + for (const m of models) {
177 + if (m.toLowerCase().includes(q)) matched.push(m);
178 + else rest.push(m);
179 + }
180 + return { matched, rest };
181 + },
182 +
183 // Model Switcher
184 async loadSwitcherState(contextId) {
185 const result = { allowed: false, presets: [], override: null };