Files
randomnoner11 6c51014055 feat: implementation dynamic model fetching
The node now polls `api.mistral.ai` on startup to get the latest model list.
2025-12-07 05:36:14 +03:00

301 lines
9.4 KiB
Python

import os
import requests
import folder_paths
import json
import base64
import io
from PIL import Image
import comfy.utils
import numpy as np
script_dir = os.path.dirname(os.path.abspath(__file__))
# --- CONFIGURATION ---
ENABLE_REMOTE_MODELS = True # Set to False to use only the hardcoded list
# ---------------------
with open(os.path.join(script_dir, "API-key.txt"), "r") as file:
API_key = file.read().strip()
FALLBACK_MODELS = [
"pixtral-large-latest",
"pixtral-12b-latest",
"ministral-3b-latest",
"ministral-8b-latest",
"ministral-14b-latest",
"open-mistral-nemo",
"mistral-small-latest",
"mistral-medium-latest",
"mistral-large-latest",
]
def get_mistral_models():
if not ENABLE_REMOTE_MODELS:
return FALLBACK_MODELS
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {API_key}",
}
try:
response = requests.get(
"https://api.mistral.ai/v1/models", headers=headers, timeout=5
)
if response.status_code == 200:
data = response.json()
model_list = []
for model in data.get("data", []):
model_name = model["id"]
if model.get("capabilities", {}).get("vision", False):
model_name += " 🖼️"
model_list.append(model_name)
return sorted(model_list)
except Exception as e:
print(
f"MistralAPI Node: Failed to fetch models from API, using fallback. Error: {e}"
)
return FALLBACK_MODELS
def _add_prompts_folder_path():
prompts_folder_name = "prompts"
prompts_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)), prompts_folder_name
)
folders, extensions = folder_paths.folder_names_and_paths.get(
prompts_folder_name, ([], set())
)
if prompts_dir not in folders:
folders.append(prompts_dir)
extensions.add(".json")
folder_paths.folder_names_and_paths[prompts_folder_name] = (folders, extensions)
_add_prompts_folder_path() # Call the function to register the path
class InvokeMistralEndpoint:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": (get_mistral_models(), {}),
"temperature": (
"FLOAT",
{"default": 1.0, "min": 0.0, "max": 1.5, "step": 0.1},
),
"top_p": (
"FLOAT",
{"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.1},
),
"max_tokens": ("INT", {"default": 512}),
"presence_penalty": (
"FLOAT",
{"default": 0, "min": -2.0, "max": 2.0, "step": 0.1},
),
"frequency_penalty": (
"FLOAT",
{"default": 0, "min": -2.0, "max": 2.0, "step": 0.1},
),
"prompt": (
"STRING",
{
"default": "Your response is directly used as a prompt: avoid quotation marks, brackets, etc! Focus on visually percievable descriptors, not touchy feely fluff. Give me a single SD prompt for the following:\n\n",
"multiline": True,
"placeholder": "There is no hidden prompt injection.",
},
),
},
"optional": {
"context": (
"STRING",
{
"multiline": True,
"defaultInput": True,
"tooltip": "Use Mistral AI prompt loader node to supply the context for few-shot prompting.",
},
),
"image": (
"IMAGE",
{"tooltip": "Only Pixtral models support image input."},
),
"random_seed": (
"INT",
{
"defaultInput": True,
"tooltip": "If you don't supply a fixed seed, it's randomized on the server.",
},
),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("response",)
FUNCTION = "chat_complete"
CATEGORY = "utils/text"
DESCRIPTION = (
"Makes a single call to the Mistral AI API. Set the API key in API-key.txt"
)
def prepare_image_for_mistral(self, image):
samples = image.movedim(-1, 1)
height = samples.shape[2]
width = samples.shape[3]
if height > 1024 or width > 1024:
if height > width:
width = round(width * 1024 / height)
height = 1024
else:
height = round(height * 1024 / width)
width = 1024
# comfy.utils.common_upscale(samples, width, height, upscale_method, crop)
samples = comfy.utils.common_upscale(
samples, width, height, "bilinear", "disabled"
)
samples = samples.movedim(1, -1)
i = 255.0 * samples.cpu().numpy().squeeze()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
with io.BytesIO() as output_bytes:
img.save(output_bytes, format="JPEG")
bytes_data = output_bytes.getvalue()
base64_str = base64.b64encode(bytes_data).decode("utf-8")
return base64_str
def chat_complete(
self,
model,
temperature,
top_p,
max_tokens,
presence_penalty,
frequency_penalty,
prompt,
random_seed=None,
context=None,
image=None,
):
target_model = model.split()[0] # Remove the emoji and whitespace
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {API_key}", # Using the API key from the file
}
messages = []
if context:
try:
context_messages = json.loads(context)
if isinstance(context_messages, list):
messages.extend(context_messages)
else:
print("Warning: Context input is not a valid JSON list.")
except json.JSONDecodeError:
print("Error: Context input is not valid JSON.")
if image is not None:
base64_image = self.prepare_image_for_mistral(image)
messages.append(
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": f"data:image/jpeg;base64,{base64_image}",
},
],
}
)
else:
messages.append({"role": "user", "content": prompt})
data = {
"model": target_model,
"messages": messages,
"temperature": temperature,
"top_p": top_p,
"max_tokens": max_tokens,
"presence_penalty": presence_penalty,
"frequency_penalty": frequency_penalty,
}
if random_seed is not None:
data["random_seed"] = random_seed
try:
# print(f"Request Data: {data}") # Print the request data
print(f"Mistral AI API request sent.")
response = requests.post(
"https://api.mistral.ai/v1/chat/completions", headers=headers, json=data
)
response.raise_for_status()
# print(f"Raw Response: {response.text}") # Print the raw response
print(f"Mistral AI API response received.")
result = response.json()
content = result["choices"][0]["message"]["content"]
return (content,)
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
raise
class LoadFewShotPrompt:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt_file": (folder_paths.get_filename_list("prompts"),),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("few_shot_prompt",)
FUNCTION = "load_prompt"
CATEGORY = "utils/text"
DESCRIPTION = "Loads a JSON file with the Messages."
def load_prompt(self, prompt_file):
prompts_dir = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "prompts"
)
file_path = os.path.join(prompts_dir, prompt_file)
try:
with open(file_path, "r") as f:
content = f.read()
return (content,)
except Exception as e:
print(f"Error loading prompt file: {e}")
return ("",)
NODE_CLASS_MAPPINGS = {
# API
"InvokeMistralEndpoint": InvokeMistralEndpoint,
# loader
"LoadFewShotPrompt": LoadFewShotPrompt,
}
NODE_DISPLAY_NAME_MAPPINGS = {
# API
"InvokeMistralEndpoint": "Mistral AI completion",
# loader
"LoadFewShotPrompt": "Mistral AI prompt loader",
}