@@ -580,21 +580,30 @@ def check_against_fingerprint(
580580# ============================================================================
581581
582582
583+ @dataclass
584+ class IdentityCheckResult :
585+ """Result of a single identity check."""
586+
587+ field : str
588+ passed : bool
589+ message : str
590+
591+
583592def verify_identity (
584593 identity : Any ,
585594 fingerprints : dict [str , Any ],
586- ) -> list [str ]:
595+ ) -> list [IdentityCheckResult ]:
587596 """Compare recipe identity block against collected runtime fingerprints.
588597
589- Returns a list of warning messages. Empty list means everything matches .
598+ Returns a list of check results (both passes and failures) .
590599
591600 Args:
592601 identity: IdentityConfig from the recipe (has .model and .frameworks)
593602 fingerprints: dict of worker_name -> fingerprint dict
594603 """
595- warnings : list [str ] = []
604+ results : list [IdentityCheckResult ] = []
596605 if not fingerprints :
597- return warnings
606+ return results
598607
599608 # Use the first worker's fingerprint for verification (they all run in the same container)
600609 fp = next (iter (fingerprints .values ()))
@@ -605,73 +614,97 @@ def verify_identity(
605614
606615 if identity .model .repo :
607616 fp_repo = fp_model .get ("hf_repo" ) or fp_model .get ("model_id" )
608- if fp_repo and identity .model .repo != fp_repo :
609- warnings .append (f"Model repo mismatch: recipe says '{ identity .model .repo } ', runtime has '{ fp_repo } '" )
610- elif not fp_repo :
611- warnings .append (
612- f"Model repo '{ identity .model .repo } ' declared in recipe but "
613- f"could not be verified (no HF metadata found at /model)"
617+ if fp_repo and identity .model .repo == fp_repo :
618+ results .append (IdentityCheckResult ("model.repo" , True , f"{ identity .model .repo } " ))
619+ elif fp_repo :
620+ results .append (
621+ IdentityCheckResult (
622+ "model.repo" ,
623+ False ,
624+ f"expected '{ identity .model .repo } ', got '{ fp_repo } '" ,
625+ )
626+ )
627+ else :
628+ results .append (
629+ IdentityCheckResult (
630+ "model.repo" ,
631+ False ,
632+ f"'{ identity .model .repo } ' declared but no HF metadata found at /model" ,
633+ )
614634 )
615635
616636 if identity .model .revision :
617637 fp_rev = fp_model .get ("hf_revision" )
618- if fp_rev and not fp_rev .startswith (identity .model .revision ):
619- warnings .append (
620- f"Model revision mismatch: recipe says '{ identity .model .revision [:12 ]} ', "
621- f"runtime has '{ fp_rev [:12 ]} '"
638+ if fp_rev and fp_rev .startswith (identity .model .revision ):
639+ results .append (IdentityCheckResult ("model.revision" , True , f"{ fp_rev [:12 ]} " ))
640+ elif fp_rev :
641+ results .append (
642+ IdentityCheckResult (
643+ "model.revision" ,
644+ False ,
645+ f"expected '{ identity .model .revision [:12 ]} ', got '{ fp_rev [:12 ]} '" ,
646+ )
622647 )
623- elif not fp_rev :
624- warnings .append (
625- f"Model revision '{ identity .model .revision [:12 ]} ' declared in recipe but "
626- f"could not be verified (no HF revision found at /model)"
648+ else :
649+ results .append (
650+ IdentityCheckResult (
651+ "model.revision" ,
652+ False ,
653+ f"'{ identity .model .revision [:12 ]} ' declared but no HF revision found at /model" ,
654+ )
627655 )
628656
629657 # --- Framework versions ---
630658 fp_frameworks = fp .get ("frameworks" ) or {}
631659 for name , expected_version in (identity .frameworks or {}).items ():
632660 actual_version = fp_frameworks .get (name )
633- if actual_version and actual_version != expected_version :
634- warnings .append (f"Framework mismatch: { name } expected '{ expected_version } ', got '{ actual_version } '" )
635- elif not actual_version :
636- warnings .append (
637- f"Framework '{ name } ' version '{ expected_version } ' declared in recipe but not detected in runtime"
661+ if actual_version and actual_version == expected_version :
662+ results .append (IdentityCheckResult (f"frameworks.{ name } " , True , f"{ actual_version } " ))
663+ elif actual_version :
664+ results .append (
665+ IdentityCheckResult (
666+ f"frameworks.{ name } " ,
667+ False ,
668+ f"expected '{ expected_version } ', got '{ actual_version } '" ,
669+ )
670+ )
671+ else :
672+ results .append (
673+ IdentityCheckResult (
674+ f"frameworks.{ name } " ,
675+ False ,
676+ f"'{ expected_version } ' declared but not detected in runtime" ,
677+ )
638678 )
639679
640- return warnings
641-
680+ return results
642681
643- def format_identity_verification (warnings : list [str ], identity : Any ) -> str :
644- """Format identity verification results as a banner for the sweep log.
645682
646- Args:
647- warnings: list of warning strings from verify_identity
648- identity: IdentityConfig from the recipe
649- """
683+ def format_identity_verification (results : list [IdentityCheckResult ], identity : Any ) -> str :
684+ """Format identity verification results as a banner for the sweep log."""
650685 lines : list [str ] = []
651686 lines .append ("=" * 60 )
652687 lines .append ("Identity Verification" )
653688 lines .append ("=" * 60 )
654689
655- if not warnings :
656- # Show what was checked
657- checks = []
658- if identity .model and identity .model .repo :
659- checks .append (f" model.repo: { identity .model .repo } " )
660- if identity .model and identity .model .revision :
661- checks .append (f" model.revision: { identity .model .revision [:12 ]} " )
662- for name , ver in (identity .frameworks or {}).items ():
663- checks .append (f" { name } : { ver } " )
664- if checks :
665- lines .append ("All checks passed:" )
666- lines .extend (checks )
667- else :
668- lines .append ("No identity fields declared — nothing to verify." )
669- else :
670- lines .append (f"WARNING: { len (warnings )} mismatch(es) detected!" )
671- lines .append ("" )
672- for w in warnings :
673- lines .append (f" ⚠ { w } " )
690+ if not results :
691+ lines .append ("No identity fields declared — nothing to verify." )
692+ lines .append ("=" * 60 )
693+ return "\n " .join (lines )
674694
695+ passes = [r for r in results if r .passed ]
696+ fails = [r for r in results if not r .passed ]
697+
698+ for r in passes :
699+ lines .append (f" OK { r .field } : { r .message } " )
700+ for r in fails :
701+ lines .append (f" !! { r .field } : { r .message } " )
702+
703+ lines .append ("" )
704+ if fails :
705+ lines .append (f"Result: { len (passes )} passed, { len (fails )} FAILED" )
706+ else :
707+ lines .append (f"Result: { len (passes )} passed, all OK" )
675708 lines .append ("=" * 60 )
676709 return "\n " .join (lines )
677710
@@ -766,12 +799,25 @@ def model_identity(model_path):
766799 mp = Path(model_path) if model_path else None
767800 if not mp or not mp.exists():
768801 return None
769- # HuggingFace: check refs/main for commit SHA
802+ # HuggingFace snapshot_download: refs/main has commit SHA
770803 for refs_path in [mp / '.huggingface' / 'refs' / 'main', mp / 'refs' / 'main']:
771804 if refs_path.exists():
772805 info['hf_revision'] = refs_path.read_text().strip()
773806 break
774- # HuggingFace: check .huggingface/download_metadata.json
807+ # HuggingFace hf download --local-dir: .cache/huggingface/download/*.metadata
808+ # Line 1 of each .metadata file is the commit hash
809+ if 'hf_revision' not in info:
810+ cache_dl = mp / '.cache' / 'huggingface' / 'download'
811+ if cache_dl.is_dir():
812+ for meta_file in sorted(cache_dl.glob('*.metadata')):
813+ try:
814+ first_line = meta_file.read_text().splitlines()[0].strip()
815+ if len(first_line) == 40 and all(c in '0123456789abcdef' for c in first_line):
816+ info['hf_revision'] = first_line
817+ break
818+ except Exception:
819+ pass
820+ # HuggingFace: check .huggingface/download_metadata.json (older format)
775821 meta = mp / '.huggingface' / 'download_metadata.json'
776822 if meta.exists():
777823 try:
0 commit comments