数据截至 (上游 commit ceade4cbe9f2)
Provider 架构:注册、解析与自动路由装配
30 秒导读: OGX 对外只暴露一套稳定的 API(inference、vector_io、tool_runtime……),背后真正干活的是可插拔的 provider(OpenAI、vLLM、Faiss、Brave Search……)。这一章讲清楚一件事:一份 YAML 配置,是怎么变成一个「活的、可调用的实现字典」的——中间要经过 provider 声明(spec)、注册表收集、依赖拓扑排序、动态实例化、协议校验,还要为几个特殊 API 自动装配「路由表 + 路由器」这对搭档。
本章在全书的位置: 先读 index 建立全景、01 请求生命周期 了解服务器骨架(
resolve_impls就是在服务器启动时被调用的)。本章讲装配——把 provider 装进一个impls字典;运行时那个字典里的 Router 到底怎么查表、把一次模型调用落到某个后端,是 03 路由层与 OpenAI 适配 的事,本章不展开。
1. 这章解决什么问题(先建直觉)
一句话定义: OGX 是一台「OpenAI 兼容的 API 服务器」,但它自己不实现推理、不实现向量库——它把每个 API 的实现权外包给 provider,再用一套解析器把你选中的 provider 装配起来。
为什么需要这层抽象? 想象你写了一堆代码调 OpenAI SDK。今天想换成本地 vLLM,明天想接 Anthropic,后天要在向量检索里换 Faiss 为 pgvector。如果这些后端的差异会渗透到你的业务代码,那每换一次都要改代码。OGX 的答案是:
- API 是契约(一个 Python
Protocol,如Inference),永远不变; - provider 是实现(一个 adapter 类),可随便换;
- 换后端 = 改一行 YAML,业务代码一个字不动。
一句话类比: 把它想成操作系统的驱动模型。应用程序调的是统一的「打印」系统调用(= API),具体是惠普还是佳能打印机由驱动(= provider)负责;开机时内核扫描并加载驱动(= 本章的注册表 + 解析器),把「打印」这个调用接到真实硬件上。
这一章的主线,就是把下面这条链路讲透:
run config (YAML) ──► 一个可调用的 impls 字典 { Api.inference: <某个活的实现>, ... }
本节不碰代码;下面开始拆。
2. 顶层全景:从 YAML 到 impls 字典
先给一张「怎么读」的 图:从左到右是启动时的装配顺序,数据一路被加工,最终产出右边那个 impls 字典。
你写的 代码里"有哪些 把两者对上、排序、 启动完成后
run config provider 可选" 实例化、校验
(YAML) (注册表)
┌──────────┐ ┌──────────────┐ ┌────────────────────┐ ┌──────────────┐
│StackConfig│ ───► │provider │ ───► │ resolve_impls │ ───► │ impls 字典 │
│.providers │ │ registry │ │ ①校验 ②补路由 │ │ {Api: 实现} │
│ 每个 API │ │ {Api:{类型: │ │ ③拓扑排序 ④import │ │ │
│ 选哪些 │ │ spec}} │ │ ⑤实例化 ⑥协议校验 │ │ 交给服务器路由│
└──────────┘ └──────────────┘ └────────────────────┘ └──────────────┘
│ ▲
│ │ available_providers()
│ ┌──────┴───────┐
└─(选择的类型) │ registry/ │ 每个 API 一个文件:
│ inference.py│ inference.py / vector_io.py / tool_runtime.py …
│ vector_io.py│
└──────────────┘
部件一句话职责:
| 部件 | 干什么 | 在哪 |
|---|---|---|
StackConfig | 运行配置的根对象:providers 里写「每个 API 用哪些 provider」 | core/datatypes.py:836 |
ProviderSpec(及子类) | 一个 provider 的「身份证」:类型、模块路径、config 类、依赖、pip 包 | ogx_api/datatypes.py:228 |
available_providers() | 每个 API 一个文件,返回该 API 所有内置 provider 的 spec 列表 | providers/registry/*.py |
get_provider_registry | 收集所有 API 的 spec,拼成 {Api: {provider_type: spec}} | core/distribution.py:101 |
builtin_automatically_routed_apis | 声明三对「路由表↔路由器」配对 | core/distribution.py:59 |
resolve_impls | 解析引擎:校验 → 补自动路由 → 拓扑排序 → 实例化 | core/resolver.py:151 |
distributions/* | 预制发行版:半自动生成的 config.yaml + <name>.py | distributions/starter/ 等 |
主线走一遍(高层): 服务器启动 → 读 YAML 得到 StackConfig → get_provider_registry() 把代码里所有可选 provider 收成一张表 → resolve_impls() 拿着「你选的」和「所有可选的」对账、排序、逐个 importlib.import_module 并调工厂函数,得到实例 → 校验实例确实实现了 API 协议 → 返回 impls 字典交给服务器。装配就完成了。
3. Provider Spec:provider 的「身份证」
本节讲:一个 provider 在被实例化之前,是怎么用一份纯声明(不含任何实现代码)描述自己的。
3.1 基类 ProviderSpec
所有 provider 都先是一份 ProviderSpec——一个 Pydantic 模型,只有元数据,没有行为。关键字段:
| 字段 | 含义 |
|---|---|
api | 这个 provider 实现的是哪个 API(Api.inference 等) |
provider_type | 唯一类型名,如 remote::openai、inline::sentence-transformers |
config_class | 全限定类名字符串,指向这个 provider 的 Pydantic config 类 |
module | 全限定模块名字符串,该模块里应有工厂函数 get_adapter_impl / get_provider_impl |
api_dependencies | 依赖的其它 API(如 file-search 依赖 vector_io+inference+files) |
optional_api_dependencies | 可选依赖(有就用,没有也能起) |
pip_packages | 这个 provider 需要的 pip 依赖(用于 build/list-deps) |
deprecation_warning / deprecation_error | 弃用告警/弃用报错(校验时触发) |
toolgroup_id | 仅 tool_runtime 用:该 provider 服务的内置工具组 ID(如 builtin::websearch) |
真实定义见 ogx_api/datatypes.py:228 class ProviderSpec。注意 config_class 和 module 都是字符串——这是整套架构能「延迟加载」的关键:spec 只是写下「去哪 import」,真正的 import 推迟到解析阶段才发生(见 §6)。
字段里还藏着一个内部 hack:
# ogx_api/datatypes.py:283 # 真实源码
# used internally by the resolver; this is a hack for now
deps__: list[str] = Field(default_factory=list)
deps__ 是解析器内部临时用来做拓扑排序的依赖列表,不是给用户填的。§6 会看到它被怎么塞值、怎么用。
3.2 两种「用户可 见」的 spec:Inline vs Remote
面向用户的 provider 分两类,对应两个子类:
InlineProviderSpec | RemoteProviderSpec | |
|---|---|---|
| 语义 | 进程内内置实现(库直接跑在服务器里) | 通过 adapter 连到外部服务 |
| 类型名前缀 | inline::<name> | remote::<adapter_type> |
| 工厂函数 | get_provider_impl(config, deps) | get_adapter_impl(config, deps) |
| 独有字段 | container_image(可选容器镜像) | adapter_type(适配器唯一标识) |
| 例子 | inline::sentence-transformers、inline::file-search | remote::openai、remote::vllm、remote::brave-search |
定义分别在 ogx_api/datatypes.py:298(InlineProviderSpec)与 :337(RemoteProviderSpec)。RemoteProviderSpec 里 container_image 直接被固定为 None(remote 不带镜像)。
一段注册声明长这样(这是真实源码,不是示意):
# providers/registry/inference.py:54 RemoteProviderSpec
RemoteProviderSpec(
api=Api.inference,
adapter_type="cerebras",
provider_type="remote::cerebras",
pip_packages=[],
module="ogx.providers.remote.inference.cerebras",
config_class="ogx.providers.remote.inference.cerebras.CerebrasImplConfig",
provider_data_validator="...CerebrasProviderDataValidator",
description="Cerebras inference provider ...",
),
一眼看懂:它没写任何实现,只写了「我是谁、去哪 import、配置类是哪个」。
3.3 两种「内部专用」的 spec:AutoRouted 与 RoutingTable
还有两个子类,用户永远不会在 YAML 里写,是解析器给自动路由 API 现造的(§5、§6):
| spec | 代表的 API 例子 | 干什么 |
|---|---|---|
AutoRoutedProviderSpec | /inference、/vector_io | 「路由器」侧:请求进来后按 key 分发到某个真实 provider |
RoutingTableProviderSpec | /models、/vector_stores | 「路由表」侧:管理「哪个 model id / store 属于哪个 provider」的注册表 |
它们定义在 core/datatypes.py:103(AutoRoutedProviderSpec)与 :117(RoutingTableProviderSpec)——注意是在 core 而非 ogx_api,因为这纯属实现细节。两者的 module 都固定指向 ogx.core.routers。为什么要成对出现,下一节讲。
4. 注册表:每个 API 一个 available_providers()
本节讲:代码里「一共有哪些 provider 可选」这张表是怎么攒出来的。
4.1 一个 API 一个文件
src/ogx/providers/registry/ 下每个 API 一个 .py 文件,每个文件导出一个 available_providers(),返回该 API 的全部内置 spec:
providers/registry/
inference.py → available_providers() 返回 ~25 个推理 provider
vector_io.py → Faiss / sqlite-vec / pgvector / qdrant / ...
tool_runtime.py → file-search / brave-search / bing-search / ...
files.py batches.py responses.py ...
providers/registry/inference.py:29 def available_providers 就是一个纯函数,return [ ...一堆 ProviderSpec... ](§3.2 那段就是它的一个元素)。加一个新推理后端 = 往这个列表里加一个 spec,不用改任何解析逻辑。
4.2 get_provider_registry:把所有文件收成一张表
core/distribution.py:101 get_provider_registry 用 importlib 动态导入每个 API 对应的注册模块,调用其 available_providers(),拼成一个两层字典:
# core/distribution.py:140 # 真实源码(节选)
for api in providable_apis():
name = api.name.lower()
module = importlib.import_module(f"ogx.providers.registry.{name}")
registry[api] = {a.provider_type: a for a in module.available_providers()}
产出结构就是全书反复出现的那个 ProviderRegistry:
{ Api.inference: { "remote::openai": <spec>, "remote::vllm": <spec>, ... },
Api.vector_io: { "inline::faiss": <spec>, ... },
... }
注意它只遍历 providable_apis()——即排除了内部 API 和自动生成的路由表 API(见 §5)。
4.3 外部 provider:两条旁路
get_provider_registry 还支持在内置之外塞第三方 provider,两条路:
- 目录式(将弃用): 配置
external_providers_dir,从providers.d/{remote,inline}/<api>/*.yaml读 spec(get_external_providers_from_dir,core/distribution.py:184)。 - 模块式(推荐): 直接在 provider 配置里写
module: some_pkg,解析时import some_pkg.provider并调其get_provider_spec()(get_external_providers_from_module,core/distribution.py:255)。
这让「装一个 pip 包就多一个 provider」成为可能,无需改 OGX 源码。
5. 自动路由的配对表:为什么 /models 和 /inference 是一对
本节讲:OGX 里最巧的一个设计——有几个 API 是自动成对生成的,用户不能也不用手写它们的 provider。
5.1 三对搭档
有些 API 天生分「管理端」和「执行端」两半:
- 你注册 model(
/models),然后调用 model 做推理(/inference); - 你建 vector store(
/vector_stores),然后查 它(/vector_io); - 你列 tool group(
/tool_groups),然后跑 工具(/tool_runtime)。
「管理端」需要一张路由表(记录 gpt-4o 属于哪个 provider);「执行端」需要一个路由器(拿到请求按 model id 查表、转发)。OGX 把这种配对写死成一张表:
# core/distribution.py:59 builtin_automatically_routed_apis() # 真实源码(节 选)
return [
AutoRoutedApiInfo(routing_table_api=Api.models, router_api=Api.inference),
AutoRoutedApiInfo(routing_table_api=Api.tool_groups, router_api=Api.tool_runtime),
AutoRoutedApiInfo(routing_table_api=Api.vector_stores, router_api=Api.vector_io),
]
| 路由表 API(管理端) | 路由器 API(执行端) | 直觉 |
|---|---|---|
models | inference | 注册模型 vs 调用模型 |
tool_groups | tool_runtime | 声明工具 vs 执行工具 |
vector_stores | vector_io | 建库 vs 查库 |
5.2 用户只配「执行端」,「管理端」白送
关键规则:用户 只在 YAML 里为路由器 API(inference/vector_io/tool_runtime)配 provider;路由表 API(models/vector_stores/tool_groups)由系统自动装配,手写会直接报错:
# core/resolver.py:233 validate_and_prepare_providers # 真实源码
if api in routing_table_apis:
raise ValueError(f"Provider for `{api_str}` is automatically provided and cannot be overridden")
这也是为什么 §4.2 的 get_provider_registry 用 providable_apis()——它故意跳过路由表 API(core/distribution.py:81,providable_apis 从全部 API 里减去 routing_table_apis 和 INTERNAL_APIS)。INTERNAL_APIS(core/distribution.py:32,含 inspect/providers/prompts 等)则是根本不走 provider 机制、由内置实现直接提供的一批。
6. 解析引擎 resolve_impls:从 spec 到活实例
本节是本章的核心。讲清楚
core/resolver.py:151resolve_impls这一个函数怎么把配置变成可调用的impls。它在服务器启动时被调用一次(见core/stack.py:806,承接 01)。
6.1 五步全景
resolve_impls 的函数体只有五句,每句一个阶段:
① validate_and_prepare_providers 校验用户选的 provider,组织成 {key: {id: ProviderWithSpec}}
② specs_for_autorouted_apis 为每对自动路由 API 现造 RoutingTable + AutoRouted 两个 spec
③ sort_providers_by_deps 用 graphlib 按依赖做拓扑排序
④ instantiate_providers 按序 import + 调工厂函数,拿到实例
⑤ (在④内) check_protocol_compliance 校验实例真的实现了 API 协议
│
▼
返回 impls: {Api: 实现}
下面逐步拆。
6.2 ① 校验与「inner-」前缀
validate_and_prepare_providers(core/resolver.py:229)遍历 run_config.providers,对每个 provider:
- 跳过被禁用的(
provider_id为空或__disabled__); validate_provider校验类型确实在注册表里、并触发弃用告警/报错(core/resolver.py:258);- 从注册表取出对应 spec,给
deps__塞值:= api_dependencies + optional_api_dependencies(core/resolver.py:248); - 把
Provider(用户配置)和ProviderSpec(注册表元数据)合成一个ProviderWithSpec。
有个关键细节:执行端 API 的 key 会被加上 inner- 前缀:
# core/resolver.py:248 # 真实源码
key = api_str if api not in router_apis else f"inner-{api_str}"
为什么?因为 inference 这个名字要留给自动路由器(那个对外的 Router);用户配的真实 provider(如 remote::openai)被藏到 inner-inference 下,成为 Router 内部转发的目标。这一步是「路由器包住真实 provider」这层结构的起点。
6.3 ② 现造自动路由的 spec
specs_for_autorouted_apis(core/resolver.py:182)对 §5.1 每一对,凭空造两个 ProviderWithSpec:
- 路由表侧:
RoutingTableProviderSpec,provider_id="__routing_table__",deps__=[f"inner-{router_api}"]——即它依赖那些被藏起来的真实 provider; - 路由器侧:
AutoRoutedProviderSpec,provider_id="__autorouted__",依赖对应的路由表。
还有一处特判:vector_io 的路由器额外声明 inference 为可选依赖,以支持「查询改写」(query rewriting):
# core/resolver.py:203 # 真实源码
if info.router_api == Api.vector_io:
optional_deps = [Api.inference]
deps_list.append(Api.inference.value)
于是依赖图里出现了这样一条链:models 路由表 → inner-inference(真实 provider),inference 路由器 → models 路由表。拓扑排序要靠它。
6.4 ③ 拓扑排序(graphlib)
依赖必须先于被依赖者实例化(要先有 inference 实例,才能实例化依赖它的 file-search)。topological_sort(core/resolver.py:354)直接用标准库 graphlib.TopologicalSorter:
# core/resolver.py:351 # 真实源码(节选)
ts: graphlib.TopologicalSorter[str] = graphlib.TopologicalSorter()
for api_str, providers in providers_with_specs.items():
deps = set()
for provider in providers:
for dep in provider.spec.deps__: # 就是 §6.2 塞进去的 deps__
if dep in providers_with_specs:
deps.add(dep)
ts.add(api_str, *deps)
排序键是 API 名字(含 inner- 前缀),依赖来自各 spec 的 deps__。若存在环,graphlib 抛 CycleError,被包装成清晰的 RuntimeError(core/resolver.py:385)。这是 deps__ 这个 hack 的唯一用途:给排序器喂一张字符串依赖图。
6.5 ④ 实例化 + 工厂分发
instantiate_providers(core/resolver.py:290)按拓扑序逐个处理,维护一个累积的 impls 字典;实例化前先从 impls 里取出该 provider 的依赖注入:
# core/resolver.py:303 # 真实源码
deps = {a: impls[a] for a in provider.spec.api_dependencies}
真正 import 和调用发生在 instantiate_provider(core/resolver.py:390)。它先 importlib.import_module(provider_spec.module),再按 spec 的类型选不同工厂函数——这是整段的枢纽,可看成一张分发表:
| spec 类型 | 调用的工厂方法 | 传入参数(简化) |
|---|---|---|
RemoteProviderSpec | get_adapter_impl | (config, deps) |
InlineProviderSpec(默认分支) | get_provider_impl | (config, deps) |
AutoRoutedProviderSpec | get_auto_router_impl | (api, 路由表, deps, run_config, policy) |
RoutingTableProviderSpec | get_routing_table_impl | (api, inner_impls, deps, dist_registry, policy) |
对前两类,config_class 字符串在这里才被 instantiate_class_type 真正 import 成 Pydantic 类,并用 provider.config(YAML 里那段)实例化:
# core/resolver.py:442 # 真实源码(InlineProviderSpec 分支)
method = "get_provider_impl"
config_type = instantiate_class_type(provider_spec.config_class)
provider_config = _inject_config_defaults(config_type, provider.config.copy())
config = config_type(**provider_config)
args = [config, deps]
后两类(路由器/路由表)的工厂就在 core/routers/__init__.py:get_auto_router_impl(:45)按 API 名映射到 InferenceRouter/VectorIORouter/ToolRuntimeRouter;get_routing_table_impl(:19)映射到 ModelsRoutingTable/VectorStoresRoutingTable/ToolGroupsRoutingTable。它们运行时怎么查表转发,是 03 的主题。
这里还能看到 §6.2 那个 inner- 前缀如何收口:实例化结果按 key 分流——inner- 开头的存进「按 provider_id 分组的内部表」,供路由表当作转发目标;其余的才进对外的 impls[api]:
# core/resolver.py:321 # 真实源码
if api_str.startswith("inner-"):
inner_impls_by_provider_id[api_str][provider.provider_id] = impl
else:
impls[Api(api_str)] = impl
6.6 ⑤ 协议合规校验
拿到实例后,instantiate_provider 立刻校验它真的实现了对应 API 的 Protocol——check_protocol_compliance(core/resolver.py:629)遍历协议里所有带 __webmethods__ 的方法,逐个检查实例:方法在不在、可不可调用、签名参数是否兼容,以及是不是「只有协议里有、实现里其实没写」的空壳。任一不满足就抛 ValueError,列出缺失方法。