diff --git a/README.md b/README.md index f2ea8ff..d19cb68 100644 --- a/README.md +++ b/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" ``` diff --git a/config.py b/config.py index eaed16a..2a460aa 100644 --- a/config.py +++ b/config.py @@ -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 diff --git a/server.py b/server.py index c008aff..360d5f6 100644 --- a/server.py +++ b/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])) diff --git a/tests/test_listmodels.py b/tests/test_listmodels.py new file mode 100644 index 0000000..d28f5f8 --- /dev/null +++ b/tests/test_listmodels.py @@ -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 diff --git a/tests/test_model_enumeration.py b/tests/test_model_enumeration.py new file mode 100644 index 0000000..7046574 --- /dev/null +++ b/tests/test_model_enumeration.py @@ -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" diff --git a/tests/test_server.py b/tests/test_server.py index 84ff793..dba5468 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -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: diff --git a/tools/__init__.py b/tools/__init__.py index 100686f..0de98d3 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -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", diff --git a/tools/base.py b/tools/base.py index 8c9d105..5013b84 100644 --- a/tools/base.py +++ b/tools/base.py @@ -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. diff --git a/tools/codereview.py b/tools/codereview.py index c2b3c11..73e2401 100644 --- a/tools/codereview.py +++ b/tools/codereview.py @@ -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.""" diff --git a/tools/listmodels.py b/tools/listmodels.py new file mode 100644 index 0000000..a7a932d --- /dev/null +++ b/tools/listmodels.py @@ -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