fffiloni commited on
Commit
704b329
·
verified ·
1 Parent(s): 095e00d

Upload 9 files

Browse files
Files changed (3) hide show
  1. src/bucket.py +87 -0
  2. src/jobs.py +3 -0
  3. src/worker_payload.py +489 -8
src/bucket.py CHANGED
@@ -335,6 +335,87 @@ def _list_run_files(
335
  return files
336
 
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  def summarize_run_bundle(run_id: str, bundle: dict[str, Any], *, bucket_source: str) -> dict[str, Any]:
339
  summary_file = bundle.get("summary_file") or {}
340
  state = bundle.get("state") or {}
@@ -361,6 +442,7 @@ def summarize_run_bundle(run_id: str, bundle: dict[str, Any], *, bucket_source:
361
  "smoke_test_passed": bool(smoke.get("ok") or smoke.get("status") == "success"),
362
  "latency_seconds": smoke.get("latency_seconds"),
363
  "expected_output_type": smoke.get("expected_output_type") or state.get("expected_output_type") or launch.get("expected_output_type") or "",
 
364
  "artifacts_url": f"https://huggingface.co/buckets/{bucket_source}/tree/runs/{run_id}",
365
  }
366
 
@@ -385,9 +467,12 @@ def read_run_bundle(run_id: str, *, bucket_source: str, token: str | None = None
385
  "hardware_attempts": _safe_read_json(f"{paths.root}/hardware_attempts.json", token=token),
386
  "technical_blockers": _safe_read_json(f"{paths.root}/TECHNICAL_BLOCKERS.json", token=token),
387
  "model_analysis": _safe_read_json(f"{paths.root}/model_analysis.json", token=token),
 
388
  "space_runtime": _safe_read_json(f"{paths.root}/space_runtime.json", token=token),
389
  "files": _list_run_files(run_id, bucket_source=bucket_source, token=token) if include_heavy else [],
390
  }
 
 
391
  bundle["summary"] = summarize_run_bundle(run_id, bundle, bucket_source=bucket_source)
392
  return bundle
393
 
@@ -462,6 +547,7 @@ def list_recent_runs(
462
  gate = read_json(f"{root}/{run_id}/inference_gate.json", token=token) or {}
463
  smoke = read_json(f"{root}/{run_id}/tests/generation_smoke.json", token=token) or read_json(f"{root}/{run_id}/generation_smoke.json", token=token) or {}
464
  hardware = read_json(f"{root}/{run_id}/hardware_strategy.json", token=token) or {}
 
465
  partial_bundle = {
466
  "summary_file": summary_file,
467
  "launch": launch,
@@ -469,6 +555,7 @@ def list_recent_runs(
469
  "inference_gate": gate,
470
  "generation_smoke": smoke,
471
  "hardware_strategy": hardware,
 
472
  }
473
  item = summarize_run_bundle(run_id, partial_bundle, bucket_source=bucket_source)
474
  item["bucket_source"] = bucket_source
 
335
  return files
336
 
337
 
338
+ def _normalize_model_name(value: str | None) -> str:
339
+ return re.sub(r"[^a-z0-9]+", "", (value or "").lower())
340
+
341
+
342
+ def _extract_pi_models_from_text(text: str) -> list[str]:
343
+ if not text:
344
+ return []
345
+ patterns = [
346
+ r"\bQwen/[A-Za-z0-9_.-]+",
347
+ r"\bQwen(?:2(?:\.5)?|3)?[-_/A-Za-z0-9.]*Coder[-_/A-Za-z0-9.]*",
348
+ r"\bKimi[-_/A-Za-z0-9.]+",
349
+ r"\bMoonshotAI/[A-Za-z0-9_.-]+",
350
+ r"\bClaude[-_/A-Za-z0-9.]+",
351
+ r"\bGPT[-_/A-Za-z0-9.]+",
352
+ r"\bDeepSeek[-_/A-Za-z0-9.]+",
353
+ r"\b[Mm]odel(?:\\s+used|\\s+selected|\\s*:)?\\s*[:=]?\\s*([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)",
354
+ ]
355
+ found: list[str] = []
356
+ for pattern in patterns:
357
+ for match in re.finditer(pattern, text, flags=re.IGNORECASE):
358
+ value = match.group(1) if match.groups() else match.group(0)
359
+ value = value.strip().strip('",` ')
360
+ if value and value not in found:
361
+ found.append(value)
362
+ return found[:12]
363
+
364
+
365
+ def _trace_based_pi_model_resolution(run_id: str, bundle: dict[str, Any], *, bucket_source: str, token: str | None = None) -> dict[str, Any]:
366
+ state = bundle.get("state") or {}
367
+ launch = bundle.get("launch") or {}
368
+ requested = (
369
+ state.get("pi_model")
370
+ or launch.get("pi_model")
371
+ or launch.get("PI_MODEL")
372
+ or state.get("pi_model_resolution", {}).get("requested_model")
373
+ or ""
374
+ )
375
+ configured = state.get("pi_model_resolution", {}).get("configured_model") or requested
376
+ root = RunPaths(run_id, bucket_source=bucket_source).root
377
+ trace_texts: list[str] = []
378
+ try:
379
+ fs = _fs(token)
380
+ for pattern in (
381
+ f"{root}/traces/redacted/**/*.jsonl",
382
+ f"{root}/traces/redacted/*.jsonl",
383
+ f"{root}/traces/raw/**/*.jsonl",
384
+ f"{root}/logs/pi_output.txt",
385
+ f"{root}/logs/pi_live_output.txt",
386
+ ):
387
+ for path in fs.glob(pattern):
388
+ try:
389
+ trace_texts.append(_safe_read_text(str(path), token=token)[:250000])
390
+ except Exception:
391
+ pass
392
+ except Exception:
393
+ pass
394
+ observed = _extract_pi_models_from_text("\n".join(trace_texts))
395
+ n_requested = _normalize_model_name(requested)
396
+ n_configured = _normalize_model_name(configured)
397
+ effective = ""
398
+ for raw in observed:
399
+ n_raw = _normalize_model_name(raw)
400
+ if n_raw and n_raw not in {n_requested, n_configured}:
401
+ effective = raw
402
+ break
403
+ if not effective and observed:
404
+ effective = observed[0]
405
+ mismatch = bool(effective and _normalize_model_name(effective) not in {n_requested, n_configured})
406
+ if not requested and not observed:
407
+ return {}
408
+ return {
409
+ "requested_model": requested,
410
+ "configured_model": configured or requested,
411
+ "observed_models": observed,
412
+ "effective_model": effective or configured or requested,
413
+ "provider": "huggingface",
414
+ "mismatch": mismatch,
415
+ "source": "published_pi_traces",
416
+ }
417
+
418
+
419
  def summarize_run_bundle(run_id: str, bundle: dict[str, Any], *, bucket_source: str) -> dict[str, Any]:
420
  summary_file = bundle.get("summary_file") or {}
421
  state = bundle.get("state") or {}
 
442
  "smoke_test_passed": bool(smoke.get("ok") or smoke.get("status") == "success"),
443
  "latency_seconds": smoke.get("latency_seconds"),
444
  "expected_output_type": smoke.get("expected_output_type") or state.get("expected_output_type") or launch.get("expected_output_type") or "",
445
+ "pi_model_resolution": bundle.get("pi_model_resolution") or state.get("pi_model_resolution") or {},
446
  "artifacts_url": f"https://huggingface.co/buckets/{bucket_source}/tree/runs/{run_id}",
447
  }
448
 
 
467
  "hardware_attempts": _safe_read_json(f"{paths.root}/hardware_attempts.json", token=token),
468
  "technical_blockers": _safe_read_json(f"{paths.root}/TECHNICAL_BLOCKERS.json", token=token),
469
  "model_analysis": _safe_read_json(f"{paths.root}/model_analysis.json", token=token),
470
+ "pi_model_resolution": _safe_read_json(f"{paths.root}/pi_model_resolution.json", token=token) or (read_json(paths.state, token=token) or {}).get("pi_model_resolution") or {},
471
  "space_runtime": _safe_read_json(f"{paths.root}/space_runtime.json", token=token),
472
  "files": _list_run_files(run_id, bucket_source=bucket_source, token=token) if include_heavy else [],
473
  }
474
+ if not bundle.get("pi_model_resolution"):
475
+ bundle["pi_model_resolution"] = _trace_based_pi_model_resolution(run_id, bundle, bucket_source=bucket_source, token=token)
476
  bundle["summary"] = summarize_run_bundle(run_id, bundle, bucket_source=bucket_source)
477
  return bundle
478
 
 
547
  gate = read_json(f"{root}/{run_id}/inference_gate.json", token=token) or {}
548
  smoke = read_json(f"{root}/{run_id}/tests/generation_smoke.json", token=token) or read_json(f"{root}/{run_id}/generation_smoke.json", token=token) or {}
549
  hardware = read_json(f"{root}/{run_id}/hardware_strategy.json", token=token) or {}
550
+ pi_model_resolution = read_json(f"{root}/{run_id}/pi_model_resolution.json", token=token) or state.get("pi_model_resolution") or {}
551
  partial_bundle = {
552
  "summary_file": summary_file,
553
  "launch": launch,
 
555
  "inference_gate": gate,
556
  "generation_smoke": smoke,
557
  "hardware_strategy": hardware,
558
+ "pi_model_resolution": pi_model_resolution,
559
  }
560
  item = summarize_run_bundle(run_id, partial_bundle, bucket_source=bucket_source)
561
  item["bucket_source"] = bucket_source
src/jobs.py CHANGED
@@ -119,6 +119,7 @@ def launch_universal_model_card_job(
119
  fallback_space_hardware: str | None = None,
120
  allow_fixed_gpu_fallback: bool = True,
121
  implementation_mode: str | None = None,
 
122
  run_id: str | None = None,
123
  bucket_name: str | None = None,
124
  ) -> dict[str, Any]:
@@ -144,6 +145,7 @@ def launch_universal_model_card_job(
144
  env["FALLBACK_SPACE_HARDWARE"] = (fallback_space_hardware or "l40sx1").strip()
145
  env["ALLOW_FIXED_GPU_FALLBACK"] = "true" if allow_fixed_gpu_fallback else "false"
146
  env["IMPLEMENTATION_MODE"] = (implementation_mode or "full-inference-gated").strip()
 
147
  job = _launch_job(token=token, env=env, bucket_source=bucket_source, timeout="60m")
148
  return _job_result(
149
  job,
@@ -159,6 +161,7 @@ def launch_universal_model_card_job(
159
  "fallback_space_hardware": env["FALLBACK_SPACE_HARDWARE"],
160
  "allow_fixed_gpu_fallback": allow_fixed_gpu_fallback,
161
  "implementation_mode": env["IMPLEMENTATION_MODE"],
 
162
  },
163
  )
164
 
 
119
  fallback_space_hardware: str | None = None,
120
  allow_fixed_gpu_fallback: bool = True,
121
  implementation_mode: str | None = None,
122
+ expected_output_type: str | None = None,
123
  run_id: str | None = None,
124
  bucket_name: str | None = None,
125
  ) -> dict[str, Any]:
 
145
  env["FALLBACK_SPACE_HARDWARE"] = (fallback_space_hardware or "l40sx1").strip()
146
  env["ALLOW_FIXED_GPU_FALLBACK"] = "true" if allow_fixed_gpu_fallback else "false"
147
  env["IMPLEMENTATION_MODE"] = (implementation_mode or "full-inference-gated").strip()
148
+ env["EXPECTED_OUTPUT_TYPE"] = (expected_output_type or "any").strip()
149
  job = _launch_job(token=token, env=env, bucket_source=bucket_source, timeout="60m")
150
  return _job_result(
151
  job,
 
161
  "fallback_space_hardware": env["FALLBACK_SPACE_HARDWARE"],
162
  "allow_fixed_gpu_fallback": allow_fixed_gpu_fallback,
163
  "implementation_mode": env["IMPLEMENTATION_MODE"],
164
+ "expected_output_type": env["EXPECTED_OUTPUT_TYPE"],
165
  },
166
  )
