Vibe-Trading 开源项目怎么接大模型:能力表与登录态通道

2026-08-05

本文基于 Vibe-Trading 仓库 commit 3a752d5(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/HKUDS/Vibe-Trading 最新代码与文档为准。

Vibe-Trading 的模型接入层给出了一个值得抄的判断:接一家供应商需要的信息分两类,一类是”往哪儿发、用哪个环境变量拿钥匙”,这是数据,放 JSON;另一类是”这家的请求体和别家有什么不一样”,这是行为,只能放代码。 前者做成了 agent/src/providers/llm_providers.json,后者做成了 agent/src/providers/capabilities.py 里的一张能力表。两层分开之后,加一家常规供应商基本只改 JSON,而遇到那种连认证方式都不一样的(比如走 ChatGPT 登录态的 openai-codex),它干脆单开了一个文件 openai_codex.py,不往通用路径里塞。

这篇只讲这套接入层的机制与代价。站内已有几篇邻近的文章分工不同:API 接入方式对比 讲的是你自己选接入形态时的取舍,模型路由策略 讲的是多模型之间怎么分流,多模型 fallback 设计 讲失败切换;本篇不谈选型也不谈路由,只拆一个真实开源仓库里”单个供应商怎么被接进来”的那一段代码。

一、它要解决的问题:同一套 Agent 循环,跑在一堆脾气不同的供应商上

先说清 Vibe-Trading 是什么量级的东西,你才知道这套接入层为谁服务。这是 HKUDS 放出的开源个人交易 Agent,MIT 许可证(Copyright 2026 Vibe-Trading Contributors)。仓库里 agent/src/ 下有 23 个模块目录,agent/src/skills/ 下 88 个技能目录,agent/src/swarm/presets/ 下 30 份编制 yaml,agent/src/trading/connectors/ 下 12 家券商连接器子目录。整个工程的形态是:把金融研究流程拆成技能、工具与多智能体编制,全部由大模型驱动。

模型接入层就是这堆东西的地基。地基一旦选错供应商适配方式,上面 88 个技能全都跑不动。

问题在于,“OpenAI 兼容”这四个字是个很松的承诺。同样是 /v1/chat/completions,各家在思维链字段、工具调用回放、温度取值、请求头上的差异足够让一个多轮 ReAct 循环在第二轮就崩掉。Vibe-Trading 的做法是承认这件事,把差异显式建模成一张表,而不是在调用点写一堆 if provider == ...

二、目录 JSON:一张任何人都能读的供应商表

llm_providers.json 是个数组,本文核对时是 22 个条目。每个条目都带的字段是 namelabelapi_key_envbase_url_envdefault_modeldefault_base_urlapi_key_required,只有部分条目才出现的是 base_url_optionsauth_typelogin_command。字段清单可以在 agent/src/api/settings_routes.pyLLMProviderOption 里对上号。

这张表有两个消费方,这点很关键:

其一是 Web 设置页。settings_routes.py 里的 _load_llm_providers() 把 JSON 反序列化成 LLMProviderOption 列表,顺手做两件防呆:name 重复直接抛 Duplicate LLM provider name,列表为空直接抛 LLM provider config must not be empty。也就是说这张表是被当成配置契约来校验的,不是随便读读。

其二是后端凭据解析。capabilities.py 里的 _provider_default_base_urls()lru_cache 把同一份 JSON 读成 name -> default_base_url 的映射。函数的文档注释把动机写得很直白:一个只在 .env 里写了 LANGCHAIN_PROVIDER 加 API key 的命令行用户,如果没有这层兜底,就会默默走到 api.openai.com 然后拿到一个 404。

真正干活的是 get_llm_credentials(provider, model)。它的解析链值得记住,因为你排查问题时全靠它:

  • API key:先取该供应商专属的环境变量(如 MOONSHOT_API_KEY),取不到回落到 OPENAI_API_KEY;如果这家 api_key_envNone(Ollama 这种),则取 OPENAI_API_KEY 或者字面量 "ollama"
  • base URL:先取该供应商专属变量,再 OPENAI_BASE_URL,再 OPENAI_API_BASE,最后才是目录里的 default_base_url

拿到之后,_sync_provider_env() 会把结果写回 OPENAI_API_KEY / OPENAI_API_BASE / OPENAI_BASE_URL,因为底层用的还是 langchain_openaiChatOpenAI。Ollama 有个小修正:_normalize_ollama_base_url() 会在缺 /v1 时补上。

三、能力表:JSON 装不下的那部分差异

capabilities.py 里的 ProviderCapabilities 是个 frozen dataclass,字段就是这套接入层对”差异”的全部定义:capture_reasoningsend_reasoning_contentgemini_thought_signaturesnormalize_assistant_contentopenrouter_reasoning_bodydefault_headersnative_adapter_package_PROVIDERS 这个字典本文核对时有 28 个键,比目录 JSON 的 22 条多出六个:四个是目录里根本没有的写法(nvidia-nim 指向和 nvidia 同一份记录,kimi 指向 moonshot 那份,iflytek 指向 spark 那份,下划线的 openai_codex 指向 openai-codex 那份),另两个是 opencode-zenopencode-go——它们在能力表里有条目,目录 JSON 里却没有,所以设置页选不到,只能手写环境变量走。顺带一提,目录里已经有的 glmqwen 在能力表里也是共享记录(分别与 zhipudashscope 共用同一个对象),只是两边都登记过,不算净增。

看几组具体差异,你就明白为什么这必须是代码:

Moonshot / Kimi 三个开关全开。llm.py_get_request_payload 的注释写明了原因:kimi-k2.6 会拒绝 content 为 null 或缺 reasoning_content 的 assistant 轮次,导致工具调用后的 ReAct 续接直接断掉。所以 normalize_assistant_contentNone 改成 ""send_reasoning_content 把 LangChain 序列化时丢掉的思维链再塞回去。

Zhipu / GLM 只开 capture_reasoning,不开 send_reasoning_content。代码注释解释得很诚实:DeepSeek 明确拒绝回放的 reasoning,zhipu 这边没有实测确认过,所以在验证之前先不开。这是我认为这份文件最值得学的地方——能力开关的默认值取决于”验证过没有”,而不是”看起来应该支持”。

Gemini 单开 gemini_thought_signatures。这条链路最绕:Agent 循环把历史当成 OpenAI 格式的 dict 回放,签名藏在 tool_calls[i].extra_content.google.thought_signature 里,而 LangChain 的 _convert_dict_to_message 会把 extra_content 整个丢掉,等到发请求时签名已经没了,Gemini 就回一个 missing thought_signature 的 400。项目的解法是重写 _convert_input——这是 invokestream 共用的唯一入口,且此时 input 还是原始 dict——在那里把签名捞回 AIMessageadditional_kwargs

推理力度(reasoning effort)更是分成三条互不相通的路:OpenRouter 与 Requesty 走 extra_body.reasoningopenrouter_reasoning_body);直连 OpenAI 走顶层的 reasoning_effort 字段;其余供应商一律不发。_supports_top_level_reasoning_effort() 的实现是个正向白名单,而且不只看名字——它还会去解析 base URL 的 hostname,只有 api.openai.comopenai.com 才算数。理由写在注释里:一个 base URL 覆盖意味着你其实指向了别的网关(Ollama、LiteLLM、公司代理),那些网关说 OpenAI 的话,但不一定认这个字段。

