`provider` 命令能配哪些字段:答案是一个都不配

2026-08-10

看到一个 CLI 里有 provider 这么个命令组,多数人的第一反应是:供应商的 base_urlapi_keymodel 就在这里配。DeepTutor 的这个组恰恰不是这么回事——这是本文要讲透的那一处反直觉。

以下行号与数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你 clone 之后行号可能已经漂了,文件名、命令名与字段名是相对稳的,照着找即可。

一、先把这个组的边界划清楚

deeptutor_cli/provider_cmd.py 全文 103 行。这个体量本身就是个信号——同目录下 init_wizard.py 是 996 行、common.py 是 912 行、skill.py 是 563 行。

组是在 deeptutor_cli/main.py:48-59 这一串 add_typer 里注册上去的,组 help 文案写在 main.py:45Manage provider OAuth login.deeptutor_cli/ 目录下 @app.command( 装饰器共 43 处(grep 计数),其中落在 provider_cmd.py只有 1 处。也就是说,deeptutor provider 下面只有一个子命令:login

provider login <provider> 的签名在 provider_cmd.py:16-33:一个位置参数,只认 openai-codexgithub-copilot 两个取值,其余一律抛 typer.BadParameter。这个参数的 help 里对后者的用词是 validate existing Copilot authprovider_cmd.py:20)——注意它说的是「校验既有认证」,不是登录。

所以标题那个问题的直接答案是:这个命令组不接收任何配置字段,它既不收 --base-url,也不收 --api-key--model。它做的是「登录 / 校验访问权」,跟「把供应商参数写进配置」是两件事。

二、那字段到底在哪儿配

deeptutor init 这个顶层命令里。顶层直挂的命令只有四个——runmain.py:75)、startmain.py:117)、servemain.py:132)、initinit_cmd.py:542)——init 就是采集 provider 字段的那一个。

init_cmd.py:1-9 的模块 docstring 自己写明:重活(provider 菜单、live /models 拉取、连通性探测、review 面板)都在 deeptutor_cli.init_wizard 里。步骤数由 total_steps = 4 if cli_only else 5 决定(init_cmd.py:429-431)。向导每一步具体怎么走,我们另有一篇专门讲;这里只关心每一类 provider 各自带哪些字段

LLM 这一类

init_cmd.py:462-470_llm_stepinit_cmd.py:88-159)的顺序是:选 provider → 是否改 base_url → 采集 api_key → 拉模型列表 → 选模型 → 可选连通性 probe。落到字段上就是四个:provider、base_urlapi_key、model。

可选的清单在 init_wizard.py:40-51FEATURED_LLM_PROVIDERS 共 10 项。这只是「精选」,不在这 10 项里的可以按菜单键位展开或自填——键位约定是 [a] Show all、[c] Custom(init_wizard.py:376-381)。具体是哪 10 项,向导那一篇里列全了,这里不重复。

模型这一栏优先走实时拉取:fetch_models() 请求 base_url.rstrip("/") + "/models",httpx 超时 5.0 秒(init_wizard.py:571-587)。拉不动才退回 LLM_FALLBACK_MODELS,这个字典共 10 个键,注释写明「只在 GET {base_url}/models 失败或 provider 为 custom 时使用」(init_wizard.py:53-78)。

Embedding 这一类

比 LLM 多两件事,也少一件事。多的是:多一个 dimension 字段,默认空串;以及一个「复用 LLM 的 api_key」的 confirm,默认 True(init_cmd.py:207-290)。少的是:这一步可以整步跳过,选择器里按 [s] 返回 SKIP_SENTINEL = "__skip__"init_wizard.py:428:472-473)。

FEATURED_EMBEDDING_PROVIDERS 也是 10 项(init_wizard.py:84-95),其中 vllm 那条的注释说明它同时覆盖 LM Studio 与 llama.cpp。对应的 EMBEDDING_FALLBACK_MODELS 只有 9 个键(init_wizard.py:100-114),比 featured 少一个。清单里具体是哪 10 项,同样交给向导那一篇。

Search 这一类:字段最规整的一处