167
 
src/worker_payload.py CHANGED
@@ -279,6 +279,105 @@ def configure_pi(events_path: Path, model: str):
279
  append_event(events_path, "pi_config", "success", "Configured Pi", {"model": model})
280
 
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  def collect_pi_traces(run_dir: Path, events_path: Path):
283
  count = sync_pi_traces(run_dir, emit_event=False)
284
  append_event(events_path, "traces", "success", "Collected Pi traces", {"count": count})
@@ -329,6 +428,221 @@ def api_names_from_schema(schema) -> list[str]:
329
  return list(dict.fromkeys(names))
330
 
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  def space_subdomain_url(target_space_id: str) -> str:
333
  owner, name = target_space_id.split("/", 1)
334
  # This matches the common Spaces app URL pattern. Keep conservative: our
@@ -846,7 +1160,7 @@ def load_json_if_exists(path: Path) -> dict:
846
  return {"parse_error": str(exc), "raw_tail": path.read_text(encoding="utf-8", errors="replace")[-2000:]}
847
 
848
 
849
- def infer_generation_gate(workspace: Path, implementation_mode: str, validation: dict, run_dir: Path, events_path: Path) -> dict:
850
  """Classify the run separately from process success.
851
 
852
  /health passing means the Space boots. It does not mean the generated Space
@@ -874,21 +1188,27 @@ def infer_generation_gate(workspace: Path, implementation_mode: str, validation:
874
  "out of scope",
875
  ]
876
  blocker_detected = bool(blockers) or any(m in combined for m in blocked_markers)
 
 
877
  implementation_signals = {
878
  "has_spaces_gpu": "@spaces.GPU" in app_text,
879
  "has_torch": "torch" in req_text or "import torch" in app_text,
880
  "has_diffusers": "diffusers" in req_text or "diffusers" in app_text,
881
  "has_video_output_hint": any(x in app_text.lower() for x in ["gr.video", "video", ".mp4", "ffmpeg"]),
882
  "health_passed": validation.get("method") in {"http_health", "gradio"},
 
 
883
  }
884
 
885
  if blocker_detected:
886
  status = "technical_blocker"
887
  message = "Space boots, but full model inference was not implemented. See TECHNICAL_BLOCKERS.json / PI_SUMMARY.md."
 
 
 
888
  elif implementation_mode in {"full-inference-gated", "full-inference-attempt"}:
889
- # Without a video smoke test, do not claim real inference success.
890
  status = "full_inference_candidate_health_passed"
891
- message = "Space boots and contains inference signals, but no generation smoke test has validated a real video output."
892
  else:
893
  status = "health_only"
894
  message = "Safe scaffold health validation passed. Full inference was not requested."
@@ -921,6 +1241,14 @@ def infer_generation_gate(workspace: Path, implementation_mode: str, validation:
921
  "blocker_detected": blocker_detected,
922
  "implementation_signals": implementation_signals,
923
  "validation_method": validation.get("method"),
 
 
 
 
 
 
 
 
924
  "blockers": blockers,
925
  }
926
  write_json(run_dir / "inference_gate.json", gate)
@@ -940,12 +1268,14 @@ def main():
940
  fallback_hardware = os.environ.get("FALLBACK_SPACE_HARDWARE", "l40sx1")
941
  allow_fixed_gpu_fallback = os.environ.get("ALLOW_FIXED_GPU_FALLBACK", "true").lower() in {"1", "true", "yes", "on"}
942
  implementation_mode = os.environ.get("IMPLEMENTATION_MODE", "full-inference-attempt")
 
943
  token = os.environ.get("HF_TOKEN")
944
 
945
  run_dir = output_root / "runs" / run_id
946
  events_path = run_dir / "events.jsonl"
947
  state_path = run_dir / "state.json"
948
  workspace = Path("/tmp/universal_workspace")
 
949
 
950
  append_event(events_path, "bootstrap", "started", "Universal model-card builder worker started", {"model_id": model_id, "target_space_id": target_space_id})
951
  write_json(state_path, {"run_id": run_id, "kind": "universal_model_card_builder", "status": "running", "message": "Attempting Universal model-card builderd Space creation", "model_id": model_id, "target_space": target_space_id, "created_by": hf_username, "bucket_source": bucket_source, "created_at": now(), "updated_at": now()})
@@ -990,6 +1320,11 @@ def main():
990
  collect_pi_traces(run_dir, events_path)
991
  fail(run_dir, events_path, "Pi failed before Space upload", {"returncode": code, "output_tail": pi_out[-4000:]})
992
  append_event(events_path, "pi_run", "success", "Pi completed universal model-card workspace pass", {"output_tail": pi_out[-2000:]})
 
 
 
 
 
993
  if not (workspace / "PI_SUMMARY.md").exists():
994
  (workspace / "PI_SUMMARY.md").write_text("# Pi Summary\n\nPi did not create a PI_SUMMARY.md. See logs/pi_output.txt.\n", encoding="utf-8")
995
 
@@ -1035,7 +1370,24 @@ def main():
1035
  raise
1036
  upload_workspace(api, workspace, target_space_id, token, run_dir, events_path)
1037
  validation = validate_live_api(api, target_space_id, token, run_dir, events_path, timeout_s=1200)
1038
- inference_gate = infer_generation_gate(workspace, implementation_mode, validation, run_dir, events_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1039
 
1040
  # If the generated app looks like real GPU inference but automatic
1041
  # hardware requests failed, classify the run honestly as needing manual
@@ -1067,7 +1419,9 @@ def main():
1067
  "target_space_url": f"https://huggingface.co/spaces/{target_space_id}",
1068
  "selected_hardware": selected_hardware,
1069
  "hardware_attempts": hardware_attempts,
 
1070
  "validation": validation,
 
1071
  "inference_gate": inference_gate,
1072
  "updated_at": now(),
1073
  "created_by": hf_username,
@@ -1086,6 +1440,12 @@ Target Space: https://huggingface.co/spaces/{target_space_id}
1086
 
1087
  Model: `{model_id}`
1088
 
 
 
 
 
 
 
1089
  ## Hardware
1090
 
1091
  Selected/requested hardware: `{selected_hardware}`
@@ -1104,6 +1464,12 @@ The wrapper validated the live Space using HTTP `/health` first, with Gradio Cli
1104
  {json.dumps(validation, indent=2, ensure_ascii=False)}
1105
  ```
1106
 
 
 
 
 
 
 
1107
  ## Full-inference gate
1108
 
1109
  ```json
@@ -1238,6 +1604,90 @@ def api_names_from_schema(schema) -> list[str]:
1238
  return names
1239
 
1240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1241
  def runtime_to_dict(runtime) -> dict:
1242
  payload = {}
1243
  for attr in ["stage", "hardware", "requested_hardware", "sleep_time", "storage", "gc_timeout"]:
@@ -1422,7 +1872,7 @@ def copy_result_artifacts(result, run_dir: Path):
1422
 
1423
 
1424
  def smoke_generate(target_space_id: str, token: str, run_dir: Path, events_path: Path):
1425
- api_name = (os.environ.get("API_NAME") or "/generate").strip()
1426
  expected_output_type = (os.environ.get("EXPECTED_OUTPUT_TYPE") or "any").strip()
1427
  test_args = parse_json_env("TEST_ARGS_JSON", ["a cinematic robot cat astronaut, detailed, studio lighting"])
1428
  test_kwargs = parse_json_env("TEST_KWARGS_JSON", {})
@@ -1434,9 +1884,33 @@ def smoke_generate(target_space_id: str, token: str, run_dir: Path, events_path:
1434
  client = make_gradio_client(target_space_id, token)
1435
  schema = client.view_api(return_format="dict")
1436
  discovered = api_names_from_schema(schema)
1437
- write_json(run_dir / "tests" / "api_schema.json", {"schema": schema, "api_names": discovered})
 
 
 
 
 
 
 
 
 
1438
  started = time.time()
1439
- result = client.predict(*test_args, api_name=api_name, **test_kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1440
  latency = time.time() - started
1441
  ok, info = result_contains_expected_output(result, expected_output_type)
1442
  copied = copy_result_artifacts(result, run_dir)
@@ -1446,12 +1920,19 @@ def smoke_generate(target_space_id: str, token: str, run_dir: Path, events_path:
1446
  "api_name": api_name,
1447
  "discovered_api_names": discovered,
1448
  "test_args": test_args,
1449
- "test_kwargs": test_kwargs,
 
 
 
1450
  "expected_output_type": expected_output_type,
1451
  "latency_seconds": round(latency, 3),
 
1452
  "result_info": info,
1453
  "copied_artifacts": copied,
1454
  "recommended_zero_gpu_duration_seconds": int(max(30, min(300, latency * 2 + 15))),
 
 
 
1455
  "validated_at": now(),
1456
  }
1457
  write_json(run_dir / "tests" / "generation_smoke.json", payload)
 
279
  append_event(events_path, "pi_config", "success", "Configured Pi", {"model": model})
280
 
281
 
282
+ def normalize_pi_model_name(value: str | None) -> str:
283
+ return re.sub(r"[^a-z0-9]+", "", (value or "").lower())
284
+
285
+
286
+ def extract_pi_models_from_text(text: str) -> list[str]:
287
+ """Best-effort extraction of assistant/model names from Pi stdout/session traces."""
288
+ if not text:
289
+ return []
290
+ patterns = [
291
+ r"\bQwen/[A-Za-z0-9_.-]+",
292
+ r"\bQwen(?:2(?:\.5)?|3)?[-_/A-Za-z0-9.]*Coder[-_/A-Za-z0-9.]*",
293
+ r"\bKimi[-_/A-Za-z0-9.]+",
294
+ r"\bMoonshotAI/[A-Za-z0-9_.-]+",
295
+ r"\bClaude[-_/A-Za-z0-9.]+",
296
+ r"\bGPT[-_/A-Za-z0-9.]+",
297
+ r"\bDeepSeek[-_/A-Za-z0-9.]+",
298
+ r"\b[Mm]odel(?:\\s+used|\\s+selected|\\s*:)?\\s*[:=]?\\s*([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)",
299
+ ]
300
+ found: list[str] = []
301
+ for pattern in patterns:
302
+ for match in re.finditer(pattern, text, flags=re.IGNORECASE):
303
+ value = match.group(1) if match.groups() else match.group(0)
304
+ value = value.strip().strip('",` ')
305
+ if value and value not in found:
306
+ found.append(value)
307
+ return found[:12]
308
+
309
+
310
+ def detect_pi_model_resolution(requested_model: str, run_dir: Path, pi_output: str) -> dict:
311
+ settings_path = Path.home() / ".pi" / "agent" / "settings.json"
312
+ configured_model = requested_model
313
+ try:
314
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
315
+ configured_model = settings.get("model") or requested_model
316
+ except Exception:
317
+ settings = {"model": requested_model, "read_error": "could_not_read_settings"}
318
+
319
+ corpus = [pi_output or ""]
320
+ for trace in (run_dir / "traces" / "redacted").rglob("*.jsonl") if (run_dir / "traces" / "redacted").exists() else []:
321
+ try:
322
+ corpus.append(trace.read_text(encoding="utf-8", errors="ignore")[:200000])
323
+ except Exception:
324
+ pass
325
+ observed = extract_pi_models_from_text("\n".join(corpus))
326
+ normalized_requested = normalize_pi_model_name(requested_model)
327
+ normalized_configured = normalize_pi_model_name(configured_model)
328
+ normalized_observed = [normalize_pi_model_name(x) for x in observed]
329
+ effective_model = ""
330
+ for raw, normalized in zip(observed, normalized_observed):
331
+ if normalized and normalized != normalized_requested and normalized != normalized_configured:
332
+ effective_model = raw
333
+ break
334
+ if not effective_model and observed:
335
+ effective_model = observed[0]
336
+ mismatch = bool(effective_model and normalize_pi_model_name(effective_model) not in {normalized_requested, normalized_configured})
337
+ payload = {
338
+ "requested_model": requested_model,
339
+ "configured_model": configured_model,
340
+ "observed_models": observed,
341
+ "effective_model": effective_model or configured_model,
342
+ "provider": "huggingface",
343
+ "mismatch": mismatch,
344
+ "source": "published_pi_traces_after_sync",
345
+ "settings": settings,
346
+ }
347
+ write_json(run_dir / "pi_model_resolution.json", payload)
348
+ return payload
349
+
350
+
351
+ def emit_pi_model_resolution(events_path: Path, resolution: dict):
352
+ if resolution.get("mismatch"):
353
+ append_event(
354
+ events_path,
355
+ "pi_model_resolution",
356
+ "warning",
357
+ "Pi assistant model appears to differ from the requested model",
358
+ {
359
+ "requested_model": resolution.get("requested_model"),
360
+ "configured_model": resolution.get("configured_model"),
361
+ "effective_model": resolution.get("effective_model"),
362
+ "observed_models": resolution.get("observed_models", [])[:8],
363
+ "provider": resolution.get("provider"),
364
+ },
365
+ )
366
+ else:
367
+ append_event(
368
+ events_path,
369
+ "pi_model_resolution",
370
+ "success",
371
+ "Pi assistant model matched the requested configuration",
372
+ {
373
+ "requested_model": resolution.get("requested_model"),
374
+ "configured_model": resolution.get("configured_model"),
375
+ "effective_model": resolution.get("effective_model"),
376
+ "provider": resolution.get("provider"),
377
+ },
378
+ )
379
+
380
+
381
  def collect_pi_traces(run_dir: Path, events_path: Path):
382
  count = sync_pi_traces(run_dir, emit_event=False)
383
  append_event(events_path, "traces", "success", "Collected Pi traces", {"count": count})
 
428
  return list(dict.fromkeys(names))
429
 
430
 
431
+ def normalize_api_name(name: str | None) -> str:
432
+ value = (name or "").strip()
433
+ if not value:
434
+ return "/generate"
435
+ return value if value.startswith("/") else "/" + value
436
+
437
+
438
+ def default_smoke_args(expected_output_type: str) -> list:
439
+ expected = (expected_output_type or "any").lower()
440
+ if expected == "image":
441
+ return ["a cinematic robot cat astronaut, detailed, studio lighting"]
442
+ if expected == "video":
443
+ return ["a short cinematic shot of a robot cat astronaut walking on the moon"]
444
+ if expected == "audio":
445
+ return ["A calm voice saying hello from Agentic Space Factory."]
446
+ return ["Explain the concept of agentic AI in one paragraph."]
447
+
448
+
449
+ def endpoint_schema_for_api(schema, api_name: str):
450
+ target = normalize_api_name(api_name)
451
+ alternatives = {target, target.lstrip("/")}
452
+ found = None
453
+
454
+ def walk(obj):
455
+ nonlocal found
456
+ if found is not None:
457
+ return
458
+ if isinstance(obj, dict):
459
+ for key, value in obj.items():
460
+ if isinstance(key, str) and key in alternatives and isinstance(value, (dict, list)):
461
+ found = value
462
+ return
463
+ value = obj.get("api_name") or obj.get("apiName")
464
+ if isinstance(value, str) and normalize_api_name(value) == target:
465
+ found = obj
466
+ return
467
+ for value in obj.values():
468
+ walk(value)
469
+ elif isinstance(obj, list):
470
+ for item in obj:
471
+ walk(item)
472
+
473
+ walk(schema)
474
+ return found
475
+
476
+
477
+ def endpoint_parameter_names(endpoint) -> list[str]:
478
+ names: list[str] = []
479
+
480
+ def add(value):
481
+ if not isinstance(value, str):
482
+ return
483
+ cleaned = value.strip()
484
+ if cleaned and cleaned not in names:
485
+ names.append(cleaned)
486
+
487
+ def from_parameter(param):
488
+ if isinstance(param, dict):
489
+ for key in ["parameter_name", "parameterName", "name", "label"]:
490
+ add(param.get(key))
491
+ component = param.get("component") or param.get("component_type")
492
+ if isinstance(component, dict):
493
+ for key in ["label", "name"]:
494
+ add(component.get(key))
495
+ elif isinstance(param, str):
496
+ add(param)
497
+
498
+ def walk(obj):
499
+ if isinstance(obj, dict):
500
+ params = obj.get("parameters") or obj.get("inputs")
501
+ if isinstance(params, list):
502
+ for param in params:
503
+ from_parameter(param)
504
+ for value in obj.values():
505
+ if isinstance(value, (dict, list)):
506
+ walk(value)
507
+ elif isinstance(obj, list):
508
+ for item in obj:
509
+ walk(item)
510
+
511
+ walk(endpoint)
512
+ return names
513
+
514
+
515
+ def result_contains_expected_output(result, expected: str):
516
+ expected = (expected or "any").lower()
517
+ info = {"result_type": type(result).__name__, "result_repr": repr(result)[:2000]}
518
+ paths = []
519
+
520
+ def visit(obj):
521
+ if isinstance(obj, (str, Path)):
522
+ value = str(obj)
523
+ if value:
524
+ paths.append(value)
525
+ elif isinstance(obj, dict):
526
+ for key in ["path", "name", "url"]:
527
+ if key in obj:
528
+ visit(obj[key])
529
+ for value in obj.values():
530
+ if isinstance(value, (dict, list, tuple)):
531
+ visit(value)
532
+ elif isinstance(obj, (list, tuple)):
533
+ for item in obj:
534
+ visit(item)
535
+
536
+ visit(result)
537
+ info["detected_paths"] = paths[:20]
538
+ if expected == "any":
539
+ return result is not None, info
540
+ image_ext = (".png", ".jpg", ".jpeg", ".webp", ".gif")
541
+ video_ext = (".mp4", ".mov", ".webm")
542
+ audio_ext = (".wav", ".mp3", ".flac", ".ogg")
543
+ if expected == "text":
544
+ return isinstance(result, str) and bool(result.strip()), info
545
+ if expected == "image":
546
+ return any(str(p).lower().endswith(image_ext) for p in paths), info
547
+ if expected == "video":
548
+ return any(str(p).lower().endswith(video_ext) for p in paths), info
549
+ if expected == "audio":
550
+ return any(str(p).lower().endswith(audio_ext) for p in paths), info
551
+ return result is not None, info
552
+
553
+
554
+ def copy_result_artifacts(result, run_dir: Path):
555
+ artifacts = run_dir / "artifacts"
556
+ artifacts.mkdir(parents=True, exist_ok=True)
557
+ copied = []
558
+
559
+ def maybe_copy(obj):
560
+ if isinstance(obj, (str, Path)):
561
+ path = Path(str(obj))
562
+ if path.exists() and path.is_file():
563
+ target = artifacts / path.name
564
+ try:
565
+ shutil.copy2(path, target)
566
+ copied.append(str(target))
567
+ except Exception:
568
+ pass
569
+ elif isinstance(obj, dict):
570
+ for key in ["path", "name"]:
571
+ if key in obj:
572
+ maybe_copy(obj[key])
573
+ for value in obj.values():
574
+ if isinstance(value, (dict, list, tuple)):
575
+ maybe_copy(value)
576
+ elif isinstance(obj, (list, tuple)):
577
+ for item in obj:
578
+ maybe_copy(item)
579
+
580
+ maybe_copy(result)
581
+ return copied
582
+
583
+
584
+ def measured_zero_gpu_recommendation(latency_seconds: float | None):
585
+ if latency_seconds is None:
586
+ return {
587
+ "observed_latency_seconds": None,
588
+ "recommended_zero_gpu_duration_seconds": None,
589
+ "recommendation_source": "not_measured",
590
+ "recommendation_confidence": "none",
591
+ }
592
+ recommended = int(max(30, min(300, latency_seconds * 2 + 15)))
593
+ return {
594
+ "observed_latency_seconds": round(latency_seconds, 3),
595
+ "recommended_zero_gpu_duration_seconds": recommended,
596
+ "recommendation_source": "live_gradio_predict",
597
+ "recommendation_confidence": "measured",
598
+ "measurement_note": "Measured from a live gradio_client.predict call, including Gradio/API/network/result serialization overhead.",
599
+ }
600
+
601
+
602
+ def run_generation_smoke(target_space_id: str, token: str, run_dir: Path, events_path: Path, expected_output_type: str):
603
+ api_name = "/generate"
604
+ test_args = default_smoke_args(expected_output_type)
605
+ test_kwargs = {}
606
+ append_event(events_path, "generation_smoke", "started", "Calling live generation endpoint for ZeroGPU timing", {"api_name": api_name, "expected_output_type": expected_output_type})
607
+ client = make_gradio_client(target_space_id, token)
608
+ schema = client.view_api(return_format="dict")
609
+ discovered = api_names_from_schema(schema)
610
+ if api_name not in discovered and discovered:
611
+ non_health = [name for name in discovered if name != "/health"]
612
+ if non_health:
613
+ api_name = non_health[0]
614
+ endpoint = endpoint_schema_for_api(schema, api_name)
615
+ endpoint_parameters = endpoint_parameter_names(endpoint)
616
+ write_json(run_dir / "tests" / "generation_api_schema.json", {"schema": schema, "api_names": discovered, "selected_api_name": api_name, "endpoint_parameters": endpoint_parameters})
617
+ started = time.time()
618
+ result = client.predict(*test_args, api_name=api_name, **test_kwargs)
619
+ latency = time.time() - started
620
+ ok, info = result_contains_expected_output(result, expected_output_type)
621
+ copied = copy_result_artifacts(result, run_dir)
622
+ recommendation = measured_zero_gpu_recommendation(latency)
623
+ payload = {
624
+ "status": "success" if ok else "failed",
625
+ "target_space": target_space_id,
626
+ "api_name": api_name,
627
+ "discovered_api_names": discovered,
628
+ "endpoint_parameters": endpoint_parameters,
629
+ "test_args": test_args,
630
+ "test_kwargs": test_kwargs,
631
+ "expected_output_type": expected_output_type,
632
+ "latency_seconds": round(latency, 3),
633
+ "result_info": info,
634
+ "copied_artifacts": copied,
635
+ "validated_at": now(),
636
+ **recommendation,
637
+ }
638
+ write_json(run_dir / "tests" / "generation_smoke.json", payload)
639
+ if ok:
640
+ append_event(events_path, "generation_smoke", "success", "Live generation smoke test passed and ZeroGPU timing was measured", {"latency_seconds": payload["latency_seconds"], "recommended_zero_gpu_duration_seconds": payload["recommended_zero_gpu_duration_seconds"], "api_name": api_name})
641
+ else:
642
+ append_event(events_path, "generation_smoke", "failed", "Live generation returned an unexpected output type", payload)
643
+ return payload
644
+
645
+
646
  def space_subdomain_url(target_space_id: str) -> str:
647
  owner, name = target_space_id.split("/", 1)
648
  # This matches the common Spaces app URL pattern. Keep conservative: our
 
1160
  return {"parse_error": str(exc), "raw_tail": path.read_text(encoding="utf-8", errors="replace")[-2000:]}
1161
 
1162
 
1163
+ def infer_generation_gate(workspace: Path, implementation_mode: str, validation: dict, generation_smoke: dict | None, run_dir: Path, events_path: Path) -> dict:
1164
  """Classify the run separately from process success.
