888 lines
33 KiB
Python
888 lines
33 KiB
Python
"""
|
|
API Provider Selector Node
|
|
A unified node for all API-based LLM providers with hardcoded model lists.
|
|
Replaces individual provider nodes (OpenAI, Gemini, Groq, BFL, Suno, WaveSpeed).
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
# Add parent directory to path
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
parent_dir = os.path.dirname(current_dir)
|
|
if parent_dir not in sys.path:
|
|
sys.path.insert(0, parent_dir)
|
|
|
|
from llmtoolkit_utils import get_api_key, get_models
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Hardcoded model lists (update these using ModelListFetcherNode)
|
|
PROVIDER_MODELS = {
|
|
"openai": [
|
|
"gpt-5-nano",
|
|
"babbage-002",
|
|
"chatgpt-4o-latest",
|
|
"chatgpt-5-latest",
|
|
"codex-mini-latest",
|
|
"computer-use-preview",
|
|
"computer-use-preview-2025-03-11",
|
|
"dall-e-2",
|
|
"dall-e-3",
|
|
"davinci-002",
|
|
"gpt-3.5-turbo",
|
|
"gpt-3.5-turbo-0125",
|
|
"gpt-3.5-turbo-1106",
|
|
"gpt-3.5-turbo-16k",
|
|
"gpt-3.5-turbo-instruct",
|
|
"gpt-3.5-turbo-instruct-0914",
|
|
"gpt-4",
|
|
"gpt-4-0125-preview",
|
|
"gpt-4-0613",
|
|
"gpt-4-1106-preview",
|
|
"gpt-4-turbo",
|
|
"gpt-4-turbo-2024-04-09",
|
|
"gpt-4-turbo-preview",
|
|
"gpt-4.1",
|
|
"gpt-4.1-2025-04-14",
|
|
"gpt-4.1-mini",
|
|
"gpt-4.1-mini-2025-04-14",
|
|
"gpt-4.1-nano",
|
|
"gpt-4.1-nano-2025-04-14",
|
|
"gpt-4.5-preview",
|
|
"gpt-4.5-preview-2025-02-27",
|
|
"gpt-4o",
|
|
"gpt-4o-2024-05-13",
|
|
"gpt-4o-2024-08-06",
|
|
"gpt-4o-2024-11-20",
|
|
"gpt-4o-audio-preview",
|
|
"gpt-4o-audio-preview-2024-10-01",
|
|
"gpt-4o-audio-preview-2024-12-17",
|
|
"gpt-4o-audio-preview-2025-06-03",
|
|
"gpt-4o-mini",
|
|
"gpt-4o-mini-2024-07-18",
|
|
"gpt-4o-mini-audio-preview",
|
|
"gpt-4o-mini-audio-preview-2024-12-17",
|
|
"gpt-4o-mini-realtime-preview",
|
|
"gpt-4o-mini-realtime-preview-2024-12-17",
|
|
"gpt-4o-mini-search-preview",
|
|
"gpt-4o-mini-search-preview-2025-03-11",
|
|
"gpt-4o-mini-transcribe",
|
|
"gpt-4o-mini-tts",
|
|
"gpt-4o-realtime-preview",
|
|
"gpt-4o-realtime-preview-2024-10-01",
|
|
"gpt-4o-realtime-preview-2024-12-17",
|
|
"gpt-4o-realtime-preview-2025-06-03",
|
|
"gpt-4o-search-preview",
|
|
"gpt-4o-search-preview-2025-03-11",
|
|
"gpt-4o-transcribe",
|
|
"gpt-5",
|
|
"gpt-5-2025-08-07",
|
|
"gpt-5-chat-latest",
|
|
"gpt-5-mini",
|
|
"gpt-5-mini-2025-08-07",
|
|
"gpt-5-nano-2025-08-07",
|
|
"gpt-audio",
|
|
"gpt-audio-2025-08-28",
|
|
"gpt-image-1",
|
|
"gpt-realtime",
|
|
"gpt-realtime-2025-08-28",
|
|
"gpt40-0806-loco-vm",
|
|
"o1",
|
|
"o1-2024-12-17",
|
|
"o1-mini",
|
|
"o1-mini-2024-09-12",
|
|
"o1-preview",
|
|
"o1-preview-2024-09-12",
|
|
"o1-pro",
|
|
"o1-pro-2025-03-19",
|
|
"o3",
|
|
"o3-2025-04-16",
|
|
"o3-deep-research",
|
|
"o3-deep-research-2025-06-26",
|
|
"o3-mini",
|
|
"o3-mini-2025-01-31",
|
|
"o3-pro",
|
|
"o3-pro-2025-06-10",
|
|
"o4-mini",
|
|
"o4-mini-2025-04-16",
|
|
"o4-mini-deep-research",
|
|
"o4-mini-deep-research-2025-06-26",
|
|
"omni-moderation-2024-09-26",
|
|
"omni-moderation-latest",
|
|
"text-embedding-3-large",
|
|
"text-embedding-3-small",
|
|
"text-embedding-ada-002",
|
|
"tts-1",
|
|
"tts-1-1106",
|
|
"tts-1-hd",
|
|
"tts-1-hd-1106",
|
|
"tts-l-hd",
|
|
"whisper-1",
|
|
"whisper-I",
|
|
],
|
|
"gemini": [
|
|
# Latest image generation models
|
|
"gemini-2.5-flash-image-preview",
|
|
"gemini-2.0-flash-preview-image-generation",
|
|
"imagen-4.0-generate-001",
|
|
"imagen-4.0-ultra-generate-001",
|
|
"imagen-3.0-generate-002",
|
|
"imagen-3.0-generate-001",
|
|
"imagen-3-light-alpha",
|
|
"aqa",
|
|
"embedding-001",
|
|
"embedding-gecko-001",
|
|
"gemini-1.5-flash",
|
|
"gemini-1.5-flash-002",
|
|
"gemini-1.5-flash-8b",
|
|
"gemini-1.5-flash-8b-001",
|
|
"gemini-1.5-flash-8b-latest",
|
|
"gemini-1.5-flash-latest",
|
|
"gemini-1.5-pro",
|
|
"gemini-1.5-pro-002",
|
|
"gemini-1.5-pro-latest",
|
|
"gemini-2.0-flash",
|
|
"gemini-2.0-flash-001",
|
|
"gemini-2.0-flash-exp",
|
|
"gemini-2.0-flash-lite",
|
|
"gemini-2.0-flash-lite-001",
|
|
"gemini-2.0-flash-lite-preview",
|
|
"gemini-2.0-flash-lite-preview-02-05",
|
|
"gemini-2.0-flash-thinking-exp",
|
|
"gemini-2.0-flash-thinking-exp-01-21",
|
|
"gemini-2.0-flash-thinking-exp-1219",
|
|
"gemini-2.0-pro-exp",
|
|
"gemini-2.0-pro-exp-02-05",
|
|
"gemini-2.5-flash",
|
|
"gemini-2.5-flash-lite",
|
|
"gemini-2.5-flash-lite-preview-06-17",
|
|
"gemini-2.5-flash-preview-05-20",
|
|
"gemini-2.5-flash-preview-tts",
|
|
"gemini-2.5-pro",
|
|
"gemini-2.5-pro-preview-03-25",
|
|
"gemini-2.5-pro-preview-05-06",
|
|
"gemini-2.5-pro-preview-06-05",
|
|
"gemini-2.5-pro-preview-tts",
|
|
"gemini-embedding-001",
|
|
"gemini-embedding-exp",
|
|
"gemini-embedding-exp-03-07",
|
|
"gemini-exp-1206",
|
|
"gemma-3-12b-it",
|
|
"gemma-3-1b-it",
|
|
"gemma-3-27b-it",
|
|
"gemma-3-4b-it",
|
|
"gemma-3n-e2b-it",
|
|
"gemma-3n-e4b-it",
|
|
"imagen-3.0-generate-002",
|
|
"imagen-4.0-generate-preview-06-06",
|
|
"imagen-4.0-ultra-generate-preview-06-06",
|
|
"learnlm-2.0-flash-experimental",
|
|
"text-embedding-004",
|
|
],
|
|
"groq": [
|
|
"moonshotai/kimi-k2-instruct",
|
|
"allam-2-7b",
|
|
"compound-beta",
|
|
"compound-beta-mini",
|
|
"deepseek-r1-distill-llama-70b",
|
|
"gemma2-9b-it",
|
|
"llama-3.1-8b-instant",
|
|
"llama-3.3-70b-versatile",
|
|
"meta-llama/llama-4-maverick-17b-128e-instruct",
|
|
"meta-llama/llama-4-scout-17b-16e-instruct",
|
|
"meta-llama/llama-guard-4-12b",
|
|
"meta-llama/llama-prompt-guard-2-22m",
|
|
"meta-llama/llama-prompt-guard-2-86m",
|
|
"openai/gpt-oss-120b",
|
|
"openai/gpt-oss-20b",
|
|
"playai-tts",
|
|
"playai-tts-arabic",
|
|
"qwen/qwen3-32b",
|
|
"whisper-large-v3",
|
|
"whisper-large-v3-turbo",
|
|
],
|
|
"anthropic": [
|
|
"claude-3-5-sonnet-20241022",
|
|
"claude-3-5-haiku-20241022",
|
|
"claude-3-opus-20240229",
|
|
"claude-3-sonnet-20240229",
|
|
"claude-3-haiku-20240307",
|
|
],
|
|
"mistral": [
|
|
"mistral-large-latest",
|
|
"mistral-medium-latest",
|
|
"mistral-small-latest",
|
|
"mistral-tiny-latest",
|
|
"codestral-latest",
|
|
"pixtral-12b-latest",
|
|
"open-mixtral-8x22b",
|
|
"open-mixtral-8x7b",
|
|
"open-mistral-7b",
|
|
],
|
|
"deepseek": [
|
|
"deepseek-chat",
|
|
"deepseek-coder",
|
|
],
|
|
"bfl": [
|
|
"flux-kontext-max",
|
|
],
|
|
"suno": [
|
|
"V3_5",
|
|
"V4",
|
|
"V4_5",
|
|
],
|
|
"wavespeed": [
|
|
"wavespeed-ai/flux-kontext-dev-ultra-fast",
|
|
"wavespeed-ai/flux-kontext-dev/multi-ultra-fast",
|
|
"wavespeed-ai/hunyuan-image-3",
|
|
"wavespeed-ai/qwen-image/edit-plus",
|
|
"bytedance/portrait",
|
|
"bytedance/seededit-v3",
|
|
"bytedance/seedream-v3.1",
|
|
"bytedance/seedream-v4",
|
|
"bytedance/seedream-v4-edit",
|
|
"bytedance/seedream-v4-edit-sequential",
|
|
"bytedance/seedream-v4-sequential",
|
|
"google/imagen4",
|
|
"google/imagen4-fast",
|
|
"google/imagen4-ultra",
|
|
"google/nano-banana/edit",
|
|
"bytedance/dreamina-v3.1/text-to-image",
|
|
"bytedance-seedance-v1-pro-t2v-480p",
|
|
"bytedance-seedance-v1-pro-t2v-720p",
|
|
"bytedance-seedance-v1-pro-t2v-1080p",
|
|
"bytedance-seedance-v1-pro-i2v-480p",
|
|
"bytedance-seedance-v1-pro-i2v-720p",
|
|
"bytedance-seedance-v1-pro-i2v-1080p",
|
|
"minimax/hailuo-02/t2v-standard",
|
|
"minimax/hailuo-02/t2v-pro",
|
|
"minimax/hailuo-02/i2v-pro",
|
|
"minimax/hailuo-02/i2v-standard",
|
|
"kwaivgi/kling-v2.1-i2v-standard",
|
|
"kwaivgi/kling-v2.1-i2v-pro",
|
|
"kwaivgi/kling-v2.1-i2v-master",
|
|
"kwaivgi/kling-v2.5-turbo-pro/image-to-video",
|
|
"kwaivgi/kling-v2.5-turbo-pro/text-to-video",
|
|
"kwaivgi/kling-lipsync/audio-to-video",
|
|
"kwaivgi/kling-lipsync/text-to-video",
|
|
"kwaivgi/kling-v1-ai-avatar-pro",
|
|
"kwaivgi/kling-effects",
|
|
"google/veo3-fast/image-to-video",
|
|
"google/veo3/image-to-video",
|
|
"google/veo3-fast",
|
|
"wavespeed-ai/veo2-t2v",
|
|
"wavespeed-ai/veo2-i2v",
|
|
"alibaba/wan-2.5/text-to-video",
|
|
"alibaba/wan-2.5/text-to-video-fast",
|
|
"alibaba/wan-2.5/image-to-video",
|
|
"alibaba/wan-2.5/image-to-video-fast",
|
|
],
|
|
"openrouter": [
|
|
"agentica-org/deepcoder-14b-preview",
|
|
"agentica-org/deepcoder-14b-preview:free",
|
|
"ai21/jamba-large-1.7",
|
|
"ai21/jamba-mini-1.7",
|
|
"aion-labs/aion-1.0",
|
|
"aion-labs/aion-1.0-mini",
|
|
"aion-labs/aion-rp-llama-3.1-8b",
|
|
"alfredpros/codellama-7b-instruct-solidity",
|
|
"alpindale/goliath-120b",
|
|
"amazon/nova-lite-v1",
|
|
"amazon/nova-micro-v1",
|
|
"amazon/nova-pro-v1",
|
|
"anthracite-org/magnum-v2-72b",
|
|
"anthracite-org/magnum-v4-72b",
|
|
"anthropic/claude-3-haiku",
|
|
"anthropic/claude-3-opus",
|
|
"anthropic/claude-3.5-haiku",
|
|
"anthropic/claude-3.5-haiku-20241022",
|
|
"anthropic/claude-3.5-sonnet",
|
|
"anthropic/claude-3.5-sonnet-20240620",
|
|
"anthropic/claude-3.7-sonnet",
|
|
"anthropic/claude-3.7-sonnet:thinking",
|
|
"anthropic/claude-opus-4",
|
|
"anthropic/claude-opus-4.1",
|
|
"anthropic/claude-sonnet-4",
|
|
"arcee-ai/coder-large",
|
|
"arcee-ai/maestro-reasoning",
|
|
"arcee-ai/spotlight",
|
|
"arcee-ai/virtuoso-large",
|
|
"arliai/qwq-32b-arliai-rpr-v1",
|
|
"arliai/qwq-32b-arliai-rpr-v1:free",
|
|
"baidu/ernie-4.5-21b-a3b",
|
|
"baidu/ernie-4.5-300b-a47b",
|
|
"baidu/ernie-4.5-vl-28b-a3b",
|
|
"baidu/ernie-4.5-vl-424b-a47b",
|
|
"bytedance/ui-tars-1.5-7b",
|
|
"cognitivecomputations/dolphin-mistral-24b-venice-edition:free",
|
|
"cognitivecomputations/dolphin-mixtral-8x22b",
|
|
"cognitivecomputations/dolphin3.0-mistral-24b",
|
|
"cognitivecomputations/dolphin3.0-mistral-24b:free",
|
|
"cognitivecomputations/dolphin3.0-r1-mistral-24b",
|
|
"cognitivecomputations/dolphin3.0-r1-mistral-24b:free",
|
|
"cohere/command",
|
|
"cohere/command-a",
|
|
"cohere/command-r",
|
|
"cohere/command-r-03-2024",
|
|
"cohere/command-r-08-2024",
|
|
"cohere/command-r-plus",
|
|
"cohere/command-r-plus-04-2024",
|
|
"cohere/command-r-plus-08-2024",
|
|
"cohere/command-r7b-12-2024",
|
|
"deepseek/deepseek-chat",
|
|
"deepseek/deepseek-chat-v3-0324",
|
|
"deepseek/deepseek-chat-v3-0324:free",
|
|
"deepseek/deepseek-chat-v3.1",
|
|
"deepseek/deepseek-chat-v3.1:free",
|
|
"deepseek/deepseek-prover-v2",
|
|
"deepseek/deepseek-r1",
|
|
"deepseek/deepseek-r1-0528",
|
|
"deepseek/deepseek-r1-0528-qwen3-8b",
|
|
"deepseek/deepseek-r1-0528-qwen3-8b:free",
|
|
"deepseek/deepseek-r1-0528:free",
|
|
"deepseek/deepseek-r1-distill-llama-70b",
|
|
"deepseek/deepseek-r1-distill-llama-70b:free",
|
|
"deepseek/deepseek-r1-distill-llama-8b",
|
|
"deepseek/deepseek-r1-distill-qwen-14b",
|
|
"deepseek/deepseek-r1-distill-qwen-14b:free",
|
|
"deepseek/deepseek-r1-distill-qwen-32b",
|
|
"deepseek/deepseek-r1:free",
|
|
"deepseek/deepseek-v3.1-base",
|
|
"eleutherai/llemma_7b",
|
|
"google/gemini-2.0-flash-001",
|
|
"google/gemini-2.0-flash-exp:free",
|
|
"google/gemini-2.0-flash-lite-001",
|
|
"google/gemini-2.5-flash",
|
|
"google/gemini-2.5-flash-image-preview",
|
|
"google/gemini-2.5-flash-image-preview:free",
|
|
"google/gemini-2.5-flash-lite",
|
|
"google/gemini-2.5-flash-lite-preview-06-17",
|
|
"google/gemini-2.5-pro",
|
|
"google/gemini-2.5-pro-exp-03-25",
|
|
"google/gemini-2.5-pro-preview",
|
|
"google/gemini-2.5-pro-preview-05-06",
|
|
"google/gemini-flash-1.5",
|
|
"google/gemini-flash-1.5-8b",
|
|
"google/gemini-pro-1.5",
|
|
"google/gemma-2-27b-it",
|
|
"google/gemma-2-9b-it",
|
|
"google/gemma-2-9b-it:free",
|
|
"google/gemma-3-12b-it",
|
|
"google/gemma-3-12b-it:free",
|
|
"google/gemma-3-27b-it",
|
|
"google/gemma-3-27b-it:free",
|
|
"google/gemma-3-4b-it",
|
|
"google/gemma-3-4b-it:free",
|
|
"google/gemma-3n-e2b-it:free",
|
|
"google/gemma-3n-e4b-it",
|
|
"google/gemma-3n-e4b-it:free",
|
|
"gryphe/mythomax-l2-13b",
|
|
"inception/mercury",
|
|
"inception/mercury-coder",
|
|
"infermatic/mn-inferor-12b",
|
|
"inflection/inflection-3-pi",
|
|
"inflection/inflection-3-productivity",
|
|
"liquid/lfm-3b",
|
|
"liquid/lfm-7b",
|
|
"mancer/weaver",
|
|
"meta-llama/llama-3-70b-instruct",
|
|
"meta-llama/llama-3-8b-instruct",
|
|
"meta-llama/llama-3.1-405b",
|
|
"meta-llama/llama-3.1-405b-instruct",
|
|
"meta-llama/llama-3.1-405b-instruct:free",
|
|
"meta-llama/llama-3.1-70b-instruct",
|
|
"meta-llama/llama-3.1-8b-instruct",
|
|
"meta-llama/llama-3.2-11b-vision-instruct",
|
|
"meta-llama/llama-3.2-1b-instruct",
|
|
"meta-llama/llama-3.2-3b-instruct",
|
|
"meta-llama/llama-3.2-3b-instruct:free",
|
|
"meta-llama/llama-3.2-90b-vision-instruct",
|
|
"meta-llama/llama-3.3-70b-instruct",
|
|
"meta-llama/llama-3.3-70b-instruct:free",
|
|
"meta-llama/llama-3.3-8b-instruct:free",
|
|
"meta-llama/llama-4-maverick",
|
|
"meta-llama/llama-4-maverick:free",
|
|
"meta-llama/llama-4-scout",
|
|
"meta-llama/llama-4-scout:free",
|
|
"meta-llama/llama-guard-2-8b",
|
|
"meta-llama/llama-guard-3-8b",
|
|
"meta-llama/llama-guard-4-12b",
|
|
"microsoft/mai-ds-r1",
|
|
"microsoft/mai-ds-r1:free",
|
|
"microsoft/phi-3-medium-128k-instruct",
|
|
"microsoft/phi-3-mini-128k-instruct",
|
|
"microsoft/phi-3.5-mini-128k-instruct",
|
|
"microsoft/phi-4",
|
|
"microsoft/phi-4-multimodal-instruct",
|
|
"microsoft/phi-4-reasoning-plus",
|
|
"microsoft/wizardlm-2-8x22b",
|
|
"minimax/minimax-01",
|
|
"minimax/minimax-m1",
|
|
"mistralai/codestral-2501",
|
|
"mistralai/codestral-2508",
|
|
"mistralai/devstral-medium",
|
|
"mistralai/devstral-small",
|
|
"mistralai/devstral-small-2505",
|
|
"mistralai/devstral-small-2505:free",
|
|
"mistralai/magistral-medium-2506",
|
|
"mistralai/magistral-medium-2506:thinking",
|
|
"mistralai/magistral-small-2506",
|
|
"mistralai/ministral-3b",
|
|
"mistralai/ministral-8b",
|
|
"mistralai/mistral-7b-instruct",
|
|
"mistralai/mistral-7b-instruct-v0.1",
|
|
"mistralai/mistral-7b-instruct-v0.3",
|
|
"mistralai/mistral-7b-instruct:free",
|
|
"mistralai/mistral-large",
|
|
"mistralai/mistral-large-2407",
|
|
"mistralai/mistral-large-2411",
|
|
"mistralai/mistral-medium-3",
|
|
"mistralai/mistral-medium-3.1",
|
|
"mistralai/mistral-nemo",
|
|
"mistralai/mistral-nemo:free",
|
|
"mistralai/mistral-saba",
|
|
"mistralai/mistral-small",
|
|
"mistralai/mistral-small-24b-instruct-2501",
|
|
"mistralai/mistral-small-24b-instruct-2501:free",
|
|
"mistralai/mistral-small-3.1-24b-instruct",
|
|
"mistralai/mistral-small-3.1-24b-instruct:free",
|
|
"mistralai/mistral-small-3.2-24b-instruct",
|
|
"mistralai/mistral-small-3.2-24b-instruct:free",
|
|
"mistralai/mistral-tiny",
|
|
"mistralai/mixtral-8x22b-instruct",
|
|
"mistralai/mixtral-8x7b-instruct",
|
|
"mistralai/pixtral-12b",
|
|
"mistralai/pixtral-large-2411",
|
|
"moonshotai/kimi-dev-72b",
|
|
"moonshotai/kimi-dev-72b:free",
|
|
"moonshotai/kimi-k2",
|
|
"moonshotai/kimi-k2:free",
|
|
"moonshotai/kimi-vl-a3b-thinking",
|
|
"moonshotai/kimi-vl-a3b-thinking:free",
|
|
"morph/morph-v3-fast",
|
|
"morph/morph-v3-large",
|
|
"neversleep/llama-3-lumimaid-70b",
|
|
"neversleep/llama-3.1-lumimaid-8b",
|
|
"neversleep/noromaid-20b",
|
|
"nousresearch/deephermes-3-llama-3-8b-preview:free",
|
|
"nousresearch/deephermes-3-mistral-24b-preview",
|
|
"nousresearch/hermes-2-pro-llama-3-8b",
|
|
"nousresearch/hermes-3-llama-3.1-405b",
|
|
"nousresearch/hermes-3-llama-3.1-70b",
|
|
"nousresearch/hermes-4-405b",
|
|
"nousresearch/hermes-4-70b",
|
|
"nvidia/llama-3.1-nemotron-70b-instruct",
|
|
"nvidia/llama-3.1-nemotron-ultra-253b-v1",
|
|
"nvidia/llama-3.1-nemotron-ultra-253b-v1:free",
|
|
"nvidia/llama-3.3-nemotron-super-49b-v1",
|
|
"openai/chatgpt-4o-latest",
|
|
"openai/codex-mini",
|
|
"openai/gpt-3.5-turbo",
|
|
"openai/gpt-3.5-turbo-0613",
|
|
"openai/gpt-3.5-turbo-16k",
|
|
"openai/gpt-3.5-turbo-instruct",
|
|
"openai/gpt-4",
|
|
"openai/gpt-4-0314",
|
|
"openai/gpt-4-1106-preview",
|
|
"openai/gpt-4-turbo",
|
|
"openai/gpt-4-turbo-preview",
|
|
"openai/gpt-4.1",
|
|
"openai/gpt-4.1-mini",
|
|
"openai/gpt-4.1-nano",
|
|
"openai/gpt-4o",
|
|
"openai/gpt-4o-2024-05-13",
|
|
"openai/gpt-4o-2024-08-06",
|
|
"openai/gpt-4o-2024-11-20",
|
|
"openai/gpt-4o-audio-preview",
|
|
"openai/gpt-4o-mini",
|
|
"openai/gpt-4o-mini-2024-07-18",
|
|
"openai/gpt-4o-mini-search-preview",
|
|
"openai/gpt-4o-search-preview",
|
|
"openai/gpt-4o:extended",
|
|
"openai/gpt-5",
|
|
"openai/gpt-5-chat",
|
|
"openai/gpt-5-mini",
|
|
"openai/gpt-5-nano",
|
|
"openai/gpt-oss-120b",
|
|
"openai/gpt-oss-120b:free",
|
|
"openai/gpt-oss-20b",
|
|
"openai/gpt-oss-20b:free",
|
|
"openai/o1",
|
|
"openai/o1-mini",
|
|
"openai/o1-mini-2024-09-12",
|
|
"openai/o1-pro",
|
|
"openai/o3",
|
|
"openai/o3-mini",
|
|
"openai/o3-mini-high",
|
|
"openai/o3-pro",
|
|
"openai/o4-mini",
|
|
"openai/o4-mini-high",
|
|
"opengvlab/internvl3-14b",
|
|
"openrouter/auto",
|
|
"perplexity/r1-1776",
|
|
"perplexity/sonar",
|
|
"perplexity/sonar-deep-research",
|
|
"perplexity/sonar-pro",
|
|
"perplexity/sonar-reasoning",
|
|
"perplexity/sonar-reasoning-pro",
|
|
"pygmalionai/mythalion-13b",
|
|
"qwen/qwen-2.5-72b-instruct",
|
|
"qwen/qwen-2.5-72b-instruct:free",
|
|
"qwen/qwen-2.5-7b-instruct",
|
|
"qwen/qwen-2.5-coder-32b-instruct",
|
|
"qwen/qwen-2.5-coder-32b-instruct:free",
|
|
"qwen/qwen-2.5-vl-7b-instruct",
|
|
"qwen/qwen-max",
|
|
"qwen/qwen-plus",
|
|
"qwen/qwen-turbo",
|
|
"qwen/qwen-vl-max",
|
|
"qwen/qwen-vl-plus",
|
|
"qwen/qwen2.5-vl-32b-instruct",
|
|
"qwen/qwen2.5-vl-32b-instruct:free",
|
|
"qwen/qwen2.5-vl-72b-instruct",
|
|
"qwen/qwen2.5-vl-72b-instruct:free",
|
|
"qwen/qwen3-14b",
|
|
"qwen/qwen3-14b:free",
|
|
"qwen/qwen3-235b-a22b",
|
|
"qwen/qwen3-235b-a22b-2507",
|
|
"qwen/qwen3-235b-a22b-thinking-2507",
|
|
"qwen/qwen3-235b-a22b:free",
|
|
"qwen/qwen3-30b-a3b",
|
|
"qwen/qwen3-30b-a3b-instruct-2507",
|
|
"qwen/qwen3-30b-a3b-thinking-2507",
|
|
"qwen/qwen3-30b-a3b:free",
|
|
"qwen/qwen3-32b",
|
|
"qwen/qwen3-4b:free",
|
|
"qwen/qwen3-8b",
|
|
"qwen/qwen3-8b:free",
|
|
"qwen/qwen3-coder",
|
|
"qwen/qwen3-coder-30b-a3b-instruct",
|
|
"qwen/qwen3-coder:free",
|
|
"qwen/qwq-32b",
|
|
"qwen/qwq-32b-preview",
|
|
"qwen/qwq-32b:free",
|
|
"raifle/sorcererlm-8x22b",
|
|
"rekaai/reka-flash-3:free",
|
|
"sao10k/l3-euryale-70b",
|
|
"sao10k/l3-lunaris-8b",
|
|
"sao10k/l3.1-euryale-70b",
|
|
"sao10k/l3.3-euryale-70b",
|
|
"scb10x/llama3.1-typhoon2-70b-instruct",
|
|
"shisa-ai/shisa-v2-llama3.3-70b",
|
|
"shisa-ai/shisa-v2-llama3.3-70b:free",
|
|
"sophosympatheia/midnight-rose-70b",
|
|
"switchpoint/router",
|
|
"tencent/hunyuan-a13b-instruct",
|
|
"tencent/hunyuan-a13b-instruct:free",
|
|
"thedrummer/anubis-70b-v1.1",
|
|
"thedrummer/anubis-pro-105b-v1",
|
|
"thedrummer/rocinante-12b",
|
|
"thedrummer/skyfall-36b-v2",
|
|
"thedrummer/unslopnemo-12b",
|
|
"thudm/glm-4-32b",
|
|
"thudm/glm-4.1v-9b-thinking",
|
|
"thudm/glm-z1-32b",
|
|
"tngtech/deepseek-r1t-chimera",
|
|
"tngtech/deepseek-r1t-chimera:free",
|
|
"tngtech/deepseek-r1t2-chimera:free",
|
|
"undi95/remm-slerp-l2-13b",
|
|
"x-ai/grok-2-1212",
|
|
"x-ai/grok-2-vision-1212",
|
|
"x-ai/grok-3",
|
|
"x-ai/grok-3-beta",
|
|
"x-ai/grok-3-mini",
|
|
"x-ai/grok-3-mini-beta",
|
|
"x-ai/grok-4",
|
|
"x-ai/grok-code-fast-1",
|
|
"x-ai/grok-vision-beta",
|
|
"z-ai/glm-4-32b",
|
|
"z-ai/glm-4.5",
|
|
"z-ai/glm-4.5-air",
|
|
"z-ai/glm-4.5-air:free",
|
|
"z-ai/glm-4.5v",
|
|
],
|
|
}
|
|
|
|
# API endpoint registration (only when node is used)
|
|
try:
|
|
from server import PromptServer
|
|
from aiohttp import web
|
|
|
|
@PromptServer.instance.routes.get("/ComfyLLMToolkit/get_provider_models")
|
|
async def get_provider_models_endpoint(request):
|
|
"""Get hardcoded models for a specific provider."""
|
|
try:
|
|
provider = request.query.get("provider", "openai")
|
|
models = PROVIDER_MODELS.get(provider, ["No models available"])
|
|
return web.json_response({"provider": provider, "models": models})
|
|
except Exception as e:
|
|
logger.error(f"Error getting provider models: {e}")
|
|
return web.json_response({"provider": provider, "models": []})
|
|
|
|
logger.info("APIProviderSelectorNode: Endpoints registered")
|
|
|
|
except (ImportError, AttributeError) as e:
|
|
logger.debug(f"APIProviderSelectorNode: Server not available ({e})")
|
|
|
|
class APIProviderSelectorNode:
|
|
"""
|
|
Single node for all API-based LLM providers.
|
|
Shows hardcoded model lists immediately without API calls.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
providers = sorted(list(PROVIDER_MODELS.keys()))
|
|
default_provider = "openai"
|
|
|
|
# Consolidate all models into one list for backend validation
|
|
all_models = []
|
|
for model_list in PROVIDER_MODELS.values():
|
|
all_models.extend(model_list)
|
|
all_models = sorted(list(set(all_models)))
|
|
|
|
default_models = PROVIDER_MODELS.get(default_provider, ["No models"])
|
|
|
|
return {
|
|
"required": {
|
|
"provider": (providers, {
|
|
"default": default_provider,
|
|
"tooltip": "Select the API provider"
|
|
}),
|
|
"llm_model": (all_models, {
|
|
"default": default_models[0] if default_models else "No models",
|
|
"tooltip": "Select the model (updates when provider changes)"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"external_api_key": ("STRING", {
|
|
"default": "",
|
|
"multiline": False,
|
|
"tooltip": "API key (optional - overrides env/.env)"
|
|
}),
|
|
"context": ("*", {}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("*",)
|
|
RETURN_NAMES = ("context",)
|
|
FUNCTION = "configure_provider"
|
|
CATEGORY = "🔗llm_toolkit/providers"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, provider, llm_model, external_api_key="", context=None):
|
|
"""Dynamically update model list when provider changes"""
|
|
try:
|
|
# Get current models for validation
|
|
if external_api_key:
|
|
api_key = external_api_key
|
|
else:
|
|
try:
|
|
api_key = get_api_key(f"{provider.upper()}_API_KEY", provider)
|
|
except ValueError:
|
|
api_key = None
|
|
|
|
# Fetch models - this ensures validation includes latest models
|
|
models = get_models(provider, "localhost", "11434", api_key)
|
|
|
|
# Update the hardcoded list with fetched models
|
|
if models and len(models) > 0:
|
|
PROVIDER_MODELS[provider] = models
|
|
|
|
# Return hash based on provider and available models
|
|
import hashlib
|
|
unique_id = f"{provider}:{len(models) if models else 0}:{llm_model}"
|
|
hash_obj = hashlib.md5(unique_id.encode())
|
|
return int(hash_obj.hexdigest(), 16) / (2**128)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in IS_CHANGED: {e}")
|
|
import hashlib
|
|
# Fallback to simple hash
|
|
state = f"{provider}-{llm_model}-{external_api_key}"
|
|
return hashlib.md5(state.encode()).hexdigest()
|
|
|
|
def configure_provider(self, provider: str, llm_model: str,
|
|
external_api_key: str = "",
|
|
context: Optional[Any] = None) -> Tuple[Any]:
|
|
"""
|
|
Configure the selected provider and return context with provider_config.
|
|
Context values override node widget values if they match.
|
|
"""
|
|
|
|
# First check if context has overrides
|
|
if context and isinstance(context, dict):
|
|
# Override with values from context if they exist
|
|
if "provider" in context:
|
|
provider = context["provider"]
|
|
logger.info(f"APIProviderSelector: Provider overridden by context to: {provider}")
|
|
if "llm_model" in context:
|
|
llm_model = context["llm_model"]
|
|
logger.info(f"APIProviderSelector: Model overridden by context to: {llm_model}")
|
|
if "external_api_key" in context:
|
|
external_api_key = context["external_api_key"]
|
|
logger.info(f"APIProviderSelector: API key overridden by context")
|
|
|
|
# Clean up inputs
|
|
external_api_key = external_api_key.strip() if external_api_key else ""
|
|
|
|
# Get API key with priority: external_api_key > .env > system env
|
|
final_api_key = ""
|
|
|
|
# 1) Use external key if provided (from widget or context override)
|
|
if external_api_key:
|
|
final_api_key = external_api_key
|
|
logger.info(f"APIProviderSelector: Using provided API key for {provider}")
|
|
|
|
# 2) Try .env file in node root directory
|
|
if not final_api_key:
|
|
env_file_path = os.path.join(parent_dir, ".env")
|
|
if os.path.exists(env_file_path):
|
|
try:
|
|
# Read .env file
|
|
with open(env_file_path, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#'):
|
|
key, _, value = line.partition('=')
|
|
env_key_map = {
|
|
"openai": "OPENAI_API_KEY",
|
|
"gemini": "GEMINI_API_KEY",
|
|
"groq": "GROQ_API_KEY",
|
|
"anthropic": "ANTHROPIC_API_KEY",
|
|
"mistral": "MISTRAL_API_KEY",
|
|
"deepseek": "DEEPSEEK_API_KEY",
|
|
"bfl": "BFL_API_KEY",
|
|
"suno": "SUNO_API_KEY",
|
|
"wavespeed": "WAVESPEED_API_KEY",
|
|
"openrouter": "OPENROUTER_API_KEY",
|
|
}
|
|
if key == env_key_map.get(provider):
|
|
final_api_key = value.strip('"').strip("'")
|
|
logger.info(f"APIProviderSelector: Using .env file API key for {provider}")
|
|
break
|
|
except Exception as e:
|
|
logger.debug(f"Could not read .env file: {e}")
|
|
|
|
# 3) Fallback to system environment
|
|
if not final_api_key:
|
|
env_key_map = {
|
|
"openai": "OPENAI_API_KEY",
|
|
"gemini": "GEMINI_API_KEY",
|
|
"groq": "GROQ_API_KEY",
|
|
"anthropic": "ANTHROPIC_API_KEY",
|
|
"mistral": "MISTRAL_API_KEY",
|
|
"deepseek": "DEEPSEEK_API_KEY",
|
|
"bfl": "BFL_API_KEY",
|
|
"suno": "SUNO_API_KEY",
|
|
"wavespeed": "WAVESPEED_API_KEY",
|
|
"openrouter": "OPENROUTER_API_KEY",
|
|
}
|
|
|
|
env_key_name = env_key_map.get(provider)
|
|
if env_key_name:
|
|
try:
|
|
final_api_key = get_api_key(env_key_name, provider)
|
|
logger.info(f"APIProviderSelector: Using system environment API key for {provider}")
|
|
except ValueError:
|
|
logger.warning(f"APIProviderSelector: No API key found for {provider}")
|
|
final_api_key = ""
|
|
|
|
# 4) Use dummy key if none found
|
|
if not final_api_key:
|
|
final_api_key = "1234"
|
|
logger.warning(f"APIProviderSelector: No API key for {provider}, using dummy key")
|
|
|
|
# No built-in verification; user must ensure the key is valid
|
|
|
|
# Build provider configuration
|
|
provider_config: Dict[str, Any] = {
|
|
"provider_name": provider,
|
|
"llm_model": llm_model,
|
|
"api_key": final_api_key,
|
|
"base_ip": None, # Cloud providers don't need IP
|
|
"port": None,
|
|
}
|
|
|
|
# Merge with incoming context
|
|
if context is not None:
|
|
if isinstance(context, dict):
|
|
merged = context.copy()
|
|
merged["provider_config"] = provider_config
|
|
result = merged
|
|
else:
|
|
result = {
|
|
"provider_config": provider_config,
|
|
"passthrough_data": context
|
|
}
|
|
else:
|
|
result = {"provider_config": provider_config}
|
|
|
|
logger.info(f"APIProviderSelector: Configured {provider} with model {llm_model}")
|
|
|
|
return (result,)
|
|
|
|
# Add route for model fetching
|
|
try:
|
|
from server import PromptServer
|
|
from aiohttp import web
|
|
|
|
# --- Route Registration Check ---
|
|
if not hasattr(PromptServer.instance, "llm_toolkit_routes"):
|
|
PromptServer.instance.llm_toolkit_routes = set()
|
|
|
|
route_path = "/ComfyLLMToolkit/get_provider_models"
|
|
route_key = f"POST:{route_path}"
|
|
|
|
if route_key not in PromptServer.instance.llm_toolkit_routes:
|
|
@PromptServer.instance.routes.post(route_path)
|
|
async def get_provider_models_endpoint_post(request):
|
|
try:
|
|
data = await request.json()
|
|
provider = data.get("provider", "openai")
|
|
external_api_key = data.get("external_api_key", "")
|
|
|
|
# Use external API key if provided, otherwise get from environment
|
|
if external_api_key:
|
|
api_key = external_api_key
|
|
else:
|
|
try:
|
|
api_key = get_api_key(f"{provider.upper()}_API_KEY", provider)
|
|
except ValueError:
|
|
api_key = None
|
|
|
|
# Get models from server
|
|
models = get_models(provider, "localhost", "11434", api_key)
|
|
|
|
# Fallback to hardcoded models if fetching fails
|
|
if not models or len(models) == 0:
|
|
models = PROVIDER_MODELS.get(provider, ["No models available"])
|
|
|
|
return web.json_response({"models": models})
|
|
|
|
except Exception as e:
|
|
# Return hardcoded models on error
|
|
provider = data.get("provider", "openai") if 'data' in locals() else "openai"
|
|
models = PROVIDER_MODELS.get(provider, ["Error fetching models"])
|
|
return web.json_response({"models": models})
|
|
|
|
PromptServer.instance.llm_toolkit_routes.add(route_key)
|
|
logger.info(f"LLMToolkit: Registered POST route: {route_path}")
|
|
else:
|
|
logger.warning(f"LLMToolkit: POST route {route_path} already registered. Skipping.")
|
|
|
|
except ImportError:
|
|
print("PromptServer not available. Model fetching route not registered.")
|
|
|
|
# Node registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"APIProviderSelectorNode": APIProviderSelectorNode
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"APIProviderSelectorNode": "API Provider Selector (🔗LLMToolkit)"
|
|
}
|