拆开"原料包":Provider 配置、凭据加解密、和六种模型实体
Day05 我们把 provider_model_bundle 当黑箱用了——今天正式开箱。主角是 core/entities/provider_configuration.py 和 core/entities/provider_entities.py。弄清三件事:①一个 ProviderConfiguration 到底装了哪些东西(系统配额 / 自定义凭据 / 模型开关 / 负载均衡);②"当前该用哪把凭据"是怎么在"系统内置额度"和"用户自填 key"之间抉择的;③LLM / Embedding / Rerank / TTS / Speech2Text / Moderation 这六类"模型实体"的类型体系怎么统一到一个门面下。这一节把 Day05 的黑箱变透明。
ProviderConfiguration 像一份"某租户在某家供应商的开户档案"——里面有"你是用平台送的试用额度(系统)还是自己充值的账户(自定义)"、"你充值账户绑了哪几张银行卡(多把 API Key)"、"哪些产品被管理员禁用了"。ModelInstance(Day05 的翻译)开工前,必须先查这份档案,才知道该刷哪张卡。类比二:六种模型实体像一家翻译公司的六个工种——笔译(LLM 生成)、朗读(TTS)、听写(Speech2Text)、查近义词打分(Rerank)、把词变成坐标(Embedding)、内容审查(Moderation)。它们招聘表(接口)长得一样(都有 invoke),但干的活完全不同。get_model_type_instance 就是"按工种派人"。痛点:Day05 那个"原料包"到底装了啥
ModelInstance 靠 provider_model_bundle 拿到凭据和"技能对象",但那个包我们当黑箱了。可现实里问题全藏在这个包里:同一家 OpenAI,有的租户用平台送的试用额度、有的用自己充的 key,凭据来源完全不同;管理员可能禁用了某个模型;用户可能配了 3 把 key 做负载均衡;而且 key 存进数据库必须加密。这些差异如果泄漏到业务层,Day05 的 ModelInstance 就没法保持"我只管调"的清爽了。ProviderConfiguration——一个"胖实体"(源码注释自己都写了 lots of logic in a BaseModel)。它把"系统/自定义切换、凭据 CRUD、模型开关、负载均衡配置"全揽进来,对外只暴露两个关键方法:get_current_credentials()(当前该用哪把凭据)和 get_model_type_instance()(给我这类模型的技能对象)。业务层永远只问这两句,脏活全在实体内部。ProviderConfiguration 全景:一份开户档案的字段
先看这个实体的字段声明(api/core/entities/provider_configuration.py:60):
# api/core/entities/provider_configuration.py:60
class ProviderConfiguration(BaseModel):
"""
This class handles:
- Provider credentials CRUD and switch # 供应商级凭据的增删改和切换
- Custom Model credentials CRUD and switch # 模型级凭据
- System vs custom provider switching # 系统额度 / 自定义 二选一
- Load balancing configurations # 负载均衡
- Model enablement/disablement # 模型启用/禁用
"""
tenant_id: str
provider: ProviderEntity # ① 这家供应商的"说明书"(有哪些模型、怎么配)
preferred_provider_type: ProviderType # ② 用户更想用哪种(系统/自定义)
using_provider_type: ProviderType # ③ 实际正在用哪种
system_configuration: SystemConfiguration # ④ 系统额度配置(平台送的试用)
custom_configuration: CustomConfiguration # ⑤ 自定义配置(用户自填的 key)
model_settings: list[ModelSettings] # ⑥ 每个模型的开关 + 负载均衡
provider: ProviderEntity"供应商说明书"——这家有哪些模型、需要填哪些字段(API Key?Base URL?)、支持哪几种模型类型。Day05 里 get_model_type_instance 就靠它当 schema。system_configuration平台内置额度。里面 quota_configurations 管"你还剩多少次免费调用"、restrict_models 管"试用额度只让用哪几个模型"。custom_configuration用户自己充值/自填的配置,分两层:供应商级(.provider,所有模型共用一把 key)和模型级(.models,某个模型单独配 key)。model_settings列表,每个元素是 ModelSettings(api/core/entities/provider_entities.py:153),带 enabled(是否禁用)和 load_balancing_configs(多 key 列表)——这正是 Day05 L06 负载均衡的数据来源!系统 vs 自定义:get_current_credentials 怎么抉择
Day05 里 ModelInstance 取凭据,最终落到这个方法(api/core/entities/provider_configuration.py:157):
# api/core/entities/provider_configuration.py:157
def get_current_credentials(self, model_type, model) -> dict | None:
if self.model_settings: # ① 先查"这个模型被禁了没"
for model_setting in self.model_settings:
if model_setting.model_type == model_type and model_setting.model == model:
if not model_setting.enabled:
raise ValueError(f"Model {model} is disabled.") # 禁用 → 直接报错
if self.using_provider_type == ProviderType.SYSTEM: # ② 走"系统额度"分支
restrict_models = []
for quota_configuration in self.system_configuration.quota_configurations:
if self.system_configuration.current_quota_type != quota_configuration.quota_type:
continue
restrict_models = quota_configuration.restrict_models # 试用额度限定的模型
copy_credentials = self.system_configuration.credentials.copy() ...
return copy_credentials # 用平台内置凭据
else: # ③ 走"自定义"分支
credentials = None
if self.custom_configuration.models: # 先看模型级有没有单配 key
for m in self.custom_configuration.models:
if m.model_type == model_type and m.model == model:
credentials = m.credentials; break
if not credentials and self.custom_configuration.provider: # 没有 → 退回供应商级 key
credentials = self.custom_configuration.provider.credentials
enabled 检查任何取凭据前,先确认这个模型没被管理员禁用。禁用了直接抛 ValueError——所以"某模型突然不能用"的报错可能来自这里。SYSTEM 分支用平台送的试用额度:从 quota_configurations 里挑出当前额度类型,凭据用平台内置的(用户看不到明文)。restrict_models 限制"试用只能调这几个模型"。CUSTOM 分支用用户自填的 key,优先级是模型级 > 供应商级:先看 gpt-4o 有没有单独配 key,没有才退回"整个 OpenAI 共用的那把"。这就是"某个贵模型单独用另一个账号"的实现。credential 合规检查拿到自定义凭据后还会走 runtime_check_credential_policy_compliance,校验这把凭据是否符合租户的合规策略——企业版的凭据治理钩子。模型类型体系:六个工种一张招聘表
Dify 把模型分成六大类型(ModelType 枚举,来自 model_runtime)。它们的"技能对象"基类各不相同,但 Day05 的 ModelInstance 对每一类都提供了一个 invoke_* 门面方法:
| 模型类型 | 干什么 | ModelInstance 门面 |
|---|---|---|
LLM | 对话/文本生成 | invoke_llm model_manager.py:154 |
TEXT_EMBEDDING | 把文本变成向量 | invoke_text_embedding model_manager.py:214 |
RERANK | 给检索结果重排打分 | invoke_rerank model_manager.py:272 |
MODERATION | 内容安全审查 | invoke_moderation model_manager.py:328 |
SPEECH2TEXT | 语音转文字 | invoke_speech2text model_manager.py:344 |
TTS | 文字转语音 | invoke_tts model_manager.py:360 |
model_type_instance 是怎么造出来的
Day05 说 model_type_instance 是"真正会调某类模型的技能对象"。它由 ProviderConfiguration.get_model_type_instance(api/core/entities/provider_configuration.py:1538)产出:
# api/core/entities/provider_configuration.py:1538
def get_model_type_instance(self, model_type: ModelType) -> AIModel:
if self._bound_model_runtime is not None:
model_runtime = self._bound_model_runtime # ① 请求已绑好的运行时,直接复用
else:
model_runtime, _ = self._get_runtime_and_provider_factory() # 否则现查
provider_schema = self._cached_provider_schema or self.provider # ② 复用已解析的说明书
return create_model_type_instance( # ③ ★按"运行时+说明书+类型"造技能对象
runtime=model_runtime,
provider_schema=provider_schema,
model_type=model_type,
)
_bound_model_runtime一个性能优化:一次请求里,"运行时"(跟插件化模型进程通信的通道)会被 bind_model_runtime() 预先绑到 configuration 上。造技能对象时直接复用,不用每次重新解析——这就是字段注释里说的"reuse the caller scope"。_cached_provider_schema同理,"供应商说明书"也缓存起来复用,避免每次都去插件目录重新拉一遍完整 catalog。create_model_type_instance★真正的工厂:拿"运行时通道 + 说明书 + 你要的类型",造出一个 AIModel 子类实例(如 LargeLanguageModel)。返回后被塞进 ProviderModelBundle.model_type_instance(api/core/entities/provider_configuration.py:2071),交给 Day05 的 ModelInstance。graphon.model_runtime + 插件进程),所以"技能对象"其实是个跨进程的代理。这也是为什么要有"运行时通道"这层——具体模型代码跑在独立插件里,主进程通过 runtime 调它。六种 invoke:同一个门面模式的复用
回到 Day05 的 ModelInstance。有了类型体系,你会发现六个 invoke_* 方法长得几乎一样。对比 invoke_text_embedding(api/core/model_manager.py:214)和 invoke_rerank(api/core/model_manager.py:272):
# api/core/model_manager.py:214
def invoke_text_embedding(self, texts, input_type=EmbeddingInputType.DOCUMENT):
if not isinstance(self.model_type_instance, TextEmbeddingModel): # ① 类型防呆
raise Exception("Model type instance is not TextEmbeddingModel")
return self._round_robin_invoke( # ② 统一走 Day05 的轮询包装
self.model_type_instance.invoke,
model=self.model_name, credentials=self.credentials,
texts=texts, input_type=input_type,
)
# api/core/model_manager.py:272
def invoke_rerank(self, query, docs, score_threshold=None, top_n=None):
if not isinstance(self.model_type_instance, RerankModel): # ① 换个基类防呆
raise Exception("Model type instance is not RerankModel")
return self._round_robin_invoke( # ② 一模一样的包装
self.model_type_instance.invoke,
model=self.model_name, credentials=self.credentials,
query=query, docs=docs, score_threshold=score_threshold, top_n=top_n,
)
invoke_* 都是三步:①类型防呆(确认技能对象是对的工种)→ ②把 model_type_instance.invoke 当函数传给 _round_robin_invoke → ③各自传各自的参数。差别只有"要求哪个基类"和"传什么参数"。因为所有 invoke 都从 Day05 的轮询包装走,负载均衡 / 故障冷却逻辑就自动对六种模型全部生效——写一份,六处白拿。这就是把"变化点"(参数)和"不变点"(调用流程)分离的威力。invoke_llm 的 isinstance(..., LargeLanguageModel) 就会失败,抛 "Model type instance is not LargeLanguageModel"。这个防呆看着啰嗦,实则救命——否则错误会一路带到模型 API 才炸,报错信息还看不懂。类型检查在最外层拦下,报错清晰。串起来 + 今日小结
t-001 在 OpenAI 的 ProviderConfiguration:using_provider_type=CUSTOM(用自己的账户);custom_configuration.provider.credentials={"openai_api_key":"sk-xxx"}(整家共用这把,加密存库);model_settings=[ModelSettings(model="gpt-4o", enabled=True, load_balancing_configs=[key-A, key-B]), ModelSettings(model="gpt-3.5", enabled=False)]。→ 现在业务调 get_current_credentials(LLM, "gpt-4o"):先查 model_settings 发现 gpt-4o 是 enabled,走 CUSTOM 分支,模型级没单配 key → 退回供应商级 sk-xxx;同时因为 gpt-4o 有 load_balancing_configs,Day05 的 _get_load_balancing_manager 会建轮询器在 key-A/key-B 间转。→ 若换成调 gpt-3.5:enabled=False,直接抛 "Model gpt-3.5 is disabled"。一份档案,决定了每一次调用刷哪张卡、能不能刷。👶 小白:ProviderConfiguration 和 Day05 的 ProviderManager,名字好像,什么关系?
👨🏫 老师:一层套一层。ProviderManager(Day05)是"档案室管理员"——它 get_configurations(tenant) 一次性把某租户所有供应商的档案都加载好;ProviderConfiguration(今天)是"其中一家的那份档案"。管理员从数据库读原始记录、解密凭据、组装成一份份 configuration。所以链条是:ProviderManager(管全部)→ 一份 ProviderConfiguration(管一家)→ 打包成 ProviderModelBundle → 交给 ModelInstance(Day05)去调。今天我们钻进了这份"档案"内部。
🧠 今天你应该能回答
- ProviderConfiguration 装了哪 6 大块?(说明书 / 系统额度 / 自定义凭据 / 模型开关+负载均衡)
- 系统额度和自定义凭据怎么二选一?(
using_provider_type,get_current_credentials分两支) - 自定义凭据两级优先级?(模型级 > 供应商级)
- Dify 有哪六种模型类型?(LLM/Embedding/Rerank/Moderation/Speech2Text/TTS)
- 六个 invoke_* 为什么代码几乎一样?(类型防呆 + 统一走
_round_robin_invoke) - model_type_instance 由谁造?(
get_model_type_instance→create_model_type_instance插件化工厂)
✋ 10 分钟动手
cd /Users/bitmart/work/codes/github/AI_WORK/dify
# 1. 开户档案的字段
sed -n '60,96p' api/core/entities/provider_configuration.py # ProviderConfiguration
sed -n '141,167p' api/core/entities/provider_entities.py # ModelSettings / 负载均衡
# 2. 凭据抉择(系统 vs 自定义)
sed -n '157,215p' api/core/entities/provider_configuration.py # get_current_credentials
# 3. 技能对象工厂 + 打包
sed -n '1538,1557p' api/core/entities/provider_configuration.py # get_model_type_instance
sed -n '2071,2081p' api/core/entities/provider_configuration.py # ProviderModelBundle
# 4. 六种 invoke 对比(看骨架多像)
grep -n "def invoke_" api/core/model_manager.py
core/prompt/——SimplePromptTransform / AdvancedPromptTransform 怎么把"系统提示词 + 变量 + 历史对话 + 用户问题"组装成 PromptMessage 列表;再看 core/llm_generator/ 里 Dify 自己怎么用 LLM 干活(自动起对话标题、生成结构化输出)。