1165
 
1166
  /health passing means the Space boots. It does not mean the generated Space
 
1188
  "out of scope",
1189
  ]
1190
  blocker_detected = bool(blockers) or any(m in combined for m in blocked_markers)
1191
+ smoke_ok = isinstance(generation_smoke, dict) and generation_smoke.get("status") == "success"
1192
+ recommendation = generation_smoke if isinstance(generation_smoke, dict) else measured_zero_gpu_recommendation(None)
1193
  implementation_signals = {
1194
  "has_spaces_gpu": "@spaces.GPU" in app_text,
1195
  "has_torch": "torch" in req_text or "import torch" in app_text,
1196
  "has_diffusers": "diffusers" in req_text or "diffusers" in app_text,
1197
  "has_video_output_hint": any(x in app_text.lower() for x in ["gr.video", "video", ".mp4", "ffmpeg"]),
1198
  "health_passed": validation.get("method") in {"http_health", "gradio"},
1199
+ "generation_smoke_passed": smoke_ok,
1200
+ "zero_gpu_duration_measured": recommendation.get("recommendation_confidence") == "measured",
1201
  }
1202
 
1203
  if blocker_detected:
1204
  status = "technical_blocker"
1205
  message = "Space boots, but full model inference was not implemented. See TECHNICAL_BLOCKERS.json / PI_SUMMARY.md."
