158 lines
6.0 KiB
Python
158 lines
6.0 KiB
Python
"""设置服务:模型网关状态聚合与生成偏好。
|
|
|
|
模型网关状态从 config/models.yaml 派生,provider 密钥按 settings 是否配置脱敏为
|
|
configured 布尔(绝不暴露值);生成偏好为本地单用户单行,无则返回默认值。
|
|
"""
|
|
|
|
from app.ai_provider.config.loader import load_models_config
|
|
from app.ai_provider.config.models import ModelConfig
|
|
from app.core.config import settings
|
|
from app.core.time import utc_now
|
|
from app.db.session import AsyncSessionLocal
|
|
from app.models.governance import GenerationPreferences
|
|
from app.repositories import preferences as preferences_repo
|
|
from app.schemas.preferences import (
|
|
GenerationPreferencesResponse,
|
|
LogicalModel,
|
|
ModelEndpointStatus,
|
|
ModelGatewayStatus,
|
|
ProviderKeyStatus,
|
|
UpdateGenerationPreferencesRequest,
|
|
)
|
|
from app.services.ai_provider_admin import get_endpoint_overrides_cache
|
|
from app.services.serializers import generation_preferences_to_response
|
|
|
|
# provider → 持有其 API Key 的环境变量名;未登记的 provider 回退 <PROVIDER>_API_KEY。
|
|
_PROVIDER_KEY_ENV: dict[str, str] = {
|
|
"openai": "OPENAI_API_KEY",
|
|
"deepseek": "DEEPSEEK_API_KEY",
|
|
"anthropic": "ANTHROPIC_API_KEY",
|
|
"google": "GOOGLE_API_KEY",
|
|
"minimax": "MINIMAX_API_KEY",
|
|
"zhipu": "ZHIPU_API_KEY",
|
|
"tencentcloud": "TENCENTCLOUD_API_KEY",
|
|
"volcengine": "VOLCENGINE_ARK_API_KEY",
|
|
"bailian": "BAILIAN_API_KEY",
|
|
}
|
|
|
|
|
|
def provider_configured(provider: str | None) -> bool:
|
|
"""provider 的 API Key 是否已在 settings 配置(仅返回布尔,不暴露值)。"""
|
|
if not provider:
|
|
return False
|
|
env_name = _PROVIDER_KEY_ENV.get(provider, f"{provider.upper()}_API_KEY")
|
|
return bool(getattr(settings, env_name.lower(), None))
|
|
|
|
|
|
def _endpoint_status(
|
|
endpoint, model_name: str, provider: str, overrides_cache
|
|
) -> ModelEndpointStatus:
|
|
ov = overrides_cache.get((model_name, endpoint.name))
|
|
return ModelEndpointStatus(
|
|
name=endpoint.name,
|
|
provider=provider,
|
|
status=(ov.get("status") or endpoint.status) if ov else endpoint.status,
|
|
weight=endpoint.weight,
|
|
priority=endpoint.priority,
|
|
is_overridden=ov is not None,
|
|
has_url_override=bool(ov and ov.get("base_url")),
|
|
has_key_override=bool(ov and ov.get("api_key")),
|
|
base_url_env=endpoint.base_url_env,
|
|
api_key_env=endpoint.api_key_env,
|
|
)
|
|
|
|
|
|
def _logical_model(cfg: ModelConfig, overrides_cache) -> LogicalModel:
|
|
provider = cfg.provider or "unknown"
|
|
endpoints = [
|
|
_endpoint_status(ep, cfg.name, provider, overrides_cache) for ep in cfg.endpoints
|
|
] or None
|
|
return LogicalModel(
|
|
name=cfg.name,
|
|
display_name=cfg.display_name or cfg.name,
|
|
provider=provider,
|
|
capabilities=[k for k, v in cfg.resolved_capabilities().items() if v],
|
|
routing_strategy=cfg.routing_strategy,
|
|
supports_streaming=bool(cfg.supports_streaming),
|
|
supports_thinking=bool(cfg.supports_thinking),
|
|
supports_structured_output=bool(cfg.supports_structured_output),
|
|
context_window=cfg.context_window,
|
|
max_output_tokens=cfg.max_output_tokens,
|
|
endpoints=endpoints,
|
|
is_default=True if cfg.name == settings.default_model_hint else None,
|
|
)
|
|
|
|
|
|
def get_provider_gateway() -> ModelGatewayStatus:
|
|
"""从 config/models.yaml 派生模型网关总览(含 DB 覆盖标记,脱敏不回原值)。"""
|
|
models_cfg = load_models_config()
|
|
overrides_cache = get_endpoint_overrides_cache()
|
|
providers_seen: dict[str, None] = {}
|
|
logical_models: list[LogicalModel] = []
|
|
for cfg in models_cfg:
|
|
logical_models.append(_logical_model(cfg, overrides_cache))
|
|
provider = cfg.provider or "unknown"
|
|
providers_seen.setdefault(provider, None)
|
|
providers = [
|
|
ProviderKeyStatus(provider=name, configured=provider_configured(name))
|
|
for name in providers_seen
|
|
]
|
|
return ModelGatewayStatus(
|
|
models=logical_models,
|
|
providers=providers,
|
|
default_model_hint=settings.default_model_hint,
|
|
)
|
|
|
|
|
|
def _default_preferences() -> GenerationPreferences:
|
|
return GenerationPreferences(
|
|
owner_scope=settings.default_owner_scope,
|
|
default_model_hint=settings.default_model_hint,
|
|
temperature=0.7,
|
|
top_p=1.0,
|
|
max_output_tokens=4096,
|
|
streaming=True,
|
|
)
|
|
|
|
|
|
async def get_generation_preferences(
|
|
owner_scope: str = settings.default_owner_scope,
|
|
) -> GenerationPreferencesResponse:
|
|
async with AsyncSessionLocal() as session:
|
|
prefs = await preferences_repo.get_preferences(session, owner_scope)
|
|
if prefs is None:
|
|
prefs = _default_preferences()
|
|
return generation_preferences_to_response(prefs)
|
|
|
|
|
|
async def update_generation_preferences(
|
|
payload: UpdateGenerationPreferencesRequest,
|
|
owner_scope: str = settings.default_owner_scope,
|
|
) -> GenerationPreferencesResponse:
|
|
async with AsyncSessionLocal() as session:
|
|
prefs = await preferences_repo.get_preferences(session, owner_scope)
|
|
if prefs is None:
|
|
prefs = _default_preferences()
|
|
for key, value in payload.model_dump(exclude_unset=True).items():
|
|
setattr(prefs, key, value)
|
|
prefs.updated_at = utc_now()
|
|
session.add(prefs)
|
|
try:
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
prefs = await preferences_repo.get_preferences(session, owner_scope)
|
|
if prefs is None:
|
|
raise
|
|
for key, value in payload.model_dump(exclude_unset=True).items():
|
|
setattr(prefs, key, value)
|
|
prefs.updated_at = utc_now()
|
|
await session.commit()
|
|
else:
|
|
for key, value in payload.model_dump(exclude_unset=True).items():
|
|
setattr(prefs, key, value)
|
|
prefs.updated_at = utc_now()
|
|
await session.commit()
|
|
await session.refresh(prefs)
|
|
return generation_preferences_to_response(prefs)
|