跳到主要内容

路由层与 OpenAI 兼容适配:一次模型调用如何落到后端

30 秒导读: 你向 OGX 发一个 {"model": "gpt-4", ...} 的 chat completion 请求。OGX 内部 没有"gpt-4"这个东西——它只有一堆 provider(OpenAI、vLLM、Ollama、Bedrock…)。这一章追踪 一个字符串 model_id 如何被一步步翻译、定位到某个 provider,再被"整形"成标准 OpenAI 调用 打到真实后端。读完你能一口气讲清:"gpt-4" → 路由表查表 → InferenceRouter 委派 → OpenAIMixin.openai_chat_completionAsyncOpenAI.chat.completions.create 的完整链路。

本章是 OGX 全景 的第 3 章。上游怎么把请求送到 Router、provider 怎么被装配进路由表, 见 01 请求生命周期02 Provider 架构; 本章拿到的是"Router 与路由表已经就位"这个起点。服务端的 agentic 编排(Responses)是 04 章, 不在这里讲。


1. 这一章解决的那个小问题

一句话:model 是个字符串,后端是个对象,中间隔着两层。

客户端只知道一个名字("gpt-4""my-llama""openai/gpt-4o-mini")。OGX 要回答三个问题, 才能真正发出请求:

问题谁回答产出
这个名字归哪个 provider?路由表(routing table)一个 provider 实例
后端真正认识的模型名是什么?路由表 + OpenAIMixinprovider_resource_id(如 gpt-4o-mini)
怎么把请求打成 OpenAI 形状发出去?OpenAIMixin + openai_compat一次 AsyncOpenAI 调用

这三步就是本章的三节主线。核心洞见是:model_id 会被翻译两次——先在 Router 侧由路由表把 "注册名"翻成"后端名",再在 OpenAIMixin 侧兜底翻一次(通常是幂等直通)。理解这个"两级翻译", 整章就通了。


2. 顶层全景:一次 inference 的路径

怎么读这张图: 从左到右是一次非流式 chat completion 的控制流;方框里第二行小字是真实符号名, 方便你 grep。虚线框是"查表/翻译"动作。

HTTP POST /v1/chat/completions {"model":"gpt-4", messages:[...]}


┌────────────────────────┐
│ ① Router:按模型选后端 │ InferenceRouter.openai_chat_completion (inference.py:198)
│ 并翻译模型名 │ ├─ _get_model_provider (inference.py:121)
└───────────┬────────────┘ │
│ │ ┌ · · · · · · · · · · · · · · · · · · ·┐
│ └─▶│ ② 路由表:查表 │
│ │ get_object_by_identifier("model",…) │ (common.py:164)
│ │ → 命中 → get_provider_impl(identifier)│ (models.py:296)
│ │ 读缓存/落盘的 DistributionRegistry │ (registry.py)
│ └ · · · · · · · · · · · · · · · · · · · · ┘
│ 产出:provider 实例 + provider_resource_id

┌────────────────────────┐
│ ③ 适配层:整成 OpenAI │ OpenAIMixin.openai_chat_completion (openai_mixin.py:400)
│ 形状并发出 │ ├─ _get_provider_model_id (兜底再翻一次) (openai_mixin.py:300)
│ │ ├─ client (构造/复用 AsyncOpenAI) (openai_mixin.py:227)
│ │ └─ prepare_openai_completion_params (openai_compat.py:60)
└───────────┬────────────┘

AsyncOpenAI.chat.completions.create(model="gpt-4o-mini", …)


真实后端(api.openai.com / vLLM / Ollama / …)

各部件一句话职责:

