import httpx from openai import AsyncOpenAI from services.runtime_settings import get_ai_settings # 按 (base_url, api_key) 缓存客户端:聊天/视觉/语音可能指向不同网关与密钥, # 各自一个 pair,最多累积 3 个,有界。配置变更(settings PUT 会 invalidate # runtime_settings 缓存)后,新的 pair 会自然生成新客户端;旧的留存无副作用。 _client_cache: dict[tuple[str, str], AsyncOpenAI] = {} _http_client_cache: dict[tuple[str, str], httpx.AsyncClient] = {} def _get_client(base_url: str, api_key: str) -> AsyncOpenAI: cache_key = (base_url or "", api_key or "") if cache_key not in _client_cache: http_client = httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=30.0)) _http_client_cache[cache_key] = http_client _client_cache[cache_key] = AsyncOpenAI( api_key=api_key or "missing", base_url=base_url or None, http_client=http_client, ) return _client_cache[cache_key] async def get_openai_client() -> tuple[AsyncOpenAI, dict]: """聊天调用方(话题/报告/总结/对话)复用:用全局 ai_base_url / ai_api_key。""" settings = await get_ai_settings() client = _get_client( settings.get("ai_base_url") or "", settings.get("ai_api_key") or "", ) return client, settings async def get_client_for(purpose: str) -> tuple[AsyncOpenAI, dict]: """按用途取客户端:purpose 为 'voice' / 'vision'。 优先用 {purpose}_base_url / {purpose}_api_key,为空则回退到全局 ai_base_url / ai_api_key(单网关场景无需重复配置)。 """ settings = await get_ai_settings() base_url = settings.get(f"{purpose}_base_url") or settings.get("ai_base_url") or "" api_key = settings.get(f"{purpose}_api_key") or settings.get("ai_api_key") or "" client = _get_client(base_url, api_key) return client, settings