Schema now lists all models including locally available models
New tool to list all models `listmodels` Integration test to for all the different combinations of API keys Tweaks to codereview prompt for a better quality input from Claude Fixed missing 'low' severity in codereview
This commit is contained in:
12
README.md
12
README.md
@@ -268,6 +268,7 @@ Just ask Claude naturally:
|
||||
- **Code needs refactoring?** → `refactor` (intelligent refactoring with decomposition focus)
|
||||
- **Need call-flow analysis?** → `tracer` (generates prompts for execution tracing and dependency mapping)
|
||||
- **Need comprehensive tests?** → `testgen` (generates test suites with edge cases)
|
||||
- **Which models are available?** → `listmodels` (shows all configured providers and models)
|
||||
- **Server info?** → `version` (version and configuration details)
|
||||
|
||||
**Auto Mode:** When `DEFAULT_MODEL=auto`, Claude automatically picks the best model for each task. You can override with: "Use flash for quick analysis" or "Use o3 to debug this".
|
||||
@@ -291,7 +292,8 @@ Just ask Claude naturally:
|
||||
7. [`refactor`](#7-refactor---intelligent-code-refactoring) - Code refactoring with decomposition focus
|
||||
8. [`tracer`](#8-tracer---static-code-analysis-prompt-generator) - Static code analysis prompt generator for call-flow mapping
|
||||
9. [`testgen`](#9-testgen---comprehensive-test-generation) - Comprehensive test generation with edge case coverage
|
||||
10. [`version`](#10-version---server-information) - Get server version and configuration
|
||||
10. [`listmodels`](#10-listmodels---list-available-models) - Display all available AI models organized by provider
|
||||
11. [`version`](#11-version---server-information) - Get server version and configuration
|
||||
|
||||
### 1. `chat` - General Development Chat & Collaborative Thinking
|
||||
**Your thinking partner - bounce ideas, get second opinions, brainstorm collaboratively**
|
||||
@@ -575,7 +577,13 @@ suites that cover realistic failure scenarios and integration points that shorte
|
||||
- Specific code coverage - target specific functions/classes rather than testing everything
|
||||
- **Image support**: Test UI components, analyze visual requirements: `"Generate tests for this login form using the UI mockup screenshot"`
|
||||
|
||||
### 10. `version` - Server Information
|
||||
### 10. `listmodels` - List Available Models
|
||||
```
|
||||
"Use zen to list available models"
|
||||
```
|
||||
Shows all configured providers, available models with aliases, and context windows.
|
||||
|
||||
### 11. `version` - Server Information
|
||||
```
|
||||
"Get zen to show its version"
|
||||
```
|
||||
|
||||
@@ -14,7 +14,7 @@ import os
|
||||
# These values are used in server responses and for tracking releases
|
||||
# IMPORTANT: This is the single source of truth for version and author info
|
||||
# Semantic versioning: MAJOR.MINOR.PATCH
|
||||
__version__ = "4.8.1"
|
||||
__version__ = "4.8.2"
|
||||
# Last update date in ISO format
|
||||
__updated__ = "2025-06-16"
|
||||
# Primary maintainer
|
||||
|
||||
15
server.py
15
server.py
@@ -51,6 +51,7 @@ from tools import (
|
||||
ChatTool,
|
||||
CodeReviewTool,
|
||||
DebugIssueTool,
|
||||
ListModelsTool,
|
||||
Precommit,
|
||||
RefactorTool,
|
||||
TestGenerationTool,
|
||||
@@ -156,6 +157,7 @@ TOOLS = {
|
||||
"debug": DebugIssueTool(), # Root cause analysis and debugging assistance
|
||||
"analyze": AnalyzeTool(), # General-purpose file and code analysis
|
||||
"chat": ChatTool(), # Interactive development chat and brainstorming
|
||||
"listmodels": ListModelsTool(), # List all available AI models by provider
|
||||
"precommit": Precommit(), # Pre-commit validation of git changes
|
||||
"testgen": TestGenerationTool(), # Comprehensive test generation with edge case coverage
|
||||
"refactor": RefactorTool(), # Intelligent code refactoring suggestions with precise line references
|
||||
@@ -209,6 +211,11 @@ PROMPT_TEMPLATES = {
|
||||
"description": "Trace code execution paths",
|
||||
"template": "Generate tracer analysis with {model}",
|
||||
},
|
||||
"listmodels": {
|
||||
"name": "listmodels",
|
||||
"description": "List available AI models",
|
||||
"template": "List all available models",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -412,6 +419,10 @@ async def handle_list_tools() -> list[Tool]:
|
||||
]
|
||||
)
|
||||
|
||||
# Log cache efficiency info
|
||||
if os.getenv("OPENROUTER_API_KEY") and os.getenv("OPENROUTER_API_KEY") != "your_openrouter_api_key_here":
|
||||
logger.debug("OpenRouter registry cache used efficiently across all tool schemas")
|
||||
|
||||
logger.debug(f"Returning {len(tools)} tools to MCP client")
|
||||
return tools
|
||||
|
||||
@@ -821,14 +832,14 @@ async def handle_version() -> list[TextContent]:
|
||||
|
||||
configured_providers = []
|
||||
available_models = ModelProviderRegistry.get_available_models(respect_restrictions=True)
|
||||
|
||||
|
||||
# Group models by provider
|
||||
models_by_provider = {}
|
||||
for model_name, provider_type in available_models.items():
|
||||
if provider_type not in models_by_provider:
|
||||
models_by_provider[provider_type] = []
|
||||
models_by_provider[provider_type].append(model_name)
|
||||
|
||||
|
||||
# Format provider information with actual available models
|
||||
if ProviderType.GOOGLE in models_by_provider:
|
||||
gemini_models = ", ".join(sorted(models_by_provider[ProviderType.GOOGLE]))
|
||||
|
||||
154
tests/test_listmodels.py
Normal file
154
tests/test_listmodels.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Tests for the ListModels tool"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from mcp.types import TextContent
|
||||
|
||||
from tools.listmodels import ListModelsTool
|
||||
|
||||
|
||||
class TestListModelsTool:
|
||||
"""Test the ListModels tool functionality"""
|
||||
|
||||
@pytest.fixture
|
||||
def tool(self):
|
||||
"""Create a ListModelsTool instance"""
|
||||
return ListModelsTool()
|
||||
|
||||
def test_tool_metadata(self, tool):
|
||||
"""Test tool has correct metadata"""
|
||||
assert tool.name == "listmodels"
|
||||
assert "LIST AVAILABLE MODELS" in tool.description
|
||||
assert tool.get_request_model().__name__ == "ToolRequest"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_no_providers(self, tool):
|
||||
"""Test listing models with no providers configured"""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
# Set auto mode
|
||||
os.environ["DEFAULT_MODEL"] = "auto"
|
||||
|
||||
result = await tool.execute({})
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
|
||||
# Parse JSON response
|
||||
response = json.loads(result[0].text)
|
||||
assert response["status"] == "success"
|
||||
|
||||
content = response["content"]
|
||||
|
||||
# Check that providers show as not configured
|
||||
assert "Google Gemini ❌" in content
|
||||
assert "OpenAI ❌" in content
|
||||
assert "X.AI (Grok) ❌" in content
|
||||
assert "OpenRouter ❌" in content
|
||||
assert "Custom/Local API ❌" in content
|
||||
|
||||
# Check summary shows 0 configured
|
||||
assert "**Configured Providers**: 0" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_gemini_configured(self, tool):
|
||||
"""Test listing models with Gemini configured"""
|
||||
env_vars = {"GEMINI_API_KEY": "test-key", "DEFAULT_MODEL": "auto"}
|
||||
|
||||
with patch.dict(os.environ, env_vars, clear=True):
|
||||
result = await tool.execute({})
|
||||
|
||||
response = json.loads(result[0].text)
|
||||
content = response["content"]
|
||||
|
||||
# Check Gemini shows as configured
|
||||
assert "Google Gemini ✅" in content
|
||||
assert "`flash` → `gemini-2.5-flash-preview-05-20`" in content
|
||||
assert "`pro` → `gemini-2.5-pro-preview-06-05`" in content
|
||||
assert "1M context" in content
|
||||
|
||||
# Check summary
|
||||
assert "**Configured Providers**: 1" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_multiple_providers(self, tool):
|
||||
"""Test listing models with multiple providers configured"""
|
||||
env_vars = {
|
||||
"GEMINI_API_KEY": "test-key",
|
||||
"OPENAI_API_KEY": "test-key",
|
||||
"XAI_API_KEY": "test-key",
|
||||
"DEFAULT_MODEL": "auto",
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, env_vars, clear=True):
|
||||
result = await tool.execute({})
|
||||
|
||||
response = json.loads(result[0].text)
|
||||
content = response["content"]
|
||||
|
||||
# Check all show as configured
|
||||
assert "Google Gemini ✅" in content
|
||||
assert "OpenAI ✅" in content
|
||||
assert "X.AI (Grok) ✅" in content
|
||||
|
||||
# Check models are listed
|
||||
assert "`o3`" in content
|
||||
assert "`grok`" in content
|
||||
|
||||
# Check summary
|
||||
assert "**Configured Providers**: 3" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_openrouter(self, tool):
|
||||
"""Test listing models with OpenRouter configured"""
|
||||
env_vars = {"OPENROUTER_API_KEY": "test-key", "DEFAULT_MODEL": "auto"}
|
||||
|
||||
with patch.dict(os.environ, env_vars, clear=True):
|
||||
result = await tool.execute({})
|
||||
|
||||
response = json.loads(result[0].text)
|
||||
content = response["content"]
|
||||
|
||||
# Check OpenRouter shows as configured
|
||||
assert "OpenRouter ✅" in content
|
||||
assert "Access to multiple cloud AI providers" in content
|
||||
|
||||
# Should show some models (mocked registry will have some)
|
||||
assert "Available Models" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_custom_api(self, tool):
|
||||
"""Test listing models with custom API configured"""
|
||||
env_vars = {"CUSTOM_API_URL": "http://localhost:11434", "DEFAULT_MODEL": "auto"}
|
||||
|
||||
with patch.dict(os.environ, env_vars, clear=True):
|
||||
result = await tool.execute({})
|
||||
|
||||
response = json.loads(result[0].text)
|
||||
content = response["content"]
|
||||
|
||||
# Check Custom API shows as configured
|
||||
assert "Custom/Local API ✅" in content
|
||||
assert "http://localhost:11434" in content
|
||||
assert "Local models via Ollama" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_output_includes_usage_tips(self, tool):
|
||||
"""Test that output includes helpful usage tips"""
|
||||
result = await tool.execute({})
|
||||
|
||||
response = json.loads(result[0].text)
|
||||
content = response["content"]
|
||||
|
||||
# Check for usage tips
|
||||
assert "**Usage Tips**:" in content
|
||||
assert "Use model aliases" in content
|
||||
assert "auto mode" in content
|
||||
|
||||
def test_model_category(self, tool):
|
||||
"""Test that tool uses FAST_RESPONSE category"""
|
||||
from tools.models import ToolModelCategory
|
||||
|
||||
assert tool.get_model_category() == ToolModelCategory.FAST_RESPONSE
|
||||
282
tests/test_model_enumeration.py
Normal file
282
tests/test_model_enumeration.py
Normal file
@@ -0,0 +1,282 @@
|
||||
"""
|
||||
Integration tests for model enumeration across all provider combinations.
|
||||
|
||||
These tests ensure that the _get_available_models() method correctly returns
|
||||
all expected models based on which providers are configured via environment variables.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from providers.registry import ModelProviderRegistry
|
||||
from tools.analyze import AnalyzeTool
|
||||
|
||||
|
||||
@pytest.mark.no_mock_provider
|
||||
class TestModelEnumeration:
|
||||
"""Test model enumeration with various provider configurations"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up clean state before each test."""
|
||||
# Save original environment state
|
||||
self._original_env = {
|
||||
"DEFAULT_MODEL": os.environ.get("DEFAULT_MODEL", ""),
|
||||
"GEMINI_API_KEY": os.environ.get("GEMINI_API_KEY", ""),
|
||||
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", ""),
|
||||
"XAI_API_KEY": os.environ.get("XAI_API_KEY", ""),
|
||||
"OPENROUTER_API_KEY": os.environ.get("OPENROUTER_API_KEY", ""),
|
||||
"CUSTOM_API_URL": os.environ.get("CUSTOM_API_URL", ""),
|
||||
}
|
||||
|
||||
# Clear provider registry
|
||||
ModelProviderRegistry._instance = None
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up after each test."""
|
||||
# Restore original environment
|
||||
for key, value in self._original_env.items():
|
||||
if value:
|
||||
os.environ[key] = value
|
||||
elif key in os.environ:
|
||||
del os.environ[key]
|
||||
|
||||
# Reload config
|
||||
import config
|
||||
|
||||
importlib.reload(config)
|
||||
|
||||
# Clear provider registry
|
||||
ModelProviderRegistry._instance = None
|
||||
|
||||
def _setup_environment(self, provider_config):
|
||||
"""Helper to set up environment variables for testing."""
|
||||
# Clear all provider-related env vars first
|
||||
for key in ["GEMINI_API_KEY", "OPENAI_API_KEY", "XAI_API_KEY", "OPENROUTER_API_KEY", "CUSTOM_API_URL"]:
|
||||
if key in os.environ:
|
||||
del os.environ[key]
|
||||
|
||||
# Set new values
|
||||
for key, value in provider_config.items():
|
||||
if value is not None:
|
||||
os.environ[key] = value
|
||||
|
||||
# Always set auto mode for these tests
|
||||
os.environ["DEFAULT_MODEL"] = "auto"
|
||||
|
||||
# Reload config to pick up changes
|
||||
import config
|
||||
|
||||
importlib.reload(config)
|
||||
|
||||
# Reload tools.base to ensure fresh state
|
||||
import tools.base
|
||||
|
||||
importlib.reload(tools.base)
|
||||
|
||||
def test_native_models_always_included(self):
|
||||
"""Test that native models from MODEL_CAPABILITIES_DESC are always included."""
|
||||
self._setup_environment({}) # No providers configured
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
# All native models should be present
|
||||
native_models = [
|
||||
"flash",
|
||||
"pro", # Gemini aliases
|
||||
"o3",
|
||||
"o3-mini",
|
||||
"o3-pro",
|
||||
"o4-mini",
|
||||
"o4-mini-high", # OpenAI models
|
||||
"grok",
|
||||
"grok-3",
|
||||
"grok-3-fast",
|
||||
"grok3",
|
||||
"grokfast", # X.AI models
|
||||
"gemini-2.5-flash-preview-05-20",
|
||||
"gemini-2.5-pro-preview-06-05", # Full Gemini names
|
||||
]
|
||||
|
||||
for model in native_models:
|
||||
assert model in models, f"Native model {model} should always be in enum"
|
||||
|
||||
def test_openrouter_models_with_api_key(self):
|
||||
"""Test that OpenRouter models are included when API key is configured."""
|
||||
self._setup_environment({"OPENROUTER_API_KEY": "test-key"})
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
# Check for some known OpenRouter model aliases
|
||||
openrouter_models = ["opus", "sonnet", "haiku", "mistral-large", "deepseek"]
|
||||
found_count = sum(1 for m in openrouter_models if m in models)
|
||||
|
||||
assert found_count >= 3, f"Expected at least 3 OpenRouter models, found {found_count}"
|
||||
assert len(models) > 20, f"With OpenRouter, should have many models, got {len(models)}"
|
||||
|
||||
def test_openrouter_models_without_api_key(self):
|
||||
"""Test that OpenRouter models are NOT included when API key is not configured."""
|
||||
self._setup_environment({}) # No OpenRouter key
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
# OpenRouter-specific models should NOT be present
|
||||
openrouter_only_models = ["opus", "sonnet", "haiku"]
|
||||
found_count = sum(1 for m in openrouter_only_models if m in models)
|
||||
|
||||
assert found_count == 0, "OpenRouter models should not be included without API key"
|
||||
|
||||
def test_custom_models_with_custom_url(self):
|
||||
"""Test that custom models are included when CUSTOM_API_URL is configured."""
|
||||
self._setup_environment({"CUSTOM_API_URL": "http://localhost:11434"})
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
# Check for custom models (marked with is_custom=true)
|
||||
custom_models = ["local-llama", "llama3.2"]
|
||||
found_count = sum(1 for m in custom_models if m in models)
|
||||
|
||||
assert found_count >= 1, f"Expected at least 1 custom model, found {found_count}"
|
||||
|
||||
def test_custom_models_without_custom_url(self):
|
||||
"""Test that custom models are NOT included when CUSTOM_API_URL is not configured."""
|
||||
self._setup_environment({}) # No custom URL
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
# Custom-only models should NOT be present
|
||||
custom_only_models = ["local-llama", "llama3.2"]
|
||||
found_count = sum(1 for m in custom_only_models if m in models)
|
||||
|
||||
assert found_count == 0, "Custom models should not be included without CUSTOM_API_URL"
|
||||
|
||||
def test_all_providers_combined(self):
|
||||
"""Test that all models are included when all providers are configured."""
|
||||
self._setup_environment(
|
||||
{
|
||||
"GEMINI_API_KEY": "test-key",
|
||||
"OPENAI_API_KEY": "test-key",
|
||||
"XAI_API_KEY": "test-key",
|
||||
"OPENROUTER_API_KEY": "test-key",
|
||||
"CUSTOM_API_URL": "http://localhost:11434",
|
||||
}
|
||||
)
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
# Should have all types of models
|
||||
assert "flash" in models # Gemini
|
||||
assert "o3" in models # OpenAI
|
||||
assert "grok" in models # X.AI
|
||||
assert "opus" in models or "sonnet" in models # OpenRouter
|
||||
assert "local-llama" in models or "llama3.2" in models # Custom
|
||||
|
||||
# Should have many models total
|
||||
assert len(models) > 50, f"With all providers, should have 50+ models, got {len(models)}"
|
||||
|
||||
# No duplicates
|
||||
assert len(models) == len(set(models)), "Should have no duplicate models"
|
||||
|
||||
def test_mixed_provider_combinations(self):
|
||||
"""Test various mixed provider configurations."""
|
||||
test_cases = [
|
||||
# (provider_config, expected_model_samples, min_count)
|
||||
(
|
||||
{"GEMINI_API_KEY": "test", "OPENROUTER_API_KEY": "test"},
|
||||
["flash", "pro", "opus"], # Gemini + OpenRouter models
|
||||
30,
|
||||
),
|
||||
(
|
||||
{"OPENAI_API_KEY": "test", "CUSTOM_API_URL": "http://localhost"},
|
||||
["o3", "o4-mini", "local-llama"], # OpenAI + Custom models
|
||||
18, # 14 native + ~4 custom models
|
||||
),
|
||||
(
|
||||
{"XAI_API_KEY": "test", "OPENROUTER_API_KEY": "test"},
|
||||
["grok", "grok-3", "opus"], # X.AI + OpenRouter models
|
||||
30,
|
||||
),
|
||||
]
|
||||
|
||||
for provider_config, expected_samples, min_count in test_cases:
|
||||
self._setup_environment(provider_config)
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
# Check expected models are present
|
||||
for model in expected_samples:
|
||||
if model in ["local-llama", "llama3.2"]: # Custom models might not all be present
|
||||
continue
|
||||
assert model in models, f"Expected {model} with config {provider_config}"
|
||||
|
||||
# Check minimum count
|
||||
assert (
|
||||
len(models) >= min_count
|
||||
), f"Expected at least {min_count} models with {provider_config}, got {len(models)}"
|
||||
|
||||
def test_no_duplicates_with_overlapping_providers(self):
|
||||
"""Test that models aren't duplicated when multiple providers offer the same model."""
|
||||
self._setup_environment(
|
||||
{
|
||||
"OPENAI_API_KEY": "test",
|
||||
"OPENROUTER_API_KEY": "test", # OpenRouter also offers OpenAI models
|
||||
}
|
||||
)
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
# Count occurrences of each model
|
||||
model_counts = {}
|
||||
for model in models:
|
||||
model_counts[model] = model_counts.get(model, 0) + 1
|
||||
|
||||
# Check no duplicates
|
||||
duplicates = {m: count for m, count in model_counts.items() if count > 1}
|
||||
assert len(duplicates) == 0, f"Found duplicate models: {duplicates}"
|
||||
|
||||
def test_schema_enum_matches_get_available_models(self):
|
||||
"""Test that the schema enum matches what _get_available_models returns."""
|
||||
self._setup_environment({"OPENROUTER_API_KEY": "test", "CUSTOM_API_URL": "http://localhost:11434"})
|
||||
|
||||
tool = AnalyzeTool()
|
||||
|
||||
# Get models from both methods
|
||||
available_models = tool._get_available_models()
|
||||
schema = tool.get_input_schema()
|
||||
schema_enum = schema["properties"]["model"]["enum"]
|
||||
|
||||
# They should match exactly
|
||||
assert set(available_models) == set(schema_enum), "Schema enum should match _get_available_models output"
|
||||
assert len(available_models) == len(schema_enum), "Should have same number of models (no duplicates)"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name,should_exist",
|
||||
[
|
||||
("flash", True), # Native Gemini
|
||||
("o3", True), # Native OpenAI
|
||||
("grok", True), # Native X.AI
|
||||
("gemini-2.5-flash-preview-05-20", True), # Full native name
|
||||
("o4-mini-high", True), # Native OpenAI variant
|
||||
("grok-3-fast", True), # Native X.AI variant
|
||||
],
|
||||
)
|
||||
def test_specific_native_models_always_present(self, model_name, should_exist):
|
||||
"""Test that specific native models are always present regardless of configuration."""
|
||||
self._setup_environment({}) # No providers
|
||||
|
||||
tool = AnalyzeTool()
|
||||
models = tool._get_available_models()
|
||||
|
||||
if should_exist:
|
||||
assert model_name in models, f"Native model {model_name} should always be present"
|
||||
else:
|
||||
assert model_name not in models, f"Model {model_name} should not be present"
|
||||
@@ -28,8 +28,8 @@ class TestServerTools:
|
||||
assert "tracer" in tool_names
|
||||
assert "version" in tool_names
|
||||
|
||||
# Should have exactly 10 tools (including refactor and tracer)
|
||||
assert len(tools) == 10
|
||||
# Should have exactly 11 tools (including refactor, tracer, and listmodels)
|
||||
assert len(tools) == 11
|
||||
|
||||
# Check descriptions are verbose
|
||||
for tool in tools:
|
||||
|
||||
@@ -6,6 +6,7 @@ from .analyze import AnalyzeTool
|
||||
from .chat import ChatTool
|
||||
from .codereview import CodeReviewTool
|
||||
from .debug import DebugIssueTool
|
||||
from .listmodels import ListModelsTool
|
||||
from .precommit import Precommit
|
||||
from .refactor import RefactorTool
|
||||
from .testgen import TestGenerationTool
|
||||
@@ -18,6 +19,7 @@ __all__ = [
|
||||
"DebugIssueTool",
|
||||
"AnalyzeTool",
|
||||
"ChatTool",
|
||||
"ListModelsTool",
|
||||
"Precommit",
|
||||
"RefactorTool",
|
||||
"TestGenerationTool",
|
||||
|
||||
@@ -99,6 +99,9 @@ class ToolRequest(BaseModel):
|
||||
|
||||
|
||||
class BaseTool(ABC):
|
||||
# Class-level cache for OpenRouter registry to avoid multiple loads
|
||||
_openrouter_registry_cache = None
|
||||
|
||||
"""
|
||||
Abstract base class for all Gemini tools.
|
||||
|
||||
@@ -210,6 +213,20 @@ class BaseTool(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _get_openrouter_registry(cls):
|
||||
"""Get cached OpenRouter registry instance."""
|
||||
if BaseTool._openrouter_registry_cache is None:
|
||||
import logging
|
||||
|
||||
from providers.openrouter_registry import OpenRouterModelRegistry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("Loading OpenRouter registry for the first time (will be cached for all tools)")
|
||||
BaseTool._openrouter_registry_cache = OpenRouterModelRegistry()
|
||||
|
||||
return BaseTool._openrouter_registry_cache
|
||||
|
||||
def is_effective_auto_mode(self) -> bool:
|
||||
"""
|
||||
Check if we're in effective auto mode for schema generation.
|
||||
|
||||
@@ -39,21 +39,32 @@ class CodeReviewRequest(ToolRequest):
|
||||
)
|
||||
prompt: str = Field(
|
||||
...,
|
||||
description="User's summary of what the code does, expected behavior, constraints, and review objectives",
|
||||
description=(
|
||||
"User's summary of what the code does, expected behavior, constraints, and review objectives. "
|
||||
"IMPORTANT: Before using this tool, Claude should first perform its own preliminary review - "
|
||||
"examining the code structure, identifying potential issues, understanding the business logic, "
|
||||
"and noting areas of concern. Include Claude's initial observations about code quality, potential "
|
||||
"bugs, architectural patterns, and specific areas that need deeper scrutiny. This dual-perspective "
|
||||
"approach (Claude's analysis + external model's review) provides more comprehensive feedback and "
|
||||
"catches issues that either reviewer might miss alone."
|
||||
),
|
||||
)
|
||||
images: Optional[list[str]] = Field(
|
||||
None,
|
||||
description="Optional images of architecture diagrams, UI mockups, design documents, or visual references for code review context",
|
||||
description=(
|
||||
"Optional images of architecture diagrams, UI mockups, design documents, or visual references "
|
||||
"for code review context"
|
||||
),
|
||||
)
|
||||
review_type: str = Field("full", description="Type of review: full|security|performance|quick")
|
||||
focus_on: Optional[str] = Field(
|
||||
None,
|
||||
description="Specific aspects to focus on, or additional context that would help understand areas of concern",
|
||||
description=("Specific aspects to focus on, or additional context that would help understand areas of concern"),
|
||||
)
|
||||
standards: Optional[str] = Field(None, description="Coding standards or guidelines to enforce")
|
||||
severity_filter: str = Field(
|
||||
"all",
|
||||
description="Minimum severity to report: critical|high|medium|all",
|
||||
description="Minimum severity to report: critical|high|medium|low|all",
|
||||
)
|
||||
|
||||
|
||||
@@ -81,7 +92,8 @@ class CodeReviewTool(BaseTool):
|
||||
"Choose thinking_mode based on review scope: 'low' for small code snippets, "
|
||||
"'medium' for standard files/modules (default), 'high' for complex systems/architectures, "
|
||||
"'max' for critical security audits or large codebases requiring deepest analysis. "
|
||||
"Note: If you're not currently using a top-tier model such as Opus 4 or above, these tools can provide enhanced capabilities."
|
||||
"Note: If you're not currently using a top-tier model such as Opus 4 or above, these tools "
|
||||
"can provide enhanced capabilities."
|
||||
)
|
||||
|
||||
def get_input_schema(self) -> dict[str, Any]:
|
||||
@@ -96,12 +108,24 @@ class CodeReviewTool(BaseTool):
|
||||
"model": self.get_model_field_schema(),
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"description": "User's summary of what the code does, expected behavior, constraints, and review objectives",
|
||||
"description": (
|
||||
"User's summary of what the code does, expected behavior, constraints, and review "
|
||||
"objectives. IMPORTANT: Before using this tool, Claude should first perform its own "
|
||||
"preliminary review - examining the code structure, identifying potential issues, "
|
||||
"understanding the business logic, and noting areas of concern. Include Claude's initial "
|
||||
"observations about code quality, potential bugs, architectural patterns, and specific "
|
||||
"areas that need deeper scrutiny. This dual-perspective approach (Claude's analysis + "
|
||||
"external model's review) provides more comprehensive feedback and catches issues that "
|
||||
"either reviewer might miss alone."
|
||||
),
|
||||
},
|
||||
"images": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Optional images of architecture diagrams, UI mockups, design documents, or visual references for code review context",
|
||||
"description": (
|
||||
"Optional images of architecture diagrams, UI mockups, design documents, or visual "
|
||||
"references for code review context"
|
||||
),
|
||||
},
|
||||
"review_type": {
|
||||
"type": "string",
|
||||
@@ -111,7 +135,10 @@ class CodeReviewTool(BaseTool):
|
||||
},
|
||||
"focus_on": {
|
||||
"type": "string",
|
||||
"description": "Specific aspects to focus on, or additional context that would help understand areas of concern",
|
||||
"description": (
|
||||
"Specific aspects to focus on, or additional context that would help understand "
|
||||
"areas of concern"
|
||||
),
|
||||
},
|
||||
"standards": {
|
||||
"type": "string",
|
||||
@@ -119,7 +146,7 @@ class CodeReviewTool(BaseTool):
|
||||
},
|
||||
"severity_filter": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "all"],
|
||||
"enum": ["critical", "high", "medium", "low", "all"],
|
||||
"default": "all",
|
||||
"description": "Minimum severity level to report",
|
||||
},
|
||||
@@ -132,16 +159,29 @@ class CodeReviewTool(BaseTool):
|
||||
"thinking_mode": {
|
||||
"type": "string",
|
||||
"enum": ["minimal", "low", "medium", "high", "max"],
|
||||
"description": "Thinking depth: minimal (0.5% of model max), low (8%), medium (33%), high (67%), max (100% of model max)",
|
||||
"description": (
|
||||
"Thinking depth: minimal (0.5% of model max), low (8%), medium (33%), high (67%), "
|
||||
"max (100% of model max)"
|
||||
),
|
||||
},
|
||||
"use_websearch": {
|
||||
"type": "boolean",
|
||||
"description": "Enable web search for documentation, best practices, and current information. Particularly useful for: brainstorming sessions, architectural design discussions, exploring industry best practices, working with specific frameworks/technologies, researching solutions to complex problems, or when current documentation and community insights would enhance the analysis.",
|
||||
"description": (
|
||||
"Enable web search for documentation, best practices, and current information. "
|
||||
"Particularly useful for: brainstorming sessions, architectural design discussions, "
|
||||
"exploring industry best practices, working with specific frameworks/technologies, "
|
||||
"researching solutions to complex problems, or when current documentation and community "
|
||||
"insights would enhance the analysis."
|
||||
),
|
||||
"default": True,
|
||||
},
|
||||
"continuation_id": {
|
||||
"type": "string",
|
||||
"description": "Thread continuation ID for multi-turn conversations. Can be used to continue conversations across different tools. Only provide this if continuing a previous conversation thread.",
|
||||
"description": (
|
||||
"Thread continuation ID for multi-turn conversations. Can be used to continue "
|
||||
"conversations across different tools. Only provide this if continuing a previous "
|
||||
"conversation thread."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["files", "prompt"] + (["model"] if self.is_effective_auto_mode() else []),
|
||||
@@ -263,7 +303,8 @@ class CodeReviewTool(BaseTool):
|
||||
{file_content}
|
||||
=== END CODE ===
|
||||
|
||||
Please provide a code review aligned with the user's context and expectations, following the format specified in the system prompt."""
|
||||
Please provide a code review aligned with the user's context and expectations, following the format specified """
|
||||
"in the system prompt." ""
|
||||
|
||||
return full_prompt
|
||||
|
||||
@@ -285,10 +326,14 @@ Please provide a code review aligned with the user's context and expectations, f
|
||||
|
||||
**Claude's Next Steps:**
|
||||
|
||||
1. **Understand the Context**: First examine the specific functions, files, and code sections mentioned in the review to understand each issue thoroughly.
|
||||
1. **Understand the Context**: First examine the specific functions, files, and code sections mentioned in """
|
||||
"""the review to understand each issue thoroughly.
|
||||
|
||||
2. **Present Options to User**: After understanding the issues, ask the user which specific improvements they would like to implement, presenting them as a clear list of options.
|
||||
2. **Present Options to User**: After understanding the issues, ask the user which specific improvements """
|
||||
"""they would like to implement, presenting them as a clear list of options.
|
||||
|
||||
3. **Implement Selected Fixes**: Only implement the fixes the user chooses, ensuring each change is made correctly and maintains code quality.
|
||||
3. **Implement Selected Fixes**: Only implement the fixes the user chooses, ensuring each change is made """
|
||||
"""correctly and maintains code quality.
|
||||
|
||||
Remember: Always understand the code context before suggesting fixes, and let the user decide which improvements to implement."""
|
||||
Remember: Always understand the code context before suggesting fixes, and let the user decide which """
|
||||
"""improvements to implement."""
|
||||
|
||||
279
tools/listmodels.py
Normal file
279
tools/listmodels.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
List Models Tool - Display all available models organized by provider
|
||||
|
||||
This tool provides a comprehensive view of all AI models available in the system,
|
||||
organized by their provider (Gemini, OpenAI, X.AI, OpenRouter, Custom).
|
||||
It shows which providers are configured and what models can be used.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
from mcp.types import TextContent
|
||||
|
||||
from tools.base import BaseTool, ToolRequest
|
||||
from tools.models import ToolModelCategory, ToolOutput
|
||||
|
||||
|
||||
class ListModelsTool(BaseTool):
|
||||
"""
|
||||
Tool for listing all available AI models organized by provider.
|
||||
|
||||
This tool helps users understand:
|
||||
- Which providers are configured (have API keys)
|
||||
- What models are available from each provider
|
||||
- Model aliases and their full names
|
||||
- Context window sizes and capabilities
|
||||
"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return "listmodels"
|
||||
|
||||
def get_description(self) -> str:
|
||||
return (
|
||||
"LIST AVAILABLE MODELS - Display all AI models organized by provider. "
|
||||
"Shows which providers are configured, available models, their aliases, "
|
||||
"context windows, and capabilities. Useful for understanding what models "
|
||||
"can be used and their characteristics."
|
||||
)
|
||||
|
||||
def get_input_schema(self) -> dict[str, Any]:
|
||||
"""Return the JSON schema for the tool's input"""
|
||||
return {"type": "object", "properties": {}, "required": []}
|
||||
|
||||
def get_system_prompt(self) -> str:
|
||||
"""No AI model needed for this tool"""
|
||||
return ""
|
||||
|
||||
def get_request_model(self):
|
||||
"""Return the Pydantic model for request validation."""
|
||||
return ToolRequest
|
||||
|
||||
async def prepare_prompt(self, request: ToolRequest) -> str:
|
||||
"""Not used for this utility tool"""
|
||||
return ""
|
||||
|
||||
def format_response(self, response: str, request: ToolRequest, model_info: Optional[dict] = None) -> str:
|
||||
"""Not used for this utility tool"""
|
||||
return response
|
||||
|
||||
async def execute(self, arguments: dict[str, Any]) -> list[TextContent]:
|
||||
"""
|
||||
List all available models organized by provider.
|
||||
|
||||
This overrides the base class execute to provide direct output without AI model calls.
|
||||
|
||||
Args:
|
||||
arguments: Standard tool arguments (none required)
|
||||
|
||||
Returns:
|
||||
Formatted list of models by provider
|
||||
"""
|
||||
from config import MODEL_CAPABILITIES_DESC
|
||||
from providers.openrouter_registry import OpenRouterModelRegistry
|
||||
|
||||
output_lines = ["# Available AI Models\n"]
|
||||
|
||||
# Check native providers
|
||||
native_providers = {
|
||||
"gemini": {
|
||||
"name": "Google Gemini",
|
||||
"env_key": "GEMINI_API_KEY",
|
||||
"models": {
|
||||
"flash": "gemini-2.5-flash-preview-05-20",
|
||||
"pro": "gemini-2.5-pro-preview-06-05",
|
||||
},
|
||||
},
|
||||
"openai": {
|
||||
"name": "OpenAI",
|
||||
"env_key": "OPENAI_API_KEY",
|
||||
"models": {
|
||||
"o3": "o3",
|
||||
"o3-mini": "o3-mini",
|
||||
"o3-pro": "o3-pro",
|
||||
"o4-mini": "o4-mini",
|
||||
"o4-mini-high": "o4-mini-high",
|
||||
},
|
||||
},
|
||||
"xai": {
|
||||
"name": "X.AI (Grok)",
|
||||
"env_key": "XAI_API_KEY",
|
||||
"models": {
|
||||
"grok": "grok-3",
|
||||
"grok-3": "grok-3",
|
||||
"grok-3-fast": "grok-3-fast",
|
||||
"grok3": "grok-3",
|
||||
"grokfast": "grok-3-fast",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Check each native provider
|
||||
for provider_key, provider_info in native_providers.items():
|
||||
api_key = os.getenv(provider_info["env_key"])
|
||||
is_configured = api_key and api_key != f"your_{provider_key}_api_key_here"
|
||||
|
||||
output_lines.append(f"## {provider_info['name']} {'✅' if is_configured else '❌'}")
|
||||
|
||||
if is_configured:
|
||||
output_lines.append("**Status**: Configured and available")
|
||||
output_lines.append("\n**Models**:")
|
||||
|
||||
for alias, full_name in provider_info["models"].items():
|
||||
# Get description from MODEL_CAPABILITIES_DESC
|
||||
desc = MODEL_CAPABILITIES_DESC.get(alias, "")
|
||||
if isinstance(desc, str):
|
||||
# Extract context window from description
|
||||
import re
|
||||
|
||||
context_match = re.search(r"\(([^)]+context)\)", desc)
|
||||
context_info = context_match.group(1) if context_match else ""
|
||||
|
||||
output_lines.append(f"- `{alias}` → `{full_name}` - {context_info}")
|
||||
|
||||
# Extract key capability
|
||||
if "Ultra-fast" in desc:
|
||||
output_lines.append(" - Fast processing, quick iterations")
|
||||
elif "Deep reasoning" in desc:
|
||||
output_lines.append(" - Extended reasoning with thinking mode")
|
||||
elif "Strong reasoning" in desc:
|
||||
output_lines.append(" - Logical problems, systematic analysis")
|
||||
elif "EXTREMELY EXPENSIVE" in desc:
|
||||
output_lines.append(" - ⚠️ Professional grade (very expensive)")
|
||||
else:
|
||||
output_lines.append(f"**Status**: Not configured (set {provider_info['env_key']})")
|
||||
|
||||
output_lines.append("")
|
||||
|
||||
# Check OpenRouter
|
||||
openrouter_key = os.getenv("OPENROUTER_API_KEY")
|
||||
is_openrouter_configured = openrouter_key and openrouter_key != "your_openrouter_api_key_here"
|
||||
|
||||
output_lines.append(f"## OpenRouter {'✅' if is_openrouter_configured else '❌'}")
|
||||
|
||||
if is_openrouter_configured:
|
||||
output_lines.append("**Status**: Configured and available")
|
||||
output_lines.append("**Description**: Access to multiple cloud AI providers via unified API")
|
||||
|
||||
try:
|
||||
registry = OpenRouterModelRegistry()
|
||||
aliases = registry.list_aliases()
|
||||
|
||||
# Group by provider for better organization
|
||||
providers_models = {}
|
||||
for alias in aliases[:20]: # Limit to first 20 to avoid overwhelming output
|
||||
config = registry.resolve(alias)
|
||||
if config and not (hasattr(config, "is_custom") and config.is_custom):
|
||||
# Extract provider from model_name
|
||||
provider = config.model_name.split("/")[0] if "/" in config.model_name else "other"
|
||||
if provider not in providers_models:
|
||||
providers_models[provider] = []
|
||||
providers_models[provider].append((alias, config))
|
||||
|
||||
output_lines.append("\n**Available Models** (showing top 20):")
|
||||
for provider, models in sorted(providers_models.items()):
|
||||
output_lines.append(f"\n*{provider.title()}:*")
|
||||
for alias, config in models[:5]: # Limit each provider to 5 models
|
||||
context_str = f"{config.context_window // 1000}K" if config.context_window else "?"
|
||||
output_lines.append(f"- `{alias}` → `{config.model_name}` ({context_str} context)")
|
||||
|
||||
total_models = len(aliases)
|
||||
output_lines.append(f"\n...and {total_models - 20} more models available")
|
||||
|
||||
except Exception as e:
|
||||
output_lines.append(f"**Error loading models**: {str(e)}")
|
||||
else:
|
||||
output_lines.append("**Status**: Not configured (set OPENROUTER_API_KEY)")
|
||||
output_lines.append("**Note**: Provides access to GPT-4, Claude, Mistral, and many more")
|
||||
|
||||
output_lines.append("")
|
||||
|
||||
# Check Custom API
|
||||
custom_url = os.getenv("CUSTOM_API_URL")
|
||||
|
||||
output_lines.append(f"## Custom/Local API {'✅' if custom_url else '❌'}")
|
||||
|
||||
if custom_url:
|
||||
output_lines.append("**Status**: Configured and available")
|
||||
output_lines.append(f"**Endpoint**: {custom_url}")
|
||||
output_lines.append("**Description**: Local models via Ollama, vLLM, LM Studio, etc.")
|
||||
|
||||
try:
|
||||
registry = OpenRouterModelRegistry()
|
||||
custom_models = []
|
||||
|
||||
for alias in registry.list_aliases():
|
||||
config = registry.resolve(alias)
|
||||
if config and hasattr(config, "is_custom") and config.is_custom:
|
||||
custom_models.append((alias, config))
|
||||
|
||||
if custom_models:
|
||||
output_lines.append("\n**Custom Models**:")
|
||||
for alias, config in custom_models:
|
||||
context_str = f"{config.context_window // 1000}K" if config.context_window else "?"
|
||||
output_lines.append(f"- `{alias}` → `{config.model_name}` ({context_str} context)")
|
||||
if config.description:
|
||||
output_lines.append(f" - {config.description}")
|
||||
|
||||
except Exception as e:
|
||||
output_lines.append(f"**Error loading custom models**: {str(e)}")
|
||||
else:
|
||||
output_lines.append("**Status**: Not configured (set CUSTOM_API_URL)")
|
||||
output_lines.append("**Example**: CUSTOM_API_URL=http://localhost:11434 (for Ollama)")
|
||||
|
||||
output_lines.append("")
|
||||
|
||||
# Add summary
|
||||
output_lines.append("## Summary")
|
||||
|
||||
# Count configured providers
|
||||
configured_count = sum(
|
||||
[
|
||||
1
|
||||
for p in native_providers.values()
|
||||
if os.getenv(p["env_key"])
|
||||
and os.getenv(p["env_key"]) != f"your_{p['env_key'].lower().replace('_api_key', '')}_api_key_here"
|
||||
]
|
||||
)
|
||||
if is_openrouter_configured:
|
||||
configured_count += 1
|
||||
if custom_url:
|
||||
configured_count += 1
|
||||
|
||||
output_lines.append(f"**Configured Providers**: {configured_count}")
|
||||
|
||||
# Get total available models
|
||||
try:
|
||||
from tools.analyze import AnalyzeTool
|
||||
|
||||
tool = AnalyzeTool()
|
||||
total_models = len(tool._get_available_models())
|
||||
output_lines.append(f"**Total Available Models**: {total_models}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add usage tips
|
||||
output_lines.append("\n**Usage Tips**:")
|
||||
output_lines.append("- Use model aliases (e.g., 'flash', 'o3', 'opus') for convenience")
|
||||
output_lines.append("- In auto mode, Claude will select the best model for each task")
|
||||
output_lines.append("- Custom models are only available when CUSTOM_API_URL is set")
|
||||
output_lines.append("- OpenRouter provides access to many cloud models with one API key")
|
||||
|
||||
# Format output
|
||||
content = "\n".join(output_lines)
|
||||
|
||||
tool_output = ToolOutput(
|
||||
status="success",
|
||||
content=content,
|
||||
content_type="text",
|
||||
metadata={
|
||||
"tool_name": self.name,
|
||||
"configured_providers": configured_count,
|
||||
},
|
||||
)
|
||||
|
||||
return [TextContent(type="text", text=tool_output.model_dump_json())]
|
||||
|
||||
def get_model_category(self) -> ToolModelCategory:
|
||||
"""Return the model category for this tool."""
|
||||
return ToolModelCategory.FAST_RESPONSE # Simple listing, no AI needed
|
||||
Reference in New Issue
Block a user