"""函数级模型路由配置服务。""" 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])