@@ -52,6 +52,7 @@ class AffiliationService(BaseService):
5252
5353 MAX_CHUNK_SIZE = 5000
5454 MAX_CONCURRENT_CHUNKS = 3
55+ MAX_FILE_SIZE_BYTES = 1_000_000
5556 FILE_PICKER_PREVIEW_MAX_CHARS = 400
5657 FILE_PICKER_BATCH_SIZE = 20
5758
@@ -74,14 +75,19 @@ class AffiliationService(BaseService):
7475 )
7576
7677 @staticmethod
77- async def read_text_file (file_path : str ) -> str :
78- async with aiofiles .open (file_path , "rb" ) as f :
79- return safe_decode (await f .read ())
80-
81- @staticmethod
82- def compute_file_hash (content : str ) -> str :
83- """SHA-256 hex digest of UTF-8 file content (not a Git blob SHA)."""
84- return hashlib .sha256 (content .encode ("utf-8" )).hexdigest ()
78+ async def compute_file_hash_from_path (
79+ file_path : str , * , retain_content : bool = False
80+ ) -> tuple [bytes | None , str ]:
81+ """SHA-256 of raw file bytes. Set retain_content to decode and parse under the size limit."""
82+ hasher = hashlib .sha256 ()
83+ chunks : list [bytes ] = []
84+ async with aiofiles .open (file_path , "rb" ) as affiliation_file :
85+ while file_chunk := await affiliation_file .read (1024 * 1024 ):
86+ hasher .update (file_chunk )
87+ if retain_content :
88+ chunks .append (file_chunk )
89+ file_bytes = b"" .join (chunks ) if retain_content else None
90+ return file_bytes , hasher .hexdigest ()
8591
8692 @classmethod
8793 def is_text_file_path (cls , relative_path : str ) -> bool :
@@ -955,15 +961,42 @@ async def process_affiliations(
955961 raise AffiliationFileNotFoundError (ai_cost = ai_cost )
956962
957963 file_path_on_disk = os .path .join (batch_info .repo_path , latest_file_path )
958- content = await self .read_text_file (file_path_on_disk )
959- file_hash = self .compute_file_hash (content )
960- latest_file_hash = file_hash
961-
962- affiliations , parse_cost = await self .resolve_snapshot (
963- registry ,
964- content ,
965- file_hash ,
966- )
964+ file_size_bytes = await aiofiles .os .path .getsize (file_path_on_disk )
965+ # Too big for llm — mark unusable and move on.
966+ if file_size_bytes > self .MAX_FILE_SIZE_BYTES :
967+ # Steady state: already gave up on this file; getsize is enough.
968+ if (
969+ registry
970+ and registry .status == AffiliationRegistryStatus .UNUSABLE .value
971+ and registry .file_path == latest_file_path
972+ and registry .file_hash
973+ ):
974+ file_hash = registry .file_hash
975+ latest_file_hash = file_hash
976+ affiliations = []
977+ parse_cost = 0.0
978+ else :
979+ # First hit: hash it so the registry can mark it as unusable.
980+ _ , file_hash = await self .compute_file_hash_from_path (file_path_on_disk )
981+ latest_file_hash = file_hash
982+ raise AffiliationAnalysisError (
983+ retain_file_hash = True ,
984+ error_message = (
985+ f"Affiliation file { latest_file_path !r} is too large for LLM parsing "
986+ f"({ file_size_bytes } bytes > { self .MAX_FILE_SIZE_BYTES } bytes)"
987+ ),
988+ )
989+ else :
990+ file_bytes , file_hash = await self .compute_file_hash_from_path (
991+ file_path_on_disk , retain_content = True
992+ )
993+ content = safe_decode (file_bytes )
994+ latest_file_hash = file_hash
995+ affiliations , parse_cost = await self .resolve_snapshot (
996+ registry ,
997+ content ,
998+ file_hash ,
999+ )
9671000 ai_cost += parse_cost
9681001
9691002 if repository .parent_repo :
0 commit comments