Skip to content

Commit 72b4f25

Browse files
James Zhuclaude
andcommitted
feat: add static model list support for endpoints
- Add list_of_models field to EndpointConfig for static model lists - Update config parsing and validation to handle static model lists - Modify endpoint manager to use static lists when provided - Update providers.json.example to demonstrate static model usage - Refactor goose tool to use centralized fetch_models method 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e35c0e1 commit 72b4f25

7 files changed

Lines changed: 85 additions & 53 deletions

File tree

code_assistant_manager/config.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,9 +416,12 @@ def get_endpoint_config(self, endpoint_name: str) -> Dict[str, str]:
416416

417417
config = endpoints[endpoint_name].copy()
418418

419-
# Convert all values to strings for compatibility
419+
# Convert values to strings for compatibility, except for lists
420420
for key, value in config.items():
421-
if isinstance(value, bool):
421+
if isinstance(value, list):
422+
# Keep lists as-is (e.g., list_of_models)
423+
config[key] = value
424+
elif isinstance(value, bool):
422425
config[key] = str(value).lower()
423426
elif isinstance(value, (int, float)):
424427
config[key] = str(value)
@@ -578,6 +581,16 @@ def _validate_endpoint(endpoint_name: str, endpoint_config: dict) -> List[str]:
578581
if list_models_cmd and not validate_command(list_models_cmd):
579582
errors.append(f"Invalid list_models_cmd for {endpoint_name}: {list_models_cmd}")
580583

584+
# Validate list_of_models if present
585+
list_of_models = endpoint_config.get("list_of_models", None)
586+
if list_of_models is not None:
587+
if not isinstance(list_of_models, list):
588+
errors.append(f"Invalid list_of_models for {endpoint_name}: must be a list")
589+
else:
590+
for model in list_of_models:
591+
if not validate_model_id(str(model)):
592+
errors.append(f"Invalid model ID in list_of_models for {endpoint_name}: {model}")
593+
581594
# Validate boolean fields
582595
boolean_fields = ["keep_proxy_config", "use_proxy"]
583596
for field_name in boolean_fields:

code_assistant_manager/domain_models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class EndpointConfig:
4646
use_proxy: bool = False
4747
keep_proxy_config: bool = False
4848
list_models_cmd: Optional[str] = None
49+
list_of_models: Optional[List[str]] = None
4950
cache_ttl_seconds: int = 86400
5051

5152
def supports_client(self, client_name: str) -> bool:
@@ -66,6 +67,10 @@ def get_api_key_value(self) -> Optional[str]:
6667
def has_list_command(self) -> bool:
6768
"""Check if endpoint has a model list command configured."""
6869
return bool(self.list_models_cmd and self.list_models_cmd.strip())
70+
71+
def has_static_models(self) -> bool:
72+
"""Check if endpoint has a static list of models configured."""
73+
return bool(self.list_of_models and len(self.list_of_models) > 0)
6974

7075
def should_use_proxy(self) -> bool:
7176
"""Determine if proxy should be used."""