搜索这一类的写法跟前两类不一样:它把「一个 provider 需要什么」显式写成了一个名叫 SearchProviderSpec 的结构(就在 init_wizard.py 里,SEARCH_PROVIDERS 上方),然后用 8 条实例把清单铺开。要找字段定义就搜类名,要找那 8 条实例则看 init_wizard.py:136-192——这两处别混,实例段里只有取值,没有字段声明。

字段里有几个值得单独记一笔:

字段卡内已核实的口径
requires_api_key这家要不要 key,例如 duckduckgo 是 False
env_keys环境变量名元组,按顺序取第一个非空
requires_base_url要不要填实例地址,例如 searxng 是 True
default_base_url要填时的默认值,searxng 是 http://localhost:8888
hint菜单里那行说明,duckduckgo 写的是 no API key needed

八条分别是 brave、tavily、jina、serper、perplexity、duckduckgo、searxng、none(各 name=init_wizard.py:138/145/152/159/166/173/179/187)。duckduckgo 与 searxng 这两条的取值见 init_wizard.py:172-185

env_keys 这个字段解释了一件常被问的事:key 不是只能手填。取值逻辑在 search_api_key_from_envinit_wizard.py:505-511),按元组顺序取第一个非空的环境变量,例如 brave 是 ("BRAVE_API_KEY", "SEARCH_API_KEY")init_wizard.py:130:141)。

还有一处容易被当成同义词的:none 不等于按 [s] 跳过init_cmd.py:320-343 的注释写得很明确,provider == "none" 是「显式禁用 web search」,这个选择仍然会写进 catalog;而 [s] 是整步返回 None。想让配置里留下「我确实不要联网搜索」这条记录,选的是 none

三、provider login 的两条分支各自做了什么

既然它不配字段,那就得说清它到底做什么——尤其因为这两条分支都会向外发真实网络请求。

openai-codex:走 OAuth。动手之前先看一句仓库自己的定性——deeptutor_cli/README.md:251 原文写的是「这条 Codex backend 兼容路径目前属于实验性能力。」这是 README 自称的状态,不是我们的评价,但它摆在那里,就该照实带上。

流程本身是这样的:会打印 Callback、Authorization URL 与 ssh_forward_command,然后轮询 service.public_status()completed 算成功,failed / expired / cancelled 一律退出码 1,轮询间隔是 asyncio.sleep(0.5)provider_cmd.py:36-68)。用户中断(asyncio.CancelledError)时调 cancel_login()raise typer.Exit(code=130)provider_cmd.py:70-73)。

github-copilot:不走 OAuth。它的校验方式是向 https://api.githubcopilot.com 发一次 model="gpt-4o"max_tokens=1 的 chat 请求,请求成功就算校验通过(provider_cmd.py:90-99)。

这一点必须如实说明:跑这条命令会用你本机已有的凭据向第三方服务发一次真实请求。同理,init 向导里的 fetch_models()probe_llm() 也都会带着你刚填的 key 发真实请求——probe_llm() 超时 15.0 秒,anthropic 走 /messages + max_tokens: 1,其余走 /chat/completions,没有 key 时用占位串 sk-no-key-required,返回 (ok, elapsed_ms, error)init_wizard.py:797-841)。这些是源码里的调用形态,不是我们运行出来的结果;实际会不会通、通了之后什么表现,本文不下结论。

四、一处文档与代码对不上的地方

main.py:45provider 组的 help 是 Manage provider OAuth login.;而 provider_cmd.py:79-99github-copilot 这条分支走的是「校验既有认证」的一次 chat 请求,命令自身参数 help 的用词也是 validate existing Copilot authprovider_cmd.py:20)。组 help 说的是 OAuth,分支实现里有一条不是 OAuth,两处口径不一致;以我们实读的仓库状态为准。

还有一处位置差异也一并记下:init_wizard.py:351-357 在「显示全部 provider」时会排除 spec.is_oauth 的条目,注释写的是 OAuth flows use 'deeptutor login'。而 main.py:75/117/132init_cmd.py:542 显示,顶层直挂命令是 runstartserveinit 四个,OAuth 登录注册在 provider 组下(main.py:48-59 那串 add_typer)。注释里的命令写法与命令树里的位置不一致。说完就停,我们不推断原因,也不据此评价什么。

五、想看当前配成了什么样,看哪儿

