Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,37 @@ 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=

# 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

# ==========================================
# 成本管控
Expand Down
2 changes: 1 addition & 1 deletion apps/api/routers/llm_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="粗读/简单任务模型")
Expand Down
27 changes: 20 additions & 7 deletions frontend/src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ const PROVIDER_PRESETS: Record<
string,
{ label: string; base_url: string; models: Partial<LLMProviderCreate> }
> = {
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/",
Expand Down Expand Up @@ -334,13 +345,15 @@ function LLMTab() {

function ProviderBadge({ provider }: { provider: string }) {
const colors: Record<string, string> = {
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<string, string> = {
xiaomi: "小米 MiMo",
zhipu: "智谱",
openai: "OpenAI",
anthropic: "Anthropic",
Expand All @@ -364,14 +377,14 @@ function AddConfigInline({
}) {
const [form, setForm] = useState<LLMProviderCreate>({
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);
Expand Down
21 changes: 14 additions & 7 deletions frontend/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ const NAV_ITEMS: { key: SettingsTab; label: string; icon: typeof Cpu }[] = [
];

const PROVIDER_PRESETS: Record<string, { label: string; base_url: string; models: Record<string, string> }> = {
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/",
Expand Down Expand Up @@ -125,11 +130,13 @@ export default function SettingsPage() {
/* ======== LLM 设置 ======== */
function ProviderBadge({ provider }: { provider: string }) {
const colors: Record<string, string> = {
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<string, string> = {
xiaomi: "小米 MiMo",
zhipu: "智谱",
openai: "OpenAI",
anthropic: "Anthropic",
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 12 additions & 7 deletions packages/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/domain/model_tier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", # 视觉专用:支持多模态
},
}
2 changes: 1 addition & 1 deletion packages/domain/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 71 additions & 6 deletions packages/integrations/llm_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
LLM 提供者抽象层 - OpenAI / Anthropic / ZhipuAI / Pseudo
LLM 提供者抽象层 - OpenAI / Anthropic / ZhipuAI / Xiaomi MiMo / Pseudo
支持从数据库动态加载激活的 LLM 配置
@author Color2333
"""
Expand All @@ -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

Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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": "",
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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],
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions scripts/copy_env_from_deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading