100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
from collections.abc import Mapping
|
|
from typing import Any
|
|
from urllib.parse import urljoin
|
|
|
|
import httpx
|
|
|
|
from app.integrations.model_router import ModelRouter
|
|
|
|
|
|
class OpenAICompatibleGateway:
|
|
"""Adapter for independently configured OpenAI-compatible model-pool items."""
|
|
|
|
def __init__(self, values: Mapping[str, Any]) -> None:
|
|
self.values = values
|
|
self.router = ModelRouter(values)
|
|
|
|
@staticmethod
|
|
def _url(item: Mapping[str, Any], path_key: str, fallback: str) -> str:
|
|
base_url = str(item.get("base_url", "")).rstrip("/") + "/"
|
|
path = str(item.get(path_key, fallback)).lstrip("/")
|
|
return urljoin(base_url, path)
|
|
|
|
@staticmethod
|
|
def _headers(item: Mapping[str, Any], *, json_content: bool = True) -> dict[str, str]:
|
|
headers = {"Authorization": f"Bearer {item.get('api_key', '')}"}
|
|
if json_content:
|
|
headers["Content-Type"] = "application/json"
|
|
return headers
|
|
|
|
async def list_models(self, model: Mapping[str, Any] | None = None) -> list[str]:
|
|
item = model or self.router.orchestrator()
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
response = await client.get(
|
|
self._url(item, "models_path", "/models"),
|
|
headers=self._headers(item),
|
|
)
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
return [candidate["id"] for candidate in payload.get("data", []) if isinstance(candidate, dict) and candidate.get("id")]
|
|
|
|
async def chat(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
*,
|
|
vision: bool = False,
|
|
model_instance_id: str | None = None,
|
|
) -> dict[str, Any]:
|
|
item = self.router.choose("spatial", model_instance_id) if vision else self.router.orchestrator()
|
|
async with httpx.AsyncClient(timeout=120) as client:
|
|
response = await client.post(
|
|
self._url(item, "chat_path", "/chat/completions"),
|
|
headers=self._headers(item),
|
|
json={"model": item["model_id"], "messages": messages},
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def generate_image(
|
|
self,
|
|
prompt: str,
|
|
*,
|
|
model_instance_id: str | None = None,
|
|
**options: Any,
|
|
) -> dict[str, Any]:
|
|
item = self.router.choose("image", model_instance_id)
|
|
payload = {"model": item["model_id"], "prompt": prompt, **options}
|
|
async with httpx.AsyncClient(timeout=180) as client:
|
|
response = await client.post(
|
|
self._url(item, "image_generation_path", "/images/generations"),
|
|
headers=self._headers(item),
|
|
json=payload,
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
async def edit_image(
|
|
self,
|
|
prompt: str,
|
|
image: bytes,
|
|
*,
|
|
filename: str = "image.png",
|
|
mask: bytes | None = None,
|
|
model_instance_id: str | None = None,
|
|
**options: Any,
|
|
) -> dict[str, Any]:
|
|
item = self.router.choose("image", model_instance_id)
|
|
files: dict[str, tuple[str, bytes, str]] = {"image": (filename, image, "image/png")}
|
|
if mask is not None:
|
|
files["mask"] = ("mask.png", mask, "image/png")
|
|
data = {"model": item["model_id"], "prompt": prompt, **options}
|
|
async with httpx.AsyncClient(timeout=180) as client:
|
|
response = await client.post(
|
|
self._url(item, "image_edit_path", "/images/edits"),
|
|
headers=self._headers(item, json_content=False),
|
|
data=data,
|
|
files=files,
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|