1206
+ elif implementation_mode in {"full-inference-gated", "full-inference-attempt"} and smoke_ok:
1207
+ status = "full_inference_success"
1208
+ message = "Space boots and a live generation smoke test passed. ZeroGPU duration recommendation was measured from real inference."
1209
  elif implementation_mode in {"full-inference-gated", "full-inference-attempt"}:
 
1210
  status = "full_inference_candidate_health_passed"
1211
+ message = "Space boots, but live generation smoke test did not produce a verified output. ZeroGPU duration recommendation was not measured."
1212
  else:
1213
  status = "health_only"
1214
  message = "Safe scaffold health validation passed. Full inference was not requested."
 
1241
  "blocker_detected": blocker_detected,
1242
  "implementation_signals": implementation_signals,
1243
  "validation_method": validation.get("method"),
1244
+ "generation_smoke": generation_smoke,
1245
+ "zero_gpu_duration_recommendation": {
1246
+ "observed_latency_seconds": recommendation.get("observed_latency_seconds"),
1247
+ "recommended_zero_gpu_duration_seconds": recommendation.get("recommended_zero_gpu_duration_seconds"),
1248
+ "recommendation_source": recommendation.get("recommendation_source"),
1249
+ "recommendation_confidence": recommendation.get("recommendation_confidence"),
1250
+ "measurement_note": recommendation.get("measurement_note"),
1251
+ },
1252
  "blockers": blockers,
