litellm / scripts /proxy_app.py
alchoholpad's picture
fix: skip model-catalog loading in proxy_app to avoid cpu-basic hang
28dec71
Raw
History Blame Contribute Delete
50.1 kB
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
@app.on_event("startup")
async def startup_event():
"""Skip re-rendering; start-litellm.sh already wrote a static config."""
pass
@app.on_event("shutdown")
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")
@app.api_route("/admin/logs", methods=["GET"])
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)
@app.api_route("/admin/discover", methods=["POST"])
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"})
@app.api_route("/admin/discover/status", methods=["GET"])
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)
@app.api_route("/admin/discover/results", methods=["GET"])
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)
@app.api_route("/v1/usable-models", methods=["GET"])
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)),
})
@app.api_route("/admin/verify", methods=["POST"])
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"})
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
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