mirror of
https://github.com/vladmandic/automatic
synced 2026-09-18 16:54:33 +02:00
+4
-3
@@ -53,9 +53,6 @@ class Api:
|
||||
self.add_api_route("/sdapi/v1/skip", server.post_skip, methods=["POST"])
|
||||
self.add_api_route("/sdapi/v1/shutdown", server.post_shutdown, methods=["POST"])
|
||||
self.add_api_route("/sdapi/v1/memory", server.get_memory, methods=["GET"], response_model=models.ResMemory)
|
||||
self.add_api_route("/sdapi/v1/options", server.get_config, methods=["GET"], response_model=models.OptionsModel)
|
||||
self.add_api_route("/sdapi/v1/options", server.set_config, methods=["POST"])
|
||||
self.add_api_route("/sdapi/v1/options-info", server.get_options_info, methods=["GET"], tags=["Server"])
|
||||
self.add_api_route("/sdapi/v1/cmd-flags", server.get_cmd_flags, methods=["GET"], response_model=models.FlagsModel)
|
||||
self.add_api_route("/sdapi/v1/gpu", gpu.get_gpu_status, methods=["GET"], response_model=list[models.ResGPU])
|
||||
|
||||
@@ -103,6 +100,10 @@ class Api:
|
||||
self.add_api_route("/sdapi/v1/modules", endpoints.get_modules, methods=["GET"], tags=["Functional"])
|
||||
self.add_api_route("/sdapi/v1/sampler", endpoints.get_sampler, methods=["GET"], response_model=dict, tags=["Functional"])
|
||||
|
||||
# options api
|
||||
from modules.api import options
|
||||
options.register_api(self.app)
|
||||
|
||||
# caption api
|
||||
from modules.api import caption
|
||||
caption.register_api()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from modules.logger import log
|
||||
from modules import shared
|
||||
from modules.api import models, helpers
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from fastapi.exceptions import HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from modules.logger import log
|
||||
import modules.errors as errors
|
||||
|
||||
from modules.api.validate import validate_request
|
||||
|
||||
errors.install()
|
||||
ignore_endpoints = [
|
||||
@@ -43,24 +43,23 @@ def setup_middleware(app: FastAPI, cmd_opts):
|
||||
|
||||
@app.middleware("http")
|
||||
async def api_preprocess(req: Request, call_next):
|
||||
log.critical(f'HERE SCOPE: {req.scope}')
|
||||
log.critical(f'HERE client: {req.client}')
|
||||
try:
|
||||
ts = time.time()
|
||||
res: Response = await call_next(req)
|
||||
duration = str(round(time.time() - ts, 4))
|
||||
res.headers["X-Process-Time"] = duration
|
||||
endpoint = req.scope.get('path', 'err')
|
||||
client = req.scope.get('client', ('0:0.0.0', 0))[0]
|
||||
token = req.cookies.get("access-token") or req.cookies.get("access-token-unsecure")
|
||||
validate_request(client, endpoint)
|
||||
if (cmd_opts.api_log) and endpoint.startswith('/sdapi'):
|
||||
if any([endpoint.startswith(x) for x in ignore_endpoints]): # noqa C419 # pylint: disable=use-a-generator
|
||||
return res
|
||||
log.info('API user={user} code={code} {prot}/{ver} {method} {endpoint} {cli} {host} {duration}'.format( # pylint: disable=consider-using-f-string, logging-format-interpolation
|
||||
log.info('API user={user} code={code} {prot}/{ver} {method} {endpoint} {client} {duration}'.format( # pylint: disable=consider-using-f-string, logging-format-interpolation
|
||||
user = app.tokens.get(token) if hasattr(app, 'tokens') else None,
|
||||
code = res.status_code,
|
||||
ver = req.scope.get('http_version', '0.0'),
|
||||
cli = req.scope.get('client', ('0:0.0.0', 0))[0],
|
||||
host = req.client.host,
|
||||
client = client,
|
||||
prot = req.scope.get('scheme', 'err'),
|
||||
method = req.scope.get('method', 'err'),
|
||||
endpoint = endpoint,
|
||||
@@ -84,6 +83,8 @@ def setup_middleware(app: FastAPI, cmd_opts):
|
||||
return JSONResponse(status_code=err['code'], content=jsonable_encoder(err))
|
||||
if err['code'] == 404 and 'file=html/' in req.url.path: # dont spam with locales
|
||||
return JSONResponse(status_code=err['code'], content=jsonable_encoder(err))
|
||||
if err["code"] == 429: # dont spam with rate limit errors
|
||||
return JSONResponse(status_code=err["code"], content=jsonable_encoder(err))
|
||||
|
||||
if not any([req.url.path.endswith(x) for x in ignore_endpoints]): # noqa C419 # pylint: disable=use-a-generator
|
||||
log.error(f"API error: {req.method}: {req.url} {err}")
|
||||
|
||||
@@ -558,9 +558,9 @@ def create_model_from_signature(func: Callable, model_name: str, base_model: typ
|
||||
extra = 'allow' if varkw else 'ignore'
|
||||
config = CustomConfig
|
||||
if base_model == BaseModel:
|
||||
create_model_args = {'__config__': config}
|
||||
create_model_args = {'__config__': config}
|
||||
else:
|
||||
create_model_args = {'__base__': base_model}
|
||||
create_model_args = {'__base__': base_model}
|
||||
|
||||
model = create_model(
|
||||
model_name,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from typing import Any
|
||||
from modules.shared import opts
|
||||
|
||||
|
||||
def get_options():
|
||||
options = {}
|
||||
for k in opts.data.keys():
|
||||
if opts.data_labels.get(k) is not None:
|
||||
options.update({k: opts.data.get(k, opts.data_labels.get(k).default)})
|
||||
else:
|
||||
options.update({k: opts.data.get(k, None)})
|
||||
if "sd_lyco" in options:
|
||||
del options["sd_lyco"]
|
||||
if "sd_lora" in options:
|
||||
del options["sd_lora"]
|
||||
return options
|
||||
|
||||
def set_options(req: dict[str, Any]):
|
||||
updated = []
|
||||
for k, v in req.items():
|
||||
updated.append({k: opts.set(k, v)})
|
||||
opts.save()
|
||||
return {"updated": updated}
|
||||
|
||||
def get_options_info():
|
||||
"""
|
||||
Return metadata for all application settings.
|
||||
Returns every registered option with its label, section, type, default value,
|
||||
component kind (slider, switch, dropdown, etc.), and component args (min/max/step/choices).
|
||||
Used by alternative UIs to dynamically build a settings editor.
|
||||
"""
|
||||
import re
|
||||
import gradio as gr
|
||||
from modules.shared_legacy import LegacyOption
|
||||
from modules.ui_components import DropdownEditable
|
||||
component_map = {
|
||||
gr.Slider: "slider",
|
||||
gr.Checkbox: "switch",
|
||||
gr.Radio: "radio",
|
||||
gr.Dropdown: "dropdown",
|
||||
gr.Textbox: "input",
|
||||
gr.Number: "number",
|
||||
gr.ColorPicker: "color",
|
||||
gr.CheckboxGroup: "checkboxgroup",
|
||||
gr.HTML: "separator",
|
||||
}
|
||||
options_info = {}
|
||||
sections_seen = {}
|
||||
for key, info in opts.data_labels.items():
|
||||
section_id = info.section[0] if info.section else None
|
||||
section_title = info.section[1] if info.section and len(info.section) > 1 else ""
|
||||
hidden = section_id is None or "hidden" in (section_id or "").lower() or "hidden" in section_title.lower()
|
||||
if section_id and section_id not in sections_seen:
|
||||
sections_seen[section_id] = {"id": section_id, "title": section_title, "hidden": hidden}
|
||||
if hidden:
|
||||
args = {}
|
||||
else:
|
||||
try:
|
||||
args = info.component_args() if callable(info.component_args) else (info.component_args or {})
|
||||
except Exception:
|
||||
args = {}
|
||||
comp_name = component_map.get(info.component, "input")
|
||||
if info.component is DropdownEditable:
|
||||
comp_name = "dropdown"
|
||||
elif info.component is None:
|
||||
comp_name = "switch" if isinstance(info.default, bool) else "number" if isinstance(info.default, (int, float)) else "input"
|
||||
visible = args.get("visible", True) and (comp_name == "separator" or len(info.label) > 2)
|
||||
serializable_args = {}
|
||||
for arg_key in ("minimum", "maximum", "step", "choices", "precision", "multiselect"):
|
||||
if arg_key in args:
|
||||
serializable_args[arg_key] = args[arg_key]
|
||||
label = info.label
|
||||
if comp_name == "separator" and not label and isinstance(info.default, str):
|
||||
label = re.sub(r"<[^>]+>", "", info.default).strip()
|
||||
options_info[key] = {
|
||||
"label": label,
|
||||
"section_id": section_id,
|
||||
"section_title": section_title,
|
||||
"visible": visible,
|
||||
"hidden": hidden,
|
||||
"type": "boolean" if isinstance(info.default, bool) else "number" if isinstance(info.default, (int, float)) else "array" if isinstance(info.default, list) else "string",
|
||||
"component": comp_name,
|
||||
"component_args": serializable_args,
|
||||
"default": info.default,
|
||||
"is_legacy": isinstance(info, LegacyOption),
|
||||
"is_secret": getattr(info, "secret", False),
|
||||
}
|
||||
return {"options": options_info, "sections": list(sections_seen.values())}
|
||||
|
||||
|
||||
def register_api(app):
|
||||
app.add_api_route("/sdapi/v1/options", get_options, methods=["GET"], response_model=dict, tags=["Server"])
|
||||
app.add_api_route("/sdapi/v1/options", set_options, methods=["POST"], tags=["Server"])
|
||||
app.add_api_route("/sdapi/v1/options-info", get_options_info, methods=["GET"], tags=["Server"])
|
||||
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
from fastapi import Request, Depends
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
@@ -86,27 +85,6 @@ def post_log(req: models.ReqPostLog):
|
||||
log.error(f'UI: {req.error}')
|
||||
return {}
|
||||
|
||||
|
||||
def get_config():
|
||||
options = {}
|
||||
for k in shared.opts.data.keys():
|
||||
if shared.opts.data_labels.get(k) is not None:
|
||||
options.update({k: shared.opts.data.get(k, shared.opts.data_labels.get(k).default)})
|
||||
else:
|
||||
options.update({k: shared.opts.data.get(k, None)})
|
||||
if 'sd_lyco' in options:
|
||||
del options['sd_lyco']
|
||||
if 'sd_lora' in options:
|
||||
del options['sd_lora']
|
||||
return options
|
||||
|
||||
def set_config(req: dict[str, Any]):
|
||||
updated = []
|
||||
for k, v in req.items():
|
||||
updated.append({ k: shared.opts.set(k, v) })
|
||||
shared.opts.save()
|
||||
return { "updated": updated }
|
||||
|
||||
def get_cmd_flags():
|
||||
return vars(shared.cmd_opts)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import re
|
||||
import limits
|
||||
from fastapi.exceptions import HTTPException
|
||||
from modules.logger import log
|
||||
|
||||
|
||||
requests_summary = {}
|
||||
request_cost = { # value is cost, 0=not rate limited, 1=default, >1 more expensive
|
||||
"/file": 0,
|
||||
"/run/predict": 0,
|
||||
"/sdapi/v1/browser/thumb": 0,
|
||||
"/sdapi/v1/network/thumb": 0,
|
||||
"/sdapi/v1/txt2img": 5,
|
||||
"/sdapi/v1/img2img": 5,
|
||||
"/sdapi/v1/control": 5,
|
||||
}
|
||||
backend = limits.storage.MemoryStorage()
|
||||
strategy = limits.strategies.SlidingWindowCounterRateLimiter(backend)
|
||||
limiter = limits.parse("300/minute")
|
||||
|
||||
|
||||
def get_stats():
|
||||
for k, v in requests_summary.items():
|
||||
if v > 1:
|
||||
log.trace(f'API stats: {k}={v}')
|
||||
|
||||
|
||||
def rate_limit(key):
|
||||
cost = request_cost.get(key, 1)
|
||||
if not strategy.hit(limiter, key, cost=cost):
|
||||
log.warning(f'API: key={key} rate limit exceeded')
|
||||
raise HTTPException(status_code=429, detail=f'{key}: rate limit exceeded')
|
||||
|
||||
|
||||
def validate_request(client, endpoint):
|
||||
api = re.match(r"^[^?#&=]+", endpoint).group(0)
|
||||
key = f"{client}:{api}"
|
||||
if key not in requests_summary:
|
||||
requests_summary[key] = 0
|
||||
requests_summary[key] += 1
|
||||
rate_limit(key)
|
||||
Reference in New Issue
Block a user