LLM 接入层:统一调用口与代码里出现的 provider 名单
一个项目接了多少家模型服务,光看 README 的清单没用,得看代码里到底注册了多少个名字、这些名字最后落到几种实现上。DeepTutor 这两件事分得很开:名字很多,实现很少。
以下行号与数字都对应我们采集的仓库快照:HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。你自己 clone 之后行号可能已经漂了,但文件名、常量名与字段名是稳的,照着搜即可。
先说位置。deeptutor/services/llm/ 这一层是 36 个文件、9263 行 Python,在 services 二十几个子领域里排第一(services 全层的划分我们另有一篇专门讲)。但这一层最该先读的文件不在它里面,而在它的上一级目录。
一、单一真源在 services/provider_registry.py
deeptutor/services/provider_registry.py 的文件头注释把话说死了:这是 provider 元数据的 “Single source of truth for provider metadata”(provider_registry.py:3-7)。元数据的落点就是同文件里的 PROVIDERS 元组,一家一条 ProviderSpec;你想知道某一家是怎么被识别、默认连到哪个地址的,答案都在那一条上,不必再去翻别处。
同一段注释还有一句容易被略过的话:“Order matters — it controls match priority and fallback. Gateways first.” 也就是说,PROVIDERS 这个元组里条目的书写顺序本身是有语义的,它决定匹配优先级,而且网关类排在前面。读这个文件的时候别把它当成一张无序的表。
ProviderSpec 是个 frozen=True 的 dataclass(provider_registry.py:18-54),字段里值得记住的几个:backend、is_gateway、is_local、is_oauth、is_direct、detect_by_key_prefix、detect_by_base_keyword、default_api_base、thinking_style。前面五个布尔量决定这条 spec 属于哪一类,后面几个决定「用户没明说是哪一家时,怎么从 key 前缀或 base_url 关键字反推」。
另外有 PROVIDER_ALIASES 22 条别名映射(provider_registry.py:77-100),例如 azure→azure_openai、google→gemini、claude→anthropic、openai_compatible→custom。你在配置里写了个不在名单上的写法却仍然能匹配上,多半是这张表在起作用。
二、36 个名字,按 spec 的布尔字段分五类
PROVIDERS 元组里 name= 出现 36 次。按 spec 上的布尔字段归类如下(行号均在 provider_registry.py):
| 类别 | 名字(带行号) |
|---|---|
直连(is_direct) | custom(:121)、custom_anthropic(:129)、azure_openai(:137) |
网关(is_gateway) | openrouter(:146)、edenai(:158)、aihubmix(:168)、siliconflow(:179)、novita(:189)、atlascloud(:199)、volcengine(:209)、volcengine_coding_plan(:220)、byteplus(:231)、byteplus_coding_plan(:243)、nvidia_nim(:452) |
| 标准 | anthropic(:255)、openai(:264)、openai_codex(:273)、github_copilot(:282)、deepseek(:293)、gemini(:303)、zhipu(:311)、dashscope(:320)、moonshot(:329)、minimax(:351)、minimax_anthropic(:360)、mistral(:368)、stepfun(:376)、xiaomi_mimo(:384)、groq(:464)、qianfan(:472) |
本地部署(is_local) | vllm(:393)、ollama(:401)、lm_studio(:411)、llama_cpp(:421)、lemonade(:431)、ovms(:441) |
OAuth(is_oauth=True) | 只有两个:openai_codex(:278)、github_copilot(:287) |
这张表照抄自源码常量,我们不对其中任何一家做推荐、比较或评价,也没有调用过其中任何一家的接口——列出来只是为了让你能一眼判断「我用的这家在不在名单里、属于哪一类」,然后自己去那一行核。
本地部署那一类还带了端口识别关键字:ollama 11434(:407)、lm_studio 1234(:417)、llama.cpp 8080(:427)、lemonade 13305(:437);key 前缀识别目前只有两条,openrouter 的 sk-or-(:152) 与 nvidia_nim 的 nvapi-(:458)。默认 base_url 也写在同一批行上,例如 ollama 是 http://localhost:11434/v1(:408)、lm_studio 是 http://localhost:1234/v1(:418)。
三、反直觉的第一处:36 个名字,只有 5 种实现
看完名单很容易以为后面有 36 套请求组装代码。没有。ProviderSpec.backend 字段的注释把候选值列全了,一共 5 个:"openai_compat" | "anthropic" | "azure_openai" | "openai_codex" | "github_copilot"(provider_registry.py:32-34)。
对应的实现在 deeptutor/services/llm/provider_core/,__init__.py 用 _LAZY_TYPES 懒加载 5 个类:AnthropicProvider、AzureOpenAIProvider、GitHubCopilotProvider、OpenAICodexProvider、OpenAICompatProvider(llm/provider_core/__init__.py:30-36)。选哪一个的分发函数是 _build_runtime_provider,按 spec.backend 分支 import,缺省落到 openai_compat(llm/provider_factory.py:45-49)。
这个结构有两个直接后果,都是排查问题时用得上的:
其一,名单里绝大多数条目共用同一条 openai_compat 代码路径,差别只在 spec 上那几个字段(default_api_base、detect_by_key_prefix、detect_by_base_keyword、thinking_style 与 model_overrides 之类)。所以你新接一家 OpenAI 兼容接口,改的是元数据,不是实现。
其二,取不到 spec 时分发也不会中断,backend 直接取缺省值 openai_compat。名字写错、别名没覆盖到,落到的不是一条”没有这个 provider”的分支,而是那条通用的 OpenAI 兼容实现。要确认自己到底被解析成了哪一条,就回 llm/provider_factory.py:45-49,看 spec 取到的是哪一条、backend 落到了哪个分支。
顺带一提,deeptutor/services/llm/provider_registry.py 这个文件只有 3 行,是对 deeptutor.services.provider_registry 的兼容再导出(llm/provider_registry.py:1-3)。两个同名文件、一个是真源一个是壳,搜索时别搜错。
四、实例池只有 2 个位置,缓存键里带密钥指纹
provider 对象不是每次调用都新建。llm/provider_factory.py:17 定义 _PROVIDER_POOL_MAXSIZE = 2,池子上限就是 2。
缓存键的构成写在 llm/provider_factory.py:28-42,逐项是:事件循环、provider 名(或 binding)、provider 模式、模型、api_key 的 sha256 前 16 位指纹、URL、api_version、headers、temperature、max_tokens、reasoning_effort。
两点值得单独说。第一,事件循环在键里,意味着这个池是跟 loop 绑的。第二,密钥不是明文进键,而是先取 sha256 前 16 位;这属于源码里的既有做法,我们只陈述它这么写了,不据此对本项目的密钥安全性下任何结论——你的 API Key 仍然是本机敏感数据,怎么存放、怎么隔离要按你自己的环境评估。
至于生成参数这一侧,默认值是 GenerationSettings(temperature=0.7, max_tokens=4096, reasoning_effort=None)(llm/provider_core/base.py:62-68),同样的 max_tokens: int = 4096 / temperature: float = 0.7 在几个后端的方法签名里重复出现。仓库里还有一批其它硬编码默认值,我们另有一篇专门逐个过,这里不铺开。
五、反直觉的第二处:两套 provider 抽象并存
这是本篇最值得记的一处,而且只看目录名根本看不出来。
生产路径是前面说的那条:services/provider_registry.py 提供元数据 → llm/provider_factory.py 分发 → llm/provider_core/ 里的 5 个类。但 deeptutor/services/llm/ 下还有一个 providers/ 子包,4 个文件、896 行:base_provider.py 204 行、routing.py 264 行、anthropic.py 258 行、open_ai.py 170 行。它自带一套独立的 register_provider 注册表(llm/registry.py),与 services/provider_registry.py 并存。
我们在全仓做了一次搜索:grep -rn 'services\.llm\.providers' . --include=*.py 只命中 2 处,均在 tests/ 下(tests/services/llm/test_base_provider.py:7、tests/services/llm/test_routing_provider.py:7);deeptutor/services/llm/*.py 里也没有 from .providers 之类的相对导入。
这个子包自己对定位有说明。llm/providers/routing.py:1-9 写:“This provider delegates to the existing function-based providers… It exists to keep the public API stable while incrementally migrating call sites to provider objects.”——按字面就是一层过渡性桥接。另外 llm/__init__.py:106 把 LLMClient / get_llm_client / reset_llm_client 标注为 “Client (legacy, prefer factory functions)”。
按纪律,我们只陈述可核查的事实:它存在、它有自己的注册表、生产代码里没有引用它、测试里有。不推断作者的意图,也不据此评价这个项目。对你的实际影响只有一条:照着 base_provider.py 或 routing.py 读逻辑,读到的可能不是你线上那条链——要确认当前跑的是哪一条,回 llm/provider_factory.py 而不是 llm/providers/。
六、能力表比注册表小一圈
第三处对不齐在能力表这侧。llm/capabilities.py:22 的 PROVIDER_CAPABILITIES 只有 23 个 binding 键,PROVIDERS 有 36 个 name,其中 16 个 provider 没有专属能力条目,它们是:aihubmix、atlascloud、byteplus_coding_plan、edenai、gemini、github_copilot、lemonade、novita、nvidia_nim、openai_codex、ovms、qianfan、stepfun、volcengine_coding_plan、xiaomi_mimo、zhipu——它们会落到 DEFAULT_CAPABILITIES(llm/capabilities.py:234;provider_registry.py:118-479)。
这里要提醒一句读法:两侧的键集并不是简单的包含关系,别拿 36 和 23 直接相减。能力表里还有一批不对应 ProviderSpec 的键,比如 llm/capabilities.py:61 的 claude 条目自述是 anthropic 的 alias,还有下一段要说的 together / together_ai。要判断某一家有没有专属条目,只能拿名字去两张表里各搜一遍。
反方向也有一条:llm/capabilities.py:148 与 :155 定义了 together / together_ai(后者注释写 # Alias),但 provider_registry.py 全文没有 together 字样(grep -c together 结果 0)。也就是这个能力条目没有对应的 ProviderSpec。两处不一致都以我们实读的仓库状态为准,说完就停。
DEFAULT_CAPABILITIES 那八个字段具体是什么、落到兜底之后意味着什么,BaseAgent 那一篇里已经展开过,这里不重复。
七、三条写在注释里的 provider 特例
名单之外,provider_registry.py 上还挂了几条只对特定家生效的约定,注释都写了缘由:
- Moonshot 的
model_overrides=(("kimi", {"temperature": None}),),注释说 Kimi 系列服务端锁死温度、传非固定值会返回 HTTP 400(provider_registry.py:335-342)。 - MiniMax 的注释写明全球站(api.minimax.io)与中国站(api.minimaxi.com)的密钥互不通用,默认走全球站(
provider_registry.py:344-356)。 - Codex 一侧有个 fallback 默认模型
CODEX_DEFAULT_MODEL = "gpt-5.6-sol",注释明确写它 “Fallback only, used when a caller does not name a model. The real list always comes from the signed-in account’s live catalog.”(codex_auth/constants.py:20-23)。另外llm/provider_core/openai_codex_provider.py:189的请求头里带着"OpenAI-Beta": "responses=experimental",READMEREADME.md:656也写了这条兼容路径 “is experimental: the upstream interface may change”——照实标出来。
这三条的共同点是:它们都是某一家服务端的约束反过来写进了客户端元数据。你自己接一家新的时,最容易漏掉的也是这类东西。
还有一个需要如实说明的边界:DeepTutor 除了通过 HTTP 调 provider,另有一个 services/subagent/ 层会驱动你本机已安装的 agent CLI 作为子代理(subagent/__init__.py:1-7),也就是会在本机执行外部程序。那条链和本文讲的 provider 接入是两回事,我们另有一篇专门讲,但读到”接入层”三个字时别把两者混为一谈。
八、你可以照着核的五步
- 打开
deeptutor/services/provider_registry.py:3-7,确认它自称单一真源,以及 “Order matters / Gateways first” 这句关于顺序的说明。 - 在同文件
:32-34找backend字段的注释,数一下候选值是 5 个;再去llm/provider_core/__init__.py:30-36数_LAZY_TYPES是不是也是 5 个类。 - 打开
llm/provider_factory.py:45-49,确认缺省分支落到openai_compat——这一条决定了名字写错时的表现。 - 跑一次
grep -rn 'services\.llm\.providers' . --include=*.py,看命中是不是只在tests/下。 - 把你要用的 provider 名分别去
provider_registry.py的PROVIDERS和llm/capabilities.py的PROVIDER_CAPABILITIES里搜一遍,看它是不是落在那 16 个没有专属条目的名字里。
需要说明的边界:本文只读了 provider_registry.py 的注释、ProviderSpec 定义、PROVIDERS 的 name= 行与部分默认值行,以及 provider_factory.py、provider_core/__init__.py、capabilities.py 的常量段;provider_core/ 下各后端的具体请求组装、鉴权流程与 codex_auth/service.py 的 OAuth 实现我们都没有读——其中最大的 provider_core/openai_compat_provider.py 有 862 行,我们同样没有逐行读。重试与超时那一整套常量另有一篇专讲,本文没有展开。上面出现的所有名单、字段与默认值都是源码中的既有内容,“存在相应代码路径”不等于”这条路径已被验证可用”。
本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.md、
pyproject.toml 与 deeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。
本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API,
因此不涉及生成质量、响应速度与教学效果的任何描述。
参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。