@@ -2187,14 +2187,59 @@ def _models_info(self, args):
21872187 return 1
21882188
21892189 from effgen .models import _catalog , _refresh
2190+ from effgen .models .model_loader import ModelLoader
2191+
2192+ # An engine-prefixed id (e.g. "transformers:Qwen/Qwen2.5-1.5B-Instruct")
2193+ # names a local engine + a bare repo id. Strip the prefix so the id
2194+ # matches the local cache and the catalog, and remember the engine so we
2195+ # can lead with the local view.
2196+ lookup_name = args .name
2197+ requested_engine = None
2198+ if ":" in lookup_name :
2199+ _prefix , _rest = lookup_name .split (":" , 1 )
2200+ if _prefix in ModelLoader ._LOCAL_ENGINE_PREFIXES and _rest :
2201+ requested_engine = _prefix
2202+ lookup_name = _rest
21902203
21912204 # Is this id sitting in the local HF cache? If so we can describe it as
21922205 # locally-runnable even when the cloud catalog has no (or a different) row.
21932206 local_entry = next (
2194- (m for m in self ._local_cached_models () if m ["id" ] == args . name ), None
2207+ (m for m in self ._local_cached_models () if m ["id" ] == lookup_name ), None
21952208 )
21962209
2197- rec = _catalog .lookup (args .name )
2210+ # An explicit local-engine request is answered from the local cache: show
2211+ # the cached copy, or report a cache miss naming what is cached (rather
2212+ # than cloud catalog suggestions the engine can't run).
2213+ if requested_engine is not None :
2214+ if local_entry is not None :
2215+ local_payload = self ._local_model_payload (local_entry )
2216+ if getattr (args , "output_json" , False ):
2217+ print (json .dumps ({"id" : lookup_name , "provider" : None ,
2218+ "engine" : requested_engine ,
2219+ "local" : local_payload }, indent = 2 ))
2220+ return 0
2221+ self .print_header (f"Model: { lookup_name } ({ requested_engine } )" )
2222+ self ._render_local_model_info (local_payload )
2223+ return 0
2224+ cached = [m ["id" ] for m in self ._local_cached_models ()]
2225+ if getattr (args , "output_json" , False ):
2226+ print (json .dumps ({"id" : lookup_name , "provider" : None ,
2227+ "engine" : requested_engine , "local" : None ,
2228+ "cached_models" : cached }, indent = 2 ))
2229+ return 1
2230+ self .print_error (
2231+ f"Model '{ lookup_name } ' is not in the local cache, so the "
2232+ f"'{ requested_engine } ' engine can't run it yet."
2233+ )
2234+ if cached :
2235+ self .print ("Locally cached models:" )
2236+ for cid in cached :
2237+ self .print (f" { cid } " )
2238+ self .print (f"\n Download it first: effgen run -m { lookup_name } "
2239+ f"--engine { requested_engine } \" ...\" (with network access)." )
2240+ return 1
2241+
2242+ rec = _catalog .lookup (lookup_name )
21982243 if rec is None :
21992244 if local_entry is not None :
22002245 # Downloaded locally but not in the cloud catalog: describe the local
@@ -2318,6 +2363,9 @@ def _models_unload(self, args):
23182363
23192364 def _models_status (self , args ):
23202365 """Show loaded models and GPU memory status."""
2366+ if getattr (args , "output_json" , False ):
2367+ return self ._models_status_json ()
2368+
23212369 self .print_header ("Model & GPU Status" )
23222370
23232371 # GPU memory info — physical (driver) view across all processes, so this
@@ -2385,6 +2433,47 @@ def _models_status(self, args):
23852433 registered = list_registered_models ()
23862434 self .print (f"\n Capability profiles registered: { len (registered )} " )
23872435
2436+ def _models_status_json (self ) -> int :
2437+ """Emit the GPU table + loaded models as JSON for ops/edge tooling."""
2438+ gib = 1024 ** 3
2439+ gpu_list : list [dict ] = []
2440+ cuda_available = True
2441+ try :
2442+ from effgen .gpu .cuda_compat import per_gpu_status
2443+ for g in per_gpu_status ():
2444+ gpu_list .append ({
2445+ "index" : g .index ,
2446+ "name" : g .name ,
2447+ "total_gb" : round (g .total_bytes / gib , 3 ),
2448+ "used_gb" : round (g .used_bytes / gib , 3 ),
2449+ "free_gb" : round (g .free_bytes / gib , 3 ),
2450+ "utilization_pct" : g .utilization_pct ,
2451+ })
2452+ if not gpu_list :
2453+ try :
2454+ import torch
2455+ cuda_available = torch .cuda .is_available ()
2456+ except ImportError :
2457+ cuda_available = False
2458+ except ImportError :
2459+ cuda_available = False
2460+
2461+ from effgen .models .capabilities import list_registered_models
2462+ from effgen .models .model_loader import ModelLoader
2463+ loaded = ModelLoader ().get_loaded_models ()
2464+ loaded_list = [
2465+ {"name" : name , "loaded" : bool (model .is_loaded ())}
2466+ for name , model in loaded .items ()
2467+ ]
2468+ payload = {
2469+ "cuda_available" : cuda_available ,
2470+ "gpus" : gpu_list ,
2471+ "loaded_models" : loaded_list ,
2472+ "capability_profiles" : len (list_registered_models ()),
2473+ }
2474+ print (json .dumps (payload , indent = 2 ))
2475+ return 0
2476+
23882477 def _models_refresh (self , args ):
23892478 """Refresh the bundled model catalog from each provider's live API.
23902479
@@ -2935,7 +3024,8 @@ def create_parser():
29353024 models_unload = models_subparsers .add_parser ('unload' , help = 'Unload a model from memory' )
29363025 models_unload .add_argument ('name' , help = 'Model name' )
29373026
2938- models_subparsers .add_parser ('status' , help = 'Show loaded models and GPU memory status' )
3027+ models_status = models_subparsers .add_parser ('status' , help = 'Show loaded models and GPU memory status' )
3028+ models_status .add_argument ('--json' , dest = 'output_json' , action = 'store_true' , help = 'Output as JSON' )
29393029
29403030 models_refresh = models_subparsers .add_parser (
29413031 'refresh' , help = "Refresh the model catalog from each provider's live API" )
@@ -4060,7 +4150,7 @@ def _read_done_indices(output_path: Path) -> dict:
40604150
40614151def _handle_batch_command (args , cli ) -> int :
40624152 """Handle the 'batch' CLI subcommand."""
4063- from effgen .core .batch import BatchConfig , BatchRunner
4153+ from effgen .core .batch import _QUERY_ALIASES , BatchConfig , BatchRunner
40644154
40654155 input_path = args .input
40664156 output_path = getattr (args , 'output' , None )
@@ -4149,14 +4239,19 @@ def _json_error(exc: Exception) -> int:
41494239 # naming the file and line number (not the parser's byte offset);
41504240 # --strict turns the first bad line into a hard failure instead.
41514241 skipped : list [int ] = []
4242+ empty_rows : list [tuple [int , list [str ]]] = []
41524243
41534244 def _on_skip (lineno : int , msg : str ) -> None :
41544245 skipped .append (lineno )
41554246 cli .print (f"Skipping malformed input at { input_path } :{ lineno } : { msg } " )
41564247
4248+ def _on_empty (lineno : int , keys : list [str ]) -> None :
4249+ empty_rows .append ((lineno , keys ))
4250+
41574251 try :
41584252 queries = runner ._read_queries (
4159- Path (input_path ), query_field , strict = strict , on_skip = _on_skip ,
4253+ Path (input_path ), query_field , strict = strict ,
4254+ on_skip = _on_skip , on_empty = _on_empty ,
41604255 )
41614256 except Exception as e : # noqa: BLE001 - one clear message, no traceback
41624257 cli .print_error (f"Could not read { input_path } : { e } " )
@@ -4167,6 +4262,22 @@ def _on_skip(lineno: int, msg: str) -> None:
41674262 f"{ len (queries )} queries loaded."
41684263 )
41694264
4265+ # A row with no recognized query text (neither --query-field nor the
4266+ # aliases query/input/prompt/question/text) can't run. Name the fields it
4267+ # did carry and how to point at the right one, rather than letting each
4268+ # empty row fail with a generic empty-task message.
4269+ if empty_rows :
4270+ lineno , keys = empty_rows [0 ]
4271+ fields = ", " .join (keys ) if keys else "none"
4272+ more = f" (and { len (empty_rows ) - 1 } more)" if len (empty_rows ) > 1 else ""
4273+ msg = (
4274+ f"Row { lineno } { more } has no query text. Fields present: { fields } . "
4275+ f"Set the query column with --query-field NAME, or key rows on one "
4276+ f"of: { ', ' .join (_QUERY_ALIASES )} ."
4277+ )
4278+ cli .print_error (msg )
4279+ return _json_error (ValueError (msg ))
4280+
41704281 # --resume: skip input rows already present in the JSONL output.
41714282 done_rows : dict [int , dict ] = {}
41724283 if resume :
0 commit comments