"""提供商凭据端点的依赖装配(运行时实现)。 把 `get_session` 装配成 `SqlCredentialStore`;把网关适配器装配成探测器。 测试经 `app.dependency_overrides` 注入内存替身——不联网。 """ from __future__ import annotations import uuid from typing import Annotated from fastapi import Depends from openai import AsyncOpenAI from sqlalchemy.ext.asyncio import AsyncSession from ww_config import get_settings from ww_db import get_session from ww_llm_gateway.adapters.base import Capabilities from ww_llm_gateway.adapters.openai_compat import OpenAICompatAdapter from ww_shared import AppError, ErrorCode from ww_api.security.credentials import ( CredentialKeyError, decrypt_api_key, ) from ww_api.services.credentials import ( CredentialStore, SqlCredentialStore, ) # 已知 OpenAI 兼容提供商 → base_url(ARCH §4.2)。 _PROVIDER_BASE_URLS: dict[str, str] = { "deepseek": "https://api.deepseek.com", "kimi": "https://api.moonshot.cn/v1", "qwen": "https://dashscope.aliyuncs.com/compatible-mode/v1", "glm": "https://open.bigmodel.cn/api/paas/v4", "openai": "https://api.openai.com/v1", } def get_credential_store( session: Annotated[AsyncSession, Depends(get_session)], ) -> CredentialStore: return SqlCredentialStore(session) class GatewayProviderProbe: """运行时探测:解密 Key→建 OpenAI 兼容适配器→最小请求验 Key→回能力矩阵。 依赖 store 取密文 + settings 取加密 key。绝不在日志/响应回显明文。 """ def __init__(self, store: CredentialStore, enc_key: str) -> None: self._store = store self._enc_key = enc_key async def probe(self, owner_id: uuid.UUID, provider: str) -> Capabilities: cred = await self._store.get_credential(owner_id, provider) if cred is None: raise AppError( ErrorCode.NOT_FOUND, f"provider {provider} 未配置凭据", {"provider": provider}, ) base_url = _PROVIDER_BASE_URLS.get(provider) if base_url is None: raise AppError( ErrorCode.VALIDATION, f"未知提供商 {provider}", {"provider": provider}, ) try: api_key = decrypt_api_key(cred.api_key_enc, key=self._enc_key) except CredentialKeyError as exc: raise AppError(ErrorCode.INTERNAL, str(exc)) from exc client = AsyncOpenAI(api_key=api_key, base_url=base_url) adapter = OpenAICompatAdapter(provider=provider, client=client) try: # 最小探测:列模型即可验证 Key 有效(不消耗生成额度)。 await client.models.list() except Exception as exc: # noqa: BLE001 — 任一失败都映射为 LLM 不可用 raise AppError( ErrorCode.LLM_UNAVAILABLE, f"provider {provider} 连接探测失败", {"provider": provider}, ) from exc return adapter.capabilities() def get_provider_probe( store: Annotated[CredentialStore, Depends(get_credential_store)], ) -> GatewayProviderProbe: return GatewayProviderProbe(store, get_settings().credential_enc_key)