Default PaintPlus OpenAI provider to gpt-image-2, fix GPU quick-start
dall-e-2/dall-e-3 retired May 12 2026 and gpt-image-1 deprecates Oct 23 2026, so move every OpenAI default (config.py, both provider classes, both compose files, .env.example, the in-app provider-settings dropdown, README) to gpt-image-2 for both generation and edits. Also fix response parsing in ai_provider.py's OpenAIProvider, which never sent a model param and assumed a url response — gpt-image-1/2 only return b64_json. Separately, .env.example shipped AI_PROVIDER=replicate by default, but replicate has no driver in remote_provider.py, so following the documented "cp .env.example .env" setup silently broke every AI call and defeated the GPU quick-start (an explicit non-empty .env value overrides docker-compose.gpu.yml's own local_gpu fallback). Default to local_gpu instead, mark replicate/stability as not-yet-implemented, and recommend Lykon/dreamshaper-8-inpainting as a hands/face-tuned HF_MODEL_INPAINT override for 4-6GB cards (Quadro P2200, GTX 1060/1660). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH
This commit is contained in:
+22
-12
@@ -19,18 +19,22 @@
|
|||||||
# local_gpu = FREE, runs on YOUR GPU — best option if you have an NVIDIA card
|
# local_gpu = FREE, runs on YOUR GPU — best option if you have an NVIDIA card
|
||||||
# (use docker-compose.gpu.yml — models auto-download on first use)
|
# (use docker-compose.gpu.yml — models auto-download on first use)
|
||||||
# mock = Free, returns original image unchanged (UI testing only)
|
# mock = Free, returns original image unchanged (UI testing only)
|
||||||
# openai = DALL-E 3 / gpt-image-1 (~$0.02-0.04/image)
|
# openai = gpt-image-2 generation + edits (~$0.006-0.21/image, see below)
|
||||||
# stability = Stability AI SDXL (~$0.01/image)
|
# stability = NOT IMPLEMENTED YET — config field exists but no driver in
|
||||||
# replicate = Multiple models (~$0.002-0.03/image)
|
# remote_provider.py; setting this breaks every AI call
|
||||||
|
# replicate = NOT IMPLEMENTED YET — same as above (Replicate IS used for
|
||||||
|
# Smart Select's SAM fallback, but that's a separate code path)
|
||||||
# invokeai = Self-hosted InvokeAI running on another machine
|
# invokeai = Self-hosted InvokeAI running on another machine
|
||||||
# comfyui = Self-hosted ComfyUI running on another machine
|
# comfyui = Self-hosted ComfyUI running on another machine
|
||||||
#
|
#
|
||||||
# GPU QUICK-START:
|
# GPU QUICK-START:
|
||||||
# docker compose -f docker-compose.gpu.yml up --build
|
# docker compose -f docker-compose.gpu.yml up --build
|
||||||
# (AI_PROVIDER defaults to local_gpu in that compose file)
|
# (AI_PROVIDER defaults to local_gpu in that compose file — but only if this
|
||||||
|
# var is unset/blank; since AI_PROVIDER=local_gpu is set explicitly below,
|
||||||
|
# that's what you get either way)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
AI_PROVIDER=replicate
|
AI_PROVIDER=local_gpu
|
||||||
|
|
||||||
# ── Local GPU settings (only relevant when AI_PROVIDER=local_gpu) ────────────
|
# ── Local GPU settings (only relevant when AI_PROVIDER=local_gpu) ────────────
|
||||||
# Auto-download HuggingFace models on first request (true/false)
|
# Auto-download HuggingFace models on first request (true/false)
|
||||||
@@ -40,7 +44,11 @@ LOCAL_GPU_MAX_PIPELINES=2
|
|||||||
# HuggingFace token — only needed for gated/private models
|
# HuggingFace token — only needed for gated/private models
|
||||||
#HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
#HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
# Override auto-selected model for any operation (leave blank = auto by VRAM tier)
|
# Override auto-selected model for any operation (leave blank = auto by VRAM tier)
|
||||||
#HF_MODEL_INPAINT=your-org/your-inpaint-model
|
# On 4-6GB cards (Quadro P2200, GTX 1060/1660, etc.) the auto-tier picks either
|
||||||
|
# slow SDXL+CPU-offload or a generic (not hand/face-tuned) SD checkpoint.
|
||||||
|
# Lykon/dreamshaper-8-inpainting is SD1.5-based (1.7GB, fast, no offload needed)
|
||||||
|
# and noticeably better on hands/faces — worth forcing on small cards:
|
||||||
|
#HF_MODEL_INPAINT=Lykon/dreamshaper-8-inpainting
|
||||||
#HF_MODEL_TXT2IMG=your-org/your-txt2img-model
|
#HF_MODEL_TXT2IMG=your-org/your-txt2img-model
|
||||||
#HF_MODEL_IMG2IMG=your-org/your-img2img-model
|
#HF_MODEL_IMG2IMG=your-org/your-img2img-model
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -76,17 +84,19 @@ LOCAL_GPU_MAX_PIPELINES=2
|
|||||||
REPLICATE_API_KEY=r8_PASTE_YOUR_KEY_HERE
|
REPLICATE_API_KEY=r8_PASTE_YOUR_KEY_HERE
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────────
|
||||||
# OPENAI (cloud, dall-e-3 / gpt-image-1)
|
# OPENAI (cloud, gpt-image-2)
|
||||||
# Get key at: https://platform.openai.com/api-keys
|
# Get key at: https://platform.openai.com/api-keys
|
||||||
# AI_PROVIDER=openai
|
# AI_PROVIDER=openai
|
||||||
# ───────────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────────
|
||||||
|
# dall-e-2 and dall-e-3 were retired May 12, 2026; gpt-image-1 deprecates
|
||||||
|
# Oct 23, 2026. gpt-image-2 is the current model and handles both
|
||||||
|
# generation and masked edits (inpaint/img2img/outpaint) — no org
|
||||||
|
# verification step needed, unlike gpt-image-1.
|
||||||
|
# Pricing (1024x1024): ~$0.006 low / $0.053 medium / $0.211 high quality.
|
||||||
#OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
#OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
#OPENAI_MODEL=dall-e-3
|
#OPENAI_MODEL=gpt-image-2
|
||||||
# Model for inpaint/img2img/outpaint (the /v1/images/edits endpoint).
|
# Model for inpaint/img2img/outpaint (the /v1/images/edits endpoint).
|
||||||
# dall-e-3 does NOT support edits at all — keep this on gpt-image-1 for
|
#OPENAI_EDIT_MODEL=gpt-image-2
|
||||||
# ChatGPT-comparable edit quality, or set dall-e-2 for the older/cheaper option.
|
|
||||||
# Note: gpt-image-1 may require completing org verification at platform.openai.com.
|
|
||||||
#OPENAI_EDIT_MODEL=gpt-image-1
|
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────────
|
||||||
# INVOKEAI (self-hosted, best for Flux/SDXL)
|
# INVOKEAI (self-hosted, best for Flux/SDXL)
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ BUILDID=$(date +%s) ./bring-up-local-gpu.sh
|
|||||||
| Provider | Setup | Cost | Quality |
|
| Provider | Setup | Cost | Quality |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `local_gpu` | GPU machine + nvidia-container-toolkit | Free | Best (SDXL/FLUX auto-selected by VRAM) |
|
| `local_gpu` | GPU machine + nvidia-container-toolkit | Free | Best (SDXL/FLUX auto-selected by VRAM) |
|
||||||
| `openai` | `OPENAI_API_KEY=sk-...` | ~$0.02–0.04/image | DALL-E 3 |
|
| `openai` | `OPENAI_API_KEY=sk-...` | ~$0.006–0.21/image | gpt-image-2 |
|
||||||
| `replicate` | `REPLICATE_API_KEY=r8_...` | ~$0.002–0.03/image | Multiple models |
|
| `replicate` | `REPLICATE_API_KEY=r8_...` | ~$0.002–0.03/image | Multiple models |
|
||||||
| `invokeai` | InvokeAI running on another machine | Self-hosted | FLUX/SDXL |
|
| `invokeai` | InvokeAI running on another machine | Self-hosted | FLUX/SDXL |
|
||||||
| `comfyui` | ComfyUI running on another machine | Self-hosted | Any model |
|
| `comfyui` | ComfyUI running on another machine | Self-hosted | Any model |
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# Provider API Keys
|
# Provider API Keys
|
||||||
openai_api_key: str = ""
|
openai_api_key: str = ""
|
||||||
openai_model: str = "dall-e-3" # text-to-image (generations endpoint)
|
openai_model: str = "gpt-image-2" # text-to-image (generations endpoint)
|
||||||
openai_edit_model: str = "gpt-image-1" # inpaint/img2img/outpaint (edits endpoint — dall-e-3 isn't supported there)
|
openai_edit_model: str = "gpt-image-2" # inpaint/img2img/outpaint (edits endpoint)
|
||||||
stability_api_key: str = ""
|
stability_api_key: str = ""
|
||||||
replicate_api_key: str = ""
|
replicate_api_key: str = ""
|
||||||
|
|
||||||
|
|||||||
@@ -62,12 +62,22 @@ class AIProvider(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class OpenAIProvider(AIProvider):
|
class OpenAIProvider(AIProvider):
|
||||||
"""OpenAI DALL-E 2 based image editing (NOTE: Lower quality than DALL-E 3)"""
|
"""OpenAI image API — gpt-image-2 (generation + edits, same model/endpoint family)."""
|
||||||
|
|
||||||
def __init__(self, api_key: str):
|
def __init__(self, api_key: str, model: str = "gpt-image-2", edit_model: str = "gpt-image-2"):
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
|
self.model = model
|
||||||
|
self.edit_model = edit_model
|
||||||
self.base_url = "https://api.openai.com/v1"
|
self.base_url = "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
async def _fetch_result(self, client: httpx.AsyncClient, item: dict) -> bytes:
|
||||||
|
# gpt-image-1/2 only ever return b64_json; dall-e-2/dall-e-3 default to a url.
|
||||||
|
if item.get('b64_json'):
|
||||||
|
return base64.b64decode(item['b64_json'])
|
||||||
|
image_response = await client.get(item['url'])
|
||||||
|
image_response.raise_for_status()
|
||||||
|
return image_response.content
|
||||||
|
|
||||||
async def edit_image(
|
async def edit_image(
|
||||||
self,
|
self,
|
||||||
patch_image_bytes: bytes,
|
patch_image_bytes: bytes,
|
||||||
@@ -77,7 +87,7 @@ class OpenAIProvider(AIProvider):
|
|||||||
full_image_bytes: Optional[bytes] = None,
|
full_image_bytes: Optional[bytes] = None,
|
||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""Edit image using OpenAI DALL-E 2 (NOTE: Uses older model, lower quality)"""
|
"""Edit image using OpenAI's images/edits endpoint"""
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
files = {
|
files = {
|
||||||
@@ -86,6 +96,7 @@ class OpenAIProvider(AIProvider):
|
|||||||
}
|
}
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
'model': model or self.edit_model,
|
||||||
'prompt': prompt,
|
'prompt': prompt,
|
||||||
'n': 1,
|
'n': 1,
|
||||||
'size': '1024x1024' # Will be adjusted based on input
|
'size': '1024x1024' # Will be adjusted based on input
|
||||||
@@ -104,13 +115,7 @@ class OpenAIProvider(AIProvider):
|
|||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
return await self._fetch_result(client, result['data'][0])
|
||||||
# Download the generated image
|
|
||||||
image_url = result['data'][0]['url']
|
|
||||||
image_response = await client.get(image_url)
|
|
||||||
image_response.raise_for_status()
|
|
||||||
|
|
||||||
return image_response.content
|
|
||||||
|
|
||||||
async def text_to_image(
|
async def text_to_image(
|
||||||
self,
|
self,
|
||||||
@@ -120,10 +125,11 @@ class OpenAIProvider(AIProvider):
|
|||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
negative_prompt: Optional[str] = None
|
negative_prompt: Optional[str] = None
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""Generate image using OpenAI DALL-E"""
|
"""Generate image using OpenAI's images/generations endpoint"""
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
data = {
|
data = {
|
||||||
|
'model': model or self.model,
|
||||||
'prompt': prompt,
|
'prompt': prompt,
|
||||||
'n': 1,
|
'n': 1,
|
||||||
'size': f'{width}x{height}' if width == height else '1024x1024'
|
'size': f'{width}x{height}' if width == height else '1024x1024'
|
||||||
@@ -141,13 +147,7 @@ class OpenAIProvider(AIProvider):
|
|||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
return await self._fetch_result(client, result['data'][0])
|
||||||
# Download the generated image
|
|
||||||
image_url = result['data'][0]['url']
|
|
||||||
image_response = await client.get(image_url)
|
|
||||||
image_response.raise_for_status()
|
|
||||||
|
|
||||||
return image_response.content
|
|
||||||
|
|
||||||
|
|
||||||
class StabilityAIProvider(AIProvider):
|
class StabilityAIProvider(AIProvider):
|
||||||
@@ -518,7 +518,7 @@ def get_ai_provider(provider_name: Optional[str] = None, model: Optional[str] =
|
|||||||
if provider == "openai":
|
if provider == "openai":
|
||||||
if not settings.openai_api_key:
|
if not settings.openai_api_key:
|
||||||
raise ValueError("OpenAI API key not configured")
|
raise ValueError("OpenAI API key not configured")
|
||||||
return OpenAIProvider(settings.openai_api_key)
|
return OpenAIProvider(settings.openai_api_key, settings.openai_model, settings.openai_edit_model)
|
||||||
|
|
||||||
elif provider == "stability":
|
elif provider == "stability":
|
||||||
if not settings.stability_api_key:
|
if not settings.stability_api_key:
|
||||||
|
|||||||
@@ -28,12 +28,11 @@ class RemoteAIProvider(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class OpenAIRemoteProvider(RemoteAIProvider):
|
class OpenAIRemoteProvider(RemoteAIProvider):
|
||||||
"""OpenAI image API — gpt-image-1 / dall-e-3."""
|
"""OpenAI image API — gpt-image-2 (generation + edits, same model/endpoint family)."""
|
||||||
|
|
||||||
def __init__(self, api_key: str, model: str = "dall-e-3", edit_model: str = "gpt-image-1"):
|
def __init__(self, api_key: str, model: str = "gpt-image-2", edit_model: str = "gpt-image-2"):
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.model = model
|
self.model = model
|
||||||
# dall-e-3 has no edits/inpaint support at all — edits need a model of their own.
|
|
||||||
self.edit_model = edit_model
|
self.edit_model = edit_model
|
||||||
self.base_url = "https://api.openai.com/v1"
|
self.base_url = "https://api.openai.com/v1"
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ class OpenAIRemoteProvider(RemoteAIProvider):
|
|||||||
return {"Authorization": f"Bearer {self.api_key}"}
|
return {"Authorization": f"Bearer {self.api_key}"}
|
||||||
|
|
||||||
async def _fetch_result(self, client: httpx.AsyncClient, item: dict) -> bytes:
|
async def _fetch_result(self, client: httpx.AsyncClient, item: dict) -> bytes:
|
||||||
# gpt-image-1 only ever returns b64_json; dall-e-2/dall-e-3 default to a url.
|
# gpt-image-1/2 only ever return b64_json; dall-e-2/dall-e-3 default to a url.
|
||||||
if item.get("b64_json"):
|
if item.get("b64_json"):
|
||||||
return base64.b64decode(item["b64_json"])
|
return base64.b64decode(item["b64_json"])
|
||||||
img_r = await client.get(item["url"])
|
img_r = await client.get(item["url"])
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ services:
|
|||||||
|
|
||||||
# ── Remote/cloud providers (all optional) ────────────────────────────────
|
# ── Remote/cloud providers (all optional) ────────────────────────────────
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||||
- OPENAI_MODEL=${OPENAI_MODEL:-dall-e-3}
|
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-image-2}
|
||||||
- OPENAI_EDIT_MODEL=${OPENAI_EDIT_MODEL:-gpt-image-1}
|
- OPENAI_EDIT_MODEL=${OPENAI_EDIT_MODEL:-gpt-image-2}
|
||||||
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
|
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
|
||||||
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
|
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ services:
|
|||||||
- SECRET_KEY=${SECRET_KEY:-change-this-secret-key-in-production}
|
- SECRET_KEY=${SECRET_KEY:-change-this-secret-key-in-production}
|
||||||
- AI_PROVIDER=${AI_PROVIDER:-mock}
|
- AI_PROVIDER=${AI_PROVIDER:-mock}
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||||
- OPENAI_MODEL=${OPENAI_MODEL:-dall-e-3}
|
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-image-2}
|
||||||
- OPENAI_EDIT_MODEL=${OPENAI_EDIT_MODEL:-gpt-image-1}
|
- OPENAI_EDIT_MODEL=${OPENAI_EDIT_MODEL:-gpt-image-2}
|
||||||
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
|
- STABILITY_API_KEY=${STABILITY_API_KEY:-}
|
||||||
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
|
- REPLICATE_API_KEY=${REPLICATE_API_KEY:-}
|
||||||
- INVOKEAI_URL=${INVOKEAI_URL:-}
|
- INVOKEAI_URL=${INVOKEAI_URL:-}
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ class Tools_ai_provider_settings_class {
|
|||||||
{
|
{
|
||||||
name: 'openai_model',
|
name: 'openai_model',
|
||||||
title: 'OpenAI model:',
|
title: 'OpenAI model:',
|
||||||
value: ls_get('openai_model', 'dall-e-3'),
|
value: ls_get('openai_model', 'gpt-image-2'),
|
||||||
values: ['dall-e-3', 'dall-e-2'],
|
values: ['gpt-image-2', 'gpt-image-1'],
|
||||||
type: 'select',
|
type: 'select',
|
||||||
},
|
},
|
||||||
// ── InvokeAI ──────────────────────────────────────────────
|
// ── InvokeAI ──────────────────────────────────────────────
|
||||||
@@ -171,7 +171,7 @@ class Tools_ai_provider_settings_class {
|
|||||||
ls_set('provider_img2img', params.provider_img2img || '');
|
ls_set('provider_img2img', params.provider_img2img || '');
|
||||||
ls_set('provider_outpaint', params.provider_outpaint || '');
|
ls_set('provider_outpaint', params.provider_outpaint || '');
|
||||||
ls_set('openai_key', params.openai_key || '');
|
ls_set('openai_key', params.openai_key || '');
|
||||||
ls_set('openai_model', params.openai_model || 'dall-e-3');
|
ls_set('openai_model', params.openai_model || 'gpt-image-2');
|
||||||
ls_set('invokeai_url', params.invokeai_url || '');
|
ls_set('invokeai_url', params.invokeai_url || '');
|
||||||
ls_set('invokeai_model', params.invokeai_model || 'flux-dev');
|
ls_set('invokeai_model', params.invokeai_model || 'flux-dev');
|
||||||
ls_set('comfyui_url', params.comfyui_url || '');
|
ls_set('comfyui_url', params.comfyui_url || '');
|
||||||
@@ -187,7 +187,7 @@ class Tools_ai_provider_settings_class {
|
|||||||
ai_provider_img2img: params.provider_img2img || '',
|
ai_provider_img2img: params.provider_img2img || '',
|
||||||
ai_provider_outpaint: params.provider_outpaint || '',
|
ai_provider_outpaint: params.provider_outpaint || '',
|
||||||
openai_api_key: params.openai_key || '',
|
openai_api_key: params.openai_key || '',
|
||||||
openai_model: params.openai_model || 'dall-e-3',
|
openai_model: params.openai_model || 'gpt-image-2',
|
||||||
invokeai_url: params.invokeai_url || '',
|
invokeai_url: params.invokeai_url || '',
|
||||||
invokeai_default_model: params.invokeai_model || 'flux-dev',
|
invokeai_default_model: params.invokeai_model || 'flux-dev',
|
||||||
comfyui_url: params.comfyui_url || '',
|
comfyui_url: params.comfyui_url || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user