jz OpenRouter VLM
A captioner that fails on purpose
- image
- text
- cost
This is the node that makes the pack worth installing even if you never touch the other ten: a vision-language model call inside your ComfyUI graph, routed through OpenRouter so you get one API key and a menu of frontier models instead of five provider accounts. Feed it an image (or a batch), get back a caption or a structured description - wire that into a prompt enhancer, a caption saver, or a metadata pipeline. This is the KB's "drop a VLM into the graph as a tool" pattern, not the frozen text encoder inside your checkpoint.
The name tells you the important part: the node's personality is in its error handling. Most API nodes in this ecosystem return an error string downstream, which then gets treated as a prompt - garbage in, garbage on. This one raises on failure instead of passing bad text through, and it detects truncated answers (finish_reason: length) and raises on those too, because the author correctly notes a half-written caption "quietly poisons whatever consumes it." An API node that fails loudly is worth its weight in debugging time.
What you actually set
- instruction - the system prompt; default "describe this image in detail." This is where you steer the job: "return JSON with subject, style, mood," "write a LoRA caption," etc.
- model - a preset dropdown:
anthropic/claude-opus-4.8,google/gemini-3.5-flash(default),google/gemini-3.1-flash-lite-preview, orcustom(which unlocks thecustom_modelfield for any OpenRouter slug). Model IDs drift, so expect to type your own. - max_tokens - cap on the answer (default 1000). The
reasoningdropdown matters here: it defaults tolowbecause reasoning models burn tokens on hidden thinking and return truncated answers otherwise - the README calls this out, and the node raises if truncation still happens.
The genuinely useful hidden knobs: max_edge (default 256) downscales images to that long edge before upload - OpenRouter rejects giant payloads, and 256 is plenty for captioning. json_output forces a valid-JSON response (with markdown fences stripped if the model wraps them anyway). content is a forceInput text socket so you can pipe in context. And seed behaves like a sampler seed - randomize to vary, fix to reproduce.
The outputs are text and cost (a string like $0.0032) - wire the cost into a logger and watch your spend grow in real time.
Keys, the security-sensitive part
The API key is resolved server-side: node api_key input first, then the OPENROUTER_API_KEY env var, then a config.ini sitting in the pack root:
[API]
OPENROUTER_API_KEY=sk-or-...
The author's rule is deliberate: keys should live in config.ini or an env var, never in the workflow file - workflows get shared, and a baked-in key is a leaked key. This is the security frame the KB's API-node doc pounds on: an API node is arbitrary Python that holds a credential and makes network calls by design, the exact category that's been weaponized before. This pack is small and open-source, so reading its ~7KB VLM module is a reasonable audit. And know what you're signing up for: every call sends your image and prompt to OpenRouter's servers, costs money per call, and the model enforces its own content policy.
How the call itself works
The node downscales images, packs the instruction + content + every frame of a batch (a batch of N frames becomes N images in one call, so "describe both images" just works), and sends it with a pooled session. Retries happen on 429/5xx and network errors, honoring Retry-After; permanent 4xx fail immediately rather than hammering a bad request. A notably more careful HTTP client than most API nodes.
Installing it
cd ComfyUI/custom_nodes
git clone https://github.com/j-zhang19/comfyui-jz
Restart ComfyUI (or Manager → search "comfyui-jz"), find it under jz/api. Deps are the pack's usual requests, pillow, numpy - no models, no GPU. You do need an OpenRouter account and key.
Common issues
Most failures are the loud ones the node was built to make: no key → a clear runtime error telling you exactly where to put it; both image and content empty → error (connect at least one); truncated answer → error telling you to raise max_tokens or lower reasoning effort. The subtle one is expecting a batch to behave like a single image - every frame goes into the one call, so a 12-frame batch is a 12-image prompt and the token cost climbs fast. Watch the cost output if your batches get big.
Solo-author personal pack, zero community footprint, so the README plus this module's source comments are your docs - but for the flagship node in the pack, the code is genuinely well-commented and the failure behavior is unusually honest.
Inputs (11)
| Name | Type | Default | Description |
|---|---|---|---|
| instruction | STRING | describe this image in detail | — |
| model | COMBO | google/gemini-3.5-flash | 4 options: anthropic/claude-opus-4.8, google/gemini-3.5-flash, google/gemini-3.1-flash-lite-preview, custom |
| max_tokens | INT | 10001–32768 | — |
| imageopt | IMAGE | — | |
| contentopt | STRING | — | |
| custom_modelopt | STRING | — | |
| api_keyopt | STRING | — | |
| max_edgeopt | INT | 25664–8192 | images are downscaled to this long edge before upload |
| seedopt | INT | 00–2147483647 | — |
| reasoningopt | COMBO | low | 4 options: default, low, medium, high |
| json_outputopt | BOOLEAN | false | force the model to answer with valid json |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| text | STRING | — |
| cost | STRING | — |