mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* chore: 补齐 LLM Actions 映射与模板校准 P1 (#1180) (#1186) * chore: align LLM channel Actions mapping (#1180) * docs: prefer validated OpenRouter default (#1180) * fix(review-feedback-1190): address latest review comments --------- Co-authored-by: Alfred <massif0601@gmail.com>
This commit is contained in:
26
.env.example
26
.env.example
@@ -117,6 +117,14 @@ GEMINI_API_KEY=
|
||||
#
|
||||
# 常用厂商模板(任选其一复制到 .env;完整说明见 docs/llm-providers.md)
|
||||
#
|
||||
# AIHubmix 聚合平台(OpenAI Compatible)
|
||||
# LLM_CHANNELS=aihubmix
|
||||
# LLM_AIHUBMIX_PROTOCOL=openai
|
||||
# LLM_AIHUBMIX_BASE_URL=https://aihubmix.com/v1
|
||||
# LLM_AIHUBMIX_API_KEY=sk-xxx
|
||||
# LLM_AIHUBMIX_MODELS=gpt-5.5,claude-sonnet-4-6,gemini-3.1-pro-preview
|
||||
# LITELLM_MODEL=openai/gpt-5.5
|
||||
#
|
||||
# OpenAI 官方
|
||||
# LLM_CHANNELS=openai
|
||||
# LLM_OPENAI_PROTOCOL=openai
|
||||
@@ -190,6 +198,24 @@ GEMINI_API_KEY=
|
||||
# LLM_VOLCENGINE_API_KEY=xxx
|
||||
# LLM_VOLCENGINE_MODELS=doubao-seed-1-6-251015,doubao-seed-1-6-thinking-251015
|
||||
# LITELLM_MODEL=openai/doubao-seed-1-6-251015
|
||||
#
|
||||
# 硅基流动 SiliconFlow(OpenAI Compatible)
|
||||
# 来源:官方模型列表 https://docs.siliconflow.cn/quickstart/models
|
||||
# LLM_CHANNELS=siliconflow
|
||||
# LLM_SILICONFLOW_PROTOCOL=openai
|
||||
# LLM_SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1
|
||||
# LLM_SILICONFLOW_API_KEY=sk-xxx
|
||||
# LLM_SILICONFLOW_MODELS=deepseek-ai/DeepSeek-V3.2,Qwen/Qwen3-235B-A22B-Thinking-2507
|
||||
# LITELLM_MODEL=openai/deepseek-ai/DeepSeek-V3.2
|
||||
#
|
||||
# OpenRouter(OpenAI Compatible)
|
||||
# 来源:官方 Models API https://openrouter.ai/docs/api/api-reference/models/get-models
|
||||
# LLM_CHANNELS=openrouter
|
||||
# LLM_OPENROUTER_PROTOCOL=openai
|
||||
# LLM_OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||
# LLM_OPENROUTER_API_KEY=sk-or-xxx
|
||||
# LLM_OPENROUTER_MODELS=~anthropic/claude-sonnet-latest,~openai/gpt-latest
|
||||
# LITELLM_MODEL=openai/~anthropic/claude-sonnet-latest
|
||||
|
||||
# 高级:模型路由 YAML 配置(可选,参考 litellm_config.example.yaml)
|
||||
# LITELLM_CONFIG=./litellm_config.yaml
|
||||
|
||||
48
.github/workflows/daily_analysis.yml
vendored
48
.github/workflows/daily_analysis.yml
vendored
@@ -164,6 +164,54 @@ jobs:
|
||||
LLM_MOONSHOT_ENABLED: ${{ vars.LLM_MOONSHOT_ENABLED || secrets.LLM_MOONSHOT_ENABLED }}
|
||||
LLM_MOONSHOT_EXTRA_HEADERS: ${{ vars.LLM_MOONSHOT_EXTRA_HEADERS || secrets.LLM_MOONSHOT_EXTRA_HEADERS }}
|
||||
|
||||
LLM_DASHSCOPE_PROTOCOL: ${{ vars.LLM_DASHSCOPE_PROTOCOL || secrets.LLM_DASHSCOPE_PROTOCOL }}
|
||||
LLM_DASHSCOPE_BASE_URL: ${{ vars.LLM_DASHSCOPE_BASE_URL || secrets.LLM_DASHSCOPE_BASE_URL }}
|
||||
LLM_DASHSCOPE_API_KEY: ${{ secrets.LLM_DASHSCOPE_API_KEY }}
|
||||
LLM_DASHSCOPE_API_KEYS: ${{ secrets.LLM_DASHSCOPE_API_KEYS }}
|
||||
LLM_DASHSCOPE_MODELS: ${{ vars.LLM_DASHSCOPE_MODELS || secrets.LLM_DASHSCOPE_MODELS }}
|
||||
LLM_DASHSCOPE_ENABLED: ${{ vars.LLM_DASHSCOPE_ENABLED || secrets.LLM_DASHSCOPE_ENABLED }}
|
||||
LLM_DASHSCOPE_EXTRA_HEADERS: ${{ vars.LLM_DASHSCOPE_EXTRA_HEADERS || secrets.LLM_DASHSCOPE_EXTRA_HEADERS }}
|
||||
|
||||
LLM_ZHIPU_PROTOCOL: ${{ vars.LLM_ZHIPU_PROTOCOL || secrets.LLM_ZHIPU_PROTOCOL }}
|
||||
LLM_ZHIPU_BASE_URL: ${{ vars.LLM_ZHIPU_BASE_URL || secrets.LLM_ZHIPU_BASE_URL }}
|
||||
LLM_ZHIPU_API_KEY: ${{ secrets.LLM_ZHIPU_API_KEY }}
|
||||
LLM_ZHIPU_API_KEYS: ${{ secrets.LLM_ZHIPU_API_KEYS }}
|
||||
LLM_ZHIPU_MODELS: ${{ vars.LLM_ZHIPU_MODELS || secrets.LLM_ZHIPU_MODELS }}
|
||||
LLM_ZHIPU_ENABLED: ${{ vars.LLM_ZHIPU_ENABLED || secrets.LLM_ZHIPU_ENABLED }}
|
||||
LLM_ZHIPU_EXTRA_HEADERS: ${{ vars.LLM_ZHIPU_EXTRA_HEADERS || secrets.LLM_ZHIPU_EXTRA_HEADERS }}
|
||||
|
||||
LLM_MINIMAX_PROTOCOL: ${{ vars.LLM_MINIMAX_PROTOCOL || secrets.LLM_MINIMAX_PROTOCOL }}
|
||||
LLM_MINIMAX_BASE_URL: ${{ vars.LLM_MINIMAX_BASE_URL || secrets.LLM_MINIMAX_BASE_URL }}
|
||||
LLM_MINIMAX_API_KEY: ${{ secrets.LLM_MINIMAX_API_KEY }}
|
||||
LLM_MINIMAX_API_KEYS: ${{ secrets.LLM_MINIMAX_API_KEYS }}
|
||||
LLM_MINIMAX_MODELS: ${{ vars.LLM_MINIMAX_MODELS || secrets.LLM_MINIMAX_MODELS }}
|
||||
LLM_MINIMAX_ENABLED: ${{ vars.LLM_MINIMAX_ENABLED || secrets.LLM_MINIMAX_ENABLED }}
|
||||
LLM_MINIMAX_EXTRA_HEADERS: ${{ vars.LLM_MINIMAX_EXTRA_HEADERS || secrets.LLM_MINIMAX_EXTRA_HEADERS }}
|
||||
|
||||
LLM_VOLCENGINE_PROTOCOL: ${{ vars.LLM_VOLCENGINE_PROTOCOL || secrets.LLM_VOLCENGINE_PROTOCOL }}
|
||||
LLM_VOLCENGINE_BASE_URL: ${{ vars.LLM_VOLCENGINE_BASE_URL || secrets.LLM_VOLCENGINE_BASE_URL }}
|
||||
LLM_VOLCENGINE_API_KEY: ${{ secrets.LLM_VOLCENGINE_API_KEY }}
|
||||
LLM_VOLCENGINE_API_KEYS: ${{ secrets.LLM_VOLCENGINE_API_KEYS }}
|
||||
LLM_VOLCENGINE_MODELS: ${{ vars.LLM_VOLCENGINE_MODELS || secrets.LLM_VOLCENGINE_MODELS }}
|
||||
LLM_VOLCENGINE_ENABLED: ${{ vars.LLM_VOLCENGINE_ENABLED || secrets.LLM_VOLCENGINE_ENABLED }}
|
||||
LLM_VOLCENGINE_EXTRA_HEADERS: ${{ vars.LLM_VOLCENGINE_EXTRA_HEADERS || secrets.LLM_VOLCENGINE_EXTRA_HEADERS }}
|
||||
|
||||
LLM_SILICONFLOW_PROTOCOL: ${{ vars.LLM_SILICONFLOW_PROTOCOL || secrets.LLM_SILICONFLOW_PROTOCOL }}
|
||||
LLM_SILICONFLOW_BASE_URL: ${{ vars.LLM_SILICONFLOW_BASE_URL || secrets.LLM_SILICONFLOW_BASE_URL }}
|
||||
LLM_SILICONFLOW_API_KEY: ${{ secrets.LLM_SILICONFLOW_API_KEY }}
|
||||
LLM_SILICONFLOW_API_KEYS: ${{ secrets.LLM_SILICONFLOW_API_KEYS }}
|
||||
LLM_SILICONFLOW_MODELS: ${{ vars.LLM_SILICONFLOW_MODELS || secrets.LLM_SILICONFLOW_MODELS }}
|
||||
LLM_SILICONFLOW_ENABLED: ${{ vars.LLM_SILICONFLOW_ENABLED || secrets.LLM_SILICONFLOW_ENABLED }}
|
||||
LLM_SILICONFLOW_EXTRA_HEADERS: ${{ vars.LLM_SILICONFLOW_EXTRA_HEADERS || secrets.LLM_SILICONFLOW_EXTRA_HEADERS }}
|
||||
|
||||
LLM_OPENROUTER_PROTOCOL: ${{ vars.LLM_OPENROUTER_PROTOCOL || secrets.LLM_OPENROUTER_PROTOCOL }}
|
||||
LLM_OPENROUTER_BASE_URL: ${{ vars.LLM_OPENROUTER_BASE_URL || secrets.LLM_OPENROUTER_BASE_URL }}
|
||||
LLM_OPENROUTER_API_KEY: ${{ secrets.LLM_OPENROUTER_API_KEY }}
|
||||
LLM_OPENROUTER_API_KEYS: ${{ secrets.LLM_OPENROUTER_API_KEYS }}
|
||||
LLM_OPENROUTER_MODELS: ${{ vars.LLM_OPENROUTER_MODELS || secrets.LLM_OPENROUTER_MODELS }}
|
||||
LLM_OPENROUTER_ENABLED: ${{ vars.LLM_OPENROUTER_ENABLED || secrets.LLM_OPENROUTER_ENABLED }}
|
||||
LLM_OPENROUTER_EXTRA_HEADERS: ${{ vars.LLM_OPENROUTER_EXTRA_HEADERS || secrets.LLM_OPENROUTER_EXTRA_HEADERS }}
|
||||
|
||||
LLM_OLLAMA_PROTOCOL: ${{ vars.LLM_OLLAMA_PROTOCOL || secrets.LLM_OLLAMA_PROTOCOL }}
|
||||
LLM_OLLAMA_BASE_URL: ${{ vars.LLM_OLLAMA_BASE_URL || secrets.LLM_OLLAMA_BASE_URL }}
|
||||
LLM_OLLAMA_API_KEY: ${{ secrets.LLM_OLLAMA_API_KEY }}
|
||||
|
||||
@@ -99,7 +99,7 @@ export const LLM_PROVIDER_TEMPLATES: LLMProviderTemplate[] = [
|
||||
label: 'OpenRouter',
|
||||
protocol: 'openai',
|
||||
baseUrl: 'https://openrouter.ai/api/v1',
|
||||
placeholderModels: '~openai/gpt-latest,~anthropic/claude-sonnet-latest',
|
||||
placeholderModels: '~anthropic/claude-sonnet-latest,~openai/gpt-latest',
|
||||
capabilities: ['openai-compatible', 'aggregator', 'model-discovery'],
|
||||
officialSources: [
|
||||
{ label: 'OpenRouter Models API', url: 'https://openrouter.ai/docs/api/api-reference/models/get-models' },
|
||||
|
||||
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- [新功能] EventMonitor 支持 `price_change_percent` 涨跌幅阈值规则,可按上涨或下跌方向触发实时告警。
|
||||
- [文档] 明确 `price_change_percent` 事件告警仅为配置与运行时规则扩展,未变更模型/provider/base URL/LiteLLM 兼容语义;回退路径为关闭/移除 Event Monitor 配置;兼容验证与回归依据见 `tests/test_multi_agent.py`、`tests/test_system_config_service.py`。
|
||||
- [chore] 抽出 Web LLM provider preset 单一模板数据源,保持现有配置保存语义不变。
|
||||
- [改进] 补齐 LLM provider channel 在 GitHub Actions 中的显式映射,并同步 `.env` 示例与配置文档。
|
||||
|
||||
## [3.14.2] - 2026-04-30
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ model_list:
|
||||
|
||||
- 运行时选择:`LLM_CHANNELS`、`LITELLM_MODEL`、`LITELLM_FALLBACK_MODELS`、`AGENT_LITELLM_MODEL`、`VISION_MODEL`、`VISION_PROVIDER_PRIORITY`、`LLM_TEMPERATURE`
|
||||
- 多 Key:`GEMINI_API_KEYS`、`ANTHROPIC_API_KEYS`、`OPENAI_API_KEYS`、`DEEPSEEK_API_KEYS`(当前 workflow 仅从 repository secrets 导入,不会读取同名 Variables)
|
||||
- 常用渠道名:`primary`、`secondary`、`gemini`、`deepseek`、`aihubmix`、`openai`、`anthropic`、`moonshot`、`ollama`
|
||||
- 常用渠道名:`primary`、`secondary`、`aihubmix`、`deepseek`、`dashscope`、`zhipu`、`moonshot`、`minimax`、`volcengine`、`siliconflow`、`openrouter`、`gemini`、`anthropic`、`openai`、`ollama`
|
||||
|
||||
例如在 GitHub Actions 中配置 `LLM_CHANNELS=primary,deepseek` 时,需同步配置 `LLM_PRIMARY_*` / `LLM_DEEPSEEK_*`。其中 `LLM_<NAME>_API_KEY` / `LLM_<NAME>_API_KEYS` 当前也仅从 repository secrets 导入;如果你把这些值放在 Variables,运行时不会生效。若使用自定义渠道名(如 `my_proxy`),GitHub Actions 还必须在 workflow `env:` 中显式新增对应的 `LLM_MY_PROXY_*` 映射;本地 `.env` 和 Docker 不受这个限制。
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ The bundled `daily_analysis.yml` explicitly passes the common LLM runtime fields
|
||||
|
||||
- Runtime selection: `LLM_CHANNELS`, `LITELLM_MODEL`, `LITELLM_FALLBACK_MODELS`, `AGENT_LITELLM_MODEL`, `VISION_MODEL`, `VISION_PROVIDER_PRIORITY`, `LLM_TEMPERATURE`
|
||||
- Multiple keys: `GEMINI_API_KEYS`, `ANTHROPIC_API_KEYS`, `OPENAI_API_KEYS`, `DEEPSEEK_API_KEYS` (the current workflow imports these from repository Secrets only, not from same-named Variables)
|
||||
- Common channel names: `primary`, `secondary`, `gemini`, `deepseek`, `aihubmix`, `openai`, `anthropic`, `moonshot`, `ollama`
|
||||
- Common channel names: `primary`, `secondary`, `aihubmix`, `deepseek`, `dashscope`, `zhipu`, `moonshot`, `minimax`, `volcengine`, `siliconflow`, `openrouter`, `gemini`, `anthropic`, `openai`, `ollama`
|
||||
|
||||
For example, if you set `LLM_CHANNELS=primary,deepseek` in GitHub Actions, also configure the corresponding `LLM_PRIMARY_*` and `LLM_DEEPSEEK_*` entries. The `LLM_<NAME>_API_KEY` / `LLM_<NAME>_API_KEYS` fields are also imported from repository Secrets only right now, so storing them in Variables will not work at runtime. If you use a custom channel name such as `my_proxy`, GitHub Actions must explicitly add matching `LLM_MY_PROXY_*` mappings in the workflow `env:` block. Local `.env` and Docker runs do not have this limitation.
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ LITELLM_MODEL=deepseek/deepseek-v4-flash
|
||||
| MiniMax | `minimax` | `openai` | `https://api.minimax.io/v1` | `MiniMax-M2.7,MiniMax-M2.7-highspeed` |
|
||||
| 火山方舟 / 豆包 | `volcengine` | `openai` | `https://ark.cn-beijing.volces.com/api/v3` | `doubao-seed-1-6-251015,doubao-seed-1-6-thinking-251015` |
|
||||
| 硅基流动 / SiliconFlow | `siliconflow` | `openai` | `https://api.siliconflow.cn/v1` | `deepseek-ai/DeepSeek-V3.2,Qwen/Qwen3-235B-A22B-Thinking-2507` |
|
||||
| OpenRouter | `openrouter` | `openai` | `https://openrouter.ai/api/v1` | `~openai/gpt-latest,~anthropic/claude-sonnet-latest` |
|
||||
| OpenRouter | `openrouter` | `openai` | `https://openrouter.ai/api/v1` | `~anthropic/claude-sonnet-latest,~openai/gpt-latest` |
|
||||
| Ollama | `ollama` | `ollama` | `http://127.0.0.1:11434` | `llama3.2,qwen2.5` |
|
||||
|
||||
## 官方来源与兼容性
|
||||
@@ -55,11 +55,29 @@ LITELLM_MODEL=deepseek/deepseek-v4-flash
|
||||
| MiniMax | [OpenAI API 兼容](https://platform.minimax.io/docs/api-reference/text-chat)、[获取模型列表](https://platform.minimax.io/docs/api-reference/models/openai/list-models) | 官方 OpenAI-compatible Base URL 为 `https://api.minimax.io/v1`,并列出 `MiniMax-M2.7`、`MiniMax-M2.7-highspeed`。中国区 Coding 工具场景可能使用 `.com`/Anthropic 专用入口,以控制台为准。 |
|
||||
| 火山方舟 / 豆包 | [在线推理(常规)](https://www.volcengine.com/docs/82379/2121998)、[模型列表](https://www.volcengine.com/docs/82379/1949118) | 官方示例使用 `https://ark.cn-beijing.volces.com/api/v3` 与 `doubao-seed-1-6-251015`;如使用 Coding Plan,请改用其专用 Base URL 和模型名,不要套用本表的在线推理模板。 |
|
||||
| SiliconFlow | [模型列表](https://docs.siliconflow.cn/quickstart/models)、[获取模型列表 API](https://docs.siliconflow.cn/cn/api-reference/models/get-model-list) | 平台模型实时更新且 `/models` 需要 API Key;模板只给常见新模型示例,保存前建议在 Web 设置页点击「获取模型」确认账号可见性。 |
|
||||
| OpenRouter | [Models API](https://openrouter.ai/docs/api/api-reference/models/get-models) | OpenRouter 支持 `~openai/gpt-latest`、`~anthropic/claude-sonnet-latest` 等 latest router alias;也可以改成具体模型 ID 固定行为。 |
|
||||
| OpenRouter | [Models API](https://openrouter.ai/docs/api/api-reference/models/get-models) | OpenRouter 支持 `~anthropic/claude-sonnet-latest`、`~openai/gpt-latest` 等 latest router alias;2026-05-03 的一次手动 live smoke 以 Claude Sonnet latest 作为默认示例通过,GPT latest 保留为可按账号权限切换的备选。 |
|
||||
| LiteLLM | [OpenAI-Compatible Endpoints](https://docs.litellm.ai/docs/providers/openai_compatible) | OpenAI 兼容端点需要把运行时模型写成 `openai/<model>`,Base URL 只填到服务商兼容入口,不额外拼接 `/chat/completions`。 |
|
||||
|
||||
当前仓库锁定 `litellm>=1.80.10,<1.82.7`(见 `requirements.txt`)。本页预设只保证配置形状与当前依赖的 OpenAI-compatible 路由规则一致;实际连通性仍取决于服务商账号权限、地域、额度和模型开通状态。回退方式:在 Web 设置页删除对应渠道,或从 `.env` 移除 `LLM_MINIMAX_*` / `LLM_VOLCENGINE_*` 并恢复原 `LITELLM_MODEL`、`LITELLM_FALLBACK_MODELS`。
|
||||
|
||||
## GitHub Actions 配置
|
||||
|
||||
仓库自带 `.github/workflows/daily_analysis.yml` 只会透传 workflow 中显式列出的环境变量。使用渠道模式时,先在 Repository Variables 或 Secrets 中设置 `LLM_CHANNELS`,再按渠道名补齐对应 `LLM_<CHANNEL>_*`。
|
||||
|
||||
| 字段 | 建议位置 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `LLM_CHANNELS` | Variables 或 Secrets | 逗号分隔渠道名,例如 `deepseek,minimax,volcengine`。 |
|
||||
| `LLM_<CHANNEL>_PROTOCOL` | Variables 或 Secrets | 非敏感,通常为 `openai`、`deepseek`、`gemini`、`anthropic` 或 `ollama`。 |
|
||||
| `LLM_<CHANNEL>_BASE_URL` | Variables 或 Secrets | 非敏感时优先放 Variables;私有网关地址可放 Secrets。 |
|
||||
| `LLM_<CHANNEL>_MODELS` | Variables 或 Secrets | 非敏感模型列表,逗号分隔。 |
|
||||
| `LLM_<CHANNEL>_ENABLED` | Variables 或 Secrets | 可选,未配置时默认启用;设为 `false` 可跳过该渠道。 |
|
||||
| `LLM_<CHANNEL>_API_KEY` / `LLM_<CHANNEL>_API_KEYS` | Secrets | 密钥字段必须放 Repository Secrets;同名 Variables 不会被 workflow 读取。 |
|
||||
| `LLM_<CHANNEL>_EXTRA_HEADERS` | Secrets 或 Variables | JSON 字符串;只要包含鉴权、租户、组织或私有网关信息,就应放 Secrets。 |
|
||||
|
||||
默认 workflow 已显式映射 `primary`、`secondary`、`aihubmix`、`deepseek`、`dashscope`、`zhipu`、`moonshot`、`minimax`、`volcengine`、`siliconflow`、`openrouter`、`gemini`、`anthropic`、`openai`、`ollama`。如果使用自定义渠道名(如 `my_proxy`),仅在 Repository Secrets / Variables 中新增 `LLM_MY_PROXY_*` 不会自动生效,需要同步扩展 workflow 的 `env:` 映射;本地 `.env`、Docker 和自托管脚本不受这个限制。
|
||||
|
||||
Ollama 默认 Base URL `http://127.0.0.1:11434` 主要面向本地、Docker 或能访问该服务的 self-hosted runner。GitHub-hosted runner 通常没有本地 Ollama 服务,直接配置 `LLM_CHANNELS=ollama` 大概率会连接失败。
|
||||
|
||||
## 排障要点
|
||||
|
||||
- 鉴权失败:检查 API Key 是否填错、复制了空格,或服务商是否要求额外项目权限。
|
||||
|
||||
114
tests/test_daily_analysis_workflow_llm_env.py
Normal file
114
tests/test_daily_analysis_workflow_llm_env.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Static checks for LLM provider channel mappings in daily_analysis.yml."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
TEMPLATE_PATH = ROOT_DIR / "apps/dsa-web/src/components/settings/llmProviderTemplates.ts"
|
||||
WORKFLOW_PATH = ROOT_DIR / ".github/workflows/daily_analysis.yml"
|
||||
ENV_EXAMPLE_PATH = ROOT_DIR / ".env.example"
|
||||
|
||||
EXPECTED_TEMPLATE_CHANNELS = {
|
||||
"aihubmix",
|
||||
"deepseek",
|
||||
"dashscope",
|
||||
"zhipu",
|
||||
"moonshot",
|
||||
"minimax",
|
||||
"volcengine",
|
||||
"siliconflow",
|
||||
"openrouter",
|
||||
"gemini",
|
||||
"anthropic",
|
||||
"openai",
|
||||
"ollama",
|
||||
}
|
||||
|
||||
|
||||
def _extract_provider_templates() -> dict[str, str]:
|
||||
content = TEMPLATE_PATH.read_text(encoding="utf-8")
|
||||
matches = re.findall(
|
||||
r"channelId:\s*'(?P<channel>[^']+)'.*?baseUrl:\s*'(?P<base_url>[^']*)'",
|
||||
content,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
assert matches, "No provider channelId entries were found in llmProviderTemplates.ts"
|
||||
|
||||
templates = {channel: base_url for channel, base_url in matches if channel != "custom"}
|
||||
assert EXPECTED_TEMPLATE_CHANNELS.issubset(templates.keys())
|
||||
assert "ark" not in templates
|
||||
return templates
|
||||
|
||||
|
||||
def _load_daily_analysis_env() -> dict[str, str]:
|
||||
workflow = yaml.safe_load(WORKFLOW_PATH.read_text(encoding="utf-8"))
|
||||
steps = workflow["jobs"]["analyze"]["steps"]
|
||||
analyze_step = next((step for step in steps if step.get("name") == "执行股票分析"), None)
|
||||
available_step_names = [step.get("name", "<unnamed>") for step in steps]
|
||||
assert analyze_step is not None, (
|
||||
"Expected daily_analysis.yml job analyze to include a step named "
|
||||
f"'执行股票分析'; available step names: {available_step_names}"
|
||||
)
|
||||
return analyze_step["env"]
|
||||
|
||||
|
||||
def test_daily_analysis_maps_all_provider_template_channels() -> None:
|
||||
templates = _extract_provider_templates()
|
||||
env = _load_daily_analysis_env()
|
||||
|
||||
for channel in templates:
|
||||
prefix = f"LLM_{channel.upper()}_"
|
||||
for suffix in (
|
||||
"PROTOCOL",
|
||||
"BASE_URL",
|
||||
"API_KEY",
|
||||
"API_KEYS",
|
||||
"MODELS",
|
||||
"ENABLED",
|
||||
"EXTRA_HEADERS",
|
||||
):
|
||||
assert f"{prefix}{suffix}" in env
|
||||
|
||||
assert not any(key.startswith("LLM_ARK_") for key in env)
|
||||
|
||||
|
||||
def test_daily_analysis_keeps_channel_secrets_in_secrets_context() -> None:
|
||||
templates = _extract_provider_templates()
|
||||
env = _load_daily_analysis_env()
|
||||
|
||||
for channel in templates:
|
||||
upper = channel.upper()
|
||||
for suffix in ("API_KEY", "API_KEYS"):
|
||||
key = f"LLM_{upper}_{suffix}"
|
||||
assert env[key] == f"${{{{ secrets.{key} }}}}"
|
||||
|
||||
for suffix in ("PROTOCOL", "BASE_URL", "MODELS", "ENABLED", "EXTRA_HEADERS"):
|
||||
key = f"LLM_{upper}_{suffix}"
|
||||
assert f"vars.{key}" in env[key]
|
||||
assert f"secrets.{key}" in env[key]
|
||||
|
||||
|
||||
def test_env_example_includes_provider_template_channel_examples() -> None:
|
||||
templates = _extract_provider_templates()
|
||||
env_example = ENV_EXAMPLE_PATH.read_text(encoding="utf-8")
|
||||
|
||||
for channel, base_url in templates.items():
|
||||
upper = channel.upper()
|
||||
assert f"LLM_CHANNELS={channel}" in env_example
|
||||
assert f"LLM_{upper}_MODELS=" in env_example
|
||||
|
||||
if channel != "ollama":
|
||||
assert f"LLM_{upper}_API_KEY=" in env_example
|
||||
if base_url:
|
||||
assert f"LLM_{upper}_BASE_URL=" in env_example
|
||||
if channel != "ollama":
|
||||
assert f"LLM_{upper}_PROTOCOL=" in env_example
|
||||
|
||||
assert "LLM_CHANNELS=ark" not in env_example
|
||||
assert "LLM_ARK_" not in env_example
|
||||
Reference in New Issue
Block a user