1253
  }
1254
  write_json(run_dir / "inference_gate.json", gate)
 
1268
  fallback_hardware = os.environ.get("FALLBACK_SPACE_HARDWARE", "l40sx1")
1269
  allow_fixed_gpu_fallback = os.environ.get("ALLOW_FIXED_GPU_FALLBACK", "true").lower() in {"1", "true", "yes", "on"}
1270
  implementation_mode = os.environ.get("IMPLEMENTATION_MODE", "full-inference-attempt")
1271
+ expected_output_type = os.environ.get("EXPECTED_OUTPUT_TYPE", "any")
1272
  token = os.environ.get("HF_TOKEN")
1273
 
1274
  run_dir = output_root / "runs" / run_id
1275
  events_path = run_dir / "events.jsonl"
1276
  state_path = run_dir / "state.json"
1277
  workspace = Path("/tmp/universal_workspace")
1278
+ pi_model_resolution = {"requested_model": pi_model, "configured_model": pi_model, "effective_model": pi_model, "provider": "huggingface", "mismatch": False}
1279
 
1280
  append_event(events_path, "bootstrap", "started", "Universal model-card builder worker started", {"model_id": model_id, "target_space_id": target_space_id})
1281
  write_json(state_path, {"run_id": run_id, "kind": "universal_model_card_builder", "status": "running", "message": "Attempting Universal model-card builderd Space creation", "model_id": model_id, "target_space": target_space_id, "created_by": hf_username, "bucket_source": bucket_source, "created_at": now(), "updated_at": now()})
 
