223 lines
8.6 KiB
Python
223 lines
8.6 KiB
Python
"""函数级模型路由配置服务。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from sqlalchemy import and_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ....models import Integration
|
|
from ..model_config import get_tier_defaults
|
|
from ....utils.timezone import now_cn_naive
|
|
|
|
ALLOWED_PROVIDERS = {"openai", "glm", "deepseek", "ollama"}
|
|
ALLOWED_TIERS = {"low", "high", "safety"}
|
|
DEFAULT_FUNCTIONS = ("signal_planner_analyze",)
|
|
INTEGRATION_TYPE = "model_routing"
|
|
|
|
|
|
class ModelRoutingService:
|
|
@staticmethod
|
|
def _safe_dict(value: Any) -> dict[str, Any]:
|
|
if isinstance(value, dict):
|
|
try:
|
|
# 深拷贝,避免浅拷贝后嵌套 dict 原地修改导致 SQLAlchemy JSON 变更丢失。
|
|
return json.loads(json.dumps(value))
|
|
except Exception:
|
|
return dict(value)
|
|
if isinstance(value, str):
|
|
try:
|
|
parsed = json.loads(value)
|
|
return parsed if isinstance(parsed, dict) else {}
|
|
except Exception:
|
|
return {}
|
|
return {}
|
|
|
|
@staticmethod
|
|
def _normalize_function_name(function_name: str) -> str:
|
|
fn = str(function_name or "").strip().lower()
|
|
if not fn:
|
|
raise ValueError("function_name 不能为空")
|
|
return fn
|
|
|
|
@staticmethod
|
|
def _normalize_provider(provider: str) -> str:
|
|
p = str(provider or "").strip().lower()
|
|
if p not in ALLOWED_PROVIDERS:
|
|
raise ValueError("provider 仅支持 openai/glm/deepseek/ollama")
|
|
return p
|
|
|
|
@staticmethod
|
|
def _normalize_tier(tier: str) -> str:
|
|
t = str(tier or "").strip().lower() or "high"
|
|
if t not in ALLOWED_TIERS:
|
|
raise ValueError("tier 仅支持 low/high/safety")
|
|
return t
|
|
|
|
@classmethod
|
|
def _rule_matches_function(cls, *, rule: dict[str, Any], function_name: str) -> bool:
|
|
if bool(rule.get("match_all", False)):
|
|
return True
|
|
fn = cls._normalize_function_name(function_name)
|
|
exact = str(rule.get("function_exact") or "").strip().lower()
|
|
if exact and exact == fn:
|
|
return True
|
|
prefix = str(rule.get("function_prefix") or "").strip().lower()
|
|
if prefix in {"*", "all"}:
|
|
return True
|
|
if prefix and fn.startswith(prefix):
|
|
return True
|
|
match_any = rule.get("match_any") if isinstance(rule.get("match_any"), list) else []
|
|
for item in match_any:
|
|
token = str(item or "").strip().lower()
|
|
if token and token in fn:
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
def _load_default_rule(cls, *, function_name: str) -> dict[str, Any] | None:
|
|
_ = function_name
|
|
return None
|
|
|
|
@classmethod
|
|
async def _get_or_init_bundle(cls, db: AsyncSession, *, tenant_id: int) -> tuple[Integration, dict[str, Any]]:
|
|
row = (
|
|
await db.execute(
|
|
select(Integration).where(
|
|
and_(
|
|
Integration.tenant_id == int(tenant_id),
|
|
Integration.type == INTEGRATION_TYPE,
|
|
)
|
|
)
|
|
)
|
|
).scalar_one_or_none()
|
|
if row is None:
|
|
row = Integration(
|
|
tenant_id=int(tenant_id),
|
|
type=INTEGRATION_TYPE,
|
|
is_active=True,
|
|
config={"mappings": {}},
|
|
)
|
|
db.add(row)
|
|
await db.flush()
|
|
bundle = cls._safe_dict(row.config)
|
|
if not isinstance(bundle.get("mappings"), dict):
|
|
bundle["mappings"] = {}
|
|
return row, bundle
|
|
|
|
@classmethod
|
|
def _effective_from_override(cls, *, function_name: str, override: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"function_name": cls._normalize_function_name(function_name),
|
|
"provider": str(override.get("provider") or "").strip().lower(),
|
|
"model": str(override.get("model") or "").strip(),
|
|
"api_env": str(override.get("api_env") or "").strip(),
|
|
"tier": str(override.get("tier") or "high").strip().lower(),
|
|
"base_url": str(override.get("base_url") or "").strip(),
|
|
"source": "override",
|
|
"updated_at": str(override.get("updated_at") or ""),
|
|
"updated_by": override.get("updated_by"),
|
|
}
|
|
|
|
@classmethod
|
|
def _effective_from_rule(cls, *, function_name: str, rule: dict[str, Any] | None) -> dict[str, Any]:
|
|
if rule is None:
|
|
low_defaults = get_tier_defaults("low")
|
|
return {
|
|
"function_name": cls._normalize_function_name(function_name),
|
|
"provider": str(low_defaults.get("provider") or "glm").strip().lower(),
|
|
"model": str(low_defaults.get("model") or "qwen-plus").strip(),
|
|
"api_env": str(low_defaults.get("api_env") or "DASHSCOPE_API_KEY").strip(),
|
|
"tier": "high",
|
|
"base_url": str(low_defaults.get("base_url") or "https://dashscope.aliyuncs.com/compatible-mode/v1").strip(),
|
|
"source": "default",
|
|
"updated_at": "",
|
|
"updated_by": None,
|
|
}
|
|
return {
|
|
"function_name": cls._normalize_function_name(function_name),
|
|
"provider": str(rule.get("provider") or "").strip().lower(),
|
|
"model": str(rule.get("model") or "").strip(),
|
|
"api_env": str(rule.get("api_env") or "").strip(),
|
|
"tier": str(rule.get("tier") or "high").strip().lower(),
|
|
"base_url": str(rule.get("base_url") or "").strip(),
|
|
"source": "default",
|
|
"updated_at": "",
|
|
"updated_by": None,
|
|
}
|
|
|
|
@classmethod
|
|
async def get_effective_routing(
|
|
cls,
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
function_name: str,
|
|
) -> dict[str, Any]:
|
|
fn = cls._normalize_function_name(function_name)
|
|
_, bundle = await cls._get_or_init_bundle(db, tenant_id=int(tenant_id))
|
|
mappings = bundle.get("mappings") if isinstance(bundle.get("mappings"), dict) else {}
|
|
override = mappings.get(fn) if isinstance(mappings.get(fn), dict) else None
|
|
if override and bool(override.get("enabled", True)):
|
|
return cls._effective_from_override(function_name=fn, override=override)
|
|
return cls._effective_from_rule(function_name=fn, rule=cls._load_default_rule(function_name=fn))
|
|
|
|
@classmethod
|
|
async def list_effective_routing(
|
|
cls,
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
) -> list[dict[str, Any]]:
|
|
_, bundle = await cls._get_or_init_bundle(db, tenant_id=int(tenant_id))
|
|
mappings = bundle.get("mappings") if isinstance(bundle.get("mappings"), dict) else {}
|
|
keys = {cls._normalize_function_name(k) for k in mappings.keys() if str(k).strip()}
|
|
keys.update(DEFAULT_FUNCTIONS)
|
|
items: list[dict[str, Any]] = []
|
|
for fn in sorted(keys):
|
|
items.append(await cls.get_effective_routing(db, tenant_id=int(tenant_id), function_name=fn))
|
|
return items
|
|
|
|
@classmethod
|
|
async def upsert_override(
|
|
cls,
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
function_name: str,
|
|
provider: str,
|
|
model: str,
|
|
api_env: str,
|
|
tier: str,
|
|
updated_by: int,
|
|
base_url: str | None = None,
|
|
) -> dict[str, Any]:
|
|
fn = cls._normalize_function_name(function_name)
|
|
normalized_provider = cls._normalize_provider(provider)
|
|
normalized_tier = cls._normalize_tier(tier)
|
|
normalized_model = str(model or "").strip()
|
|
if not normalized_model:
|
|
raise ValueError("model 不能为空")
|
|
normalized_api_env = str(api_env or "").strip()
|
|
if normalized_provider != "ollama" and not normalized_api_env:
|
|
raise ValueError("api_env 不能为空")
|
|
|
|
row, bundle = await cls._get_or_init_bundle(db, tenant_id=int(tenant_id))
|
|
mappings = bundle.get("mappings") if isinstance(bundle.get("mappings"), dict) else {}
|
|
mappings[fn] = {
|
|
"provider": normalized_provider,
|
|
"model": normalized_model,
|
|
"api_env": normalized_api_env,
|
|
"tier": normalized_tier,
|
|
"base_url": str(base_url or "").strip(),
|
|
"enabled": True,
|
|
"updated_at": now_cn_naive().isoformat(),
|
|
"updated_by": int(updated_by),
|
|
}
|
|
bundle["mappings"] = mappings
|
|
row.config = bundle
|
|
await db.flush()
|
|
return cls._effective_from_override(function_name=fn, override=mappings[fn])
|