159 lines
5.1 KiB
Python
159 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Mapping
|
|
|
|
from apps.ai.providers.utils import (
|
|
API_CHAT,
|
|
API_GEMINI,
|
|
API_IMAGES,
|
|
API_IMAGES_EDITS,
|
|
detect_api_type,
|
|
)
|
|
|
|
from .models import AiModel, ModelAlias
|
|
|
|
|
|
def import_ai_models_config(
|
|
config: Mapping[str, Any],
|
|
*,
|
|
create_default_aliases: bool = False,
|
|
) -> dict[str, int]:
|
|
"""Import cmbot-style ai_models.json data into AiModel records."""
|
|
raw_models = config.get("models")
|
|
if not isinstance(raw_models, list):
|
|
raise ValueError("ai_models config must contain a models list")
|
|
|
|
created = 0
|
|
updated = 0
|
|
imported_models: list[AiModel] = []
|
|
for raw_model in raw_models:
|
|
if not isinstance(raw_model, Mapping):
|
|
raise ValueError("each model config must be an object")
|
|
ai_model, was_created = _upsert_ai_model(raw_model)
|
|
imported_models.append(ai_model)
|
|
if was_created:
|
|
created += 1
|
|
else:
|
|
updated += 1
|
|
|
|
aliases = 0
|
|
if create_default_aliases:
|
|
aliases = _ensure_default_aliases(imported_models)
|
|
|
|
return {"created": created, "updated": updated, "aliases": aliases}
|
|
|
|
|
|
def _upsert_ai_model(raw_model: Mapping[str, Any]) -> tuple[AiModel, bool]:
|
|
name = str(raw_model.get("name") or raw_model.get("model") or "").strip()
|
|
if not name:
|
|
raise ValueError("model config is missing name/model")
|
|
|
|
defaults = {
|
|
"url": str(raw_model.get("url") or ""),
|
|
"model": str(raw_model.get("model") or ""),
|
|
"api_type": str(raw_model.get("api_type") or AiModel.ApiType.AUTO),
|
|
"timeout_seconds": _to_non_negative_int(raw_model.get("timeout_seconds"), 0),
|
|
"connect_timeout_seconds": _to_positive_int(
|
|
raw_model.get("connect_timeout_seconds"), 30
|
|
),
|
|
"extra_body": dict(raw_model.get("extra_body") or {}),
|
|
"capabilities": _infer_capabilities(raw_model),
|
|
"is_active": True,
|
|
}
|
|
ai_model, created = AiModel.objects.update_or_create(name=name, defaults=defaults)
|
|
|
|
api_key = str(raw_model.get("api_key") or "").strip()
|
|
if api_key:
|
|
ai_model.set_api_key(api_key)
|
|
ai_model.save(update_fields=("api_key_encrypted", "updated_at"))
|
|
return ai_model, created
|
|
|
|
|
|
def _infer_capabilities(raw_model: Mapping[str, Any]) -> list[str]:
|
|
explicit = raw_model.get("capabilities")
|
|
if explicit:
|
|
if isinstance(explicit, dict):
|
|
return [
|
|
str(key)
|
|
for key, value in explicit.items()
|
|
if key in {"text", "image", "vision"} and bool(value)
|
|
]
|
|
if isinstance(explicit, str):
|
|
return [item.strip() for item in explicit.split(",") if item.strip()]
|
|
return [str(item) for item in explicit]
|
|
|
|
api_type = str(raw_model.get("api_type") or AiModel.ApiType.AUTO)
|
|
url = str(raw_model.get("url") or "")
|
|
resolved_api_type = detect_api_type(url, api_type)
|
|
name_model_text = f"{raw_model.get('name', '')} {raw_model.get('model', '')}".lower()
|
|
|
|
if resolved_api_type == API_IMAGES_EDITS:
|
|
return ["image", "vision"]
|
|
if resolved_api_type == API_IMAGES:
|
|
return ["image"]
|
|
if resolved_api_type == API_GEMINI:
|
|
return ["text", "image", "vision"]
|
|
if resolved_api_type == API_CHAT and (
|
|
"image" in name_model_text or "banana" in name_model_text
|
|
):
|
|
return ["image", "vision"]
|
|
return ["text", "vision"]
|
|
|
|
|
|
def _ensure_default_aliases(models: list[AiModel]) -> int:
|
|
aliases = 0
|
|
text_model = _first_model_with_capability(models, "text")
|
|
image_model = _first_model_with_capability(models, "image")
|
|
|
|
if text_model:
|
|
ModelAlias.objects.filter(
|
|
operation_type=ModelAlias.OperationType.TITLE,
|
|
is_default=True,
|
|
).update(is_default=False)
|
|
ModelAlias.objects.update_or_create(
|
|
operation_type=ModelAlias.OperationType.TITLE,
|
|
alias="title-standard",
|
|
defaults={"ai_model": text_model, "is_default": True, "is_active": True},
|
|
)
|
|
aliases += 1
|
|
|
|
if image_model:
|
|
ModelAlias.objects.filter(
|
|
operation_type=ModelAlias.OperationType.IMAGE,
|
|
is_default=True,
|
|
).update(is_default=False)
|
|
ModelAlias.objects.update_or_create(
|
|
operation_type=ModelAlias.OperationType.IMAGE,
|
|
alias="image-standard",
|
|
defaults={"ai_model": image_model, "is_default": True, "is_active": True},
|
|
)
|
|
aliases += 1
|
|
|
|
return aliases
|
|
|
|
|
|
def _first_model_with_capability(models: list[AiModel], capability: str) -> AiModel | None:
|
|
for model in models:
|
|
if capability in model.capabilities_set():
|
|
return model
|
|
return None
|
|
|
|
|
|
def _to_non_negative_int(value: Any, default: int) -> int:
|
|
parsed = _to_int(value, default)
|
|
return parsed if parsed >= 0 else default
|
|
|
|
|
|
def _to_positive_int(value: Any, default: int) -> int:
|
|
parsed = _to_int(value, default)
|
|
return parsed if parsed > 0 else default
|
|
|
|
|
|
def _to_int(value: Any, default: int) -> int:
|
|
if value is None or value == "":
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return default
|