longcat: the isolated Python 3.10 venv and its packaging (v0.2.30)

Step 1 of the LongCat-Video integration: provision the runtime. No engine code yet.

LongCat is the strictest isolation in the tree — it pins Python 3.10, torch 2.6+cu124,
transformers 4.41 and numpy 1.26 against the main venv's 3.13 / 2.11+cu130 / 5.x / 2.4,
four simultaneous conflicts, plus opencv-python against the bundled opencv-contrib.
Installing it into the main venv would walk transformers and numpy backwards and break
the stack, the failure already recorded for parler-tts.

Crucially, coderai cannot build this venv itself: every venv bootstrap in codai/ uses
`sys.executable -m venv`, which is 3.13. The only 3.10 in the tree is the standalone
interpreter hand-built for the lip-sync tools and bundled as py310/, so LongCat rides
that same interpreter in a venv of its own (its torch differs, so they cannot share one).

- requirements-longcat.txt: upstream's pins off the cu124 index. flash-attn is
  deliberately NOT pinned — it is a source build needing ninja and a CUDA toolchain, and
  upstream accepts xformers, which ships a wheel; longcat.attention = "flash" is the
  opt-in. streamlit (upstream's demo UI) is left out.
- build_oci_image.sh: declares LONGCAT_VENV and bundles it. The py3.10 interpreter is now
  discovered from EITHER py3.10 venv's pyvenv.cfg — it was read only from the lip-sync
  one, so a host with LongCat and no lip-sync venv would have bundled a venv with no
  interpreter to run it.
- Dockerfile.oci-venv: copies longcat_venv and the pyvenv.cfg/bin-symlink rewrite now
  loops over both venvs. A bundled venv still carries the build host's `home =`, so
  without that its bin/python points at an interpreter the image does not have.
- smoke_test_services.sh: asserts 3.10 + torch 2.6 + transformers 4.41, not just that
  torch imports — a venv re-pointed at the WRONG interpreter passes a bare import while
  being useless.
- LongcatConfig (enabled, service_url, venv, python, auto_build, host, port, gpu,
  attention, ready_timeout, extra_args, extra_env), attached to Config, serialised by
  save_config, reconstructed in load() and exposed/parsed by the admin settings API.
  auto_build ships OFF: it pulls torch 2.6 plus its CUDA runtime.

16 tests. The config round-trip one caught a real hole: load() reconstructs each section
explicitly, so without its line the operator could set LongCat up in the interface and
lose it on the next restart.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182DhR1eFNPQyGmDrpHbedY
parent 8ae02a35
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.2.29" __version__ = "0.2.30"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -3778,6 +3778,20 @@ def build_settings_dict(c, gpu_cards): ...@@ -3778,6 +3778,20 @@ def build_settings_dict(c, gpu_cards):
"tp_size": c.ktransformers.tp_size, "tp_size": c.ktransformers.tp_size,
"nodes": list(c.ktransformers.nodes or []), "nodes": list(c.ktransformers.nodes or []),
}, },
"longcat": {
"enabled": c.longcat.enabled,
"service_url": c.longcat.service_url,
"venv": c.longcat.venv,
"python": c.longcat.python,
"auto_build": c.longcat.auto_build,
"host": c.longcat.host,
"port": c.longcat.port,
"gpu": c.longcat.gpu,
"attention": c.longcat.attention,
"ready_timeout": c.longcat.ready_timeout,
"extra_args": c.longcat.extra_args,
"extra_env": c.longcat.extra_env,
},
"vllm": { "vllm": {
"enabled": c.vllm.enabled, "enabled": c.vllm.enabled,
"service_url": c.vllm.service_url, "service_url": c.vllm.service_url,
...@@ -4398,6 +4412,28 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad ...@@ -4398,6 +4412,28 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
from codai.cluster.multinode import parse_nodes from codai.cluster.multinode import parse_nodes
c.ktransformers.nodes = parse_nodes(d.get("nodes")) c.ktransformers.nodes = parse_nodes(d.get("nodes"))
if "longcat" in data:
d = data["longcat"]
lc = c.longcat
lc.enabled = bool(d.get("enabled", lc.enabled))
for _k in ("service_url", "venv", "python", "gpu", "attention",
"extra_args", "extra_env"):
if _k in d:
setattr(lc, _k, (d.get(_k) or "").strip())
if "host" in d: lc.host = (d.get("host") or "127.0.0.1").strip()
if "auto_build" in d: lc.auto_build = bool(d.get("auto_build"))
if "port" in d:
try:
lc.port = max(0, int(d.get("port") or 0))
except (TypeError, ValueError):
pass
if "ready_timeout" in d:
try:
lc.ready_timeout = max(0.0, float(d.get("ready_timeout") or 0.0))
except (TypeError, ValueError):
pass
if lc.attention not in ("xformers", "flash", "sdpa"):
lc.attention = "xformers"
if "vllm" in data: if "vllm" in data:
d = data["vllm"] d = data["vllm"]
if "service_url" in d: if "service_url" in d:
......
...@@ -743,6 +743,52 @@ class RunpodConfig: ...@@ -743,6 +743,52 @@ class RunpodConfig:
global_cost_period: str = "unlimited" # hour | day | week | month | unlimited global_cost_period: str = "unlimited" # hour | day | week | month | unlimited
@dataclass
class LongcatConfig:
"""LongCat-Video (Meituan, 13.6B dense, MIT) — driven as a managed subprocess.
LongCat does text-to-video, image-to-video, video-continuation, long-video,
interactive and audio-driven avatar generation, in three coarse-to-fine stages
(base 480p → distilled LoRA → 720p refinement LoRA). There is no diffusers pipeline
for it (diffusers merged LongCat-*Image*, not the video model), so coderai drives the
upstream repo's own ``LongCatVideoPipeline`` out of an ISOLATED venv.
That venv is the strictest in the tree: Python 3.10, torch 2.6+cu124,
transformers 4.41 and numpy 1.26 — four simultaneous conflicts with the main venv
(3.13 / 2.11+cu130 / 5.x / 2.4). See requirements-longcat.txt.
coderai CANNOT build this venv by itself: every venv bootstrap in ``codai/`` uses
``sys.executable -m venv``, which is 3.13. The image bundles a standalone 3.10
(shared with the lip-sync tools, at /opt/coderai/py310) and the built venv; on a
source install, point ``python`` at a 3.10 interpreter or build the venv by hand.
Like the other engines this serves MODELS FROM THE MODEL LIST: tag an entry
``backend: "longcat"``. Per-model settings (variant, quantisation, segments,
context-parallel size) live on the entry; what is here is how to reach the runtime.
"""
enabled: bool = False
# Point this at a LongCat service already running elsewhere (another host, a
# container, a rented pod) and coderai proxies to it instead of building,
# downloading or launching anything locally. Blank = manage it here.
service_url: str = ""
venv: str = "" # isolated venv dir; blank = auto (baked/cache/home)
python: str = "" # a Python 3.10 interpreter for building the venv;
# blank = the bundled /opt/coderai/py310, else PATH
auto_build: bool = False # build the venv on first use (needs `python` above).
# OFF by default: it pulls torch 2.6 + its CUDA runtime,
# which is minutes and gigabytes on a first request.
host: str = "127.0.0.1"
port: int = 0 # 0 = pick a free port per service
gpu: str = "" # CUDA device(s) (CUDA_VISIBLE_DEVICES); blank = all
# FlashAttention-2 is upstream's default but is a source build needing ninja and a
# CUDA toolchain; xformers ships a wheel and is what requirements-longcat.txt
# installs. "flash" assumes you installed flash-attn into the venv yourself.
attention: str = "xformers" # xformers | flash | sdpa
ready_timeout: float = 1800.0 # a 13.6B DiT load is not quick
extra_args: str = "" # appended to the service command line
extra_env: str = "" # KEY=VALUE pairs for the service process
@dataclass @dataclass
class OcrConfig: class OcrConfig:
"""Dedicated OCR subsystem configuration. """Dedicated OCR subsystem configuration.
...@@ -911,6 +957,7 @@ class Config: ...@@ -911,6 +957,7 @@ class Config:
k3: K3Config = field(default_factory=K3Config) k3: K3Config = field(default_factory=K3Config)
ktransformers: KtransformersConfig = field(default_factory=KtransformersConfig) ktransformers: KtransformersConfig = field(default_factory=KtransformersConfig)
vllm: VllmConfig = field(default_factory=VllmConfig) vllm: VllmConfig = field(default_factory=VllmConfig)
longcat: LongcatConfig = field(default_factory=LongcatConfig)
runpod: RunpodConfig = field(default_factory=RunpodConfig) runpod: RunpodConfig = field(default_factory=RunpodConfig)
ocr: OcrConfig = field(default_factory=OcrConfig) ocr: OcrConfig = field(default_factory=OcrConfig)
compaction: CompactionConfig = field(default_factory=CompactionConfig) compaction: CompactionConfig = field(default_factory=CompactionConfig)
...@@ -1104,6 +1151,7 @@ class ConfigManager: ...@@ -1104,6 +1151,7 @@ class ConfigManager:
k3=_dc(K3Config, config_data.get("k3", {})), k3=_dc(K3Config, config_data.get("k3", {})),
ktransformers=_dc(KtransformersConfig, config_data.get("ktransformers", {})), ktransformers=_dc(KtransformersConfig, config_data.get("ktransformers", {})),
vllm=_dc(VllmConfig, config_data.get("vllm", {})), vllm=_dc(VllmConfig, config_data.get("vllm", {})),
longcat=_dc(LongcatConfig, config_data.get("longcat", {})),
runpod=_dc(RunpodConfig, config_data.get("runpod", {})), runpod=_dc(RunpodConfig, config_data.get("runpod", {})),
ocr=_dc(OcrConfig, config_data.get("ocr", {})), ocr=_dc(OcrConfig, config_data.get("ocr", {})),
compaction=_dc(CompactionConfig, config_data.get("compaction", {})), compaction=_dc(CompactionConfig, config_data.get("compaction", {})),
...@@ -1370,6 +1418,20 @@ class ConfigManager: ...@@ -1370,6 +1418,20 @@ class ConfigManager:
"tp_size": self.config.ktransformers.tp_size, "tp_size": self.config.ktransformers.tp_size,
"nodes": list(self.config.ktransformers.nodes or []), "nodes": list(self.config.ktransformers.nodes or []),
}, },
"longcat": {
"enabled": self.config.longcat.enabled,
"service_url": self.config.longcat.service_url,
"venv": self.config.longcat.venv,
"python": self.config.longcat.python,
"auto_build": self.config.longcat.auto_build,
"host": self.config.longcat.host,
"port": self.config.longcat.port,
"gpu": self.config.longcat.gpu,
"attention": self.config.longcat.attention,
"ready_timeout": self.config.longcat.ready_timeout,
"extra_args": self.config.longcat.extra_args,
"extra_env": self.config.longcat.extra_env,
},
"vllm": { "vllm": {
"enabled": self.config.vllm.enabled, "enabled": self.config.vllm.enabled,
"service_url": self.config.vllm.service_url, "service_url": self.config.vllm.service_url,
......
...@@ -58,17 +58,19 @@ RUN set -eux; \ ...@@ -58,17 +58,19 @@ RUN set -eux; \
mkdir -p /opt/coderai/py310; \ mkdir -p /opt/coderai/py310; \
rsync -a /tmp/local-bundle/py310/ /opt/coderai/py310/; \ rsync -a /tmp/local-bundle/py310/ /opt/coderai/py310/; \
fi; \ fi; \
for d in lipsync_venv Wav2Lip SadTalker ds4 colibri kimi-k3-in-c; do \ for d in lipsync_venv longcat_venv Wav2Lip SadTalker ds4 colibri kimi-k3-in-c; do \
if [ -d "/tmp/local-bundle/$d" ]; then \ if [ -d "/tmp/local-bundle/$d" ]; then \
mkdir -p "/opt/coderai/$d"; \ mkdir -p "/opt/coderai/$d"; \
rsync -a "/tmp/local-bundle/$d/" "/opt/coderai/$d/"; \ rsync -a "/tmp/local-bundle/$d/" "/opt/coderai/$d/"; \
fi; \ fi; \
done; \ done; \
cfg="/opt/coderai/lipsync_venv/pyvenv.cfg"; \ for v in lipsync_venv longcat_venv; do \
if [ -f "$cfg" ]; then \ cfg="/opt/coderai/$v/pyvenv.cfg"; \
sed -i 's|^home *=.*|home = /opt/coderai/py310/bin|; s|^command *=.*|command = /opt/coderai/py310/bin/python3.10|' "$cfg"; \ if [ -f "$cfg" ]; then \
for p in python python3 python3.10; do ln -sf /opt/coderai/py310/bin/python3.10 "/opt/coderai/lipsync_venv/bin/$p"; done; \ sed -i 's|^home *=.*|home = /opt/coderai/py310/bin|; s|^command *=.*|command = /opt/coderai/py310/bin/python3.10|' "$cfg"; \
fi; \ for p in python python3 python3.10; do ln -sf /opt/coderai/py310/bin/python3.10 "/opt/coderai/$v/bin/$p"; done; \
fi; \
done; \
if [ -d /tmp/local-bundle/local-bin ]; then \ if [ -d /tmp/local-bundle/local-bin ]; then \
mkdir -p /opt/coderai/staged-local-bin; \ mkdir -p /opt/coderai/staged-local-bin; \
rsync -a /tmp/local-bundle/local-bin/ /opt/coderai/staged-local-bin/; \ rsync -a /tmp/local-bundle/local-bin/ /opt/coderai/staged-local-bin/; \
......
...@@ -30,6 +30,9 @@ INCLUDE_TOOLS=1 ...@@ -30,6 +30,9 @@ INCLUDE_TOOLS=1
# halving the torch footprint. Repo code is bundled WITHOUT model weights — those # halving the torch footprint. Repo code is bundled WITHOUT model weights — those
# download on first lip-sync use into the /cache volume. # download on first lip-sync use into the /cache volume.
LIPSYNC_VENV="${CODERAI_LIPSYNC_VENV:-$HOME/.coderai/lipsync_venv}" LIPSYNC_VENV="${CODERAI_LIPSYNC_VENV:-$HOME/.coderai/lipsync_venv}"
# LongCat-Video rides the same standalone Python 3.10 as the lip-sync tools, in a venv
# of its own (its torch 2.6+cu124 differs, so the venvs cannot be shared).
LONGCAT_VENV="${CODERAI_LONGCAT_VENV:-$HOME/.coderai/longcat_venv}"
WAV2LIP_DIR="${CODERAI_WAV2LIP_SRC:-$HOME/.coderai/Wav2Lip}" WAV2LIP_DIR="${CODERAI_WAV2LIP_SRC:-$HOME/.coderai/Wav2Lip}"
SADTALKER_DIR="${CODERAI_SADTALKER_SRC:-$HOME/.coderai/SadTalker}" SADTALKER_DIR="${CODERAI_SADTALKER_SRC:-$HOME/.coderai/SadTalker}"
DS4_DIR="${CODERAI_DS4_DIR:-$HOME/.coderai/ds4}" DS4_DIR="${CODERAI_DS4_DIR:-$HOME/.coderai/ds4}"
...@@ -383,25 +386,32 @@ prepare_venv_bundle() { ...@@ -383,25 +386,32 @@ prepare_venv_bundle() {
fi fi
fi fi
# Isolated lip-sync tools + ds4 engine. The two venvs share one standalone # Isolated lip-sync tools, LongCat-Video and the ds4 engine. The py3.10 venvs share
# Python 3.10 (read from a venv's pyvenv.cfg `home`); it's bundled once and the # one standalone Python 3.10 (read from whichever venv's pyvenv.cfg `home` is present);
# venvs are re-pointed at it during the image build. # it's bundled once and every venv is re-pointed at it during the image build.
if [[ "$INCLUDE_TOOLS" == "1" ]]; then if [[ "$INCLUDE_TOOLS" == "1" ]]; then
local py310_dir="" local py310_dir=""
if [[ -f "$LIPSYNC_VENV/pyvenv.cfg" ]]; then local _v
local home_bin for _v in "$LIPSYNC_VENV" "$LONGCAT_VENV"; do
home_bin="$(sed -n 's/^home *= *//p' "$LIPSYNC_VENV/pyvenv.cfg" | head -1)" [[ -n "$py310_dir" ]] && break
[[ -n "$home_bin" ]] && py310_dir="$(dirname "$home_bin")" if [[ -f "$_v/pyvenv.cfg" ]]; then
fi local home_bin
home_bin="$(sed -n 's/^home *= *//p' "$_v/pyvenv.cfg" | head -1)"
[[ -n "$home_bin" ]] && py310_dir="$(dirname "$home_bin")"
fi
done
if [[ -n "$py310_dir" && -d "$py310_dir" ]]; then if [[ -n "$py310_dir" && -d "$py310_dir" ]]; then
mkdir -p "$bundle/py310" mkdir -p "$bundle/py310"
rsync -a "$py310_dir/" "$bundle/py310/" rsync -a "$py310_dir/" "$bundle/py310/"
echo "Bundled standalone Python 3.10 from: $py310_dir" echo "Bundled standalone Python 3.10 from: $py310_dir"
else else
echo "Warning: could not locate the py3.10 interpreter for the lip-sync venv" >&2 echo "Warning: could not locate a py3.10 interpreter (lip-sync / LongCat venvs)" >&2
fi fi
local _venv_excl=(--exclude '__pycache__' --exclude '*.pyc' --exclude 'pip/' --exclude '*.dist-info/RECORD') local _venv_excl=(--exclude '__pycache__' --exclude '*.pyc' --exclude 'pip/' --exclude '*.dist-info/RECORD')
if [[ -d "$LIPSYNC_VENV" ]]; then rsync -a "${_venv_excl[@]}" "$LIPSYNC_VENV/" "$bundle/lipsync_venv/"; echo "Bundled shared lip-sync venv"; fi if [[ -d "$LIPSYNC_VENV" ]]; then rsync -a "${_venv_excl[@]}" "$LIPSYNC_VENV/" "$bundle/lipsync_venv/"; echo "Bundled shared lip-sync venv"; fi
# LongCat's venv carries its own torch 2.6+cu124; weights are NOT bundled (they
# download at runtime into the /cache volume, like every other model).
if [[ -d "$LONGCAT_VENV" ]]; then rsync -a "${_venv_excl[@]}" "$LONGCAT_VENV/" "$bundle/longcat_venv/"; echo "Bundled LongCat-Video venv"; fi
# Repo CODE ONLY — checkpoints/weights are excluded and download at runtime. # Repo CODE ONLY — checkpoints/weights are excluded and download at runtime.
if [[ -d "$WAV2LIP_DIR" ]]; then rsync -a --exclude 'checkpoints/' --exclude 'face_detection/detection/sfd/*.pth' "$WAV2LIP_DIR/" "$bundle/Wav2Lip/"; echo "Bundled Wav2Lip code (no weights)"; fi if [[ -d "$WAV2LIP_DIR" ]]; then rsync -a --exclude 'checkpoints/' --exclude 'face_detection/detection/sfd/*.pth' "$WAV2LIP_DIR/" "$bundle/Wav2Lip/"; echo "Bundled Wav2Lip code (no weights)"; fi
if [[ -d "$SADTALKER_DIR" ]]; then rsync -a --exclude 'checkpoints/*' --exclude 'gfpgan/weights/*' "$SADTALKER_DIR/" "$bundle/SadTalker/"; echo "Bundled SadTalker code (no weights)"; fi if [[ -d "$SADTALKER_DIR" ]]; then rsync -a --exclude 'checkpoints/*' --exclude 'gfpgan/weights/*' "$SADTALKER_DIR/" "$bundle/SadTalker/"; echo "Bundled SadTalker code (no weights)"; fi
......
...@@ -77,6 +77,21 @@ if "${DK[@]}" exec "$NAME" /opt/coderai/lipsync_venv/bin/python -c "import torch ...@@ -77,6 +77,21 @@ if "${DK[@]}" exec "$NAME" /opt/coderai/lipsync_venv/bin/python -c "import torch
else else
bad "lipsync venv" "python/torch import failed" bad "lipsync venv" "python/torch import failed"
fi fi
echo "== LongCat-Video venv (py3.10 + its own torch 2.6/cu124) =="
if "${DK[@]}" exec "$NAME" sh -lc "test -d /opt/coderai/longcat_venv"; then
# Its torch is NOT the main venv's: assert the version it pins, or a venv that was
# re-pointed at the wrong interpreter would pass a bare "import torch" while being
# unusable for LongCat.
if "${DK[@]}" exec "$NAME" /opt/coderai/longcat_venv/bin/python -c \
"import sys,torch,transformers; assert sys.version_info[:2]==(3,10), sys.version; assert torch.__version__.startswith('2.6'), torch.__version__; assert transformers.__version__.startswith('4.41'), transformers.__version__" >/dev/null 2>&1; then
ok "longcat venv imports py3.10 + torch 2.6 + transformers 4.41"
else
bad "longcat venv" "py3.10/torch-2.6/transformers-4.41 check failed"
fi
else
echo " (skipped: no longcat venv in this image)"
fi
# Repo code is bundled; weights are NOT (download on first lip-sync use). # Repo code is bundled; weights are NOT (download on first lip-sync use).
if "${DK[@]}" exec "$NAME" sh -lc "test -f /opt/coderai/Wav2Lip/inference.py && test -f /opt/coderai/SadTalker/inference.py"; then if "${DK[@]}" exec "$NAME" sh -lc "test -f /opt/coderai/Wav2Lip/inference.py && test -f /opt/coderai/SadTalker/inference.py"; then
ok "lip-sync repo code present" ok "lip-sync repo code present"
......
# ---------------------------------------------------------------------------
# CoderAI — LongCat-Video (Meituan, 13.6B dense, MIT), ISOLATED venv on Python 3.10.
#
# This is the strictest isolation in the tree. LongCat pins FOUR things against the main
# coderai venv at once, so nothing here can be relaxed into it:
#
# main venv LongCat
# Python 3.13.x 3.10 (its pins do not resolve on 3.13)
# torch 2.11.x+cu130 2.6.0+cu124
# transformers 5.x 4.41.0 (a MAJOR version back)
# numpy 2.4.x 1.26.4
# opencv opencv-contrib opencv-python
#
# Installing it into the main venv would walk transformers and numpy backwards and break
# the stack — the failure already recorded for parler-tts. coderai therefore runs it as a
# managed subprocess (tools/longcat_service.py) out of this venv, the same shape as the
# OCR Paddle/Surya engines, ktransformers/SGLang and vLLM.
#
# The interpreter is the standalone Python 3.10 already bundled for the lip-sync tools
# (packaging/linux/build_oci_image.sh reads it out of the venv's pyvenv.cfg and ships it
# as py310/). It CANNOT be built by coderai itself: every venv bootstrap in codai/ uses
# `sys.executable -m venv`, which is 3.13. So:
#
# /opt/coderai/py310/bin/python3.10 -m venv <dir>
# <dir>/bin/python -m pip install -r requirements-longcat.txt
# (or set longcat.auto_build = true to have coderai build it on first use, which still
# needs a 3.10 interpreter on PATH or at longcat.python)
#
# Always `python -m pip`, never the venv's pip script: a copied or re-pointed venv
# carries a stale shebang and would install into the ORIGINAL interpreter.
#
# ATTENTION: this file is the single source of truth for the venv. Adding a dependency
# here is not enough on its own — the image bundles the BUILT venv, so rebuild it locally
# and re-run packaging/linux/build_oci_image.sh.
# ---------------------------------------------------------------------------
--extra-index-url https://download.pytorch.org/whl/cu124
torch==2.6.0
torchvision==0.21.0
torchaudio==2.6.0
# Upstream's pins. transformers and numpy are the two that cannot move.
transformers==4.41.0
numpy==1.26.4
diffusers==0.35.1
einops==0.8.0
ftfy==6.2.0
loguru==0.7.2
psutil==6.0.0
av==12.0.0
opencv-python==4.9.0.80
imageio==2.37.0
imageio-ffmpeg==0.6.0
pyarrow==20.0.0
# Attention backend. Upstream defaults to FlashAttention-2 but documents FA3 and xformers
# as alternatives, and flash-attn==2.7.4.post1 is a SOURCE build needing ninja and a CUDA
# toolchain — minutes of compile, and it fails outright on a host without nvcc wired up.
# xformers ships a wheel for this torch, so it is the default here; set
# longcat.attention = "flash" and install flash-attn by hand when the toolchain is there.
xformers==0.0.29.post2
# NOT installed: streamlit (upstream's demo UI — coderai serves the API itself).
"""Step 1 of the LongCat-Video integration: the isolated Python 3.10 venv and its
packaging.
LongCat is the strictest isolation in the tree — Python 3.10, torch 2.6+cu124,
transformers 4.41 and numpy 1.26, four simultaneous conflicts with the main venv
(3.13 / 2.11+cu130 / 5.x / 2.4). Nothing in codai/ can even create the venv, because
every bootstrap there uses `sys.executable -m venv`. So the interpreter is the standalone
3.10 already bundled for the lip-sync tools, and these tests pin the contract that makes
that work: the pins that must not drift, and the four packaging hooks that carry the venv
into the image.
No venv, no weights, no GPU: this reads the declared files.
"""
import re
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from codai.config import Config, LongcatConfig
REQ = (ROOT / "requirements-longcat.txt").read_text()
def _pin(name: str):
m = re.search(rf"^{re.escape(name)}==([^\s#]+)", REQ, re.M)
return m.group(1) if m else None
# ------------------------------------------------------------ the pins
def test_the_four_conflicting_pins_are_exact():
"""These are the reason the venv exists. A range here would let pip resolve them to
the main venv's versions and the isolation would quietly stop being isolation."""
assert _pin("torch") == "2.6.0"
assert _pin("transformers") == "4.41.0"
assert _pin("numpy") == "1.26.4"
assert _pin("diffusers") == "0.35.1"
def test_it_installs_from_the_cu124_index():
"""torch 2.6.0 from PyPI is not the cu124 build; LongCat's CUDA stack is 12.4."""
assert "download.pytorch.org/whl/cu124" in REQ
def test_flash_attn_is_not_installed_by_default():
"""flash-attn==2.7.4.post1 is a SOURCE build needing ninja and a CUDA toolchain, and
upstream accepts xformers instead. Pinning it here would make the venv unbuildable on
any host without nvcc wired up."""
assert not re.search(r"^flash-attn==", REQ, re.M)
assert _pin("xformers") is not None
def test_streamlit_is_not_installed():
"""Upstream's demo UI. coderai serves the API itself."""
assert not re.search(r"^streamlit==", REQ, re.M)
def test_the_opencv_clash_is_recorded():
"""opencv-python against the bundled opencv-contrib is the fourth conflict; it is
installed deliberately IN THIS VENV."""
assert _pin("opencv-python") == "4.9.0.80"
# ------------------------------------------------------------ the config section
def test_the_config_section_is_attached():
c = Config()
assert isinstance(c.longcat, LongcatConfig)
def test_it_ships_disabled_and_does_not_auto_build():
"""auto_build pulls torch 2.6 plus its CUDA runtime — minutes and gigabytes on a
first request, so the operator opts in."""
lc = LongcatConfig()
assert lc.enabled is False
assert lc.auto_build is False
def test_the_attention_default_matches_what_the_requirements_install():
assert LongcatConfig().attention == "xformers"
def test_the_ready_timeout_allows_for_a_13b_load():
assert LongcatConfig().ready_timeout >= 600.0
def test_the_remote_escape_hatch_exists():
"""A service already running elsewhere (a pod, another host) must be reachable
without building or downloading anything locally."""
assert LongcatConfig().service_url == ""
assert hasattr(LongcatConfig(), "service_url")
def test_the_section_round_trips_through_config_json(tmp_path):
"""Save and reload: a field that save_config does not write is one the operator can
set in the interface and lose on the next restart."""
from codai.config import ConfigManager
cm = ConfigManager(str(tmp_path))
cm.config = cm.load()
cm.config.longcat.enabled = True
cm.config.longcat.venv = "/opt/coderai/longcat_venv"
cm.config.longcat.python = "/opt/coderai/py310/bin/python3.10"
cm.config.longcat.auto_build = True
cm.config.longcat.attention = "flash"
cm.config.longcat.ready_timeout = 2400.0
cm.config.longcat.service_url = "http://pod:8000"
cm.config.longcat.extra_env = "NCCL_DEBUG=WARN"
cm.save_config()
again = ConfigManager(str(tmp_path)).load()
assert again.longcat.enabled is True
assert again.longcat.venv == "/opt/coderai/longcat_venv"
assert again.longcat.python == "/opt/coderai/py310/bin/python3.10"
assert again.longcat.auto_build is True
assert again.longcat.attention == "flash"
assert again.longcat.ready_timeout == 2400.0
assert again.longcat.service_url == "http://pod:8000"
assert again.longcat.extra_env == "NCCL_DEBUG=WARN"
def test_the_settings_api_exposes_the_section():
"""The admin settings route is what the interface reads; a section missing there
cannot be configured from the UI at all."""
src = (ROOT / "codai/admin/routes.py").read_text()
assert '"longcat": {' in src
assert 'if "longcat" in data:' in src # and it is parsed back on save
# ------------------------------------------------------------ packaging hooks
BUILD = (ROOT / "packaging/linux/build_oci_image.sh").read_text()
DOCKER = (ROOT / "packaging/linux/Dockerfile.oci-venv").read_text()
SMOKE = (ROOT / "packaging/linux/smoke_test_services.sh").read_text()
def test_the_build_declares_and_bundles_the_venv():
assert "LONGCAT_VENV=" in BUILD
assert 'rsync -a "${_venv_excl[@]}" "$LONGCAT_VENV/" "$bundle/longcat_venv/"' in BUILD
def test_the_interpreter_is_found_from_either_py310_venv():
"""It used to be read only from the lip-sync venv's pyvenv.cfg, so a host with
LongCat but no lip-sync venv would bundle the venv and no interpreter for it."""
assert 'for _v in "$LIPSYNC_VENV" "$LONGCAT_VENV"' in BUILD
def test_the_image_copies_the_venv_and_repoints_it():
"""A bundled venv still carries the BUILD host's `home =`; without the rewrite its
bin/python points at an interpreter that does not exist in the image."""
assert "for d in lipsync_venv longcat_venv" in DOCKER
assert "for v in lipsync_venv longcat_venv" in DOCKER
assert "home = /opt/coderai/py310/bin" in DOCKER
def test_the_smoke_test_checks_the_versions_not_just_the_import():
"""A venv re-pointed at the WRONG interpreter passes a bare `import torch` while
being useless for LongCat, so the check asserts 3.10 / 2.6 / 4.41."""
assert "longcat_venv" in SMOKE
assert "(3,10)" in SMOKE
assert "'2.6'" in SMOKE
assert "'4.41'" in SMOKE
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment