创建或扩展提供方
提供方插件针对某个厂商 API 实现一项或多项统一契约。可参考 @dsh-ai-providers/openai 和 @dsh-ai-providers/jina,以及 @dsh-ai-providers/openai-compatible 和 @dsh-ai-providers/jina-cohere——它们是多个厂商共用、而厂商之间互不依赖的协议库,但只应公开目标 API 确实支持的能力。
1. 定义并验证配置
使用 Schemastery Config schema 定义连接、模型名称和能力选项。密钥保存在 DSH 凭据服务中,插件配置只保存凭据引用。
在配置边界规范化并验证基础 URL;发起请求前拒绝不支持的协议和无效地址。
2. 实现能力工厂
创建具有稳定 ID 的 AiProvider,并为每项受支持能力提供工厂函数。工厂解析当前配置,返回符合核心接口的实现。
不支持的能力应直接省略,而不是实现一个始终失败的方法。声音发现等可选操作在厂商没有对应端点时可以返回空结果。
工厂被调用时必须读取实时配置。不要在注册时捕获连接、凭据值或模型,否则设置看似已经更新,请求却仍在使用旧状态。实现中的 provider、model 与 capability 标识应保持准确,因为使用方和诊断功能可以读取它们。
路由、重试与发现
路由(LlmRouteSpec)可以带有 baseUrl(其配置固定的端点)和 retry(DSH 重试策略),运行时会把两者报告给 DSH。如果模型的聊天模板会把对话中任意位置改变的系统消息当作完整的系统提示,可为该模型设置 systemPromptUpdate: 'in-history'。重写 LlmService.interrogate(draft, signal),可列出尚未配置的端点所拥有的模型(draft.baseUrl,以及仅为这一次提问提供的密钥 draft.apiKey);默认实现会抛出 unsupported 错误。列出已配置路由的模型仍用 discover(route)。
厂商控制项(采样中)
厂商包的目标是在配置保持同一结构的前提下,暴露厂商 API 的能力。模型请求的控制项请遵循以下约定:
- 路由、模型和策略都携带同一个
sampling对象,按此顺序逐字段合并,最后是请求自身的值。核心包中的LlmSampling规定了所有提供方通用的字段(temperature、topP、topK、minP、各类惩罚、seed、stop、maxTokens、toolChoice),以及body——用于任何其他顶层请求字段的逃生口。凡是能对应上的,请使用这些名称和含义,并自行映射到厂商的线上名称。 - 厂商自有的控制项是同一个扁平对象里的额外可选字段,使用 camelCase,并以厂商参数命名(
num_ctx对应numCtx)。请在继承LlmSampling的接口中声明,并写入路由与模型相同的sampling模式。缺省始终表示服务器默认值;不要填入厂商文档中没有的默认值。 - 只有当某个控制项是厂商 API 的稳定设置时才为它设置字段。实验性的、或只针对某个模型的参数,放进
body;用于选择思考等级的参数,则放进推理等级的body。 LlmSampling之外的字段会原样通过运行时的合并,所以了解该厂商的使用方可以通过返回厂商的扩展类型,在策略中设置它们;不了解该厂商的使用方完全不需要关心。- 厂商无法满足的控制项不要凭空模拟:请记录哪些会被忽略,并对会改变请求含义的情况保持明确失败。
- 在提供方页面中按线上名称记录每个字段。
Ollama 提供方是参考实现:OllamaSampling 继承 LlmSampling,其页面列出每个字段及对应的请求名称。
流式响应
@dsh-ai-providers/core 提供了流式响应体的读取器,厂商无需自己编写:sseData(body, activity, signal) 逐个产出服务器发送事件的数据,ndjsonLines(body, activity, signal) 逐行产出按行分隔的流。两者都会在收到每一段数据时调用 activity(供静默看门狗使用),即使底层读取忽略信号也会在信号触发时结束,并且在流结束时没有最终换行的情况下仍会读出最后一个事件或最后一行。
可选的 LLM 模型控制
只有提供方具备真正的模型管理 API 时,才设置 LlmService.modelControl。在 operations 中列出已实现的方法,并省略提供方无法执行的方法。
import {LlmService, type LlmModelControl} from '@dsh-ai-providers/core'
class VendorLlmService extends LlmService {
readonly modelControl: LlmModelControl = {
operations: ['status', 'unload'],
status: (route, model, signal) => this.modelStatus(route, model, signal),
unload: (route, model, signal) => this.unloadModel(route, model, signal),
}
}
每次调用都应重新解析路由及其连接,让设置变更立即生效。发送请求前验证模型名称,将信号传给传输层,并把厂商状态映射为 LlmModelRuntimeStatus。只有响应包含有效的非负数值时,才报告以字节计的内存用量。
模型管理通常会改变进程的共享状态。不要在 stream() 内自动卸载模型,也不要暗示中止 HTTP 请求能够撤销服务器已经接受的加载或卸载操作。
3. 通过上下文注册
在插件作用域内向 ctx.aiProviders 注册。注册需要清理时使用 ctx.effect(),确保热重载能正确移除提供方。
ctx.effect(() => ctx.aiProviders.register(provider))
错误应映射到稳定的 AiError 分类,并保留不含凭据的厂商响应作为诊断信息。
映射失败时,应考虑调用方接下来能做什么。调用方数据不合法使用 invalid-input;厂商无法执行某项功能使用 unsupported;凭据、配额和模型拒绝使用 rejected;传输及服务器失败使用 unavailable;请求被中止使用 cancelled。not-configured 只用于因选择或配置问题而无法解析服务的情况。
4. 暴露设置元数据
为设置界面描述可复用连接和能力选项,并提供英文和简体中文标签。仅启动时生效的选项应明确标记;其余配置应支持实时更新。
语言模型集成还可提供路由工厂和本地化请求选项。路由 ID 必须保持稳定,因为其他 DSH 配置可能引用它。
5. 验证接口契约
至少测试请求和响应映射、凭据解析、基础 URL、取消传播、流清理、错误归一化、异常响应、配置实时变更及卸载清理。
发布前运行工作区测试和类型检查。不要只依赖公共云服务进行断言;兼容的本地服务器也是重要使用场景。
发布前复核
- 提供方 ID 与每个 LLM 路由 ID 稳定且唯一。
- 配置只保存凭据引用,绝不保存密钥值。
- 每个外部响应在跨过核心契约前都经过验证。
AbortSignal会传到传输层,流在完成、失败、提前返回和取消时都会释放资源。- 注册、监听器和定时器都通过
ctx管理作用域,并能在卸载时清理。 - UI 标签和选项说明同时提供英文与简体中文。
- 仅在提供方确实实现时才公开厂商专属选项。
modelControl.operations与服务实际实现的方法一致。- 模型控制测试覆盖路由解析、授权、取消、异常状态响应和不支持的操作。
- 测试覆盖兼容的本地或模拟服务器,日常验证无需付费凭据。