Files
2026-08-21 16:00:13 +08:00

109 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""AI provider 运行时治理:endpoint 覆盖(DB 优先于 .env/yaml)+ 热重载。
override 写 DB → 读 DB 全量刷 spec 同步缓存 → ProviderManager 下次构建读新缓存。
api_key 明文存 DB(本地单用户边界),响应层脱敏不回值(见 schemas)。
"""
from __future__ import annotations
from typing import Any
from app.ai_provider.config.loader import load_models_config
from app.core.errors import AppError
from app.db.session import AsyncSessionLocal
from app.models.governance import ModelEndpointOverride
from app.repositories import model_endpoints as overrides_repo
from app.schemas.preferences import UpdateEndpointOverrideRequest
from app.services.audit import record_audit_event
# —— endpoint 运行时覆盖(DB 优先于 .env/yaml)——
# reload_provider_manager 时(异步 service)读 DB 全量刷此同步缓存,
# load_models_config 仍可独立使用。None 字段表示不覆盖、回退 yaml/env。
_endpoint_overrides_cache: dict[tuple[str, str], dict[str, Any]] = {}
def set_endpoint_overrides_cache(overrides: list[dict[str, Any]]) -> None:
"""用 DB 全量覆盖列表刷新模块缓存(同步)。"""
global _endpoint_overrides_cache
_endpoint_overrides_cache = {
(o["logical_model_name"], o["endpoint_name"]): o for o in overrides
}
def get_endpoint_overrides_cache() -> dict[tuple[str, str], dict[str, Any]]:
"""读取当前覆盖缓存快照。"""
return dict(_endpoint_overrides_cache)
async def list_endpoint_overrides() -> list[dict[str, Any]]:
"""读 DB 全量覆盖,序列化为 dict 列表(供刷缓存 + 响应)。"""
async with AsyncSessionLocal() as session:
rows = await overrides_repo.list_overrides(session)
return [_override_to_dict(r) for r in rows]
async def reload_provider_manager() -> None:
"""读 DB 全量 → 刷 endpoint 覆盖同步缓存。"""
overrides = await list_endpoint_overrides()
set_endpoint_overrides_cache(overrides)
async def update_endpoint_override(
logical_model_name: str,
endpoint_name: str,
payload: UpdateEndpointOverrideRequest,
) -> dict[str, Any]:
"""upsert 覆盖(payload 字段全量写入,None 清除字段)+ reload + 审计。
校验 (model, endpoint) 存在于 yaml(避免覆盖幽灵 endpoint)。
"""
if not _endpoint_exists(logical_model_name, endpoint_name):
raise AppError("not_found", "endpoint 不存在于模型配置中。")
async with AsyncSessionLocal() as session:
override = ModelEndpointOverride(
logical_model_name=logical_model_name,
endpoint_name=endpoint_name,
base_url=payload.base_url,
api_key=payload.api_key,
status=payload.status,
weight=payload.weight,
priority=payload.priority,
)
saved = await overrides_repo.upsert_override(session, override)
await record_audit_event(
session,
action="model_endpoint.override_updated",
actor="user",
input_summary=f"{logical_model_name}/{endpoint_name}",
resource_type="model_endpoint_override",
resource_id=f"{logical_model_name}/{endpoint_name}",
)
await session.commit()
await session.refresh(saved)
result = _override_to_dict(saved)
await reload_provider_manager()
return result
def _endpoint_exists(logical_model_name: str, endpoint_name: str) -> bool:
"""校验 (model, endpoint) 在 yaml 配置中存在(避免覆盖幽灵 endpoint)。"""
models = load_models_config()
for model in models:
if model.name != logical_model_name:
continue
return any(ep.name == endpoint_name for ep in model.endpoints)
return False
def _override_to_dict(row: ModelEndpointOverride) -> dict[str, Any]:
return {
"logical_model_name": row.logical_model_name,
"endpoint_name": row.endpoint_name,
"base_url": row.base_url,
"api_key": row.api_key,
"status": row.status,
"weight": row.weight,
"priority": row.priority,
"updated_at": row.updated_at,
}