[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""model-support-checker package: SGLang/vLLM model support checker."""
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Step 1: resolve a model's architecture name from its config.json."""
|
||||
|
||||
import json
|
||||
|
||||
from .http_utils import _get, _get_json
|
||||
|
||||
|
||||
def get_architecture(model_id, source="modelscope", arch_override=None):
|
||||
"""Return (architectures: list[str], source_used: str).
|
||||
|
||||
If *arch_override* is given (comma-separated architecture string), return it
|
||||
directly without fetching config.json. This allows the caller to bypass
|
||||
the network lookup when the config is unavailable or the user already knows
|
||||
the architecture name.
|
||||
"""
|
||||
if arch_override:
|
||||
archs = [a.strip() for a in arch_override.split(",") if a.strip()]
|
||||
if archs:
|
||||
return archs, "manual"
|
||||
|
||||
if source in ("auto", "hf"):
|
||||
url = f"https://huggingface.co/{model_id}/resolve/main/config.json"
|
||||
try:
|
||||
body, _ = _get(url)
|
||||
except RuntimeError:
|
||||
body = None
|
||||
if body:
|
||||
try:
|
||||
data = json.loads(body)
|
||||
if data.get("architectures"):
|
||||
return data["architectures"], "huggingface"
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if source in ("auto", "modelscope"):
|
||||
url = f"https://modelscope.cn/models/{model_id}/resolve/master/config.json"
|
||||
try:
|
||||
body, _ = _get(url)
|
||||
except RuntimeError:
|
||||
body = None
|
||||
if body:
|
||||
try:
|
||||
data = json.loads(body)
|
||||
if data.get("architectures"):
|
||||
return data["architectures"], "modelscope"
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
api = f"https://modelscope.cn/api/v1/models/{model_id}"
|
||||
data = _get_json(api)
|
||||
if data:
|
||||
arch = data.get("Architectures") or data.get("architectures")
|
||||
if arch:
|
||||
return [arch] if isinstance(arch, str) else arch, "modelscope-api"
|
||||
|
||||
raise RuntimeError(
|
||||
f"Could not fetch config.json for '{model_id}' from HuggingFace or ModelScope.\n"
|
||||
f"Try: --source hf | --source modelscope (pick a specific platform)\n"
|
||||
f" or: --arch <ArchitectureName> (e.g. --arch LlamaForCausalLM)"
|
||||
)
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check whether a HuggingFace / ModelScope model is supported by SGLang, vLLM, or vLLM-Ascend,
|
||||
and since which version.
|
||||
|
||||
Usage:
|
||||
python3 main.py --setup local # first run: clone repos into .state/repos/ (recommended)
|
||||
python3 main.py --setup token # first run: use GitHub API (GITHUB_TOKEN per run)
|
||||
python3 main.py --doctor # check setup state and local checkouts
|
||||
GITHUB_TOKEN=xxx python3 main.py --framework sglang Qwen/Qwen3.6-35B-A3B
|
||||
python3 main.py --framework vllm deepseek-ai/DeepSeek-V3 # after setup
|
||||
python3 main.py --framework vllm-ascend deepseek-ai/DeepSeek-V3
|
||||
python3 main.py --arch LlamaForCausalLM # manual architecture (skip config.json fetch)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from . import state
|
||||
from .architecture import get_architecture
|
||||
from .framework_strategies import STRATEGIES
|
||||
|
||||
# Max seconds to wait for background checkout refreshes before exiting.
|
||||
_REFRESH_JOIN_SECONDS = 15
|
||||
|
||||
# Framework names that can be used with --framework
|
||||
FRAMEWORK_CHOICES = ["sglang", "vllm", "vllm-ascend", "all"]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Check SGLang/vLLM/vLLM-Ascend model support.")
|
||||
ap.add_argument("model_id", nargs="?", help="e.g. Qwen/Qwen3.6-35B-A3B (optional with --arch)")
|
||||
ap.add_argument("--framework", choices=FRAMEWORK_CHOICES, default="all",
|
||||
help="which framework to check (default: all)")
|
||||
ap.add_argument("--source", choices=["auto", "hf", "modelscope"],
|
||||
default="modelscope", help="where to read config.json")
|
||||
ap.add_argument("--arch", default=None,
|
||||
help="manually specify architecture name(s), comma-separated "
|
||||
"(e.g. LlamaForCausalLM). Skips config.json fetch.")
|
||||
ap.add_argument("--token", default=os.environ.get("GITHUB_TOKEN"),
|
||||
help="GitHub token (or set GITHUB_TOKEN env)")
|
||||
ap.add_argument("--no-docs", action="store_true", help="skip docs check")
|
||||
ap.add_argument("--vllm-ref", default=None,
|
||||
help="vLLM git ref (branch/tag) for registry check (default: main)")
|
||||
ap.add_argument("--vllm-path", default=None,
|
||||
help="local vllm repo checkout path (skip GitHub API)")
|
||||
ap.add_argument("--sglang-path", default=None,
|
||||
help="local sglang repo checkout path (skip GitHub API)")
|
||||
ap.add_argument("--vllm-ascend-path", default=None,
|
||||
help="local vllm-ascend repo checkout path (skip GitHub API)")
|
||||
ap.add_argument("--setup", choices=["local", "token"], default=None,
|
||||
help="first-run setup: 'local' clones repos (recommended), "
|
||||
"'token' uses the GitHub API. Saved to .state/state.json.")
|
||||
ap.add_argument("--reset-state", action="store_true",
|
||||
help="forget saved setup state")
|
||||
ap.add_argument("--doctor", action="store_true",
|
||||
help="check setup state and exit")
|
||||
ap.add_argument("-v", "--verbose", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.reset_state:
|
||||
state.reset()
|
||||
if not args.setup:
|
||||
return
|
||||
if args.setup:
|
||||
state.ensure_setup(args.setup, args)
|
||||
return
|
||||
|
||||
# Resolve setup state: first run asks the user (TTY menu) or tells the
|
||||
# caller to run --setup. With --doctor we skip the interactive menu and
|
||||
# just report whatever is (or isn't) on disk.
|
||||
st = state.load()
|
||||
if st is None and args.doctor:
|
||||
has_errors = state.print_report(state.doctor(st, args, framework=args.framework))
|
||||
sys.exit(1 if has_errors else 0)
|
||||
|
||||
if st is None:
|
||||
if sys.stdin.isatty():
|
||||
state.ensure_setup(state.first_run_menu(), args)
|
||||
st = state.load()
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"First run: no setup found (.state/state.json missing). Ask the "
|
||||
"user to choose, then run one of:\n"
|
||||
" python3 main.py --setup local # recommended\n"
|
||||
" python3 main.py --setup local --vllm-path P1 --sglang-path P2 --vllm-ascend-path P3\n"
|
||||
" python3 main.py --setup token # needs GITHUB_TOKEN"
|
||||
)
|
||||
|
||||
# The agent is expected to run `python3 main.py --doctor` explicitly
|
||||
# before any model check (see SKILL.md workflow). When --doctor is passed
|
||||
# directly, we inspect state and exit without touching a model.
|
||||
if args.doctor:
|
||||
issues = state.doctor(st, args, framework=args.framework)
|
||||
has_errors = state.print_report(issues)
|
||||
sys.exit(1 if has_errors else 0)
|
||||
|
||||
# Build path mapping: framework name -> local path
|
||||
paths = {
|
||||
"vllm": args.vllm_path or st.get("vllm_path"),
|
||||
"sglang": args.sglang_path or st.get("sglang_path"),
|
||||
"vllm-ascend": args.vllm_ascend_path or st.get("vllm_ascend_path"),
|
||||
}
|
||||
|
||||
if not args.model_id and not args.arch:
|
||||
ap.error("model_id is required (unless --arch is specified)")
|
||||
|
||||
frameworks = list(STRATEGIES) if args.framework == "all" else [args.framework]
|
||||
|
||||
# Local mode: refresh checkouts in the background while checking runs;
|
||||
# failures/staleness are only reported at the very end, never blocking.
|
||||
local_paths = {}
|
||||
for fw_name in frameworks:
|
||||
lp = paths.get(fw_name)
|
||||
if lp and os.path.isdir(os.path.join(lp, ".git")):
|
||||
local_paths[fw_name] = lp
|
||||
freshness, refresh_threads = state.start_refresh(local_paths)
|
||||
|
||||
# Step 1 (framework-independent): architecture name.
|
||||
archs, src = get_architecture(args.model_id or "", args.source,
|
||||
arch_override=args.arch)
|
||||
arch = archs[0]
|
||||
model_display = args.model_id or "(manual)"
|
||||
print(f"Model: {model_display}")
|
||||
print(f"[1] Architecture(s): {', '.join(archs)} (source: {src})\n")
|
||||
|
||||
results = {}
|
||||
|
||||
for fw_name in frameworks:
|
||||
local_path = paths.get(fw_name)
|
||||
strategy = STRATEGIES[fw_name]
|
||||
ctx = strategy.make_context(local_path)
|
||||
|
||||
print(f"========== {strategy.label} ==========")
|
||||
if ctx.is_local:
|
||||
print(f" (using local checkout: {ctx.local_dir})")
|
||||
|
||||
# Step 3 (authoritative)
|
||||
print("[3] GitHub source check (authoritative)...")
|
||||
supported, file_path = strategy.check_support(arch, ctx, args.token, args.verbose)
|
||||
print(f" Supported in source: {'YES' if supported else 'NO'}"
|
||||
+ (f" ({file_path})" if file_path else ""))
|
||||
|
||||
# Framework-specific extra checks (e.g. vLLM registry)
|
||||
extra = strategy.extra_checks(arch, ctx, args.token, args.verbose, ref=args.vllm_ref)
|
||||
|
||||
# Step 2 (docs, supplementary)
|
||||
docs = "skipped"
|
||||
if not args.no_docs:
|
||||
print("[2] Official docs check...")
|
||||
docs = strategy.check_docs(arch, args.verbose)
|
||||
print(f" Docs mention: {docs}")
|
||||
|
||||
# Step 4 (version)
|
||||
version = None
|
||||
if supported:
|
||||
print(f"[4] Determining first supporting {strategy.label} version...")
|
||||
version = strategy.get_version(strategy.version_path(file_path), ctx, args.token, args.verbose)
|
||||
print(f" First version: {version}")
|
||||
else:
|
||||
print("[4] Skipped (not found in source).")
|
||||
|
||||
results[fw_name] = (supported, file_path, version, docs, extra, strategy)
|
||||
print()
|
||||
|
||||
# Final combined summary.
|
||||
print("=== Summary ===")
|
||||
print(f" Model : {model_display}")
|
||||
print(f" Architecture : {arch}")
|
||||
for fw_name in frameworks:
|
||||
supported, file_path, version, docs, extra, strategy = results[fw_name]
|
||||
line = f" {strategy.label:12} supported : {'YES' if supported else 'NO'}"
|
||||
if supported:
|
||||
line += f" | since {version} | {file_path}"
|
||||
print(line)
|
||||
strategy.format_summary_extra(extra)
|
||||
print("\nNote: GitHub source is the authoritative signal for all frameworks.")
|
||||
print("The 'since version' is approximated from the implementation file's first")
|
||||
print("commit date and may be off by a release or two; it needs GITHUB_TOKEN")
|
||||
print("on rate-limited IPs.")
|
||||
|
||||
_print_refresh_notes(freshness, refresh_threads, args.verbose)
|
||||
|
||||
|
||||
def _print_refresh_notes(freshness, threads, verbose=False):
|
||||
"""Report background checkout refresh results after the main flow.
|
||||
|
||||
Waits at most _REFRESH_JOIN_SECONDS total so a slow fetch never blocks
|
||||
the exit for long; unfinished workers are simply not reported.
|
||||
"""
|
||||
deadline = time.monotonic() + _REFRESH_JOIN_SECONDS
|
||||
for t in threads:
|
||||
t.join(timeout=max(0.0, deadline - time.monotonic()))
|
||||
notes = []
|
||||
for fw_name, info in sorted(freshness.items()):
|
||||
label = STRATEGIES[fw_name].label
|
||||
if "error" in info:
|
||||
notes.append(f"{label}: checkout refresh failed ({info['error']}); "
|
||||
f"results reflect the checkout as-is.")
|
||||
elif info.get("behind"):
|
||||
notes.append(f"{label}: checkout is {info['behind']} commit(s) behind "
|
||||
f"its upstream — update with: git -C {info['path']} pull")
|
||||
elif verbose and not info.get("no_upstream"):
|
||||
notes.append(f"{label}: checkout is up to date.")
|
||||
if notes:
|
||||
print("\n=== Checkout freshness ===")
|
||||
for n in notes:
|
||||
print(f" NOTE: {n}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except RuntimeError as e:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Execution context for framework checks.
|
||||
|
||||
Replaces the global mutable state in config.py. Each check runs with its own
|
||||
Context, making the code concurrency-safe and easier to test.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from .http_utils import _get
|
||||
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
"""Immutable context for a single framework check."""
|
||||
|
||||
repo: str
|
||||
branch: str
|
||||
models_dir: str
|
||||
docs_url: str
|
||||
label: str
|
||||
local_dir: Optional[str] = None
|
||||
|
||||
@property
|
||||
def api(self) -> str:
|
||||
return f"https://api.github.com/repos/{self.repo}"
|
||||
|
||||
@property
|
||||
def raw(self) -> str:
|
||||
return f"https://raw.githubusercontent.com/{self.repo}/{self.branch}"
|
||||
|
||||
@property
|
||||
def is_local(self) -> bool:
|
||||
return self.local_dir is not None
|
||||
|
||||
def read_source(self, rel_path: str) -> Optional[str]:
|
||||
"""Read a repository-relative file.
|
||||
|
||||
Local mode: read from disk.
|
||||
Remote mode: fetch from GitHub raw CDN.
|
||||
Returns None on failure.
|
||||
"""
|
||||
if self.is_local:
|
||||
p = os.path.join(self.local_dir, rel_path)
|
||||
try:
|
||||
with open(p, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except OSError:
|
||||
return None
|
||||
try:
|
||||
body, _ = _get(f"{self.raw}/{rel_path}")
|
||||
except RuntimeError:
|
||||
return None
|
||||
return body
|
||||
|
||||
@classmethod
|
||||
def from_strategy(cls, strategy, local_dir: Optional[str] = None) -> "Context":
|
||||
"""Create a Context from a strategy instance."""
|
||||
return cls(
|
||||
repo=strategy.repo,
|
||||
branch=strategy.branch,
|
||||
models_dir=strategy.models_dir,
|
||||
docs_url=strategy.docs_url,
|
||||
label=strategy.label,
|
||||
local_dir=os.path.expanduser(local_dir) if local_dir else None,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Framework strategies: each framework is a strategy instance."""
|
||||
|
||||
from .base import FrameworkStrategy
|
||||
from .sglang import SglangStrategy
|
||||
from .vllm import VllmStrategy
|
||||
from .vllm_ascend import VllmAscendStrategy
|
||||
|
||||
STRATEGIES = {s.name: s() for s in (VllmStrategy, SglangStrategy, VllmAscendStrategy)}
|
||||
|
||||
__all__ = ["FrameworkStrategy", "STRATEGIES"]
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Base strategy class for framework-specific behavior."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from ..context import Context
|
||||
from ..http_utils import BROWSER_UA, _get, _get_json
|
||||
|
||||
|
||||
class FrameworkStrategy:
|
||||
name = None
|
||||
label = None
|
||||
repo = None
|
||||
branch = None
|
||||
models_dir = None
|
||||
docs_url = None
|
||||
|
||||
def make_context(self, local_dir: Optional[str] = None) -> Context:
|
||||
return Context.from_strategy(self, local_dir)
|
||||
|
||||
def check_docs(self, arch: str, verbose: bool = False) -> str:
|
||||
body, _ = _get(self.docs_url)
|
||||
if not body:
|
||||
if verbose:
|
||||
print(f" [docs] could not fetch {self.docs_url} (may be JS-rendered).")
|
||||
return "unknown"
|
||||
if arch in body or arch.lower() in body.lower():
|
||||
return "yes"
|
||||
family = re.split(r"(?=[A-Z])", arch)[0].lower()
|
||||
if family and family in body.lower():
|
||||
return "maybe"
|
||||
return "no"
|
||||
|
||||
def check_support(
|
||||
self, arch: str, ctx: Context, token: Optional[str] = None, verbose: bool = False
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
if ctx.is_local:
|
||||
return self._check_local(arch, ctx, verbose)
|
||||
|
||||
if token:
|
||||
inner = "^" + ctx.models_dir.replace("/", r"\/") + r"\/"
|
||||
q = f"{arch} repo:{ctx.repo} path:/{inner}/"
|
||||
url = f"{ctx.api}/search/code?q={urllib.parse.quote(q)}&per_page=20"
|
||||
data = _get_json(url, token)
|
||||
if data:
|
||||
for item in data.get("items", []):
|
||||
if item["path"].startswith(ctx.models_dir) and item["path"].endswith(".py"):
|
||||
if verbose:
|
||||
print(f" [github] code-search hit: {item['path']}")
|
||||
return True, item["path"]
|
||||
elif verbose:
|
||||
print(" [github] code search returned nothing / was rate-limited.")
|
||||
|
||||
if not token and verbose:
|
||||
print(" [github] no token: using best-effort tree scrape "
|
||||
"(may be incomplete; set GITHUB_TOKEN for a definitive answer).")
|
||||
files = self.list_model_files(ctx, verbose)
|
||||
candidates = self.match_files(arch, files) if files else []
|
||||
if verbose:
|
||||
print(" [github] fallback candidates: " + ", ".join(candidates) or "(none)")
|
||||
for fname in candidates:
|
||||
path = f"{ctx.models_dir}/{fname}"
|
||||
body = ctx.read_source(path)
|
||||
if body and arch in body:
|
||||
if verbose:
|
||||
print(f" [github] '{arch}' found in {path}")
|
||||
return True, path
|
||||
return False, None
|
||||
|
||||
def get_version(
|
||||
self, file_path: str, ctx: Context, token: Optional[str] = None, verbose: bool = False
|
||||
) -> Optional[str]:
|
||||
if not file_path:
|
||||
return None
|
||||
if ctx.is_local:
|
||||
return self._get_version_local(file_path, ctx, verbose)
|
||||
try:
|
||||
date = self._earliest_commit_date(file_path, ctx, token)
|
||||
except RuntimeError as e:
|
||||
return f"unknown ({e}; try GITHUB_TOKEN)"
|
||||
if verbose:
|
||||
print(f" [version] earliest commit touching {file_path}: {date}")
|
||||
if not date:
|
||||
return "unknown"
|
||||
try:
|
||||
return self._first_release_after(date, ctx, token)
|
||||
except RuntimeError as e:
|
||||
return f"unknown ({e}; try GITHUB_TOKEN)"
|
||||
|
||||
def version_path(self, file_path: Optional[str]) -> Optional[str]:
|
||||
return file_path
|
||||
|
||||
def extra_checks(self, arch: str, ctx: Context, token: Optional[str], verbose: bool, ref: Optional[str] = None):
|
||||
return None
|
||||
|
||||
def format_summary_extra(self, info) -> None:
|
||||
pass
|
||||
|
||||
def list_model_files(self, ctx: Context, verbose: bool = False) -> Optional[set]:
|
||||
if ctx.is_local:
|
||||
d = os.path.join(ctx.local_dir, ctx.models_dir)
|
||||
try:
|
||||
names = {f for f in os.listdir(d) if f.endswith(".py")}
|
||||
except OSError:
|
||||
if verbose:
|
||||
print(f" [local] could not list models directory {d}.")
|
||||
return None
|
||||
names.discard("__init__.py")
|
||||
if names:
|
||||
if verbose:
|
||||
print(f" [local] listed {len(names)} model files from {d}.")
|
||||
return names
|
||||
return None
|
||||
|
||||
url = f"{ctx.api}/contents/{ctx.models_dir}"
|
||||
data = _get_json(url)
|
||||
if data:
|
||||
names = {
|
||||
e["name"]
|
||||
for e in data
|
||||
if e.get("type") == "file" and e["name"].endswith(".py")
|
||||
}
|
||||
names.discard("__init__.py")
|
||||
if names:
|
||||
if verbose:
|
||||
print(f" [github] listed {len(names)} model files via Contents API.")
|
||||
return names
|
||||
|
||||
if verbose:
|
||||
print(" [github] Contents API unavailable; scraping tree page.")
|
||||
turl = f"https://github.com/{ctx.repo}/tree/{ctx.branch}/{ctx.models_dir}"
|
||||
try:
|
||||
body, _ = _get(turl, ua=BROWSER_UA)
|
||||
except RuntimeError:
|
||||
body = None
|
||||
if not body:
|
||||
if verbose:
|
||||
print(" [github] could not fetch models directory listing.")
|
||||
return None
|
||||
names = set(re.findall(r'"name":"([a-zA-Z0-9_]+\.py)"', body))
|
||||
names.discard("__init__.py")
|
||||
return names
|
||||
|
||||
@staticmethod
|
||||
def match_files(arch: str, files: set) -> list:
|
||||
a = arch.lower().replace("_", "")
|
||||
fam = re.match(r"[A-Za-z]+", arch)
|
||||
fam = fam.group(0).lower() if fam else ""
|
||||
out = []
|
||||
for f in files:
|
||||
if not f.endswith(".py"):
|
||||
continue
|
||||
stem = f[:-3].lower().replace("_", "")
|
||||
if (
|
||||
a.startswith(stem)
|
||||
or stem.startswith(a)
|
||||
or (fam and (stem.startswith(fam) or fam.startswith(stem)))
|
||||
):
|
||||
out.append(f)
|
||||
return sorted(out, key=len)
|
||||
|
||||
def _check_local(self, arch: str, ctx: Context, verbose: bool = False) -> Tuple[bool, Optional[str]]:
|
||||
files = self.list_model_files(ctx, verbose)
|
||||
candidates = self.match_files(arch, files) if files else []
|
||||
if verbose:
|
||||
print(" [local] candidates: " + (", ".join(candidates) or "(none)"))
|
||||
for fname in candidates:
|
||||
path = f"{ctx.models_dir}/{fname}"
|
||||
body = ctx.read_source(path)
|
||||
if body and arch in body:
|
||||
if verbose:
|
||||
print(f" [local] '{arch}' found in {path}")
|
||||
return True, path
|
||||
return False, None
|
||||
|
||||
def _earliest_commit_date(self, path: str, ctx: Context, token: Optional[str]) -> Optional[str]:
|
||||
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
||||
url = f"{ctx.api}/commits?path={urllib.parse.quote(path)}&per_page=1"
|
||||
try:
|
||||
body, hdrs = _get(url, headers)
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError(str(e)) from e
|
||||
if not body:
|
||||
return None
|
||||
link = hdrs.get("Link", "")
|
||||
last_page = 1
|
||||
for part in link.split(","):
|
||||
if 'rel="last"' in part:
|
||||
m = re.search(r"[?&]page=(\d+)", part)
|
||||
if m:
|
||||
last_page = int(m.group(1))
|
||||
if last_page > 1:
|
||||
try:
|
||||
body, _ = _get(f"{url}&page={last_page}", headers)
|
||||
except RuntimeError:
|
||||
body = None
|
||||
if not body:
|
||||
return None
|
||||
commits = json.loads(body)
|
||||
if not commits:
|
||||
return None
|
||||
commit = commits[-1]["commit"]
|
||||
return commit.get("committer", {}).get("date") or commit.get("author", {}).get("date")
|
||||
|
||||
def _first_release_after(self, date_iso: str, ctx: Context, token: Optional[str]) -> str:
|
||||
rels = _get_json(f"{ctx.api}/releases?per_page=100", token) or []
|
||||
picked = None
|
||||
for r in rels:
|
||||
pub = r.get("published_at")
|
||||
if not pub:
|
||||
continue
|
||||
if pub >= date_iso and (picked is None or pub < picked["published_at"]):
|
||||
picked = r
|
||||
return picked["tag_name"] if picked else "unreleased (main)"
|
||||
|
||||
def _get_version_local(self, file_path: str, ctx: Context, verbose: bool = False) -> str:
|
||||
repo = ctx.local_dir
|
||||
log = subprocess.run(
|
||||
["git", "-C", repo, "log", "--reverse", "--format=%aI", "--", file_path],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if log.returncode != 0 or not log.stdout.strip():
|
||||
if verbose:
|
||||
print(f" [version] no git history for {file_path}.")
|
||||
return "unknown"
|
||||
first_date = log.stdout.strip().splitlines()[0]
|
||||
if verbose:
|
||||
print(f" [version] earliest commit touching {file_path}: {first_date}")
|
||||
|
||||
tags = subprocess.run(
|
||||
["git", "-C", repo, "for-each-ref", "--sort=creatordate",
|
||||
"--format=%(creatordate:iso-strict) %(refname:short)", "refs/tags"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if tags.returncode != 0:
|
||||
return "unreleased (main)"
|
||||
picked = None
|
||||
for line in tags.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
date, tag = line.split(" ", 1)
|
||||
if date >= first_date and (picked is None or date < picked[0]):
|
||||
picked = (date, tag)
|
||||
return picked[1] if picked else "unreleased (main)"
|
||||
@@ -0,0 +1,12 @@
|
||||
"""SGLang strategy."""
|
||||
|
||||
from .base import FrameworkStrategy
|
||||
|
||||
|
||||
class SglangStrategy(FrameworkStrategy):
|
||||
name = "sglang"
|
||||
label = "SGLang"
|
||||
repo = "sgl-project/sglang"
|
||||
branch = "main"
|
||||
models_dir = "python/sglang/srt/models"
|
||||
docs_url = "https://docs.sglang.io/docs/supported-models"
|
||||
@@ -0,0 +1,10 @@
|
||||
"""vLLM framework strategy."""
|
||||
|
||||
from .registry import (
|
||||
check_vllm_registry,
|
||||
fetch_vllm_registry,
|
||||
parse_vllm_registry,
|
||||
)
|
||||
from .strategy import VllmStrategy
|
||||
|
||||
__all__ = ["VllmStrategy", "fetch_vllm_registry", "parse_vllm_registry", "check_vllm_registry"]
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
"""vLLM registry.py source-level parsing (category, module, class, etc.)."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from ...context import Context
|
||||
from ...http_utils import _get
|
||||
|
||||
_REGISTRY_PATH = "vllm/model_executor/models/registry.py"
|
||||
|
||||
_CATEGORY_MAP = {
|
||||
"_TEXT_GENERATION_MODELS": "text_generation",
|
||||
"_EMBEDDING_MODELS": "embedding",
|
||||
"_LATE_INTERACTION_MODELS": "late_interaction",
|
||||
"_REWARD_MODELS": "reward",
|
||||
"_TOKEN_CLASSIFICATION_MODELS": "token_classification",
|
||||
"_SEQUENCE_CLASSIFICATION_MODELS": "sequence_classification",
|
||||
"_MULTIMODAL_MODELS": "multimodal",
|
||||
"_SPECULATIVE_DECODING_MODELS": "speculative_decoding",
|
||||
"_TRANSFORMERS_SUPPORTED_MODELS": "transformers_supported",
|
||||
"_TRANSFORMERS_BACKEND_MODELS": "transformers_backend",
|
||||
}
|
||||
|
||||
_BROWSER_UA = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _parse_dict_entries(source, dict_name):
|
||||
"""Extract architecture keys from a Python dict literal in source.
|
||||
|
||||
Returns dict[arch_name] = (module_str, class_str).
|
||||
"""
|
||||
pattern = rf"^{re.escape(dict_name)}\s*=\s*\{{"
|
||||
match = re.search(pattern, source, re.MULTILINE)
|
||||
if not match:
|
||||
return {}
|
||||
|
||||
start = match.end()
|
||||
depth = 1
|
||||
i = start
|
||||
while i < len(source) and depth > 0:
|
||||
ch = source[i]
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
elif ch in ("'", '"'):
|
||||
quote = ch
|
||||
i += 1
|
||||
while i < len(source):
|
||||
if source[i] == "\\":
|
||||
i += 2
|
||||
continue
|
||||
if source[i] == quote:
|
||||
break
|
||||
i += 1
|
||||
i += 1
|
||||
|
||||
body = source[start : i - 1]
|
||||
|
||||
entry_re = re.compile(
|
||||
r'''["'](\w+)["']\s*:\s*\(\s*["']([^"']+)["']\s*,\s*["']([^"']+)["']\s*,?\s*\)'''
|
||||
)
|
||||
result = {}
|
||||
for m in entry_re.finditer(body):
|
||||
result[m.group(1)] = (m.group(2), m.group(3))
|
||||
return result
|
||||
|
||||
|
||||
def _parse_simple_dict(source, dict_name):
|
||||
"""Extract entries from a simple dict: "Key": "Value"."""
|
||||
pattern = rf"^{re.escape(dict_name)}\s*=\s*\{{"
|
||||
match = re.search(pattern, source, re.MULTILINE)
|
||||
if not match:
|
||||
return {}
|
||||
|
||||
start = match.end()
|
||||
depth = 1
|
||||
i = start
|
||||
while i < len(source) and depth > 0:
|
||||
ch = source[i]
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
elif ch in ("'", '"'):
|
||||
quote = ch
|
||||
i += 1
|
||||
while i < len(source):
|
||||
if source[i] == "\\":
|
||||
i += 2
|
||||
continue
|
||||
if source[i] == quote:
|
||||
break
|
||||
i += 1
|
||||
i += 1
|
||||
|
||||
body = source[start : i - 1]
|
||||
|
||||
entry_re = re.compile(r'''["'](\w+)["']\s*:\s*["']([^"']+)["']''')
|
||||
result = {}
|
||||
for m in entry_re.finditer(body):
|
||||
result[m.group(1)] = m.group(2)
|
||||
return result
|
||||
|
||||
|
||||
def fetch_vllm_registry(ctx: Context, ref: Optional[str] = None, verbose: bool = False) -> str:
|
||||
"""Fetch vLLM registry.py source.
|
||||
|
||||
In local mode (ctx.is_local) the file is read from disk; otherwise
|
||||
it is fetched from the GitHub raw CDN.
|
||||
"""
|
||||
if ctx.is_local:
|
||||
p = os.path.join(ctx.local_dir, _REGISTRY_PATH)
|
||||
if verbose:
|
||||
print(f" [registry] reading local {p}")
|
||||
try:
|
||||
with open(p, encoding="utf-8") as f:
|
||||
body = f.read()
|
||||
except OSError:
|
||||
raise RuntimeError(f"Could not read local registry.py at {p}")
|
||||
if not body:
|
||||
raise RuntimeError(f"Empty local registry.py at {p}")
|
||||
return body
|
||||
|
||||
ref = ref or ctx.branch
|
||||
url = f"https://raw.githubusercontent.com/{ctx.repo}/{ref}/{_REGISTRY_PATH}"
|
||||
if verbose:
|
||||
print(f" [registry] fetching {url}")
|
||||
body, _ = _get(url, ua=_BROWSER_UA)
|
||||
if not body:
|
||||
raise RuntimeError(f"Could not fetch registry.py from {url}")
|
||||
return body
|
||||
|
||||
|
||||
def parse_vllm_registry(source, verbose: bool = False):
|
||||
"""Parse registry.py source and return structured info.
|
||||
|
||||
Returns:
|
||||
supported: dict[arch] -> {category, module, class_name}
|
||||
previously: dict[arch] -> version_str
|
||||
oot: dict[arch] -> plugin_url
|
||||
"""
|
||||
supported = {}
|
||||
for dict_name, category in _CATEGORY_MAP.items():
|
||||
entries = _parse_dict_entries(source, dict_name)
|
||||
for arch, (mod, cls) in entries.items():
|
||||
supported[arch] = {
|
||||
"category": category,
|
||||
"module": mod,
|
||||
"class_name": cls,
|
||||
}
|
||||
if verbose and entries:
|
||||
print(f" [registry] {dict_name}: {len(entries)} entries")
|
||||
|
||||
previously = _parse_simple_dict(source, "_PREVIOUSLY_SUPPORTED_MODELS")
|
||||
if verbose and previously:
|
||||
print(f" [registry] _PREVIOUSLY_SUPPORTED_MODELS: {len(previously)} entries")
|
||||
|
||||
oot = _parse_simple_dict(source, "_OOT_SUPPORTED_MODELS")
|
||||
if verbose and oot:
|
||||
print(f" [registry] _OOT_SUPPORTED_MODELS: {len(oot)} entries")
|
||||
|
||||
return supported, previously, oot
|
||||
|
||||
|
||||
def check_vllm_registry(arch, supported, previously, oot):
|
||||
"""Check an architecture against parsed vLLM registry data.
|
||||
|
||||
Returns a dict with status details.
|
||||
"""
|
||||
if arch in supported:
|
||||
info = supported[arch]
|
||||
mod = info["module"]
|
||||
if mod.startswith("vllm."):
|
||||
full_module = mod
|
||||
else:
|
||||
full_module = f"vllm.model_executor.models.{mod}"
|
||||
return {
|
||||
"status": "supported",
|
||||
"category": info["category"],
|
||||
"module": full_module,
|
||||
"class_name": info["class_name"],
|
||||
}
|
||||
|
||||
if arch in previously:
|
||||
return {
|
||||
"status": "previously_supported",
|
||||
"last_version": previously[arch],
|
||||
}
|
||||
|
||||
if arch in oot:
|
||||
return {
|
||||
"status": "oot_plugin",
|
||||
"plugin_url": oot[arch],
|
||||
}
|
||||
|
||||
return {"status": "unsupported"}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
"""vLLM strategy: registry.py parsing, version path, summary formatting."""
|
||||
|
||||
import os
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from ...context import Context
|
||||
from .registry import (
|
||||
check_vllm_registry,
|
||||
fetch_vllm_registry,
|
||||
parse_vllm_registry,
|
||||
)
|
||||
from ..base import FrameworkStrategy
|
||||
|
||||
_NEW_MODELS_DIR = "vllm/models"
|
||||
|
||||
|
||||
class VllmStrategy(FrameworkStrategy):
|
||||
name = "vllm"
|
||||
label = "vLLM"
|
||||
repo = "vllm-project/vllm"
|
||||
branch = "main"
|
||||
models_dir = "vllm/model_executor/models"
|
||||
docs_url = "https://docs.vllm.ai/en/stable/models/supported_models/"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._registry_file_path = None
|
||||
|
||||
def check_support(
|
||||
self, arch: str, ctx: Context, token: Optional[str] = None, verbose: bool = False
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
supported, file_path = super().check_support(arch, ctx, token, verbose)
|
||||
if supported:
|
||||
return True, file_path
|
||||
try:
|
||||
reg_source = fetch_vllm_registry(ctx=ctx, ref=self.branch, verbose=verbose)
|
||||
reg_supported, _, _ = parse_vllm_registry(reg_source, verbose=verbose)
|
||||
except RuntimeError as e:
|
||||
if verbose:
|
||||
print(f" [registry-fallback] skipped: {e}")
|
||||
return False, None
|
||||
if arch not in reg_supported:
|
||||
return False, None
|
||||
module = reg_supported[arch]["module"]
|
||||
if not module.startswith("vllm.models."):
|
||||
return False, None
|
||||
resolved = self._resolve_new_style_path(module, arch, ctx, verbose)
|
||||
if resolved:
|
||||
self._registry_file_path = resolved
|
||||
if verbose:
|
||||
print(f" [registry-fallback] found via registry: {resolved}")
|
||||
return True, resolved
|
||||
return False, None
|
||||
|
||||
def _resolve_new_style_path(self, module, arch, ctx, verbose):
|
||||
pkg_rel = module.replace(".", os.sep)
|
||||
candidates = [
|
||||
f"{pkg_rel}/__init__.py",
|
||||
f"{pkg_rel}.py",
|
||||
]
|
||||
for rel in candidates:
|
||||
body = ctx.read_source(rel)
|
||||
if body and arch in body:
|
||||
return rel
|
||||
if ctx.is_local:
|
||||
pkg_dir = os.path.join(ctx.local_dir, pkg_rel)
|
||||
if os.path.isdir(pkg_dir):
|
||||
for fname in sorted(os.listdir(pkg_dir)):
|
||||
if fname.endswith(".py") and fname != "__init__.py":
|
||||
fpath = os.path.join(pkg_dir, fname)
|
||||
try:
|
||||
with open(fpath, encoding="utf-8") as f:
|
||||
body = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
if arch in body:
|
||||
return f"{pkg_rel}/{fname}"
|
||||
return None
|
||||
|
||||
def extra_checks(self, arch: str, ctx: Context, token: Optional[str], verbose: bool, ref: Optional[str] = None):
|
||||
"""Parse registry.py for detailed vLLM model info."""
|
||||
try:
|
||||
reg_source = fetch_vllm_registry(ctx=ctx, ref=ref or self.branch, verbose=verbose)
|
||||
reg_supported, reg_previously, reg_oot = parse_vllm_registry(
|
||||
reg_source, verbose=verbose
|
||||
)
|
||||
print(f" Registry parsed: {len(reg_supported)} architectures registered")
|
||||
info = check_vllm_registry(arch, reg_supported, reg_previously, reg_oot)
|
||||
status = info["status"]
|
||||
if status == "supported":
|
||||
print(f" Category : {info['category']}")
|
||||
print(f" Module : {info['module']}")
|
||||
print(f" Class : {info['class_name']}")
|
||||
elif status == "previously_supported":
|
||||
print(f" PREVIOUSLY SUPPORTED (removed)")
|
||||
print(f" Last version: v{info['last_version']}")
|
||||
elif status == "oot_plugin":
|
||||
print(f" OUT-OF-TREE PLUGIN REQUIRED")
|
||||
print(f" Plugin: {info['plugin_url']}")
|
||||
return info
|
||||
except RuntimeError as e:
|
||||
if verbose:
|
||||
print(f" [registry] skipped: {e}")
|
||||
return None
|
||||
|
||||
def version_path(self, file_path: Optional[str]) -> Optional[str]:
|
||||
"""Prefer registry-resolved path, then file_path, then registry.py."""
|
||||
return self._registry_file_path or file_path or f"{self.models_dir}/registry.py"
|
||||
|
||||
def format_summary_extra(self, info) -> None:
|
||||
if not info:
|
||||
return
|
||||
status = info["status"]
|
||||
if status == "previously_supported":
|
||||
print(f" NOTE: removed after v{info['last_version']}")
|
||||
elif status == "oot_plugin":
|
||||
print(f" NOTE: requires plugin {info['plugin_url']}")
|
||||
elif status == "supported":
|
||||
print(f" Category: {info['category']}")
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
"""vLLM-Ascend framework strategy."""
|
||||
|
||||
from .registry import (
|
||||
check_vllm_ascend_registry,
|
||||
fetch_vllm_ascend_registry,
|
||||
parse_vllm_ascend_registry,
|
||||
)
|
||||
from .strategy import VllmAscendStrategy
|
||||
|
||||
__all__ = [
|
||||
"VllmAscendStrategy",
|
||||
"fetch_vllm_ascend_registry",
|
||||
"parse_vllm_ascend_registry",
|
||||
"check_vllm_ascend_registry",
|
||||
]
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
"""vLLM-Ascend __init__.py parsing for registered models.
|
||||
|
||||
vLLM-Ascend uses ModelRegistry.register_model() calls in __init__.py
|
||||
to register model architectures, unlike vLLM which uses a dict-based registry.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from ...context import Context
|
||||
from ...http_utils import _get
|
||||
|
||||
_INIT_PATH = "vllm_ascend/models/__init__.py"
|
||||
|
||||
_BROWSER_UA = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def fetch_vllm_ascend_registry(ctx: Context, ref: Optional[str] = None, verbose: bool = False) -> str:
|
||||
"""Fetch vLLM-Ascend models/__init__.py source.
|
||||
|
||||
In local mode (ctx.is_local) the file is read from disk; otherwise
|
||||
it is fetched from the GitHub raw CDN.
|
||||
"""
|
||||
if ctx.is_local:
|
||||
p = os.path.join(ctx.local_dir, _INIT_PATH)
|
||||
if verbose:
|
||||
print(f" [registry] reading local {p}")
|
||||
try:
|
||||
with open(p, encoding="utf-8") as f:
|
||||
body = f.read()
|
||||
except OSError:
|
||||
raise RuntimeError(f"Could not read local __init__.py at {p}")
|
||||
if not body:
|
||||
raise RuntimeError(f"Empty local __init__.py at {p}")
|
||||
return body
|
||||
|
||||
ref = ref or ctx.branch
|
||||
url = f"https://raw.githubusercontent.com/{ctx.repo}/{ref}/{_INIT_PATH}"
|
||||
if verbose:
|
||||
print(f" [registry] fetching {url}")
|
||||
body, _ = _get(url, ua=_BROWSER_UA)
|
||||
if not body:
|
||||
raise RuntimeError(f"Could not fetch __init__.py from {url}")
|
||||
return body
|
||||
|
||||
|
||||
def parse_vllm_ascend_registry(source: str, verbose: bool = False):
|
||||
"""Parse __init__.py source and extract registered models.
|
||||
|
||||
Looks for ModelRegistry.register_model() calls and extracts:
|
||||
- arch_name: the architecture name (first argument)
|
||||
- class_path: the module:class path (second argument)
|
||||
|
||||
Returns:
|
||||
registered: dict[arch_name] = class_path
|
||||
"""
|
||||
# Match: ModelRegistry.register_model("ArchName", "module.path:ClassName")
|
||||
# Handles multi-line calls with various whitespace
|
||||
pattern = re.compile(
|
||||
r'ModelRegistry\.register_model\s*\(\s*'
|
||||
r'["\'](\w+)["\']\s*,\s*'
|
||||
r'["\']([^"\']+)["\']',
|
||||
re.MULTILINE
|
||||
)
|
||||
|
||||
registered = {}
|
||||
for m in pattern.finditer(source):
|
||||
arch_name = m.group(1)
|
||||
class_path = m.group(2)
|
||||
registered[arch_name] = class_path
|
||||
|
||||
if verbose and registered:
|
||||
print(f" [registry] found {len(registered)} registered models")
|
||||
|
||||
return registered
|
||||
|
||||
|
||||
def check_vllm_ascend_registry(arch: str, registered: dict) -> dict:
|
||||
"""Check an architecture against parsed vLLM-Ascend registry data.
|
||||
|
||||
Returns a dict with status details.
|
||||
"""
|
||||
if arch in registered:
|
||||
class_path = registered[arch]
|
||||
# Parse "module.path:ClassName" format
|
||||
if ":" in class_path:
|
||||
module_path, class_name = class_path.rsplit(":", 1)
|
||||
else:
|
||||
module_path = class_path
|
||||
class_name = class_path.split(".")[-1]
|
||||
|
||||
return {
|
||||
"status": "supported",
|
||||
"module": module_path,
|
||||
"class_name": class_name,
|
||||
}
|
||||
|
||||
return {"status": "unsupported"}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
"""vLLM-Ascend strategy: __init__.py parsing for registered models."""
|
||||
|
||||
import os
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from ...context import Context
|
||||
from .registry import (
|
||||
check_vllm_ascend_registry,
|
||||
fetch_vllm_ascend_registry,
|
||||
parse_vllm_ascend_registry,
|
||||
)
|
||||
from ..base import FrameworkStrategy
|
||||
|
||||
|
||||
class VllmAscendStrategy(FrameworkStrategy):
|
||||
name = "vllm-ascend"
|
||||
label = "vLLM-Ascend"
|
||||
repo = "vllm-project/vllm-ascend"
|
||||
branch = "main"
|
||||
models_dir = "vllm_ascend/models"
|
||||
docs_url = "https://docs.vllm.ai/projects/ascend/en/latest/"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._registry_file_path = None
|
||||
|
||||
def check_support(
|
||||
self, arch: str, ctx: Context, token: Optional[str] = None, verbose: bool = False
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
supported, file_path = super().check_support(arch, ctx, token, verbose)
|
||||
if supported:
|
||||
return True, file_path
|
||||
try:
|
||||
init_source = fetch_vllm_ascend_registry(ctx=ctx, ref=self.branch, verbose=verbose)
|
||||
registered = parse_vllm_ascend_registry(init_source, verbose=verbose)
|
||||
except RuntimeError as e:
|
||||
if verbose:
|
||||
print(f" [registry-fallback] skipped: {e}")
|
||||
return False, None
|
||||
if arch not in registered:
|
||||
return False, None
|
||||
class_path = registered[arch]
|
||||
if ":" in class_path:
|
||||
module_path = class_path.rsplit(":", 1)[0]
|
||||
else:
|
||||
module_path = class_path
|
||||
if not module_path.startswith("vllm_ascend.models."):
|
||||
return False, None
|
||||
resolved = self._resolve_new_style_path(module_path, arch, ctx, verbose)
|
||||
if resolved:
|
||||
self._registry_file_path = resolved
|
||||
if verbose:
|
||||
print(f" [registry-fallback] found via registry: {resolved}")
|
||||
return True, resolved
|
||||
return False, None
|
||||
|
||||
def _resolve_new_style_path(self, module, arch, ctx, verbose):
|
||||
pkg_rel = module.replace(".", os.sep)
|
||||
candidates = [
|
||||
f"{pkg_rel}/__init__.py",
|
||||
f"{pkg_rel}.py",
|
||||
]
|
||||
for rel in candidates:
|
||||
body = ctx.read_source(rel)
|
||||
if body and arch in body:
|
||||
return rel
|
||||
if ctx.is_local:
|
||||
pkg_dir = os.path.join(ctx.local_dir, pkg_rel)
|
||||
if os.path.isdir(pkg_dir):
|
||||
for fname in sorted(os.listdir(pkg_dir)):
|
||||
if fname.endswith(".py") and fname != "__init__.py":
|
||||
fpath = os.path.join(pkg_dir, fname)
|
||||
try:
|
||||
with open(fpath, encoding="utf-8") as f:
|
||||
body = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
if arch in body:
|
||||
return f"{pkg_rel}/{fname}"
|
||||
return None
|
||||
|
||||
def extra_checks(self, arch: str, ctx: Context, token: Optional[str], verbose: bool, ref: Optional[str] = None):
|
||||
"""Parse __init__.py for registered model info."""
|
||||
try:
|
||||
init_source = fetch_vllm_ascend_registry(ctx=ctx, ref=ref or self.branch, verbose=verbose)
|
||||
registered = parse_vllm_ascend_registry(init_source, verbose=verbose)
|
||||
print(f" Registry parsed: {len(registered)} architectures registered")
|
||||
info = check_vllm_ascend_registry(arch, registered)
|
||||
status = info["status"]
|
||||
if status == "supported":
|
||||
print(f" Module : {info['module']}")
|
||||
print(f" Class : {info['class_name']}")
|
||||
return info
|
||||
except RuntimeError as e:
|
||||
if verbose:
|
||||
print(f" [registry] skipped: {e}")
|
||||
return None
|
||||
|
||||
def version_path(self, file_path: Optional[str]) -> Optional[str]:
|
||||
"""Prefer registry-resolved path, then file_path, then __init__.py."""
|
||||
return self._registry_file_path or file_path or f"{self.models_dir}/__init__.py"
|
||||
|
||||
def format_summary_extra(self, info) -> None:
|
||||
if not info:
|
||||
return
|
||||
status = info["status"]
|
||||
if status == "supported":
|
||||
print(f" Module: {info['module']}")
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Minimal HTTP helpers (standard library only)."""
|
||||
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
UA = {"User-Agent": "model-support-check/1.0"}
|
||||
|
||||
BROWSER_UA = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _get(url, headers=None, timeout=25, ua=None):
|
||||
h = dict(ua or UA)
|
||||
if headers:
|
||||
h.update(headers)
|
||||
req = urllib.request.Request(url, headers=h)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.read().decode("utf-8", "replace"), r.headers
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code in (403, 429):
|
||||
raise RuntimeError(f"GitHub rate limit / forbidden ({e.code})")
|
||||
if e.code in (401, 404):
|
||||
return None, e.headers
|
||||
raise
|
||||
|
||||
|
||||
def _get_json(url, token=None):
|
||||
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
||||
try:
|
||||
body, _ = _get(url, headers)
|
||||
except RuntimeError:
|
||||
return None
|
||||
if body is None:
|
||||
return None
|
||||
return json.loads(body)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""State management for model-support-checker.
|
||||
|
||||
State lives in <skill root>/.state/ (gitignored via .gitignore) so the
|
||||
first-run choice (GitHub PAT vs local clone) survives across runs without
|
||||
being tracked by git. The GitHub token itself is NEVER stored here.
|
||||
"""
|
||||
|
||||
from .doctor import doctor, print_report
|
||||
from .persistence import load, save, reset
|
||||
from .setup import first_run_menu, ensure_setup
|
||||
from .refresh import start_refresh
|
||||
|
||||
__all__ = [
|
||||
"load",
|
||||
"save",
|
||||
"reset",
|
||||
"first_run_menu",
|
||||
"ensure_setup",
|
||||
"start_refresh",
|
||||
"doctor",
|
||||
"print_report",
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Repository cloning and validation."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from ..framework_strategies import STRATEGIES
|
||||
from .constants import REPOS_DIR
|
||||
|
||||
|
||||
def clone(url, target):
|
||||
"""Clone url into target (full history -- shallow clones break step 4)."""
|
||||
if os.path.isdir(os.path.join(target, ".git")):
|
||||
print(f" reusing existing clone: {target}")
|
||||
return target
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
print(f" cloning {url} -> {target} (full history, may take a while)...")
|
||||
r = subprocess.run(["git", "clone", url, target])
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"git clone failed for {url}")
|
||||
return target
|
||||
|
||||
|
||||
def validate_checkout(path, fw_name):
|
||||
"""Validate that a path looks like a valid framework checkout."""
|
||||
strategy = STRATEGIES[fw_name]
|
||||
if not os.path.isfile(os.path.join(path, strategy.models_dir, "__init__.py")):
|
||||
print(
|
||||
f" WARNING: {strategy.models_dir} not found under {path}; "
|
||||
f"is this really a {strategy.label} checkout?"
|
||||
)
|
||||
|
||||
|
||||
def get_clone_url(fw_name):
|
||||
"""Get the clone URL for a framework."""
|
||||
return f"https://github.com/{STRATEGIES[fw_name].repo}"
|
||||
|
||||
|
||||
def get_default_clone_path(fw_name):
|
||||
"""Get the default clone path for a framework."""
|
||||
return os.path.join(REPOS_DIR, fw_name)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""State constants."""
|
||||
|
||||
import os
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
STATE_DIR = os.path.join(ROOT, ".state")
|
||||
REPOS_DIR = os.path.join(STATE_DIR, "repos")
|
||||
STATE_FILE = os.path.join(STATE_DIR, "state.json")
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Health checks for persisted setup state.
|
||||
|
||||
The `doctor()` function inspects `.state/state.json` and the configured
|
||||
framework checkouts, returning a list of actionable issues. It is run
|
||||
automatically on every invocation so callers (human or agent) do not need
|
||||
to manually inspect `.state/` with `ls` or `cat`.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from ..framework_strategies import STRATEGIES
|
||||
from .constants import STATE_FILE
|
||||
|
||||
|
||||
class Issue:
|
||||
"""A single doctor issue with severity and an actionable message."""
|
||||
|
||||
def __init__(self, severity, message, framework=None):
|
||||
self.severity = severity # "error" | "warning"
|
||||
self.message = message
|
||||
self.framework = framework
|
||||
|
||||
def __str__(self):
|
||||
prefix = "ERROR" if self.severity == "error" else "WARN"
|
||||
if self.framework:
|
||||
return f"[{prefix} {self.framework}] {self.message}"
|
||||
return f"[{prefix}] {self.message}"
|
||||
|
||||
|
||||
def doctor(st, args=None, framework=None):
|
||||
"""Check setup state and local checkouts.
|
||||
|
||||
Args:
|
||||
st: The loaded state dict, or None if no state file exists.
|
||||
args: Optional argparse namespace with path overrides.
|
||||
framework: Framework name to limit the check to, or None/'all' to check
|
||||
every configured framework.
|
||||
|
||||
Returns:
|
||||
A list of Issue objects. An empty list means the configuration looks
|
||||
healthy enough to proceed.
|
||||
"""
|
||||
issues = []
|
||||
|
||||
if st is None:
|
||||
issues.append(
|
||||
Issue(
|
||||
"error",
|
||||
f"No setup state found ({STATE_FILE} missing). "
|
||||
"Run: python3 main.py --setup local",
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
mode = st.get("mode")
|
||||
if mode not in ("local", "token"):
|
||||
issues.append(
|
||||
Issue(
|
||||
"error",
|
||||
f"Invalid mode '{mode}' in state. Run: python3 main.py --setup local",
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
if mode == "token":
|
||||
token = _get_token(args)
|
||||
if not token:
|
||||
issues.append(
|
||||
Issue(
|
||||
"warning",
|
||||
"GitHub token mode but no GITHUB_TOKEN set. "
|
||||
"Code search results will be best-effort only.",
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
# Local mode: validate the requested framework path(s).
|
||||
frameworks = list(STRATEGIES) if framework in (None, "all") else [framework]
|
||||
for fw_name in frameworks:
|
||||
if fw_name not in STRATEGIES:
|
||||
issues.append(Issue("error", f"Unknown framework: {fw_name}"))
|
||||
continue
|
||||
strategy = STRATEGIES[fw_name]
|
||||
path_key = f"{fw_name.replace('-', '_')}_path"
|
||||
configured_path = st.get(path_key)
|
||||
override_path = _get_override_path(args, fw_name)
|
||||
path = override_path or configured_path
|
||||
|
||||
if not configured_path and not override_path:
|
||||
issues.append(
|
||||
Issue(
|
||||
"error",
|
||||
f"No local checkout configured. Run: python3 main.py --setup local --{fw_name}-path /path/to/{fw_name}",
|
||||
framework=strategy.label,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if override_path and not configured_path:
|
||||
issues.append(
|
||||
Issue(
|
||||
"warning",
|
||||
f"Using one-time override {override_path}; state is missing a saved path.",
|
||||
framework=strategy.label,
|
||||
)
|
||||
)
|
||||
|
||||
if not os.path.isdir(path):
|
||||
issues.append(
|
||||
Issue(
|
||||
"error",
|
||||
f"Configured path does not exist: {path}",
|
||||
framework=strategy.label,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if not os.path.isdir(os.path.join(path, ".git")):
|
||||
issues.append(
|
||||
Issue(
|
||||
"error",
|
||||
f"Path is not a git repository: {path}",
|
||||
framework=strategy.label,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
init_file = os.path.join(path, strategy.models_dir, "__init__.py")
|
||||
if not os.path.isfile(init_file):
|
||||
issues.append(
|
||||
Issue(
|
||||
"warning",
|
||||
f"{strategy.models_dir}/__init__.py not found under {path}; "
|
||||
"is this really a checkout?",
|
||||
framework=strategy.label,
|
||||
)
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def _get_token(args):
|
||||
if args and getattr(args, "token", None):
|
||||
return args.token
|
||||
return os.environ.get("GITHUB_TOKEN")
|
||||
|
||||
|
||||
def _get_override_path(args, fw_name):
|
||||
if args is None:
|
||||
return None
|
||||
attr = f"{fw_name.replace('-', '_')}_path"
|
||||
return getattr(args, attr, None)
|
||||
|
||||
|
||||
def print_report(issues):
|
||||
"""Print doctor issues in a consistent format.
|
||||
|
||||
Returns True if any errors were reported.
|
||||
"""
|
||||
has_errors = any(i.severity == "error" for i in issues)
|
||||
if not issues:
|
||||
print("doctor: setup state looks healthy.")
|
||||
return False
|
||||
|
||||
print("=== doctor ===")
|
||||
for issue in issues:
|
||||
print(f" {issue}")
|
||||
print()
|
||||
return has_errors
|
||||
@@ -0,0 +1,33 @@
|
||||
"""State persistence: load, save, reset."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from .constants import STATE_DIR, STATE_FILE, REPOS_DIR
|
||||
|
||||
|
||||
def load():
|
||||
"""Return the saved state dict, or None if not set up yet."""
|
||||
try:
|
||||
with open(STATE_FILE, encoding="utf-8") as f:
|
||||
st = json.load(f)
|
||||
return st if isinstance(st, dict) and st.get("mode") else None
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def save(st):
|
||||
os.makedirs(STATE_DIR, exist_ok=True)
|
||||
with open(STATE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(st, f, indent=2)
|
||||
return st
|
||||
|
||||
|
||||
def reset():
|
||||
"""Forget setup state. Cloned repos under .state/repos/ are kept and will
|
||||
be reused automatically by a later `--setup local`."""
|
||||
try:
|
||||
os.remove(STATE_FILE)
|
||||
print(f"Removed {STATE_FILE} (cloned repos kept at {REPOS_DIR}).")
|
||||
except FileNotFoundError:
|
||||
print("No saved state found.")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Background checkout refresh."""
|
||||
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
|
||||
def fetch_status(path, result, key):
|
||||
"""Background worker: `git fetch` a local checkout and count how far
|
||||
behind its upstream it is. Writes into result[key]; NEVER raises — any
|
||||
failure is recorded as {"error": ...} so the main flow is never blocked."""
|
||||
info = {"path": path}
|
||||
try:
|
||||
remotes = subprocess.run(
|
||||
["git", "-C", path, "remote"],
|
||||
capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
if not remotes:
|
||||
info["no_upstream"] = True
|
||||
result[key] = info
|
||||
return
|
||||
f = subprocess.run(
|
||||
["git", "-C", path, "fetch", "--quiet", "origin"],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
)
|
||||
if f.returncode != 0:
|
||||
lines = [ln for ln in f.stderr.strip().splitlines() if ln.strip()]
|
||||
info["error"] = lines[-1] if lines else "git fetch failed"
|
||||
else:
|
||||
up = subprocess.run(
|
||||
["git", "-C", path, "rev-parse", "--abbrev-ref",
|
||||
"--symbolic-full-name", "@{upstream}"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
head = subprocess.run(
|
||||
["git", "-C", path, "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
if up.returncode != 0 or head in ("", "HEAD"):
|
||||
info["no_upstream"] = True
|
||||
else:
|
||||
cnt = subprocess.run(
|
||||
["git", "-C", path, "rev-list", "--count",
|
||||
f"HEAD..{up.stdout.strip()}"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if cnt.returncode == 0 and cnt.stdout.strip().isdigit():
|
||||
info["behind"] = int(cnt.stdout.strip())
|
||||
else:
|
||||
info["no_upstream"] = True
|
||||
except (subprocess.SubprocessError, OSError, ValueError) as e:
|
||||
# ponytail: broad catch is the point — refresh must never be fatal
|
||||
info["error"] = str(e)
|
||||
result[key] = info
|
||||
|
||||
|
||||
def start_refresh(framework_paths):
|
||||
"""Spawn daemon threads to refresh local checkouts concurrently.
|
||||
|
||||
Returns (results_dict, threads); results are filled in as workers finish.
|
||||
"""
|
||||
results = {}
|
||||
threads = []
|
||||
for key, path in framework_paths.items():
|
||||
t = threading.Thread(target=fetch_status, args=(path, results, key),
|
||||
daemon=True)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
return results, threads
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Interactive setup and first-run menu."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ..framework_strategies import STRATEGIES
|
||||
from .clone import clone, validate_checkout, get_clone_url, get_default_clone_path
|
||||
from .constants import REPOS_DIR, STATE_FILE
|
||||
from .persistence import save
|
||||
|
||||
|
||||
def print_tradeoffs():
|
||||
"""Print the tradeoffs between local clone and GitHub PAT modes."""
|
||||
print(
|
||||
"First-run setup: choose how to access vLLM/SGLang sources.\n"
|
||||
"\n"
|
||||
" 1) Local clone (recommended)\n"
|
||||
" + definitive results: files grepped on disk, no rate limits\n"
|
||||
" + fast repeat checks; version detection from full git history\n"
|
||||
" + works offline after the initial clone\n"
|
||||
" - one-time download (vLLM full history ~1 GB+, SGLang smaller)\n"
|
||||
" - needs occasional 'git pull' or answers go stale\n"
|
||||
"\n"
|
||||
" 2) GitHub PAT\n"
|
||||
" + no disk usage; always reflects remote main\n"
|
||||
" - code search 403s without a token; anonymous results are\n"
|
||||
" best-effort only\n"
|
||||
" - API rate limits can break batch/version checks\n"
|
||||
" - GITHUB_TOKEN must be provided every run (never stored here)"
|
||||
)
|
||||
|
||||
|
||||
def first_run_menu():
|
||||
"""Interactive TTY menu. Returns the chosen mode ('local' | 'token')."""
|
||||
print_tradeoffs()
|
||||
while True:
|
||||
choice = input("Choose [1/2] (default 1): ").strip()
|
||||
if choice in ("", "1"):
|
||||
return "local"
|
||||
if choice == "2":
|
||||
return "token"
|
||||
print("Please enter 1 or 2.")
|
||||
|
||||
|
||||
def ensure_setup(mode, args):
|
||||
"""Create and persist setup state. Returns the new state dict."""
|
||||
st = {"mode": mode}
|
||||
if mode == "local":
|
||||
vllm_path = args.vllm_path
|
||||
sglang_path = args.sglang_path
|
||||
vllm_ascend_path = getattr(args, "vllm_ascend_path", None)
|
||||
|
||||
if sys.stdin.isatty():
|
||||
if not vllm_path:
|
||||
raw = input(
|
||||
f"vLLM checkout path [Enter = clone into {REPOS_DIR}/vllm]: "
|
||||
).strip()
|
||||
vllm_path = raw or None
|
||||
if not sglang_path:
|
||||
raw = input(
|
||||
f"SGLang checkout path [Enter = clone into {REPOS_DIR}/sglang]: "
|
||||
).strip()
|
||||
sglang_path = raw or None
|
||||
if not vllm_ascend_path:
|
||||
raw = input(
|
||||
f"vLLM-Ascend checkout path [Enter = clone into {REPOS_DIR}/vllm-ascend]: "
|
||||
).strip()
|
||||
vllm_ascend_path = raw or None
|
||||
|
||||
if not vllm_path:
|
||||
vllm_path = clone(get_clone_url("vllm"), get_default_clone_path("vllm"))
|
||||
if not sglang_path:
|
||||
sglang_path = clone(get_clone_url("sglang"), get_default_clone_path("sglang"))
|
||||
if not vllm_ascend_path:
|
||||
vllm_ascend_path = clone(get_clone_url("vllm-ascend"), get_default_clone_path("vllm-ascend"))
|
||||
|
||||
st["vllm_path"] = os.path.abspath(os.path.expanduser(vllm_path))
|
||||
st["sglang_path"] = os.path.abspath(os.path.expanduser(sglang_path))
|
||||
st["vllm_ascend_path"] = os.path.abspath(os.path.expanduser(vllm_ascend_path))
|
||||
|
||||
validate_checkout(st["vllm_path"], "vllm")
|
||||
validate_checkout(st["sglang_path"], "sglang")
|
||||
validate_checkout(st["vllm_ascend_path"], "vllm-ascend")
|
||||
elif mode == "token":
|
||||
if not args.token and not os.environ.get("GITHUB_TOKEN"):
|
||||
print(
|
||||
"NOTE: no GITHUB_TOKEN set. Code search needs it; export "
|
||||
"GITHUB_TOKEN per run (it is never stored in .state/)."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Unknown setup mode: {mode}")
|
||||
|
||||
save(st)
|
||||
print(f"Setup saved ({mode}) -> {STATE_FILE}")
|
||||
return st
|
||||
Reference in New Issue
Block a user