openrouter_node
One node, one API key, and every hosted model OpenRouter sells — inside your graph
- pdf_data
- audio_data
- Output
- image
- Stats
- Credits
- video
This is the node that does not run a model. It is an HTTP client with your key in it - you fill in a prompt, it phones OpenRouter, and whatever comes back (text, an image, or a video) drops into the workflow like any other node output. The pack registers the single class as both OpenRouterNode and openrouter_node.
Why you'd want it: an LLM in the graph is routine by now, and OpenRouter is the cheap way to get one without a second server. The usual jobs are rewriting a rough idea into a structured prompt, captioning images for a LoRA set, and reaching closed models you were never allowed to download - Nano Banana and friends - without leaving ComfyUI.
How it works
Under the hood it's requests against openrouter.ai/api/v1/chat/completions, plus /images and /videos for the other request types. Three catalogs (/models, /images/models, /videos/models) are fetched on a background thread and cached, so the model dropdown fills itself without freezing the canvas.
The routing is done by appending suffixes to the model ID:
web_searchappends:onlinecheapest(on by default) appends:floorfastestappends:nitro
Look at the order in the source, because it's a trap: if :online isn't already on the ID, cheapest is checked first and fastest only applies when cheapest is off. Turn both on and you quietly get floor routing. Pick one.
Your key resolves as UI field, then LLM_KEY env var, then openrouter_api_key.json in the node directory. The node clears the key from saved widget values, but that's damage control, not safety.
The inputs that matter
Most of the required list you can ignore forever. For chat you set four things: api_key (blank if you're using the env var or JSON file), system_prompt (be specific - it's a chat model), user_message_box (or wire in user_message_input instead), and model.
Then the knobs worth knowing: temperature (0–2, slider), reasoning_effort (auto sends no override, none disables thinking, minimal through xhigh request more where supported), chat_mode for multi-turn context, and request_timeout (120s default).
request_type changes everything else. On image you also get aspect_ratio, image_resolution (1K/2K/4K), image_quality, image_background and seed, validated against what the chosen model advertises - an unsupported explicit setting fails before it costs you anything. On video you get video_mode (text_to_video, first_frame, first_last_frame, reference_images), video_duration, video_resolution, video_generate_audio, video_wait_timeout and video_job_id.
Media inputs are optional and self-revealing: image_1 is always visible, image_2 through image_10 appear as you connect images, and pdf_data with pdf_engine (auto, mistral-ocr, pdf-text) handles documents. audio_data takes one Load Audio clip, chat mode only.
The outputs
Output is the text response - connect a ShowText node to read it. image is an IMAGE tensor when the response contains a generated image, and an empty 1×1 placeholder when it doesn't. Stats carries TPS, token counts, temperature, the resolved model, and cost or tier when the API reports them. Credits is your remaining balance ("Remaining: $9.792"). video goes to Save Video - that one needs ComfyUI 0.3.31+ with PyAV, though chat and image keep working on older installs.
Install
ComfyUI Manager, search the pack title "ComfyUI OpenRouter Node" (registry ID gabe-init/ComfyUI-Openrouter_node). Or by hand:
cd ComfyUI/custom_nodes
git clone https://github.com/gabe-init/ComfyUI-Openrouter_node
pip install -r requirements.txt # tiktoken, Pillow; requests ships with ComfyUI
Restart ComfyUI, drop in an API key from openrouter.ai, and run. Put it in a file rather than the widget if you ever share workflows:
echo '{"api_key": "sk-or-..."}' > openrouter_api_key.json
chmod 600 openrouter_api_key.json
What goes wrong
The failure mode to internalise: chat and image errors don't raise. They land as text in the Output slot - Error: API Request Error: ... | Status: 401, with "Stats N/A due to error". A run that "succeeded" and produced a paragraph of error text is a normal symptom; read the box. Video deliberately raises instead, so Save Video doesn't cache a failure as a result.
- Model list empty or stale: catalog discovery is public and needs no key. Hit Refresh Models; a failed refresh keeps the last good snapshot rather than blanking the list.
- "Cannot verify the selected chat model's media capabilities": you sent an image, audio, or image settings while the catalog was unavailable. Refresh and rerun.
- "The selected chat model does not support image input": you're on a text-only model. Plenty of cheap ones are.
- Video job died mid-poll: stopping ComfyUI does not cancel the remote job. The last accepted ID is stashed in workflow properties - click Resume Last Video to fill
video_job_idand run again. Clear it before starting a fresh generation. - Rate limits: on free
:freemodels, the 429 in Output is OpenRouter's per-day cap, not a bug in the node. - Chat context lost between runs: sessions live in
chats/in the node directory and expire after an hour of inactivity. There's a helper script:
python manage_chats.py list
python manage_chats.py view session_20261206_143022_hello_how_are_you_today
python manage_chats.py clean -d 30
One last thing, the honest one: this is the node shape that has already been weaponised - it holds a credential and calls the network by design, so misbehaviour doesn't look like misbehaviour. Prefer the official ComfyUI API nodes where they cover you, and read a fresh pack before its first run.
Inputs (28)
| Name | Type | Default | Description |
|---|---|---|---|
| api_key | STRING | — | |
| system_prompt | STRING | You are a helpful assistant. | — |
| user_message_box | STRING | Hello, how are you? | — |
| model | COMBO | 1 options: openai/gpt-4o | |
| web_search | BOOLEAN | false | — |
| cheapest | BOOLEAN | true | — |
| fastest | BOOLEAN | false | — |
| aspect_ratio | COMBO | auto | 15 options: auto, 1:1 (1024x1024), 2:3 (832x1248), 3:2 (1248x832), 3:4 (864x1184), 4:3 (1184x864), +9 |
| image_resolution | COMBO | auto | 5 options: auto, 0.5K, 1K, 2K, 4K |
| reasoning_effort | COMBO | auto | 7 options: auto, none, minimal, low, medium, high, +1 |
| seed | INT | 00–18446744073709550000 | — |
| temperature | FLOAT | 1.000–2 | — |
| pdf_engine | COMBO | auto | 3 options: auto, mistral-ocr, pdf-text |
| chat_mode | BOOLEAN | false | — |
| request_timeout | INT | 1201–3600 | — |
| pdf_dataopt | * | — | |
| user_message_inputopt | STRING | — | |
| audio_dataopt | AUDIO | — | |
| request_typeopt | COMBO | chat | 3 options: chat, image, video |
| service_tieropt | COMBO | auto | 5 options: auto, default, flex, priority, ultrafast |
| image_qualityopt | COMBO | auto | 4 options: auto, low, medium, high |
| image_backgroundopt | COMBO | auto | 3 options: auto, opaque, transparent |
| video_modeopt | COMBO | text_to_video | 4 options: text_to_video, first_frame, first_last_frame, reference_images |
| video_durationopt | STRING | auto | — |
| video_resolutionopt | STRING | auto | — |
| video_generate_audioopt | BOOLEAN | false | — |
| video_wait_timeoutopt | INT | 9001–3600 | — |
| video_job_idopt | STRING | — |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| Output | STRING | — |
| image | IMAGE | — |
| Stats | STRING | — |
| Credits | STRING | — |
| video | VIDEO | — |