Spaces:
Running
Running
| import json | |
| import os | |
| import time | |
| import uuid | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Any | |
| import httpx | |
| import uvicorn | |
| from fastapi import FastAPI, Request | |
| from fastapi.responses import JSONResponse, Response, StreamingResponse | |
| from starlette.background import BackgroundTask | |
| UPSTREAM_URL = os.environ.get("LITELLM_UPSTREAM_URL", "http://127.0.0.1:7861").rstrip("/") | |
| MODEL_CATALOG_PATH = Path(os.environ.get("MODEL_CATALOG_PATH", "/app/config/model-catalog.json")) | |
| USABLE_MODELS_PATH = Path(os.environ.get("USABLE_MODELS_PATH", "/app/config/usable-models.json")) | |
| GENLABS_BASE_URL = os.environ.get("GENLABS_API_BASE", "https://api.genlabs.dev/deca/v1").rstrip("/") | |
| GENLABS_MODELS = ( | |
| "genlabs/deca-2.5-mini", | |
| "genlabs/deca-2.5-pro", | |
| "genlabs/deca-2.5-ultra", | |
| ) | |
| NONSTREAM_UPSTREAM_MODELS = { | |
| model.strip() | |
| for model in os.environ.get("NONSTREAM_UPSTREAM_MODELS", "voidai/gpt-oss-120b").split(",") | |
| if model.strip() | |
| } | |
| CLOUDFLARE_IMAGE_MODELS = { | |
| model.strip() | |
| for model in os.environ.get( | |
| "CLOUDFLARE_IMAGE_MODELS", | |
| ",".join( | |
| [ | |
| "cloudflare/@cf/black-forest-labs/flux-1-schnell", | |
| "cloudflare/@cf/black-forest-labs/flux-2-dev", | |
| "cloudflare/@cf/black-forest-labs/flux-2-klein-4b", | |
| "cloudflare/@cf/black-forest-labs/flux-2-klein-9b", | |
| "cloudflare/@cf/bytedance/stable-diffusion-xl-lightning", | |
| "cloudflare/@cf/leonardo/lucid-origin", | |
| "cloudflare/@cf/leonardo/phoenix-1.0", | |
| "cloudflare/@cf/lykon/dreamshaper-8-lcm", | |
| "cloudflare/@cf/runwayml/stable-diffusion-v1-5-img2img", | |
| "cloudflare/@cf/runwayml/stable-diffusion-v1-5-inpainting", | |
| "cloudflare/@cf/stabilityai/stable-diffusion-xl-base-1.0", | |
| ] | |
| ), | |
| ).split(",") | |
| if model.strip() | |
| } | |
| CLOUDFLARE_IMAGE_MAX_N = max(1, int(os.environ.get("CLOUDFLARE_IMAGE_MAX_N", "1"))) | |
| POLLINATIONS_IMAGE_MODELS = { | |
| model.strip() | |
| for model in os.environ.get( | |
| "POLLINATIONS_IMAGE_MODELS", | |
| "pollinations/flux,pollinations/zimage,pollinations/gptimage", | |
| ).split(",") | |
| if model.strip() | |
| } | |
| MODELSLAB_IMAGE_MODELS = { | |
| "modelslab/midjourney", | |
| "modelslab/anything-v3", | |
| "modelslab/wifu-diffusion", | |
| "modelslab/arcane-diffusion", | |
| } | |
| HOP_BY_HOP_HEADERS = { | |
| "connection", | |
| "content-length", | |
| "host", | |
| "keep-alive", | |
| "proxy-authenticate", | |
| "proxy-authorization", | |
| "te", | |
| "trailer", | |
| "transfer-encoding", | |
| "upgrade", | |
| } | |
| app = FastAPI() | |
| client = httpx.AsyncClient(timeout=httpx.Timeout(300.0, connect=30.0)) | |
| catalog_metadata_cache: dict[str, dict[str, Any]] | None = None | |
| usable_metadata_cache: dict[str, dict[str, Any]] | None = None | |
| async def startup_event(): | |
| """Skip re-rendering; start-litellm.sh already wrote a static config.""" | |
| pass | |
| async def shutdown_event() -> None: | |
| await client.aclose() | |
| _render_config_done = False | |
| def _try_render_config(): | |
| """Try to render litellm config. Called on startup and first request.""" | |
| global _render_config_done | |
| if _render_config_done: | |
| return | |
| import subprocess | |
| config_template = os.environ.get("LITELLM_CONFIG_TEMPLATE", "/app/config/config.yaml") | |
| config_rendered = os.environ.get("LITELLM_RENDERED_CONFIG", "/tmp/litellm-config.yaml") | |
| try: | |
| result = subprocess.run( | |
| ["python", "/app/scripts/render-config.py", config_template, config_rendered, "--include-legacy-aliases"], | |
| timeout=30, | |
| capture_output=True, | |
| text=True, | |
| ) | |
| if result.returncode == 0: | |
| _render_config_done = True | |
| else: | |
| pass # Will retry on next request | |
| except Exception: | |
| pass # Will retry on next request | |
| def clean_headers(headers: Any) -> dict[str, str]: | |
| return { | |
| key: value | |
| for key, value in dict(headers).items() | |
| if key.lower() not in HOP_BY_HOP_HEADERS | |
| } | |
| def model_looks_free(model_id: str, name: str = "") -> bool: | |
| text = f"{model_id} {name}".lower() | |
| return ( | |
| model_id.lower().endswith(":free") | |
| or ":free" in model_id.lower() | |
| or " free" in text | |
| or text.startswith("free ") | |
| or text.endswith("(free)") | |
| ) | |
| def pricing_is_free(pricing: Any) -> bool: | |
| if not isinstance(pricing, dict) or not pricing: | |
| return False | |
| numeric_prices = [] | |
| for value in pricing.values(): | |
| try: | |
| numeric_prices.append(float(value)) | |
| except (TypeError, ValueError): | |
| pass | |
| return bool(numeric_prices) and all(price == 0 for price in numeric_prices) | |
| def capability_from_mode(mode: str | None) -> list[str]: | |
| if not mode: | |
| return [] | |
| normalized = mode.strip().lower().replace("-", "_") | |
| if normalized in {"image", "image_generation", "text_to_image"}: | |
| return ["image"] | |
| if normalized in {"audio_transcription", "transcription"}: | |
| return ["audio", "transcription"] | |
| if normalized in {"audio_speech", "speech", "text_to_speech"}: | |
| return ["audio", "speech"] | |
| if normalized in {"embedding", "embeddings"}: | |
| return ["embedding"] | |
| if normalized in {"rerank", "reranking"}: | |
| return ["rerank"] | |
| if normalized in {"chat", "completion", "responses"}: | |
| return ["text"] | |
| return [] | |
| def unique_strings(values: list[Any]) -> list[str]: | |
| seen: set[str] = set() | |
| out: list[str] = [] | |
| for value in values: | |
| if not isinstance(value, str): | |
| continue | |
| label = value.strip().lower().replace("-", "_") | |
| if not label or label in seen: | |
| continue | |
| seen.add(label) | |
| out.append(label) | |
| return out | |
| def suffix_parts(suffix: Any) -> tuple[str, str, dict[str, Any]]: | |
| if isinstance(suffix, dict): | |
| alias = str(suffix.get("alias") or suffix.get("id") or suffix.get("model") or "") | |
| model = str(suffix.get("model") or alias) | |
| return alias, model, suffix | |
| value = str(suffix) | |
| return value, value, {} | |
| def explicit_bool(value: Any) -> bool | None: | |
| if isinstance(value, bool): | |
| return value | |
| if isinstance(value, str): | |
| lowered = value.strip().lower() | |
| if lowered in {"true", "yes", "y", "1", "free"}: | |
| return True | |
| if lowered in {"false", "no", "n", "0", "paid"}: | |
| return False | |
| return None | |
| def catalog_entry_metadata(group: dict[str, Any], suffix: Any) -> tuple[str, dict[str, Any]] | None: | |
| alias_prefix = str(group.get("alias_prefix") or "").strip() | |
| model_prefix = str(group.get("model_prefix") or "").strip() | |
| alias_suffix, model_suffix, suffix_meta = suffix_parts(suffix) | |
| if not alias_prefix or not alias_suffix: | |
| return None | |
| model_id = f"{alias_prefix}/{alias_suffix}" | |
| upstream_model = f"{model_prefix}/{model_suffix}" if model_prefix else model_suffix | |
| group_info = group.get("model_info") if isinstance(group.get("model_info"), dict) else {} | |
| suffix_info = suffix_meta.get("model_info") if isinstance(suffix_meta.get("model_info"), dict) else {} | |
| model_info = {**group_info, **suffix_info} | |
| mode = ( | |
| suffix_meta.get("mode") | |
| or group.get("mode") | |
| or model_info.get("mode") | |
| or suffix_meta.get("task") | |
| or group.get("task") | |
| ) | |
| mode = str(mode).strip() if mode else None | |
| raw_capabilities = [] | |
| for source in (group, suffix_meta, model_info): | |
| capabilities = source.get("capabilities") if isinstance(source, dict) else None | |
| if isinstance(capabilities, list): | |
| raw_capabilities.extend(capabilities) | |
| elif isinstance(capabilities, dict): | |
| raw_capabilities.extend( | |
| key for key, enabled in capabilities.items() if enabled | |
| ) | |
| pricing = suffix_meta.get("pricing") or group.get("pricing") or model_info.get("pricing") | |
| free_value = ( | |
| explicit_bool(suffix_meta.get("free")) | |
| if "free" in suffix_meta | |
| else explicit_bool(suffix_meta.get("is_free")) | |
| ) | |
| if free_value is None: | |
| free_value = ( | |
| explicit_bool(group.get("free")) | |
| if "free" in group | |
| else explicit_bool(group.get("is_free")) | |
| ) | |
| if free_value is None: | |
| free_value = pricing_is_free(pricing) or model_looks_free(model_id, str(model_info.get("name") or "")) | |
| capabilities = unique_strings([*capability_from_mode(mode), *raw_capabilities]) | |
| provider = str(group.get("provider") or alias_prefix).strip() | |
| metadata: dict[str, Any] = { | |
| "id": model_id, | |
| "provider": provider, | |
| "source_model": upstream_model, | |
| "free": bool(free_value), | |
| "is_free": bool(free_value), | |
| "capabilities": capabilities, | |
| "catalog_source": "config/model-catalog.json", | |
| } | |
| if mode: | |
| metadata["task"] = mode | |
| metadata["mode"] = mode | |
| if isinstance(pricing, dict): | |
| metadata["pricing"] = pricing | |
| if model_info: | |
| metadata["model_info"] = { | |
| **model_info, | |
| "mode": mode or model_info.get("mode"), | |
| "capabilities": capabilities or model_info.get("capabilities", []), | |
| "free": bool(free_value), | |
| "is_free": bool(free_value), | |
| } | |
| else: | |
| metadata["model_info"] = { | |
| "mode": mode, | |
| "capabilities": capabilities, | |
| "free": bool(free_value), | |
| "is_free": bool(free_value), | |
| } | |
| return model_id, metadata | |
| def merge_catalog_metadata(existing: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]: | |
| merged = dict(existing) | |
| for key, value in incoming.items(): | |
| if key == "model_info": | |
| current_info = merged.get("model_info") | |
| merged_info = dict(current_info) if isinstance(current_info, dict) else {} | |
| if isinstance(value, dict): | |
| for info_key, info_value in value.items(): | |
| current_value = merged_info.get(info_key) | |
| if current_value in (None, [], {}) and info_value not in (None, [], {}): | |
| merged_info[info_key] = info_value | |
| elif ( | |
| info_key == "capabilities" | |
| and isinstance(info_value, list) | |
| and isinstance(current_value, list) | |
| and len(info_value) > len(current_value) | |
| ): | |
| merged_info[info_key] = info_value | |
| if merged_info: | |
| merged["model_info"] = merged_info | |
| continue | |
| current = merged.get(key) | |
| if current in (None, [], {}) and value not in (None, [], {}): | |
| merged[key] = value | |
| elif ( | |
| key == "capabilities" | |
| and isinstance(value, list) | |
| and isinstance(current, list) | |
| and len(value) > len(current) | |
| ): | |
| merged[key] = value | |
| return merged | |
| def load_catalog_metadata() -> dict[str, dict[str, Any]]: | |
| """Return empty metadata — model-catalog.json is huge (3579 models) and | |
| parsing it on cpu-basic hangs. render-config already filtered the config.""" | |
| global catalog_metadata_cache | |
| if catalog_metadata_cache is not None: | |
| return catalog_metadata_cache | |
| catalog_metadata_cache = {} | |
| return catalog_metadata_cache | |
| def _load_probe_file(path_or_url: str, local_path: Path) -> dict | None: | |
| import urllib.request | |
| payload = None | |
| try: | |
| resp = urllib.request.urlopen(path_or_url, timeout=10) | |
| payload = json.loads(resp.read()) | |
| except Exception: | |
| pass | |
| if payload is None: | |
| try: | |
| payload = json.loads(local_path.read_text(encoding="utf-8")) | |
| except (OSError, json.JSONDecodeError): | |
| pass | |
| return payload if isinstance(payload, dict) else None | |
| def _merge_probe(metadata: dict, payload: dict, flag: str, source: str) -> None: | |
| checked_at = payload.get("checked_at") or payload.get("image_checked_at") or payload.get("vision_checked_at") | |
| for model_id in payload.get("usable_model_ids", []) or payload.get("image_usable_model_ids", []) or payload.get("vision_usable_model_ids", []): | |
| if not isinstance(model_id, str) or not model_id.strip(): | |
| continue | |
| item = metadata.setdefault(model_id.strip(), {}) | |
| item[flag] = True | |
| item[f"verified_{flag}"] = True | |
| item[f"{flag}_checked_at"] = checked_at | |
| item[f"{flag}_source"] = source | |
| if flag == "chat_usable": | |
| item["usable"] = True | |
| for model in payload.get("models", []) or payload.get("image_models", []) or payload.get("vision_models", []): | |
| if not isinstance(model, dict): | |
| continue | |
| model_id = model.get("id") | |
| if not isinstance(model_id, str) or not model_id.strip(): | |
| continue | |
| key = model_id.strip() | |
| item = metadata.setdefault(key, {flag: True, f"verified_{flag}": True, f"{flag}_checked_at": checked_at, f"{flag}_source": source}) | |
| item[flag] = True | |
| item[f"{flag}_latency_ms"] = model.get("latency_ms") | |
| item[f"{flag}_http_status"] = model.get("http_status") | |
| if flag == "chat_usable": | |
| item["usable"] = True | |
| def load_usable_metadata() -> dict[str, dict[str, Any]]: | |
| global usable_metadata_cache | |
| metadata: dict[str, dict[str, Any]] = {} | |
| chat_dataset_url = os.environ.get( | |
| "USABLE_MODELS_DATASET_URL", | |
| "https://huggingface.co/datasets/alchoholpad/litellm-usable-models/resolve/main/usable-models.json", | |
| ) | |
| chat_payload = _load_probe_file(chat_dataset_url, USABLE_MODELS_PATH) | |
| if chat_payload: | |
| _merge_probe(metadata, chat_payload, "chat_usable", "config/usable-models.json") | |
| image_payload = _load_probe_file( | |
| "https://huggingface.co/datasets/alchoholpad/litellm-usable-models/resolve/main/usable-image-models.json", | |
| Path("/app/config/usable-image-models.json"), | |
| ) | |
| if image_payload: | |
| _merge_probe(metadata, image_payload, "image_usable", "config/usable-image-models.json") | |
| vision_payload = _load_probe_file( | |
| "https://huggingface.co/datasets/alchoholpad/litellm-usable-models/resolve/main/usable-vision-models.json", | |
| Path("/app/config/usable-vision-models.json"), | |
| ) | |
| if vision_payload: | |
| _merge_probe(metadata, vision_payload, "vision_usable", "config/usable-vision-models.json") | |
| usable_metadata_cache = metadata | |
| return metadata | |
| def model_metadata(model_id: str) -> dict[str, Any]: | |
| return { | |
| **load_catalog_metadata().get(model_id, {}), | |
| **load_usable_metadata().get(model_id, {}), | |
| } | |
| def runtime_extra_model_ids() -> set[str]: | |
| model_ids: set[str] = set() | |
| if genlabs_api_key(): | |
| model_ids.update(GENLABS_MODELS) | |
| if cloudflare_api_token() and cloudflare_account_id(): | |
| model_ids.update(CLOUDFLARE_IMAGE_MODELS) | |
| if pollinations_api_key(): | |
| model_ids.update(POLLINATIONS_IMAGE_MODELS) | |
| if stablehorde_api_key(): | |
| model_ids.update(STABLEHORDE_MODELS) | |
| return model_ids | |
| def merge_model_metadata(raw_model: dict[str, Any], metadata: dict[str, Any]) -> dict[str, Any]: | |
| enriched = dict(raw_model) | |
| for key in ( | |
| "provider", | |
| "free", | |
| "is_free", | |
| "pricing", | |
| "task", | |
| "mode", | |
| "capabilities", | |
| "catalog_source", | |
| "usable", | |
| "verified_usable", | |
| "usable_checked_at", | |
| "usable_source", | |
| "usable_latency_ms", | |
| "usable_http_status", | |
| "image_usable", | |
| "verified_image_usable", | |
| "image_usable_checked_at", | |
| "image_usable_source", | |
| "image_usable_latency_ms", | |
| "image_usable_http_status", | |
| "image_usable_carried_from_previous", | |
| "image_usable_last_probe_error_class", | |
| "image_usable_last_probe_http_status", | |
| "image_usable_last_probe_checked_at", | |
| ): | |
| if key in metadata and key not in enriched: | |
| enriched[key] = metadata[key] | |
| raw_info = enriched.get("model_info") | |
| merged_info = dict(raw_info) if isinstance(raw_info, dict) else {} | |
| metadata_info = metadata.get("model_info") | |
| if isinstance(metadata_info, dict): | |
| for key, value in metadata_info.items(): | |
| if key not in merged_info or merged_info.get(key) in (None, [], {}): | |
| merged_info[key] = value | |
| if merged_info: | |
| enriched["model_info"] = merged_info | |
| return enriched | |
| def genlabs_api_key() -> str | None: | |
| value = os.environ.get("GENLABS_API_KEY", "").strip() | |
| return value or None | |
| def cloudflare_api_token() -> str | None: | |
| value = os.environ.get("CLOUDFLARE_API_TOKEN", "").strip() | |
| return value or None | |
| def cloudflare_account_id() -> str | None: | |
| value = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "").strip() | |
| return value or None | |
| def pollinations_api_key() -> str | None: | |
| for name in ("POLLINATIONS_API_KEY", "POLLINATIONS_API_KEY_1"): | |
| value = os.environ.get(name, "").strip() | |
| if value: | |
| return value | |
| return None | |
| STABLEHORDE_MODELS = {"stablehorde/stablehorde"} | |
| def stablehorde_api_key() -> str | None: | |
| for name in ("STABLEHORDE_API_KEY", "STABLEHORDE_API_KEY_1"): | |
| value = os.environ.get(name, "").strip() | |
| if value: | |
| return value | |
| return None | |
| def is_genlabs_chat_payload(payload: Any) -> bool: | |
| return isinstance(payload, dict) and payload.get("model") in GENLABS_MODELS | |
| def is_nonstream_upstream_payload(payload: Any) -> bool: | |
| return ( | |
| isinstance(payload, dict) | |
| and bool(payload.get("stream")) | |
| and str(payload.get("model", "")) in NONSTREAM_UPSTREAM_MODELS | |
| ) | |
| def cloudflare_image_model_id(model: str) -> str | None: | |
| if model in CLOUDFLARE_IMAGE_MODELS: | |
| return model.split("/", 1)[1] | |
| if model.startswith("@cf/") and f"cloudflare/{model}" in CLOUDFLARE_IMAGE_MODELS: | |
| return model | |
| return None | |
| def pollinations_image_model_name(model: str) -> str | None: | |
| if model in POLLINATIONS_IMAGE_MODELS: | |
| return model.split("/", 1)[1] | |
| return None | |
| def modelslab_image_model_id(model: str) -> str | None: | |
| if model in MODELSLAB_IMAGE_MODELS: | |
| return model.split("/", 1)[1] | |
| return None | |
| def is_image_only_model(model: str) -> bool: | |
| return bool(cloudflare_image_model_id(model) or pollinations_image_model_name(model) or modelslab_image_model_id(model)) | |
| def genlabs_payload(payload: dict[str, Any], *, stream: bool) -> dict[str, Any]: | |
| out = dict(payload) | |
| out["model"] = str(payload["model"]).split("/", 1)[1] | |
| out["stream"] = stream | |
| return out | |
| def sse_payload(data: dict[str, Any] | str) -> bytes: | |
| if isinstance(data, str): | |
| return f"data: {data}\n\n".encode("utf-8") | |
| return f"data: {json.dumps(data, separators=(',', ':'))}\n\n".encode("utf-8") | |
| def completion_to_sse(payload: dict[str, Any]) -> Any: | |
| choices = payload.get("choices") | |
| choice = choices[0] if isinstance(choices, list) and choices else {} | |
| message = choice.get("message") if isinstance(choice, dict) else {} | |
| content = message.get("content") if isinstance(message, dict) else "" | |
| if content is None: | |
| content = "" | |
| if not isinstance(content, str): | |
| content = json.dumps(content, ensure_ascii=False) | |
| response_id = payload.get("id") or f"chatcmpl-{uuid.uuid4().hex}" | |
| created = payload.get("created") or int(time.time()) | |
| model = payload.get("model") or "unknown" | |
| finish_reason = choice.get("finish_reason") if isinstance(choice, dict) else None | |
| finish_reason = finish_reason or "stop" | |
| def event( | |
| delta: dict[str, Any], | |
| *, | |
| finish: str | None = None, | |
| usage: dict[str, Any] | None = None, | |
| ) -> dict[str, Any]: | |
| out: dict[str, Any] = { | |
| "id": response_id, | |
| "object": "chat.completion.chunk", | |
| "created": created, | |
| "model": model, | |
| "choices": [ | |
| { | |
| "index": 0, | |
| "delta": delta, | |
| "finish_reason": finish, | |
| } | |
| ], | |
| } | |
| if usage is not None: | |
| out["usage"] = usage | |
| return out | |
| yield sse_payload(event({"role": "assistant"})) | |
| if content: | |
| yield sse_payload(event({"content": content})) | |
| yield sse_payload(event({}, finish=finish_reason)) | |
| usage = payload.get("usage") | |
| if isinstance(usage, dict): | |
| yield sse_payload( | |
| { | |
| "id": response_id, | |
| "object": "chat.completion.chunk", | |
| "created": created, | |
| "model": model, | |
| "choices": [], | |
| "usage": usage, | |
| } | |
| ) | |
| yield sse_payload("[DONE]") | |
| async def nonstream_upstream_chat(request: Request, payload: dict[str, Any]) -> Response: | |
| upstream_payload = dict(payload) | |
| upstream_payload["stream"] = False | |
| upstream_response = await client.post( | |
| f"{UPSTREAM_URL}/v1/chat/completions", | |
| params=request.query_params, | |
| headers=clean_headers(request.headers), | |
| json=upstream_payload, | |
| ) | |
| if upstream_response.status_code < 200 or upstream_response.status_code >= 300: | |
| return Response( | |
| upstream_response.content, | |
| status_code=upstream_response.status_code, | |
| headers=clean_headers(upstream_response.headers), | |
| media_type=upstream_response.headers.get("content-type"), | |
| ) | |
| try: | |
| response_payload = upstream_response.json() | |
| except json.JSONDecodeError: | |
| return Response( | |
| upstream_response.content, | |
| status_code=upstream_response.status_code, | |
| headers=clean_headers(upstream_response.headers), | |
| media_type=upstream_response.headers.get("content-type"), | |
| ) | |
| return StreamingResponse( | |
| completion_to_sse(response_payload), | |
| status_code=upstream_response.status_code, | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-cache", | |
| "X-Accel-Buffering": "no", | |
| }, | |
| ) | |
| async def genlabs_stream(payload: dict[str, Any]) -> Response: | |
| api_key = genlabs_api_key() | |
| if not api_key: | |
| return JSONResponse({"error": "GENLABS_API_KEY is not configured"}, status_code=503) | |
| request = client.build_request( | |
| "POST", | |
| f"{GENLABS_BASE_URL}/chat/completions", | |
| headers={ | |
| "Authorization": f"Bearer {api_key}", | |
| "Accept": "text/event-stream", | |
| "Content-Type": "application/json", | |
| }, | |
| json=genlabs_payload(payload, stream=True), | |
| ) | |
| response = await client.send(request, stream=True) | |
| return StreamingResponse( | |
| response.aiter_raw(), | |
| status_code=response.status_code, | |
| headers=clean_headers(response.headers), | |
| background=BackgroundTask(response.aclose), | |
| ) | |
| async def genlabs_completion(payload: dict[str, Any]) -> Response: | |
| api_key = genlabs_api_key() | |
| if not api_key: | |
| return JSONResponse({"error": "GENLABS_API_KEY is not configured"}, status_code=503) | |
| model_name = str(payload["model"]) | |
| content_parts: list[str] = [] | |
| finish_reason = "stop" | |
| usage: dict[str, Any] | None = None | |
| response_id = f"chatcmpl-{uuid.uuid4().hex}" | |
| async with client.stream( | |
| "POST", | |
| f"{GENLABS_BASE_URL}/chat/completions", | |
| headers={ | |
| "Authorization": f"Bearer {api_key}", | |
| "Accept": "text/event-stream", | |
| "Content-Type": "application/json", | |
| }, | |
| json=genlabs_payload(payload, stream=True), | |
| ) as response: | |
| if response.status_code < 200 or response.status_code >= 300: | |
| body = await response.aread() | |
| return Response( | |
| body, | |
| status_code=response.status_code, | |
| headers=clean_headers(response.headers), | |
| media_type=response.headers.get("content-type"), | |
| ) | |
| async for line in response.aiter_lines(): | |
| if not line.startswith("data:"): | |
| continue | |
| data = line.removeprefix("data:").strip() | |
| if not data or data == "[DONE]": | |
| continue | |
| try: | |
| chunk = json.loads(data) | |
| except json.JSONDecodeError: | |
| continue | |
| response_id = chunk.get("id") or response_id | |
| if isinstance(chunk.get("usage"), dict): | |
| usage = chunk["usage"] | |
| choices = chunk.get("choices") | |
| if not isinstance(choices, list) or not choices: | |
| continue | |
| choice = choices[0] | |
| if not isinstance(choice, dict): | |
| continue | |
| if choice.get("finish_reason"): | |
| finish_reason = str(choice["finish_reason"]) | |
| delta = choice.get("delta") | |
| if isinstance(delta, dict) and isinstance(delta.get("content"), str): | |
| content_parts.append(delta["content"]) | |
| return JSONResponse( | |
| { | |
| "id": response_id, | |
| "object": "chat.completion", | |
| "created": int(time.time()), | |
| "model": model_name, | |
| "choices": [ | |
| { | |
| "index": 0, | |
| "message": { | |
| "role": "assistant", | |
| "content": "".join(content_parts), | |
| }, | |
| "finish_reason": finish_reason, | |
| } | |
| ], | |
| "usage": usage | |
| or { | |
| "prompt_tokens": 0, | |
| "completion_tokens": 0, | |
| "total_tokens": 0, | |
| }, | |
| } | |
| ) | |
| def openai_image_data(image_base64: str, prompt: str, media_type: str = "image/jpeg") -> dict[str, str]: | |
| return { | |
| "b64_json": image_base64, | |
| "url": f"data:{media_type};base64,{image_base64}", | |
| "revised_prompt": prompt, | |
| } | |
| def cloudflare_image_payload(payload: dict[str, Any]) -> dict[str, Any]: | |
| out: dict[str, Any] = {"prompt": payload.get("prompt")} | |
| for key in ("steps", "seed"): | |
| if payload.get(key) is not None: | |
| out[key] = payload[key] | |
| return out | |
| async def cloudflare_images_generations(payload: dict[str, Any]) -> Response: | |
| model = str(payload.get("model") or "") | |
| model_id = cloudflare_image_model_id(model) | |
| if not model_id: | |
| return JSONResponse( | |
| {"error": f"Unsupported Cloudflare image model: {model or '(missing)'}"}, | |
| status_code=400, | |
| ) | |
| prompt = payload.get("prompt") | |
| if not isinstance(prompt, str) or not prompt.strip(): | |
| return JSONResponse({"error": "Image generation prompt is required"}, status_code=400) | |
| api_token = cloudflare_api_token() | |
| account_id = cloudflare_account_id() | |
| if not api_token or not account_id: | |
| return JSONResponse( | |
| {"error": "CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID must be configured"}, | |
| status_code=503, | |
| ) | |
| requested_n = payload.get("n") if isinstance(payload.get("n"), int) else 1 | |
| image_count = min(max(1, requested_n), CLOUDFLARE_IMAGE_MAX_N) | |
| images: list[dict[str, str]] = [] | |
| for _ in range(image_count): | |
| response = await client.post( | |
| f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/{model_id}", | |
| headers={ | |
| "Authorization": f"Bearer {api_token}", | |
| "Content-Type": "application/json", | |
| }, | |
| json=cloudflare_image_payload(payload), | |
| ) | |
| content_type = response.headers.get("content-type", "") | |
| if response.status_code < 200 or response.status_code >= 300: | |
| return Response( | |
| response.content, | |
| status_code=response.status_code, | |
| headers=clean_headers(response.headers), | |
| media_type=content_type, | |
| ) | |
| if content_type.startswith("image/"): | |
| import base64 | |
| media_type = content_type.split(";", 1)[0] | |
| images.append(openai_image_data(base64.b64encode(response.content).decode("ascii"), prompt, media_type)) | |
| continue | |
| try: | |
| response_payload = response.json() | |
| except json.JSONDecodeError: | |
| return Response( | |
| response.content, | |
| status_code=response.status_code, | |
| headers=clean_headers(response.headers), | |
| media_type=content_type, | |
| ) | |
| result = response_payload.get("result") if isinstance(response_payload, dict) else None | |
| image_base64 = result.get("image") if isinstance(result, dict) else None | |
| if not isinstance(image_base64, str) or not image_base64: | |
| return JSONResponse( | |
| { | |
| "error": "Cloudflare image response did not include result.image", | |
| "provider_response": response_payload, | |
| }, | |
| status_code=502, | |
| ) | |
| images.append(openai_image_data(image_base64, prompt)) | |
| return JSONResponse( | |
| { | |
| "created": int(time.time()), | |
| "data": images, | |
| } | |
| ) | |
| async def modelslab_images_generations(payload: dict[str, Any]) -> Response: | |
| model = str(payload.get("model") or "") | |
| model_id = modelslab_image_model_id(model) | |
| if not model_id: | |
| return JSONResponse( | |
| {"error": f"Unsupported ModelSLAB image model: {model or '(missing)'}"}, | |
| status_code=400, | |
| ) | |
| prompt = payload.get("prompt") | |
| if not isinstance(prompt, str) or not prompt.strip(): | |
| return JSONResponse({"error": "Image generation prompt is required"}, status_code=400) | |
| api_key = os.environ.get("MODELSLAB_API_KEY", "").strip() | |
| if not api_key: | |
| return JSONResponse( | |
| {"error": "MODELSLAB_API_KEY must be configured"}, | |
| status_code=503, | |
| ) | |
| requested_n = payload.get("n") if isinstance(payload.get("n"), int) else 1 | |
| image_count = min(max(1, requested_n), 4) | |
| images: list[dict[str, str]] = [] | |
| for _ in range(image_count): | |
| response = await client.post( | |
| "https://modelslab.com/api/v6/images/text2img", | |
| headers={"Content-Type": "application/json"}, | |
| json={ | |
| "key": api_key, | |
| "model_id": model_id, | |
| "prompt": prompt, | |
| "negative_prompt": payload.get("negative_prompt", ""), | |
| "width": payload.get("size", "1024x1024").split("x")[0] if isinstance(payload.get("size"), str) else 1024, | |
| "height": payload.get("size", "1024x1024").split("x")[1] if isinstance(payload.get("size"), str) else 1024, | |
| "samples": 1, | |
| }, | |
| ) | |
| if response.status_code < 200 or response.status_code >= 300: | |
| return Response( | |
| response.content, | |
| status_code=response.status_code, | |
| headers=clean_headers(response.headers), | |
| media_type=response.headers.get("content-type"), | |
| ) | |
| try: | |
| response_payload = response.json() | |
| except json.JSONDecodeError: | |
| return JSONResponse({"error": "Invalid JSON response from ModelSLAB"}, status_code=502) | |
| if response_payload.get("status") == "error": | |
| return JSONResponse( | |
| {"error": response_payload.get("message", "ModelSLAB error")}, | |
| status_code=400, | |
| ) | |
| image_urls = response_payload.get("output", []) | |
| if not image_urls: | |
| return JSONResponse( | |
| {"error": "ModelSLAB did not return any images", "provider_response": response_payload}, | |
| status_code=502, | |
| ) | |
| import base64 | |
| for url in image_urls[:1]: | |
| if isinstance(url, str) and url.startswith("http"): | |
| img_response = await client.get(url) | |
| if img_response.status_code == 200: | |
| media_type = img_response.headers.get("content-type", "image/png").split(";")[0] | |
| images.append(openai_image_data(base64.b64encode(img_response.content).decode("ascii"), prompt, media_type)) | |
| return JSONResponse( | |
| { | |
| "created": int(time.time()), | |
| "data": images, | |
| } | |
| ) | |
| def pollinations_image_payload(payload: dict[str, Any], model_name: str) -> dict[str, Any]: | |
| out: dict[str, Any] = { | |
| "prompt": payload.get("prompt"), | |
| "model": model_name, | |
| } | |
| for key in ("n", "size", "quality", "response_format", "user", "image", "safe"): | |
| if payload.get(key) is not None: | |
| out[key] = payload[key] | |
| return out | |
| async def pollinations_images_generations(payload: dict[str, Any]) -> Response: | |
| model = str(payload.get("model") or "") | |
| model_name = pollinations_image_model_name(model) | |
| if not model_name: | |
| return JSONResponse( | |
| {"error": f"Unsupported Pollinations image model: {model or '(missing)'}"}, | |
| status_code=400, | |
| ) | |
| prompt = payload.get("prompt") | |
| if not isinstance(prompt, str) or not prompt.strip(): | |
| return JSONResponse({"error": "Image generation prompt is required"}, status_code=400) | |
| api_key = pollinations_api_key() | |
| if not api_key: | |
| return JSONResponse({"error": "POLLINATIONS_API_KEY must be configured"}, status_code=503) | |
| response = await client.post( | |
| "https://gen.pollinations.ai/v1/images/generations", | |
| headers={ | |
| "Authorization": f"Bearer {api_key}", | |
| "Content-Type": "application/json", | |
| }, | |
| json=pollinations_image_payload(payload, model_name), | |
| ) | |
| return Response( | |
| response.content, | |
| status_code=response.status_code, | |
| headers=clean_headers(response.headers), | |
| media_type=response.headers.get("content-type"), | |
| ) | |
| async def genlabs_chat_completions(request: Request) -> Response: | |
| payload = await request.json() | |
| if bool(payload.get("stream")): | |
| return await genlabs_stream(payload) | |
| return await genlabs_completion(payload) | |
| async def models_response(request: Request) -> Response: | |
| try: | |
| upstream = await client.get( | |
| f"{UPSTREAM_URL}/v1/models", | |
| params=request.query_params, | |
| headers=clean_headers(request.headers), | |
| timeout=5, | |
| ) | |
| except Exception: | |
| # upstream litellm not ready yet; return static model list | |
| static_ids = ["groq/llama-3.3-70b-versatile", "sambanova/Meta-Llama-3.3-70B-Instruct", "gemini-flash-3"] | |
| return JSONResponse({ | |
| "object": "list", | |
| "data": [{"id": mid, "object": "model", "created": 0, "owned_by": mid.split("/", 1)[0]} for mid in static_ids] | |
| }) | |
| try: | |
| payload = upstream.json() | |
| except json.JSONDecodeError: | |
| return Response( | |
| upstream.content, | |
| status_code=upstream.status_code, | |
| headers=clean_headers(upstream.headers), | |
| media_type=upstream.headers.get("content-type"), | |
| ) | |
| if isinstance(payload, dict) and isinstance(payload.get("data"), list): | |
| enriched_data: list[Any] = [] | |
| existing = set() | |
| for item in payload["data"]: | |
| if isinstance(item, dict): | |
| model_id = item.get("id") | |
| if isinstance(model_id, str): | |
| existing.add(model_id) | |
| metadata = model_metadata(model_id) | |
| enriched_data.append(merge_model_metadata(item, metadata) if metadata else item) | |
| continue | |
| enriched_data.append(item) | |
| payload["data"] = enriched_data | |
| for model in sorted(runtime_extra_model_ids()): | |
| if model not in existing: | |
| metadata = model_metadata(model) | |
| payload["data"].append( | |
| { | |
| "id": model, | |
| "object": "model", | |
| "created": 0, | |
| "owned_by": metadata.get("provider") or model.split("/", 1)[0], | |
| **{key: value for key, value in metadata.items() if key != "id"}, | |
| } | |
| ) | |
| return JSONResponse(payload, status_code=upstream.status_code) | |
| def model_catalog_response() -> Response: | |
| metadata = { | |
| **load_catalog_metadata(), | |
| } | |
| for model_id, usable in load_usable_metadata().items(): | |
| metadata[model_id] = {**metadata.get(model_id, {"id": model_id}), **usable} | |
| return JSONResponse( | |
| { | |
| "object": "model_catalog", | |
| "sources": [str(MODEL_CATALOG_PATH), str(USABLE_MODELS_PATH)], | |
| "count": len(metadata), | |
| "data": [metadata[key] for key in sorted(metadata)], | |
| } | |
| ) | |
| async def proxy_request(path: str, request: Request) -> Response: | |
| url = f"{UPSTREAM_URL}/{path}" | |
| body = await request.body() | |
| upstream_request = client.build_request( | |
| request.method, | |
| url, | |
| params=request.query_params, | |
| headers=clean_headers(request.headers), | |
| content=body, | |
| ) | |
| upstream_response = await client.send(upstream_request, stream=True) | |
| return StreamingResponse( | |
| upstream_response.aiter_raw(), | |
| status_code=upstream_response.status_code, | |
| headers=clean_headers(upstream_response.headers), | |
| background=BackgroundTask(upstream_response.aclose), | |
| ) | |
| verify_lock = False | |
| discover_lock = False | |
| USABLE_MODELS_DATASET = os.environ.get("USABLE_MODELS_DATASET", "alchoholpad/litellm-usable-models") | |
| async def admin_logs() -> Response: | |
| try: | |
| log_path = Path("/tmp/verify.log") | |
| if log_path.exists(): | |
| content = log_path.read_text(encoding="utf-8", errors="replace") | |
| return Response(content[-4096:], media_type="text/plain") | |
| return JSONResponse({"error": "no log file yet"}, status_code=404) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def admin_discover() -> Response: | |
| global discover_lock | |
| if discover_lock: | |
| return JSONResponse({"status": "already running"}, status_code=409) | |
| discover_lock = True | |
| async def _run_discover(): | |
| global discover_lock | |
| log_path = Path("/tmp/discover.log") | |
| log_path.write_text("starting catalog discovery...\n") | |
| try: | |
| catalog = json.loads(MODEL_CATALOG_PATH.read_text(encoding="utf-8")) | |
| groups = catalog.get("groups", []) | |
| discovered: dict[str, list[str]] = {} | |
| errors: dict[str, str] = {} | |
| for group in groups: | |
| alias = group.get("alias_prefix", "unknown") | |
| api_base = (group.get("params") or {}).get("api_base", "") | |
| key_env = group.get("api_key_env", "") | |
| if not api_base or not key_env: | |
| continue | |
| api_key = os.environ.get(key_env, "") | |
| if not api_key: | |
| errors[alias] = "no_api_key" | |
| continue | |
| models_url = f"{api_base.rstrip('/')}/models" | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"[{alias}] fetching {models_url}\n") | |
| try: | |
| import httpx as _httpx | |
| async with _httpx.AsyncClient(timeout=_httpx.Timeout(15.0)) as _client: | |
| resp = await _client.get(models_url, headers={"Authorization": f"Bearer {api_key}"}) | |
| if resp.status_code >= 400: | |
| errors[alias] = f"HTTP {resp.status_code}" | |
| continue | |
| data = resp.json() | |
| model_list = data.get("data", data.get("models", data if isinstance(data, list) else [])) | |
| ids = [m.get("id", m) if isinstance(m, dict) else str(m) for m in model_list if m] | |
| discovered[alias] = sorted(ids) | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"[{alias}] found {len(ids)} models\n") | |
| except Exception as exc: | |
| errors[alias] = f"{type(exc).__name__}: {exc}" | |
| output = { | |
| "discovered_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), | |
| "providers": {alias: {"count": len(models), "models": models} for alias, models in sorted(discovered.items())}, | |
| "errors": errors, | |
| "total_models": sum(len(m) for m in discovered.values()), | |
| "total_providers": len(discovered), | |
| } | |
| out_path = Path("/app/config/discovered-models.json") | |
| tmp = out_path.with_suffix(".tmp") | |
| tmp.write_text(json.dumps(output, indent=2, sort_keys=True) + "\n", encoding="utf-8") | |
| tmp.replace(out_path) | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"[done] {output['total_providers']} providers, {output['total_models']} models\n") | |
| except Exception as exc: | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"[fatal] {type(exc).__name__}: {exc}\n") | |
| finally: | |
| discover_lock = False | |
| import asyncio | |
| asyncio.get_event_loop().create_task(_run_discover()) | |
| return JSONResponse({"status": "started"}) | |
| async def admin_discover_status() -> Response: | |
| try: | |
| log_path = Path("/tmp/discover.log") | |
| if log_path.exists(): | |
| return Response(log_path.read_text(encoding="utf-8", errors="replace")[-4096:], media_type="text/plain") | |
| return JSONResponse({"error": "no log file yet"}, status_code=404) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def admin_discover_results() -> Response: | |
| try: | |
| results_path = Path("/app/config/discovered-models.json") | |
| if results_path.exists(): | |
| return Response(results_path.read_text(encoding="utf-8", errors="replace"), media_type="application/json") | |
| return JSONResponse({"error": "no results yet, run /admin/discover first"}, status_code=404) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| async def usable_models_endpoint() -> Response: | |
| meta = load_catalog_metadata() | |
| usable = load_usable_metadata() | |
| chat, image, vision = [], [], [] | |
| for model_id, info in usable.items(): | |
| upstream = "" | |
| cat = meta.get(model_id) | |
| if cat and cat.get("source_model"): | |
| upstream = cat["source_model"] | |
| else: | |
| upstream = model_id | |
| if info.get("chat_usable"): | |
| chat.append(upstream) | |
| if info.get("image_usable"): | |
| image.append(upstream) | |
| if info.get("vision_usable"): | |
| vision.append(upstream) | |
| return JSONResponse({ | |
| "chat": sorted(set(chat)), | |
| "image": sorted(set(image)), | |
| "vision": sorted(set(vision)), | |
| }) | |
| async def admin_verify() -> Response: | |
| global verify_lock | |
| if verify_lock: | |
| return JSONResponse({"status": "already running"}, status_code=409) | |
| verify_lock = True | |
| async def _run_verify(): | |
| global verify_lock, usable_metadata_cache | |
| log_path = Path("/tmp/verify.log") | |
| log_path.write_text("starting verify (chat + image + vision)...\n") | |
| try: | |
| import asyncio | |
| base_url_val = os.environ.get("LITELLM_BASE_URL") or f"https://{os.environ.get('SPACE_HOST', 'alchoholpad-litellm.hf.space')}" | |
| probes = [ | |
| ("chat", ["--probe", "chat", "--output", "/app/config/usable-models.json"]), | |
| ("image", ["--probe", "image", "--image-scope", "all", "--no-filter", "--output", "/app/config/usable-image-models.json"]), | |
| ("vision", ["--probe", "vision", "--no-filter", "--retries", "3", "--retry-sleep", "5", "--concurrency", "4", "--output", "/app/config/usable-vision-models.json"]), | |
| ] | |
| for probe_idx, (probe_name, extra_args) in enumerate(probes): | |
| if probe_idx > 0: | |
| cooldown = 300 # 5 min between probes to avoid rate limits | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"\n[cooldown] waiting {cooldown}s before {probe_name} probe...\n") | |
| await asyncio.sleep(cooldown) | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"\n=== {probe_name.upper()} PROBE ===\n") | |
| proc = await asyncio.create_subprocess_exec( | |
| "python", "-u", "/app/scripts/verify-usable-models.py", | |
| "--base-url", base_url_val, | |
| "--concurrency", "8", "--timeout", "25", "--retries", "2", "--yes", | |
| *extra_args, | |
| stdout=asyncio.subprocess.PIPE, | |
| stderr=asyncio.subprocess.STDOUT, | |
| ) | |
| with open(log_path, "a") as log_f: | |
| async for line in proc.stdout: | |
| log_f.write(line.decode(errors="replace")) | |
| log_f.flush() | |
| await proc.wait() | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"[{probe_name}] exit code: {proc.returncode}\n") | |
| # Upload all results to HF dataset | |
| try: | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| hf_token = os.environ.get("HF_TOKEN") | |
| for fname in ["usable-models.json", "usable-image-models.json", "usable-vision-models.json"]: | |
| fpath = Path(f"/app/config/{fname}") | |
| if fpath.exists(): | |
| api.upload_file( | |
| path_or_fileobj=str(fpath), | |
| path_in_repo=fname, | |
| repo_id=USABLE_MODELS_DATASET, | |
| repo_type="dataset", | |
| token=hf_token, | |
| ) | |
| usable_metadata_cache = None | |
| with open(log_path, "a") as log_f: | |
| log_f.write("[upload] success\n") | |
| except Exception as e: | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"[upload error] {e}\n") | |
| except Exception as e: | |
| with open(log_path, "a") as log_f: | |
| log_f.write(f"[fatal] {type(e).__name__}: {e}\n") | |
| finally: | |
| verify_lock = False | |
| import asyncio | |
| asyncio.get_event_loop().create_task(_run_verify()) | |
| return JSONResponse({"status": "started"}) | |
| async def route(path: str, request: Request) -> Response: | |
| normalized = f"/{path}" | |
| if request.method == "GET" and normalized in {"/v1/model-catalog", "/model-catalog"}: | |
| return model_catalog_response() | |
| if request.method == "GET" and normalized == "/v1/models": | |
| return await models_response(request) | |
| if request.method == "POST" and normalized == "/v1/chat/completions": | |
| _try_render_config() # Ensure litellm config is rendered before first request | |
| try: | |
| payload = await request.json() | |
| except json.JSONDecodeError: | |
| return JSONResponse({"error": "Invalid JSON body"}, status_code=400) | |
| if is_image_only_model(str(payload.get("model") or "")): | |
| return JSONResponse( | |
| { | |
| "error": { | |
| "message": ( | |
| f"{payload.get('model')} is an image-only route in this wrapper. " | |
| "Use /v1/images/generations for image generation and select a chat model " | |
| "for normal Open WebUI conversations." | |
| ), | |
| "type": "invalid_request_error", | |
| "code": "image_model_used_for_chat", | |
| } | |
| }, | |
| status_code=400, | |
| ) | |
| if is_genlabs_chat_payload(payload): | |
| if bool(payload.get("stream")): | |
| return await genlabs_stream(payload) | |
| return await genlabs_completion(payload) | |
| if is_nonstream_upstream_payload(payload): | |
| return await nonstream_upstream_chat(request, payload) | |
| if request.method == "POST" and normalized in {"/v1/images/generations", "/images/generations"}: | |
| try: | |
| payload = await request.json() | |
| except json.JSONDecodeError: | |
| return JSONResponse({"error": "Invalid JSON body"}, status_code=400) | |
| if cloudflare_image_model_id(str(payload.get("model") or "")): | |
| return await cloudflare_images_generations(payload) | |
| if pollinations_image_model_name(str(payload.get("model") or "")): | |
| return await pollinations_images_generations(payload) | |
| if modelslab_image_model_id(str(payload.get("model") or "")): | |
| return await modelslab_images_generations(payload) | |
| return await proxy_request(path, request) | |
| if __name__ == "__main__": | |
| uvicorn.run( | |
| app, | |
| host=os.environ.get("HOST", "0.0.0.0"), | |
| port=int(os.environ.get("PORT", "7860")), | |
| ) | |
| # build: 2026-06-21T10:49:48Z | |