feat: add DeepSeek and Xiaomi MiMo LLM provider presets

- Add providers.py with 5 provider presets (OpenAI, DeepSeek, Xiaomi MiMo, Alibaba DashScope, MiniMax)
- Add LLM_PROVIDER env var for one-line provider switching
- Improve <think> tag stripping for reasoning models
- Add .env.example with documented configuration
- Update README with provider configuration section
This commit is contained in:
Kunthawat Greethong
2026-06-17 11:13:34 +07:00
parent 96096ea0ff
commit f395309207
6 changed files with 406 additions and 27 deletions

View File

@@ -17,6 +17,47 @@ else:
load_dotenv(override=True)
def _resolve_llm_config() -> tuple[str, str, str | None]:
"""
解析LLM配置。
优先级:
1. 如果设置了 LLM_PROVIDER使用提供商预设填充 base_url 和 model但可被显式值覆盖
2. 否则使用 LLM_BASE_URL / LLM_MODEL_NAME兼容原有行为
Returns:
(base_url, model_name, provider_name_or_none)
"""
from .providers import get_provider
provider_name = os.environ.get('LLM_PROVIDER', '').strip()
explicit_base_url = os.environ.get('LLM_BASE_URL', '').strip()
explicit_model = os.environ.get('LLM_MODEL_NAME', '').strip()
if provider_name:
preset = get_provider(provider_name)
if preset is None:
provider_names = ["openai", "deepseek", "xiaomi_mimo", "alibaba_dashscope", "minimax"]
available = ", ".join(provider_names)
raise ValueError(
f"未知的 LLM_PROVIDER: '{provider_name}'. "
f"可用值: {available}"
)
# 显式值优先于预设默认值
base_url = explicit_base_url or preset.base_url
model = explicit_model or preset.default_model
return base_url, model, provider_name
else:
# 兼容原有行为:无 LLM_PROVIDER 时直接使用显式值
base_url = explicit_base_url or 'https://api.openai.com/v1'
model = explicit_model or 'gpt-4o-mini'
return base_url, model, None
# 在模块加载时解析配置(避免重复计算)
_llm_base_url, _llm_model_name, _llm_provider = _resolve_llm_config()
class Config:
"""Flask配置类"""
@@ -29,8 +70,9 @@ class Config:
# LLM配置统一使用OpenAI格式
LLM_API_KEY = os.environ.get('LLM_API_KEY')
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
LLM_MODEL_NAME = os.environ.get('LLM_MODEL_NAME', 'gpt-4o-mini')
LLM_PROVIDER = _llm_provider # e.g. "deepseek" or None
LLM_BASE_URL = _llm_base_url
LLM_MODEL_NAME = _llm_model_name
# Zep配置
ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
@@ -73,3 +115,19 @@ class Config:
errors.append("ZEP_API_KEY 未配置")
return errors
@classmethod
def get_active_provider_info(cls) -> dict:
"""返回当前活跃的LLM提供商信息用于日志/API展示"""
from .providers import get_provider
info = {
"provider": cls.LLM_PROVIDER or "custom",
"base_url": cls.LLM_BASE_URL,
"model": cls.LLM_MODEL_NAME,
}
if cls.LLM_PROVIDER:
preset = get_provider(cls.LLM_PROVIDER)
if preset:
info["display_name"] = preset.display_name
info["notes"] = preset.notes
return info

133
backend/app/providers.py Normal file
View File

