Files
MeteorAndyandClaude 6f2aa13575 Align with official API nodes: VIDEO/AUDIO output + real vendor formats
Rewrite each node to faithfully mirror the official comfy_api_nodes/
contract (apis/*.py + Comfy-Org/embedded-docs). The ComfyOrg proxy
forwards the body verbatim, so the official request/response models
ARE the real vendor API.

- Video nodes now return VIDEO (InputImpl.VideoFromFile), not file paths
- Audio nodes now return AUDIO ({waveform, sample_rate}), not file paths
- Request/response bodies match official pydantic models field-for-field
- Model ids, options, defaults, ranges match official + embedded docs
- Every node exposes api_key + base_url (overridable for relays/proxies)
- utils.py: add ComfyUI-native helpers — video_from_bytes,
  download_video_output, audio_from_bytes, download_audio_output,
  poll_until, validate_prompt, tensor_to_data_uri
- Rewrite nodes_minimax.py (submit -> poll -> retrieve -> download VIDEO)
- nodes_extra.py: base_url on all three, poll_until, validate_prompt
- Clean __init__.py (drop duplicate grok import/keys); 37 nodes total
- README: output-type alignment, node list, relay examples

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 20:07:04 +08:00

217 lines
8.9 KiB
Python

"""Anthropic Claude (Messages API) node for BYOKey.
Bring-your-own-key node that calls the Anthropic Messages API directly
(``POST {base_url}/v1/messages`` with the ``x-api-key`` + ``anthropic-version``
headers) instead of going through the ComfyOrg proxy.
Modeled field-for-field on the official ComfyUI ``ClaudeNode``
(``comfy_api_nodes/nodes_anthropic.py`` + ``comfy_api_nodes/apis/anthropic.py``):
same model labels/ids, same max_tokens / temperature / reasoning_effort
handling, the same request body shape, the same text-block extraction, and the
same STRING output. The only intentional difference is that images are sent as
base64 ``source`` blocks (the official node uploads them and uses URL sources;
the underlying Anthropic API natively supports base64 sources for direct calls).
"""
from .utils import http_request, run_async, tensor_to_base64, validate_prompt
# Model label -> Anthropic model id (mirrors CLAUDE_MODELS in the official node).
CLAUDE_MODELS = {
"Opus 4.7": "claude-opus-4-7",
"Opus 4.6": "claude-opus-4-6",
"Sonnet 4.6": "claude-sonnet-4-6",
"Sonnet 4.5": "claude-sonnet-4-5-20250929",
"Haiku 4.5": "claude-haiku-4-5-20251001",
}
CLAUDE_MAX_IMAGES = 20
# Models that do not support extended thinking.
_THINKING_UNSUPPORTED = {"Haiku 4.5"}
# Models that use the newer "adaptive" thinking mode (Anthropic picks the budget
# from the output_config.effort hint). Others use the explicit budget API.
_ADAPTIVE_THINKING_MODELS = {"Opus 4.7", "Opus 4.6", "Sonnet 4.6"}
# Budget mode (e.g. Sonnet 4.5): effort -> reasoning budget in tokens (< max_tokens).
_REASONING_BUDGET = {
"low": 2048,
"medium": 8192,
"high": 16384,
}
_REASONING_EFFORTS = ["off", "low", "medium", "high"]
class BYOKey_Claude:
"""Generate text responses from an Anthropic Claude model.
Drop-in replacement for the official ``ClaudeNode`` (same STRING output and
same model/parameter handling), but calls the Anthropic API directly with
your own key.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": ("STRING", {
"default": "",
"tooltip": "Anthropic API key (sent as the x-api-key header).",
}),
"base_url": ("STRING", {
"default": "https://api.anthropic.com",
"tooltip": "API base URL. Default targets Anthropic directly; "
"override for a compatible relay/proxy.",
}),
"prompt": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Text input to the model.",
}),
"model": (list(CLAUDE_MODELS.keys()), {
"default": "Sonnet 4.6",
"tooltip": "The Claude model used to generate the response.",
}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 2147483647,
"control_after_generate": True,
"tooltip": "Seed controls whether the node should re-run; "
"results are non-deterministic regardless of seed.",
}),
},
"optional": {
"images": ("IMAGE", {
"tooltip": f"Optional image(s) for multimodal context. Up to {CLAUDE_MAX_IMAGES} images.",
}),
"system_prompt": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Foundational instructions that dictate the model's behavior.",
}),
"max_tokens": ("INT", {
"default": 32768,
"min": 4096,
"max": 64000,
"tooltip": "Maximum number of tokens to generate "
"(includes reasoning tokens when enabled).",
}),
"temperature": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "Controls randomness. 0.0 is deterministic, 1.0 is most random. "
"Ignored for Opus 4.7 and any model when reasoning_effort is set.",
}),
"reasoning_effort": (_REASONING_EFFORTS, {
"default": "off",
"tooltip": "Extended thinking effort. 'off' disables reasoning. "
"Ignored for models that do not support thinking (e.g. Haiku 4.5).",
}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("output",)
FUNCTION = "execute"
CATEGORY = "api/byokey/text"
DESCRIPTION = (
"Generate text responses with Anthropic's Claude models. Provide a text "
"prompt and optionally one or more images for multimodal context."
)
# ------------------------------------------------------------------ #
# Helpers #
# ------------------------------------------------------------------ #
@staticmethod
def _get_text_from_response(response: dict) -> str:
"""Join text blocks from the response; thinking blocks are dropped."""
blocks = response.get("content") or []
return "\n".join(
block.get("text", "")
for block in blocks
if isinstance(block, dict) and block.get("type") == "text" and block.get("text")
)
# ------------------------------------------------------------------ #
# Execution #
# ------------------------------------------------------------------ #
def execute(self, api_key, base_url, prompt, model, seed,
images=None, system_prompt="", max_tokens=32768,
temperature=1.0, reasoning_effort="off"):
validate_prompt(prompt, min_length=1)
model_label = model
model_id = CLAUDE_MODELS.get(model_label, model_label)
thinking_enabled = (
reasoning_effort not in ("off", None)
and model_label not in _THINKING_UNSUPPORTED
)
# Anthropic requires temperature unset (defaults to 1.0) when thinking is
# enabled; Opus 4.7 also rejects a user-supplied temperature.
if thinking_enabled or model_label == "Opus 4.7":
send_temperature = None
else:
send_temperature = temperature
thinking_cfg = None
output_cfg = None
if thinking_enabled:
if model_label in _ADAPTIVE_THINKING_MODELS:
# Adaptive mode - Anthropic chooses the budget from the effort hint.
thinking_cfg = {"type": "adaptive"}
output_cfg = {"effort": reasoning_effort}
else:
# Budget mode. Leave at least 1024 tokens for the actual response.
budget = _REASONING_BUDGET[reasoning_effort]
budget = min(budget, max(1024, max_tokens - 1024))
thinking_cfg = {"type": "enabled", "budget_tokens": budget}
# Build the user message content: image blocks first, then the text block.
content = []
if images is not None:
n = images.shape[0]
if n > CLAUDE_MAX_IMAGES:
raise ValueError(
f"Up to {CLAUDE_MAX_IMAGES} images are supported per request (got {n})."
)
for i in range(n):
b64 = tensor_to_base64(images[i].unsqueeze(0), "image/png")
content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": b64,
},
})
content.append({"type": "text", "text": prompt})
# Request body mirrors AnthropicMessagesRequest. Omit unset optionals.
data = {
"model": model_id,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": content}],
}
if system_prompt:
data["system"] = system_prompt
if send_temperature is not None:
data["temperature"] = send_temperature
if thinking_cfg is not None:
data["thinking"] = thinking_cfg
if output_cfg is not None:
data["output_config"] = output_cfg
url = f"{base_url.rstrip('/')}/v1/messages"
headers = {
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
}
response = run_async(http_request("POST", url, headers, data))
text = self._get_text_from_response(response)
return (text or "Empty response from Claude model.",)