Added support for Anthropic models

This commit is contained in:
Sirshendu Ganguly
2026-05-08 15:25:45 +05:30
parent 100644fdfb
commit 4ed18c2fa0
12 changed files with 363 additions and 5 deletions
+15 -1
View File
@@ -25,6 +25,12 @@ class RunwareTextInference:
"providerSettings": ("RUNWAREPROVIDERSETTINGS", {
"tooltip": "Optional provider-specific settings (wrapped by provider prefix from the model AIR).",
}),
"toolChoice": ("RUNWARETEXTINFERENCETOOLCHOICE", {
"tooltip": "Connect Runware Text Inference Tool Choice for tool-calling strategy (auto / any / tool / none).",
}),
"inputs": ("RUNWARETEXTINFERENCEINPUTS", {
"tooltip": "Connect Runware Text Inference Inputs for inputs.images / inputs.videos (URLs or mediaUUIDs).",
}),
},
}
@@ -32,7 +38,7 @@ class RunwareTextInference:
RETURN_NAMES = ("text",)
FUNCTION = "infer_text"
CATEGORY = "Runware/Text"
DESCRIPTION = "Run text/chat inference using Runware textInference. Connect Text Model and Text Inference Messages."
DESCRIPTION = "Run text/chat inference using Runware textInference. Connect Text Model and Text Inference Messages. Optional: Settings, provider settings, tool choice, inputs (images/videos)."
@staticmethod
def _extract_text_from_data_item(item: Dict[str, Any]) -> Optional[str]:
@@ -70,6 +76,8 @@ class RunwareTextInference:
messages: List[Dict[str, str]] = kwargs.get("messages") or []
text_settings = kwargs.get("settings")
provider_settings = kwargs.get("providerSettings")
tool_choice = kwargs.get("toolChoice")
text_inputs = kwargs.get("inputs")
if not model:
raise Exception("model is empty.")
@@ -94,6 +102,12 @@ class RunwareTextInference:
provider_name = model.split(":")[0] if ":" in model else model
task["providerSettings"] = {provider_name: provider_settings}
if tool_choice is not None and isinstance(tool_choice, dict) and len(tool_choice) > 0:
task["toolChoice"] = tool_choice
if text_inputs is not None and isinstance(text_inputs, dict) and len(text_inputs) > 0:
task["inputs"] = dict(text_inputs)
gen_config = [task]
print(f"[Runware Text Inference] Request: {rwUtils.safe_json_dumps(gen_config, indent=2)}")
+48
View File
@@ -0,0 +1,48 @@
"""Runware Text Inference Inputs — merge Images + Videos blocks into task.inputs for textInference."""
from typing import Any, Dict, Tuple
class RunwareTextInferenceInputs:
"""Merge Runware Text Inference Inputs Images and/or Videos into a single inputs dict."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"images": ("RUNWARETEXTINFERENCEINPUTSIMAGES", {
"tooltip": "Connect Runware Text Inference Inputs Images for inputs.images[].",
}),
"videos": ("RUNWARETEXTINFERENCEINPUTSVIDEOS", {
"tooltip": "Connect Runware Text Inference Inputs Videos for inputs.videos[].",
}),
},
}
RETURN_TYPES = ("RUNWARETEXTINFERENCEINPUTS",)
RETURN_NAMES = ("inputs",)
FUNCTION = "merge_inputs"
CATEGORY = "Runware/Text"
DESCRIPTION = (
"Combine Runware Text Inference Inputs Images and/or Videos into inputs for Runware Text Inference "
"(inputs.images, inputs.videos)."
)
def merge_inputs(self, **kwargs) -> Tuple[Dict[str, Any], ...]:
images_block = kwargs.get("images")
videos_block = kwargs.get("videos")
out: Dict[str, Any] = {}
if isinstance(images_block, dict) and images_block.get("images"):
imgs = images_block["images"]
if isinstance(imgs, list) and len(imgs) > 0:
out["images"] = [str(x).strip() for x in imgs if x is not None and str(x).strip()]
if isinstance(videos_block, dict) and videos_block.get("videos"):
vids = videos_block["videos"]
if isinstance(vids, list) and len(vids) > 0:
out["videos"] = [str(x).strip() for x in vids if x is not None and str(x).strip()]
return (out,)
+50
View File
@@ -0,0 +1,50 @@
"""Runware Text Inference Inputs Images — builds inputs.images from ComfyUI IMAGE tensors."""
from typing import Any, Dict, List, Tuple
from .utils import runwareUtils as rwUtils
class RunwareTextInferenceInputsImages:
"""Collect IMAGE tensors and convert them for textInference task.inputs.images.
Each connected IMAGE is converted via runwareUtils.convertTensor2IMG, which returns
either a cached mediaUUID (when caching is enabled) or a base64 data URI.
"""
_MAX_IMAGES = 8
@classmethod
def INPUT_TYPES(cls):
optional: Dict[str, tuple] = {}
for i in range(1, cls._MAX_IMAGES + 1):
ordinal = rwUtils.getOrdinal(i)
optional[f"Image {i}"] = ("IMAGE", {
"tooltip": (
f"{ordinal.capitalize()} image for the model. Connect a Load Image (or any IMAGE source) node. "
"Tensor is converted to base64 / mediaUUID before being sent to the API."
),
})
return {"required": {}, "optional": optional}
RETURN_TYPES = ("RUNWARETEXTINFERENCEINPUTSIMAGES",)
RETURN_NAMES = ("images",)
FUNCTION = "createImages"
CATEGORY = "Runware/Text"
DESCRIPTION = (
"Collect one or more IMAGE tensors (Load Image, generated images, etc.) for Runware Text Inference "
"inputs.images. Connect the output to Runware Text Inference Inputs."
)
def createImages(self, **kwargs) -> Tuple[Dict[str, Any], ...]:
urls: List[str] = []
for i in range(1, self._MAX_IMAGES + 1):
tensor = kwargs.get(f"Image {i}", None)
if tensor is None:
continue
urls.append(rwUtils.convertTensor2IMG(tensor))
if not urls:
return ({},)
return ({"images": urls},)
+51
View File
@@ -0,0 +1,51 @@
"""Runware Text Inference Inputs Videos — builds inputs.videos (URL or mediaUUID per item)."""
from typing import Any, Dict, List, Tuple
from .utils import runwareUtils as rwUtils
class RunwareTextInferenceInputsVideos:
"""Collect video references for textInference task.inputs.videos."""
_MAX_VIDEOS = 4
@classmethod
def INPUT_TYPES(cls):
optional: Dict[str, tuple] = {
"useVideos": ("BOOLEAN", {
"default": False,
"tooltip": "Enable to include inputs.videos in the text inference request.",
}),
}
for i in range(1, cls._MAX_VIDEOS + 1):
ordinal = rwUtils.getOrdinal(i)
optional[f"Video{i}"] = ("STRING", {
"default": "",
"tooltip": f"Video URL or mediaUUID for the {ordinal} video. Only used when 'Use Videos' is enabled.",
})
return {"required": {}, "optional": optional}
RETURN_TYPES = ("RUNWARETEXTINFERENCEINPUTSVIDEOS",)
RETURN_NAMES = ("videos",)
FUNCTION = "createVideos"
CATEGORY = "Runware/Text"
DESCRIPTION = (
"Collect one or more video references (URL or mediaUUID) for Runware Text Inference "
"inputs.videos. Connect the output to Runware Text Inference Inputs."
)
def createVideos(self, **kwargs) -> Tuple[Dict[str, Any], ...]:
if not kwargs.get("useVideos", False):
return ({},)
urls: List[str] = []
for i in range(1, self._MAX_VIDEOS + 1):
raw = kwargs.get(f"Video{i}", "")
if raw is not None and str(raw).strip():
urls.append(str(raw).strip())
if not urls:
return ({},)
return ({"videos": urls},)
+15 -4
View File
@@ -70,9 +70,13 @@ class RunwareTextInferenceSettings:
"tooltip": f"Top-k sampling (1–{cls._MAX_TOP_K}).",
}),
# Keep this in the original slot to avoid shifting legacy widgets_values.
"thinkingLevel": (["none", "low", "medium", "high"], {
"thinkingLevel": (["none", "low", "medium", "high", "max", "adaptive"], {
"default": "none",
"tooltip": "Extended reasoning level: none, low, medium, high.",
"tooltip": (
"Extended reasoning level: none, low, medium, high, max, adaptive. "
"off -> disabled. low/medium/high/max -> adaptive thinking with the chosen effort. "
"adaptive -> model picks budget."
),
}),
"useStopSequences": ("BOOLEAN", {
"default": False,
@@ -123,6 +127,9 @@ class RunwareTextInferenceSettings:
"default": "{}",
"tooltip": "JSON object for toolChoice.",
}),
"cache": ("RUNWARETEXTINFERENCESETTINGSCACHE", {
"tooltip": "Connect Runware Text Inference Settings Cache for settings.cache.{scope, ttl}.",
}),
},
}
@@ -132,7 +139,7 @@ class RunwareTextInferenceSettings:
CATEGORY = "Runware/Text"
DESCRIPTION = (
"Optional text inference settings (systemPrompt, maxTokens, temperature, topP, topK, "
"stopSequences, presencePenalty, frequencyPenalty, tools, toolChoice, thinkingLevel). "
"stopSequences, presencePenalty, frequencyPenalty, tools, toolChoice, thinkingLevel, cache). "
"Connect to Runware Text Inference."
)
@@ -191,7 +198,7 @@ class RunwareTextInferenceSettings:
"minimal": "low",
}
value = aliases.get(value, value)
if value not in ("none", "low", "medium", "high"):
if value not in ("none", "low", "medium", "high", "max", "adaptive"):
return "none"
return value
@@ -242,6 +249,10 @@ class RunwareTextInferenceSettings:
if tl != "none":
out["thinkingLevel"] = tl
cache_cfg = kwargs.get("cache")
if cache_cfg is not None and isinstance(cache_cfg, dict) and len(cache_cfg) > 0:
out["cache"] = cache_cfg
if not out:
return ({},)
+64
View File
@@ -0,0 +1,64 @@
"""
Runware Text Inference Settings Cache Node
Builds the cache object for Runware Text Inference (settings.cache.{scope, ttl}).
Maps cache_control placement (Anthropic):
- scope: where cache_control is attached
* system -> system block only
* system+history -> system block + last user message block (default)
- ttl: cache lifetime (5m or 1h)
"""
from typing import Dict, Any
class RunwareTextInferenceSettingsCache:
"""Runware Text Inference Settings Cache node."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"scope": (["system", "system+history"], {
"default": "system+history",
"tooltip": (
"Where cache_control is attached.\n"
"- system: cache the system block only.\n"
"- system+history: cache the system block and the last user message block."
),
}),
"ttl": (["5m", "1h"], {
"default": "5m",
"tooltip": "Cache lifetime: 5m (5 minutes) or 1h (1 hour).",
}),
}
}
RETURN_TYPES = ("RUNWARETEXTINFERENCESETTINGSCACHE",)
RETURN_NAMES = ("cache",)
FUNCTION = "createCache"
CATEGORY = "Runware/Text"
DESCRIPTION = (
"Build a settings.cache object (scope + ttl) and connect it to Runware Text Inference. "
"Merged into the request as settings.cache.{scope, ttl}."
)
def createCache(self, **kwargs) -> tuple[Dict[str, Any]]:
scope = (kwargs.get("scope") or "system+history").strip().lower()
if scope not in ("system", "system+history"):
scope = "system+history"
ttl = (kwargs.get("ttl") or "5m").strip().lower()
if ttl not in ("5m", "1h"):
ttl = "5m"
return ({"scope": scope, "ttl": ttl},)
NODE_CLASS_MAPPINGS = {
"RunwareTextInferenceSettingsCache": RunwareTextInferenceSettingsCache,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareTextInferenceSettingsCache": "Runware Text Inference Settings Cache",
}
+73
View File
@@ -0,0 +1,73 @@
"""
Runware Text Inference Tool Choice Node
Builds the toolChoice object for Runware Text Inference (textInference).
Strategies (type):
- auto: model decides whether to call a tool (recommended default)
- any: model must call at least one tool but chooses which one
- tool: model must call the specific tool identified by `name`
- none: model will not call any tool
"""
from typing import Dict, Any
class RunwareTextInferenceToolChoice:
"""Runware Text Inference Tool Choice node."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"type": (["auto", "any", "tool", "none"], {
"default": "auto",
"tooltip": (
"Strategy the model uses to decide when and which tools to call.\n"
"- auto: model decides (recommended default).\n"
"- any: model must call at least one tool, of its choosing.\n"
"- tool: model must call the specific tool named below.\n"
"- none: model will not call any tool."
),
}),
"name": ("STRING", {
"default": "",
"tooltip": "Name of the specific tool the model must call. Required when type is 'tool'; ignored otherwise.",
}),
}
}
RETURN_TYPES = ("RUNWARETEXTINFERENCETOOLCHOICE",)
RETURN_NAMES = ("toolChoice",)
FUNCTION = "createToolChoice"
CATEGORY = "Runware/Text"
DESCRIPTION = (
"Build a toolChoice object (type + optional tool name) and connect it to "
"Runware Text Inference. Required when type is 'tool'."
)
def createToolChoice(self, **kwargs) -> tuple[Dict[str, Any]]:
choice_type = (kwargs.get("type") or "auto").strip().lower()
if choice_type not in ("auto", "any", "tool", "none"):
choice_type = "auto"
name = (kwargs.get("name") or "").strip()
result: Dict[str, Any] = {"type": choice_type}
if choice_type == "tool":
if not name:
raise Exception(
"Tool Choice: 'name' is required when type is 'tool'. "
"Provide the exact tool name the model must call."
)
result["name"] = name
return (result,)
NODE_CLASS_MAPPINGS = {
"RunwareTextInferenceToolChoice": RunwareTextInferenceToolChoice,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RunwareTextInferenceToolChoice": "Runware Text Inference Tool Choice",
}
+6
View File
@@ -13,6 +13,11 @@ class RunwareTextModelSearch:
"openai:gpt@5.4-mini (GPT-5.4 Mini)",
"openai:gpt@5.4-nano (GPT-5.4 Nano)",
],
"Anthropic": [
"anthropic:claude@opus-4.7 (Claude Opus 4.7)",
"anthropic:claude@sonnet-4.6 (Claude Sonnet 4.6)",
"anthropic:claude@haiku-4.5 (Claude Haiku 4.5)",
],
"Google": [
"google:gemini@3-flash (Gemini 3 Flash)",
],
@@ -23,6 +28,7 @@ class RunwareTextModelSearch:
MODEL_PROVIDERS = [
"All",
"Anthropic",
"Google",
"MiniMax",
"OpenAI",