Merge pull request #227 from svnlto/fix/uvx-resource-packaging

fix: uvx resource packaging issues for OpenRouter functionality
This commit is contained in:
Beehive Innovations
2025-08-11 12:31:49 -07:00
committed by GitHub
6 changed files with 174 additions and 17 deletions

View File

@@ -67,16 +67,16 @@ echo "📋 Step 1: Running Linting and Formatting Checks"
echo "--------------------------------------------------" echo "--------------------------------------------------"
echo "🔧 Running ruff linting with auto-fix..." echo "🔧 Running ruff linting with auto-fix..."
$RUFF check --fix --exclude test_simulation_files $RUFF check --fix --exclude test_simulation_files --exclude .zen_venv
echo "🎨 Running black code formatting..." echo "🎨 Running black code formatting..."
$BLACK . --exclude="test_simulation_files/" $BLACK . --exclude="test_simulation_files/" --exclude=".zen_venv/"
echo "📦 Running import sorting with isort..." echo "📦 Running import sorting with isort..."
$ISORT . --skip-glob=".zen_venv/*" --skip-glob="test_simulation_files/*" $ISORT . --skip-glob=".zen_venv/*" --skip-glob="test_simulation_files/*"
echo "✅ Verifying all linting passes..." echo "✅ Verifying all linting passes..."
$RUFF check --exclude test_simulation_files $RUFF check --exclude test_simulation_files --exclude .zen_venv
echo "✅ Step 1 Complete: All linting and formatting checks passed!" echo "✅ Step 1 Complete: All linting and formatting checks passed!"
echo "" echo ""

