dsh-modal-h3
Verified@logictan/dsh-modal-h3 · v0.1.0 · MIT · Web UI
DSH Modal H3: MiniMax H3 Ref2VA video generation on Modal as native mcp__modal_h3__* tools, with a multi-account pool kept in a credential. DSH Modal H3 视频生成子插件。
Install
dsh plugin add @logictan/dsh-modal-h3 Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
@logictan/dsh-modal-h3
MiniMax H3 Ref2VA video generation on Modal, as six native
mcp__modal_h3__* tools — one generation per Modal account, several accounts in
parallel, with the account pool kept in a credential instead of a config file.
Replaces the scripts/modal_h3_*.py + .agents/skills/modal-minimax-h3-video
route: the same contract, the same Modal App, the same accounts, but packaged as
a service any project can call and any device can sync.
What it gives the model
mcp__modal_h3__generate_video mcp__modal_h3__get_generation_task
mcp__modal_h3__list_generation_tasks mcp__modal_h3__cancel_generation_task
mcp__modal_h3__accounts_status mcp__modal_h3__accounts_sync
| Tool | What it does |
|---|---|
generate_video |
Validates the prompt and references, then spawns one generation. dry_run: true stops after the local validation — no network, no writes, no charge. |
get_generation_task |
Polls one task and reports ffprobe evidence once it completes. |
list_generation_tasks |
The local task records, newest first. Never contacts Modal. |
cancel_generation_task |
Cancels a recorded call. GPU time already spent is not refunded. |
accounts_status |
Per-account deployment state and in-flight count. live: true probes Modal; without it you see the last cached probe. |
accounts_sync |
Deploys the App and verifies its model artifacts for one or all accounts. Idempotent, takes up to a few hours, needs pixi on PATH. |
Submit and poll are separate calls, so several accounts can be busy at once:
generate_video with wait_for_completion: false returns as soon as the call is
spawned, and get_generation_task collects it later.
The account pool
Settings → Plugins → modal-h3 takes one JSON value, stored as the
MODAL_H3_ACCOUNTS credential:
{"tokens": [
{"name": "acc1", "token_id": "ak-...", "token_secret": "...", "monthly_credit_usd": 30},
{"name": "acc2", "token_id": "ak-...", "token_secret": "..."}
]}
name is what task records refer to, so renaming an account orphans its
in-flight tasks.
monthly_credit_usd is the account's cycle budget in dollars, declared by hand
because Modal exposes no plan allowance through its API: the billing endpoints
return only metered_cost and billed_cost, and the cycle-budget RPCs need an
environment id that costs a round trip to resolve. Nothing enforces the value. It
is carried in the pool rather than in config so it travels with the tokens on
config sync, and accounts_status reports it as $<n>/cycle so the model can
see which accounts are cheap to spend. An account without it simply says nothing.
To read an account's real spend, run its billing summary:
cd packages/dsh-modal-h3
MODAL_TOKEN_ID=... MODAL_TOKEN_SECRET=... \
pixi exec -s "python=3.11" -s "modal-client==1.5.5" python -m modal billing summary
Credits is the allowance already consumed this cycle, so an account whose
Credits equals its declared monthly_credit_usd is at its limit — that is the
state a spent account shows right before submissions start failing with
workspace billing cycle spend limit reached.
Four properties of this arrangement are deliberate:
- It is a credential, not a config field. A
role('secret')config field would write the tokens into the profile'scordis.patch.ymlin plaintext. As a credential the pool lives in$DSH_HOME/.credentials.yaml, travels with config sync's credentials section, and never appears in a patch row. - It is ONE credential holding N accounts, because credential refs cannot be enumerated — a pool split across N refs could not be listed back, and the card could not show what is configured.
- No token reaches disk. The pool is passed to the child process as
MODAL_TOKEN_ID/MODAL_TOKEN_SECRET;~/.modal.tomlis neither written nor read. Modal's client resolves env before that file, so the pool also overrides whatever login the machine already has. - The card is write-only.
credentials.describereports that a value exists and never returns it, so the textarea is never pre-filled; pasting a new pool replaces the old one wholesale.
Get tokens from https://modal.com/settings/tokens.
Deployment
An account can only generate after its App is deployed. Two different things answer "is this account usable", and they cost very differently:
| Question | Command | Cost |
|---|---|---|
| Is the App deployed? | accounts_status with live: true |
one metadata lookup per account |
| Deploy it (or re-verify it) | accounts_sync |
a rolling deploy plus a full sha256 pass over ~59 GB |
accounts_status is the health check; accounts_sync is not. It is idempotent —
bootstrap skips artifacts whose size and sha256 already match — so re-running it
after a partial failure only re-verifies, but it is still the expensive path.
Deployment state is decided by probing, not by a local record. A fresh device
therefore needs no deploy at all for an account that is already deployed
elsewhere: Function.from_name(...).hydrate() answers that question for any
account. Only a genuinely new account needs accounts_sync once.
Concurrency
Each account's worker is max_containers=1, so one generation per account at a
time; the width of the pool is the parallelism. generate_video picks the least
busy account (ties broken by least recently used) unless account names one.
A task is bound to the account that spawned it — a Modal function call id only resolves inside its own workspace — so removing an account from the pool leaves its in-flight tasks readable but unpollable, with an explicit error rather than a crash. The token for an account is re-read on every poll, so rotating a token in the pool takes effect immediately, including for tasks submitted before the rotation.
Contract
The route is MiniMax H3 Ref2VA Turbo6, and the values below are the vendored contract's, not this package's choices:
| Resolution / rate | 1344x768, 24 fps, 96 output frames, native stereo audio |
| Duration | 4–15 s, quantised to whole frames |
| References | up to 9 images, 3 videos, 3 audios, 12 files total |
| Attempts / budget | 2 attempts, USD 3.00 per generation |
| Modes | Ref2VA only |
The prompt must carry six sections in this order, each heading on its own line as
name: — subject_definitions, summary, retention_analysis,
detailed_description, overall_soundscape, non_diegetic_music — and state
effective duration of N seconds, where N is what dry_run reports. Reference
labels <Picture n> / <Video n> / <Audio n> must first appear in order from 1.
First and last frame targets are best effort only.
T2VA / I2VA / FL2VA / L2VA / Hybrid and exact keyframe locks are not supported.
The plugin announces all of this in the system prompt (src/guidance.js) rather
than only rejecting bad prompts, because a model that learns the shape from a
rejection writes the prompt twice.
Local files
| Path | Contents |
|---|---|
$DSH_HOME/.credentials.yaml |
The account pool, under MODAL_H3_ACCOUNTS |
$DSH_HOME/dsh-modal-h3/tasks/<taskId>/ |
job.json, meta.json, request.json, call.json, output.mp4, report.json |
$DSH_HOME/dsh-modal-h3/pool.json |
Cached probe results and last-used order |
Task directories are named h3-<timestamp>-<hex> and carry no project name —
every project calls this plugin, so nothing here belongs to one of them. out_dir
copies the delivered MP4 and report elsewhere; the task directory keeps its own
copy either way.
Requirements
pixionPATH. The runner executes inside a pinned environment (python=3.11,pillow,ffmpeg,modal-client==1.5.5), resolved and cached on first use — so the first call after install is slower than later ones.dry_runneeds this too: it runs the real admission locally.- A Modal account with the App deployed (
accounts_sync).
app/ and assets/ ship from the package root and are the deployed App itself:
app/modal_h3_app.py and the contract beside it are drop-in replacements for the
copies that used to live in the calling repository. Any fix to the App has to be
made here.
Tests
npm test runs both halves: the JS suite (node --test, covering the pool,
task state, tool definitions, and the runner command line) and the Python suite
(python3 -m unittest discover -s tests, covering reference inspection inside the
vendored contract). The Python tests skip themselves when Pillow or ffprobe is
absent, so a machine without them still gets a green run rather than a false one.
License
MIT. See LICENSE.