Files
Nisim Joseph 28e99336fe Fix compatibility with newer OpenAI models (gpt-5-mini, o1, o3)
- Add detection for models requiring max_completion_tokens vs max_tokens
- Add detection for models that don't support custom temperature values
- Conditionally apply API parameters based on model capabilities
- Update module documentation with new features
- Follows SOLID principles with separate detection methods
2025-10-27 21:55:53 +02:00

271 lines
12 KiB
Python

"""
OpenAI Prompt Generator for ComfyUI
This module provides a ComfyUI custom node that integrates with OpenAI's API to generate
creative prompts for image generation. It handles both legacy and modern OpenAI API versions,
automatically detecting the appropriate parameter format for different model generations.
Key Features:
- Supports both old (max_tokens) and new (max_completion_tokens) OpenAI API parameters
- Automatically detects model capabilities and adjusts parameters accordingly
- Conditionally applies temperature parameter (newer models only support default value)
- Caches available models and client instances to avoid repeated API calls
- Handles various API errors gracefully with user-friendly error messages
- Compatible with OpenAI v1.0.0+ and legacy versions
Architecture:
- Singleton pattern for model caching to improve performance
- Model detection logic separated into dedicated methods (_uses_max_completion_tokens, _supports_custom_temperature)
- API call logic isolated in _generate_prompt_internal for better testability
- Follows Open/Closed Principle: extensible for new model types without modifying existing code
Interactions:
- ComfyUI: Registers as a custom node in the "prompt" category
- OpenAI API: Makes chat completion requests with appropriate parameters based on model version
- Environment: Reads OPENAI_API_KEY from environment variables
"""
import os
import json
import openai
from openai import OpenAI
from typing import List, Dict
__reload_libs__ = True
class OpenAIPromptGenerator:
_instance = None
_models_cache = None
_client_cache = None
def __init__(self):
if OpenAIPromptGenerator._models_cache is None:
self.client = None
self.available_models = []
self._initialize_client()
# Cache both the models and the client
OpenAIPromptGenerator._models_cache = self.available_models
OpenAIPromptGenerator._client_cache = self.client
else:
# Restore both from cache
self.available_models = OpenAIPromptGenerator._models_cache
self.client = OpenAIPromptGenerator._client_cache
@classmethod
def INPUT_TYPES(cls):
try:
if cls._instance is None:
cls._instance = cls()
return {
"required": {
"model": (cls._instance.available_models,),
"prompt_context": ("STRING", {
"multiline": True,
"default": "Generate a detailed prompt for an image generation AI"
}),
"additional_instructions": ("STRING", {
"multiline": True,
"default": "give it without any prefix, or things like \"here the prompt for:\". just give the image prompt"
}),
"max_tokens": ("INT", {
"default": 512,
"min": 256,
"max": 4096,
"step": 256
}),
"temperature": ("FLOAT", {
"default": 0.7,
"min": 0.0,
"max": 2.0,
"step": 0.1
}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"step": 1
})
}
}
except Exception as e:
print(f"Error initializing INPUT_TYPES: {str(e)}")
return {
"required": {
"error": ("STRING", {"default": "Error: OpenAI API key not found or invalid"})
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("generated_prompt",) # Changed to match the key used in generate_prompt method
FUNCTION = "generate_prompt"
CATEGORY = "prompt" # Changed to match README's category description
def _initialize_client(self):
api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
self.available_models = ["Error: OpenAI API key not configured"]
return
try:
# Check if we're using OpenAI v1.0.0 or newer
if hasattr(openai, 'OpenAI'):
self.client = OpenAI(api_key=api_key)
else:
# For older versions of the OpenAI package
openai.api_key = api_key
self.client = openai
try:
# Handle both new and old API versions
if hasattr(self.client, 'models'):
models = self.client.models.list()
else:
models = self.client.Model.list()
except openai.AuthenticationError as auth_err:
self.available_models = ["Error: Invalid API key"]
return
except openai.PermissionError as perm_err:
self.available_models = ["Error: Permission denied"]
return
self.available_models = [
model.id for model in models.data
if "gpt" in model.id.lower() # Filter for GPT models
]
if not self.available_models:
# Fallback to common models
self.available_models = ["gpt-3.5-turbo"]
except openai.APIError as api_err:
self.available_models = ["Error: OpenAI API error occurred"]
except openai.APIConnectionError as conn_err:
self.available_models = ["Error: Could not connect to OpenAI"]
except Exception as e:
self.available_models = ["Error: Could not fetch models"]
def generate_prompt(self, model: str, prompt_context: str, additional_instructions: str, max_tokens: int, temperature: float, seed: int):
result = self._generate_prompt_internal(model, prompt_context, additional_instructions, max_tokens, temperature, seed)
if isinstance(result, tuple) and len(result) == 1 and isinstance(result[0], str):
return result
elif isinstance(result, str):
return (result,)
else:
error_message = "⚠️ Invalid response format"
return (error_message,)
def _uses_max_completion_tokens(self, model: str) -> bool:
"""
Determines if a model uses the new 'max_completion_tokens' parameter.
Newer models (gpt-5-mini, o1, o3, etc.) require max_completion_tokens instead of max_tokens.
"""
model_lower = model.lower()
# Check for GPT-5 series
is_gpt5_series = "gpt-5" in model_lower
# Check for o-series models (o1, o3, etc.)
is_o_series = model_lower.startswith("o1") or model_lower.startswith("o3")
# Newer models use max_completion_tokens
should_use_new_param = is_gpt5_series or is_o_series
return should_use_new_param
def _supports_custom_temperature(self, model: str) -> bool:
"""
Determines if a model supports custom temperature values.
Some newer models (o1, o3, gpt-5 series) only support the default temperature of 1.
"""
model_lower = model.lower()
# Check for GPT-5 series
is_gpt5_series = "gpt-5" in model_lower
# Check for o-series models
is_o_series = model_lower.startswith("o1") or model_lower.startswith("o3")
# These models don't support custom temperature
does_not_support_custom = is_gpt5_series or is_o_series
return not does_not_support_custom
def _generate_prompt_internal(self, model: str, prompt_context: str, additional_instructions: str, max_tokens: int, temperature: float, seed: int):
if any(error in model for error in ["Error:", "quota exceeded"]):
return ("⚠️ Please configure a valid OpenAI API key with available credits",)
try:
generated_prompt = ""
# Determine model capabilities
uses_new_api = self._uses_max_completion_tokens(model)
supports_custom_temp = self._supports_custom_temperature(model)
# For OpenAI v1.0.0+
if hasattr(self.client, 'chat'):
api_params = {
"model": model,
"messages": [
{"role": "system", "content": "You are a creative assistant that generates detailed, descriptive prompts for image generation AI. Focus on visual details, style, mood, and composition."},
{"role": "user", "content": f"{prompt_context}\n\n{additional_instructions}"}
],
"seed": seed
}
# Add temperature only if model supports it
if supports_custom_temp:
api_params["temperature"] = temperature
# Use appropriate max tokens parameter based on model
if uses_new_api:
api_params["max_completion_tokens"] = max_tokens
else:
api_params["max_tokens"] = max_tokens
response = self.client.chat.completions.create(**api_params)
generated_prompt = response.choices[0].message.content.strip()
return (generated_prompt,)
# For older versions
else:
response = self.client.ChatCompletion.create(
model=model,
messages=[
{"role": "system", "content": "You are a creative assistant that generates detailed, descriptive prompts for image generation AI. Focus on visual details, style, mood, and composition."},
{"role": "user", "content": f"{prompt_context}\n\n{additional_instructions}"}
],
max_tokens=max_tokens,
temperature=temperature
)
generated_prompt = response.choices[0].message['content'].strip()
return (generated_prompt,)
except openai.RateLimitError as e:
error_message = ("⚠️ OpenAI API quota exceeded. Please visit https://platform.openai.com/account/billing "
"to add credits to your account or check your payment method.")
self.available_models = ["Error: Quota exceeded - Add credits to continue"]
return (str(error_message),)
except openai.APIError as e:
if "insufficient_quota" in str(e):
error_message = "⚠️ OpenAI API quota exceeded. Please check your billing details at https://platform.openai.com/account/billing"
else:
error_message = f"⚠️ OpenAI API Error: {str(e)}"
return (error_message,)
except Exception as e:
error_message = f"⚠️ Error: {str(e)}"
return (error_message,)
# Node registration
NODE_CLASS_MAPPINGS = {
"OpenAI Prompt Generator": OpenAIPromptGenerator
}
NODE_DISPLAY_NAME_MAPPINGS = {
"OpenAI Prompt Generator": "OpenAI Prompt Generator"
}
def reset_cache():
OpenAIPromptGenerator._instance = None
OpenAIPromptGenerator._models_cache = None
OpenAIPromptGenerator._client_cache = None
reset_cache() # Reset cache when module is reloaded