Files
diary-news/backend/app/services/llm/providers.py
xiaji aaf728f3f4 feat(admin): Angel(Agnes) provider 凭据 DB 化 + 安全 key_set 字段
- llm_settings.agnes_api_key           TEXT   (DB key 优先,.env 兜底)
- llm_settings.agnes_base_url_override VARCHAR (留空 = 用 .env)
- alembic 0005_agnes_key 迁移
- LlmSettingOut.agnes_api_key_set (bool) 替代直接回传 key
- LlmSettingUpdate 加 agnes_api_key / agnes_base_url_override(可空可清空)
- providers.get_angel_client 改用 DB key 优先
- enrichment.py 改为 get_angel_client() 工厂调用(热改 key 不需重启)
- /admin/llm/settings/test 走 get_angel_client(测的是 DB 里的 key)
- 前端 AdminLlmSettings 在'总开关 + 模型'卡里加 Angel api_key 输入框 +
  base_url 覆盖 + 已配置/未配置指示灯 + 清空按钮
- 顶部'测连接'按钮复用(测的就是 Angel)
2026-06-12 20:43:54 +08:00

121 lines
4.3 KiB
Python

"""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,
},
}