Nv Reason Ct
NVIDIAOfficialCreated by NVIDIA MedTech
- Skill
- Tool integration
- Science and research
- Apache-2.0
- Medium risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
NV-Reason-CT provides a standardized wrapper for executing the NV-Reason-CT model on 3D NIfTI chest or abdominal CT volumes. Designed for research and engineering environments, this skill facilitates inference using Hugging Face Transformers. It includes robust setup checks, support for custom model exports, and structured JSON output for downstream analysis. The skill emphasizes security by requiring explicit user authorization before loading custom model code and provides diagnostic tools to verify environment readiness and asset availability.
What it does
- Executes inference on 3D NIfTI CT volumes using the NV-Reason-CT model.
- Supports anatomy-specific processing for chest and abdominal regions.
- Provides a diagnostic mode to verify environment setup and model asset availability.
- Generates structured JSON output containing response text, metadata, and runtime information.
- Allows for the use of custom local model exports or specific Hugging Face model revisions.
- Includes safety gates requiring explicit user consent before executing custom model code.
When to use it
- When performing research or engineering analysis on CT scan data.
- When you need to integrate NV-Reason-CT inference into a larger automated pipeline.
- When verifying that a computing environment is correctly configured for model inference.
- When processing specific chest or abdominal CT volumes for non-clinical research purposes.
Advantages
- Standardized wrapper ensures consistent inference execution and output formatting.
- Explicit authorization requirements for custom code enhance security awareness.
- Diagnostic tools help identify missing dependencies or incomplete model assets before runtime.
- Supports both cloud-based model loading and local model exports for flexibility.
- Provides detailed runtime metadata for reproducibility in research workflows.
Usage examples
- “Run inference on chest_scan.nii.gz for a chest CT analysis.”
- The skill executes the model on the provided volume, generates a structured report in JSON format, and outputs the result to the specified directory.
- “Verify if the current environment is ready for NV-Reason-CT inference.”
- The skill runs a diagnostic check, validating dependencies and model asset presence, and returns a status report without executing a full inference.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/nvidia/nv-reason-ct/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip nv-reason-ct.zip -d .agents/skills
Documentation by NVIDIA MedTech
NV-Reason-CT
Purpose
Runs the documented
nvidia/NV-Reason-CTHugging Face Transformers path on one user-provided 3D NIfTI CT volume and text prompt.Emits
result_jsonwith input geometry and hash, anatomy-region selection, response text, runtime identity, dependency versions, and limitations.Manifest I/O: inputs are
ct_request_or_fixtureand optional directct_volume; output isresult_jsonon stdout.Not for diagnosis, treatment, triage, patient-facing output, or clinical reporting.
Instructions
Read
skill_manifest.yamlbefore changing arguments, dependencies, side effects, or validation gates.Run
scripts/run_nv_reason_ct.py; do not write replacement inference code or use the Gradio demo for normal runs. Before inference, explain that the selected model and processor contain custom Python code running with the caller's permissions. Review the selected Hub revision or local export and obtain the caller's explicit authorization to load that code; ask if it has not already been granted. Only then pass--trust-model-code. Do not infer this consent from a CT request file, environment setting, cached files, or a successful setup check.After that authorization, hosts exposing a helper may use
run_script("scripts/run_nv_reason_ct.py", args=["PATH_TO_CT.nii.gz", "--trust-model-code", "--anatomy-region", "chest", "--prompt", "PROMPT", "--out-dir", "OUT_DIR"]).Inspect stdout JSON and the exit status. Preserve JSON for the caller or eval harness, including partial output on failure; do not redirect it away with
>. Successful inference writes only stdout, without creating--out-dir. Exit code3means generation hit the token ceiling without EOS: the text is incomplete and the completion gate fails. Only then does the wrapper attempt to create--out-dirand save a uniquepartial_result_*.json. Only a successful save addsoutput.partial_json_pathand prints the path on stderr. If saving fails, stderr warns to retain stdout; the JSON remains available there without a file path. Input, dependency, and inference errors exit2with an error on stderr.
Tool scope: Bash runs the committed wrapper and documented setup commands; Read covers skill files, caller-selected inputs, and model assets; Write covers caller-selected outputs and isolated caches. Env is SkillSpector's environment-capability label for the runtime variables listed below, accessed through shell/Python on hosts without a separate environment tool. These declarations do not authorize unrelated file access, printing authentication values, or changing an existing development environment.
Examples
For the end-to-end example, follow EXAMPLES.md to download the upstream Git LFS volumes into a separate checkout, then run examples/example_1.nii.gz through the wrapper for both chest and abdomen. That scan covers both regions according to the upstream README.
Live inference on a chest CT, after explicit custom-code authorization:
python skills/nv-reason-ct/scripts/run_nv_reason_ct.py PATH_TO_CT.nii.gz \
--trust-model-code \
--anatomy-region chest \
--prompt "write a structured chest CT report" \
--out-dir runs/nv_reason_ct_caseRequire exit 0, runtime.mode == "hf_transformers", runtime.mock == false, nonempty response text, and runtime.truncated_by_max_new_tokens == false. A JSON request may also point to the CT volume; replace volume_path in fixtures/example_request.json with an existing authorized file before running it.
For a complete local model-and-processor export saved by upstream SFT or GRPO:
python skills/nv-reason-ct/scripts/run_nv_reason_ct.py \
--model ./data/nv_reason_ct_sft_example --check-setup --fail-on-not-ready
python skills/nv-reason-ct/scripts/run_nv_reason_ct.py PATH_TO_CT.nii.gz \
--model ./data/nv_reason_ct_sft_example --anatomy-region chest --trust-model-codeThe directory must contain the full model, custom code, tokenizer, and processor assets, not just adapters or an intermediate optimizer checkpoint. Review local custom code before loading it. Local exports bypass Hub revision resolution and load with local_files_only=True; they record runtime.model_source == "local", an absolute runtime.model path, and null revision/resolved_revision. Keep export content hashes separately: a path is not immutable provenance. Do not pass --revision for local exports; NV_REASON_CT_REVISION is ignored for them. --model-id remains an alias for --model. This does not sandbox custom Python code or validate model compatibility beyond the upstream loading contract.
For an abdominal crop, pass --anatomy-region abdomen. Pass --anatomy-region none only for a manually cropped input; upstream preprocessing then uses a centered crop. Pass --no-thinking for a concise response without thinking output.
The region defaults match upstream inference.py: write a structured chest CT report or write a structured abdominal CT report. To exercise another documented prompt in an engineering check, pass --prompt "full chest CT reasoning analysis" (or the abdominal equivalent), or preserve the caller's focused question verbatim. Supply a scan that actually covers the requested anatomy; the wrapper does not establish that coverage.
Each invocation loads the model and starts a new single-turn conversation. The upstream README also documents model reuse and follow-up history, but this wrapper does not preserve earlier turns. Do not simulate a follow-up by silently discarding history or carry answers between scans. Upstream training JSONL files are not runnable demo fixtures: they contain records but no CT volumes. Use the upstream examples/ volumes or a caller-owned authorized NIfTI scan; do not use reference assistant answers as inference prompts.
Available Scripts
| Script | Purpose | Arguments |
|---|---|---|
scripts/run_nv_reason_ct.py | Read-only setup checks and inference on an existing NIfTI volume. | CT_OR_REQUEST --trust-model-code [--anatomy-region chest|abdomen|none] [--prompt TEXT] [--thinking|--no-thinking] [--out-dir OUT_DIR]; --check-setup needs neither CT input nor code-execution consent. |
Prerequisites
Python 3.11+ for the wrapper, a CUDA-capable NVIDIA GPU with bfloat16 support for live inference, and enough memory for the model plus volumetric visual tokens.
Follow NV-Reason-CT's installation instructions in a fresh environment. The upstream repository owns dependency requirements and compatibility guidance; this skill does not duplicate its requirements file or enforce package-version constraints. Use the upstream inference setup, not the training extras. The wrapper never installs or upgrades packages.
The manifest inventories required packages, including
huggingface-hubfor model access. Setup reports package availability and installed versions; the historical environment below is advisory evidence, not an installation prescription. Review the selected environment's dependency security separately.Model assets may download from
https://huggingface.counder~/.cache/huggingface/. SetHF_TOKENwhen repository access requires authentication.The upstream examples are available from the public NV-Reason-CT repository and require Git LFS. Download actual volumes with
git lfs pull; a small text pointer named.nii.gzis not a CT image. Do not install Git LFS globally or change an existing checkout's configuration without authorization; the example guide uses checkout-local configuration.Run a read-only setup check before downloading weights or starting inference. It checks the NV-Reason-CT processor/code assets and all indexed weight shards in only the selected cached Hub snapshot or local export. This is offline file-presence inspection, without model initialization or integrity verification. Other model layouts may require different assets:
python skills/nv-reason-ct/scripts/run_nv_reason_ct.py --check-setupThis diagnostic command exits 0 when it produces a report, even if setup is not ready. For an automation gate after staging model assets, use --check-setup --fail-on-not-ready: exit 0 means ready_for_live_cuda_inference, exit 1 means setup is not ready, and exit 2 means an argument or execution error. Inspect the JSON recommendation in either mode. Readiness covers imports, CUDA/bfloat16, and asset presence, not package-version compatibility or a vulnerability audit; version_constraints_checked is false. Enable offline settings only after the selected model assets and custom code are cached.
Inference without --trust-model-code exits 2 before model lookup or loading. With it, the wrapper warns on stderr before loading custom code, including the resolved Hub commit or local export path. This flag is an acknowledgement, not a sandbox, code audit, or signature verification. The setup report's model_code_opt_in_required remains true even when assets are ready. The manifest's default command does not supply consent; use the documented explicit CLI invocation for authorized live inference.
For a disposable run, keep caches separate from installed packages: set HF_HOME, HF_HUB_CACHE, HF_MODULES_CACHE, XDG_CACHE_HOME, and TORCH_HOME to subdirectories of a caller-owned temporary directory before launching Python. After the process exits, remove only those explicitly identified task caches when cleanup is requested, retaining input scans and result JSON. Do not purge a shared Hugging Face cache or change an existing development environment.
Verified environment baseline
The live CUDA path was reverified on 2026-09-21 with the upstream examples/example_1.nii.gz in both chest and abdomen modes, using the versions below. These are recorded successful-run settings, not required versions, an upstream compatibility guarantee, or a security recommendation. Select the environment using upstream guidance and a PyTorch CUDA build compatible with the host driver; the recorded PyTorch distribution version was 2.12.0, and torch.__version__ reported 2.12.0+cu130.
The historical Transformers version below is affected by CVE-2026-9856. Consult upstream maintainers for a compatible, security-reviewed environment; this skill has not verified a remediated live baseline. Moving dependency ownership upstream does not resolve that finding.
| Component | Verified version or setting |
|---|---|
| Python | 3.12.3 |
| PyTorch | 2.12.0 (2.12.0+cu130 runtime build) |
| CUDA reported by PyTorch | 13.0 |
| Transformers | 5.6.2 |
| dynamic-network-architectures | 0.4.3 |
| MONAI | 1.6.0 |
| NiBabel | 5.4.2 |
| SciPy | 1.16.0 |
| NumPy | 2.5.2 |
| Pillow | 11.2.1 |
| timm | 1.0.22 |
| einops | 0.8.2 |
| safetensors | 0.7.0 |
| huggingface-hub | 1.30.0 |
| packaging | 25.0 |
The verified inference settings were one 48 GB NVIDIA RTX 6000 Ada GPU (driver 580.178.04), bfloat16 model weights, SDPA attention, deterministic decoding (do_sample=False), thinking enabled, and max_new_tokens=2048. The upstream example generated 438 tokens with the chest crop and 453 with the abdomen crop; neither response was truncated. Both matched the upstream CLI after trimming surrounding whitespace, using the same reviewed immutable model revision and offline assets in a disposable environment. All three upstream volumes also passed input/geometry/hash checks. This is a recorded baseline, not a fresh run of every subsequent skill revision. It verifies inference wiring, not clinical correctness. Lower-memory GPUs and other PyTorch/CUDA combinations have not yet been verified by this skill.
Environment variables:
| Variable | When to use |
|---|---|
NV_REASON_CT_MODEL | Override the Hub model id or select a complete local export. |
NV_REASON_CT_REVISION | Select a reviewed Hub model revision; prefer an immutable revision for evidence runs. Ignored for local exports. |
HF_HOME | Point to a caller-managed Hugging Face cache. |
HF_HUB_CACHE | Set an explicit model cache under HF_HOME when isolating a run; overrides any inherited shared Hub cache location. |
HF_MODULES_CACHE | Isolate downloaded Transformers custom-code modules for a disposable run. |
XDG_CACHE_HOME | Isolate supporting library caches for a disposable run. |
TORCH_HOME | Isolate Torch asset caches for a disposable run. |
HF_TOKEN | Authenticate model access when required. |
TRANSFORMERS_OFFLINE | Set to 1 only after all model and custom-code files are cached. |
HF_HUB_OFFLINE | Set to 1 only after all Hugging Face assets are cached. |
CUDA_VISIBLE_DEVICES | Restrict which GPU the wrapper may use. |
Inference uses the upstream contract: AutoModelForImageTextToText and AutoProcessor with trust_remote_code=True, bfloat16, SDPA attention, images3d=[CT_PATH], deterministic generation, and anatomy-aware chest or abdomen cropping. For Hub models, each run resolves the requested revision once and uses that immutable commit for model weights, processor assets, and custom code. Output records runtime.model_source == "hub", retains the requested runtime.revision, and records the commit in runtime.resolved_revision. Keep private revision identifiers in local evidence until approved for disclosure. CUDA is always required; select a GPU with CUDA_VISIBLE_DEVICES.
Limitations
The wrapper loads upstream custom code only after explicit
--trust-model-codeopt-in. Review that code and use an isolated environment and an immutable reviewed revision for evidence runs. Neither offline mode nor a local export prevents arbitrary actions by custom code.It checks readable 3D NIfTI structure, positive voxel spacing, a finite affine, and file identity. It does not verify de-identification, Hounsfield-unit calibration, anatomy coverage, crop quality, or clinical correctness.
Model output can hallucinate, omit findings, or present unreliable reasoning. Reviewable reasoning text is generated output, not proof of the model's internal computation.
CT scans and model weights must be supplied separately. The upstream examples demonstrate inference wiring and provide no clinical ground truth. A successful setup check is not completed model inference.
The moving
mainrevision is suitable only for development. Replace it with the reviewed immutable public release revision before publication.This wrapper does not cover the Gradio UI, finetuning, retraining, clinical deployment, or patient-facing use.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
Model-code consent required (exit 2) | Inference was requested without explicit opt-in. | Explain the code-execution risk and obtain authorization for the selected model, then pass --trust-model-code. Do not auto-consent for the caller. --check-setup remains available without it. |
Missing or broken dependency in --check-setup | A required package is unavailable or cannot import. | Inspect setup.dependencies and follow upstream installation guidance in a fresh environment. Installed versions are recorded, not enforced by this skill. |
| Inference API or dependency compatibility error | Import checks passed but the installed stack may not support the selected model. | Compare the reported versions with upstream guidance; retain the error for upstream review. Setup success alone does not prove compatibility. |
| Authentication or repository-not-found error | Model assets are not public or the current account lacks access. | Set an authorized HF_TOKEN; do not copy tokens into commands, fixtures, or logs. |
Cache is incomplete in --check-setup | The selected revision or some of its required assets are absent. | Inspect missing_files, then download the selected revision with authorized access before enabling offline mode. Files cached for another revision do not satisfy this check. |
| Incomplete local export | Training saved only adapters, an intermediate checkpoint, or no processor assets. | Supply the complete model-and-processor export from upstream training; do not substitute a Hub revision for missing files. |
| CUDA unavailable or bfloat16 unsupported | Inference is running on an unsupported device or isolated GPU context. | Use a compatible CUDA host before retrying inference. |
| NIfTI shape or spacing error | Input is unreadable, 4D, or has invalid geometry metadata. | Supply one 3D .nii or .nii.gz CT volume with valid spacing and affine metadata. |
| Git LFS pointer instead of a CT volume | The checkout contains the small pointer file, not the NIfTI data. | Run git lfs pull --include="examples/*.nii.gz" in the upstream checkout, then retry. |
| Poor chest or abdomen crop | Automatic anatomy heuristics do not fit the scan geometry. | Inspect upstream preprocessing, manually crop the CT, and pass --anatomy-region none. |
| Empty or truncated response | Generation stopped without text, or reached the token limit without EOS (exit 3). | Preserve any partial JSON and stderr; do not treat it as a completed response. Verify setup and input, then adjust --max-new-tokens if truncation is reported. |
License
This skill's documentation, wrapper, and tests are licensed under Apache-2.0. See LICENSE for the full terms.
The separately obtained upstream NV-Reason-CT model code, weights, and example data retain their upstream terms, including OpenMDW-1.1. The skill's Apache-2.0 license does not relicense those assets. Retain the applicable licenses and notices when redistributing either the skill or upstream materials.
Files
- ATTRIBUTION.mdAdded by AutoGineer918 B
- BENCHMARK.md9.1 KiB
- EXAMPLES.md6.4 KiB
- LICENSE11.0 KiB
- SKILL.md17.8 KiB
- evals/evals.json9.0 KiB
- fixtures/example_request.json193 B
- scripts/run_nv_reason_ct.pyScript30.5 KiB
- skill-card.md4.2 KiB
- skill.oms.sig6.5 KiB
- skill_manifest.yaml9.1 KiB
- tests/conftest.pyScript3.5 KiB
- tests/test_nv_reason_ct.pyScript32.6 KiB
- tests/test_upstream_examples.pyScript6.3 KiB
- validators/output_schema.json4.8 KiB
scripts/run_nv_reason_ct.py
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Run NV-Reason-CT inference on one existing NIfTI CT volume."""
from __future__ import annotations
import argparse
import hashlib
import importlib.metadata
import json
import math
import os
import re
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any
DEFAULT_MODEL = "nvidia/NV-Reason-CT"
DEFAULT_REVISION = "main"
DEFAULT_ANATOMY_REGION = "chest"
DEFAULT_MAX_NEW_TOKENS = 2048
ANATOMY_REGIONS = ("chest", "abdomen", "none")
TRUTHY = {"1", "true", "yes", "on"}
HASH_BLOCK_BYTES = 1024 * 1024
GIT_LFS_POINTER_HEADER = b"version https://git-lfs.github.com/spec/v1"
UPSTREAM_SETUP_URL = "https://github.com/NVIDIA-Medtech/NV-Reason-CT#installation"
# Import inventory only; upstream owns version constraints and installation.
DEPENDENCY_IMPORTS = {
"torch": "torch",
"transformers": "transformers",
"monai": "monai",
"nibabel": "nibabel",
"dynamic-network-architectures": "dynamic_network_architectures",
"scipy": "scipy",
"numpy": "numpy",
"Pillow": "PIL",
"timm": "timm",
"einops": "einops",
"safetensors": "safetensors",
"huggingface-hub": "huggingface_hub",
}
ENVIRONMENT_PACKAGES = tuple(DEPENDENCY_IMPORTS)
# Assets used by the reviewed upstream composite processor and generation path.
# Check these in one snapshot; weights alone do not make offline inference ready.
REQUIRED_MODEL_FILES = (
"config.json",
"generation_config.json",
"model.py",
"processor.py",
"processor_config.json",
"preprocessor_config.json",
"video_preprocessor_config.json",
"image_processor_3d/preprocessor_config.json",
"tokenizer_config.json",
"tokenizer.json",
"chat_template.jinja",
)
HUB_COMMIT_PATTERN = r"[0-9a-f]{40}"
LIMITATIONS = [
"Output is engineering evidence only; it is not a diagnosis or treatment recommendation.",
"NV-Reason-CT output can hallucinate, omit findings, or present unreliable reasoning.",
"The wrapper does not verify CT de-identification, Hounsfield-unit calibration, crop quality, or clinical correctness.",
"A qualified professional must review any use involving medical interpretation.",
]
class SkillError(Exception):
"""Input, dependency, or runtime error that should be shown cleanly."""
@dataclass(frozen=True)
class VolumeInfo:
"""Inspectable NIfTI metadata used by the wrapper contract."""
shape: tuple[int, int, int]
spacing_mm: tuple[float, float, float]
dtype: str
sha256: str
@dataclass(frozen=True)
class InputSpec:
"""Resolved input and inference settings."""
volume_path: Path
source: str
prompt: str
case_id: str
anatomy_region: str
enable_thinking: bool
def _truthy(value: str | None) -> bool:
return str(value or "").strip().lower() in TRUTHY
def _installed_version(dist_name: str) -> str | None:
try:
return importlib.metadata.version(dist_name)
except importlib.metadata.PackageNotFoundError:
return None
def _package_status(dist_name: str, import_name: str) -> dict[str, Any]:
if DEPENDENCY_IMPORTS.get(dist_name) != import_name:
raise SkillError("setup probes are limited to the documented dependencies")
version = _installed_version(dist_name)
if version is None:
return {"installed": False, "importable": False, "version": None}
try:
importlib.import_module(import_name)
importable = True
error = None
except Exception as exc: # setup probes must report broken binary imports
importable = False
error = str(exc)
result: dict[str, Any] = {
"installed": True,
"importable": importable,
"version": version,
}
if error:
result["error"] = error
return result
def _cuda_report() -> dict[str, Any]:
try:
import torch
except Exception as exc:
return {
"available": False,
"bfloat16_supported": False,
"devices": [],
"error": f"torch import failed: {exc}",
}
available = bool(torch.cuda.is_available())
devices = []
if available:
for index in range(torch.cuda.device_count()):
properties = torch.cuda.get_device_properties(index)
devices.append(
{
"index": index,
"name": properties.name,
"memory_total_mb": round(properties.total_memory / 1024 / 1024),
}
)
try:
bfloat16_supported = bool(available and torch.cuda.is_bf16_supported())
except Exception:
bfloat16_supported = False
return {
"available": available,
"bfloat16_supported": bfloat16_supported,
"device_count": len(devices),
"devices": devices,
"torch_cuda_version": getattr(torch.version, "cuda", None),
}
def _cached_weight_requirements(files: dict[str, Path]) -> set[str]:
"""Return the monolithic weight file or every shard named by its index."""
if "model.safetensors" in files:
return {"model.safetensors"}
index_name = "model.safetensors.index.json"
if index_name not in files:
return {"model.safetensors"}
index = json.loads(files[index_name].read_text())
weight_map = index.get("weight_map") if isinstance(index, dict) else None
if not isinstance(weight_map, dict) or not weight_map:
raise ValueError("safetensors index must contain a nonempty weight_map")
if not all(isinstance(name, str) and name for name in weight_map.values()):
raise ValueError("safetensors index contains an invalid shard filename")
return {index_name, *weight_map.values()}
def _model_cache_report(model_id: str, revision: str | None) -> dict[str, Any]:
"""Inspect only the selected local export or cached Hub snapshot, offline."""
report: dict[str, Any] = {
"inspectable": False,
"cached": False,
"repo_id": model_id,
"revision": revision,
"resolved_revision": None,
"complete": False,
"missing_files": sorted((*REQUIRED_MODEL_FILES, "model.safetensors")),
}
try:
if revision is None:
snapshot = Path(model_id)
else:
from huggingface_hub import snapshot_download
snapshot = Path(
snapshot_download(model_id, revision=revision, local_files_only=True, token=False)
)
if not re.fullmatch(HUB_COMMIT_PATTERN, snapshot.name):
raise ValueError("cached snapshot has no immutable Hub revision")
report["resolved_revision"] = snapshot.name
report.update(inspectable=True, cached=snapshot.is_dir())
files = {
path.relative_to(snapshot).as_posix(): path
for path in snapshot.rglob("*")
if path.is_file() and path.stat().st_size > 0
}
weight_files = _cached_weight_requirements(files)
report["missing_files"] = sorted((set(REQUIRED_MODEL_FILES) | weight_files) - files.keys())
report["complete"] = not report["missing_files"]
except Exception as exc:
report["error"] = f"could not inspect selected model assets: {exc}"
return report
def _setup_report(model_id: str, revision: str | None) -> dict[str, Any]:
dependencies = {
name: _package_status(name, import_name) for name, import_name in DEPENDENCY_IMPORTS.items()
}
missing_or_broken = [
name
for name, status in dependencies.items()
if not status["installed"] or not status["importable"]
]
cuda = _cuda_report()
cache = _model_cache_report(model_id, revision)
if missing_or_broken:
recommendation = "install_or_repair_upstream_dependencies"
elif not cuda["available"]:
recommendation = "use_a_cuda_host"
elif not cuda["bfloat16_supported"]:
recommendation = "use_a_cuda_gpu_with_bfloat16_support"
elif not cache["complete"]:
recommendation = "stage_complete_model_and_processor_assets"
else:
recommendation = "ready_for_live_cuda_inference"
return {
"skill": "nv_reason_ct",
"setup": {
"python": sys.executable,
"model": model_id,
"model_source": "local" if revision is None else "hub",
"revision": revision,
"recommended_torch_dtype": "bfloat16",
"recommended_attention_implementation": "sdpa",
"dependencies": dependencies,
"version_constraints_checked": False,
"dependency_setup_url": UPSTREAM_SETUP_URL,
"model_code_opt_in_required": True,
"cuda": cuda,
"model_cache": cache,
"recommendation": recommendation,
},
}
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(HASH_BLOCK_BYTES), b""):
digest.update(block)
return digest.hexdigest()
def _is_nifti_path(path: Path) -> bool:
lower_name = path.name.lower()
return lower_name.endswith(".nii") or lower_name.endswith(".nii.gz")
def _nifti_modules() -> tuple[Any, Any]:
try:
import nibabel as nib
import numpy as np
except Exception as exc:
raise SkillError(
"NIfTI input handling requires nibabel and numpy. Install the "
f"SKILL.md prerequisites. Import error: {exc}"
) from exc
return nib, np
def _volume_info(path: Path) -> VolumeInfo:
if not path.exists():
raise SkillError(f"volume not found: {path}")
if not path.is_file():
raise SkillError(f"volume path is not a file: {path}")
if not _is_nifti_path(path):
raise SkillError(f"unsupported volume format for {path}: expected .nii or .nii.gz")
try:
with path.open("rb") as stream:
is_lfs_pointer = stream.read(len(GIT_LFS_POINTER_HEADER)) == GIT_LFS_POINTER_HEADER
except OSError as exc:
raise SkillError(f"could not read NIfTI volume {path}: {exc}") from exc
if is_lfs_pointer:
raise SkillError(
f"{path} is a Git LFS pointer, not a downloaded CT volume. "
"Run git lfs pull --include='examples/*.nii.gz' in the "
"upstream NV-Reason-CT checkout before inference."
)
nib, np = _nifti_modules()
try:
image = nib.load(str(path))
except Exception as exc:
raise SkillError(f"could not read NIfTI volume {path}: {exc}") from exc
shape = tuple(int(value) for value in image.shape)
if len(shape) != 3 or any(value < 1 for value in shape):
raise SkillError(f"NV-Reason-CT requires one 3D NIfTI volume; got shape {shape}")
spacing = tuple(float(value) for value in image.header.get_zooms()[:3])
if len(spacing) != 3 or any(not math.isfinite(value) or value <= 0 for value in spacing):
raise SkillError(f"NIfTI voxel spacing must contain three positive values; got {spacing}")
if not bool(np.isfinite(np.asarray(image.affine)).all()):
raise SkillError("NIfTI affine contains non-finite values")
return VolumeInfo(
shape=(shape[0], shape[1], shape[2]),
spacing_mm=(spacing[0], spacing[1], spacing[2]),
dtype=str(image.get_data_dtype()),
sha256=_sha256(path),
)
def _normalize_region(value: Any) -> str:
region = str(value or DEFAULT_ANATOMY_REGION).strip().lower()
if region not in ANATOMY_REGIONS:
raise SkillError("anatomy_region must be 'chest', 'abdomen', or 'none'")
return region
def _default_prompt(region: str) -> str:
if region == "chest":
return "write a structured chest CT report"
if region == "abdomen":
return "write a structured abdominal CT report"
return "Describe this CT volume."
def _fixture_bool(fixture: dict[str, Any], key: str, default: bool) -> bool:
value = fixture.get(key, default)
if not isinstance(value, bool):
raise SkillError(f"fixture field {key} must be a boolean")
return value
def _load_json_fixture(
path: Path,
cli_prompt: str | None,
cli_region: str | None,
cli_enable_thinking: bool | None,
) -> InputSpec:
try:
fixture = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise SkillError(f"could not read JSON request {path}: {exc}") from exc
if not isinstance(fixture, dict):
raise SkillError(f"fixture must be a JSON object: {path}")
unknown = fixture.keys() - {
"volume_path",
"ct_path",
"prompt",
"case_id",
"anatomy_region",
"enable_thinking",
}
if unknown:
raise SkillError(f"unsupported request fields: {', '.join(sorted(unknown))}")
volume_value = str(fixture.get("volume_path") or fixture.get("ct_path") or "").strip()
region = _normalize_region(
cli_region if cli_region is not None else fixture.get("anatomy_region")
)
prompt = cli_prompt or str(fixture.get("prompt") or _default_prompt(region)).strip()
if not prompt:
raise SkillError("prompt must not be empty")
enable_thinking = (
cli_enable_thinking
if cli_enable_thinking is not None
else _fixture_bool(fixture, "enable_thinking", True)
)
case_id = str(fixture.get("case_id") or path.stem).strip()
if not case_id:
raise SkillError("case_id must not be empty")
if volume_value:
volume_path = Path(volume_value)
if not volume_path.is_absolute():
volume_path = (path.parent / volume_path).resolve()
source = "fixture_file"
else:
raise SkillError("request must include volume_path pointing to an existing NIfTI volume")
return InputSpec(
volume_path=volume_path,
source=source,
prompt=prompt,
case_id=case_id,
anatomy_region=region,
enable_thinking=enable_thinking,
)
def _load_input(
path: Path,
cli_prompt: str | None,
cli_region: str | None,
cli_enable_thinking: bool | None,
) -> InputSpec:
if not path.exists():
raise SkillError(f"input not found: {path}")
if path.suffix.lower() == ".json":
return _load_json_fixture(
path,
cli_prompt,
cli_region,
cli_enable_thinking,
)
region = _normalize_region(cli_region)
prompt = str(cli_prompt or _default_prompt(region)).strip()
if not prompt:
raise SkillError("prompt must not be empty")
enable_thinking = True if cli_enable_thinking is None else cli_enable_thinking
return InputSpec(
volume_path=path.resolve(),
source="file",
prompt=prompt,
case_id=path.name.removesuffix(".gz").removesuffix(".nii"),
anatomy_region=region,
enable_thinking=enable_thinking,
)
def _select_cuda_device() -> str:
try:
import torch
except Exception as exc:
raise SkillError(f"live inference requires torch: {exc}") from exc
if not torch.cuda.is_available():
raise SkillError("CUDA is unavailable for NV-Reason-CT inference. Use a CUDA host.")
try:
if not torch.cuda.is_bf16_supported():
raise SkillError("the visible CUDA device does not report bfloat16 support")
except AttributeError as exc:
raise SkillError("installed torch cannot verify CUDA bfloat16 support") from exc
return "cuda"
def _model_location(model_id: str, revision: str | None) -> tuple[str, str | None]:
"""A local export has no Hub revision; never send its path to Hub APIs."""
if not model_id.strip():
raise SkillError("--model must not be empty")
path = Path(model_id).expanduser()
if path.exists():
if not path.is_dir():
raise SkillError(f"local model path is not a directory: {path}")
if revision is not None:
raise SkillError("--revision applies only to Hub models, not local exports")
return str(path.resolve()), None
if model_id.startswith(("/", ".", "~")) or len(path.parts) > 2:
raise SkillError(f"local model directory not found: {path}")
revision = (
revision
if revision is not None
else os.environ.get("NV_REASON_CT_REVISION", DEFAULT_REVISION)
)
if not revision.strip():
raise SkillError("--revision must not be empty")
return model_id, revision
def _resolve_model_revision(
model_id: str, revision: str, *, local_files_only: bool, token: str | None
) -> str:
"""Resolve a Hub ref once, without executing any model-provided code."""
try:
from huggingface_hub import hf_hub_download
config_path = Path(
hf_hub_download(
model_id,
"config.json",
revision=revision,
local_files_only=local_files_only,
token=token,
)
)
except Exception as exc:
raise SkillError(f"could not resolve model revision {revision!r}: {exc}") from exc
# Keep the snapshot path, not Path.resolve(): the file is a symlink to a blob.
commit = config_path.parent.name
if config_path.parent.parent.name != "snapshots" or not re.fullmatch(
HUB_COMMIT_PATTERN, commit
):
raise SkillError("could not identify the immutable Hub snapshot for config.json")
return commit
def _require_model_code_consent(trust_model_code: bool) -> None:
if trust_model_code is not True:
raise SkillError(
"model loading requires executing custom Python code with this "
"process's permissions, including for local exports. Review and "
"authorize the selected model/processor code, then pass "
"--trust-model-code explicitly. Use --check-setup for inspection "
"without loading model code."
)
def _run_transformers_inference(
*,
volume_path: Path,
prompt: str,
anatomy_region: str,
enable_thinking: bool,
model_id: str,
revision: str | None,
max_new_tokens: int,
local_files_only: bool,
trust_model_code: bool = False,
) -> tuple[str, dict[str, Any]]:
# Also guard direct Python callers before imports, Hub lookup, or loading.
_require_model_code_consent(trust_model_code)
try:
import torch
import transformers
from transformers import AutoModelForImageTextToText, AutoProcessor
except Exception as exc:
raise SkillError(
"live inference requires the upstream inference dependencies. "
f"Follow {UPSTREAM_SETUP_URL} in a fresh environment and run "
f"--check-setup first. Import error: {exc}"
) from exc
device = _select_cuda_device()
processor_region = None if anatomy_region == "none" else anatomy_region
resolved_revision = None
load_options: dict[str, Any] = {"local_files_only": True}
if revision is not None:
token = os.environ.get("HF_TOKEN") or None
resolved_revision = _resolve_model_revision(
model_id, revision, local_files_only=local_files_only, token=token
)
load_options.update(
local_files_only=local_files_only,
token=token,
)
source = (
f"Hub model {model_id!r} at commit {resolved_revision}"
if revision is not None
else f"local export {model_id!r}"
)
print(
f"warning: --trust-model-code authorizes executing custom model and "
f"processor Python code from {source} with this process's permissions. "
"This is not sandboxed; a commit hash or local path does not establish "
"code safety.",
file=sys.stderr,
flush=True,
)
try:
model = (
AutoModelForImageTextToText.from_pretrained(
model_id,
trust_remote_code=trust_model_code,
# An immutable commit for Hub assets; None for a local export.
revision=resolved_revision,
code_revision=resolved_revision,
dtype=torch.bfloat16,
attn_implementation="sdpa",
**load_options,
)
.eval()
.to(device)
)
processor = AutoProcessor.from_pretrained(
model_id,
trust_remote_code=trust_model_code,
revision=resolved_revision,
code_revision=resolved_revision,
**load_options,
)
messages = [
{
"role": "user",
"content": [
{"type": "image"},
{"type": "text", "text": prompt},
],
}
]
formatted_prompt = processor.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=enable_thinking,
)
inputs = processor(
text=formatted_prompt,
images3d=[str(volume_path)],
anatomy_region=processor_region,
return_tensors="pt",
).to(model.device)
with torch.inference_mode():
generated_ids = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
use_cache=True,
)
new_tokens = generated_ids[:, inputs.input_ids.shape[1] :]
generated_tokens = int(new_tokens.shape[-1])
eos_ids = model.generation_config.eos_token_id
eos_ids = [eos_ids] if isinstance(eos_ids, int) else (eos_ids or [])
ended_with_eos = bool(generated_tokens and int(new_tokens[0, -1].item()) in eos_ids)
response_text = processor.batch_decode(
new_tokens,
skip_special_tokens=True,
clean_up_tokenization_spaces=False,
)[0]
except Exception as exc:
raise SkillError(f"NV-Reason-CT inference failed: {exc}") from exc
if not response_text.strip():
raise SkillError("NV-Reason-CT returned empty text")
return response_text, {
"resolved_revision": resolved_revision,
"device": str(getattr(model, "device", device)),
"torch_dtype": "bfloat16",
"transformers_version": getattr(transformers, "__version__", None),
"torch_version": getattr(torch, "__version__", None),
"generated_tokens": generated_tokens,
"truncated_by_max_new_tokens": (generated_tokens >= max_new_tokens and not ended_with_eos),
}
def _environment_packages() -> dict[str, str]:
return {
name: version
for name in ENVIRONMENT_PACKAGES
if (version := _installed_version(name)) is not None
}
def _preserve_partial_result(payload: dict[str, Any], out_dir: Path) -> None:
"""Retain partial JSON even when a caller discards stdout on nonzero exit."""
try:
out_dir = out_dir.resolve()
out_dir.mkdir(parents=True, exist_ok=True)
# A unique name preserves results from earlier attempts in the same out-dir.
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
prefix="partial_result_",
suffix=".json",
dir=out_dir,
delete=False,
) as stream:
payload["output"]["partial_json_path"] = stream.name
json.dump(payload, stream, indent=2, sort_keys=True)
stream.write("\n")
except OSError as exc:
payload["output"].pop("partial_json_path", None)
print(
f"warning: could not save partial JSON; retain stdout: {exc}",
file=sys.stderr,
)
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run NV-Reason-CT inference on one 3D NIfTI CT volume."
)
parser.add_argument(
"volume_or_fixture",
nargs="?",
type=Path,
help="Existing .nii/.nii.gz volume, or JSON request pointing to one.",
)
parser.add_argument("--prompt", default=None, help="Text prompt for the model.")
parser.add_argument(
"--anatomy-region",
choices=ANATOMY_REGIONS,
default=None,
help="Chest/abdomen crop, or none for a manually cropped volume.",
)
thinking = parser.add_mutually_exclusive_group()
thinking.add_argument(
"--thinking",
dest="enable_thinking",
action="store_true",
help="Request reviewable reasoning text (default).",
)
thinking.add_argument(
"--no-thinking",
dest="enable_thinking",
action="store_false",
help="Disable thinking output for concise responses.",
)
parser.set_defaults(enable_thinking=None)
parser.add_argument("--out-dir", type=Path, default=Path("runs/nv_reason_ct"))
parser.add_argument(
"--model",
"--model-id",
dest="model_id",
default=os.environ.get("NV_REASON_CT_MODEL", DEFAULT_MODEL),
help=f"Hub model id or complete local model/processor export (default: {DEFAULT_MODEL}).",
)
parser.add_argument(
"--revision",
help="Hugging Face model revision (default: main).",
)
parser.add_argument("--max-new-tokens", type=int, default=DEFAULT_MAX_NEW_TOKENS)
parser.add_argument("--local-files-only", action="store_true")
parser.add_argument(
"--trust-model-code",
action="store_true",
help="Authorize unsandboxed custom model/processor Python code (Hub or local); required for inference.",
)
parser.add_argument(
"--check-setup",
action="store_true",
help="Print dependency/GPU/cache readiness JSON and exit.",
)
parser.add_argument(
"--fail-on-not-ready",
action="store_true",
help="With --check-setup, exit 1 unless live CUDA inference is ready.",
)
return parser
def main(argv: list[str] | None = None) -> int:
args = _build_parser().parse_args(argv)
if args.fail_on_not_ready and not args.check_setup:
print("error: --fail-on-not-ready requires --check-setup", file=sys.stderr)
return 2
if args.max_new_tokens < 1:
print("error: --max-new-tokens must be >= 1", file=sys.stderr)
return 2
try:
model_id, revision = _model_location(args.model_id, args.revision)
if args.check_setup:
report = _setup_report(model_id, revision)
print(json.dumps(report, indent=2, sort_keys=True))
not_ready = report["setup"]["recommendation"] != "ready_for_live_cuda_inference"
return 1 if args.fail_on_not_ready and not_ready else 0
if args.volume_or_fixture is None:
print(
"error: volume_or_fixture is required unless --check-setup is used",
file=sys.stderr,
)
return 2
_require_model_code_consent(args.trust_model_code)
spec = _load_input(
args.volume_or_fixture.resolve(),
args.prompt,
args.anatomy_region,
args.enable_thinking,
)
info = _volume_info(spec.volume_path)
local_files_only = bool(
args.local_files_only
or revision is None
or _truthy(os.environ.get("TRANSFORMERS_OFFLINE"))
or _truthy(os.environ.get("HF_HUB_OFFLINE"))
)
started = time.perf_counter()
response_text, runtime_extra = _run_transformers_inference(
volume_path=spec.volume_path,
prompt=spec.prompt,
anatomy_region=spec.anatomy_region,
enable_thinking=spec.enable_thinking,
model_id=model_id,
revision=revision,
max_new_tokens=args.max_new_tokens,
local_files_only=local_files_only,
trust_model_code=args.trust_model_code,
)
elapsed = time.perf_counter() - started
payload = {
"skill": "nv_reason_ct",
"input": {
"case_id": spec.case_id,
"prompt": spec.prompt,
"anatomy_region": spec.anatomy_region,
"enable_thinking": spec.enable_thinking,
"volume": {
"path": str(spec.volume_path),
"source": spec.source,
"format": "nifti",
"ndim": 3,
"shape": list(info.shape),
"spacing_mm": [round(value, 6) for value in info.spacing_mm],
"dtype": info.dtype,
"sha256": info.sha256,
},
},
"output": {
"response_text": response_text,
"text_chars": len(response_text),
},
"runtime": {
"model": model_id,
"model_source": "local" if revision is None else "hub",
"revision": revision,
"resolved_revision": runtime_extra["resolved_revision"],
"mode": "hf_transformers",
"mock": False,
"device": str(runtime_extra["device"]),
"torch_dtype": str(runtime_extra["torch_dtype"]),
"attention_implementation": "sdpa",
"trust_remote_code": True,
"deterministic_generation": True,
"max_new_tokens": args.max_new_tokens,
"inference_seconds": round(elapsed, 6),
"local_files_only": local_files_only,
"transformers_version": runtime_extra["transformers_version"],
"torch_version": runtime_extra["torch_version"],
"generated_tokens": runtime_extra["generated_tokens"],
"truncated_by_max_new_tokens": runtime_extra["truncated_by_max_new_tokens"],
},
"environment": {"packages": _environment_packages()},
"limitations": LIMITATIONS,
}
if runtime_extra["truncated_by_max_new_tokens"]:
_preserve_partial_result(payload, args.out_dir)
print(json.dumps(payload, indent=2, sort_keys=True))
if runtime_extra["truncated_by_max_new_tokens"]:
print(
"error: generation reached --max-new-tokens without EOS; partial "
"output is preserved in stdout JSON. Review it before retrying "
"with a larger token limit.",
file=sys.stderr,
)
if "partial_json_path" in payload["output"]:
print(
f"partial JSON: {payload['output']['partial_json_path']}",
file=sys.stderr,
)
return 3
return 0
except (SkillError, OSError, UnicodeError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs Python
Security
What our scanner noticed· Medium risk
- Large base64-encoded blobs
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- NVIDIA MedTech
- Publisher
- NVIDIA (Official)
- Source
- NVIDIA/skills · skills/nv-reason-ct · commit fce3cca
- Licence
- Apache License 2.0 (redistribution allowed)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by NVIDIA MedTech or NVIDIA.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download nv-reason-ct.zip