还有一批不进能力表、直接写在 build_llm() 里的硬约束:MiniMax 的温度必须在 (0.0, 1.0],默认的 0.0 会被钳到 0.01;Kimi 的 K 系列推理模型只接受温度 1,靠 _KIMI_FORCED_TEMPERATURE_RE 匹配后强制改写。Anthropic 那条更有意思,_make_temperature_safe_anthropic() 动态生成一个子类,第一次遇到”该模型已弃用 temperature”的报错就把模型名记进 _ANTHROPIC_TEMPERATURE_UNSUPPORTED,重试一次,之后所有请求都不再带这个字段——因为哪些模型会拒绝并不可预测,只能运行时学。

原生适配器也是能力表的一部分:native_adapter_package 标记了 Anthropic 用 langchain-anthropic、DeepSeek 用 langchain-deepseek。DeepSeek 还有三挡模式,由 VIBE_TRADING_DEEPSEEK_ADAPTER 控制,auto(装了就用)、native(没装就报错)、openai-compatible(强制走通用路径)。

四、走登录态的那一条:openai-codex 为什么必须单开

目录 JSON 里只有一个条目带 auth_type: "oauth",就是 openai-codex,它的 api_key_envnullapi_key_requiredfalse,另外多了一个 login_command 字段,值是 vibe-trading provider login openai-codex