1320
  collect_pi_traces(run_dir, events_path)
1321
  fail(run_dir, events_path, "Pi failed before Space upload", {"returncode": code, "output_tail": pi_out[-4000:]})
1322
  append_event(events_path, "pi_run", "success", "Pi completed universal model-card workspace pass", {"output_tail": pi_out[-2000:]})
1323
+ # Pi sessions are the source of truth for the assistant/model actually used.
1324
+ # Sync them first, then resolve requested/configured/observed model identity.
1325
+ collect_pi_traces(run_dir, events_path)
1326
+ pi_model_resolution = detect_pi_model_resolution(pi_model, run_dir, pi_out)
1327
+ emit_pi_model_resolution(events_path, pi_model_resolution)
1328
  if not (workspace / "PI_SUMMARY.md").exists():
1329
  (workspace / "PI_SUMMARY.md").write_text("# Pi Summary\n\nPi did not create a PI_SUMMARY.md. See logs/pi_output.txt.\n", encoding="utf-8")
1330
 
 
1370
  raise
1371
  upload_workspace(api, workspace, target_space_id, token, run_dir, events_path)
1372
  validation = validate_live_api(api, target_space_id, token, run_dir, events_path, timeout_s=1200)
1373
+ generation_smoke = None
1374
+ if implementation_mode in {"full-inference-gated", "full-inference-attempt"}:
1375
+ try:
1376
+ generation_smoke = run_generation_smoke(target_space_id, token, run_dir, events_path, expected_output_type)
1377
+ except Exception as smoke_error:
1378
+ generation_smoke = {
1379
+ "status": "failed",
1380
+ "target_space": target_space_id,
1381
+ "expected_output_type": expected_output_type,
1382
+ "error": str(smoke_error)[:4000],
1383
+ **measured_zero_gpu_recommendation(None),
1384
+ }
1385
+ write_json(run_dir / "tests" / "generation_smoke.json", generation_smoke)
1386
+ append_event(events_path, "generation_smoke", "failed", "Live generation smoke test failed; ZeroGPU duration was not measured", generation_smoke)
1387
+ else:
1388
+ generation_smoke = measured_zero_gpu_recommendation(None) | {"status": "skipped", "expected_output_type": expected_output_type}
1389
+ write_json(run_dir / "tests" / "generation_smoke.json", generation_smoke)
1390
+ inference_gate = infer_generation_gate(workspace, implementation_mode, validation, generation_smoke, run_dir, events_path)
1391
 
