[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.state/
|
||||
@@ -0,0 +1 @@
|
||||
XingfenD
|
||||
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: model-support-checker
|
||||
description: Use when checking HuggingFace or ModelScope model support for SGLang, vLLM, or vLLM-Ascend, finding the first supporting version, or diagnosing "model not supported" errors.
|
||||
---
|
||||
|
||||
# Model Support Checker
|
||||
|
||||
Check whether a HuggingFace / ModelScope model is supported by **SGLang**, **vLLM**, or **vLLM-Ascend**, and since which version.
|
||||
|
||||
## Methodology
|
||||
|
||||
The authoritative signal: does the framework's **source code** contain an implementation for the model's architecture string? Docs are supplementary.
|
||||
|
||||
1. **Architecture** — `architectures` from `config.json` (HF → ModelScope fallback). Override with `--arch`.
|
||||
2. **Docs** (supplementary) — grep the framework's supported-models page.
|
||||
3. **Source** (authoritative) — search the models directory for the architecture string.
|
||||
4. **Reconcile** — if docs say YES but source says NO, manually inspect the framework's repo to resolve the discrepancy (see Step 4b).
|
||||
5. **Version** — earliest commit on the implementation file → nearest release tag.
|
||||
|
||||
The tool is stateful; mode persists in `.state/state.json`. Local clone (recommended) greps on disk. Token mode uses GitHub API.
|
||||
|
||||
## Skill Workflow
|
||||
|
||||
**Step 1 — Doctor.** `python3 main.py --doctor`. `[ERROR]` → fix first, stop. `[WARN]` → ignore, continue.
|
||||
|
||||
**Step 2 — Setup (optional).** Only if no state exists. Recommend local: `python3 main.py --setup local`.
|
||||
|
||||
**Step 3 — Resolve model ID.** Infer full `<org>/<model_name>` from user input, matching the hosting platform convention. Parse URLs or short names. Query the web if uncertain.
|
||||
|
||||
**Step 4 — Run.**
|
||||
```bash
|
||||
python3 main.py [--framework <fw>] [--source <src>] [--arch <arch>] <model_id>
|
||||
```
|
||||
- All frameworks: `python3 main.py <model_id>`
|
||||
- One framework: `python3 main.py --framework vllm <model_id>`
|
||||
- Manual arch (no config.json needed): `python3 main.py --arch LlamaForCausalLM`
|
||||
- Arch + model: `python3 main.py --framework vllm --arch DeepseekV3ForCausalLM <model_id>`
|
||||
|
||||
**Step 4b — Reconcile discrepancies.** If docs=YES but source=NO for any framework, the directory layout has likely changed. Do not assume known paths; instead:
|
||||
1. Read the framework's registry file to find where the architecture is registered.
|
||||
2. Extract the module path from the registry entry.
|
||||
3. Trace that module path to locate the actual implementation file on disk.
|
||||
4. Verify the architecture string exists in that file.
|
||||
5. If the registry does not contain the architecture, search the entire framework repo for the architecture string (e.g. `rg <ArchName> <repo_root>`) to locate where it is referenced.
|
||||
6. Update the result to YES with the correct file path.
|
||||
|
||||
**Step 5 — Report.** Parse Summary. Report: YES/NO per framework, minimum version, file path, vLLM category/module/class.
|
||||
|
||||
## Fallback Strategies
|
||||
|
||||
### Fallback 1: config.json fetch fails
|
||||
|
||||
Escalate in order:
|
||||
|
||||
1. **Switch platform** — retry with `--source hf` (default is modelscope).
|
||||
2. **webfetch** — agent fetches `config.json` directly:
|
||||
- HF: `https://huggingface.co/<org>/<model>/resolve/main/config.json`
|
||||
- MS: `https://modelscope.cn/models/<org>/<model>/resolve/master/config.json`
|
||||
Extract `architectures`, then: `python3 main.py --arch <ArchName> <model_id>`
|
||||
3. **Guide user** — if webfetch fails (private/gated), provide links for the user to read `architectures` from `config.json`:
|
||||
- HF: `https://huggingface.co/<org>/<model>/blob/main/config.json`
|
||||
- MS: `https://modelscope.cn/models/<org>/<model>/files`
|
||||
Then run with `--arch`.
|
||||
|
||||
### Fallback 2: Script cannot run
|
||||
|
||||
Bypass the script; follow methodology manually:
|
||||
|
||||
1. Get architecture (webfetch or ask user).
|
||||
2. Search framework GitHub repos for the architecture string:
|
||||
- vLLM (`vllm-project/vllm`): `vllm/model_executor/models/` or `vllm/models/<family>/`
|
||||
- SGLang (`sgl-project/sglang`): `python/sglang/srt/models/`
|
||||
- vLLM-Ascend (`vllm-project/vllm-ascend`): `vllm_ascend/models/` (may include subdirs like `minimax_m3/`)
|
||||
3. Check official supported-models docs.
|
||||
4. Earliest commit → nearest release tag for version.
|
||||
5. Report in script Summary format.
|
||||
|
||||
### Fallback 3: Source=NO but model is recent / may ship support out-of-tree
|
||||
|
||||
The script's source check is authoritative for the **merged `main` branch checkout**, but framework teams frequently ship support through channels that are NOT in the default `main` checkout the script greps:
|
||||
|
||||
- **Official Docker images** with the implementation baked in (e.g. `lmsysorg/sglang:glm-5.3-flash`, `vllm/vllm-openai-rocm:glm53-flash`).
|
||||
- **A separate support branch** not yet merged to main (e.g. `xinyuan/glm-5.3-flash-support`).
|
||||
- **Vendor recipes / cookbook pages** (e.g. `recipes.vllm.ai/<org>/<model>`, `cookbook.sglang.io/...`).
|
||||
|
||||
This is the reverse of Step 4b (docs=YES, source=NO). When source=NO, do NOT report a flat "unsupported" until the official deployment docs are checked:
|
||||
|
||||
1. Fetch the model card on **both** HF and ModelScope (one is often gated/401):
|
||||
- HF: `https://huggingface.co/<org>/<model>` and `.../resolve/main/README.md`
|
||||
- MS: `https://modelscope.cn/models/<org>/<model>` and `.../resolve/master/README.md`
|
||||
2. Find a "Deploy / Serve locally" section. If it lists vLLM / SGLang / vLLM-Ascend, the model IS deployable — capture the exact mechanism:
|
||||
- Docker image tag(s) and required version (e.g. "vLLM 0.29.0+", "use docker before integration is in the public repo").
|
||||
- Extra requirements: FlashInfer version, GPU arch (Hopper+/Blackwell/gfx950), `--tool-call-parser` / `--reasoning-parser`, MTP/speculative config.
|
||||
- Caveats: features only on a specific branch/PR, image alone insufficient, etc.
|
||||
3. Follow the recipe links to get the concrete launch command and image name.
|
||||
4. Report a **nuanced** result instead of a bare NO. Distinguish these states:
|
||||
- `merged` — in the public repo/PyPI, version-known.
|
||||
- `image-only` — runs via vendor Docker image, not yet in public main.
|
||||
- `branch-only` — needs a specific git branch/PR checkout.
|
||||
- `unsupported` — no official path at all.
|
||||
|
||||
The official model card + recipes are authoritative about HOW to run, even when the implementation is not in the default `main` checkout. This supersedes a flat NO from the script.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--framework` | `sglang` / `vllm` / `vllm-ascend` / `all` |
|
||||
| `--source` | `auto` / `hf` / `modelscope` |
|
||||
| `--arch` | Manual arch name(s); skips config.json |
|
||||
| `--doctor` | Check setup state |
|
||||
| `--setup` | `local` or `token` |
|
||||
|
||||
`python3 main.py --help` for full flag list.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
- **Skipping doctor** — always `--doctor` first.
|
||||
- **Giving up on config.json failure** — escalate: switch platform → webfetch → guide user.
|
||||
- **Not using `--arch`** — when config.json is unreachable, pass `--arch` instead of aborting.
|
||||
- **Trusting source=NO when docs=YES** — frameworks may reorganize directories; do Step 4b reconciliation.
|
||||
- **Trusting source=NO as final for recent models** — vendors often ship support via official Docker images / separate branches / recipe pages that are NOT in the default `main` checkout the script greps. Check the official model card + recipes (Fallback 3) before concluding "unsupported".
|
||||
@@ -0,0 +1,131 @@
|
||||
# model-support-checker
|
||||
|
||||
Check whether a HuggingFace / ModelScope model is supported by **SGLang**,
|
||||
**vLLM**, or **vLLM-Ascend**, and since which version.
|
||||
|
||||
## Setup (one-time)
|
||||
|
||||
The tool is **stateful**: your access mode is persisted in `.state/state.json`
|
||||
(gitignored), so later runs need no flags or tokens.
|
||||
|
||||
On first run you choose between two access modes — **local clone is recommended**:
|
||||
|
||||
| | Local clone (recommended) | GitHub PAT |
|
||||
|---|---|---|
|
||||
| Result quality | Definitive (files grepped on disk) | Best-effort without token; code search 403s anonymously |
|
||||
| Rate limits | None | Yes — can break batch/version checks |
|
||||
| Repeat checks | Fast (disk reads) | Slower (GitHub API) |
|
||||
| Works offline | Yes, after initial clone | No |
|
||||
| Cost | ~1 GB+ disk (vLLM full history needed) | None |
|
||||
| Freshness | Stale until `git pull` | Always current |
|
||||
|
||||
```bash
|
||||
python3 main.py --setup local # clone all repos into .state/repos/
|
||||
python3 main.py --setup local --vllm-path P1 --sglang-path P2 --vllm-ascend-path P3 # reuse existing checkouts
|
||||
python3 main.py --setup token # GitHub API mode
|
||||
|
||||
python3 main.py --doctor # check setup state and local checkouts
|
||||
python3 main.py --reset-state # forget setup (cloned repos are kept and reused)
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Local clones are full clones (`--depth 1` breaks version detection).
|
||||
- The GitHub token is NEVER written to `.state/`; in token mode export
|
||||
`GITHUB_TOKEN` per run.
|
||||
- Re-run `--setup <mode>` anytime to switch modes; explicit `--vllm-path` /
|
||||
`--sglang-path` / `--vllm-ascend-path` / `--token` flags override the saved state for that run.
|
||||
- Each run in local mode refreshes checkouts in the background (`git fetch`)
|
||||
and reports staleness or failures at the end — never blocking the main
|
||||
check.
|
||||
|
||||
## Methodology
|
||||
|
||||
For each framework the checker runs four framework-agnostic steps:
|
||||
|
||||
1. **Architecture name** — read `architectures` from the model's `config.json`
|
||||
(HuggingFace first, ModelScope API as fallback). Can be overridden with
|
||||
`--arch` to skip the network lookup entirely.
|
||||
2. **Official docs** (supplementary) — grep the framework's supported-models page.
|
||||
3. **GitHub source** (authoritative) — search the framework's models directory on
|
||||
GitHub. All frameworks register a model by mapping the HF architecture
|
||||
string to a class in `<models_dir>/<file>.py`, so the architecture string is
|
||||
always present in its implementation file.
|
||||
- For vLLM this step additionally parses `registry.py` to extract the model
|
||||
category, module path, class name, and checks
|
||||
`_PREVIOUSLY_SUPPORTED_MODELS` / `_OOT_SUPPORTED_MODELS`.
|
||||
- For vLLM-Ascend this step parses `__init__.py` for `ModelRegistry.register_model()`
|
||||
calls to extract module and class.
|
||||
4. **Version** — determine the first framework release that contains the
|
||||
implementation (via the file's earliest commit date → nearest release).
|
||||
|
||||
The authoritative "is it supported" check uses the GitHub **code-search API**
|
||||
with a path filter on the models directory. Anonymous code search returns 403,
|
||||
so a `GITHUB_TOKEN` is required for a definitive answer. Without a token the
|
||||
tool falls back to scraping the models directory tree + grepping candidate files
|
||||
on the raw CDN; that fallback can be incomplete, so a NO without a token is not
|
||||
definitive. Step 4 also needs a token on rate-limited IPs.
|
||||
|
||||
### Local checkouts (no GitHub API)
|
||||
|
||||
Local mode is the recommended setup (`--setup local` clones all repos into
|
||||
`.state/repos/` automatically). All source reads (models directory,
|
||||
`registry.py`, and version history) are served from disk via `git`, and **no
|
||||
GitHub API requests are made** — only the architecture lookup (step 1) and the
|
||||
docs check (step 2) still touch the network.
|
||||
|
||||
```bash
|
||||
# Override saved paths for a single run (no GITHUB_TOKEN needed for source/registry/version)
|
||||
python3 main.py --framework vllm --vllm-path /path/to/vllm deepseek-ai/DeepSeek-V3
|
||||
python3 main.py --framework sglang --sglang-path /path/to/sglang Qwen/Qwen3.6-35B-A3B
|
||||
python3 main.py --framework vllm-ascend --vllm-ascend-path /path/to/vllm-ascend deepseek-ai/DeepSeek-V3
|
||||
```
|
||||
|
||||
`--vllm-ref` is ignored when `--vllm-path` is given. The local path must be a
|
||||
git checkout of the framework repo (so `git log`/`git tag` are available for
|
||||
version detection). Keep checkouts fresh with an occasional `git pull`.
|
||||
|
||||
No third-party dependencies — standard library only.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# After setup, just pass a model id
|
||||
python3 main.py --framework sglang Qwen/Qwen3.6-35B-A3B
|
||||
python3 main.py --framework vllm deepseek-ai/DeepSeek-V3
|
||||
python3 main.py --framework vllm-ascend deepseek-ai/DeepSeek-V3
|
||||
|
||||
# Check all frameworks at once (default)
|
||||
python3 main.py meta-llama/Llama-3.1-8B
|
||||
|
||||
# Token mode: GITHUB_TOKEN per run (never stored)
|
||||
GITHUB_TOKEN=xxx python3 main.py meta-llama/Llama-3.1-8B
|
||||
|
||||
# Manually specify architecture name (skip config.json fetch)
|
||||
python3 main.py --arch LlamaForCausalLM
|
||||
python3 main.py --framework vllm --arch DeepseekV3ForCausalLM
|
||||
python3 main.py --framework vllm --arch "DeepseekV3ForCausalLM,DeepseekV2ForCausalLM" some-org/some-model
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Flag | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `model_id` | HuggingFace or ModelScope model id (positional; optional when `--arch` is given). |
|
||||
| `--setup` | One-time setup: `local` (clone repos into `.state/repos/`, recommended) or `token`; persists to `.state/state.json`. |
|
||||
| `--reset-state` | Forget saved setup state (cloned repos kept). |
|
||||
| `--doctor` | Check setup state and local checkouts, then exit. |
|
||||
| `--framework` | `sglang`, `vllm`, `vllm-ascend`, or `all` (default: `all`). |
|
||||
| `--source` | `auto`, `hf`, or `modelscope` for reading `config.json`. |
|
||||
| `--arch` | Manually specify architecture name(s), comma-separated. Skips config.json fetch entirely. |
|
||||
| `--token` | GitHub token (or set `GITHUB_TOKEN` env); overrides saved state for this run. |
|
||||
| `--no-docs` | Skip the docs check. |
|
||||
| `--vllm-ref` | vLLM git ref (branch/tag) for the registry check (default: `main`). |
|
||||
| `--vllm-path` | Local vllm repo checkout; overrides saved path for this run. |
|
||||
| `--sglang-path` | Local sglang repo checkout; overrides saved path for this run. |
|
||||
| `--vllm-ascend-path` | Local vllm-ascend repo checkout; overrides saved path for this run. |
|
||||
| `-v, --verbose` | Verbose output. |
|
||||
|
||||
## License
|
||||
|
||||
[The Unlicense](../LICENSE) — released into the public domain.
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Entry point for model-support-checker.
|
||||
|
||||
Usage:
|
||||
python3 main.py --setup local # first run (recommended)
|
||||
python3 main.py --doctor # check setup state
|
||||
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
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from src.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except RuntimeError as e:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -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