openai_codex.py 的文件级注释把边界说清楚了:这条路认的是 ChatGPT 账号的 OAuth token,由 oauth-cli-kit 负责取得与持久化,而 ChatGPT 的 OAuth token 不是 OpenAI 的 API key,所以它”有意”与标准 OpenAI 路径分开。

分开体现在四处:

第一,环境变量处理被短路。_sync_provider_env() 一进来就判断 provider 是不是 openai-codex,是的话只设 OPENAI_API_BASEOPENAI_BASE_URL,然后 os.environ.pop("OPENAI_API_KEY", None) 把可能存在的 API key 从环境里摘掉,直接 return,完全不走 get_llm_credentials

第二,base URL 被锁死。validate_codex_base_url() 逐段校验 scheme 必须是 https、netloc 必须是 chatgpt.com、path 必须是 /backend-api/codex/responses,任何一项不符就抛 ValueError。函数注释写明理由:ChatGPT 的 OAuth token 不能被发到任意 OpenAI 兼容的 base URL 去。这是一个安全取舍,代价见下一节。

第三,协议根本不是同一个。这条路走的是 Responses 风格的事件流,所以整套转换都得自己写:_convert_messages() 把 OpenAI 格式的消息列表拆成 instructions(系统提示)与 input 数组,assistant 的工具调用变成 function_call 项,tool 结果变成 function_call_output_convert_tools() 把工具定义摊平;_events_from_lines() 按空行切 SSE 事件;_message_chunks_from_events() 处理 response.output_item.addedresponse.output_text.deltaresponse.function_call_arguments.delta / .doneresponse.output_item.doneresponse.completed 这几类事件,把工具调用参数一片片攒起来。请求体里还有几个固定项:store: Falseinclude: ["reasoning.encrypted_content"]、以及一个由消息内容 sha256 算出来的 prompt_cache_key。请求头带 chatgpt-account-idOpenAI-Beta: responses=experimentaloriginator

第四,错误分类被单独建模。CodexStreamError 继承 RuntimeError 但额外携带 status_code,注释说明了动机:不带状态码的话,上层 ProviderStreamError.retryable 看到 status_code=None 会一律判定可重试,于是 Codex 的 400/401/403 这类确定性失败会白白吃掉重试预算。

适配器类 OpenAICodexLLM 本身很薄:stream() 是主路径,invoke() 就是把流出来的 CodexAIMessage__add__ 累加,ainvoke()asyncio.to_thread 包一层同步实现。

五、四块拼图各管什么

组成部分它负责什么仓库位置你什么时候会碰到它
供应商目录 JSON名称、标签、环境变量名、默认模型与默认 base URL、是否需要 key、认证类型agent/src/providers/llm_providers.json新增一家常规供应商,或想知道某家的默认端点是什么
能力表请求体与响应体的差异开关、专属请求头、原生适配器包名agent/src/providers/capabilities.py多轮工具调用在某一家上断掉,或思维链字段丢失
工厂与兼容子类读配置、算凭据、按能力拼请求、温度特例、构造 LangChain 模型agent/src/providers/llm.py排查”配好了却连不上”,或想加一条新的请求体特例
登录态适配器ChatGPT OAuth token 获取、Responses 事件流解析、错误状态码分类agent/src/providers/openai_codex.py用 ChatGPT 账号而非 API key 驱动这套 Agent
设置页路由把目录 JSON 暴露给 Web 设置页并做重名/空表校验agent/src/api/settings_routes.py在 Web 界面里改供应商,而不是手写 .env
启动预检启动时判定供应商是否可用,LLM 这一项失败视为致命agent/src/preflight.py启动就被拦住,需要看它给的 impact 提示

