mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* fix: align DeepSeek V4 channel config * fix: preserve DeepSeek legacy default --------- Co-authored-by: zhulinsen <zhuls97@163.com>
This commit is contained in:
@@ -58,6 +58,7 @@ GEMINI_API_KEY=
|
||||
# 多 Key 负载均衡:GEMINI_API_KEYS=key1,key2,key3
|
||||
|
||||
# DeepSeek(https://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
|
||||
#
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 非必要不更新,细节优先进入专题文档
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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,它逻辑能力大概率跟不上,不仅达不到预期,还会白跑一堆空流程。*
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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 驱动)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user