New node ✨🧠 Universal LLM API (MNeMiC_LLMAPI) talks to any LLM through three protocols: OpenAI-compatible (OpenAI, Gemini, Grok, Groq, OpenRouter, Mistral, DeepSeek, LM Studio, llama.cpp, vLLM…), Anthropic, and native Ollama. Twelve endpoints are built in; users add their own in the git-ignored nodes/llm/UserEndpoints.json. Secrets never enter a workflow: the node stores only the endpoint and model name. URLs, keys and headers are resolved on the backend from the endpoint config and .env (${VAR} / ${VAR:-fallback}), keys never reach the browser, and any key a server echoes back is masked in outputs, errors and logs. Features: live streaming preview on the node, searchable model browser, connection test, preset viewer, vision (image batch), reasoning control mapped per provider, <think> separation into a thinking output, JSON mode, retries with backoff, cancel mid-stream, automatic retry without parameters an endpoint rejects, and Ollama keep_alive/num_ctx plus a ComfyUI VRAM free. Shared infrastructure: utils/env_manager.py is now the single secret store (hot-reloads .env, redaction, placeholder detection) and utils/prompt_presets.py loads the presets the Groq nodes and the new node share. The Groq nodes use both, so .env and preset edits apply without a restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmQWqMnuUXU4XwF6xyaDyM
40 lines
1.9 KiB
Python
40 lines
1.9 KiB
Python
import requests
|
|
import json
|
|
import time
|
|
|
|
def make_api_request(data, headers, url, max_retries, console_log=False, timeout=120):
|
|
for attempt in range(max_retries):
|
|
response = requests.post(url, headers=headers, json=data, timeout=timeout)
|
|
if console_log:
|
|
print(f"Response status: {response.status_code}, Response body: {response.text}")
|
|
if response.status_code == 200:
|
|
try:
|
|
response_json = json.loads(response.text)
|
|
if 'choices' in response_json and response_json['choices']:
|
|
assistant_message = response_json['choices'][0]['message']['content']
|
|
if console_log:
|
|
print(f"Extracted message: {assistant_message}")
|
|
return assistant_message, True, "200 OK"
|
|
else:
|
|
return "No valid response content found.", False, "200 OK but no content"
|
|
except Exception as e:
|
|
print(f"Error parsing response: {str(e)}")
|
|
return "Error parsing JSON response.", False, "200 OK but failed to parse JSON"
|
|
else:
|
|
return "ERROR", False, f"{response.status_code} {response.reason}"
|
|
time.sleep(2)
|
|
return "Failed after all retries.", False, "Failed after all retries"
|
|
|
|
def load_prompt_options(prompt_files):
|
|
prompt_options = {}
|
|
for json_file in prompt_files:
|
|
try:
|
|
with open(json_file, 'r', encoding='utf-8') as file:
|
|
prompts = json.load(file)
|
|
prompt_options.update({prompt['name']: prompt['content'] for prompt in prompts})
|
|
except Exception as e:
|
|
print(f"Failed to load prompts from {json_file}: {str(e)}")
|
|
return prompt_options
|
|
|
|
def get_prompt_content(prompt_options, prompt_name):
|
|
return prompt_options.get(prompt_name, "No content found for selected prompt") |