1392
  # If the generated app looks like real GPU inference but automatic
1393
  # hardware requests failed, classify the run honestly as needing manual
 
1419
  "target_space_url": f"https://huggingface.co/spaces/{target_space_id}",
1420
  "selected_hardware": selected_hardware,
1421
  "hardware_attempts": hardware_attempts,
1422
+ "pi_model_resolution": pi_model_resolution,
1423
  "validation": validation,
1424
+ "generation_smoke": generation_smoke,
1425
  "inference_gate": inference_gate,
1426
  "updated_at": now(),
1427
  "created_by": hf_username,
 
1440
 
1441
  Model: `{model_id}`
1442
 
1443
+ ## Pi assistant model
1444
+
1445
+ ```json
1446
+ {json.dumps(pi_model_resolution, indent=2, ensure_ascii=False)}
1447
+ ```
1448
+
1449
  ## Hardware
1450
 
1451
  Selected/requested hardware: `{selected_hardware}`
 
1464
  {json.dumps(validation, indent=2, ensure_ascii=False)}
1465
  ```
1466
 
1467
+ ## Live generation smoke / ZeroGPU duration
1468
+
1469
+ ```json
1470
+ {json.dumps(generation_smoke, indent=2, ensure_ascii=False)}
1471
+ ```
1472
+
1473
  ## Full-inference gate
1474
 
1475
  ```json
 
1604
  return names
1605
 
1606
 
