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 };