六、边界与代价:这套设计放弃了什么

未知供应商名会静默回落。 get_provider_capabilities() 的行为是 _PROVIDERS.get(normalized, _PROVIDERS["openai"])——名字打错不报错,直接按 OpenAI 的能力走。好处是新网关不至于开箱即崩,代价是你可能花半小时才发现是拼写问题。这也是为什么 _supports_top_level_reasoning_effort() 要额外校验 hostname:它不能信任”回落到 openai 能力”这件事本身。

OPENAI_API_KEY 是通吃兜底。 任何供应商在专属 key 缺失时都会拿 OPENAI_API_KEY 顶上。这让”只配一个变量就能跑”成立,但也意味着你环境里残留的一把 OpenAI key 可能被当成 Bearer 发给另一家的域名。同理,OPENAI_BASE_URL 一旦设置,会盖过除专属变量之外的所有供应商。凭据的暴露面要自己盯,这方面的取舍可以对照 API 密钥安全管理 里的思路。

宿主环境的 OpenAI 变量会污染请求头。 因为底层是 OpenAI SDK,OPENAI_CUSTOM_HEADERSOPENAI_ORG_IDOPENAI_ORGANIZATIONOPENAI_PROJECT_ID 这些会被自动带上。项目为此写了 _provider_scoped_extra_headers(),对非 OpenAI 的供应商用 SDK 的 Omit()OpenAI-OrganizationOpenAI-Project 以及所有环境里注入的自定义头逐个抹掉;如果环境里那些头里含 Authorization,还会用当前供应商的 key 重新写一个规范拼写的 Authorization,避免大小写变体造成重复。这段代码存在本身就说明:借用别家 SDK 是有隐性成本的。

Codex 那条路不给你自定义端点。 validate_codex_base_url() 把地址钉死,意味着你没法把它指向自建代理或公司网关。这是刻意的安全边界,但如果你的网络必须过代理,这条路就走不通。

能力表是硬编码的 Python 常量。 加一家行为特殊的供应商必须改代码、发版;只改 JSON 只能覆盖”端点与变量名”这一层。这是两层拆分的必然代价。

它明确不管的事:不做多模型路由,不做失败后自动换家,不做配额与计费管理,不做上游可用性的真实探测——preflight.py 对常规供应商只是 requests.get 打一下根路径测 TCP 与 SSL,对 Codex 则只检查本地有没有持久化的 token,两者都不代表模型真能调通。

顺带一提,仓库根目录的 NOTICE 里对因子部分的来源写得很清楚:agent/src/factors/ 下有 482 个文件,其中 Microsoft Qlib 的特征定义按 Apache 2.0 引入,另有几组公式来自公开论文与研报(各因子库子目录下另有 LICENSE.md),仓库把这些公式当作数学事实重新实现,并声明未复制原文的正文、表格与插图。这不是”项目自研的因子库”,能否商用请以许可证原文为准,本文不提供法律意见。历史表现不代表未来,本文只讨论工程实现。

七、上手与避坑清单

供应商名拼错,症状是”连上了但 404”。 为什么会踩:回落逻辑不报错,你会以为配置生效了。怎么避:跑 vibe-trading provider doctor,它调用 provider_diagnostics() 打印一份脱敏 JSON,里面有 providerbase_urladaptercapabilities 几段,直接对照你以为的那家。

.env 找错了文件。 为什么会踩:候选路径有三个,按 ~/.vibe-trading/.envagent/.env → 当前工作目录 .env 的顺序取第一个存在的,家目录那份会盖住仓库里那份。怎么避:_ensure_dotenv() 会打一条日志说明用了哪个槽位(只打符号标签,不打绝对路径,避免泄露用户名),启动时看这一行。

