Advertisement
Guest User

ComfyUI - Streaming LLM Text Generator Custom Node

a guest
Jun 3rd, 2025
16
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 5.13 KB | Source Code | 0 0
  1. # ~/ComfyUI/custom_nodes/llm_stream_node.py
  2. import requests, json, sys
  3.  
  4. class LLMStreamNode:
  5.     CATEGORY      = "LLM"
  6.     OUTPUT_NODE   = True
  7.     RETURN_TYPES  = ("STRING",)
  8.     FUNCTION      = "generate"
  9.  
  10.     DESCRIPTION = (
  11.         "Connect to any OpenAI-compatible LLM endpoint.\n\n"
  12.         "  •  Set 'Max Tokens' to 0 for no limit.\n"
  13.         "  •  'Deterministic' forces greedy sampling.\n"
  14.         "  •  Disable 'Enable Thinking' to inject '/no_think'.\n"
  15.         "  •  'Stream console' to stream tokens in ComfyUI console."
  16.     )
  17.  
  18.     # ---------------- strip reasoning helper -------------
  19.     @staticmethod
  20.     def _strip(txt, start_tag, end_tag):
  21.         out, i = "", 0
  22.         while (s := txt.find(start_tag, i)) != -1:
  23.             out += txt[i:s]
  24.             e = txt.find(end_tag, s)
  25.             if e == -1:
  26.                 break
  27.             i = e + len(end_tag)
  28.         out += txt[i:]
  29.         return out.lstrip()
  30.  
  31.     # ---------------- UI spec ----------------------------
  32.     @classmethod
  33.     def INPUT_TYPES(cls):
  34.         return {"required": {
  35.             "endpoint": ("STRING", {
  36.                 "default": "http://127.0.0.1:5000",
  37.                 "tooltip": "Base URL of OpenAI-compatible LLM server (no /v1/…)"
  38.             }),
  39.             "prompt": ("STRING", {"multiline": True}),
  40.             "max_tokens": ("INT", {
  41.                 "default": 0, "min": 0, "max": 32768, "step": 512,
  42.                 "tooltip": "Maximum tokens to generate; set to 0 to disable any limit"
  43.             }),
  44.             "seed": ("INT", {"default": 1, "min": -1, "max": 2**31-1}),
  45.             "deterministic": ("BOOLEAN", {
  46.                 "default": False,
  47.                 "tooltip": "Force temperature=0, top_p=1, top_k=0, repeat_penalty=1.0"
  48.             }),
  49.             "enable_thinking": ("BOOLEAN", {
  50.                 "default": True,
  51.                 "tooltip": "If False, injects `/no_think` as a system message"
  52.             }),
  53.             "stream_console": ("BOOLEAN", {
  54.                 "default": True,
  55.                 "tooltip": "Print full token stream (thinking + answer) to console"
  56.             }),
  57.             "think_start_tag": ("STRING", {"default": "<think>"}),
  58.             "think_end_tag":   ("STRING", {"default": "</think>"})
  59.         }}
  60.  
  61.     # ---------------- main -------------------------------
  62.     def generate(self, endpoint, prompt, max_tokens, seed,
  63.                  deterministic, enable_thinking, stream_console,
  64.                  think_start_tag, think_end_tag):
  65.  
  66.         url = f"{endpoint.rstrip('/')}/v1/chat/completions"
  67.         sys_prompt = "/no_think" if not enable_thinking else "/think"
  68.         body = {
  69.             "messages": [
  70.                 {"role": "system", "content": sys_prompt},
  71.                 {"role": "user",   "content": prompt}
  72.             ],
  73.             "stream": stream_console
  74.         }
  75.         if max_tokens > 0:
  76.             body["max_tokens"] = max_tokens
  77.  
  78.         # only set sampler params in deterministic mode
  79.         if deterministic:
  80.             body.update(dict(
  81.                 temperature=0,
  82.                 top_p=1,
  83.                 top_k=0,
  84.                 repeat_penalty=1.0
  85.             ))
  86.  
  87.         if seed >= 0:
  88.             body["seed"] = seed
  89.  
  90.         # -------- streaming branch --------
  91.         if stream_console:
  92.             answer, thinking = "", False
  93.             try:
  94.                 with requests.post(url, json=body, stream=True, timeout=None) as r:
  95.                     r.encoding = "utf-8"
  96.                     r.raise_for_status()
  97.                     for raw in r.iter_lines(decode_unicode=True):
  98.                         if not raw or raw == "data: [DONE]" or not raw.startswith("data:"):
  99.                             continue
  100.                         delta = json.loads(raw[5:])["choices"][0].get("delta") or {}
  101.                         text  = delta.get("content") or ""
  102.  
  103.                         if text:
  104.                             sys.stdout.write(text)
  105.                             sys.stdout.flush()
  106.  
  107.                         if think_start_tag in text:
  108.                             thinking = True
  109.                             continue
  110.                         if think_end_tag in text:
  111.                             thinking = False
  112.                             answer += text.split(think_end_tag, 1)[1]
  113.                             continue
  114.                         if not thinking:
  115.                             answer += text
  116.                 sys.stdout.write("\n")
  117.             except Exception as e:
  118.                 answer = f"Error: {e}"
  119.             return (answer.lstrip(),)
  120.  
  121.         # -------- blocking branch ---------
  122.         try:
  123.             r = requests.post(url, json=body, timeout=180)
  124.             r.encoding = "utf-8"
  125.             r.raise_for_status()
  126.             raw = r.json()["choices"][0]["message"]["content"]
  127.             answer = self._strip(raw, think_start_tag, think_end_tag)
  128.         except Exception as e:
  129.             answer = f"Error: {e}"
  130.         return (answer.lstrip(),)
  131.  
  132. # register
  133. NODE_CLASS_MAPPINGS        = {"LLMStream": LLMStreamNode}
  134. NODE_DISPLAY_NAME_MAPPINGS = {"LLMStream": "Streaming LLM Text Generator"}
  135.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement