Breaking change: openrouter_models.json -> custom_models.json

* Support for Custom URLs and custom models, including locally hosted models such as ollama
* Support for native + openrouter + local models (i.e. dozens of models) means you can start delegating sub-tasks to particular models or work to local models such as localizations or other boring work etc.
* Several tests added
* precommit to also include untracked (new) files
* Logfile auto rollover
* Improved logging
This commit is contained in:
Fahad
2025-06-13 15:22:09 +04:00
parent f5fdf7b2ed
commit f44ca326ef
27 changed files with 1692 additions and 351 deletions

View File

@@ -24,6 +24,7 @@ import os
import sys
import time
from datetime import datetime
from logging.handlers import RotatingFileHandler
from typing import Any
from mcp.server import Server
@@ -79,16 +80,17 @@ logging.basicConfig(
for handler in logging.getLogger().handlers:
handler.setFormatter(LocalTimeFormatter(log_format))
# Add file handler for Docker log monitoring
# Add rotating file handler for Docker log monitoring
try:
file_handler = logging.FileHandler("/tmp/mcp_server.log")
# Main server log with rotation (10MB max, keep 2 files)
file_handler = RotatingFileHandler("/tmp/mcp_server.log", maxBytes=10 * 1024 * 1024, backupCount=2)
file_handler.setLevel(getattr(logging, log_level, logging.INFO))
file_handler.setFormatter(LocalTimeFormatter(log_format))
logging.getLogger().addHandler(file_handler)
# Create a special logger for MCP activity tracking
# Create a special logger for MCP activity tracking with rotation
mcp_logger = logging.getLogger("mcp_activity")
mcp_file_handler = logging.FileHandler("/tmp/mcp_activity.log")
mcp_file_handler = RotatingFileHandler("/tmp/mcp_activity.log", maxBytes=10 * 1024 * 1024, backupCount=2)
mcp_file_handler.setLevel(logging.INFO)
mcp_file_handler.setFormatter(LocalTimeFormatter("%(asctime)s - %(message)s"))
mcp_logger.addHandler(mcp_file_handler)
@@ -128,6 +130,7 @@ def configure_providers():
"""
from providers import ModelProviderRegistry
from providers.base import ProviderType
from providers.custom import CustomProvider
from providers.gemini import GeminiModelProvider
from providers.openai import OpenAIModelProvider
from providers.openrouter import OpenRouterProvider
@@ -135,6 +138,7 @@ def configure_providers():
valid_providers = []
has_native_apis = False
has_openrouter = False
has_custom = False
# Check for Gemini API key
gemini_key = os.getenv("GEMINI_API_KEY")
@@ -157,31 +161,68 @@ def configure_providers():
has_openrouter = True
logger.info("OpenRouter API key found - Multiple models available via OpenRouter")
# Register providers - native APIs first to ensure they take priority
# Check for custom API endpoint (Ollama, vLLM, etc.)
custom_url = os.getenv("CUSTOM_API_URL")
if custom_url:
# IMPORTANT: Always read CUSTOM_API_KEY even if empty
# - Some providers (vLLM, LM Studio, enterprise APIs) require authentication
# - Others (Ollama) work without authentication (empty key)
# - DO NOT remove this variable - it's needed for provider factory function
custom_key = os.getenv("CUSTOM_API_KEY", "") # Default to empty (Ollama doesn't need auth)
custom_model = os.getenv("CUSTOM_MODEL_NAME", "llama3.2")
valid_providers.append(f"Custom API ({custom_url})")
has_custom = True
logger.info(f"Custom API endpoint found: {custom_url} with model {custom_model}")
if custom_key:
logger.debug("Custom API key provided for authentication")
else:
logger.debug("No custom API key provided (using unauthenticated access)")
# Register providers in priority order:
# 1. Native APIs first (most direct and efficient)
if has_native_apis:
if gemini_key and gemini_key != "your_gemini_api_key_here":
ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
if openai_key and openai_key != "your_openai_api_key_here":
ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
# Register OpenRouter last so native APIs take precedence
# 2. Custom provider second (for local/private models)
if has_custom:
# Factory function that creates CustomProvider with proper parameters
def custom_provider_factory(api_key=None):
# api_key is CUSTOM_API_KEY (can be empty for Ollama), base_url from CUSTOM_API_URL
base_url = os.getenv("CUSTOM_API_URL", "")
return CustomProvider(api_key=api_key or "", base_url=base_url) # Use provided API key or empty string
ModelProviderRegistry.register_provider(ProviderType.CUSTOM, custom_provider_factory)
# 3. OpenRouter last (catch-all for everything else)
if has_openrouter:
ModelProviderRegistry.register_provider(ProviderType.OPENROUTER, OpenRouterProvider)
# Require at least one valid provider
if not valid_providers:
raise ValueError(
"At least one API key is required. Please set either:\n"
"At least one API configuration is required. Please set either:\n"
"- GEMINI_API_KEY for Gemini models\n"
"- OPENAI_API_KEY for OpenAI o3 model\n"
"- OPENROUTER_API_KEY for OpenRouter (multiple models)"
"- OPENROUTER_API_KEY for OpenRouter (multiple models)\n"
"- CUSTOM_API_URL for local models (Ollama, vLLM, etc.)"
)
logger.info(f"Available providers: {', '.join(valid_providers)}")
# Log provider priority if both are configured
if has_native_apis and has_openrouter:
logger.info("Provider priority: Native APIs (Gemini, OpenAI) will be checked before OpenRouter")
# Log provider priority
priority_info = []
if has_native_apis:
priority_info.append("Native APIs (Gemini, OpenAI)")
if has_custom:
priority_info.append("Custom endpoints")
if has_openrouter:
priority_info.append("OpenRouter (catch-all)")
if len(priority_info) > 1:
logger.info(f"Provider priority: {''.join(priority_info)}")
@server.list_tools()
@@ -536,7 +577,7 @@ async def handle_get_version() -> list[TextContent]:
if ModelProviderRegistry.get_provider(ProviderType.OPENAI):
configured_providers.append("OpenAI (o3, o3-mini)")
if ModelProviderRegistry.get_provider(ProviderType.OPENROUTER):
configured_providers.append("OpenRouter (configured via conf/openrouter_models.json)")
configured_providers.append("OpenRouter (configured via conf/custom_models.json)")
# Format the information in a human-readable way
text = f"""Zen MCP Server v{__version__}