[fix][feat] #1108 #1109: 同步 DeepSeek V4 渠道配置 (#1110)

* fix: align DeepSeek V4 channel config

* fix: preserve DeepSeek legacy default

---------

Co-authored-by: zhulinsen <zhuls97@163.com>
This commit is contained in:
Alfred
2026-04-25 18:50:42 +08:00
committed by GitHub
parent 43352cadd5
commit 9a08ba47fa
14 changed files with 351 additions and 53 deletions

View File

@@ -58,6 +58,7 @@ GEMINI_API_KEY=
# 多 Key 负载均衡GEMINI_API_KEYS=key1,key2,key3
# DeepSeekhttps://platform.deepseek.com
# 兼容默认:仅填 DEEPSEEK_API_KEY 时仍使用 deepseek-chat并在日志提示迁移到 deepseek-v4-flash
# DEEPSEEK_API_KEY=
# AIHubmix 聚合https://aihubmix.com/?aff=CfMq
@@ -89,8 +90,8 @@ GEMINI_API_KEY=
# 示例DeepSeek + Gemini 双渠道
# LLM_CHANNELS=deepseek,gemini
# LLM_DEEPSEEK_API_KEY=sk-xxx
# LLM_DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
# LLM_DEEPSEEK_MODELS=deepseek-chat
# LLM_DEEPSEEK_BASE_URL=https://api.deepseek.com
# LLM_DEEPSEEK_MODELS=deepseek-v4-flash,deepseek-v4-pro
# LLM_GEMINI_API_KEYS=key1,key2
# LLM_GEMINI_MODELS=gemini-2.5-flash
#

View File