code_assistant_manager/endpoints.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,21 @@ def fetch_models(
292292
if cache_result.should_use:
293293
return True, cache_result.models
294294

295-
# Fetch fresh models
295+
# Check if static list of models is provided
296+
list_of_models = endpoint_config.get("list_of_models", None)
297+
if list_of_models is not None:
298+
if isinstance(list_of_models, list):
299+
print(f"Using static model list ({len(list_of_models)} models)")
300+
models = [m for m in list_of_models if validate_model_id(str(m))]
301+
self._model_cache.write_cache(endpoint_name, models)
302+
return True, models
303+
else:
304+
print("Warning: list_of_models is not a list, ignoring")
305+
306+
# Fetch fresh models using command
296307
list_cmd = endpoint_config.get("list_models_cmd", "")
297308
if not list_cmd:
298-
print("Warning: No list_models_cmd configured, using empty model list")
309+
print("Warning: No list_models_cmd or list_of_models configured, using empty model list")
299310
return True, []
300311

301312
print("Fetching model list...")

code_assistant_manager/repositories.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ def _parse_endpoint(self, name: str, data: Dict) -> EndpointConfig:
155155
# Parse other fields
156156
keep_proxy = str(data.get("keep_proxy_config", "false")).lower() == "true"
157157
list_models_cmd = data.get("list_models_cmd", "")
158+
list_of_models = data.get("list_of_models", None)
159+
if list_of_models is not None and not isinstance(list_of_models, list):
160+
list_of_models = None
158161
cache_ttl = 86400
159162
if self._common_cache is not None:
160163
cache_ttl = int(self._common_cache.get("cache_ttl_seconds", "86400"))
@@ -169,6 +172,7 @@ def _parse_endpoint(self, name: str, data: Dict) -> EndpointConfig:
169172
use_proxy=use_proxy,
170173
keep_proxy_config=keep_proxy,
171174
list_models_cmd=list_models_cmd,
175+
list_of_models=list_of_models,
172176
cache_ttl_seconds=cache_ttl,
173177
)
174178

code_assistant_manager/tools/goose.py

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -168,36 +168,15 @@ def _get_available_models(self, endpoint_name: str) -> Optional[List[str]]:
168168
if not success:
169169
return None
170170

171-
# Get models from list_models_cmd
172-
models = []
173-
if "list_models_cmd" in endpoint_config:
174-
try:
175-
import subprocess
176-
import shlex
177-
178-
env = os.environ.copy()
179-
env["endpoint"] = endpoint_config.get("endpoint", "")
180-
env["api_key"] = endpoint_config.get("actual_api_key", "")
181-
182-
cmd_parts = shlex.split(endpoint_config["list_models_cmd"])
183-
result = subprocess.run(
184-
cmd_parts,
185-
shell=False,
186-
capture_output=True,
187-
text=True,
188-
timeout=30,
189-
env=env,
190-
)
191-
if result.returncode == 0 and result.stdout.strip():
192-
models = [line.strip() for line in result.stdout.split('\n') if line.strip()]
193-
except Exception as e:
194-
print(f"Warning: Failed to execute list_models_cmd for {endpoint_name}: {e}")
195-
return None
196-
else:
197-
# Fallback if no list_models_cmd
198-
models = [endpoint_name.replace(":", "-").replace("_", "-")]
171+
# Use the endpoint_manager's fetch_models method for consistency
172+
success, models = self.endpoint_manager.fetch_models(
173+
endpoint_name, endpoint_config, use_cache_if_available=False
174+
)
175+
176+
if not success or not models:
177+
return None
199178

200-
return models if models else None
179+
return models
201180

202181
def _select_models_from_endpoint(self, endpoint_name: str, available_models: List[str]) -> Optional[List[str]]:
203182
"""Let user select multiple models from the given endpoint."""

code_assistant_manager/validators.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,19 +218,30 @@ def _do_validate(self, data: Dict) -> Tuple[bool, List[str]]:
218218

219219

220220
class CommandValidator(ValidationHandler):
221-
"""Validates command strings."""
221+
"""Validates command strings and model lists."""
222222

223223
def _do_validate(self, data: Dict) -> Tuple[bool, List[str]]:
224-
"""Validate command string if present."""
224+
"""Validate command string and list of models if present."""
225+
errors = []
226+
227+
# Validate list_models_cmd if present
225228
command = data.get("list_models_cmd", "")
226-
227-
# Command is optional
228-
if not command:
229-
return True, []
230-
231-
if not self._is_safe_command(command):
232-
return False, ["Command contains potentially dangerous patterns"]
233-
229+
if command and not self._is_safe_command(command):
230+
errors.append("Command contains potentially dangerous patterns")
231+
232+
# Validate list_of_models if present
233+
list_of_models = data.get("list_of_models", None)
234+
if list_of_models is not None:
235+
if not isinstance(list_of_models, list):
236+
errors.append("list_of_models must be a list")
237+
else:
238+
from .config import validate_model_id
239+
for model in list_of_models:
240+
if not validate_model_id(str(model)):
241+
errors.append(f"Invalid model ID in list_of_models: {model}")
242+
243+
if errors:
244+
return False, errors
234245
return True, []
235246

