"""设置服务:模型网关状态聚合与生成偏好。 模型网关状态从 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 回退 _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)