167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
import base64
|
|
import json
|
|
import requests
|
|
from PIL import Image
|
|
import io
|
|
import torch
|
|
import re
|
|
from torchvision.transforms import ToPILImage
|
|
|
|
class OpenrouterNode:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"base_url": ("STRING", {
|
|
"multiline": False,
|
|
"default": "https://openrouter.ai/api/v1/chat/completions"
|
|
}),
|
|
"model": ("STRING", {
|
|
"multiline": False,
|
|
"default": "gpt4o"
|
|
}),
|
|
"api_key": ("STRING", {
|
|
"multiline": False,
|
|
"default": ""
|
|
}),
|
|
"prompt": ("STRING", {
|
|
"multiline": True,
|
|
"default": ""
|
|
}),
|
|
"system_prompt": ("STRING", {
|
|
"multiline": True,
|
|
"default": "",
|
|
}),
|
|
"temperature": ("FLOAT", {
|
|
"default": 0.7,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.01,
|
|
"display_round": 2
|
|
}),
|
|
"trim_think": ("BOOLEAN", {
|
|
"default": True,
|
|
}),
|
|
},
|
|
"optional": {
|
|
"image_input": ("IMAGE",)
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
FUNCTION = "get_completion"
|
|
CATEGORY = "OpenRouter"
|
|
|
|
def remove_think_tags(self, text):
|
|
# Pattern to match <think>...</think> tags and their content
|
|
pattern = r'<think>.*?</think>'
|
|
# Remove the tags and their content using regex
|
|
return re.sub(pattern, '', text, flags=re.DOTALL)
|
|
|
|
def get_completion(self, base_url, model, api_key, prompt, system_prompt, temperature, trim_think, image_input=None):
|
|
try:
|
|
headers = {
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
# Initialize messages list
|
|
messages = []
|
|
|
|
# Add system message if system_prompt is provided and not empty
|
|
if system_prompt and system_prompt.strip():
|
|
messages.append({
|
|
"role": "system",
|
|
"content": system_prompt.strip()
|
|
})
|
|
|
|
# Add user message with content as a list
|
|
user_message = {
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "text",
|
|
"text": prompt
|
|
}
|
|
]
|
|
}
|
|
|
|
if image_input is not None:
|
|
if isinstance(image_input, torch.Tensor):
|
|
# Handle 4D tensors by removing the batch dimension
|
|
if image_input.dim() == 4:
|
|
image_input = image_input.squeeze(0)
|
|
if image_input.dim() != 3:
|
|
return ("Error: Image tensor must be 3D after squeezing.",)
|
|
# Rearrange tensor from [H, W, C] to [C, H, W] if necessary
|
|
if image_input.shape[-1] in [1, 3, 4]:
|
|
image_input = image_input.permute(2, 0, 1)
|
|
else:
|
|
return (f"Error: Invalid number of channels: {image_input.shape[-1]}.",)
|
|
to_pil = ToPILImage()
|
|
pil_image = to_pil(image_input)
|
|
elif isinstance(image_input, Image.Image):
|
|
pil_image = image_input
|
|
else:
|
|
return ("Error: Unsupported image_input type.",)
|
|
|
|
# Save the image without resizing or cropping
|
|
buffered = io.BytesIO()
|
|
# Handle different Pillow versions for resampling filters
|
|
resample_filter = getattr(Image, "Resampling", None)
|
|
if resample_filter and hasattr(resample_filter, "LANCZOS"):
|
|
pil_image.save(buffered, format="PNG", resample=Image.Resampling.LANCZOS)
|
|
else:
|
|
pil_image.save(buffered, format="PNG", resample=Image.LANCZOS)
|
|
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
image_data = f"data:image/png;base64,{img_str}"
|
|
|
|
image_message = {
|
|
"type": "image_url",
|
|
"image_url": {
|
|
"url": image_data
|
|
}
|
|
}
|
|
|
|
# Append the image message to the content list
|
|
user_message["content"].append(image_message)
|
|
|
|
# Add the user message to messages list
|
|
messages.append(user_message)
|
|
|
|
body = {
|
|
"model": model,
|
|
"messages": messages,
|
|
"temperature": temperature
|
|
}
|
|
|
|
response = requests.post(base_url, headers=headers, data=json.dumps(body), timeout=120)
|
|
response.raise_for_status()
|
|
|
|
response_json = response.json()
|
|
|
|
if "choices" in response_json and len(response_json["choices"]) > 0:
|
|
assistant_message = response_json["choices"][0].get("message", {}).get("content", "")
|
|
|
|
# If trim_think is enabled, remove think tags
|
|
if trim_think:
|
|
assistant_message = self.remove_think_tags(assistant_message)
|
|
|
|
return (assistant_message,)
|
|
else:
|
|
return ("No response from the model.",)
|
|
|
|
except requests.exceptions.RequestException as req_err:
|
|
return (f"Request Error: {str(req_err)}",)
|
|
except Exception as e:
|
|
return (f"Error: {str(e)}",)
|
|
|
|
# Node registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"OpenrouterNode": OpenrouterNode
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"OpenrouterNode": "OpenRouter Node"
|
|
} |