From 01328290179cb61b095ea8cb54b5e5f317411882 Mon Sep 17 00:00:00 2001 From: Concedo <39025047+LostRuins@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:40:54 +0800 Subject: [PATCH] openai image edit endpoint --- koboldcpp.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/koboldcpp.py b/koboldcpp.py index c193322d0..f236e3b70 100644 --- a/koboldcpp.py +++ b/koboldcpp.py @@ -2600,6 +2600,24 @@ def sd_oai_transform_params(genparams): genparams["height"] = height return genparams +def sd_oai_transform_edit_params(genparams): + genparams = sd_oai_transform_params(genparams) + images = genparams.pop("image", None) + if images is None: + images = genparams.pop("image[]", None) + if images and not isinstance(images, list): + images = [images] + images = [img for img in (images or []) if img] + if images: + genparams["init_images"] = [] + extra_images = genparams.get("extra_images", images) + if not isinstance(extra_images, list): + extra_images = [extra_images] + genparams["extra_images"] = extra_images + if not genparams.get("denoising_strength"): + genparams["denoising_strength"] = 0.6 + return genparams + def sd_comfyui_tranform_params(genparams): promptobj = genparams.get('prompt', None) if promptobj and isinstance(promptobj, dict): @@ -5076,7 +5094,7 @@ class KcppProxyHandler(http.server.BaseHTTPRequestHandler): is_chat_completions_path = (clean_path.endswith('/v1/chat/completions') or clean_path=='/chat/completions') #any requests to the following endpoints is capable of waking the server - wake_requests = ["/api/extra/generate/stream","/api/extra/tokencount","/api/v1/generate","/sdapi/v1/interrogate","/v1/completions","/v1/chat/completions","/v1/responses","/completions","/chat/completions","/responses","/api/extra/transcribe","/v1/audio/transcriptions","/api/extra/tts","/v1/audio/speech","/api/extra/embeddings","/v1/embeddings","/api/embed","/api/extra/music/prepare","/api/extra/music/generate","/images/generations","/v1/images/generations","/sdapi/v1/txt2img","/sdapi/v1/img2img","/sdapi/v1/upscale"] + wake_requests = ["/api/extra/generate/stream","/api/extra/tokencount","/api/v1/generate","/sdapi/v1/interrogate","/v1/completions","/v1/chat/completions","/v1/responses","/completions","/chat/completions","/responses","/api/extra/transcribe","/v1/audio/transcriptions","/api/extra/tts","/v1/audio/speech","/api/extra/embeddings","/v1/embeddings","/api/embed","/api/extra/music/prepare","/api/extra/music/generate","/images/generations","/v1/images/generations","/images/edits","/v1/images/edits","/sdapi/v1/txt2img","/sdapi/v1/img2img","/sdapi/v1/upscale"] is_wake_request = clean_path in wake_requests autoswapEnabled = global_memory["autoswapmode"] is not None and global_memory["autoswapmode"] @@ -5130,7 +5148,7 @@ class KcppProxyHandler(http.server.BaseHTTPRequestHandler): ttsReqs = ["/api/extra/tts", "/v1/audio/speech"] embedReqs = ["/api/extra/embeddings", "/v1/embeddings", "/api/embed"] musicReqs = ["/api/extra/music/prepare","/api/extra/music/generate"] - imageReqs = ["/images/generations", "/v1/images/generations", "/sdapi/v1/txt2img", "/sdapi/v1/img2img", "/sdapi/v1/upscale"] # "/sdapi/v1/sd-models", "/sdapi/v1/options", "/sdapi/v1/samplers" + imageReqs = ["/images/generations", "/v1/images/generations", "/images/edits", "/v1/images/edits", "/sdapi/v1/txt2img", "/sdapi/v1/img2img", "/sdapi/v1/upscale"] # "/sdapi/v1/sd-models", "/sdapi/v1/options", "/sdapi/v1/samplers" swapModeChanged = False if any(clean_path.endswith(e) for e in textReqs) and (global_memory["swapReqType"] is None or global_memory["swapReqType"] != "text"): @@ -5351,6 +5369,58 @@ class KcppServerRequestHandler(http.server.SimpleHTTPRequestHandler): print(f"File Upload Process Error: {e}") return result + def extract_multipart_formdata(self, body): + result = {"fields": {}, "files": {}} + try: + content_type = self.headers.get('content-type', '') or self.headers.get('Content-Type', '') + boundary_match = re.search(r'boundary=(?:"([^"]+)"|([^;]+))', content_type, flags=re.IGNORECASE) + if not boundary_match: + return result + boundary = (boundary_match.group(1) or boundary_match.group(2) or "").strip() + if not boundary: + return result + delimiter = ("--" + boundary).encode() + for part in body.split(delimiter): + part = part.strip(b"\r\n") + if not part or part == b"--": + continue + if part.endswith(b"--"): + part = part[:-2].rstrip(b"\r\n") + header_end = part.find(b"\r\n\r\n") + if header_end < 0: + continue + header_text = part[:header_end].decode("utf-8", errors="ignore") + content = part[header_end + 4:] + if content.endswith(b"\r\n"): + content = content[:-2] + name_match = re.search(r'Content-Disposition[^;]*;.*\bname=(?:"([^"]+)"|([^;\s]+))', header_text, flags=re.IGNORECASE) + if not name_match: + continue + name = name_match.group(1) or name_match.group(2) + filename_match = re.search(r'\bfilename=(?:"([^"]*)"|([^;\s]*))', header_text, flags=re.IGNORECASE) + if filename_match: + file_data_base64 = base64.b64encode(content).decode("utf-8", "ignore") + result["files"].setdefault(name, []).append(file_data_base64) + else: + result["fields"][name] = content.decode("utf-8", errors="ignore").strip() + return result + except Exception as e: + print(f"Multipart FormData Process Error: {e}") + return result + + def extract_oai_image_edit_formdata(self, body): + formdata = self.extract_multipart_formdata(body) + genparams = dict(formdata.get("fields", {})) + files = formdata.get("files", {}) + images = [] + for key in ("image", "image[]"): + images.extend(files.get(key, [])) + if images: + genparams["image"] = images + if files.get("mask"): + genparams["mask"] = files["mask"][0] + return genparams + def prepare_basic_responses_body(self,resp_id,genparams): global friendlymodelname global autoswapmode, textName, sttName, ttsName, embedName, musicName, imageName, mmprojName @@ -7235,6 +7305,7 @@ Change Mode
is_imggen = False is_comfyui_imggen = False is_oai_imggen = False + is_oai_imgedit = False is_img_upscale = False is_transcribe = False is_tts = False @@ -7334,12 +7405,15 @@ Change Mode
api_format = 9 elif clean_path.endswith('/sdapi/v1/extra-single-image') or clean_path.endswith('/sdapi/v1/upscale'): is_img_upscale = True - elif clean_path=="/prompt" or clean_path=="/images/generations" or clean_path.endswith('/v1/images/generations') or clean_path.endswith('/sdapi/v1/txt2img') or clean_path.endswith('/sdapi/v1/img2img'): + elif clean_path=="/prompt" or clean_path=="/images/generations" or clean_path.endswith('/v1/images/generations') or clean_path=="/images/edits" or clean_path.endswith('/v1/images/edits') or clean_path.endswith('/sdapi/v1/txt2img') or clean_path.endswith('/sdapi/v1/img2img'): is_imggen = True if clean_path=="/prompt": is_comfyui_imggen = True elif clean_path.endswith('/v1/images/generations') or clean_path=="/images/generations": is_oai_imggen = True + elif clean_path.endswith('/v1/images/edits') or clean_path=="/images/edits": + is_oai_imggen = True + is_oai_imgedit = True elif clean_path.endswith('/api/extra/transcribe') or clean_path.endswith('/v1/audio/transcriptions') or clean_path=="/audio/transcriptions": is_transcribe = True elif clean_path.endswith('/api/extra/tts') or clean_path.endswith('/v1/audio/speech') or clean_path=="/audio/speech" or clean_path.endswith('/tts_to_audio'): @@ -7379,6 +7453,8 @@ Change Mode
genparams["prompt"] = formdata["prompt"] if "language" in formdata and formdata["language"]: genparams["language"] = formdata["language"] + elif is_oai_imgedit: + genparams = self.extract_oai_image_edit_formdata(body) if not genparams: utfprint("Body Err: " + str(body)) @@ -7710,6 +7786,8 @@ Change Mode
if is_comfyui_imggen: lastgeneratedcomfyimg = b'' genparams = sd_comfyui_tranform_params(genparams) + elif is_oai_imgedit: + genparams = sd_oai_transform_edit_params(genparams) elif is_oai_imggen: genparams = sd_oai_transform_params(genparams) if not genparams.get('lora'): @@ -7742,7 +7820,8 @@ Change Mode
lastgeneratedcomfyimg = b'' genresp = (json.dumps({"prompt_id": "12345678-0000-0000-0000-000000000001","number": 0,"node_errors":{}}).encode()) elif is_oai_imggen: - genresp = (json.dumps({"created":int(time.time()),"data":[{"b64_json":gendat}],"background":"opaque","output_format":"png","size":"1024x1024","quality":"medium"}).encode()) + response_size = genparams.get("size", f"{genparams.get('width', 1024)}x{genparams.get('height', 1024)}") + genresp = (json.dumps({"created":int(time.time()),"data":[{"b64_json":gendat}],"background":"opaque","output_format":"png","size":response_size,"quality":"medium"}).encode()) else: genresp = (json.dumps({"images":[gendat],"parameters":{},"info":geninfo,"animated":genanim,"extra_data":gendatextra, "final_frame":genfinalframe}).encode()) self.send_response(200)