deeptutor config showconfig 组的 help 是 Inspect configuration.(各组 help 集中写在 main.py:36-46),组下同样只注册了 1 个命令。输出内容是 ports(backend / frontend)、llm、embedding、search、language 与 tools 列表(config_cmd.py:68-92)。

注意两件事:

其一,所有 api_key 一律被掩码,输出 "***""(not set)"config_cmd.py:41:54:84)。想核对 key 本身对不对,config show 帮不了你。init 向导里的展示用的是另一套规则:_mask_secret() 显示前 4 + 后 4 个字符,长度 ≤ 8 时全掩码,空值显示 (empty)init_wizard.py:264-270)。

其二,embedding 解析抛 ValueError 时,这一栏输出的是 {"status": "not_configured", "message": ...}config_cmd.py:57-61)。看到 not_configured 不代表你没填过,它也可能是解析失败。

字段落盘的位置是 <data>/user/settings/model_catalog.json(常量 CATALOG_PATHdeeptutor/services/config/model_catalog.py:20);<data>get_runtime_home() 推出,优先级是显式参数 → DEEPTUTOR_HOME 环境变量 → 当前工作目录(deeptutor/runtime/home.py:12-27:30-33)。init 保存时分两处:非 --cli 模式才 runtime.save_system(system),而 catalog_service.save(catalog) 两种模式都执行(init_cmd.py:528-530)——也就是说,provider 字段这一份在 CLI-only 模式下照样会落盘。这是本机上的敏感文件,怎么保管请结合自身环境评估,本文不给安全方案。

另外,单次调用里临时改配置不走 provider 组,走的是 run / chat 上的 --config KEY=VALUE(缺 = 或 key 为空会抛 Invalid --config item ... Expected KEY=VALUE.common.py:92-99)与 --config-json(必须是 JSON 对象,否则抛 JSON config must be an object.common.py:102-112)。两者同时给时的合并顺序是先 config_json、再用 --config 逐项覆盖(common.py:842-843)。

举个按上述参数语义组合出来的形态:

deeptutor provider login github-copilot
deeptutor config show
deeptutor run chat "hello" --config-json '{"KEY":"A"}' --config KEY=B

第三条里的 KEY 只是占位键名,事实卡里没有核实过任何一个具体配置键是否被识别,别照抄成真键名去试。以上为按仓库中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准;这条里最终生效的应是 --config 覆盖后的 B,这是按 common.py:842-843 的合并顺序读出来的语义,不是运行结果。

六、你可以照着核的五步

  1. wc -l deeptutor_cli/provider_cmd.py 看是不是 103 行,再 grep -c "@app.command(" deeptutor_cli/provider_cmd.py 确认只有 1 个命令。
  2. 打开 provider_cmd.py:16-33,确认只认 openai-codexgithub-copilot 两个取值,其余抛 typer.BadParameter
  3. main.py:45 的组 help 与 provider_cmd.py:90-99 的 copilot 分支并排看一遍,这就是第四节那处不一致。
  4. init_wizard.py 里搜 SearchProviderSpec,把这个结构的字段声明抄下来;再往下翻到 init_wizard.py:136-192,对照那 8 条实例看每个字段实际填了什么。
  5. 打开 deeptutor/services/config/model_catalog.py:20 找到 CATALOG_PATH,再回 deeptutor/runtime/home.py:12-27 确认 <data> 是怎么被解析出来的,就知道你的 key 会落在哪个目录。

边界说明:本文只读了 provider_cmd.py 全文、init_wizard.py 中与 provider 清单 / 拉模型 / probe 相关的段落,以及 init_cmd.pyconfig_cmd.py 的对应行。provider_cmd.py 依赖的 deeptutor/services/codex_auth 我们没有读,README 里关于 callback 端口与 SSH 隧道的描述没有在代码中交叉验证;init_wizard.py 里那些 strings["init.*"] 的实际中英文案,来自 deeptutor/runtime/banner.pylabels_for(),我们没有打开该文件,因此向导提示的具体措辞未核实。上面出现的所有超时秒数与默认值都是源码中的默认配置,不是运行结果的保证。


本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.mdpyproject.tomldeeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。 本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API, 因此不涉及生成质量、响应速度与教学效果的任何描述。 参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

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