1607
+ def normalize_api_name(name: str | None) -> str:
1608
+ value = (name or "").strip()
1609
+ if not value:
1610
+ return "/generate"
1611
+ return value if value.startswith("/") else "/" + value
1612
+
1613
+
1614
+ def endpoint_schema_for_api(schema, api_name: str):
1615
+ target = normalize_api_name(api_name)
1616
+ alternatives = {target, target.lstrip("/")}
1617
+ found = None
1618
+
1619
+ def walk(obj):
1620
+ nonlocal found
1621
+ if found is not None:
1622
+ return
1623
+ if isinstance(obj, dict):
1624
+ for key, value in obj.items():
1625
+ if isinstance(key, str) and key in alternatives and isinstance(value, (dict, list)):
1626
+ found = value
1627
+ return
1628
+ value = obj.get("api_name") or obj.get("apiName")
1629
+ if isinstance(value, str) and normalize_api_name(value) == target:
1630
+ found = obj
1631
+ return
1632
+ for value in obj.values():
1633
+ walk(value)
1634
+ elif isinstance(obj, list):
1635
+ for item in obj:
1636
+ walk(item)
1637
+
1638
+ walk(schema)
1639
+ return found
1640
+
1641
+
1642
+ def endpoint_parameter_names(endpoint) -> list[str]:
1643
+ names: list[str] = []
1644
+
1645
+ def add(value):
1646
+ if not isinstance(value, str):
1647
+ return
1648
+ cleaned = value.strip()
1649
+ if cleaned and cleaned not in names:
1650
+ names.append(cleaned)
1651
+
1652
+ def from_parameter(param):
1653
+ if isinstance(param, dict):
1654
+ for key in ["parameter_name", "parameterName", "name", "label"]:
1655
+ add(param.get(key))
1656
+ component = param.get("component") or param.get("component_type")
1657
+ if isinstance(component, dict):
1658
+ for key in ["label", "name"]:
1659
+ add(component.get(key))
1660
+ elif isinstance(param, str):
1661
+ add(param)
1662
+
1663
+ def walk(obj):
1664
+ if isinstance(obj, dict):
1665
+ params = obj.get("parameters") or obj.get("inputs")
1666
+ if isinstance(params, list):
1667
+ for param in params:
1668
+ from_parameter(param)
1669
+ for value in obj.values():
1670
+ if isinstance(value, (dict, list)):
1671
+ walk(value)
1672
+ elif isinstance(obj, list):
1673
+ for item in obj:
1674
+ walk(item)
1675
+
1676
+ walk(endpoint)
1677
+ return names
1678
+
1679
+
1680
+ def sanitize_kwargs_for_schema(api_name: str, schema, kwargs: dict):
1681
+ endpoint = endpoint_schema_for_api(schema, api_name)
1682
+ parameter_names = endpoint_parameter_names(endpoint)
1683
+ if not parameter_names:
1684
+ return kwargs, {}, []
1685
+ allowed = set(parameter_names)
1686
+ sanitized = {key: value for key, value in kwargs.items() if key in allowed}
1687
+ dropped = {key: value for key, value in kwargs.items() if key not in allowed}
1688
+ return sanitized, dropped, parameter_names
1689
+
1690
+
1691
  def runtime_to_dict(runtime) -> dict:
1692
  payload = {}
1693
  for attr in ["stage", "hardware", "requested_hardware", "sleep_time", "storage", "gc_timeout"]:
 
1872
 
1873
 
1874
  def smoke_generate(target_space_id: str, token: str, run_dir: Path, events_path: Path):
1875
+ api_name = normalize_api_name(os.environ.get("API_NAME") or "/generate")
1876
  expected_output_type = (os.environ.get("EXPECTED_OUTPUT_TYPE") or "any").strip()
1877
  test_args = parse_json_env("TEST_ARGS_JSON", ["a cinematic robot cat astronaut, detailed, studio lighting"])
1878
  test_kwargs = parse_json_env("TEST_KWARGS_JSON", {})
 
1884
  client = make_gradio_client(target_space_id, token)
1885
  schema = client.view_api(return_format="dict")
1886
  discovered = api_names_from_schema(schema)
1887
+ safe_kwargs, dropped_kwargs, endpoint_parameters = sanitize_kwargs_for_schema(api_name, schema, test_kwargs)
1888
+ write_json(run_dir / "tests" / "api_schema.json", {"schema": schema, "api_names": discovered, "selected_api_name": api_name, "endpoint_parameters": endpoint_parameters})
1889
+ if dropped_kwargs:
1890
+ append_event(
1891
+ events_path,
1892
+ "generation_smoke",
1893
+ "warning",
1894
+ "Ignored unsupported validation kwargs for this Gradio endpoint",
1895
+ {"api_name": api_name, "ignored_kwargs": sorted(dropped_kwargs), "endpoint_parameters": endpoint_parameters},
1896
+ )
1897
  started = time.time()
1898
+ try:
1899
+ result = client.predict(*test_args, api_name=api_name, **safe_kwargs)
1900
+ except Exception as exc:
1901
+ message = str(exc)
1902
+ if safe_kwargs and "not a valid key-word argument" in message:
1903
+ append_event(
1904
+ events_path,
1905
+ "generation_smoke",
1906
+ "warning",
1907
+ "Retrying validation without keyword arguments after Gradio rejected kwargs",
1908
+ {"api_name": api_name, "error": message[:1500], "dropped_kwargs": sorted(safe_kwargs)},
1909
+ )
1910
+ safe_kwargs = {}
1911
+ result = client.predict(*test_args, api_name=api_name)
1912
+ else:
1913
+ raise
1914
  latency = time.time() - started
1915
  ok, info = result_contains_expected_output(result, expected_output_type)
1916
  copied = copy_result_artifacts(result, run_dir)
 
1920
  "api_name": api_name,
1921
  "discovered_api_names": discovered,
1922
  "test_args": test_args,
1923
+ "test_kwargs": safe_kwargs,
1924
+ "original_test_kwargs": test_kwargs,
1925
+ "ignored_test_kwargs": dropped_kwargs,
1926
+ "endpoint_parameters": endpoint_parameters,
1927
  "expected_output_type": expected_output_type,
1928
  "latency_seconds": round(latency, 3),
1929
+ "observed_latency_seconds": round(latency, 3),
1930
  "result_info": info,
1931
  "copied_artifacts": copied,
1932
  "recommended_zero_gpu_duration_seconds": int(max(30, min(300, latency * 2 + 15))),
1933
+ "recommendation_source": "live_gradio_predict",
1934
+ "recommendation_confidence": "measured",
1935
+ "measurement_note": "Measured from a live gradio_client.predict call, including Gradio/API/network/result serialization overhead.",
1936
  "validated_at": now(),
1937
  }
1938
  write_json(run_dir / "tests" / "generation_smoke.json", payload)