Files
llm-proxy/config/settings.py
T
hz4th_coder 904a4d0d6c v2.1.2: auto调用全部失败时邮件通知 + 网页配置邮箱
- 新增邮件通知: auto 配置被调用后所有提供商都失败(含无可用提供商)时自动发邮件
- 通知路径: chat_completions 400/503 + 通用能力端点(生图/语音/视频) + ASR端点
- 防轰炸: cooldown_seconds(默认300s)最小间隔, 连续失败只发一封(实测3次失败1封)
- 网页配置: 系统配置页新增「邮件通知」卡片, 支持 SMTP服务器/端口/加密(plain/starttls/ssl)/账号密码/发件人/收件人/间隔 + 发送测试邮件按钮
- API: GET/PUT /api/admin/email(密码掩码), POST /api/admin/email/test
- 默认SMTP: mail.tphai.com:587 plain, 收件人 wlq@tphai.com
- 修复: config页loadEmailConfig竞态(卡片未渲染就填充); 邮件失败不影响API响应
2026-08-28 13:17:31 +08:00

447 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
大模型API中转系统配置 - 支持动态修改
v2.1.0 - 能力(Capability)体系: 每个模型标记能力,AUTO配置按能力绑定
能力类型:
text 文本推理
vision 视觉能力 (多模态输入)
audio_out 语音输出 (TTS)
audio_in 语音输入 (ASR)
image_gen 图片生成
video_gen 视频生成
"""
import json
from pathlib import Path
# 配置文件路径
CONFIG_FILE = Path(__file__).parent.parent / 'data' / 'config.json'
# 能力定义
CAPABILITY_DEFS = {
"text": "文本推理",
"vision": "视觉能力",
"audio_out": "语音输出",
"audio_in": "语音输入",
"image_gen": "图片生成",
"video_gen": "视频生成",
}
# 默认上游模型配置
# capabilities: 该提供商下所有模型的默认能力(模型管理页可对单个模型覆盖)
DEFAULT_PROVIDERS = [
{
"id": "local-qwen",
"name": "Local Qwen",
"priority": 1,
"base_url": "http://121.40.164.32:18003/v1",
"api_key": "sk-xxxx",
"models": [
{"name": "unsloth/Qwen3.8-27B-Q6_K", "capabilities": ["text", "vision"]},
{"name": "unsloth/Qwen3.8-27B-Q4_K_M", "capabilities": ["text", "vision"]},
],
"default_model": "unsloth/Qwen3.8-27B-Q6_K",
"capabilities": ["text", "vision"],
"timeout": 180,
"enabled": True,
},
{
"id": "siliconflow-llm",
"name": "SiliconFlow LLM",
"priority": 2,
"base_url": "https://api.siliconflow.cn/v1",
"api_key": "sk-fhpoexpptvjghpnphtaxbkhjwulzovoqfffbckcfscjmwhcg",
"models": [
{"name": "deepseek-ai/DeepSeek-V4-Flash", "capabilities": ["text"]},
{"name": "meituan-longcat/LongCat-2.0", "capabilities": ["text"]},
],
"default_model": "deepseek-ai/DeepSeek-V4-Flash",
"capabilities": ["text"],
"timeout": 180,
"enabled": True,
},
{
"id": "autodl",
"name": "Autodl",
"priority": 3,
"base_url": "https://www.autodl.art/api/v1",
"api_key": "F9MBfolzuapqTsD4KmUf9qen720rXvUZ3Sp3IrWiCTukqonx",
"models": [
{"name": "qwen3.6-plus", "capabilities": ["text", "vision"]},
{"name": "GLM-5.3-flash", "capabilities": ["text", "vision"]},
],
"default_model": "GLM-5.3-flash",
"capabilities": ["text", "vision"],
"timeout": 180,
"enabled": True,
},
{
"id": "autodl-image",
"name": "Autodl Image",
"priority": 4,
"base_url": "https://www.autodl.art/api/v1",
"api_key": "F9MBfolzuapqTsD4KmUf9qen720rXvUZ3Sp3IrWiCTukqonx",
"models": [
{"name": "Qwen-Image", "capabilities": ["image_gen"]},
],
"default_model": "Qwen-Image",
"capabilities": ["image_gen"],
"timeout": 180,
"enabled": True,
},
]
# 默认模型别名
DEFAULT_MODEL_ALIASES = {
"auto": "auto",
"auto-text": "auto-text",
"auto-vision": "auto-vision",
"auto-image": "auto-image",
"auto-voice-out": "auto-voice-out",
"auto-voice-in": "auto-voice-in",
"auto-video": "auto-video",
"auto-vlm": "auto-vision", # 兼容旧配置
"qwen": "unsloth/Qwen3.8-27B-Q6_K",
"qwen3.8": "unsloth/Qwen3.8-27B-Q6_K",
"local": "unsloth/Qwen3.8-27B-Q6_K",
"deepseek": "deepseek-ai/DeepSeek-V4-Flash",
"deepseek-v4-flash": "deepseek-ai/DeepSeek-V4-Flash",
"longcat": "meituan-longcat/LongCat-2.0",
"glm": "GLM-5.3-flash",
"qwen3.6-plus": "qwen3.6-plus",
"gpt-4": "GLM-5.3-flash",
}
# 默认Auto配置:每个auto配置固定绑定一个能力
# capability: 固定此auto的功能类型(取自 CAPABILITY_DEFS
# models: 有序的**具体模型**列表(按优先级,可拖动排序);为空时回退为按提供商能力选择
# providers: 候选提供商(* 表示所有启用的、且具备该能力模型的提供商)
DEFAULT_AUTO_PROFILES = {
"auto": {
"name": "默认Auto",
"description": "文本推理 - 按模型列表顺序自动选择",
"capability": "text",
"models": [
"unsloth/Qwen3.8-27B-Q6_K",
"unsloth/Qwen3.8-27B-Q4_K_M",
"deepseek-ai/DeepSeek-V4-Flash",
"meituan-longcat/LongCat-2.0",
"qwen3.6-plus",
"GLM-5.3-flash",
],
"providers": ["*"],
"strategy": "priority",
},
"auto-text": {
"name": "文本推理",
"description": "纯文本推理,按模型列表顺序自动选择",
"capability": "text",
"models": [
"unsloth/Qwen3.8-27B-Q6_K",
"unsloth/Qwen3.8-27B-Q4_K_M",
"deepseek-ai/DeepSeek-V4-Flash",
"meituan-longcat/LongCat-2.0",
"qwen3.6-plus",
"GLM-5.3-flash",
],
"providers": ["*"],
"strategy": "priority",
},
"auto-vision": {
"name": "视觉能力",
"description": "多模态视觉理解,按模型列表顺序自动选择",
"capability": "vision",
"models": [
"unsloth/Qwen3.8-27B-Q6_K",
"unsloth/Qwen3.8-27B-Q4_K_M",
"qwen3.6-plus",
"GLM-5.3-flash",
],
"providers": ["*"],
"strategy": "priority",
},
"auto-image": {
"name": "图片生成",
"description": "文生图,按模型列表顺序自动选择",
"capability": "image_gen",
"models": ["Qwen-Image"],
"providers": ["*"],
"strategy": "priority",
},
"auto-voice-out": {
"name": "语音输出",
"description": "语音合成(TTS),按模型列表顺序自动选择",
"capability": "audio_out",
"models": [],
"providers": ["*"],
"strategy": "priority",
},
"auto-voice-in": {
"name": "语音输入",
"description": "语音识别(ASR),按模型列表顺序自动选择",
"capability": "audio_in",
"models": [],
"providers": ["*"],
"strategy": "priority",
},
"auto-video": {
"name": "视频生成",
"description": "文生视频,按模型列表顺序自动选择",
"capability": "video_gen",
"models": [],
"providers": ["*"],
"strategy": "priority",
},
}
# 路由缓存配置(历史上下文优先复用上次模型,命中前缀缓存节省成本)
ROUTING_CONFIG = {
"prefer_cache_model": True, # 开启后:带历史上下文的 auto 请求优先复用上次使用的模型
"cache_ttl_seconds": 3600, # 记忆有效期(秒)
}
# 邮件通知配置(auto 调用全部失败时发邮件通知)
EMAIL_CONFIG = {
"enabled": True, # 总开关
"notify_on_auto_failure": True, # auto 调用全部失败时发邮件
"smtp_host": "mail.tphai.com",
"smtp_port": 587,
"smtp_mode": "plain", # plain | starttls | ssl
"smtp_user": "hz4th_coder@tphai.com",
"smtp_password": "hz4th_coder@!",
"from_name": "LLM Proxy",
"from_addr": "hz4th_coder@tphai.com",
"to_addrs": ["wlq@tphai.com"],
"cooldown_seconds": 300, # 失败通知最小间隔(防轰炸)
}
def load_email_config():
"""加载邮件通知配置(运行时配置优先)"""
config = load_config()
return {**EMAIL_CONFIG, **config.get("email_config", {})}
def save_email_config(data):
"""保存邮件通知配置"""
config = load_config()
merged = {**EMAIL_CONFIG, **data}
config["email_config"] = merged
save_config(config)
return merged
def load_routing_config():
"""加载路由缓存配置(运行时配置优先)"""
config = load_config()
return {**ROUTING_CONFIG, **config.get("routing_config", {})}
def save_routing_config(data):
"""保存路由缓存配置"""
config = load_config()
config["routing_config"] = {**ROUTING_CONFIG, **data}
save_config(config)
return config["routing_config"]
def load_config():
"""加载配置"""
if CONFIG_FILE.exists():
try:
data = json.loads(CONFIG_FILE.read_text(encoding='utf-8'))
return data
except:
pass
return {
"providers": DEFAULT_PROVIDERS,
"model_aliases": DEFAULT_MODEL_ALIASES,
}
def save_config(config):
"""保存配置"""
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
CONFIG_FILE.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding='utf-8')
def get_providers():
"""获取提供商列表"""
config = load_config()
return config.get("providers", DEFAULT_PROVIDERS)
def get_provider(provider_id):
"""获取单个提供商"""
providers = get_providers()
for p in providers:
if p.get("id") == provider_id:
return p
return None
def add_provider(provider):
"""添加提供商"""
config = load_config()
providers = config.get("providers", [])
# 生成ID
if not provider.get("id"):
provider["id"] = provider["name"].lower().replace(" ", "-").replace(".", "-")
providers.append(provider)
config["providers"] = providers
save_config(config)
return provider
def update_provider(provider_id, data):
"""更新提供商"""
config = load_config()
providers = config.get("providers", [])
for i, p in enumerate(providers):
if p.get("id") == provider_id:
providers[i] = {**p, **data}
config["providers"] = providers
save_config(config)
return providers[i]
return None
def delete_provider(provider_id):
"""删除提供商"""
config = load_config()
providers = config.get("providers", [])
providers = [p for p in providers if p.get("id") != provider_id]
config["providers"] = providers
save_config(config)
return True
def update_priority(provider_ids):
"""更新优先级顺序"""
config = load_config()
providers = config.get("providers", [])
# 按新顺序设置优先级
for i, pid in enumerate(provider_ids):
for p in providers:
if p.get("id") == pid:
p["priority"] = i + 1
config["providers"] = providers
save_config(config)
return providers
def get_model_aliases():
"""获取模型别名"""
config = load_config()
return config.get("model_aliases", DEFAULT_MODEL_ALIASES)
def update_model_alias(alias, target):
"""更新模型别名"""
config = load_config()
aliases = config.get("model_aliases", {})
aliases[alias] = target
config["model_aliases"] = aliases
save_config(config)
return aliases
def delete_model_alias(alias):
"""删除模型别名"""
if alias == "auto":
return False # 不能删除默认的auto
config = load_config()
aliases = config.get("model_aliases", {})
if alias in aliases:
del aliases[alias]
config["model_aliases"] = aliases
save_config(config)
return True
return False
def get_auto_profiles():
"""获取Auto配置列表"""
config = load_config()
return config.get("auto_profiles", DEFAULT_AUTO_PROFILES)
def get_auto_profile(profile_name):
"""获取单个Auto配置"""
profiles = get_auto_profiles()
return profiles.get(profile_name)
def add_auto_profile(profile_name, profile_data):
"""添加Auto配置"""
config = load_config()
profiles = config.get("auto_profiles", {})
profiles[profile_name] = profile_data
config["auto_profiles"] = profiles
save_config(config)
return profiles
def update_auto_profile(profile_name, profile_data):
"""更新Auto配置"""
config = load_config()
profiles = config.get("auto_profiles", {})
if profile_name in profiles:
profiles[profile_name] = {**profiles[profile_name], **profile_data}
config["auto_profiles"] = profiles
save_config(config)
return profiles[profile_name]
return None
def delete_auto_profile(profile_name):
"""删除Auto配置"""
if profile_name == "auto":
return False # 不能删除默认的auto
config = load_config()
profiles = config.get("auto_profiles", {})
if profile_name in profiles:
del profiles[profile_name]
config["auto_profiles"] = profiles
save_config(config)
return True
return False
# 初始化配置
config = load_config()
UPSTREAM_PROVIDERS = config.get("providers", DEFAULT_PROVIDERS)
MODEL_ALIASES = config.get("model_aliases", DEFAULT_MODEL_ALIASES)
# 服务配置
SERVER_CONFIG = {
"host": "0.0.0.0",
"port": 16003,
"debug": False,
}
# 日志配置
LOG_CONFIG = {
"log_dir": "logs",
"log_requests": True,
"log_errors": True,
}
# 重试/熔断配置
RETRY_CONFIG = {
"max_retries": 3,
"retry_delay": 1,
"cooldown_seconds": 60, # 熔断冷却期,到期自动恢复(半开)
"retry_on_errors": [
"connection_error",
"timeout",
"rate_limit",
"server_error",
],
}