Refactor: Data-driven model search + default presets

keyboardstaff committed Mar 16, 2026 at 02:51 UTC 09a74381050b757f094458630a061aed6fbe5a4b
3 files changed +123 -78
conf/model_providers.yaml
+56
@@ -15,11 +15,21 @@
15 # Optional fields:
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.
21 +# Absolute URL (https://...) is used directly.
22 +# 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.
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"
33 kwargs:
34 api_base: https://llm.agent-zero.ai/v1
35 venice_parameters:
@@ -27,15 +37,24 @@ chat:
37 anthropic:
38 name: Anthropic
39 litellm_provider: anthropic
40 + models_endpoint: "https://api.anthropic.com/v1/models"
41 + models_params:
42 + limit: "1000"
43 cometapi:
44 name: CometAPI
45 litellm_provider: cometapi
46 deepseek:
47 name: DeepSeek
48 litellm_provider: deepseek
49 + models_endpoint: "https://api.deepseek.com/models"
50 github_copilot:
51 name: GitHub Copilot
52 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"
@@ -44,36 +63,54 @@ 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"
71 groq:
72 name: Groq
73 litellm_provider: groq
74 + models_endpoint: "https://api.groq.com/openai/v1/models"
75 huggingface:
76 name: HuggingFace
77 litellm_provider: huggingface
78 lm_studio:
79 name: LM Studio
80 litellm_provider: lm_studio
81 + models_endpoint: "/v1/models"
82 + default_base: "http://host.docker.internal:1234"
83 mistral:
84 name: Mistral AI
85 litellm_provider: mistral
86 + models_endpoint: "https://api.mistral.ai/v1/models"
87 moonshot:
88 name: Moonshot AI
89 litellm_provider: moonshot
90 + models_endpoint: "https://api.moonshot.cn/v1/models"
91 ollama:
92 name: Ollama
93 litellm_provider: ollama
94 + models_endpoint: "/api/tags"
95 + models_format: "ollama"
96 + default_base: "http://host.docker.internal:11434"
97 openai:
98 name: OpenAI
99 litellm_provider: openai
100 + models_endpoint: "https://api.openai.com/v1/models"
101 azure:
102 name: OpenAI Azure
103 litellm_provider: azure
104 + models_endpoint: "/openai/models"
105 + models_params:
106 + api-version: "2024-10-21"
107 bedrock:
108 name: AWS Bedrock
109 litellm_provider: bedrock
110 openrouter:
111 name: OpenRouter
112 litellm_provider: openrouter
113 + models_endpoint: "https://openrouter.ai/api/v1/models"
114 kwargs:
115 extra_headers:
116 "HTTP-Referer": "https://agent-zero.ai/"
@@ -84,6 +121,7 @@ chat:
121 sambanova:
122 name: Sambanova
123 litellm_provider: sambanova
124 + models_endpoint: "https://api.sambanova.ai/v1/models"
125 venice:
126 name: Venice.ai
127 litellm_provider: openai
@@ -94,14 +132,32 @@ chat:
132 xai:
133 name: xAI
134 litellm_provider: xai
135 + models_endpoint: "https://api.x.ai/v1/models"
136 zai:
137 name: Z.AI
138 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"
149 kwargs:
150 api_base: https://api.z.ai/api/paas/v4
151 zai_coding:
152 name: Z.AI Coding
153 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"
161 kwargs:
162 api_base: https://api.z.ai/api/coding/paas/v4
163 other:
plugins/_model_config/api/model_search.py
+44 -77
@@ -3,44 +3,6 @@ from helpers.api import ApiHandler, Request, Response
3 from helpers.providers import get_provider_config
4 import models
5
6 -_CLOUD_ENDPOINTS: dict[str, str] = {
7 - "openai": "https://api.openai.com/v1/models",
8 - "anthropic": "https://api.anthropic.com/v1/models",
9 - "groq": "https://api.groq.com/openai/v1/models",
10 - "deepseek": "https://api.deepseek.com/models",
11 - "mistral": "https://api.mistral.ai/v1/models",
12 - "openrouter": "https://openrouter.ai/api/v1/models",
13 - "xai": "https://api.x.ai/v1/models",
14 - "sambanova": "https://api.sambanova.ai/v1/models",
15 - "moonshot": "https://api.moonshot.cn/v1/models",
16 - "google": "https://generativelanguage.googleapis.com/v1beta/models",
17 - "a0_venice": "https://api.venice.ai/api/v1/models",
18 - "venice": "https://api.venice.ai/api/v1/models",
19 -}
20 -
21 -# Local providers with default base URLs (no auth required).
22 -_LOCAL_DEFAULTS: dict[str, str] = {
23 - "ollama": "http://host.docker.internal:11434",
24 - "lm_studio": "http://host.docker.internal:1234",
25 -}
26 -
27 -# Providers with hardcoded model lists (no listing API available).
28 -_STATIC_MODELS: dict[str, list[str]] = {
29 - "github_copilot": [
30 - "gpt-4.1", "gpt-4o", "gpt-5-mini", "oswe-vscode-prime",
31 - ],
32 - "zai": [
33 - "glm-4-plus", "glm-4-air-250414", "glm-4-airx",
34 - "glm-4-long", "glm-4-flashx", "glm-4-flash-250414",
35 - "glm-4v-plus", "glm-4v", "glm-3-turbo",
36 - ],
37 - "zai_coding": [
38 - "codegeex-4",
39 - "glm-4-plus", "glm-4-air-250414", "glm-4-airx",
40 - "glm-4-flashx", "glm-4-flash-250414",
41 - ],
42 -}
43 -
6 # Model name substrings to exclude from litellm fallback results
7 _LITELLM_EXCLUDE = frozenset({
8 "dall-e", "gpt-image", "tts", "whisper", "audio",
@@ -58,40 +20,52 @@ class ModelSearch(ApiHandler):
20 if not provider:
21 return {"models": []}
22
61 - if provider in _STATIC_MODELS:
62 - all_models = list(_STATIC_MODELS[provider])
23 + cfg = self._get_search_config(model_type, provider)
24 +
25 + static = cfg.get("static_models")
26 + if static:
27 + all_models = list(static)
28 else:
64 - provider_cfg = get_provider_config(model_type, provider)
65 - all_models = await self._fetch_models(provider, provider_cfg, user_api_base) or []
29 + all_models = await self._fetch_models(provider, cfg, user_api_base) or []
30
31 if not all_models:
68 - litellm_provider = (provider_cfg or {}).get("litellm_provider", provider)
32 + litellm_provider = (cfg or {}).get("litellm_provider", provider)
33 if litellm_provider == provider:
70 - all_models = self._litellm_fallback(provider, provider_cfg)
34 + all_models = self._litellm_fallback(provider, cfg)
35
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}
40
77 - async def _fetch_models(self, provider: str, cfg: dict | None, user_api_base: str = "") -> list[str] | None:
78 - api_base = user_api_base or (cfg or {}).get("kwargs", {}).get("api_base", "")
41 + @staticmethod
42 + def _get_search_config(model_type: str, provider: str) -> dict:
43 + """Get provider config with search metadata, falling back to chat config."""
44 + 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"):
46 + 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
53 + return cfg
54 +
55 + async def _fetch_models(self, provider: str, cfg: dict, user_api_base: str = "") -> list[str] | None:
56 api_key = models.get_api_key(provider)
57 + api_base = user_api_base or (cfg or {}).get("kwargs", {}).get("api_base", "")
58
81 - url, fmt = self._resolve_url(provider, api_base)
59 + url, fmt = self._resolve_url(cfg, api_base)
60 if not url:
61 return None
62
63 headers = self._build_headers(provider, api_key, cfg)
86 - params: dict[str, str] = {}
87 - if provider == "google":
88 - if api_key and api_key != "None":
89 - params["key"] = api_key
90 - params["pageSize"] = "1000"
91 - elif provider == "anthropic":
92 - params["limit"] = "1000"
93 - elif provider == "azure":
94 - params["api-version"] = "2024-10-21"
64 + params = dict(cfg.get("models_params", {}) or {})
65 +
66 + # Google uses query-param auth
67 + if provider == "google" and api_key and api_key != "None":
68 + params.setdefault("key", api_key)
69
70 try:
71 async with httpx.AsyncClient(timeout=10.0) as client:
@@ -105,31 +79,24 @@ class ModelSearch(ApiHandler):
79
80 return None
81
108 - def _resolve_url(self, provider: str, api_base: str) -> tuple[str | None, str]:
109 - if provider == "ollama":
110 - base = api_base or _LOCAL_DEFAULTS.get("ollama", "")
111 - return (base.rstrip("/") + "/api/tags" if base else None), "ollama"
112 -
113 - if provider == "google":
114 - if api_base:
115 - return api_base.rstrip("/") + "/models", "google"
116 - return _CLOUD_ENDPOINTS["google"], "google"
117 -
118 - if provider == "azure":
119 - if not api_base:
120 - return None, "openai"
121 - return api_base.rstrip("/") + "/openai/models", "openai"
82 + @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", "")
87
123 - if provider in _CLOUD_ENDPOINTS:
124 - return _CLOUD_ENDPOINTS[provider], "openai"
88 + if endpoint.startswith("http"):
89 + return endpoint, fmt
90
126 - if api_base:
127 - return api_base.rstrip("/") + "/models", "openai"
91 + base = api_base or default_base
92 + if not base:
93 + return None, fmt
94
129 - if provider in _LOCAL_DEFAULTS:
130 - return _LOCAL_DEFAULTS[provider] + "/v1/models", "openai"
95 + if endpoint:
96 + return base.rstrip("/") + endpoint, fmt
97
132 - return None, "openai"
98 + # Generic fallback: base + /models
99 + return base.rstrip("/") + "/models", fmt
100
101 def _build_headers(self, provider: str, api_key: str, cfg: dict | None) -> dict[str, str]:
102 headers: dict[str, str] = {}
plugins/_model_config/default_config.yaml
+23 -1
@@ -32,4 +32,26 @@ embedding_model:
32
33 browser_http_headers: {}
34
35 -model_presets: []
35 +model_presets:
36 + - name: "Efficiency"
37 + chat:
38 + provider: "openrouter"
39 + name: "openai/gpt-5.2-chat"
40 + api_key: ""
41 + api_base: ""
42 + utility:
43 + provider: "openrouter"
44 + name: "openai/gpt-5-nano"
45 + api_key: ""
46 + api_base: ""
47 + - name: "Intelligence"
48 + chat:
49 + provider: "openrouter"
50 + name: "anthropic/claude-opus-4.6"
51 + api_key: ""
52 + api_base: ""
53 + utility:
54 + provider: "openrouter"
55 + name: "google/gemini-3-flash-preview"
56 + api_key: ""
57 + api_base: ""