@@ -0,0 +1,133 @@
"""
LLM Provider Presets
====================
预设的LLM提供商配置简化环境变量设置。
使用方式:
方式1推荐设置 LLM_PROVIDER 环境变量为提供商名称,自动填充 base_url 和 model
LLM_PROVIDER=deepseek
LLM_API_KEY=sk-xxx
方式2手动指定所有配置兼容原有方式
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL_NAME=deepseek-chat
支持的提供商:
- openai : OpenAI GPT系列
- deepseek : DeepSeek (深度求索)
- xiaomi_mimo : Xiaomi MiMo (小米MiMo)
- alibaba_dashscope : 阿里百炼 (通义千问)
- minimax : MiniMax (海螺AI)
"""
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class ProviderPreset:
"""LLM提供商预设配置"""
name: str
display_name: str
base_url: str
default_model: str
api_key_url: str
notes: str = ""
# 某些提供商的响应可能包含<think>标签如DeepSeek推理模型
may_include_think_tags: bool = False
# ============================================================
# Provider Presets
# ============================================================
PROVIDERS: dict[str, ProviderPreset] = {
"openai": ProviderPreset(
name="openai",
display_name="OpenAI",
base_url="https://api.openai.com/v1",
default_model="gpt-4o-mini",
api_key_url="https://platform.openai.com/api-keys",
notes="GPT-4o-mini recommended for cost efficiency.",
),
"deepseek": ProviderPreset(
name="deepseek",
display_name="DeepSeek (深度求索)",
base_url="https://api.deepseek.com/v1",
default_model="deepseek-chat",
api_key_url="https://platform.deepseek.com",
notes=(
"deepseek-chat: general purpose; "
"deepseek-reasoner: reasoning model with <think> tags in output. "
"Pricing: https://api-docs.deepseek.com/quick_start/pricing"
),
may_include_think_tags=True,
),
"xiaomi_mimo": ProviderPreset(
name="xiaomi_mimo",
display_name="Xiaomi MiMo (小米MiMo)",
base_url="https://api.xiaomimimo.com/v1",
default_model="mimo-v2.5-pro",
api_key_url="https://platform.xiaomimimo.com",
notes=(
"mimo-v2.5-pro: flagship model; "
"mimo-v2-flash: fast & economical. "
"OpenAI SDK compatible. May include <think> tags for reasoning."
),
may_include_think_tags=True,
),
"alibaba_dashscope": ProviderPreset(
name="alibaba_dashscope",
display_name="Alibaba DashScope (阿里百炼)",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
default_model="qwen-plus",
api_key_url="https://bailian.console.aliyun.com/",
notes=(
"qwen-plus: recommended balance of quality & cost. "
"High token consumption — try <40 round simulations first."
),
),
"minimax": ProviderPreset(
name="minimax",
display_name="MiniMax (海螺AI)",
base_url="https://api.minimax.chat/v1",
default_model="MiniMax-M2.5",
api_key_url="https://platform.minimaxi.com/",
notes="MiniMax-M2.5 may include <think> tags.",
may_include_think_tags=True,
),
}
def get_provider(name: str) -> Optional[ProviderPreset]:
"""
获取提供商预设配置。
Args:
name: 提供商名称(不区分大小写)
Returns:
ProviderPreset 或 None如果未找到
"""
return PROVIDERS.get(name.lower().strip())
def list_providers() -> list[dict]:
"""
列出所有可用的提供商预设。
Returns:
提供商信息列表
"""
return [
{
"name": p.name,
"display_name": p.display_name,
"base_url": p.base_url,
"default_model": p.default_model,
"api_key_url": p.api_key_url,
"notes": p.notes,
}
for p in PROVIDERS.values()
]

View File

@@ -1,6 +1,6 @@
"""
LLM客户端封装
统一使用OpenAI格式调用
统一使用OpenAI格式调用支持提供商预设DeepSeek、Xiaomi MiMo等
"""
import json
@@ -11,27 +11,56 @@ from openai import OpenAI
from ..config import Config
# <think>标签的正则表达式
# 匹配 <think>...</think> 标签及其内容(支持多行,非贪婪匹配)
# 也处理 <think>...</think> 变体(某些模型可能使用略微不同的格式)
THINK_TAG_PATTERN = re.compile(r'<think>[\s\S]*?</think>\s*', re.IGNORECASE)
# 某些提供商的推理模型会在响应中包含<think>标签
PROVIDERS_WITH_THINK_TAGS = {"deepseek", "xiaomi_mimo", "minimax"}
class LLMClient:
"""LLM客户端"""
"""LLM客户端,支持提供商预设配置"""
def __init__(
self,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
model: Optional[str] = None
model: Optional[str] = None,
provider: Optional[str] = None,
):
self.api_key = api_key or Config.LLM_API_KEY
self.base_url = base_url or Config.LLM_BASE_URL
self.model = model or Config.LLM_MODEL_NAME
self.provider = provider or Config.LLM_PROVIDER
if not self.api_key:
raise ValueError("LLM_API_KEY 未配置")
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
)
# 判断是否需要强制清理<think>标签
# 如果是已知的推理模型提供商,总是清理;否则也清理(安全兜底)
self._should_strip_think = (
self.provider in PROVIDERS_WITH_THINK_TAGS
or True # 总是清理,因为不影响正常输出
)
def _strip_think_tags(self, content: str) -> str:
"""
移除响应中的<think>思考内容标签。
某些模型如DeepSeek Reasoner、Xiaomi MiMo、MiniMax M2.5
会在响应中包含<think>...</think>标签,需要移除以获得纯净输出。
"""
if not content:
return content
return THINK_TAG_PATTERN.sub('', content).strip()
def chat(
self,
messages: List[Dict[str, str]],
@@ -62,9 +91,11 @@ class LLMClient:
kwargs["response_format"] = response_format
response = self.client.chat.completions.create(**kwargs)
content = response.choices[0].message.content
# 部分模型如MiniMax M2.5会在content中包含<think>思考内容,需要移除
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
content = response.choices[0].message.content or ""
# 移除<think>思考内容标签
content = self._strip_think_tags(content)
return content
def chat_json(
@@ -101,3 +132,10 @@ class LLMClient:
except json.JSONDecodeError:
raise ValueError(f"LLM返回的JSON格式无效: {cleaned_response}")
def get_info(self) -> Dict[str, Any]:
"""返回当前客户端配置信息(用于日志/调试)"""
return {
"provider": self.provider or "custom",
"base_url": self.base_url,
"model": self.model,
}