Skip to content

Commit 7e91807

Browse files
jzhuclaude
andcommitted
feat: add litellm models integration, improve config validation, and reorganize CLI imports
- Add litellm_models.py for fetching models from Litellm API - Enhance config loading with better JSON error handling - Move CLI imports to top to fix E402 linting errors - Update tests for mock return codes Co-Authored-By: Droid <noreply@anthropic.com>
1 parent 8de8fe6 commit 7e91807

4 files changed

Lines changed: 158 additions & 11 deletions

File tree

code_assistant_manager/cli.py

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
import typer
1616
from typer import Context
1717

18-
logger = logging.getLogger(__name__)
19-
2018
from code_assistant_manager.config import ConfigManager
2119
from code_assistant_manager.mcp.cli import app as mcp_app
2220
from code_assistant_manager.tools import (
@@ -25,6 +23,8 @@
2523
get_registered_tools,
2624
)
2725

26+
logger = logging.getLogger(__name__)
27+
2828
app = typer.Typer(
2929
name="cam",
3030
help="Code Assistant Manager - CLI utilities for working with AI coding assistants",
@@ -637,11 +637,87 @@ def doctor_alias(
637637
return doctor(ctx, verbose)
638638

639639

640+
@app.command("validate")
641+
def validate_config(
642+
config: Optional[str] = typer.Option(
643+
None, "--config", "-c", help="Path to config file"
644+
),
645+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
646+
):
647+
"""Validate the configuration file for syntax and semantic errors."""
648+
from code_assistant_manager.config import ConfigManager
649+
from code_assistant_manager.menu.base import Colors
650+
651+
try:
652+
cm = ConfigManager(config)
653+
typer.echo(
654+
f"{Colors.GREEN}✓ Configuration file loaded successfully{Colors.RESET}"
655+
)
656+
657+
# Run full validation
658+
is_valid, errors = cm.validate_config()
659+
660+
if is_valid:
661+
typer.echo(f"{Colors.GREEN}✓ Configuration validation passed{Colors.RESET}")
662+
return 0
663+
else:
664+
typer.echo(f"{Colors.RED}✗ Configuration validation failed:{Colors.RESET}")
665+
for error in errors:
666+
typer.echo(f" - {error}")
667+
return 1
668+
669+
except FileNotFoundError as e:
670+
typer.echo(f"{Colors.RED}✗ Configuration file not found: {e}{Colors.RESET}")
671+
return 1
672+
except ValueError as e:
673+
typer.echo(f"{Colors.RED}✗ Configuration validation failed: {e}{Colors.RESET}")
674+
return 1
675+
676+
677+
@app.command("validate")
678+
def validate_config(
679+
config: Optional[str] = typer.Option(
680+
None, "--config", "-c", help="Path to config file"
681+
),
682+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
683+
):
684+
"""Validate the configuration file for syntax and semantic errors."""
685+
from code_assistant_manager.config import ConfigManager
686+
from code_assistant_manager.menu.base import Colors
687+
688+
try:
689+
cm = ConfigManager(config)
690+
typer.echo(
691+
f"{Colors.GREEN}✓ Configuration file loaded successfully{Colors.RESET}"
692+
)
693+
694+
# Run full validation
695+
is_valid, errors = cm.validate_config()
696+
697+
if is_valid:
698+
typer.echo(f"{Colors.GREEN}✓ Configuration validation passed{Colors.RESET}")
699+
return 0
700+
else:
701+
typer.echo(f"{Colors.RED}✗ Configuration validation failed:{Colors.RESET}")
702+
for error in errors:
703+
typer.echo(f" - {error}")
704+
return 1
705+
706+
except FileNotFoundError as e:
707+
typer.echo(f"{Colors.RED}✗ Configuration file not found: {e}{Colors.RESET}")
708+
return 1
709+
except ValueError as e:
710+
typer.echo(f"{Colors.RED}✗ Configuration validation failed: {e}{Colors.RESET}")
711+
return 1
712+
except Exception as e:
713+
typer.echo(
714+
f"{Colors.RED}✗ Unexpected error during validation: {e}{Colors.RESET}"
715+
)
716+
return 1
717+
718+
640719
@app.command()
641720
def completion(shell: str = typer.Argument(..., help="Shell type (bash, zsh)")):
642-
"""Generate shell completion script for bash or zsh (alias: comp)"""
643-
644-
shell = shell.lower()
645721
if shell not in ["bash", "zsh"]:
646722
typer.echo(f"Error: Unsupported shell '{shell}'. Supported shells: bash, zsh")
647723
raise typer.Exit(1)

code_assistant_manager/config.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,20 @@ def reload(self):
6666
"""Reload configuration from file and invalidate cache."""
6767
logger.debug(f"Reloading configuration from: {self.config_path}")
6868
if self.config_path.exists():
69-
with open(self.config_path, "r", encoding="utf-8") as f:
70-
self.config_data = json.load(f)
71-
logger.debug(
72-
f"Successfully loaded config with {len(self.config_data.get('endpoints', {}))} endpoints"
73-
)
69+
try:
70+
with open(self.config_path, "r", encoding="utf-8") as f:
71+
self.config_data = json.load(f)
72+
logger.debug(
73+
f"Successfully loaded config with {len(self.config_data.get('endpoints', {}))} endpoints"
74+
)
75+
except json.JSONDecodeError as e:
76+
error_msg = (
77+
f"Invalid JSON in configuration file {self.config_path}.\n"
78+
f"Error: {e}\n"
79+
f"Please check the JSON syntax and fix any formatting issues."
80+
)
81+
logger.error(error_msg)
82+
raise ValueError(error_msg) from e
7483
else:
7584
logger.error(f"Configuration file not found: {self.config_path}")
7685
raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Litellm API models fetcher."""
2+
3+
import json
4+
import logging
5+
import os
6+
7+
import requests
8+
9+
from .env_loader import load_env
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def fetch_litellm_models(api_key: str, base_url: str = "https://10.189.8.10:4142"):
15+
"""Fetch models from Litellm API."""
16+
url = f"{base_url}/v1/models"
17+
params = {
18+
"return_wildcard_routes": "false",
19+
"include_model_access_groups": "false",
20+
"only_model_access_groups": "false",
21+
"include_metadata": "false",
22+
}
23+
headers = {
24+
"accept": "application/json",
25+
"x-litellm-api-key": api_key,
26+
}
27+
# Security: Add timeout to prevent hanging connections
28+
r = requests.get(url, params=params, headers=headers, timeout=30, verify=False)
29+
r.raise_for_status()
30+
return r.json()
31+
32+
33+
def list_models():
34+
"""List available Litellm models. Returns model IDs, one per line."""
35+
# Load environment variables from .env file
36+
logger.debug("Loading environment variables from .env file")
37+
load_env()
38+
logger.debug("Environment variables loaded")
39+
40+
api_key = os.environ.get("API_KEY_LITELLM")
41+
if not api_key:
42+
logger.error("API_KEY_LITELLM environment variable is required but not found")
43+
raise SystemExit("API_KEY_LITELLM environment variable is required")
44+
45+
logger.debug("Fetching Litellm models")
46+
try:
47+
models_data = fetch_litellm_models(api_key)
48+
model_count = len(models_data.get("data", []))
49+
logger.debug(f"Found {model_count} models")
50+
51+
for m in models_data.get("data", []):
52+
print(m.get("id"))
53+
except requests.RequestException as e:
54+
logger.error(f"Failed to fetch models: {e}")
55+
raise SystemExit(f"Failed to fetch models: {e}")
56+
except json.JSONDecodeError as e:
57+
logger.error(f"Failed to parse response: {e}")
58+
raise SystemExit(f"Failed to parse response: {e}")
59+
60+
61+
if __name__ == "__main__":
62+
list_models()

tests/test_endpoints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class TestEndpointManagerCaching:
197197
@patch("code_assistant_manager.endpoints.subprocess.run")
198198
def test_fetch_models_caches_result(self, mock_run, endpoint_manager):
199199
"""Test that models are cached."""
200-
mock_run.return_value = MagicMock(stdout="model1\nmodel2")
200+
mock_run.return_value = MagicMock(stdout="model1\nmodel2", returncode=0)
201201
config = {"endpoint": "https://api.example.com", "list_models_cmd": "echo test"}
202202

203203
# First fetch

0 commit comments

Comments
 (0)