1
conf/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Configuration data for Zen MCP Server."""

View File

@@ -1,10 +1,12 @@
"""OpenRouter model registry for managing model configurations and aliases.""" """OpenRouter model registry for managing model configurations and aliases."""
import importlib.resources
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
# Import handled via importlib.resources.files() calls directly
from utils.file_utils import read_json_file from utils.file_utils import read_json_file
from .base import ( from .base import (
@@ -26,7 +28,8 @@ class OpenRouterModelRegistry:
self.alias_map: dict[str, str] = {} # alias -> model_name self.alias_map: dict[str, str] = {} # alias -> model_name
self.model_map: dict[str, ModelCapabilities] = {} # model_name -> config self.model_map: dict[str, ModelCapabilities] = {} # model_name -> config
# Determine config path # Determine config path and loading strategy
self.use_resources = False
if config_path: if config_path:
# Direct config_path parameter # Direct config_path parameter
self.config_path = Path(config_path) self.config_path = Path(config_path)
@@ -37,9 +40,33 @@ class OpenRouterModelRegistry:
# Environment variable path # Environment variable path
self.config_path = Path(env_path) self.config_path = Path(env_path)
else: else:
# Default to conf/custom_models.json - use relative path from this file # Try importlib.resources for robust packaging support
# This works in development environment self.config_path = None
self.config_path = Path(__file__).parent.parent / "conf" / "custom_models.json" self.use_resources = False
try:
resource_traversable = importlib.resources.files("conf").joinpath("custom_models.json")
if hasattr(resource_traversable, "read_text"):
self.use_resources = True
else:
raise AttributeError("read_text not available")
except Exception:
pass
if not self.use_resources:
# Fallback to file system paths
potential_paths = [
Path(__file__).parent.parent / "conf" / "custom_models.json",
Path.cwd() / "conf" / "custom_models.json",
]
for path in potential_paths:
if path.exists():
self.config_path = path
break
if self.config_path is None:
self.config_path = potential_paths[0]
# Load configuration # Load configuration
self.reload() self.reload()
@@ -91,20 +118,44 @@ class OpenRouterModelRegistry:
self.model_map = {} self.model_map = {}
def _read_config(self) -> list[ModelCapabilities]: def _read_config(self) -> list[ModelCapabilities]:
"""Read configuration from file. """Read configuration from file or package resources.
Returns: Returns:
List of model configurations List of model configurations
""" """
if not self.config_path.exists():
logging.warning(f"OpenRouter model config not found at {self.config_path}")
return []
try: try:
# Use centralized JSON reading utility if self.use_resources:
data = read_json_file(str(self.config_path)) # Use importlib.resources for packaged environments
try:
resource_path = importlib.resources.files("conf").joinpath("custom_models.json")
if hasattr(resource_path, "read_text"):
# Python 3.9+
config_text = resource_path.read_text(encoding="utf-8")
else:
# Python 3.8 fallback
with resource_path.open("r", encoding="utf-8") as f:
config_text = f.read()
import json
data = json.loads(config_text)
logging.debug("Loaded OpenRouter config from package resources")
except Exception as e:
logging.warning(f"Failed to load config from resources: {e}")
return []
else:
# Use file path loading
if not self.config_path.exists():
logging.warning(f"OpenRouter model config not found at {self.config_path}")
return []
# Use centralized JSON reading utility
data = read_json_file(str(self.config_path))
logging.debug(f"Loaded OpenRouter config from file: {self.config_path}")
if data is None: if data is None:
raise ValueError(f"Could not read or parse JSON from {self.config_path}") location = "resources" if self.use_resources else str(self.config_path)
raise ValueError(f"Could not read or parse JSON from {location}")
# Parse models # Parse models
configs = [] configs = []
@@ -137,7 +188,8 @@ class OpenRouterModelRegistry:
# Re-raise ValueError for specific config errors # Re-raise ValueError for specific config errors
raise raise
except Exception as e: except Exception as e:
raise ValueError(f"Error reading config from {self.config_path}: {e}") location = "resources" if self.use_resources else str(self.config_path)
raise ValueError(f"Error reading config from {location}: {e}")
def _build_maps(self, configs: list[ModelCapabilities]) -> None: def _build_maps(self, configs: list[ModelCapabilities]) -> None:
"""Build alias and model maps from configurations. """Build alias and model maps from configurations.

View File

@@ -12,7 +12,7 @@ dependencies = [
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["tools*", "providers*", "systemprompts*", "utils*"] include = ["tools*", "providers*", "systemprompts*", "utils*", "conf*"]
[tool.setuptools] [tool.setuptools]
py-modules = ["server", "config"] py-modules = ["server", "config"]

View File

@@ -3,6 +3,7 @@ google-genai>=1.19.0
openai>=1.55.2 # Minimum version for httpx 0.28.0 compatibility openai>=1.55.2 # Minimum version for httpx 0.28.0 compatibility
pydantic>=2.0.0 pydantic>=2.0.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0
importlib-resources>=5.0.0; python_version<"3.9"
# Development dependencies (install with pip install -r requirements-dev.txt) # Development dependencies (install with pip install -r requirements-dev.txt)
# pytest>=7.4.0 # pytest>=7.4.0

View File

@@ -0,0 +1,103 @@
"""Tests for uvx path resolution functionality."""
from pathlib import Path
from unittest.mock import patch
from providers.openrouter_registry import OpenRouterModelRegistry
class TestUvxPathResolution:
"""Test uvx path resolution for OpenRouter model registry."""
def test_normal_operation(self):
"""Test that normal operation works in development environment."""
registry = OpenRouterModelRegistry()
assert len(registry.list_models()) > 0
assert len(registry.list_aliases()) > 0
def test_config_path_resolution(self):
"""Test that the config path resolution finds the config file in multiple locations."""
# Check that the config file exists in the development location
config_file = Path(__file__).parent.parent / "conf" / "custom_models.json"
assert config_file.exists(), "Config file should exist in conf/custom_models.json"
# Test that a registry can find and use the config
registry = OpenRouterModelRegistry()
# When using resources, config_path is None; when using file system, it should exist
if registry.use_resources:
assert registry.config_path is None, "When using resources, config_path should be None"
else:
assert registry.config_path.exists(), "When using file system, config path should exist"
assert len(registry.list_models()) > 0, "Registry should load models from config"
def test_explicit_config_path_override(self):
"""Test that explicit config path works correctly."""
config_path = Path(__file__).parent.parent / "conf" / "custom_models.json"
registry = OpenRouterModelRegistry(config_path=str(config_path))
# Should use the provided file path
assert registry.config_path == config_path
assert len(registry.list_models()) > 0
def test_environment_variable_override(self):
"""Test that CUSTOM_MODELS_CONFIG_PATH environment variable works."""
config_path = Path(__file__).parent.parent / "conf" / "custom_models.json"
with patch.dict("os.environ", {"CUSTOM_MODELS_CONFIG_PATH": str(config_path)}):
registry = OpenRouterModelRegistry()
# Should use environment path
assert registry.config_path == config_path
assert len(registry.list_models()) > 0
@patch("providers.openrouter_registry.importlib.resources.files")
@patch("pathlib.Path.exists")
def test_multiple_path_fallback(self, mock_exists, mock_files):
"""Test that multiple path resolution works for different deployment scenarios."""
# Make resources loading fail to trigger file system fallback
mock_files.side_effect = Exception("Resource loading failed")
# Simulate dev path failing, and working directory path succeeding
# The third `True` is for the check within `reload()`
mock_exists.side_effect = [False, True, True]
registry = OpenRouterModelRegistry()
# Should have fallen back to file system mode
assert not registry.use_resources, "Should fall back to file system when resources fail"
# Assert that the registry fell back to the second potential path
assert registry.config_path == Path.cwd() / "conf" / "custom_models.json"
# Should load models successfully
assert len(registry.list_models()) > 0
def test_missing_config_handling(self):
"""Test behavior when config file is missing."""
# Use a non-existent path
registry = OpenRouterModelRegistry(config_path="/nonexistent/path/config.json")
# Should gracefully handle missing config
assert len(registry.list_models()) == 0
assert len(registry.list_aliases()) == 0
def test_resource_loading_success(self):
"""Test successful resource loading via importlib.resources."""
# Just test that the registry works normally in our environment
# This validates the resource loading mechanism indirectly
registry = OpenRouterModelRegistry()
# Should load successfully using either resources or file system fallback
assert len(registry.list_models()) > 0
assert len(registry.list_aliases()) > 0
def test_use_resources_attribute(self):
"""Test that the use_resources attribute is properly set."""
registry = OpenRouterModelRegistry()
# Should have the use_resources attribute
assert hasattr(registry, "use_resources")
assert isinstance(registry.use_resources, bool)