From 13e4c32d670d12cac75e0ecaa1e707f3d2bde14a Mon Sep 17 00:00:00 2001 From: Color2333 <1552429809@qq.com> Date: Fri, 8 May 2026 15:48:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(llm):=20=E6=8E=A5=E5=85=A5=E5=B0=8F?= =?UTF-8?q?=E7=B1=B3=20MiMo=20=E4=BD=9C=E4=B8=BA=E9=BB=98=E8=AE=A4=20chat?= =?UTF-8?q?=20provider=EF=BC=8C=E9=98=BF=E9=87=8C=E7=99=BE=E7=82=BC?= =?UTF-8?q?=E6=89=BF=E6=8B=85=20embedding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 xiaomi provider(OpenAI 兼容,base_url=token-plan-cn.xiaomimimo.com/v1) - 默认 LLM_PROVIDER=xiaomi,模型 tier 映射:skim=mimo-v2-omni / deep+fallback=mimo-v2.5-pro / vision=mimo-v2.5 - embedding 解耦至独立配置(EMBEDDING_API_KEY/BASE_URL/DIMENSIONS),默认走阿里百炼 text-embedding-v4 (1024 维) - 前端 PROVIDER_PRESETS、ProviderBadge、ConfigModal 默认值同步至 xiaomi - 智谱 zhipu provider 完整保留作为兜底,可前端切换 - 部署脚本与 .env.example 提示更新 Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 31 +++++---- apps/api/routers/llm_configs.py | 2 +- frontend/src/components/SettingsDialog.tsx | 27 ++++++-- frontend/src/pages/Settings.tsx | 21 ++++-- frontend/src/types/index.ts | 2 +- packages/config.py | 19 ++++-- packages/domain/model_tier.py | 6 ++ packages/domain/schemas.py | 2 +- packages/integrations/llm_client.py | 77 ++++++++++++++++++++-- scripts/copy_env_from_deploy.sh | 15 +++-- scripts/docker_deploy.sh | 2 +- 11 files changed, 158 insertions(+), 46 deletions(-) diff --git a/.env.example b/.env.example index 8f74aab..1a9dd29 100644 --- a/.env.example +++ b/.env.example @@ -34,8 +34,15 @@ CORS_ALLOW_ORIGINS=* # ========================================== # LLM API 配置(必须填写至少一个!) # ========================================== -# LLM Provider: openai / anthropic / zhipu -LLM_PROVIDER=zhipu +# LLM Provider: xiaomi / zhipu / openai / anthropic +LLM_PROVIDER=xiaomi + +# 小米 MiMo(默认推荐,OpenAI 兼容) +# Base URL: https://token-plan-cn.xiaomimimo.com/v1 +XIAOMI_API_KEY= + +# ZhipuAI 智谱(备选) +ZHIPU_API_KEY= # OpenAI(可选) OPENAI_API_KEY= @@ -43,19 +50,21 @@ OPENAI_API_KEY= # Anthropic(可选) ANTHROPIC_API_KEY= -# ZhipuAI 智谱(推荐,便宜) -ZHIPU_API_KEY= - # 外部 API SEMANTIC_SCHOLAR_API_KEY= OPENALEX_EMAIL= -# 模型配置 -LLM_MODEL_SKIM=glm-4.7 -LLM_MODEL_DEEP=glm-4.7 -LLM_MODEL_VISION=glm-4.6v -LLM_MODEL_FALLBACK=glm-4.7 -EMBEDDING_MODEL=embedding-3 +# 模型配置(小米 MiMo 默认) +LLM_MODEL_SKIM=mimo-v2-omni +LLM_MODEL_DEEP=mimo-v2.5-pro +LLM_MODEL_VISION=mimo-v2.5 +LLM_MODEL_FALLBACK=mimo-v2.5-pro + +# Embedding 独立 provider(小米 MiMo 不提供 embedding,默认走阿里百炼 DashScope) +EMBEDDING_MODEL=text-embedding-v4 +EMBEDDING_API_KEY= +EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 +EMBEDDING_DIMENSIONS=1024 # ========================================== # 成本管控 diff --git a/apps/api/routers/llm_configs.py b/apps/api/routers/llm_configs.py index c58abc5..e091c4d 100644 --- a/apps/api/routers/llm_configs.py +++ b/apps/api/routers/llm_configs.py @@ -26,7 +26,7 @@ class LLMConfigItem(BaseModel): class LLMConfigCreate(BaseModel): name: str = Field(..., description="配置名称") - provider: str = Field(..., description="提供商:zhipu/openai/anthropic/siliconflow") + provider: str = Field(..., description="提供商:xiaomi/zhipu/openai/anthropic/siliconflow") api_key: str = Field(..., description="API Key") api_base_url: str | None = Field(None, description="自定义 API Base URL") model_skim: str = Field(..., description="粗读/简单任务模型") diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index ea8d3da..b98afb6 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -110,6 +110,17 @@ const PROVIDER_PRESETS: Record< string, { label: string; base_url: string; models: Partial } > = { + xiaomi: { + label: "小米 MiMo", + base_url: "https://token-plan-cn.xiaomimimo.com/v1", + models: { + model_skim: "mimo-v2-omni", + model_deep: "mimo-v2.5-pro", + model_vision: "mimo-v2.5", + model_embedding: "text-embedding-v4", + model_fallback: "mimo-v2.5-pro", + }, + }, zhipu: { label: "智谱 AI", base_url: "https://open.bigmodel.cn/api/paas/v4/", @@ -334,6 +345,7 @@ function LLMTab() { function ProviderBadge({ provider }: { provider: string }) { const colors: Record = { + xiaomi: "bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300", zhipu: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300", openai: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300", @@ -341,6 +353,7 @@ function ProviderBadge({ provider }: { provider: string }) { "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300", }; const labels: Record = { + xiaomi: "小米 MiMo", zhipu: "智谱", openai: "OpenAI", anthropic: "Anthropic", @@ -364,14 +377,14 @@ function AddConfigInline({ }) { const [form, setForm] = useState({ name: "", - provider: "zhipu", + provider: "xiaomi", api_key: "", - api_base_url: PROVIDER_PRESETS.zhipu.base_url, - model_skim: PROVIDER_PRESETS.zhipu.models.model_skim || "", - model_deep: PROVIDER_PRESETS.zhipu.models.model_deep || "", - model_vision: PROVIDER_PRESETS.zhipu.models.model_vision || "", - model_embedding: PROVIDER_PRESETS.zhipu.models.model_embedding || "", - model_fallback: PROVIDER_PRESETS.zhipu.models.model_fallback || "", + api_base_url: PROVIDER_PRESETS.xiaomi.base_url, + model_skim: PROVIDER_PRESETS.xiaomi.models.model_skim || "", + model_deep: PROVIDER_PRESETS.xiaomi.models.model_deep || "", + model_vision: PROVIDER_PRESETS.xiaomi.models.model_vision || "", + model_embedding: PROVIDER_PRESETS.xiaomi.models.model_embedding || "", + model_fallback: PROVIDER_PRESETS.xiaomi.models.model_fallback || "", }); const [showKey, setShowKey] = useState(false); const [submitting, setSubmitting] = useState(false); diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index bf22094..c6cf99e 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -57,6 +57,11 @@ const NAV_ITEMS: { key: SettingsTab; label: string; icon: typeof Cpu }[] = [ ]; const PROVIDER_PRESETS: Record }> = { + xiaomi: { + label: "小米 MiMo", + base_url: "https://token-plan-cn.xiaomimimo.com/v1", + models: { model_skim: "mimo-v2-omni", model_deep: "mimo-v2.5-pro", model_vision: "mimo-v2.5", model_embedding: "text-embedding-v4", model_fallback: "mimo-v2.5-pro" }, + }, zhipu: { label: "智谱 AI", base_url: "https://open.bigmodel.cn/api/paas/v4/", @@ -125,11 +130,13 @@ export default function SettingsPage() { /* ======== LLM 设置 ======== */ function ProviderBadge({ provider }: { provider: string }) { const colors: Record = { + xiaomi: "bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300", zhipu: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300", openai: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300", anthropic: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300", }; const labels: Record = { + xiaomi: "小米 MiMo", zhipu: "智谱", openai: "OpenAI", anthropic: "Anthropic", @@ -330,14 +337,14 @@ function ConfigModal({ config, onClose, onSaved }: { config?: any; onClose: () = const { toast } = useToast(); const [form, setForm] = useState({ name: config?.name || "", - provider: config?.provider || "zhipu", + provider: config?.provider || "xiaomi", api_key: "", - api_base_url: config?.api_base_url || PROVIDER_PRESETS.zhipu.base_url, - model_skim: config?.model_skim || "glm-4.7", - model_deep: config?.model_deep || "glm-4.7", - model_vision: config?.model_vision || "glm-4.6v", - model_embedding: config?.model_embedding || "embedding-3", - model_fallback: config?.model_fallback || "glm-4.7", + api_base_url: config?.api_base_url || PROVIDER_PRESETS.xiaomi.base_url, + model_skim: config?.model_skim || "mimo-v2-omni", + model_deep: config?.model_deep || "mimo-v2.5-pro", + model_vision: config?.model_vision || "mimo-v2.5", + model_embedding: config?.model_embedding || "text-embedding-v4", + model_fallback: config?.model_fallback || "mimo-v2.5-pro", }); const [showKey, setShowKey] = useState(false); const [submitting, setSubmitting] = useState(false); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index da98525..6c68115 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -713,7 +713,7 @@ export interface ChatMessage { } /* ========== LLM 配置 ========== */ -export type LLMProvider = "openai" | "anthropic" | "zhipu"; +export type LLMProvider = "openai" | "anthropic" | "zhipu" | "xiaomi"; export interface LLMProviderConfig { id: string; diff --git a/packages/config.py b/packages/config.py index 8e9c892..63f23c8 100644 --- a/packages/config.py +++ b/packages/config.py @@ -41,17 +41,22 @@ class Settings(BaseSettings): "https://pm.vibingu.cn" # 自定义域名 HTTPS ) - # LLM Provider: openai / anthropic / zhipu - llm_provider: str = "zhipu" - llm_model_skim: str = "glm-4.7" - llm_model_deep: str = "glm-4.7" - llm_model_vision: str = "glm-4.6v" - llm_model_fallback: str = "glm-4.7" - embedding_model: str = "embedding-3" + # LLM Provider: openai / anthropic / zhipu / xiaomi + llm_provider: str = "xiaomi" + llm_model_skim: str = "mimo-v2-omni" + llm_model_deep: str = "mimo-v2.5-pro" + llm_model_vision: str = "mimo-v2.5" + llm_model_fallback: str = "mimo-v2.5-pro" + # Embedding 独立 provider(小米 MiMo 不提供 embedding,默认走阿里百炼 DashScope) + embedding_model: str = "text-embedding-v4" + embedding_api_key: str | None = None + embedding_base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1" + embedding_dimensions: int = 1024 openai_api_key: str | None = None anthropic_api_key: str | None = None zhipu_api_key: str | None = None + xiaomi_api_key: str | None = None semantic_scholar_api_key: str | None = None openalex_email: str | None = None ieee_api_key: str | None = None diff --git a/packages/domain/model_tier.py b/packages/domain/model_tier.py index 80cbc2c..c2c1cae 100644 --- a/packages/domain/model_tier.py +++ b/packages/domain/model_tier.py @@ -56,4 +56,10 @@ class ModelTier(str, Enum): ModelTier.PREMIUM: "glm-4.7", # 统一使用 GLM-4.7 ModelTier.VISION: "glm-4.6v", # 视觉专用 }, + "xiaomi": { + ModelTier.ECONOMY: "mimo-v2-omni", # 经济型:多模态轻量 + ModelTier.STANDARD: "mimo-v2.5-pro", # 标准型:纯文本强推理 + ModelTier.PREMIUM: "mimo-v2.5-pro", # 高级型:纯文本强推理 + ModelTier.VISION: "mimo-v2.5", # 视觉专用:支持多模态 + }, } diff --git a/packages/domain/schemas.py b/packages/domain/schemas.py index a915336..139675c 100644 --- a/packages/domain/schemas.py +++ b/packages/domain/schemas.py @@ -91,7 +91,7 @@ class TopicUpdate(BaseModel): class LLMProviderCreate(BaseModel): name: str - provider: str # openai / anthropic / zhipu + provider: str # openai / anthropic / zhipu / xiaomi api_key: str api_base_url: str | None = None model_skim: str diff --git a/packages/integrations/llm_client.py b/packages/integrations/llm_client.py index 77104c3..767ef01 100644 --- a/packages/integrations/llm_client.py +++ b/packages/integrations/llm_client.py @@ -1,5 +1,5 @@ """ -LLM 提供者抽象层 - OpenAI / Anthropic / ZhipuAI / Pseudo +LLM 提供者抽象层 - OpenAI / Anthropic / ZhipuAI / Xiaomi MiMo / Pseudo 支持从数据库动态加载激活的 LLM 配置 @author Color2333 """ @@ -14,8 +14,11 @@ import socket import threading import time -from collections.abc import Iterator from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterator from packages.config import get_settings @@ -144,6 +147,9 @@ def _load_active_config() -> LLMConfig: if settings.llm_provider == "zhipu": api_key = settings.zhipu_api_key base_url = "https://open.bigmodel.cn/api/paas/v4/" + elif settings.llm_provider == "xiaomi": + api_key = settings.xiaomi_api_key + base_url = "https://token-plan-cn.xiaomimimo.com/v1" elif settings.llm_provider == "openai": api_key = settings.openai_api_key elif settings.llm_provider == "anthropic": @@ -177,6 +183,7 @@ def invalidate_llm_config_cache() -> None: # 预置的 provider → base_url 映射 PROVIDER_BASE_URLS: dict[str, str] = { "zhipu": "https://open.bigmodel.cn/api/paas/v4/", + "xiaomi": "https://token-plan-cn.xiaomimimo.com/v1", "openai": "https://api.openai.com/v1", "anthropic": "", } @@ -284,7 +291,7 @@ def summarize_text( max_tokens: int | None = None, ) -> LLMResult: cfg = self._config() - if cfg.provider in ("openai", "zhipu") and cfg.api_key: + if cfg.provider in ("openai", "zhipu", "xiaomi") and cfg.api_key: return self._call_openai_compatible( prompt, stage, @@ -370,7 +377,7 @@ def vision_analyze( """发送图片 + 文本给 Vision 模型(GLM-4.6V 等)""" cfg = self._config() model = cfg.model_vision or cfg.model_deep - if cfg.provider in ("openai", "zhipu") and cfg.api_key: + if cfg.provider in ("openai", "zhipu", "xiaomi") and cfg.api_key: try: base_url = self._resolve_base_url(cfg) client = _get_openai_client(cfg.api_key or "", base_url) @@ -422,12 +429,59 @@ def vision_analyze( def embed_text(self, text: str, dimensions: int = 1536) -> list[float]: cfg = self._config() - if cfg.provider in ("openai", "zhipu") and cfg.api_key: + # 优先使用独立的 embedding 配置(适用于 chat 与 embedding 不同 provider 的场景, + # 例如 chat 走小米 MiMo,embedding 走阿里百炼 DashScope) + if self.settings.embedding_api_key: + maybe = self._embed_dedicated(text) + if maybe: + return maybe + if cfg.provider in ("openai", "zhipu", "xiaomi") and cfg.api_key: maybe = self._embed_openai_compatible(text, cfg) if maybe: return maybe return self._pseudo_embedding(text, dimensions) + def _embed_dedicated(self, text: str) -> list[float] | None: + """使用独立配置的 embedding provider(OpenAI 兼容协议)""" + if not text: + return None + try: + api_key = self.settings.embedding_api_key or "" + base_url = self.settings.embedding_base_url or None + model = self.settings.embedding_model + client = _get_openai_client(api_key, base_url) + kwargs: dict = {"model": model, "input": text} + if self.settings.embedding_dimensions: + kwargs["dimensions"] = self.settings.embedding_dimensions + response = client.embeddings.create(**kwargs) + vector = response.data[0].embedding + usage = response.usage + in_tokens = getattr(usage, "total_tokens", None) or getattr( + usage, "prompt_tokens", None + ) + in_cost, _ = self._estimate_cost( + model=model, + input_tokens=in_tokens, + output_tokens=0, + ) + self.trace_result( + LLMResult( + content="", + input_tokens=in_tokens, + output_tokens=0, + input_cost_usd=in_cost, + output_cost_usd=0.0, + total_cost_usd=in_cost, + ), + stage="embed", + model=model, + prompt_digest=f"embed:{text[:80]}", + ) + return [float(v) for v in vector] + except Exception as exc: + logger.warning("Dedicated embedding call failed: %s", exc) + return None + def chat_stream( self, messages: list[dict], @@ -436,7 +490,7 @@ def chat_stream( ) -> Iterator[StreamEvent]: """Stream chat completions with optional tool calling support""" cfg = self._config() - if cfg.provider in ("openai", "zhipu") and cfg.api_key: + if cfg.provider in ("openai", "zhipu", "xiaomi") and cfg.api_key: yield from self._chat_stream_openai_compatible(messages, tools, max_tokens, cfg) elif cfg.provider == "anthropic" and cfg.api_key: yield from self._chat_stream_anthropic_fallback(messages, max_tokens, cfg) @@ -974,6 +1028,17 @@ def _estimate_cost( ("glm-4-flash", 0.01, 0.01), ("glm-4v", 0.14, 0.14), ("glm-4", 0.1, 0.1), + # 小米 MiMo(套餐内 Credits 计费,此处为占位估值,仅用于成本展示) + ("mimo-v2.5-pro", 0.5, 1.5), + ("mimo-v2.5-tts", 0.2, 0.2), + ("mimo-v2.5", 0.3, 0.9), + ("mimo-v2-pro", 0.5, 1.5), + ("mimo-v2-omni", 0.3, 0.9), + ("mimo-v2-tts", 0.2, 0.2), + # 阿里百炼 DashScope embedding(占位估值) + ("text-embedding-v4", 0.05, 0.0), + ("text-embedding-v3", 0.05, 0.0), + ("text-embedding-v2", 0.05, 0.0), ("embedding", 0.005, 0.0), ] in_million = 1.0 diff --git a/scripts/copy_env_from_deploy.sh b/scripts/copy_env_from_deploy.sh index b1340c5..5fdd505 100755 --- a/scripts/copy_env_from_deploy.sh +++ b/scripts/copy_env_from_deploy.sh @@ -59,15 +59,22 @@ echo # Step 4: 验证配置 echo "🔍 验证配置文件..." -if grep -q "ZHIPU_API_KEY=" "$PROJECT_ROOT/.env"; then +if grep -q "XIAOMI_API_KEY=" "$PROJECT_ROOT/.env"; then + api_key=$(grep "XIAOMI_API_KEY=" "$PROJECT_ROOT/.env" | cut -d'=' -f2) + if [ -n "$api_key" ]; then + echo "✅ XIAOMI_API_KEY 已配置" + else + echo "⚠️ XIAOMI_API_KEY 为空,请编辑 .env 填写" + fi +elif grep -q "ZHIPU_API_KEY=" "$PROJECT_ROOT/.env"; then api_key=$(grep "ZHIPU_API_KEY=" "$PROJECT_ROOT/.env" | cut -d'=' -f2) if [ -n "$api_key" ]; then - echo "✅ ZHIPU_API_KEY 已配置" + echo "✅ ZHIPU_API_KEY 已配置(如需切换至小米 MiMo,请改 LLM_PROVIDER=xiaomi 并填 XIAOMI_API_KEY)" else - echo "⚠️ ZHIPU_API_KEY 为空,请编辑 .env 填写" + echo "⚠️ 未配置任何 LLM API Key,请编辑 .env 填写 XIAOMI_API_KEY 或 ZHIPU_API_KEY" fi else - echo "⚠️ 未找到 ZHIPU_API_KEY 配置项" + echo "⚠️ 未找到 LLM API Key 配置项(XIAOMI_API_KEY / ZHIPU_API_KEY)" fi echo diff --git a/scripts/docker_deploy.sh b/scripts/docker_deploy.sh index a49071f..c2901bf 100755 --- a/scripts/docker_deploy.sh +++ b/scripts/docker_deploy.sh @@ -24,7 +24,7 @@ if [ ! -f "$DEPLOY_DIR/.env" ]; then echo "✅ 已创建 $DEPLOY_DIR/.env" echo echo "❗ 请编辑 $DEPLOY_DIR/.env 填写以下配置:" - echo " - ZHIPU_API_KEY (或其他 LLM API Key)" + echo " - XIAOMI_API_KEY (小米 MiMo,默认 provider) 或 ZHIPU_API_KEY/其他 LLM API Key" echo " - SMTP_USER (邮箱地址)" echo " - SMTP_PASSWORD (SMTP 授权码)" echo " - NOTIFY_DEFAULT_TO (接收日报的邮箱)"