images.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. import asyncio
  2. import base64
  3. import json
  4. import logging
  5. import mimetypes
  6. import re
  7. import uuid
  8. from pathlib import Path
  9. from typing import Optional
  10. import requests
  11. from fastapi import Depends, FastAPI, HTTPException, Request, APIRouter
  12. from fastapi.middleware.cors import CORSMiddleware
  13. from pydantic import BaseModel
  14. from open_webui.config import CACHE_DIR
  15. from open_webui.constants import ERROR_MESSAGES
  16. from open_webui.env import ENV, SRC_LOG_LEVELS, ENABLE_FORWARD_USER_INFO_HEADERS
  17. from open_webui.utils.auth import get_admin_user, get_verified_user
  18. from open_webui.utils.images.comfyui import (
  19. ComfyUIGenerateImageForm,
  20. ComfyUIWorkflow,
  21. comfyui_generate_image,
  22. )
  23. log = logging.getLogger(__name__)
  24. log.setLevel(SRC_LOG_LEVELS["IMAGES"])
  25. IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
  26. IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
  27. router = APIRouter()
  28. @router.get("/config")
  29. async def get_config(request: Request, user=Depends(get_admin_user)):
  30. return {
  31. "enabled": request.app.state.config.ENABLE_IMAGE_GENERATION,
  32. "engine": request.app.state.config.IMAGE_GENERATION_ENGINE,
  33. "openai": {
  34. "OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
  35. "OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
  36. },
  37. "automatic1111": {
  38. "AUTOMATIC1111_BASE_URL": request.app.state.config.AUTOMATIC1111_BASE_URL,
  39. "AUTOMATIC1111_API_AUTH": request.app.state.config.AUTOMATIC1111_API_AUTH,
  40. "AUTOMATIC1111_CFG_SCALE": request.app.state.config.AUTOMATIC1111_CFG_SCALE,
  41. "AUTOMATIC1111_SAMPLER": request.app.state.config.AUTOMATIC1111_SAMPLER,
  42. "AUTOMATIC1111_SCHEDULER": request.app.state.config.AUTOMATIC1111_SCHEDULER,
  43. },
  44. "comfyui": {
  45. "COMFYUI_BASE_URL": request.app.state.config.COMFYUI_BASE_URL,
  46. "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW,
  47. "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES,
  48. },
  49. }
  50. class OpenAIConfigForm(BaseModel):
  51. OPENAI_API_BASE_URL: str
  52. OPENAI_API_KEY: str
  53. class Automatic1111ConfigForm(BaseModel):
  54. AUTOMATIC1111_BASE_URL: str
  55. AUTOMATIC1111_API_AUTH: str
  56. AUTOMATIC1111_CFG_SCALE: Optional[str | float | int]
  57. AUTOMATIC1111_SAMPLER: Optional[str]
  58. AUTOMATIC1111_SCHEDULER: Optional[str]
  59. class ComfyUIConfigForm(BaseModel):
  60. COMFYUI_BASE_URL: str
  61. COMFYUI_WORKFLOW: str
  62. COMFYUI_WORKFLOW_NODES: list[dict]
  63. class ConfigForm(BaseModel):
  64. enabled: bool
  65. engine: str
  66. openai: OpenAIConfigForm
  67. automatic1111: Automatic1111ConfigForm
  68. comfyui: ComfyUIConfigForm
  69. @router.post("/config/update")
  70. async def update_config(
  71. request: Request, form_data: ConfigForm, user=Depends(get_admin_user)
  72. ):
  73. request.app.state.config.IMAGE_GENERATION_ENGINE = form_data.engine
  74. request.app.state.config.ENABLE_IMAGE_GENERATION = form_data.enabled
  75. request.app.state.config.IMAGES_OPENAI_API_BASE_URL = (
  76. form_data.openai.OPENAI_API_BASE_URL
  77. )
  78. request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
  79. request.app.state.config.AUTOMATIC1111_BASE_URL = (
  80. form_data.automatic1111.AUTOMATIC1111_BASE_URL
  81. )
  82. request.app.state.config.AUTOMATIC1111_API_AUTH = (
  83. form_data.automatic1111.AUTOMATIC1111_API_AUTH
  84. )
  85. request.app.state.config.AUTOMATIC1111_CFG_SCALE = (
  86. float(form_data.automatic1111.AUTOMATIC1111_CFG_SCALE)
  87. if form_data.automatic1111.AUTOMATIC1111_CFG_SCALE
  88. else None
  89. )
  90. request.app.state.config.AUTOMATIC1111_SAMPLER = (
  91. form_data.automatic1111.AUTOMATIC1111_SAMPLER
  92. if form_data.automatic1111.AUTOMATIC1111_SAMPLER
  93. else None
  94. )
  95. request.app.state.config.AUTOMATIC1111_SCHEDULER = (
  96. form_data.automatic1111.AUTOMATIC1111_SCHEDULER
  97. if form_data.automatic1111.AUTOMATIC1111_SCHEDULER
  98. else None
  99. )
  100. request.app.state.config.COMFYUI_BASE_URL = (
  101. form_data.comfyui.COMFYUI_BASE_URL.strip("/")
  102. )
  103. request.app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
  104. request.app.state.config.COMFYUI_WORKFLOW_NODES = (
  105. form_data.comfyui.COMFYUI_WORKFLOW_NODES
  106. )
  107. return {
  108. "enabled": request.app.state.config.ENABLE_IMAGE_GENERATION,
  109. "engine": request.app.state.config.IMAGE_GENERATION_ENGINE,
  110. "openai": {
  111. "OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
  112. "OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
  113. },
  114. "automatic1111": {
  115. "AUTOMATIC1111_BASE_URL": request.app.state.config.AUTOMATIC1111_BASE_URL,
  116. "AUTOMATIC1111_API_AUTH": request.app.state.config.AUTOMATIC1111_API_AUTH,
  117. "AUTOMATIC1111_CFG_SCALE": request.app.state.config.AUTOMATIC1111_CFG_SCALE,
  118. "AUTOMATIC1111_SAMPLER": request.app.state.config.AUTOMATIC1111_SAMPLER,
  119. "AUTOMATIC1111_SCHEDULER": request.app.state.config.AUTOMATIC1111_SCHEDULER,
  120. },
  121. "comfyui": {
  122. "COMFYUI_BASE_URL": request.app.state.config.COMFYUI_BASE_URL,
  123. "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW,
  124. "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES,
  125. },
  126. }
  127. def get_automatic1111_api_auth(request: Request):
  128. if request.app.state.config.AUTOMATIC1111_API_AUTH is None:
  129. return ""
  130. else:
  131. auth1111_byte_string = request.app.state.config.AUTOMATIC1111_API_AUTH.encode(
  132. "utf-8"
  133. )
  134. auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string)
  135. auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode("utf-8")
  136. return f"Basic {auth1111_base64_encoded_string}"
  137. @router.get("/config/url/verify")
  138. async def verify_url(request: Request, user=Depends(get_admin_user)):
  139. if request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111":
  140. try:
  141. r = requests.get(
  142. url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
  143. headers={"authorization": get_automatic1111_api_auth(request)},
  144. )
  145. r.raise_for_status()
  146. return True
  147. except Exception:
  148. request.app.state.config.ENABLE_IMAGE_GENERATION = False
  149. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
  150. elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
  151. try:
  152. r = requests.get(
  153. url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info"
  154. )
  155. r.raise_for_status()
  156. return True
  157. except Exception:
  158. request.app.state.config.ENABLE_IMAGE_GENERATION = False
  159. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
  160. else:
  161. return True
  162. def set_image_model(request: Request, model: str):
  163. log.info(f"Setting image model to {model}")
  164. request.app.state.config.IMAGE_GENERATION_MODEL = model
  165. if request.app.state.config.IMAGE_GENERATION_ENGINE in ["", "automatic1111"]:
  166. api_auth = get_automatic1111_api_auth()
  167. r = requests.get(
  168. url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
  169. headers={"authorization": api_auth},
  170. )
  171. options = r.json()
  172. if model != options["sd_model_checkpoint"]:
  173. options["sd_model_checkpoint"] = model
  174. r = requests.post(
  175. url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
  176. json=options,
  177. headers={"authorization": api_auth},
  178. )
  179. return request.app.state.config.IMAGE_GENERATION_MODEL
  180. def get_image_model(request):
  181. if request.app.state.config.IMAGE_GENERATION_ENGINE == "openai":
  182. return (
  183. request.app.state.config.IMAGE_GENERATION_MODEL
  184. if request.app.state.config.IMAGE_GENERATION_MODEL
  185. else "dall-e-2"
  186. )
  187. elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
  188. return (
  189. request.app.state.config.IMAGE_GENERATION_MODEL
  190. if request.app.state.config.IMAGE_GENERATION_MODEL
  191. else ""
  192. )
  193. elif (
  194. request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111"
  195. or request.app.state.config.IMAGE_GENERATION_ENGINE == ""
  196. ):
  197. try:
  198. r = requests.get(
  199. url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
  200. headers={"authorization": get_automatic1111_api_auth()},
  201. )
  202. options = r.json()
  203. return options["sd_model_checkpoint"]
  204. except Exception as e:
  205. request.app.state.config.ENABLE_IMAGE_GENERATION = False
  206. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  207. class ImageConfigForm(BaseModel):
  208. MODEL: str
  209. IMAGE_SIZE: str
  210. IMAGE_STEPS: int
  211. @router.get("/image/config")
  212. async def get_image_config(request: Request, user=Depends(get_admin_user)):
  213. return {
  214. "MODEL": request.app.state.config.IMAGE_GENERATION_MODEL,
  215. "IMAGE_SIZE": request.app.state.config.IMAGE_SIZE,
  216. "IMAGE_STEPS": request.app.state.config.IMAGE_STEPS,
  217. }
  218. @router.post("/image/config/update")
  219. async def update_image_config(
  220. request: Request, form_data: ImageConfigForm, user=Depends(get_admin_user)
  221. ):
  222. set_image_model(request, form_data.MODEL)
  223. pattern = r"^\d+x\d+$"
  224. if re.match(pattern, form_data.IMAGE_SIZE):
  225. request.app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
  226. else:
  227. raise HTTPException(
  228. status_code=400,
  229. detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
  230. )
  231. if form_data.IMAGE_STEPS >= 0:
  232. request.app.state.config.IMAGE_STEPS = form_data.IMAGE_STEPS
  233. else:
  234. raise HTTPException(
  235. status_code=400,
  236. detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
  237. )
  238. return {
  239. "MODEL": request.app.state.config.IMAGE_GENERATION_MODEL,
  240. "IMAGE_SIZE": request.app.state.config.IMAGE_SIZE,
  241. "IMAGE_STEPS": request.app.state.config.IMAGE_STEPS,
  242. }
  243. @router.get("/models")
  244. def get_models(request: Request, user=Depends(get_verified_user)):
  245. try:
  246. if request.app.state.config.IMAGE_GENERATION_ENGINE == "openai":
  247. return [
  248. {"id": "dall-e-2", "name": "DALL·E 2"},
  249. {"id": "dall-e-3", "name": "DALL·E 3"},
  250. ]
  251. elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
  252. # TODO - get models from comfyui
  253. r = requests.get(
  254. url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info"
  255. )
  256. info = r.json()
  257. workflow = json.loads(request.app.state.config.COMFYUI_WORKFLOW)
  258. model_node_id = None
  259. for node in request.app.state.config.COMFYUI_WORKFLOW_NODES:
  260. if node["type"] == "model":
  261. if node["node_ids"]:
  262. model_node_id = node["node_ids"][0]
  263. break
  264. if model_node_id:
  265. model_list_key = None
  266. print(workflow[model_node_id]["class_type"])
  267. for key in info[workflow[model_node_id]["class_type"]]["input"][
  268. "required"
  269. ]:
  270. if "_name" in key:
  271. model_list_key = key
  272. break
  273. if model_list_key:
  274. return list(
  275. map(
  276. lambda model: {"id": model, "name": model},
  277. info[workflow[model_node_id]["class_type"]]["input"][
  278. "required"
  279. ][model_list_key][0],
  280. )
  281. )
  282. else:
  283. return list(
  284. map(
  285. lambda model: {"id": model, "name": model},
  286. info["CheckpointLoaderSimple"]["input"]["required"][
  287. "ckpt_name"
  288. ][0],
  289. )
  290. )
  291. elif (
  292. request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111"
  293. or request.app.state.config.IMAGE_GENERATION_ENGINE == ""
  294. ):
  295. r = requests.get(
  296. url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models",
  297. headers={"authorization": get_automatic1111_api_auth()},
  298. )
  299. models = r.json()
  300. return list(
  301. map(
  302. lambda model: {"id": model["title"], "name": model["model_name"]},
  303. models,
  304. )
  305. )
  306. except Exception as e:
  307. request.app.state.config.ENABLE_IMAGE_GENERATION = False
  308. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  309. class GenerateImageForm(BaseModel):
  310. model: Optional[str] = None
  311. prompt: str
  312. size: Optional[str] = None
  313. n: int = 1
  314. negative_prompt: Optional[str] = None
  315. def save_b64_image(b64_str):
  316. try:
  317. image_id = str(uuid.uuid4())
  318. if "," in b64_str:
  319. header, encoded = b64_str.split(",", 1)
  320. mime_type = header.split(";")[0]
  321. img_data = base64.b64decode(encoded)
  322. image_format = mimetypes.guess_extension(mime_type)
  323. image_filename = f"{image_id}{image_format}"
  324. file_path = IMAGE_CACHE_DIR / f"{image_filename}"
  325. with open(file_path, "wb") as f:
  326. f.write(img_data)
  327. return image_filename
  328. else:
  329. image_filename = f"{image_id}.png"
  330. file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
  331. img_data = base64.b64decode(b64_str)
  332. # Write the image data to a file
  333. with open(file_path, "wb") as f:
  334. f.write(img_data)
  335. return image_filename
  336. except Exception as e:
  337. log.exception(f"Error saving image: {e}")
  338. return None
  339. def save_url_image(url):
  340. image_id = str(uuid.uuid4())
  341. try:
  342. r = requests.get(url)
  343. r.raise_for_status()
  344. if r.headers["content-type"].split("/")[0] == "image":
  345. mime_type = r.headers["content-type"]
  346. image_format = mimetypes.guess_extension(mime_type)
  347. if not image_format:
  348. raise ValueError("Could not determine image type from MIME type")
  349. image_filename = f"{image_id}{image_format}"
  350. file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
  351. with open(file_path, "wb") as image_file:
  352. for chunk in r.iter_content(chunk_size=8192):
  353. image_file.write(chunk)
  354. return image_filename
  355. else:
  356. log.error("Url does not point to an image.")
  357. return None
  358. except Exception as e:
  359. log.exception(f"Error saving image: {e}")
  360. return None
  361. @router.post("/generations")
  362. async def image_generations(
  363. request: Request,
  364. form_data: GenerateImageForm,
  365. user=Depends(get_verified_user),
  366. ):
  367. width, height = tuple(map(int, request.app.state.config.IMAGE_SIZE.split("x")))
  368. r = None
  369. try:
  370. if request.app.state.config.IMAGE_GENERATION_ENGINE == "openai":
  371. headers = {}
  372. headers["Authorization"] = (
  373. f"Bearer {request.app.state.config.IMAGES_OPENAI_API_KEY}"
  374. )
  375. headers["Content-Type"] = "application/json"
  376. if ENABLE_FORWARD_USER_INFO_HEADERS:
  377. headers["X-OpenWebUI-User-Name"] = user.name
  378. headers["X-OpenWebUI-User-Id"] = user.id
  379. headers["X-OpenWebUI-User-Email"] = user.email
  380. headers["X-OpenWebUI-User-Role"] = user.role
  381. data = {
  382. "model": (
  383. request.app.state.config.IMAGE_GENERATION_MODEL
  384. if request.app.state.config.IMAGE_GENERATION_MODEL != ""
  385. else "dall-e-2"
  386. ),
  387. "prompt": form_data.prompt,
  388. "n": form_data.n,
  389. "size": (
  390. form_data.size
  391. if form_data.size
  392. else request.app.state.config.IMAGE_SIZE
  393. ),
  394. "response_format": "b64_json",
  395. }
  396. # Use asyncio.to_thread for the requests.post call
  397. r = await asyncio.to_thread(
  398. requests.post,
  399. url=f"{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations",
  400. json=data,
  401. headers=headers,
  402. )
  403. r.raise_for_status()
  404. res = r.json()
  405. images = []
  406. for image in res["data"]:
  407. image_filename = save_b64_image(image["b64_json"])
  408. images.append({"url": f"/cache/image/generations/{image_filename}"})
  409. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
  410. with open(file_body_path, "w") as f:
  411. json.dump(data, f)
  412. return images
  413. elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
  414. data = {
  415. "prompt": form_data.prompt,
  416. "width": width,
  417. "height": height,
  418. "n": form_data.n,
  419. }
  420. if request.app.state.config.IMAGE_STEPS is not None:
  421. data["steps"] = request.app.state.config.IMAGE_STEPS
  422. if form_data.negative_prompt is not None:
  423. data["negative_prompt"] = form_data.negative_prompt
  424. form_data = ComfyUIGenerateImageForm(
  425. **{
  426. "workflow": ComfyUIWorkflow(
  427. **{
  428. "workflow": request.app.state.config.COMFYUI_WORKFLOW,
  429. "nodes": request.app.state.config.COMFYUI_WORKFLOW_NODES,
  430. }
  431. ),
  432. **data,
  433. }
  434. )
  435. res = await comfyui_generate_image(
  436. request.app.state.config.IMAGE_GENERATION_MODEL,
  437. form_data,
  438. user.id,
  439. request.app.state.config.COMFYUI_BASE_URL,
  440. )
  441. log.debug(f"res: {res}")
  442. images = []
  443. for image in res["data"]:
  444. image_filename = save_url_image(image["url"])
  445. images.append({"url": f"/cache/image/generations/{image_filename}"})
  446. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
  447. with open(file_body_path, "w") as f:
  448. json.dump(form_data.model_dump(exclude_none=True), f)
  449. log.debug(f"images: {images}")
  450. return images
  451. elif (
  452. request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111"
  453. or request.app.state.config.IMAGE_GENERATION_ENGINE == ""
  454. ):
  455. if form_data.model:
  456. set_image_model(form_data.model)
  457. data = {
  458. "prompt": form_data.prompt,
  459. "batch_size": form_data.n,
  460. "width": width,
  461. "height": height,
  462. }
  463. if request.app.state.config.IMAGE_STEPS is not None:
  464. data["steps"] = request.app.state.config.IMAGE_STEPS
  465. if form_data.negative_prompt is not None:
  466. data["negative_prompt"] = form_data.negative_prompt
  467. if request.app.state.config.AUTOMATIC1111_CFG_SCALE:
  468. data["cfg_scale"] = request.app.state.config.AUTOMATIC1111_CFG_SCALE
  469. if request.app.state.config.AUTOMATIC1111_SAMPLER:
  470. data["sampler_name"] = request.app.state.config.AUTOMATIC1111_SAMPLER
  471. if request.app.state.config.AUTOMATIC1111_SCHEDULER:
  472. data["scheduler"] = request.app.state.config.AUTOMATIC1111_SCHEDULER
  473. # Use asyncio.to_thread for the requests.post call
  474. r = await asyncio.to_thread(
  475. requests.post,
  476. url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
  477. json=data,
  478. headers={"authorization": get_automatic1111_api_auth()},
  479. )
  480. res = r.json()
  481. log.debug(f"res: {res}")
  482. images = []
  483. for image in res["images"]:
  484. image_filename = save_b64_image(image)
  485. images.append({"url": f"/cache/image/generations/{image_filename}"})
  486. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
  487. with open(file_body_path, "w") as f:
  488. json.dump({**data, "info": res["info"]}, f)
  489. return images
  490. except Exception as e:
  491. error = e
  492. if r != None:
  493. data = r.json()
  494. if "error" in data:
  495. error = data["error"]["message"]
  496. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))