key 里混进了看不见的字符。 为什么会踩:从网页或 JSON 里复制时常带上换行、空格或全角引号。怎么避:_validate_authorization_credential() 会在构造模型前直接抛 RuntimeError,报错文案明确要求”用原始 API key 而不是粘贴的 JSON/HTML/格式化文本”——看到这条别去改代码,回去重新复制 key。

Codex 模型名忘了带前缀。 为什么会踩:.env.example 里写的是 openai-codex/gpt-5.4,容易随手简化。怎么避:记住 _strip_model_prefix() 只负责在发请求时剥掉 openai-codex/openai_codex/,前缀写不写取决于你,但配置文件给的样例形态是带前缀的。

Codex 的 base URL 被改动。 为什么会踩:习惯性地把所有供应商都指向自己的网关。怎么避:这条会在构造 OpenAICodexLLM 时抛 ValueError,错误信息里直接给出唯一合法地址,别绕,改回去。

推理力度设了没反应。 为什么会踩:LANGCHAIN_REASONING_EFFORT 是全局变量,但只有三条路会真的把它发出去。怎么避:先确认你这家属于哪条路,然后用 provider doctorcapabilities 段里 openrouter_reasoning_bodytop_level_reasoning_effort 的取值。合法值在 settings_routes.pyLLM_REASONING_EFFORTS 里列着,空串表示不设。

DeepSeek 选了 native 但没装包。 为什么会踩:auto 模式下缺包会静默降级到通用路径,很多人以为 native 也一样。怎么避:native 模式缺包会抛 VIBE_TRADING_DEEPSEEK_ADAPTER=native requires langchain-deepseek,要么装上,要么改回 auto

模型名意外触发了推断。 为什么会踩:_infer_from_model() 会按模型名前缀猜供应商(geminideepseeknvidia/glm 开头,或名字里含 kimi/moonshot)。怎么避:记住它只在 provider 为空或就是 openai 时才生效,显式写了非 OpenAI 的 provider 就一定以你写的为准——这个优先级在 get_provider_capabilities() 的文档注释里写死了。

八、收尾

这套接入层的核心可以压成三句话:能用数据表达的差异放 JSON,只能用代码表达的差异放能力表,认证方式都不一样的另开文件。三层之间的边界是靠”这个差异能不能被一个布尔开关描述”划出来的。

给自己的接入层做体检,可以问四个问题:新增一家常规供应商,我需要动几个文件?某家的请求体特例,我是写在调用点还是写在一张表里?供应商名写错时,我的系统是报错还是静默回落?凭据的兜底链路,我能一句话说清吗?

想继续往下读,建议按这个顺序:先 agent/src/providers/capabilities.py(最短,一眼看完全部差异维度),再 agent/src/providers/llm.pybuild_llm()provider_diagnostics()(前者是构造路径,后者是排查路径),最后 agent/src/providers/chat.py——ChatLLMbuild_llm() 的唯一上层消费者,ProviderStreamError.retryable 的判定逻辑就在那里,正好接上本文提到的 Codex 状态码问题。至于这套 Agent 能不能连真实券商、能连到什么程度,本文没有覆盖,也不打算给结论:涉及实盘下单、资金授权与程序化交易备案的部分,一律以你所在司法辖区的监管要求与券商协议为准,合规义务本身就因辖区而异。有一点是工程上必须先想明白的——模型接入层这条链路上,凭据是全程明文经手的(专属变量、OPENAI_API_KEY 兜底、写回环境、拼进 Authorization 头),任何一环的日志、崩溃栈或截图都可能把它带出去;而一旦这套凭据的下游连着真实下单通道,模型发错的一笔委托在成交之后是撤不回来的,损失也不会因为”是 Agent 自己决定的”而消失。所以先在纸面账户或只读连接上跑通,再谈别的。

本篇属于一个把开源个人交易 Agent 项目 Vibe-Trading逐层拆开讲的系列,整体地图见 Vibe-Trading 是什么:HKUDS 这个开源交易 Agent 项目的工程全景与边界;沿着这条线往下,还可以看 Vibe-Trading 开源交易 Agent:三个入口与第一次配置Vibe-Trading 开源项目的数据源层拆解:一个注册表、一条回退链和一份路由技能文档

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。