"""LLM provider 工厂。 历史:全站只用一个 LlmClient(单例)指 Agnes。 现在:支持多个 provider,各自独立 base_url / api_key / model / 节流。 - `get_angel_client(setting)` — Agnes 客户端(原 LlmClient 等价) - `get_meituan_client(setting)` — 美团大模型客户端(OpenAI 兼容,LongCat) 设计: - 工厂每次返回新实例(无状态;节流靠 client 内部 Semaphore 自带) - Provider 不可用(api_key 空)= 返回 None - `get_provider_commentary_defaults()` 暴露 Angel / 美团 的 temperature / max_tokens / system 差异。 """ from __future__ import annotations import logging from typing import Any from app.models.llm_setting import LlmSetting from app.services.llm.client import LlmClient logger = logging.getLogger("news.llm.providers") # === Provider 名常量(供 enrichment/前端/日志统一引用)=== PROVIDER_ANGEL = "angel" # Agnes(原 LlmClient 默认端点) PROVIDER_MEITUAN = "meituan" # 美团大模型(LongCat,OpenAI 兼容) def get_angel_client(setting: LlmSetting) -> LlmClient: """Agnes(Angel)客户端。 凭据优先级(高 → 低): 1. llm_settings.agnes_api_key(DB 里存的 key,UI 可改) 2. .env AGNES_API_KEY 3. .env 任意一个都不配 = LlmClient.is_configured() = False base_url 优先级: 1. llm_settings.agnes_base_url_override(DB 里存的) 2. .env AGNES_BASE_URL """ from app.config import settings as app_settings api_key = (getattr(setting, "agnes_api_key", "") or app_settings.agnes_api_key) or "" base_url = ( getattr(setting, "agnes_base_url_override", "") or app_settings.agnes_base_url ).rstrip("/") return LlmClient( base_url=base_url or "https://apihub.agnes-ai.com/v1", api_key=api_key, chat_model=setting.chat_model or "agnes-2.0-flash", image_model=setting.image_model or "agnes-image-2.1-flash", interval_sec=setting.interval_sec or 2.0, ) def get_meituan_client(setting: LlmSetting) -> LlmClient | None: """美团大模型(LongCat)客户端。 配置来源:llm_settings 表里 meituan_* 字段(API key / base_url / model / interval / enabled)。 """ from app.config import settings as app_settings # 延迟导入,避免循环 api_key = getattr(setting, "meituan_api_key", "") or app_settings.meituan_api_key base_url = ( getattr(setting, "meituan_base_url", "") or app_settings.meituan_base_url ) model = ( getattr(setting, "meituan_chat_model", "") or app_settings.meituan_chat_model ) interval = ( getattr(setting, "meituan_interval_sec", None) or app_settings.meituan_interval_sec ) if not api_key: return None return LlmClient( base_url=base_url or "https://api.longcat.chat/openai/v1", api_key=api_key, chat_model=model or "LongCat-2.0-Preview", interval_sec=float(interval or 2.0), ) def get_provider_client(provider: str, setting: LlmSetting) -> LlmClient | None: """统一入口:按 provider 名取客户端。不可用时返回 None。""" if provider == PROVIDER_ANGEL: c = get_angel_client(setting) return c if c.is_configured() else None if provider == PROVIDER_MEITUAN: return get_meituan_client(setting) raise ValueError(f"unknown provider: {provider}") def is_provider_enabled(provider: str, setting: LlmSetting) -> bool: """provider 是否启用 + 配置齐全。""" if not setting.enabled: return False if provider == PROVIDER_ANGEL: return get_provider_client(PROVIDER_ANGEL, setting) is not None if provider == PROVIDER_MEITUAN: if not bool(getattr(setting, "meituan_enabled", True)): return False return get_provider_client(PROVIDER_MEITUAN, setting) is not None return False # === Provider 评论差异(温度 / max_tokens / system)=== # Angel: temperature=0.6, max_tokens=600, system="你是资深新闻评论员。" # 美团: temperature=0.7, max_tokens=1000, system=None(用户示例无 system 字段) PROVIDER_COMMENTARY_DEFAULTS: dict[str, dict[str, Any]] = { PROVIDER_ANGEL: { "temperature": 0.6, "max_tokens": 600, "system": "你是资深新闻评论员。", }, PROVIDER_MEITUAN: { "temperature": 0.7, "max_tokens": 1000, "system": None, }, }