236247
def _is_safe_command(self, command: str) -> bool:

providers.json.example

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,41 +25,50 @@
2525
"openai-compatible:dashscope-qwen3-models": {
2626
"endpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1",
2727
"api_key_env": "API_KEY_QWEN",
28-
"list_models_cmd": "qwen3-max qwen3-coder-plus",
28+
"list_of_models": ["qwen3-max", "qwen3-coder-plus"],
2929
"use_proxy": false,
30-
"description": "Alibaba Dashscope Endpoint",
30+
"description": "Alibaba Dashscope Endpoint - using static model list",
3131
"supported_client": "qwen,droid,iflow,crush"
3232
},
3333
"azure-openai:east2": {
3434
"endpoint": "https://azure-openai-gpt-o1-east2.openai.azure.com/openai/v1",
3535
"api_key_env": "API_KEY_AZURE_OPENAI",
36-
"list_models_cmd": "model-router",
36+
"list_of_models": ["model-router"],
3737
"use_proxy": false,
38-
"description": "Azure Openai Endpoint",
38+
"description": "Azure Openai Endpoint - using static model list",
3939
"supported_client": "codex"
4040
},
4141
"azure-openai:zhujian0003-1688-resource": {
4242
"endpoint": "https://zhujian0003-1688-resource.services.ai.azure.com/openai/v1/",
4343
"api_key_env": "zhujian0003_1688_resource_API_KEY_AZURE_OPENAI",
44-
"list_models_cmd": "grok-3",
44+
"list_of_models": ["grok-3"],
4545
"use_proxy": false,
46-
"description": "Azure Openai Endpoint",
46+
"description": "Azure Openai Endpoint - using static model list",
4747
"supported_client": "codex"
4848
},
4949
"qwen-claude-proxy:dashscope-qwen3-coder-plus": {
5050
"endpoint": "https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
5151
"api_key_env": "API_KEY_QWEN",
52-
"list_models_cmd": "qwen3-coder-plus",
52+
"list_of_models": ["qwen3-coder-plus"],
5353
"use_proxy": false,
54-
"description": "Alibaba Dashscope Endpoint Claude Proxy",
54+
"description": "Alibaba Dashscope Endpoint Claude Proxy - using static model list",
5555
"supported_client": "claude"
5656
},
5757
"blackbox-official": {
5858
"endpoint": "https://api.blackbox.ai",
5959
"api_key_env": "BLACKBOX_API_KEY",
60-
"list_models_cmd": "echo 'blackboxai/anthropic/claude-sonnet-4.5\nblackboxai/anthropic/claude-opus-4.5\nblackboxai/anthropic/claude-sonnet-4\nblackboxai/anthropic/claude-opus-4\nblackboxai/google/gemini-2.5-flash\nblackboxai/meta-llama/llama-3.3-70b-instruct\nblackboxai/openai/gpt-4o\nblackboxai/x-ai/grok-code-fast-1:free'",
60+
"list_of_models": [
61+
"blackboxai/anthropic/claude-sonnet-4.5",
62+
"blackboxai/anthropic/claude-opus-4.5",
63+
"blackboxai/anthropic/claude-sonnet-4",
64+
"blackboxai/anthropic/claude-opus-4",
65+
"blackboxai/google/gemini-2.5-flash",
66+
"blackboxai/meta-llama/llama-3.3-70b-instruct",
67+
"blackboxai/openai/gpt-4o",
68+
"blackboxai/x-ai/grok-code-fast-1:free"
69+
],
6170
"use_proxy": false,
62-
"description": "Blackbox AI Official API",
71+
"description": "Blackbox AI Official API - using static model list",
6372
"supported_client": "blackbox"
6473
}
6574
}

0 commit comments

Comments
 (0)