mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Add custom model download functionality and enhance model management
- Implemented `model-download-custom` command to download HuggingFace models. - Added support for discovering manually placed models in the cache. - Enhanced the model list view to display recommended and discovered models separately. - Introduced JSON editor for direct configuration mode in API settings. - Added validation and formatting features for JSON input. - Updated translations for new API settings and common actions. - Improved user interface for model management, including action buttons and tooltips.
This commit is contained in:
@@ -1995,6 +1995,55 @@ def model_delete(
|
||||
console.print(f" Freed space: {data['deleted_size_mb']:.1f} MB")
|
||||
|
||||
|
||||
@app.command(name="model-download-custom")
|
||||
def model_download_custom(
|
||||
model_name: str = typer.Argument(..., help="Full HuggingFace model name (e.g., BAAI/bge-small-en-v1.5)."),
|
||||
model_type: str = typer.Option("embedding", "--type", help="Model type: embedding or reranker."),
|
||||
json_mode: bool = typer.Option(False, "--json", help="Output JSON response."),
|
||||
) -> None:
|
||||
"""Download a custom HuggingFace model by name.
|
||||
|
||||
This allows downloading any fastembed-compatible model from HuggingFace.
|
||||
|
||||
Example:
|
||||
codexlens model-download-custom BAAI/bge-small-en-v1.5
|
||||
codexlens model-download-custom BAAI/bge-reranker-base --type reranker
|
||||
"""
|
||||
try:
|
||||
from codexlens.cli.model_manager import download_custom_model
|
||||
|
||||
if not json_mode:
|
||||
console.print(f"[bold]Downloading custom model:[/bold] {model_name}")
|
||||
console.print(f"[dim]Model type: {model_type}[/dim]")
|
||||
console.print("[dim]This may take a few minutes depending on your internet connection...[/dim]\n")
|
||||
|
||||
progress_callback = None if json_mode else lambda msg: console.print(f"[cyan]{msg}[/cyan]")
|
||||
|
||||
result = download_custom_model(model_name, model_type=model_type, progress_callback=progress_callback)
|
||||
|
||||
if json_mode:
|
||||
print_json(**result)
|
||||
else:
|
||||
if not result["success"]:
|
||||
console.print(f"[red]Error:[/red] {result.get('error', 'Unknown error')}")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
data = result["result"]
|
||||
console.print(f"[green]✓[/green] Custom model downloaded successfully!")
|
||||
console.print(f" Model: {data['model_name']}")
|
||||
console.print(f" Type: {data['model_type']}")
|
||||
console.print(f" Cache size: {data['cache_size_mb']:.1f} MB")
|
||||
console.print(f" Location: [dim]{data['cache_path']}[/dim]")
|
||||
|
||||
except ImportError:
|
||||
if json_mode:
|
||||
print_json(success=False, error="fastembed not installed. Install with: pip install codexlens[semantic]")
|
||||
else:
|
||||
console.print("[red]Error:[/red] fastembed not installed")
|
||||
console.print("[yellow]Install with:[/yellow] pip install codexlens[semantic]")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
@app.command(name="model-info")
|
||||
def model_info(
|
||||
profile: str = typer.Argument(..., help="Model profile to get info (fast, code, multilingual, balanced)."),
|
||||
|
||||
@@ -76,6 +76,31 @@ RERANKER_MODEL_PROFILES = {
|
||||
"use_case": "Fast reranking with good accuracy",
|
||||
"recommended": True,
|
||||
},
|
||||
# Additional reranker models (commonly used)
|
||||
"bge-reranker-v2-m3": {
|
||||
"model_name": "BAAI/bge-reranker-v2-m3",
|
||||
"cache_name": "BAAI/bge-reranker-v2-m3",
|
||||
"size_mb": 560,
|
||||
"description": "BGE v2 M3 reranker, multilingual",
|
||||
"use_case": "Multilingual reranking, latest BGE version",
|
||||
"recommended": True,
|
||||
},
|
||||
"bge-reranker-v2-gemma": {
|
||||
"model_name": "BAAI/bge-reranker-v2-gemma",
|
||||
"cache_name": "BAAI/bge-reranker-v2-gemma",
|
||||
"size_mb": 2000,
|
||||
"description": "BGE v2 Gemma reranker, best quality",
|
||||
"use_case": "Maximum quality with Gemma backbone",
|
||||
"recommended": False,
|
||||
},
|
||||
"cross-encoder-ms-marco": {
|
||||
"model_name": "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
||||
"cache_name": "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
||||
"size_mb": 90,
|
||||
"description": "Original cross-encoder MS MARCO",
|
||||
"use_case": "Classic cross-encoder baseline",
|
||||
"recommended": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +163,106 @@ MODEL_PROFILES = {
|
||||
"use_case": "High-quality semantic search, balanced performance",
|
||||
"recommended": False, # 1024d not recommended
|
||||
},
|
||||
# Additional embedding models (commonly used)
|
||||
"bge-large": {
|
||||
"model_name": "BAAI/bge-large-en-v1.5",
|
||||
"cache_name": "qdrant/bge-large-en-v1.5-onnx-q",
|
||||
"dimensions": 1024,
|
||||
"size_mb": 650,
|
||||
"description": "BGE large model, highest quality",
|
||||
"use_case": "Maximum quality semantic search",
|
||||
"recommended": False,
|
||||
},
|
||||
"e5-small": {
|
||||
"model_name": "intfloat/e5-small-v2",
|
||||
"cache_name": "qdrant/e5-small-v2-onnx",
|
||||
"dimensions": 384,
|
||||
"size_mb": 80,
|
||||
"description": "E5 small model, fast and lightweight",
|
||||
"use_case": "Low latency applications",
|
||||
"recommended": True,
|
||||
},
|
||||
"e5-base": {
|
||||
"model_name": "intfloat/e5-base-v2",
|
||||
"cache_name": "qdrant/e5-base-v2-onnx",
|
||||
"dimensions": 768,
|
||||
"size_mb": 220,
|
||||
"description": "E5 base model, balanced",
|
||||
"use_case": "General purpose semantic search",
|
||||
"recommended": True,
|
||||
},
|
||||
"e5-large": {
|
||||
"model_name": "intfloat/e5-large-v2",
|
||||
"cache_name": "qdrant/e5-large-v2-onnx",
|
||||
"dimensions": 1024,
|
||||
"size_mb": 650,
|
||||
"description": "E5 large model, high quality",
|
||||
"use_case": "High quality semantic search",
|
||||
"recommended": False,
|
||||
},
|
||||
"jina-base-en": {
|
||||
"model_name": "jinaai/jina-embeddings-v2-base-en",
|
||||
"cache_name": "jinaai/jina-embeddings-v2-base-en",
|
||||
"dimensions": 768,
|
||||
"size_mb": 150,
|
||||
"description": "Jina base English model",
|
||||
"use_case": "English text semantic search",
|
||||
"recommended": True,
|
||||
},
|
||||
"jina-small-en": {
|
||||
"model_name": "jinaai/jina-embeddings-v2-small-en",
|
||||
"cache_name": "jinaai/jina-embeddings-v2-small-en",
|
||||
"dimensions": 512,
|
||||
"size_mb": 60,
|
||||
"description": "Jina small English model, very fast",
|
||||
"use_case": "Low latency English text search",
|
||||
"recommended": True,
|
||||
},
|
||||
"snowflake-arctic": {
|
||||
"model_name": "Snowflake/snowflake-arctic-embed-m",
|
||||
"cache_name": "Snowflake/snowflake-arctic-embed-m",
|
||||
"dimensions": 768,
|
||||
"size_mb": 220,
|
||||
"description": "Snowflake Arctic embedding model",
|
||||
"use_case": "Enterprise semantic search, high quality",
|
||||
"recommended": True,
|
||||
},
|
||||
"nomic-embed": {
|
||||
"model_name": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"cache_name": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"dimensions": 768,
|
||||
"size_mb": 280,
|
||||
"description": "Nomic embedding model, open source",
|
||||
"use_case": "Open source text embedding",
|
||||
"recommended": True,
|
||||
},
|
||||
"gte-small": {
|
||||
"model_name": "thenlper/gte-small",
|
||||
"cache_name": "thenlper/gte-small",
|
||||
"dimensions": 384,
|
||||
"size_mb": 70,
|
||||
"description": "GTE small model, fast",
|
||||
"use_case": "Fast text embedding",
|
||||
"recommended": True,
|
||||
},
|
||||
"gte-base": {
|
||||
"model_name": "thenlper/gte-base",
|
||||
"cache_name": "thenlper/gte-base",
|
||||
"dimensions": 768,
|
||||
"size_mb": 220,
|
||||
"description": "GTE base model, balanced",
|
||||
"use_case": "General purpose text embedding",
|
||||
"recommended": True,
|
||||
},
|
||||
"gte-large": {
|
||||
"model_name": "thenlper/gte-large",
|
||||
"cache_name": "thenlper/gte-large",
|
||||
"dimensions": 1024,
|
||||
"size_mb": 650,
|
||||
"description": "GTE large model, high quality",
|
||||
"use_case": "High quality text embedding",
|
||||
"recommended": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +304,92 @@ def _get_model_cache_path(cache_dir: Path, info: Dict) -> Path:
|
||||
return cache_dir / sanitized_name
|
||||
|
||||
|
||||
def scan_discovered_models(model_type: str = "embedding") -> List[Dict]:
|
||||
"""Scan cache directory for manually placed models not in predefined profiles.
|
||||
|
||||
This allows users to manually download models (e.g., via huggingface-cli or
|
||||
by copying the model directory) and have them recognized automatically.
|
||||
|
||||
Args:
|
||||
model_type: Type of models to scan for ("embedding" or "reranker")
|
||||
|
||||
Returns:
|
||||
List of discovered model info dictionaries
|
||||
"""
|
||||
cache_dir = get_cache_dir()
|
||||
if not cache_dir.exists():
|
||||
return []
|
||||
|
||||
# Get known model cache names based on type
|
||||
if model_type == "reranker":
|
||||
known_cache_names = {
|
||||
f"models--{info.get('cache_name', info['model_name']).replace('/', '--')}"
|
||||
for info in RERANKER_MODEL_PROFILES.values()
|
||||
}
|
||||
else:
|
||||
known_cache_names = {
|
||||
f"models--{info.get('cache_name', info['model_name']).replace('/', '--')}"
|
||||
for info in MODEL_PROFILES.values()
|
||||
}
|
||||
|
||||
discovered = []
|
||||
|
||||
# Scan for model directories in cache
|
||||
for item in cache_dir.iterdir():
|
||||
if not item.is_dir() or not item.name.startswith("models--"):
|
||||
continue
|
||||
|
||||
# Skip known predefined models
|
||||
if item.name in known_cache_names:
|
||||
continue
|
||||
|
||||
# Parse model name from directory (models--org--model -> org/model)
|
||||
parts = item.name[8:].split("--") # Remove "models--" prefix
|
||||
if len(parts) >= 2:
|
||||
model_name = "/".join(parts)
|
||||
else:
|
||||
model_name = parts[0] if parts else item.name
|
||||
|
||||
# Detect model type by checking for common patterns
|
||||
is_reranker = any(keyword in model_name.lower() for keyword in [
|
||||
"reranker", "cross-encoder", "ms-marco"
|
||||
])
|
||||
is_embedding = any(keyword in model_name.lower() for keyword in [
|
||||
"embed", "bge", "e5", "jina", "minilm", "gte", "nomic", "arctic"
|
||||
])
|
||||
|
||||
# Filter based on requested type
|
||||
if model_type == "reranker" and not is_reranker:
|
||||
continue
|
||||
if model_type == "embedding" and is_reranker:
|
||||
continue
|
||||
|
||||
# Calculate cache size
|
||||
try:
|
||||
total_size = sum(
|
||||
f.stat().st_size
|
||||
for f in item.rglob("*")
|
||||
if f.is_file()
|
||||
)
|
||||
cache_size_mb = round(total_size / (1024 * 1024), 1)
|
||||
except (OSError, PermissionError):
|
||||
cache_size_mb = 0
|
||||
|
||||
discovered.append({
|
||||
"profile": f"discovered:{model_name.replace('/', '-')}",
|
||||
"model_name": model_name,
|
||||
"cache_name": model_name,
|
||||
"cache_path": str(item),
|
||||
"actual_size_mb": cache_size_mb,
|
||||
"description": f"Manually discovered model",
|
||||
"use_case": "User-provided model",
|
||||
"installed": True,
|
||||
"source": "discovered", # Mark as discovered
|
||||
})
|
||||
|
||||
return discovered
|
||||
|
||||
|
||||
def list_models() -> Dict[str, any]:
|
||||
"""List available model profiles and their installation status.
|
||||
|
||||
@@ -224,14 +435,45 @@ def list_models() -> Dict[str, any]:
|
||||
"description": info["description"],
|
||||
"use_case": info["use_case"],
|
||||
"installed": installed,
|
||||
"source": "predefined", # Mark as predefined
|
||||
"recommended": info.get("recommended", True),
|
||||
})
|
||||
|
||||
# Add discovered models (manually placed by user)
|
||||
discovered = scan_discovered_models(model_type="embedding")
|
||||
for model in discovered:
|
||||
# Try to estimate dimensions based on common model patterns
|
||||
dimensions = 768 # Default
|
||||
name_lower = model["model_name"].lower()
|
||||
if "small" in name_lower or "mini" in name_lower:
|
||||
dimensions = 384
|
||||
elif "large" in name_lower:
|
||||
dimensions = 1024
|
||||
|
||||
model["dimensions"] = dimensions
|
||||
model["estimated_size_mb"] = model.get("actual_size_mb", 0)
|
||||
model["recommended"] = False # User-provided models are not recommended by default
|
||||
models.append(model)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": {
|
||||
"models": models,
|
||||
"cache_dir": str(cache_dir),
|
||||
"cache_exists": cache_exists,
|
||||
"manual_install_guide": {
|
||||
"steps": [
|
||||
"1. Download: huggingface-cli download <org>/<model>",
|
||||
"2. Or copy to cache directory (see paths below)",
|
||||
"3. Refresh to see discovered models"
|
||||
],
|
||||
"example": "huggingface-cli download BAAI/bge-small-en-v1.5",
|
||||
"paths": {
|
||||
"windows": "%USERPROFILE%\\.cache\\huggingface\\models--<org>--<model>",
|
||||
"linux": "~/.cache/huggingface/models--<org>--<model>",
|
||||
"macos": "~/.cache/huggingface/models--<org>--<model>",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -313,6 +555,92 @@ def download_model(profile: str, progress_callback: Optional[callable] = None) -
|
||||
}
|
||||
|
||||
|
||||
def download_custom_model(model_name: str, model_type: str = "embedding", progress_callback: Optional[callable] = None) -> Dict[str, any]:
|
||||
"""Download a custom model by HuggingFace model name.
|
||||
|
||||
This allows users to download any HuggingFace model that is compatible
|
||||
with fastembed (TextEmbedding or TextCrossEncoder).
|
||||
|
||||
Args:
|
||||
model_name: Full HuggingFace model name (e.g., "BAAI/bge-small-en-v1.5")
|
||||
model_type: Type of model ("embedding" or "reranker")
|
||||
progress_callback: Optional callback function to report progress
|
||||
|
||||
Returns:
|
||||
Result dictionary with success status
|
||||
"""
|
||||
if model_type == "embedding":
|
||||
if not FASTEMBED_AVAILABLE:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "fastembed not installed. Install with: pip install codexlens[semantic]",
|
||||
}
|
||||
else:
|
||||
if not RERANKER_AVAILABLE:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "fastembed reranker not available. Install with: pip install fastembed>=0.4.0",
|
||||
}
|
||||
|
||||
# Validate model name format (org/model-name)
|
||||
if not model_name or "/" not in model_name:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Invalid model name format. Expected: 'org/model-name' (e.g., 'BAAI/bge-small-en-v1.5')",
|
||||
}
|
||||
|
||||
try:
|
||||
cache_dir = get_cache_dir()
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(f"Downloading custom model {model_name}...")
|
||||
|
||||
if model_type == "reranker":
|
||||
# Download reranker model
|
||||
reranker = TextCrossEncoder(model_name=model_name, cache_dir=str(cache_dir))
|
||||
if progress_callback:
|
||||
progress_callback(f"Initializing reranker {model_name}...")
|
||||
list(reranker.rerank("test query", ["test document"]))
|
||||
else:
|
||||
# Download embedding model
|
||||
embedder = TextEmbedding(model_name=model_name, cache_dir=str(cache_dir))
|
||||
if progress_callback:
|
||||
progress_callback(f"Initializing {model_name}...")
|
||||
list(embedder.embed(["test"]))
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(f"Custom model {model_name} downloaded successfully")
|
||||
|
||||
# Get cache info
|
||||
sanitized_name = f"models--{model_name.replace('/', '--')}"
|
||||
model_cache_path = cache_dir / sanitized_name
|
||||
|
||||
cache_size = 0
|
||||
if model_cache_path.exists():
|
||||
total_size = sum(
|
||||
f.stat().st_size
|
||||
for f in model_cache_path.rglob("*")
|
||||
if f.is_file()
|
||||
)
|
||||
cache_size = round(total_size / (1024 * 1024), 1)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": {
|
||||
"model_name": model_name,
|
||||
"model_type": model_type,
|
||||
"cache_size_mb": cache_size,
|
||||
"cache_path": str(model_cache_path),
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to download custom model: {str(e)}",
|
||||
}
|
||||
|
||||
|
||||
def delete_model(profile: str) -> Dict[str, any]:
|
||||
"""Delete a downloaded model from cache.
|
||||
|
||||
@@ -464,14 +792,35 @@ def list_reranker_models() -> Dict[str, any]:
|
||||
"use_case": info["use_case"],
|
||||
"installed": installed,
|
||||
"recommended": info.get("recommended", True),
|
||||
"source": "predefined", # Mark as predefined
|
||||
})
|
||||
|
||||
# Add discovered reranker models (manually placed by user)
|
||||
discovered = scan_discovered_models(model_type="reranker")
|
||||
for model in discovered:
|
||||
model["estimated_size_mb"] = model.get("actual_size_mb", 0)
|
||||
model["recommended"] = False # User-provided models are not recommended by default
|
||||
models.append(model)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": {
|
||||
"models": models,
|
||||
"cache_dir": str(cache_dir),
|
||||
"cache_exists": cache_exists,
|
||||
"manual_install_guide": {
|
||||
"steps": [
|
||||
"1. Download: huggingface-cli download <org>/<model>",
|
||||
"2. Or copy to cache directory (see paths below)",
|
||||
"3. Refresh to see discovered models",
|
||||
],
|
||||
"example": "huggingface-cli download BAAI/bge-reranker-base",
|
||||
"paths": {
|
||||
"windows": "%USERPROFILE%\\.cache\\huggingface\\models--<org>--<model>",
|
||||
"linux": "~/.cache/huggingface/models--<org>--<model>",
|
||||
"macos": "~/.cache/huggingface/models--<org>--<model>",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user