import json import logging import asyncio import aiohttp from typing import Tuple, Optional, Dict, Any from datasets import load_dataset from huggingface_hub import HfApi, ModelCard, hf_hub_download from huggingface_hub import hf_api from huggingface_hub.utils import build_hf_headers from transformers import AutoConfig, AutoTokenizer from app.config.base import HF_TOKEN from app.config.hf_config import OFFICIAL_PROVIDERS_REPO from app.core.formatting import LogFormatter logger = logging.getLogger(__name__) class ModelValidator: def __init__(self): self.token = HF_TOKEN self.api = HfApi(token=self.token) self.headers = {"Authorization": f"Bearer {self.token}"} if self.token else {} async def check_model_card(self, model_id: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]: """Check if model has a valid model card""" try: logger.info(LogFormatter.info(f"Checking model card for {model_id}")) # Get model card content using ModelCard.load try: model_card = await asyncio.to_thread( ModelCard.load, model_id ) logger.info(LogFormatter.success("Model card found")) except Exception as e: error_msg = "Please add a model card to your model to explain how you trained/fine-tuned it." logger.error(LogFormatter.error(error_msg, e)) return False, error_msg, None # Check license in model card data if model_card.data.license is None and not ("license_name" in model_card.data and "license_link" in model_card.data): error_msg = "License not found. Please add a license to your model card using the `license` metadata or a `license_name`/`license_link` pair." logger.warning(LogFormatter.warning(error_msg)) return False, error_msg, None # Enforce card content length if len(model_card.text) < 200: error_msg = "Please add a description to your model card, it is too short." logger.warning(LogFormatter.warning(error_msg)) return False, error_msg, None logger.info(LogFormatter.success("Model card validation passed")) return True, "", model_card except Exception as e: error_msg = "Failed to validate model card" logger.error(LogFormatter.error(error_msg, e)) return False, str(e), None async def get_safetensors_metadata(self, model_id: str, is_adapter: bool = False, revision: str = "main") -> Optional[Dict]: """Get metadata from a safetensors file""" try: if is_adapter: metadata = await asyncio.to_thread( hf_api.parse_safetensors_file_metadata, model_id, "adapter_model.safetensors", token=self.token, revision=revision, ) else: metadata = await asyncio.to_thread( hf_api.get_safetensors_metadata, repo_id=model_id, token=self.token, revision=revision, ) return metadata except Exception as e: logger.error(f"Failed to get safetensors metadata: {str(e)}") return None async def get_model_size( self, model_info: Any, precision: str, base_model: str, revision: str ) -> Tuple[Optional[float], Optional[str]]: """Get model size in billions of parameters""" try: logger.info(LogFormatter.info(f"Checking model size for {model_info.modelId}")) # Check if model is adapter is_adapter = any(s.rfilename == "adapter_config.json" for s in model_info.siblings if hasattr(s, 'rfilename')) # Try to get size from safetensors first model_size = None if is_adapter and base_model: # For adapters, we need both adapter and base model sizes adapter_meta = await self.get_safetensors_metadata(model_info.id, is_adapter=True, revision=revision) base_meta = await self.get_safetensors_metadata(base_model, revision="main") if adapter_meta and base_meta: adapter_size = sum(adapter_meta.parameter_count.values()) base_size = sum(base_meta.parameter_count.values()) model_size = adapter_size + base_size else: # For regular models, just get the model size meta = await self.get_safetensors_metadata(model_info.id, revision=revision) if meta: model_size = sum(meta.parameter_count.values()) # total params if model_size is None: # If model size could not be determined, return an error return None, "Model size could not be determined" # Adjust size for GPTQ models size_factor = 8 if (precision == "GPTQ" or "gptq" in model_info.id.lower()) else 1 model_size = model_size / 1e9 # Convert to billions, assuming float16 model_size = round(size_factor * model_size, 3) logger.info(LogFormatter.success(f"Model size: {model_size}B parameters")) return model_size, None except Exception as e: logger.error(LogFormatter.error(f"Error while determining model size: {e}")) return None, str(e) async def check_chat_template( self, model_id: str, revision: str ) -> Tuple[bool, Optional[str]]: """Check if model has a valid chat template""" try: logger.info(LogFormatter.info(f"Checking chat template for {model_id}")) try: config_file = await asyncio.to_thread( hf_hub_download, repo_id=model_id, filename="tokenizer_config.json", revision=revision, repo_type="model" ) with open(config_file, 'r') as f: tokenizer_config = json.load(f) if 'chat_template' not in tokenizer_config: error_msg = f"The model {model_id} doesn't have a chat_template in its tokenizer_config.json. Please add a chat_template before submitting or submit without it." logger.error(LogFormatter.error(error_msg)) return False, error_msg logger.info(LogFormatter.success("Valid chat template found")) return True, None except Exception as e: error_msg = f"Error checking chat_template: {str(e)}" logger.error(LogFormatter.error(error_msg)) return False, error_msg except Exception as e: error_msg = "Failed to check chat template" logger.error(LogFormatter.error(error_msg, e)) return False, str(e) async def is_model_on_hub( self, model_name: str, revision: str, test_tokenizer: bool = False, trust_remote_code: bool = False ) -> Tuple[bool, Optional[str], Optional[Any]]: """Check if model exists and is properly configured on the Hub""" try: # First try regular API approach try: config = await asyncio.to_thread( AutoConfig.from_pretrained, model_name, revision=revision, trust_remote_code=trust_remote_code, token=self.token, force_download=True ) # Flag to indicate this is a regular config config.is_lfs = False except Exception as e: logger.info(f"Standard config loading failed, attempting LFS fallback: {str(e)}") # Fallback to direct LFS file access async with aiohttp.ClientSession() as session: config_url = f"https://huggingface.co./{model_name}/raw/{revision}/config.json" headers = build_hf_headers(token=self.token) async with session.get(config_url, headers=headers) as response: if response.status == 200: config_json = await response.json() logger.info("Successfully loaded config.json from LFS") config = AutoConfig.from_dict(config_json) # Flag to indicate this is from LFS and store original JSON config.is_lfs = True config.lfs_json = config_json else: error_msg = f"Failed to load config.json (Status {response.status})" logger.error(error_msg) raise Exception(error_msg) # Test tokenizer if requested if test_tokenizer: try: await asyncio.to_thread( AutoTokenizer.from_pretrained, model_name, revision=revision, trust_remote_code=trust_remote_code, token=self.token ) except ValueError as e: return False, f"The tokenizer is not available in an official Transformers release: {e}", None except Exception: return False, "The tokenizer cannot be loaded. Ensure the tokenizer class is part of a stable Transformers release and correctly configured.", None return True, None, config except ValueError as e: if "trust_remote_code" in str(e): return False, "The model requires `trust_remote_code=True` to launch, and for safety reasons, we don't accept such models automatically.", None return False, str(e), None except Exception as e: if "You are trying to access a gated repo." in str(e): return True, "The model is gated and requires special access permissions.", None return False, f"The model was not found or is misconfigured on the Hub. Error: {e}", None except ValueError as e: if "trust_remote_code" in str(e): return False, "The model requires `trust_remote_code=True` to launch, and for safety reasons, we don't accept such models automatically.", None return False, str(e), None except Exception as e: if "You are trying to access a gated repo." in str(e): return True, "The model is gated and requires special access permissions.", None return False, f"The model was not found or is misconfigured on the Hub. Error: {e}", None except ValueError: return False, "The model requires `trust_remote_code=True` to launch, and for safety reasons, we don't accept such models automatically.", None except Exception as e: if "You are trying to access a gated repo." in str(e): return True, "The model is gated and requires special access permissions.", None return False, f"The model was not found or is misconfigured on the Hub. Error: {e.args[0]}", None async def check_official_provider_status( self, model_id: str, existing_models: Dict[str, list] ) -> Tuple[bool, Optional[str]]: """ Check if model is from official provider and has finished submission. Args: model_id: The model identifier (org/model-name) existing_models: Dictionary of models by status from get_models() Returns: Tuple[bool, Optional[str]]: (is_valid, error_message) """ try: logger.info(LogFormatter.info(f"Checking official provider status for {model_id}")) # Get model organization model_org = model_id.split('/')[0] if '/' in model_id else None if not model_org: return True, None # Load official providers dataset dataset = load_dataset(OFFICIAL_PROVIDERS_REPO) official_providers = dataset["train"][0]["CURATED_SET"] # Check if model org is in official providers is_official = model_org in official_providers if is_official: logger.info(LogFormatter.info(f"Model organization '{model_org}' is an official provider")) # Check for finished submissions if "finished" in existing_models: for model in existing_models["finished"]: if model["name"] == model_id: error_msg = ( f"Model {model_id} is an official provider model " f"with a completed evaluation. " f"To re-evaluate, please open a discussion." ) logger.error(LogFormatter.error("Validation failed", error_msg)) return False, error_msg logger.info(LogFormatter.success("No finished submission found for this official provider model")) else: logger.info(LogFormatter.info(f"Model organization '{model_org}' is not an official provider")) return True, None except Exception as e: error_msg = f"Failed to check official provider status: {str(e)}" logger.error(LogFormatter.error(error_msg)) return False, error_msg