feat: add aimlapi.com LLM channel preset - #1
Open
Lookoff-AIMLAPI wants to merge 3 commits into
Open
Conversation
Users who want to reach several vendors through one OpenAI-compatible key currently have to hand-build a custom channel: they have to know the Base URL, that models must be written as bare aliases, and that a provider-prefixed id would be routed away from the gateway by LiteLLM. Adding the preset removes those three failure modes from the settings page and from .env. The channel itself needs no new runtime code — LLM_<NAME>_* is generic — so the diff is the preset, the .env.example block, the workflow mapping that tests/test_daily_analysis_workflow_llm_env.py requires for every template channel, and the docs. Attribution reuses the mechanism that already exists for the AIHubmix sponsored header rather than adding a second one: the three copies of that base-URL check plus the one in image_stock_extractor now share build_provider_extra_headers(), which returns a fresh dict per deployment, leaves the module-level constants untouched, and lets a channel's own LLM_<NAME>_EXTRA_HEADERS win on a key clash. AIHubmix keeps its existing substring match; the aimlapi headers are matched on hostname so they cannot ride a request to another vendor or to a proxy that merely fronts the gateway. Two traps in that gateway are pinned by tests because both fail invisibly. A malformed partner id is dropped silently rather than rejected, so its shape is asserted instead of being discovered in production. And the gateway answers 400 for a dozen request fields sent as JSON null — temperature, tools, max_tokens among them — which is exactly what an unset optional serializes to; the repo already omits those keys, and a regression test now says so. The Anthropic example uses the dotted spelling because the gateway ships both: claude-sonnet-4.6 declares tools, vision and structured_output in its catalog capabilities while the otherwise identical claude-sonnet-4-6 declares only streaming.
Moves the aimlapi.com preset to the front of every hand-ordered list (the settings-page template array and its order assertion, the .env.example vendor templates, the docs preset table) and makes the "add channel" dropdown default to the first template instead of a hardcoded 'aihubmix'. This is a placement preference for our fork, not a change any upstream reviewer asked for, so it is isolated in one commit that can be dropped without touching the preset itself. The repo has no "recommended"/featured badge concept — the capability chips are factual labels — so none was invented.
The placeholder part_dailystockanalysis was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_ZBzsnhYW8aO7HueEfvKZM4je. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Type
Background And Problem
仓库的渠道模式(
LLM_CHANNELS+LLM_<NAME>_*)本身是通用的,但「预设服务商」列表决定了用户在 Web 设置页能不能一步配好一个 OpenAI-compatible 聚合平台。目前想用 aimlapi.com 的用户只能走「自定义渠道」,并且必须自己知道三件事才不会踩坑:https://api.aimlapi.com/v1(该网关只有/chat/completions与/responses,没有/v1/completions);claude-sonnet-4.6);/v1/models里带anthropic/、gemini/、xai/前缀的 id,normalize_llm_channel_model()会把它识别为直连 provider 前缀并原样保留,LiteLLM 于是不再走本渠道 Base URL,请求会被发到 Anthropic/Google 官方端点。本 PR 把这三件事收敛进一个预设条目 +
.env.example模板 + 文档,运行时不新增任何 Python 分支。同时补齐渠道归因请求头。这里没有引入新机制:仓库已经有「按 base_url 注入服务商赞助头」的实现(AIHubmix 的
APP-Code),只是它被复制成了 4 份(src/config.py三处 +src/services/image_stock_extractor.py一处)。本 PR 把这 4 处收敛成一个build_provider_extra_headers(),并在其中加入 aimlapi 的归因头。Scope Of Change
.env.example.github/workflows/00-daily-analysis.ymlapps/dsa-web/src/components/settings/LLMChannelEditor.tsxapps/dsa-web/src/components/settings/llmProviderTemplates.tsapps/dsa-web/src/components/settings/__tests__/llmProviderTemplates.test.tsdocs/CHANGELOG.mddocs/LLM_CONFIG_GUIDE.mddocs/LLM_CONFIG_GUIDE_EN.mddocs/llm-providers.mdsrc/config.pysrc/services/image_stock_extractor.pytests/test_llm_channel_config.pydocs/*):docs/CHANGELOG.md、docs/llm-providers.md、docs/LLM_CONFIG_GUIDE.md、docs/LLM_CONFIG_GUIDE_EN.md两个 commit 是有意拆开的:
fa6f7e0feat: add aimlapi.com LLM channel preset.env.example、workflow 映射、文档、测试ccd175dchore(aimlapi): fork-only placement — do not send upstream'aihubmix'改为LLM_PROVIDER_TEMPLATES[0].channelId第二个 commit 是本 fork 的排序偏好,可以整条 drop 掉而不影响第一个 commit。仓库没有 “recommended / 精选” 徽章机制(capability 标签是事实性描述),因此没有新造一个。
Issue Link
无对应 issue。动机与验收标准:让 aimlapi.com 成为可一步配置的 OpenAI-compatible 渠道预设。验收标准 ——
.env模板可直接复制运行;tests/test_daily_analysis_workflow_llm_env.py对每个模板渠道要求的 workflow env 映射与.env.example示例契约通过;api.aimlapi.com时出现,且用户自定义的同名 header 优先。Verification Commands And Results
基线(改动前,pristine
origin/maincheckout)与改动后使用同一环境:Python 3.12.14 venv +requirements.txt,Node 26.7.0 +npm ci。Web 的 3 个失败在 pristine checkout 上完全一致,与本 PR 无关:
tests/ui_governance.test.ts > does not reintroduce native title attributes on common interactive elementssrc/components/alerts/__tests__/AlertRuleForm.test.tsx > shows JP/KR options for market region in Chinese UI modesrc/components/settings/__tests__/SettingsField.test.tsx > localizes TickFlow field descriptions instead of falling back to backend English schema关于环境噪声(如实说明):本机在验证期间同时跑着其他重负载任务,load average 长时间在 60–150。高负载下两侧都会出现
Test timed out类失败,且每次失败集合都不一样——这是超时噪声的特征,不是回归:test_blocked_scheduled_analysis_times_out_and_allows_next_run、test_three_production_tools_execute_through_spawned_worker、test_cold_waiters_degrade_after_timeout_but_fetch_lands、test_run_agent_loop_allows_choice_compare_stock_codes失败,负载下降后单独重跑全部通过;其中test_blocked_scheduled_analysis_times_out_and_allows_next_run在 pristineorigin/main上同样会因负载失败。上面填报的6583 passed / 0 failed是完整一轮的结果。1209 passed / 3 failed来自一轮低负载的完整跑,失败项与基线逐条一致。之后在高负载下重跑会额外出现 14–25 条Test timed out in 5000ms(HomePage / DecisionSignalsPage / PortfolioPage 等与本 PR 无关的页面,每轮不同)。与本 PR 直接相关的文件在最终 head 上单独跑过:llmProviderTemplates.test.ts10 passed、LLMChannelEditor.test.tsx62 passed、ui_governance.test.ts仅剩上述基线失败。建议以 CI 结果为准复核这两组数字。
真实推理验证(非 mock)
通过本 PR 实际改动的代码路径跑了真实请求:
Config._load_from_env()构造llm_model_list,原样交给litellm.Router——即src/analyzer.py::_init_llm()的 channel 分支。API Key 经环境变量注入,未写入仓库、commit 或日志。模型 id 核对
预设 /
.env.example/ 文档中出现的三个示例模型,都对GET https://api.aimlapi.com/v1/models的id与aliases两个集合逐个核对(该目录 936 条记录 / 785 个唯一 id / 684 个 alias,chat 面为type == "openai/chat-completions"的 353 条),并各自实际跑通一次 completion:gpt-5.5-2026-04-23openai/gpt-5.5-2026-04-23的 aliasOKclaude-sonnet-4.6anthropic/claude-sonnet-4.6的 aliasaimlapi channel ok,tool call 正常deepseek-v4-flashdeepseek/deepseek-v4-flash的 aliasOKAnthropic 示例特意用点号拼写:该网关同时提供
claude-sonnet-4.6与claude-sonnet-4-6,两者都能调用,但GET /v1/models?include=all里只有点号形式声明了tools/vision/structured_output/parallel_tool_calls/reasoning等能力,横杠形式只声明streaming。同样的差异也出现在claude-opus-4.7/claude-opus-4-7、claude-opus-4.8/claude-opus-4-8上。对依赖 Agent 工具调用与 Vision 的本仓库来说,写点号形式更安全。顺带记录一个被换掉的候选:
gemini-3.1-pro-preview在目录中确实存在、正常调用也可用,但当max_tokens被 reasoning token 吃满、finish_reason == "length"时,该网关的 Google 路由会返回一个 不含choices[0].message、且usage缺completion_tokens的 200 响应,LiteLLM 直接抛InternalServerError: Invalid response object;同样条件下claude-sonnet-4.6/gpt-5.5-2026-04-23/deepseek-v4-flash都返回合法结构。为免用户第一次试用就撞上一个看不懂的报错,示例模型改成了截断行为可预期的三个。ai-governance:not run / backend-gate:not run / docker-build:not run / web-gate:not run—— 本仓库为 fork,未启用 GitHub Actions(gh api repos/.../actions/workflows返回total_count: 0),因此没有 CI 运行链接。上面的本地命令是对四个 gate 的等价复现:ai-governance↔python scripts/check_ai_assets.py;backend-gate↔py_compile+flake8 E9,F63,F7,F82+./scripts/test.sh code|yfinance+pytest -m "not network";web-gate↔npm run lint+npm run build。未本地复现的是docker-build(未构建镜像),本 PR 不改docker/、依赖清单或模块导入结构,风险按低评估。Visual Evidence (if applicable)
本 PR 修改了 Web 设置页「AI 模型配置」的渠道预设下拉:新增一个
aimlapi.com选项;在 fork-only commit 中该选项被移到列表首位,并成为「新增渠道」下拉的默认值。未附截图,原因与替代证据如下。 本机执行
npx playwright install chromium时被~/Library/Caches/ms-playwright/__dirlock阻塞,chromium_headless_shell-1208始终没有落盘,因此e2e/smoke.spec.ts只能报browserType.launch: Executable doesn't exist。webServer 两段(python main.py --webui-only与vite dev)都能正常起来,缺的只是浏览器二进制。可复现命令(在能装浏览器的机器 / CI 上执行即可得到产物):
在没有截图的前提下,本次改动的用户可见项由以下确定性证据锁定,且都能定位到具体变更项:
apps/dsa-web/src/components/settings/__tests__/llmProviderTemplates.test.tskeeps provider template order aligned with the existing preset dropdown order—— 断言下拉的完整顺序,aimlapi现在是第一项;keeps the aimlapi preset on the OpenAI-compatible chat completions entry—— 断言下拉项展示名为aimlapi.com、Base URL、模型示例与配置提示文案。npm run test -- src/components/settings/__tests__/LLMChannelEditor.test.tsx全绿(62 tests),覆盖「新增渠道」按预设默认值建渠道的行为。tests/test_daily_analysis_workflow_llm_env.py断言该预设在.env.example与 workflow 中都有对应示例/映射。Compatibility And Risk
normalize_llm_channel_model()、resolve_llm_channel_protocol()、API Surface 判定逻辑均未改动。build_provider_extra_headers()对 AIHubmix 保留原来的'aihubmix.com' in base_url子串匹配与setdefault语义,行为等价;aimlapi 的头按urlparse(base_url).hostname == "api.aimlapi.com"精确匹配注入,因此不会跟着请求发到其他服务商,也不会命中仅前缀相似的域名(例如api.aimlapi.com.example.com)。渠道自身LLM_<NAME>_EXTRA_HEADERS中的同名字段始终优先;模块级常量不会被就地修改,每次调用返回新 dict。以上四点都有回归测试(tests/test_llm_channel_config.py::ProviderExtraHeadersTestCase),其中包含对 partner id 形状^part_[A-Za-z0-9]{1,64}$的断言——该值写错时网关不会报错、只会静默丢弃归因,只能靠测试兜住。null返回 400 ——temperature、top_p、seed、tools、tool_choice、response_format、stream、stream_options、parallel_tool_calls、max_tokens、max_completion_tokens;而stop、presence_penalty、frequency_penalty、n、user、logprobs、logit_bias、reasoning_effort接受null。「未设置的可选参数」在 OpenAI 风格客户端里正好序列化成null,所以这是一个容易让每次调用都 400、却被 mock 测试全绿掩盖的坑;tools尤其危险——把 tools 置空写成None的调用方会在 agent 循环第 1 轮成功、第 2 轮失败。本仓库现有实现是安全的:apply_litellm_generation_params()要么省略temperature要么写入 float(未配置时回落到0.7);src/agent/llm_adapter.py用if tools:/if max_tokens is not None:决定是否写键;top_p/seed/tool_choice根本不会被主动设置(src/llm/errors.py只在报错后移除它们);response_format/stream只会被写成具体值。为防止以后有人把默认值改成None,新增了AimlapiRequestParamsTestCase(含渠道litellm_params不含None的断言)固定这一契约。排障提示:该网关 400 的顶层message是通用文案,真正的字段名在error.details[].path/.reason里。.env或运行时配置;未配置aimlapi渠道的用户行为完全不变。requirements.txt现有约束litellm>=1.80.10,!=1.82.7,!=1.82.8,<1.99.0下验证,未引入新依赖。GET https://api.aimlapi.com/v1/models的实时返回复核。这是当前运行时约束,不是临时兼容处理。/models返回和「测试连接」结果为准。Rollback Plan
revert this PR。无需额外配置或数据回滚:预设条目、.env.example注释块、workflow env 映射与文档都是纯新增;build_provider_extra_headers()被 revert 后会恢复成原来 4 处内联的 AIHubmix 判断。若只想去掉排序偏好而保留渠道,drop 掉ccd175d这一个 commit 即可。Checklist
docs/CHANGELOG.md