@@ -24,8 +24,8 @@ const CHANNEL_PRESETS: Record<string, ChannelPreset> = {
deepseek: {
label: 'DeepSeek 官方',
protocol: 'deepseek',
baseUrl: 'https://api.deepseek.com/v1',
placeholder: 'deepseek-chat,deepseek-reasoner',
baseUrl: 'https://api.deepseek.com',
placeholder: 'deepseek-v4-flash,deepseek-v4-pro',
},
dashscope: {
label: '通义千问Dashscope',
@@ -99,8 +99,8 @@ const PROTOCOL_OPTIONS: Array<{ value: ChannelProtocol; label: string }> = [
];
const MODEL_PLACEHOLDERS: Record<ChannelProtocol, string> = {
openai: 'gpt-4o-mini,deepseek-chat,qwen-plus',
deepseek: 'deepseek-chat,deepseek-reasoner',
openai: 'gpt-4o-mini,qwen-plus',
deepseek: 'deepseek-v4-flash,deepseek-v4-pro',
gemini: 'gemini-2.5-flash,gemini-2.5-pro',
anthropic: 'claude-3-5-sonnet-20241022',
vertex_ai: 'gemini-2.5-flash',
@@ -617,6 +617,43 @@ function usesDirectEnvProvider(model: string): boolean {
return Boolean(provider) && !MANAGED_PROVIDERS.has(provider);
}
function isRuntimeModelAvailable(model: string, availableModels: string[]): boolean {
return availableModels.includes(model) || usesDirectEnvProvider(model);
}
function sanitizeRuntimeConfigForSave(runtimeConfig: RuntimeConfig, availableModels: string[]): RuntimeConfig {
if (availableModels.length === 0) {
return runtimeConfig;
}
const primaryModel = runtimeConfig.primaryModel && !isRuntimeModelAvailable(runtimeConfig.primaryModel, availableModels)
? ''
: runtimeConfig.primaryModel;
const agentPrimaryModel = runtimeConfig.agentPrimaryModel && !isRuntimeModelAvailable(runtimeConfig.agentPrimaryModel, availableModels)
? ''
: runtimeConfig.agentPrimaryModel;
const visionModel = runtimeConfig.visionModel && !isRuntimeModelAvailable(runtimeConfig.visionModel, availableModels)
? ''
: runtimeConfig.visionModel;
const fallbackModels = runtimeConfig.fallbackModels.filter((model) => isRuntimeModelAvailable(model, availableModels));
return {
...runtimeConfig,
primaryModel,
agentPrimaryModel,
fallbackModels,
visionModel,
};
}
function runtimeConfigsAreEqual(left: RuntimeConfig, right: RuntimeConfig): boolean {
return left.primaryModel === right.primaryModel
&& left.agentPrimaryModel === right.agentPrimaryModel
&& left.visionModel === right.visionModel
&& left.temperature === right.temperature
&& left.fallbackModels.join(',') === right.fallbackModels.join(',');
}
function resolveTemperatureFromItems(itemMap: Map<string, string>): string {
const unified = itemMap.get('LLM_TEMPERATURE');
if (unified) return unified;
@@ -949,24 +986,31 @@ export const LLMChannelEditor: React.FC<LLMChannelEditorProps> = ({
return;
}
const runtimeConfigForSave = managesRuntimeConfig
? sanitizeRuntimeConfigForSave(runtimeConfig, availableModels)
: runtimeConfig;
if (!runtimeConfigsAreEqual(runtimeConfigForSave, runtimeConfig)) {
setRuntimeConfig(runtimeConfigForSave);
}
if (managesRuntimeConfig && availableModels.length > 0) {
const invalidPrimaryModel = runtimeConfig.primaryModel
&& !availableModels.includes(runtimeConfig.primaryModel)
&& !usesDirectEnvProvider(runtimeConfig.primaryModel);
const invalidPrimaryModel = runtimeConfigForSave.primaryModel
&& !availableModels.includes(runtimeConfigForSave.primaryModel)
&& !usesDirectEnvProvider(runtimeConfigForSave.primaryModel);
if (invalidPrimaryModel) {
setSaveMessage({ type: 'local-error', text: '当前主模型不在已启用渠道的模型列表中,请重新选择。' });
return;
}
const invalidAgentPrimaryModel = runtimeConfig.agentPrimaryModel
&& !availableModels.includes(runtimeConfig.agentPrimaryModel)
&& !usesDirectEnvProvider(runtimeConfig.agentPrimaryModel);
const invalidAgentPrimaryModel = runtimeConfigForSave.agentPrimaryModel
&& !availableModels.includes(runtimeConfigForSave.agentPrimaryModel)
&& !usesDirectEnvProvider(runtimeConfigForSave.agentPrimaryModel);
if (invalidAgentPrimaryModel) {
setSaveMessage({ type: 'local-error', text: '当前 Agent 主模型不在已启用渠道的模型列表中,请重新选择。' });
return;
}
const invalidFallbackModel = runtimeConfig.fallbackModels.some(
const invalidFallbackModel = runtimeConfigForSave.fallbackModels.some(
(model) => !availableModels.includes(model) && !usesDirectEnvProvider(model),
);
if (invalidFallbackModel) {
@@ -974,9 +1018,9 @@ export const LLMChannelEditor: React.FC<LLMChannelEditorProps> = ({
return;
}
const invalidVisionModel = runtimeConfig.visionModel
&& !availableModels.includes(runtimeConfig.visionModel)
&& !usesDirectEnvProvider(runtimeConfig.visionModel);
const invalidVisionModel = runtimeConfigForSave.visionModel
&& !availableModels.includes(runtimeConfigForSave.visionModel)
&& !usesDirectEnvProvider(runtimeConfigForSave.visionModel);
if (invalidVisionModel) {
setSaveMessage({ type: 'local-error', text: '当前 Vision 模型不在已启用渠道的模型列表中,请重新选择。' });
return;
@@ -987,7 +1031,7 @@ export const LLMChannelEditor: React.FC<LLMChannelEditorProps> = ({
setSaveMessage(null);
try {
const updateItems = channelsToUpdateItems(channels, initialNames, runtimeConfig, managesRuntimeConfig);
const updateItems = channelsToUpdateItems(channels, initialNames, runtimeConfigForSave, managesRuntimeConfig);
await systemConfigApi.update({
configVersion,
maskToken,

View File

@@ -103,6 +103,123 @@ describe('LLMChannelEditor', () => {
expect(within(visionModelSelect).getByRole('option', { name: 'minimax/MiniMax-M1' })).toBeInTheDocument();
});
it('uses DeepSeek V4 defaults when adding the official preset', async () => {
render(
<LLMChannelEditor
items={[]}
configVersion="v1"
maskToken="******"
onSaved={() => {}}
/>
);
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'deepseek' } });
fireEvent.click(screen.getByRole('button', { name: '+ 添加渠道' }));
await screen.findByRole('button', { name: /DeepSeek 官方/i });
expect(screen.getByLabelText('Base URL')).toHaveValue('https://api.deepseek.com');
expect(screen.getByLabelText('模型(逗号分隔)')).toHaveValue('deepseek-v4-flash,deepseek-v4-pro');
});
it('sanitizes stale runtime models before saving DeepSeek V4 channel changes', async () => {
update.mockResolvedValue({
success: true,
configVersion: 'v2',
appliedCount: 1,
skippedMaskedCount: 0,
reloadTriggered: true,
updatedKeys: ['LLM_DEEPSEEK_MODELS', 'LITELLM_MODEL'],
warnings: [],
});
render(
<LLMChannelEditor
items={[
{ key: 'LLM_CHANNELS', value: 'deepseek' },
{ key: 'LLM_DEEPSEEK_PROTOCOL', value: 'deepseek' },
{ key: 'LLM_DEEPSEEK_BASE_URL', value: 'https://api.deepseek.com' },
{ key: 'LLM_DEEPSEEK_ENABLED', value: 'true' },
{ key: 'LLM_DEEPSEEK_API_KEY', value: 'sk-test' },
{ key: 'LLM_DEEPSEEK_MODELS', value: 'deepseek-chat,deepseek-reasoner' },
{ key: 'LITELLM_MODEL', value: 'deepseek/deepseek-chat' },
{ key: 'AGENT_LITELLM_MODEL', value: 'deepseek/deepseek-reasoner' },
{ key: 'LITELLM_FALLBACK_MODELS', value: 'deepseek/deepseek-v4-pro,deepseek/deepseek-chat,cohere/command-r-plus' },
{ key: 'VISION_MODEL', value: 'deepseek/deepseek-reasoner' },
]}
configVersion="v1"
maskToken="******"
onSaved={() => {}}
/>
);
fireEvent.click(screen.getByRole('button', { name: /DeepSeek 官方/i }));
fireEvent.change(screen.getByLabelText('模型(逗号分隔)'), {
target: { value: 'deepseek-v4-flash,deepseek-v4-pro' },
});
fireEvent.click(screen.getByRole('button', { name: '保存 AI 配置' }));
await waitFor(() => {
expect(update).toHaveBeenCalled();
});
const updatePayload = update.mock.calls[0][0];
expect(updatePayload.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ key: 'LITELLM_MODEL', value: '' }),
expect.objectContaining({ key: 'AGENT_LITELLM_MODEL', value: '' }),
expect.objectContaining({ key: 'LITELLM_FALLBACK_MODELS', value: 'deepseek/deepseek-v4-pro,cohere/command-r-plus' }),
expect.objectContaining({ key: 'VISION_MODEL', value: '' }),
expect.objectContaining({ key: 'LLM_DEEPSEEK_MODELS', value: 'deepseek-v4-flash,deepseek-v4-pro' }),
]),
);
});
it('keeps direct-env provider runtime models while saving channel changes', async () => {
update.mockResolvedValue({
success: true,
configVersion: 'v2',
appliedCount: 1,
skippedMaskedCount: 0,
reloadTriggered: true,
updatedKeys: ['LLM_DEEPSEEK_BASE_URL'],
warnings: [],
});
render(
<LLMChannelEditor
items={[
{ key: 'LLM_CHANNELS', value: 'deepseek' },
{ key: 'LLM_DEEPSEEK_PROTOCOL', value: 'deepseek' },
{ key: 'LLM_DEEPSEEK_BASE_URL', value: 'https://api.deepseek.com/v1' },
{ key: 'LLM_DEEPSEEK_ENABLED', value: 'true' },
{ key: 'LLM_DEEPSEEK_API_KEY', value: 'sk-test' },
{ key: 'LLM_DEEPSEEK_MODELS', value: 'deepseek-v4-flash' },
{ key: 'LITELLM_MODEL', value: 'cohere/command-r-plus' },
]}
configVersion="v1"
maskToken="******"
onSaved={() => {}}
/>
);
fireEvent.click(screen.getByRole('button', { name: /DeepSeek 官方/i }));
fireEvent.change(screen.getByLabelText('Base URL'), {
target: { value: 'https://api.deepseek.com' },
});
fireEvent.click(screen.getByRole('button', { name: '保存 AI 配置' }));
await waitFor(() => {
expect(update).toHaveBeenCalled();
});
const updatePayload = update.mock.calls[0][0];
expect(updatePayload.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ key: 'LITELLM_MODEL', value: 'cohere/command-r-plus' }),
]),
);
});
it('checks protocol-prefixed selected model when discovery returns bare id', async () => {
discoverLLMChannelModels.mockResolvedValue({
success: true,

View File

@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- [修复] GitHub Actions 每日分析工作流补齐 `LLM_CHANNELS`、多 Key 与常用 `LLM_<NAME>_*` 渠道变量透传避免本地可用的多模型配置在云端定时任务中失效Fixes #1063, #872
- [文档] 修正 `feishu_sender.py` 中飞书自定义机器人 Webhook 消息格式示例为 interactive card JSON并补充飞书自动化 Webhook 触发器配置教程(参数 JSON 与 `card.elements[0].text.content` 字段映射)。
- [修复] 历史报告详情接口修正 `change_pct` 取值:使用 `is None` 判断避免把 0.0(平盘)当作缺失值丢弃,移除错误的 `change_60d` 兜底,并在 `enhanced_context.realtime` 缺涨跌幅时回退到 `realtime_quote_raw.change_pct` / `pct_chg`,避免历史详情页“不显示涨跌幅” (Fixes #1084)
- [修复] DeepSeek 官方渠道预设与示例配置同步到 V4保留 legacy `deepseek-chat` 默认值并增加废弃提示,同时修正模型发现后旧运行时选择导致保存失败的问题 (Fixes #1108, #1109)
- [文档] 优化根 README 结构保留功能特性、技术栈、快速开始、推送效果、Web、Agent、赞助商和新闻源链接入口将细配置、交易纪律和基本面语义收口到完整指南并将 Docker 徽章指向官方镜像页
- [文档] 同步英文与繁中 README 的精简入口结构,并补齐完整指南中的 LLM 用量 API 与持仓管理说明
- [文档] 调整 AI 协作与 PR 模板中的 README 维护规则,明确 README 非必要不更新,细节优先进入专题文档

View File

@@ -209,13 +209,13 @@ PROXY_PORT=10809
```bash
# 不需要配置 GEMINI_API_KEY
OPENAI_API_KEY=sk-xxxxxxxx
OPENAI_BASE_URL=https://api.deepseek.com/v1
OPENAI_MODEL=deepseek-chat
# 思考模式:deepseek-reasoner、deepseek-r1、qwq 等自动识别deepseek-chat 系统按模型名自动启用
OPENAI_BASE_URL=https://api.deepseek.com
OPENAI_MODEL=deepseek-v4-flash
# deepseek-chat / deepseek-reasoner 仍兼容,但官方已标记为 2026/07/24 后废弃
```
支持的模型服务:
- DeepSeek: `https://api.deepseek.com/v1`
- DeepSeek: `https://api.deepseek.com`
- 通义千问: `https://dashscope.aliyuncs.com/compatible-mode/v1`
- Moonshot: `https://api.moonshot.cn/v1`

View File

@@ -207,13 +207,13 @@ Use channel mode: set `LLM_CHANNELS=aihubmix,deepseek,gemini` and configure each
```bash
# No need to configure GEMINI_API_KEY
OPENAI_API_KEY=sk-xxxxxxxx
OPENAI_BASE_URL=https://api.deepseek.com/v1
OPENAI_MODEL=deepseek-chat
# Thinking mode: deepseek-reasoner, deepseek-r1, qwq auto-detected; deepseek-chat enabled by model name
OPENAI_BASE_URL=https://api.deepseek.com
OPENAI_MODEL=deepseek-v4-flash
# deepseek-chat / deepseek-reasoner remain compatible, but DeepSeek marks them deprecated after 2026/07/24
```
Supported model services:
- DeepSeek: `https://api.deepseek.com/v1`
- DeepSeek: `https://api.deepseek.com`
- Qwen (Tongyi Qianwen): `https://dashscope.aliyuncs.com/compatible-mode/v1`
- Moonshot: `https://api.moonshot.cn/v1`

View File

@@ -40,7 +40,8 @@ LITELLM_MODEL=openai/deepseek-ai/DeepSeek-V3
# 填入你在 DeepSeek 官方平台申请的 API Key
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx
```
*提示:仅这一行,系统会自动识别并默认使用 DeepSeek 模型。*
*兼容提示:仅这一行,系统仍会默认使用 `deepseek/deepseek-chat` 并在日志提示迁移。*
`deepseek-chat` / `deepseek-reasoner` 仍可用于兼容旧配置,但 DeepSeek 官方已标记为 2026/07/24 后废弃;新配置建议通过 Web 快速渠道或显式 `LITELLM_MODEL=deepseek/deepseek-v4-flash` 迁移到 `deepseek-v4-flash` / `deepseek-v4-pro`
### 示例 3使用 Gemini 免费 API
```env
@@ -81,9 +82,9 @@ LITELLM_MODEL=ollama/qwen3:8b
LLM_CHANNELS=deepseek,aihubmix
# 2. 渠道一:配置 DeepSeek 官方
LLM_DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
LLM_DEEPSEEK_BASE_URL=https://api.deepseek.com
LLM_DEEPSEEK_API_KEY=sk-1111111111111
LLM_DEEPSEEK_MODELS=deepseek-chat,deepseek-reasoner
LLM_DEEPSEEK_MODELS=deepseek-v4-flash,deepseek-v4-pro
# 3. 渠道二:配置一个常用的聚合中转 API
LLM_AIHUBMIX_BASE_URL=https://api.aihubmix.com/v1
@@ -92,9 +93,9 @@ LLM_AIHUBMIX_MODELS=gpt-4o-mini,claude-3-5-sonnet
# 4. 【关键】指定主模型和备用模型列表
# 平时首选用 deepseek 这款模型:
LITELLM_MODEL=deepseek/deepseek-chat
LITELLM_MODEL=deepseek/deepseek-v4-flash
# 可选Agent 问股单独指定主模型(留空则继承主模型)
AGENT_LITELLM_MODEL=deepseek/deepseek-reasoner
AGENT_LITELLM_MODEL=deepseek/deepseek-v4-pro
# 主模型崩了立刻挨个尝试下面这俩备用模型:
LITELLM_FALLBACK_MODELS=openai/gpt-4o-mini,anthropic/claude-3-5-sonnet
```
@@ -118,6 +119,7 @@ LITELLM_MODEL=ollama/qwen3:8b
- Web 设置页里的主模型、Agent 主模型、Fallback、Vision 下拉会保留这个值原样展示,不会再错误改写成 `openai/minimax/<模型名>`
> **致命避坑说明**:如果你启用了 `LLM_CHANNELS`,那么你直接写在外面的 `DEEPSEEK_API_KEY` 或 `OPENAI_API_KEY` 将**全部失效(系统一律无视)**!二者**选其一即可**,千万不要既写了新手模式又写了渠道模式结果产生冲突。
> **Docker 注意**:如果你在 `docker compose environment:` 或 `docker run -e` 中显式传入 `LITELLM_MODEL`、`LLM_CHANNELS`、`LLM_DEEPSEEK_MODELS` 等变量,容器重启后这些环境变量会覆盖 Web 设置页写入的 `.env`,需要同步修改部署配置。
---
@@ -140,8 +142,8 @@ LITELLM_MODEL=ollama/qwen3:8b
model_list:
- model_name: my-smart-model
litellm_params:
model: openai/deepseek-chat
api_base: https://api.deepseek.com/v1
model: deepseek/deepseek-v4-flash
api_base: https://api.deepseek.com
api_key: "os.environ/MY_CUSTOM_SECRET_KEY" # 从环境变量读取 Key安全防泄漏
# Ollama 本地模型(无需 api_key
@@ -215,4 +217,4 @@ VISION_PROVIDER_PRIORITY=gemini,anthropic,openai
| **转圈转不停,最后报 Timeout / ConnectionRefused 等** | 1. 在国内使用国外原版(像 Google、OpenAI没开代理被墙了。<br>2. 你买的云服务器压根不能出境。 | 非常推荐使用**国内官方**如DeepSeek、阿里或者各种**兼容 OpenAI 的聚合中转接口**。因为中转站把网络问题帮你解决好了。 |
| **Ollama 报 404、`Could not get model info` 或 `api/generate/api/show`** | 误用 `OPENAI_BASE_URL` 配置 Ollama系统会错误拼接 URL | 改用 `OLLAMA_API_BASE=http://localhost:11434` 或渠道模式(`LLM_CHANNELS=ollama` + `LLM_OLLAMA_BASE_URL` |
*进阶老手的叮嘱:如果你开启了 **Agent (深度思考网络搜索问股) 模式**,这里有个经验之谈,推荐选用如 `deepseek-reasoner` 这种自带强悍逻辑推导和思考机制的大模型。如果为了省钱用小微模型跑 Agent它逻辑能力大概率跟不上不仅达不到预期还会白跑一堆空流程。*
*进阶老手的叮嘱:如果你开启了 **Agent (深度思考网络搜索问股) 模式**,这里有个经验之谈,推荐选用如 `deepseek-v4-pro` 这种逻辑推导能力更强的大模型。如果为了省钱用小微模型跑 Agent它逻辑能力大概率跟不上不仅达不到预期还会白跑一堆空流程。*

View File

@@ -40,7 +40,8 @@ LITELLM_MODEL=openai/deepseek-ai/DeepSeek-V3
# Fill in the API Key requested from the official DeepSeek platform
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx
```
*Note: Only this single line is needed. The system will automatically detect and default to the DeepSeek model.*
*Compatibility note: with only this line, the system still defaults to `deepseek/deepseek-chat` and logs a migration warning.*
`deepseek-chat` / `deepseek-reasoner` still work for compatibility with old configs, but DeepSeek marks them deprecated after 2026/07/24. New configs should migrate through the Web quick channel or explicitly set `LITELLM_MODEL=deepseek/deepseek-v4-flash` for `deepseek-v4-flash` / `deepseek-v4-pro`.
### Example 3: Using the Free Gemini API
```env
@@ -81,9 +82,9 @@ If you prefer modifying files, configuring this in the `.env` file is also very
LLM_CHANNELS=deepseek,aihubmix
# 2. Channel 1: Configure Official DeepSeek
LLM_DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
LLM_DEEPSEEK_BASE_URL=https://api.deepseek.com
LLM_DEEPSEEK_API_KEY=sk-1111111111111
LLM_DEEPSEEK_MODELS=deepseek-chat,deepseek-reasoner
LLM_DEEPSEEK_MODELS=deepseek-v4-flash,deepseek-v4-pro
# 3. Channel 2: Configure a common relay/proxy API
LLM_AIHUBMIX_BASE_URL=https://api.aihubmix.com/v1
@@ -92,9 +93,9 @@ LLM_AIHUBMIX_MODELS=gpt-4o-mini,claude-3-5-sonnet
# 4. [Key Step] Specify the primary model and fallback list
# Set your primary model:
LITELLM_MODEL=deepseek/deepseek-chat
LITELLM_MODEL=deepseek/deepseek-v4-flash
# Optional: set an Agent-only primary model (empty = inherit the primary model)
AGENT_LITELLM_MODEL=deepseek/deepseek-reasoner
AGENT_LITELLM_MODEL=deepseek/deepseek-v4-pro
# If the primary model crashes, try these fallbacks sequentially:
LITELLM_FALLBACK_MODELS=openai/gpt-4o-mini,anthropic/claude-3-5-sonnet
```
@@ -118,6 +119,7 @@ LITELLM_MODEL=ollama/qwen3:8b
- The Web settings page now keeps that value unchanged in Primary, Agent Primary, Fallback, and Vision selectors instead of rewriting it to `openai/minimax/<model-name>`.
> **Critical Warning**: If you enable `LLM_CHANNELS`, any standard `DEEPSEEK_API_KEY` or `OPENAI_API_KEY` declared independently will be **completely ignored**. **Use only one mode** to prevent configuration conflicts.
> **Docker note**: If `LITELLM_MODEL`, `LLM_CHANNELS`, `LLM_DEEPSEEK_MODELS`, or related variables are explicitly passed through `docker compose environment:` or `docker run -e`, they will override the `.env` written by the Web settings page after a container restart. Update the deployment environment at the same time.
---
@@ -138,8 +140,8 @@ Example `litellm_config.yaml`:
model_list:
- model_name: my-smart-model
litellm_params:
model: openai/deepseek-chat
api_base: https://api.deepseek.com/v1
model: deepseek/deepseek-v4-flash
api_base: https://api.deepseek.com
api_key: "os.environ/MY_CUSTOM_SECRET_KEY" # Fetch from environment vars for security
# Ollama local model (no api_key needed)
@@ -199,4 +201,4 @@ Afraid you got the config wrong? Type the following commands in your terminal to
| **Spins endlessly, eventually hits Timeout/ConnectionRefused** | You are using restricted APIs (like Google/OpenAI) in a blocked region without a proxy, or your cloud server lacks external internet access. | Highly recommend using **official regional APIs** (like DeepSeek) or **OpenAI-compatible relay platforms**. Third-party platforms bypass these network constraints. |
| **Ollama returns 404, `Could not get model info`, or `api/generate/api/show`** | Using `OPENAI_BASE_URL` for Ollama makes the system concatenate URLs incorrectly | Use `OLLAMA_API_BASE=http://localhost:11434` or channel mode (`LLM_CHANNELS=ollama` + `LLM_OLLAMA_BASE_URL`) instead |
*Veteran's Tip: If you enable **Agent Mode (Deep-thinking & web-search)**, experience shows you should use an advanced reasoning model like `deepseek-reasoner`. Trying to save money by using weak mini-models for agents will likely result in infinite loops or missed objectives.*
*Veteran's Tip: If you enable **Agent Mode (Deep-thinking & web-search)**, experience shows you should use a stronger model like `deepseek-v4-pro`. Trying to save money by using weak mini-models for agents will likely result in infinite loops or missed objectives.*

View File

@@ -59,8 +59,8 @@ daily_stock_analysis/
|------------|------|:----:|
| `GEMINI_API_KEY` | [Google AI Studio](https://aistudio.google.com/) 获取免费 Key | ✅* |
| `OPENAI_API_KEY` | OpenAI 兼容 API Key支持 DeepSeek、通义千问等 | 可选 |
| `OPENAI_BASE_URL` | OpenAI 兼容 API 地址(如 `https://api.deepseek.com/v1` | 可选 |
| `OPENAI_MODEL` | 模型名称(如 `gemini-3.1-pro-preview``deepseek-chat``gpt-5.2` | 可选 |
| `OPENAI_BASE_URL` | OpenAI 兼容 API 地址(如 `https://api.deepseek.com` | 可选 |
| `OPENAI_MODEL` | 模型名称(如 `gemini-3.1-pro-preview``deepseek-v4-flash``gpt-5.2` | 可选 |
> *注:`GEMINI_API_KEY` 和 `OPENAI_API_KEY` 至少配置一个
@@ -894,9 +894,9 @@ GEMINI_MODEL=gemini-3-flash-preview
# OpenAI 兼容(备选)
OPENAI_API_KEY=xxx
OPENAI_BASE_URL=https://api.deepseek.com/v1
OPENAI_MODEL=deepseek-chat
# 思考模式:deepseek-reasoner、deepseek-r1、qwq 等自动识别deepseek-chat 系统按模型名自动启用
OPENAI_BASE_URL=https://api.deepseek.com
OPENAI_MODEL=deepseek-v4-flash
# deepseek-chat / deepseek-reasoner 仍兼容,但官方已标记为 2026/07/24 后废弃
```
### 高级模型路由(底层由 LiteLLM 驱动)

View File

@@ -59,8 +59,8 @@ Go to your forked repo → `Settings` → `Secrets and variables` → `Actions`
|------------|------|:----:|
| `GEMINI_API_KEY` | Get free key from [Google AI Studio](https://aistudio.google.com/) | ✅* |
| `OPENAI_API_KEY` | OpenAI-compatible API Key (supports DeepSeek, Qwen, etc.) | Optional |
| `OPENAI_BASE_URL` | OpenAI-compatible API endpoint (e.g., `https://api.deepseek.com/v1`) | Optional |
| `OPENAI_MODEL` | Model name (e.g., `deepseek-chat`) | Optional |
| `OPENAI_BASE_URL` | OpenAI-compatible API endpoint (e.g., `https://api.deepseek.com`) | Optional |
| `OPENAI_MODEL` | Model name (e.g., `deepseek-v4-flash`) | Optional |
> *Note: Configure at least one of `GEMINI_API_KEY` or `OPENAI_API_KEY`
@@ -750,9 +750,9 @@ GEMINI_MODEL=gemini-3-flash-preview
# OpenAI compatible (backup)
OPENAI_API_KEY=xxx
OPENAI_BASE_URL=https://api.deepseek.com/v1
OPENAI_MODEL=deepseek-chat
# Thinking mode: deepseek-reasoner, deepseek-r1, qwq auto-detected; deepseek-chat enabled by model name
OPENAI_BASE_URL=https://api.deepseek.com
OPENAI_MODEL=deepseek-v4-flash
# deepseek-chat / deepseek-reasoner remain compatible, but DeepSeek marks them deprecated after 2026/07/24
```
### Advanced Model Routing (Powered by LiteLLM)

View File

@@ -934,6 +934,7 @@ class Config:
# LITELLM_MODEL: explicit config takes precedence; else infer from available keys
litellm_model = os.getenv('LITELLM_MODEL', '').strip()
inferred_legacy_deepseek_model = False
if not litellm_model:
_gemini_model_name = os.getenv('GEMINI_MODEL', 'gemini-3-flash-preview').strip()
_anthropic_model_name = os.getenv('ANTHROPIC_MODEL', 'claude-3-5-sonnet-20241022').strip()
@@ -944,6 +945,7 @@ class Config:
litellm_model = f'anthropic/{_anthropic_model_name}'
elif deepseek_api_keys:
litellm_model = 'deepseek/deepseek-chat'
inferred_legacy_deepseek_model = True
elif openai_api_keys:
# For openai-compatible models, add prefix only if not already prefixed
if '/' not in _openai_model_name:
@@ -997,6 +999,17 @@ class Config:
if llm_model_list:
llm_models_source = "legacy_env"
if (
inferred_legacy_deepseek_model
and llm_models_source == "legacy_env"
and litellm_model == 'deepseek/deepseek-chat'
):
logger.warning(
"Deprecation warning:\n"
"deepseek-chat will be deprecated on 2026-07-24,\n"
"please migrate to deepseek-v4-flash."
)
# Auto-infer LITELLM_MODEL from channels when not explicitly set
if not litellm_model and llm_channels:
for _ch in llm_channels:

View File

@@ -85,7 +85,7 @@ _FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = {
# ------------------------------------------------------------------
"LITELLM_MODEL": {
"title": "Primary Model",
"description": "Primary model in provider/model format (e.g. gemini/gemini-3-flash-preview, openai/deepseek-chat, anthropic/claude-3-5-sonnet-20241022). If empty, it is auto-inferred from available API keys or channel declarations.",
"description": "Primary model in provider/model format (e.g. gemini/gemini-3-flash-preview, deepseek/deepseek-v4-flash, anthropic/claude-3-5-sonnet-20241022). If empty, it is auto-inferred from available API keys or channel declarations.",
"category": "ai_model",
"data_type": "string",
"ui_control": "text",
@@ -189,7 +189,7 @@ _FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = {
# ------------------------------------------------------------------
"DEEPSEEK_API_KEY": {
"title": "DeepSeek API Key",
"description": "Official DeepSeek API key (from https://platform.deepseek.com). Auto-infers openai/deepseek-chat when set alone. Also works in multi-channel mode.",
"description": "Official DeepSeek API key (from https://platform.deepseek.com). For compatibility, a key set alone still auto-infers deepseek/deepseek-chat and logs a deprecation warning; new configs should migrate to deepseek/deepseek-v4-flash. Also works in multi-channel mode.",
"category": "ai_model",
"data_type": "string",
"ui_control": "password",

View File

@@ -134,6 +134,104 @@ class LLMChannelConfigTestCase(unittest.TestCase):
self.assertEqual(config.litellm_model, "gemini/gemini-3-flash-preview")
self.assertAlmostEqual(config.llm_temperature, 0.15)
@patch("src.config.setup_env")
@patch.object(Config, "_parse_litellm_yaml", return_value=[])
@patch("src.config.logger.warning")
def test_deepseek_key_defaults_to_legacy_chat_model_with_deprecation_warning(
self,
mock_warning,
_mock_parse_yaml,
_mock_setup_env,
) -> None:
env = {
"DEEPSEEK_API_KEY": "sk-test-value",
}
with patch.dict(os.environ, env, clear=True):
config = Config._load_from_env()
self.assertEqual(config.litellm_model, "deepseek/deepseek-chat")
mock_warning.assert_called_once_with(
"Deprecation warning:\n"
"deepseek-chat will be deprecated on 2026-07-24,\n"
"please migrate to deepseek-v4-flash."
)
@patch("src.config.setup_env")
@patch.object(Config, "_parse_litellm_yaml", return_value=[])
@patch("src.config.logger.warning")
def test_explicit_deepseek_litellm_model_is_preserved(
self,
mock_warning,
_mock_parse_yaml,
_mock_setup_env,
) -> None:
env = {
"DEEPSEEK_API_KEY": "sk-test-value",
"LITELLM_MODEL": "deepseek/deepseek-chat",
}
with patch.dict(os.environ, env, clear=True):
config = Config._load_from_env()
self.assertEqual(config.litellm_model, "deepseek/deepseek-chat")
mock_warning.assert_not_called()
@patch("src.config.setup_env")
@patch.object(Config, "_parse_litellm_yaml", return_value=[])
@patch("src.config.logger.warning")
def test_deepseek_key_does_not_warn_when_channels_take_precedence(
self,
mock_warning,
_mock_parse_yaml,
_mock_setup_env,
) -> None:
env = {
"DEEPSEEK_API_KEY": "sk-test-value",
"LLM_CHANNELS": "primary",
"LLM_PRIMARY_PROTOCOL": "deepseek",
"LLM_PRIMARY_API_KEY": "sk-channel-value",
"LLM_PRIMARY_MODELS": "deepseek-v4-flash",
}
with patch.dict(os.environ, env, clear=True):
config = Config._load_from_env()
self.assertEqual(config.llm_models_source, "llm_channels")
mock_warning.assert_not_called()
@patch("src.config.setup_env")
@patch.object(
Config,
"_parse_litellm_yaml",
return_value=[
{
"model_name": "primary",
"litellm_params": {
"model": "deepseek/deepseek-v4-flash",
"api_key": "sk-yaml-value",
},
}
],
)
@patch("src.config.logger.warning")
def test_deepseek_key_does_not_warn_when_litellm_yaml_takes_precedence(
self,
mock_warning,
_mock_parse_yaml,
_mock_setup_env,
) -> None:
env = {
"DEEPSEEK_API_KEY": "sk-test-value",
"LITELLM_CONFIG": "/tmp/litellm.yaml",
}
with patch.dict(os.environ, env, clear=True):
config = Config._load_from_env()
self.assertEqual(config.llm_models_source, "litellm_config")
mock_warning.assert_not_called()
@patch("src.config.setup_env")
@patch.object(Config, "_parse_litellm_yaml", return_value=[])
def test_llm_temperature_prefers_unified_setting_when_present(self, _mock_parse_yaml, _mock_setup_env) -> None:

View File

@@ -292,6 +292,21 @@ class SystemConfigServiceTestCase(unittest.TestCase):
self.assertFalse(validation["valid"])
self.assertTrue(any(issue["key"] == "LITELLM_MODEL" and issue["code"] == "unknown_model" for issue in validation["issues"]))
def test_validate_accepts_deepseek_v4_primary_model_for_channel(self) -> None:
validation = self.service.validate(
items=[
{"key": "LLM_CHANNELS", "value": "deepseek"},
{"key": "LLM_DEEPSEEK_PROTOCOL", "value": "deepseek"},
{"key": "LLM_DEEPSEEK_BASE_URL", "value": "https://api.deepseek.com"},
{"key": "LLM_DEEPSEEK_API_KEY", "value": "sk-test-value"},
{"key": "LLM_DEEPSEEK_MODELS", "value": "deepseek-v4-flash,deepseek-v4-pro"},
{"key": "LITELLM_MODEL", "value": "deepseek/deepseek-v4-flash"},
]
)
self.assertTrue(validation["valid"], validation["issues"])
self.assertEqual(validation["issues"], [])
def test_validate_reports_unknown_agent_primary_model_for_channels(self) -> None:
validation = self.service.validate(
items=[
@@ -702,6 +717,11 @@ class SystemConfigServiceTestCase(unittest.TestCase):
self.assertEqual(models_url, "https://example.com/v1/models")
def test_build_llm_models_url_supports_deepseek_root_base_url(self) -> None:
models_url = SystemConfigService._build_llm_models_url("https://api.deepseek.com")
self.assertEqual(models_url, "https://api.deepseek.com/models")
def test_validate_reports_invalid_event_rule_semantics(self) -> None:
validation = self.service.validate(items=[{
"key": "AGENT_EVENT_ALERT_RULES_JSON",