部件干什么文件
InferenceRoutermodel_id 选出 provider,委派调用,收尾算指标/落库core/routers/inference.py:73
ModelsRoutingTable模型的注册表:查表、注册、动态发现、访问控制core/routing_tables/models.py:41
CommonRoutingTableImpl所有路由表的公共底座:落库、RBAC 钩子、通用查表core/routing_tables/common.py:88
DistributionRegistry把"哪个名字归哪个 provider"持久化到 KVStore + 内存缓存core/store/registry.py:22
OpenAIMixin把"任意 remote 后端"统一成 OpenAI 形状的适配基类providers/utils/inference/openai_mixin.py:51
openai_compat / model_registry参数整形 + provider 配置/别名模型表providers/utils/inference/*.py

3. 路由层:名字如何变成一个 provider

这一节讲图里的 ①②——路由表怎么把一个字符串定位到某个 provider,并顺带翻出后端认识的模型名。

3.1 先分清两个 model_id(整章的地基)

路由表里每个模型是一个 Model 对象,身上有两个关键字段,千万别混:

字段含义例子
identifier客户端暴露的注册名(OGX 命名空间)gpt-4openai/gpt-4o-minimy-llama
provider_resource_id后端真正认识的模型名gpt-4o-mini
provider_id归属哪个 provider 实例openaivllmollama

路由 = 把 identifier 翻成 (provider 实例, provider_resource_id) 记住这句话,后面全是它的展开。

3.2 注册:一条模型如何进表

在服务发起调用前,模型得先"在册"。有三条注册路径,最终都汇到 register_object:

  • 手动注册:用户调 POST /v1/models,进 ModelsRoutingTable.register_model(models.py:315)。
  • 动态发现:后台 refresh() 定时向 provider 要 list_models(),把结果灌进 update_registered_models(models.py:414)。
  • 配置启动:distribution 配置里预声明的模型,初始化时落库。

register_model 做几件事,最有意思的是给名字补前缀auto 解析:

# 示意,非源码 —— 摘自 ModelsRoutingTable.register_model 的核心逻辑
provider_model_id = provider_model_id or model_id # 后端名缺省=注册名
if provider_model_id == "auto": # "auto" → 问 provider 要第一个匹配的模型
provider_model_id = await self._resolve_auto_model(provider_id, model_type)
if model_id.startswith(f"{provider_id}/"): # 已带前缀就不重复加
identifier = model_id
else:
identifier = f"{provider_id}/{model_id}" # 否则加 provider 前缀做命名空间

真实实现见 models.py:315register_model;"auto" 别名解析见 _resolve_auto_model(models.py:46)—— 它调 provider.list_models(),按 model_type 过滤后取第一个当作真实模型名。

补前缀这步是多 provider 共存的关键:两个后端都叫 llama3 也不会撞,因为注册名变成 vllm/llama3ollama/llama3

注册的最后一步落到公共底座 CommonRoutingTableImpl.register_object(common.py:184):它先跑 创建权限检查,再调 register_object_with_provider(common.py:37,按 API 类型分派到 p.register_model),最后 dist_registry.register(...) 落库。

# 示意,非源码 —— register_object 的骨架(common.py:184)
if not obj.provider_id: # 没指定就挑第一个 provider
obj.provider_id = list(self.impls_by_provider_id.keys())[0]
creator = get_authenticated_user()
if not is_action_allowed(self.policy, "create", obj, creator): # ← 访问控制钩子
raise AccessDeniedError("create", obj, creator)
obj.owner = creator # 记下属主(供后续 ABAC)
registered = await register_object_with_provider(obj, p) # 通知 provider "你多了个模型"
await self.dist_registry.register(registered) # 落 KVStore + 缓存

3.3 解析:调用时怎么查表(本章最容易看错的一处)

调用时的查表有两个不同入口,别搞混:

入口签名谁用特点
CommonRoutingTableImpl.get_provider_impl(routing_key, provider_id=None)tool_runtime / vector_io 路由表同步缓存 get_cached,不查磁盘
ModelsRoutingTable.get_provider_impl(model_id)inference 走这条(方法被重写)lookup_model → 带 RBAC 的异步查

也就是说:任务里提到的 common.py:130 那个基类版 get_provider_impl 是给工具组/向量库路由表用的; 模型路由表把它重写了(models.py:296),inference 请求命中的是重写版。基类版长这样:

# 示意,非源码 —— 基类 get_provider_impl(common.py:130),tool/vector 用
obj = self.dist_registry.get_cached(objtype, routing_key) # 同步读内存缓存
if not obj:
raise ValueError(f"{objtype} `{routing_key}` not served by ...")
if not provider_id or provider_id == obj.provider_id:
return self.impls_by_provider_id[obj.provider_id] # 名字 → provider 实例

而 inference 侧真正走的是 InferenceRouter._get_model_provider(inference.py:121),它做了两次查表:

# 示意,非源码 —— _get_model_provider(inference.py:121)
model = await self.routing_table.get_object_by_identifier("model", model_id) # 查①:拿 Model 对象(带 RBAC)
if model:
if model.model_type != expected_model_type: # 顺手校验类型(llm/embedding/rerank)
raise ModelTypeError(...)
provider = await self.routing_table.get_provider_impl(model.identifier) # 查②:拿 provider 实例
return provider, model.provider_resource_id # ★ 返回 provider + 后端模型名
return await self._get_provider_by_fallback(model_id, expected_model_type) # 没命中 → 走兜底

get_object_by_identifier(common.py:164)是"带门禁的查表":查到对象后立刻跑 is_action_allowed(policy, "read", obj, user),读权限不足就当作查不到(返回 None),不泄露存在性。

3.4 兜底:provider_id/model_id 直连

如果注册表里查不到,_get_provider_by_fallback(inference.py:133)给一条后门:把 model_id/ 切成 provider_id + provider_resource_id,只要那个 provider 存在,就临时拼一个 ModelWithOwner 过一遍 RBAC,直接用。这让客户端能用 "openai/gpt-4o-mini" 这种"裸后端名"直连,不必事先注册。

"openai/gpt-4o-mini"
│ split("/", 1)

provider_id="openai" provider_resource_id="gpt-4o-mini"
│ provider 在册? 且 RBAC read 通过?

impls_by_provider_id["openai"], "gpt-4o-mini"

3.5 访问控制钩子:路由不旁路 ABAC

路由表是强制安全边界,不是纯查表器。每个入口都挂了 is_action_allowed:

动作钩子位置效果
读(查表/列表)get_object_by_identifier common.py:164get_all_with_type common.py:237无权 → 视为不存在
创建(注册)register_object common.py:184无权 → AccessDeniedError
删除(注销)unregister_object common.py:177无权 → AccessDeniedError
任意动作断言assert_action_allowed common.py:223先按 type/identifier 取对象,再判权限

assert_action_allowed(common.py:223)是给上层"我要对这个资源做 X,先替我把关"用的通用闸门。 多租户/ABAC 的完整机制(policy、owner、attributes)见 06 存储与多租户; 这里只需记住:路由的每一步都顺带做了门禁,绕不过去。

3.6 DistributionRegistry:把路由表落盘

路由表本身不存数据,数据在 DistributionRegistry(registry.py:22)。生产用的是 CachedDiskDistributionRegistry(registry.py:143):内存缓存 + KVStore 落盘两层。

  • get_cached(registry.py:184)是纯内存、同步读——基类 get_provider_impl 靠它做到"查表零 IO"。
  • get(registry.py:219)缓存未命中再读磁盘,兼顾多 worker 场景(别的进程写的对象也能看到)。
  • register(registry.py:238)先读权威磁盘值再写,缓存随后更新;子集重复注册是幂等 no-op。
  • 缓存有 TTL(默认 5s),_refresh_cache_from_db 定期与磁盘对账,解决多 worker 各自缓存漂移。
get_provider_impl(基类) get_object_by_identifier / register
│ │
▼ ▼
get_cached(同步·内存) ─miss→ get(异步) ──→ KVStore(磁盘/SQL/…)
▲ │
└──── 5s TTL 回填缓存 ┘

存储层与 KVStore 的更多细节在 06 章


4. Router 侧:选完后端之后做什么

InferenceRouter(inference.py:73)是 Inference 协议的实现,但它自己不推理——它只做 "选后端 + 委派 + 收尾"。核心是每个 openai_* 方法开头那三行相同的舞步:

# 示意,非源码 —— openai_chat_completion 的开场(inference.py:198)
request_model_id = params.model # 记住客户端给的原始名
provider, provider_resource_id = await self._get_model_provider( # 查表:拿 provider + 后端名
params.model, ModelType.llm
)
params.model = provider_resource_id # ★ 把 model 换成后端认识的名字
...
response = await provider.openai_chat_completion(params) # 委派给具体 provider
response.model = request_model_id # ★ 回填成客户端给的名字

两个 是关键对称动作:

  1. 进门翻译:把 params.modelidentifier 换成 provider_resource_id,这样打到后端的是后端认识的名字。
  2. 出门还原:响应里 response.model 改回客户端最初给的名字,客户端看到的始终是自己传的那个 model

openai_completion(inference.py:177)、openai_embeddings(inference.py:298)是同一套模式,只是 expected_model_type 分别是 llm/embedding——类型不符会在 _get_model_provider 里报 ModelTypeError

流式路径额外套一层 stream_tokens_and_compute_metrics_openai_chat,一边转发 chunk 一边攒指标/落库; 非流式则在 finally 里记 inference_duration 等指标。这些指标/落库属于收尾,不影响"落到哪个后端"的主线, 细节留给读者按 inference.py:382 去看。

小结:Router 的全部智能就是"用路由表把 model 名翻一道,委派,再翻回来"。真正把请求变成 HTTP 的活,在下一节的适配层。


5. 适配层:OpenAIMixin 如何把任意后端统一成 OpenAI 形状

这是本章的技术核心。OGX 支持一大堆 remote 后端(OpenAI、vLLM、Ollama、Together、Groq、Fireworks、 Azure、Databricks…),它们大多暴露 OpenAI 兼容的 HTTP 接口OpenAIMixin(openai_mixin.py:51) 就是那块"公共适配底座":一个 provider 只要继承它、实现一个 get_base_url(),就能免费得到 chat/completion/embeddings 全套。

5.1 一个 provider 有多"薄"

看具体后端就懂这层多省事。OpenAI 官方 adapter 几乎是空的:

# 示意,非源码 —— OpenAIInferenceAdapter(remote/inference/openai/openai.py:49)
class OpenAIInferenceAdapter(OpenAIMixin):
provider_data_api_key_field: str = "openai_api_key" # 允许按请求头带 key
def get_base_url(self) -> str:
return str(self.config.base_url) # 就这一行是"必答题"

vLLM 的同样只需实现 get_base_url(vllm/vllm.py:79)。"接一个新 OpenAI 兼容后端 ≈ 写一个 get_base_url"——这就是这套设计的杠杆。

5.2 client:构造并复用 AsyncOpenAI

client 属性(openai_mixin.py:227)是所有调用的出口。它做两件事:拼出 key/base_url, (api_key, base_url) 缓存客户端以复用连接。

# 示意,非源码 —— client 属性核心(openai_mixin.py:227)
api_key = self._get_api_key_from_config_or_provider_data() # 配置 key 或按请求头覆盖
if not api_key:
raise ValueError("API key not provided. ...")
base_url = self.get_base_url() # ← 子类唯一必答
cache_key = (api_key, base_url)
if self._cached_client and self._cached_client_key == cache_key:
return self._cached_client # 命中:直接复用
client = AsyncOpenAI(api_key=api_key, base_url=base_url, **extra_params)
self._cached_client, self._cached_client_key = client, cache_key
return client

两个来源:

  • get_api_key(openai_mixin.py:124)从 config.auth_credential 取静态 key。
  • _get_api_key_from_config_or_provider_data(openai_mixin.py:276)在此基础上,允许用请求头 x-ogx-provider-data 里的 key 按请求覆盖(字段名由 provider_data_api_key_field 指定)。 这就是"多租户各带各的 key"的实现点——缓存键含 key,所以换 key 会换一个 client,不串号。

5.3 _get_provider_model_id:第二道翻译(通常幂等)

进到 mixin,params.model 已经是 Router 翻好的 provider_resource_id 了。为什么还要 _get_provider_model_id(openai_mixin.py:300)再翻一次?

# 示意,非源码 —— _get_provider_model_id(openai_mixin.py:300)
if not await self.model_store.has_model(model): # 不是注册名 → 原样返回(幂等直通)
return model
model_obj = await self.model_store.get_model(model)
return model_obj.provider_resource_id # 是注册名 → 再翻成后端名

原因是mixin 可能被直接调用,不一定经过 Router(比如 Responses 层、单测、内部复用)。这道兜底 保证"无论谁来调,进后端的都是 provider_resource_id"。经 Router 来的场景下,model 已是后端名、 has_model 返回 False,于是原样返回,幂等。这正是第 1 节说的"翻译两次,第二次通常空转"。

model_store 不是 mixin 自己 new 的——它是运行时被注入的:路由表初始化时 CommonRoutingTableImpl.initialize(common.py:101)把 p.model_store = self 挂上去,provider 借此反查注册表。 __provider_id__ 同理由 resolver 注入(resolver.py:450)。装配全貌见 02 章

5.4 三个 openai_* 方法:整形 → 发出

openai_chat_completion(openai_mixin.py:400)是主路径,骨架:

# 示意,非源码 —— openai_chat_completion(openai_mixin.py:400)
stream_options = get_stream_options_for_telemetry(...) # 有链路追踪就强开 usage 统计
provider_model_id = await self._get_provider_model_id(params.model) # 第二道翻译
self._validate_model_allowed(provider_model_id) # allowed_models 白名单校验
# 需要时把远程图片下成 base64(download_images=True 的后端)
request_params = await prepare_openai_completion_params( # 丢掉 None、把 Pydantic 转 dict
model=provider_model_id, messages=messages, temperature=params.temperature, ...)
if extra_body := params.model_extra:
request_params["extra_body"] = extra_body # 透传后端私有参数
resp = await self.client.chat.completions.create(**request_params) # ★ 真正的 HTTP 调用
return await self._postprocess_chunk(resp, params.stream) # 流式/非流式的收尾整形
  • openai_completion(openai_mixin.py:357)是老式 text completion,同构。
  • openai_embeddings(openai_mixin.py:472)把后端返回的向量重新装成 OpenAIEmbeddingsResponse

_postprocess_chunk 处理两个后端不合规的地方:给不返回 id 的后端补一个 cltsd-<uuid>(overwrite_completion_id);把 Gemini 那种"每个 chunk 都塞 usage"的流合并成 一条合规的末尾 usage chunk(coalesce_streaming_usage)。这些"一个开关治一种后端毛病"的旋钮, 就是"统一成 OpenAI 形状"的真实成本所在。

5.5 register_model / list_models / check_model_availability

mixin 同时实现了模型管理的私有协议,支撑第 3 节的"动态发现":

方法位置作用
list_modelsopenai_mixin.py:553/v1/models 列出后端真实模型,缓存进 _model_cache
check_model_availabilityopenai_mixin.py:599先查已注册,再查 _model_cache,判断某模型可用
register_modelopenai_mixin.py:535默认跳过校验;开了 model_validation 才去 check_model_availability
should_refresh_modelsopenai_mixin.py:617返回 config.refresh_models,决定路由表是否周期刷新

list_models 默认实现里的 list_provider_model_ids(openai_mixin.py:195)就是 [m.id async for m in self.client.models.list()]——直接问后端要清单。这条线把 "后端有什么模型"回灌进 ModelsRoutingTable.refresh(models.py:90),闭合了"发现 → 注册 → 可路由"的环。

5.6 openai_compat.pymodel_registry.py 的角色

这两个工具文件是适配层的配角:

  • openai_compat.py —— 参数整形prepare_openai_completion_params(openai_compat.py:60) 递归丢掉 None、把 Pydantic 模型转成 dict,产出干净的 kwargs; get_stream_options_for_telemetry(openai_compat.py:87)在有链路追踪时强制 include_usage=True, 保证指标完整;convert_tooldef_to_openai_tool(openai_compat.py:17)把工具定义转成 OpenAI tool schema。
  • model_registry.py —— 配置与别名RemoteInferenceProviderConfig(model_registry.py:159) 是所有 remote provider 的公共配置基类(allowed_modelsrefresh_modelsauth_credentialnetwork); ProviderModelEntry/ModelRegistryHelper(model_registry.py:183/192)提供静态别名表—— 给不支持 /v1/models 自省的后端,预声明"这些别名 → 这个 provider_model_id"。

一句话分工:OpenAIMixin 管"怎么调",openai_compat 管"参数长啥样",model_registry 管"有哪些模型、叫什么"。


6. 端到端:"gpt-4" 从字符串到方法调用

把全章串成一次真实调用(非流式,已注册 gpt-4 → provider openai,后端名 gpt-4o-mini):

代码位置params.model 此刻的值发生了什么
1inference.py:198"gpt-4"请求进 InferenceRouter.openai_chat_completion,记下 request_model_id="gpt-4"
2inference.py:121"gpt-4"_get_model_provider 查表
3common.py:164"gpt-4"get_object_by_identifier 拿到 Model(过 RBAC read),provider_resource_id="gpt-4o-mini"
4models.py:296"gpt-4"get_provider_impl 返回 openai provider 实例
5inference.py:210"gpt-4o-mini"Router 把 params.model 换成后端名
6openai_mixin.py:400"gpt-4o-mini"委派进 OpenAIMixin.openai_chat_completion
7openai_mixin.py:300"gpt-4o-mini"_get_provider_model_id:非注册名 → 幂等原样返回
8openai_mixin.py:227"gpt-4o-mini"clientget_api_key+get_base_url 构造/复用 AsyncOpenAI
9openai_mixin.py:468"gpt-4o-mini"client.chat.completions.create(model="gpt-4o-mini", …) 发 HTTP
10inference.py:260响应 .model"gpt-4"Router 把响应里的 model 还原成客户端给的名字

这张表就是本章的答案:一个字符串,经"路由表两级翻译 + Router 委派 + OpenAIMixin 整形"落到某个 provider 上的一次 create 调用,再把名字换回去交给客户端。


7. 巧妙之处(可借鉴)

  • 两级翻译 + 幂等兜底。 Router 翻一次、mixin 兜底再翻一次,且第二次对已翻过的输入是空转 (openai_mixin.py:300)。好处:mixin 既能挂在 Router 后面,也能被 Responses/单测直接调,行为一致。
  • 进门翻译、出门还原对称。 params.model 换后端名、response.model 换回注册名 (inference.py:208/260),客户端全程只见自己传的 model,provider 差异被彻底藏住。
  • 接后端 ≈ 写一个 get_base_url OpenAI/vLLM adapter 各自只有几行(openai/openai.py:151vllm/vllm.py:79),全部重活在 OpenAIMixin 复用。
  • 一个布尔开关治一种后端毛病。 overwrite_completion_idcoalesce_streaming_usagesupports_stream_optionsdownload_images(openai_mixin.py:85起)把各家对 OpenAI 规范的偏离 收成声明式旋钮,而不是散落的 if。
  • 客户端按 (key, base_url) 缓存。 既复用连接,又让"按请求换 key"的多租户自然隔离 (openai_mixin.py:227)。
  • 路由不旁路门禁。 查表即鉴权,读无权当作不存在(common.py:164),安全边界内建在最热的路径上。

8. 边界与局限

  • 基类 get_provider_impl 与模型路由用的不是同一条。 common.py:130 走同步缓存 get_cached, 服务于 tool_runtime/vector_io;inference 走 models.py:296 的重写版(带 RBAC 的异步查)。看错会误判缓存行为。
  • "auto" 只取第一个匹配模型。 _resolve_auto_model(models.py:46)注释自陈"未来可优选热模型", 当前就是列表第一个,不保证最优或最快。
  • 动态发现依赖后端支持 /v1/models 不自省的后端要靠 model_registry.py 的静态别名表兜底, 否则 list_models 无从发现。
  • 缓存 TTL 带来短暂不一致。 多 worker 下别的进程刚注册的模型,最多 5s(默认 TTL)后才在本 worker 可见 (registry.py:187 起)。
  • OpenAIMixin 假设后端"基本 OpenAI 兼容"。 偏离越多、需要开的旋钮/重写的方法越多;完全异构的后端 (非 OpenAI 形状)不走这套,得另写 provider。

9. 代码地图(导航索引)

主题文件路径符号名
Router:按模型选后端并委派src/ogx/core/routers/inference.pyInferenceRouter.openai_chat_completion
Router:查表拿 provider+后端名src/ogx/core/routers/inference.pyInferenceRouter._get_model_provider
Router:provider_id/model 兜底直连src/ogx/core/routers/inference.pyInferenceRouter._get_provider_by_fallback
模型路由表:查表(重写版)src/ogx/core/routing_tables/models.pyModelsRoutingTable.get_provider_impl
模型注册 + 前缀/auto 解析src/ogx/core/routing_tables/models.pyModelsRoutingTable.register_model / _resolve_auto_model
动态发现:回灌 provider 模型src/ogx/core/routing_tables/models.pyModelsRoutingTable.refresh / update_registered_models
公共底座:通用查表(tool/vector 用)src/ogx/core/routing_tables/common.pyCommonRoutingTableImpl.get_provider_impl
公共底座:带门禁的取对象src/ogx/core/routing_tables/common.pyCommonRoutingTableImpl.get_object_by_identifier
公共底座:注册 + 权限 + 落库src/ogx/core/routing_tables/common.pyCommonRoutingTableImpl.register_object
公共底座:动作权限断言src/ogx/core/routing_tables/common.pyCommonRoutingTableImpl.assert_action_allowed
注入 model_store 到 providersrc/ogx/core/routing_tables/common.pyCommonRoutingTableImpl.initialize
持久化:缓存+磁盘注册表src/ogx/core/store/registry.pyCachedDiskDistributionRegistry / get_cached / register
适配基类:统一成 OpenAI 形状src/ogx/providers/utils/inference/openai_mixin.pyOpenAIMixin
适配:构造/复用 AsyncOpenAIsrc/ogx/providers/utils/inference/openai_mixin.pyOpenAIMixin.client / get_api_key / get_base_url
适配:第二道模型名翻译src/ogx/providers/utils/inference/openai_mixin.pyOpenAIMixin._get_provider_model_id
适配:发出 chat/completion/embeddingssrc/ogx/providers/utils/inference/openai_mixin.pyOpenAIMixin.openai_chat_completion / openai_completion / openai_embeddings
适配:模型发现与可用性src/ogx/providers/utils/inference/openai_mixin.pyOpenAIMixin.list_models / check_model_availability
参数整形 + 遥测流选项src/ogx/providers/utils/inference/openai_compat.pyprepare_openai_completion_params / get_stream_options_for_telemetry
provider 公共配置 + 静态别名表src/ogx/providers/utils/inference/model_registry.pyRemoteInferenceProviderConfig / ModelRegistryHelper
具体后端:几行接一个 providersrc/ogx/providers/remote/inference/openai/openai.pyOpenAIInferenceAdapter.get_base_url