From bafca3880d3b24d4b90c5ced0f622174c9abc70b Mon Sep 17 00:00:00 2001 From: tazztone <62671577+tazztone@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:15:22 +0200 Subject: [PATCH 1/4] feat: add secure API key handling via environment variables and JSON config --- README.md | 18 ++++++++++++++- node.py | 39 ++++++++++++++++++++++++++++++++- openrouter_api_key.json.example | 3 +++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 openrouter_api_key.json.example diff --git a/README.md b/README.md index 383b71a..d8a0fd5 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,27 @@ pip install -r requirements.txt The OpenRouter node provides a simple interface to interact with various LLMs through the OpenRouter API. +### API Key Security + +> [!WARNING] +> Entering your API key directly into the node's input field in ComfyUI will embed it in the **workflow metadata** of every image you save. This is a security risk if you share your images. + +To keep your API key secure, use one of the following methods: + +1. **JSON Config File (Recommended)**: + - Create a file named `openrouter_api_key.json` in the node directory. + - Add your key like this: `{"api_key": "your_key_here"}`. + - Leave the **api_key** field blank in the ComfyUI interface. + +2. **Environment Variable**: + - Set an environment variable named `LLM_KEY` on your system. + - The node will automatically pick it up if the UI field is empty. + ### Inputs #### Required Inputs: -- **api_key**: Your OpenRouter API key. You can get one from [OpenRouter](https://openrouter.ai/). +- **api_key**: Your OpenRouter API key. Can be left blank if provided via `openrouter_api_key.json` or `LLM_KEY` environment variable. - **system_prompt**: The system prompt that sets the behavior of the LLM. - **user_message_box**: The user message to send to the LLM. - **model**: The model to use for generation. The node automatically fetches the list of available models from OpenRouter. diff --git a/node.py b/node.py index 75b4d56..c7f4a09 100644 --- a/node.py +++ b/node.py @@ -8,6 +8,7 @@ import torch import tiktoken from PIL import Image import hashlib # Added for hashing PDF bytes in IS_CHANGED +import os from .chat_manager import ChatSessionManager # Define a placeholder type name for PDF data. @@ -32,6 +33,35 @@ class OpenRouterNode: def __init__(self): self.chat_manager = ChatSessionManager() + def get_api_key(self, api_key_ui): + """ + Resolves the API key from: + 1. UI input field (if not empty) + 2. Environment variable 'LLM_KEY' + 3. config file 'openrouter_api_key.json' in node directory + """ + if api_key_ui and api_key_ui.strip(): + return api_key_ui.strip() + + # Check environment variable + env_key = os.environ.get("LLM_KEY") + if env_key and env_key.strip(): + return env_key.strip() + + # Check JSON file + config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "openrouter_api_key.json") + if os.path.exists(config_path): + try: + with open(config_path, 'r') as f: + config = json.load(f) + file_key = config.get("api_key") + if file_key and file_key.strip(): + return file_key.strip() + except Exception as e: + print(f"Error reading openrouter_api_key.json: {e}") + + return "" + @classmethod def INPUT_TYPES(cls): """ @@ -136,6 +166,7 @@ class OpenRouterNode: Fetches the user's credits information from the OpenRouter API. Returns a formatted string with remaining credits. """ + api_key = self.get_api_key(api_key) if not api_key: return "API Key not provided." @@ -188,8 +219,12 @@ class OpenRouterNode: """ # Create empty placeholder image placeholder_image = torch.zeros((1, 1, 1, 3), dtype=torch.float32) + + # Resolve API key + api_key = self.get_api_key(api_key) + if not api_key: - return ("Error: API Key not provided.", placeholder_image, "Stats N/A", "Credits N/A") + return ("Error: API Key not provided. Set LLM_KEY env var or use openrouter_api_key.json", placeholder_image, "Stats N/A", "Credits N/A") url = "https://openrouter.ai/api/v1/chat/completions" headers = { @@ -643,6 +678,8 @@ class OpenRouterNode: # Combine all relevant inputs into a tuple for comparison # Use primitive types where possible for reliable hashing/comparison + # Note: We don't hash the resolved API key from file/env to avoid re-running + # if only the hidden key changes (which is rare), but we still pass the UI value. return (api_key, system_prompt, user_message_box, model, web_search, cheapest, fastest, temp_float, pdf_engine, chat_mode, aspect_ratio, image_resolution, seed, tuple(image_hashes), pdf_hash, user_message_input) diff --git a/openrouter_api_key.json.example b/openrouter_api_key.json.example new file mode 100644 index 0000000..3124938 --- /dev/null +++ b/openrouter_api_key.json.example @@ -0,0 +1,3 @@ +{ + "api_key": "your_openrouter_api_key_here" +} From 916a6e7938e56e1dc375d83ba6d06b6eaee7a2ae Mon Sep 17 00:00:00 2001 From: tazztone <62671577+tazztone@users.noreply.github.com> Date: Sun, 26 Apr 2026 20:18:55 +0200 Subject: [PATCH 2/4] ui: add security warning to api_key widget label --- web/openrouter_dynamic_inputs.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/openrouter_dynamic_inputs.js b/web/openrouter_dynamic_inputs.js index d0e8e61..fd44b6b 100644 --- a/web/openrouter_dynamic_inputs.js +++ b/web/openrouter_dynamic_inputs.js @@ -31,6 +31,12 @@ app.registerExtension({ nodeType.prototype.onNodeCreated = function () { const me = onNodeCreated?.apply(this); + // Find api_key widget and update label with security warning + const apiKeyWidget = this.widgets?.find(w => w.name === "api_key"); + if (apiKeyWidget) { + apiKeyWidget.label = "api_key (Leave blank for secure loading)"; + } + // Start with a new dynamic input - exactly like cozy example this.addInput(PREFIX, TYPE); From e4a7aaadc33fe85b726952963c2f3b7eab283e02 Mon Sep 17 00:00:00 2001 From: tazztone <62671577+tazztone@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:05:39 +0200 Subject: [PATCH 3/4] gitignore jsons --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 32247cd..9a783d3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,6 @@ Thumbs.db # Secrets api_keys.json secrets.json -*.key \ No newline at end of file +*.key +openrouter_api_key.json +chats/ From dbf3ed629525cca7ceeda2ead86382dce1f67e95 Mon Sep 17 00:00:00 2001 From: Gabe Date: Sat, 9 May 2026 08:06:09 -0500 Subject: [PATCH 4/4] review: minor cleanups on top of secure-api-key - node.py: mark get_api_key as @staticmethod since it doesn't use self - node.py: clarify the IS_CHANGED comment about why env/file keys are intentionally excluded from the cache key - README: warn users to clear the api_key field if they ever typed it in (since the value persists in the workflow JSON), and add a chmod 600 hint for the JSON config on Linux/macOS No behavior changes. --- README.md | 3 +++ node.py | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d8a0fd5..f36a316 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ To keep your API key secure, use one of the following methods: - Set an environment variable named `LLM_KEY` on your system. - The node will automatically pick it up if the UI field is empty. +> [!NOTE] +> If you've ever typed your key into the **api_key** field, clear it before saving the workflow — the value is stored in the workflow JSON. On Linux/macOS, restrict the JSON config with `chmod 600 openrouter_api_key.json` so only your user can read it. + ### Inputs #### Required Inputs: diff --git a/node.py b/node.py index c7f4a09..cfa51fa 100644 --- a/node.py +++ b/node.py @@ -33,7 +33,8 @@ class OpenRouterNode: def __init__(self): self.chat_manager = ChatSessionManager() - def get_api_key(self, api_key_ui): + @staticmethod + def get_api_key(api_key_ui): """ Resolves the API key from: 1. UI input field (if not empty) @@ -678,8 +679,9 @@ class OpenRouterNode: # Combine all relevant inputs into a tuple for comparison # Use primitive types where possible for reliable hashing/comparison - # Note: We don't hash the resolved API key from file/env to avoid re-running - # if only the hidden key changes (which is rare), but we still pass the UI value. + # Note: api_key here is the UI value only. Keys resolved from the LLM_KEY + # env var or openrouter_api_key.json are intentionally NOT part of the + # cache key — they're treated as user environment, not workflow inputs. return (api_key, system_prompt, user_message_box, model, web_search, cheapest, fastest, temp_float, pdf_engine, chat_mode, aspect_ratio, image_resolution, seed, tuple(image_hashes), pdf_hash, user_message_input)