LLM 接入层:统一调用口与代码里出现的 provider 名单

2026-08-10

一个项目接了多少家模型服务,光看 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),字段里值得记住的几个:backendis_gatewayis_localis_oauthis_directdetect_by_key_prefixdetect_by_base_keyworddefault_api_basethinking_style。前面五个布尔量决定这条 spec 属于哪一类,后面几个决定「用户没明说是哪一家时,怎么从 key 前缀或 base_url 关键字反推」。

另外有 PROVIDER_ALIASES 22 条别名映射(provider_registry.py:77-100),例如 azure→azure_openaigoogle→geminiclaude→anthropicopenai_compatible→custom。你在配置里写了个不在名单上的写法却仍然能匹配上,多半是这张表在起作用。

二、36 个名字,按 spec 的布尔字段分五类

PROVIDERS 元组里 name= 出现 36 次。按 spec 上的布尔字段归类如下(行号均在 provider_registry.py):

类别名字(带行号)
直连(is_directcustom(:121)、custom_anthropic(:129)、azure_openai(:137)
网关(is_gatewayopenrouter(: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_localvllm(: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 个类:AnthropicProviderAzureOpenAIProviderGitHubCopilotProviderOpenAICodexProviderOpenAICompatProviderllm/provider_core/__init__.py:30-36)。选哪一个的分发函数是 _build_runtime_provider,按 spec.backend 分支 import,缺省落到 openai_compatllm/provider_factory.py:45-49)。

这个结构有两个直接后果,都是排查问题时用得上的:

其一,名单里绝大多数条目共用同一条 openai_compat 代码路径,差别只在 spec 上那几个字段(default_api_basedetect_by_key_prefixdetect_by_base_keywordthinking_stylemodel_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:7tests/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:106LLMClient / get_llm_client / reset_llm_client 标注为 “Client (legacy, prefer factory functions)”。

按纪律,我们只陈述可核查的事实:它存在、它有自己的注册表、生产代码里没有引用它、测试里有。不推断作者的意图,也不据此评价这个项目。对你的实际影响只有一条:照着 base_provider.pyrouting.py 读逻辑,读到的可能不是你线上那条链——要确认当前跑的是哪一条,回 llm/provider_factory.py 而不是 llm/providers/

六、能力表比注册表小一圈

第三处对不齐在能力表这侧。llm/capabilities.py:22PROVIDER_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_CAPABILITIESllm/capabilities.py:234provider_registry.py:118-479)。

这里要提醒一句读法:两侧的键集并不是简单的包含关系,别拿 36 和 23 直接相减。能力表里还有一批不对应 ProviderSpec 的键,比如 llm/capabilities.py:61claude 条目自述是 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",README README.md:656 也写了这条兼容路径 “is experimental: the upstream interface may change”——照实标出来。

这三条的共同点是:它们都是某一家服务端的约束反过来写进了客户端元数据。你自己接一家新的时,最容易漏掉的也是这类东西。

还有一个需要如实说明的边界:DeepTutor 除了通过 HTTP 调 provider,另有一个 services/subagent/ 层会驱动你本机已安装的 agent CLI 作为子代理subagent/__init__.py:1-7),也就是会在本机执行外部程序。那条链和本文讲的 provider 接入是两回事,我们另有一篇专门讲,但读到”接入层”三个字时别把两者混为一谈。

八、你可以照着核的五步

  1. 打开 deeptutor/services/provider_registry.py:3-7,确认它自称单一真源,以及 “Order matters / Gateways first” 这句关于顺序的说明。
  2. 在同文件 :32-34backend 字段的注释,数一下候选值是 5 个;再去 llm/provider_core/__init__.py:30-36_LAZY_TYPES 是不是也是 5 个类。
  3. 打开 llm/provider_factory.py:45-49,确认缺省分支落到 openai_compat——这一条决定了名字写错时的表现。
  4. 跑一次 grep -rn 'services\.llm\.providers' . --include=*.py,看命中是不是只在 tests/ 下。
  5. 把你要用的 provider 名分别去 provider_registry.pyPROVIDERSllm/capabilities.pyPROVIDER_CAPABILITIES 里搜一遍,看它是不是落在那 16 个没有专属条目的名字里。

需要说明的边界:本文只读了 provider_registry.py 的注释、ProviderSpec 定义、PROVIDERSname= 行与部分默认值行,以及 provider_factory.pyprovider_core/__init__.pycapabilities.py 的常量段;provider_core/ 下各后端的具体请求组装、鉴权流程与 codex_auth/service.py 的 OAuth 实现我们都没有读——其中最大的 provider_core/openai_compat_provider.py 有 862 行,我们同样没有逐行读。重试与超时那一整套常量另有一篇专讲,本文没有展开。上面出现的所有名单、字段与默认值都是源码中的既有内容,“存在相应代码路径”不等于”这条路径已被验证可用”。


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

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