Files
zeekay d094a8b481 rebrand: rename litellm module to llm — complete purge
All imports, test files, docs, and config references updated.
import litellm → import llm across entire codebase.
2026-02-25 15:13:14 -08:00

456 lines
16 KiB
Python

"""LLM management tool for enabling/disabling LLM providers."""
import json
from typing import Unpack, Optional, Annotated, TypedDict, final, override
from pathlib import Path
from pydantic import Field
from mcp.server.fastmcp import Context as MCPContext
from hanzo_tools.core import BaseTool, ToolContext, auto_timeout, create_tool_context
from .llm_unified import LLMTool
Action = Annotated[
str,
Field(
description="Action to perform: list, enable, disable, test",
min_length=1,
),
]
Provider = Annotated[
Optional[str],
Field(
description="Provider name (for enable/disable/test actions)",
default=None,
),
]
Model = Annotated[
Optional[str],
Field(
description="Model to test (for test action)",
default=None,
),
]
class LLMManageParams(TypedDict, total=False):
"""Parameters for LLM management tool."""
action: str
provider: Optional[str]
model: Optional[str]
@final
class LLMManageTool(BaseTool):
"""Tool for managing LLM providers."""
def __init__(self):
"""Initialize the LLM management tool."""
self.llm_tool = LLMTool()
self.config_file = Path.home() / ".hanzo" / "llm" / "providers.json"
self.config_file.parent.mkdir(parents=True, exist_ok=True)
self._load_config()
def _load_config(self):
"""Load provider configuration."""
if self.config_file.exists():
try:
with open(self.config_file, "r") as f:
self.config = json.load(f)
except Exception:
self.config = {"disabled_providers": []}
else:
self.config = {"disabled_providers": []}
def _save_config(self):
"""Save provider configuration."""
with open(self.config_file, "w") as f:
json.dump(self.config, f, indent=2)
@property
@override
def name(self) -> str:
"""Get the tool name."""
return "llm_manage"
@property
@override
def description(self) -> str:
"""Get the tool description."""
return """Manage LLM providers and test configurations.
Actions:
- list: Show all providers and their status
- models: List all available models (optionally filtered by provider)
- enable: Enable a provider's tools
- disable: Disable a provider's tools
- test: Test a model to verify it works
Examples:
- llm_manage --action list
- llm_manage --action models
- llm_manage --action models --provider openai
- llm_manage --action enable --provider openai
- llm_manage --action disable --provider perplexity
- llm_manage --action test --model "gpt-4"
- llm_manage --action test --provider groq --model "mixtral"
Providers are automatically detected based on environment variables:
- OpenAI: OPENAI_API_KEY
- Anthropic: ANTHROPIC_API_KEY or CLAUDE_API_KEY
- Google: GOOGLE_API_KEY or GEMINI_API_KEY
- Groq: GROQ_API_KEY
- Mistral: MISTRAL_API_KEY
- Perplexity: PERPLEXITY_API_KEY
- And many more...
"""
@override
@auto_timeout("llm_manage")
async def call(
self,
ctx: MCPContext,
**params: Unpack[LLMManageParams],
) -> str:
"""Manage LLM providers.
Args:
ctx: MCP context
**params: Tool parameters
Returns:
Result of the management action
"""
tool_ctx = create_tool_context(ctx)
await tool_ctx.set_tool_info(self.name)
# Extract parameters
action = params.get("action")
if not action:
return "Error: action is required (list, enable, disable, test, models)"
provider = params.get("provider")
model = params.get("model")
# Handle different actions
if action == "list":
return self._list_providers()
elif action == "models":
return self._list_all_models(provider)
elif action == "enable":
return self._enable_provider(provider)
elif action == "disable":
return self._disable_provider(provider)
elif action == "test":
return await self._test_model(ctx, provider, model)
else:
return f"Error: Invalid action '{action}'. Must be one of: list, models, enable, disable, test"
def _list_providers(self) -> str:
"""List all providers and their status."""
output = ["=== LLM Providers ==="]
output.append("")
# Get all possible providers
all_providers = sorted(LLMTool.API_KEY_ENV_VARS.keys())
available_providers = self.llm_tool.available_providers
disabled_providers = self.config.get("disabled_providers", [])
# Categorize providers
active = []
available_but_disabled = []
no_api_key = []
for provider in all_providers:
if provider in available_providers:
if provider in disabled_providers:
available_but_disabled.append(provider)
else:
active.append(provider)
else:
no_api_key.append(provider)
# Show active providers
if active:
output.append("✅ Active Providers (API key found, enabled):")
for provider in active:
env_vars = available_providers.get(provider, [])
output.append(f" - {provider}: {', '.join(env_vars)}")
# Show example models
examples = self._get_example_models(provider)
if examples:
output.append(f" Models: {', '.join(examples[:3])}")
output.append("")
# Show disabled providers
if available_but_disabled:
output.append("⚠️ Available but Disabled (API key found, disabled):")
for provider in available_but_disabled:
env_vars = available_providers.get(provider, [])
output.append(f" - {provider}: {', '.join(env_vars)}")
output.append(
f" Use: llm_manage --action enable --provider {provider}"
)
output.append("")
# Show providers without API keys
if no_api_key:
output.append("❌ No API Key Found:")
for provider in no_api_key[:10]: # Show first 10
env_vars = LLMTool.API_KEY_ENV_VARS.get(provider, [])
output.append(f" - {provider}: Set one of {', '.join(env_vars)}")
if len(no_api_key) > 10:
output.append(f" ... and {len(no_api_key) - 10} more")
output.append("")
# Summary
output.append("=== Summary ===")
output.append(f"Total providers: {len(all_providers)}")
output.append(f"Active: {len(active)}")
output.append(f"Disabled: {len(available_but_disabled)}")
output.append(f"No API key: {len(no_api_key)}")
# Show available tools
if active:
output.append("\n=== Available LLM Tools ===")
output.append("- llm: Universal LLM tool (all providers)")
output.append("- consensus: Query multiple models in parallel")
provider_tools = []
for provider in active:
if provider in [
"openai",
"anthropic",
"google",
"groq",
"mistral",
"perplexity",
]:
tool_name = "gemini" if provider == "google" else provider
provider_tools.append(tool_name)
if provider_tools:
output.append(f"- Provider tools: {', '.join(provider_tools)}")
return "\n".join(output)
def _enable_provider(self, provider: Optional[str]) -> str:
"""Enable a provider."""
if not provider:
return "Error: provider is required for enable action"
if provider not in self.llm_tool.available_providers:
env_vars = LLMTool.API_KEY_ENV_VARS.get(provider, [])
if env_vars:
return f"Error: No API key found for {provider}. Set one of: {', '.join(env_vars)}"
else:
return f"Error: Unknown provider '{provider}'"
disabled = self.config.get("disabled_providers", [])
if provider in disabled:
disabled.remove(provider)
self.config["disabled_providers"] = disabled
self._save_config()
return f"Successfully enabled {provider}"
else:
return f"{provider} is already enabled"
def _disable_provider(self, provider: Optional[str]) -> str:
"""Disable a provider."""
if not provider:
return "Error: provider is required for disable action"
disabled = self.config.get("disabled_providers", [])
if provider not in disabled:
disabled.append(provider)
self.config["disabled_providers"] = disabled
self._save_config()
return f"Successfully disabled {provider}. Its tools will no longer be available."
else:
return f"{provider} is already disabled"
def _list_all_models(self, provider: Optional[str] = None) -> str:
"""List all available models from LLM."""
try:
from .llm_unified import LLMTool
all_models = LLMTool.get_all_models()
if not all_models:
return "No models available or LLM not installed"
output = ["=== Available LLM Models ==="]
if provider:
# Show models for specific provider
provider_lower = provider.lower()
models = all_models.get(provider_lower, [])
if not models:
return f"No models found for provider '{provider}'"
output.append(f"\n{provider.upper()} ({len(models)} models):")
output.append("-" * 40)
# Show first 50 models
for model in models[:50]:
output.append(f" {model}")
if len(models) > 50:
output.append(f" ... and {len(models) - 50} more")
else:
# Show summary of all providers
total_models = sum(len(models) for models in all_models.values())
output.append(f"Total models available: {total_models}")
output.append("")
# Show providers with counts
for provider_name, models in sorted(all_models.items()):
if models:
output.append(f"{provider_name}: {len(models)} models")
output.append(
"\nUse 'llm_manage --action models --provider <name>' to see models for a specific provider"
)
# Show recommended models
output.append("\n=== Recommended Models ===")
recommended = {
"OpenAI": ["gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo"],
"Anthropic": [
"claude-3-opus-20240229",
"claude-3-5-sonnet-20241022",
"claude-3-haiku-20240307",
],
"Google": ["gemini/gemini-1.5-pro", "gemini/gemini-1.5-flash"],
"Groq": [
"groq/llama3-70b-8192",
"groq/llama3-8b-8192",
"groq/gemma2-9b-it",
],
"Mistral": [
"mistral/mistral-large-latest",
"mistral/mistral-medium",
],
}
for provider_name, models in recommended.items():
available = LLMTool().available_providers
provider_key = provider_name.lower()
if provider_key in available:
output.append(f"\n{provider_name} (✅ API key found):")
for model in models:
output.append(f" - {model}")
else:
output.append(f"\n{provider_name} (❌ No API key):")
for model in models:
output.append(f" - {model}")
return "\n".join(output)
except Exception as e:
return f"Error listing models: {str(e)}"
async def _test_model(
self, ctx: MCPContext, provider: Optional[str], model: Optional[str]
) -> str:
"""Test a model to verify it works."""
if not model and not provider:
return "Error: Either model or provider is required for test action"
# Determine model to test
if model:
test_model = model
else:
# Use default model for provider
default_models = {
"openai": "gpt-3.5-turbo",
"anthropic": "claude-3-haiku-20240307",
"google": "gemini/gemini-pro",
"groq": "groq/mixtral-8x7b-32768",
"mistral": "mistral/mistral-tiny",
"perplexity": "perplexity/sonar-small-online",
}
test_model = default_models.get(provider)
if not test_model:
return f"Error: No default model for provider '{provider}'. Please specify a model."
# Test the model
test_prompt = "Hello! Please respond with 'OK' if you can hear me."
output = [f"Testing model: {test_model}"]
output.append(f"Prompt: {test_prompt}")
output.append("")
try:
# Call the LLM
params = {
"model": test_model,
"prompt": test_prompt,
"max_tokens": 10,
"temperature": 0,
}
response = await self.llm_tool.call(ctx, **params)
if response.startswith("Error:"):
output.append("❌ Test failed:")
output.append(response)
else:
output.append("✅ Test successful!")
output.append(f"Response: {response}")
output.append("")
output.append(f"Model '{test_model}' is working correctly.")
# Show provider info
detected_provider = self.llm_tool._get_provider_for_model(test_model)
if detected_provider:
output.append(f"Provider: {detected_provider}")
except Exception as e:
output.append("❌ Test failed with exception:")
output.append(str(e))
return "\n".join(output)
def _get_example_models(self, provider: str) -> list[str]:
"""Get example models for a provider."""
examples = {
"openai": ["gpt-4o", "gpt-4", "gpt-3.5-turbo", "o1-preview"],
"anthropic": [
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
],
"google": [
"gemini/gemini-pro",
"gemini/gemini-1.5-pro",
"gemini/gemini-1.5-flash",
],
"groq": [
"groq/mixtral-8x7b-32768",
"groq/llama3-70b-8192",
"groq/llama3-8b-8192",
],
"mistral": [
"mistral/mistral-large-latest",
"mistral/mistral-medium",
"mistral/mistral-small",
],
"perplexity": [
"perplexity/sonar-medium-online",
"perplexity/sonar-small-online",
],
}
return examples.get(provider, [])
def register(self, mcp_server) -> None:
"""Register this tool with the MCP server."""
pass