拓冰建站拓冰建站
首页 / 资讯中心 / 正文

DeepSeek Harness 自定义 LLM 适配器怎么写:LlmAdapter、StreamChunk 协议与 registerAdapter 注册

DeepSeek Harness 自定义 LLM 适配器怎么写LlmAdapter、StreamChunk 协议与 registerAdapter 注册【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness如果你要给 DeepSeek Harness 接入一个新的模型提供方自己的网关、自建推理服务或其他厂商的 API需要写一个 LLM 适配器插件它继承LlmAdapter把 Harness 的提供方无关请求转换成具体提供方的 API 调用再把响应流转换回 Harness 的StreamChunk分片最后通过ctx.llm.registerAdapter()注册到llm服务上。完成后组合composition里的 agent-loop 就能像调用内置 DeepSeek 路由一样调用你的提供方。本文的主路径是读懂契约 → 写适配器类 → 遵守 StreamChunk 协议 → 处理错误 → 注册并在cordis.yml中挂载 → 验证路由可用。契约的权威定义在 packages/llm/llmdeepseek-ai/dsh-llm及其类型源 packages/llm/llm/src/types.ts操作指南见 LLM adapters协议详解见 LLM Streaming 子系统文档。契约要点一个必须实现的方法LlmAdapter是抽象类源码中标注stream()是唯一的必需方法abstract stream(options: GenerateOptions): AsyncIterableStreamChunk;其余方法都是可选覆写按需添加方法用途resolveModel(provider, model, signal?)一次查询返回精确的 provider/model 身份加可选的context上下文容量、reasoning推理强度元数据listModels(provider)向模型选择器公布可选模型。建议性目录advisory适配器可以接受未列出的 model id消费方不得把未列出当作请求拒绝providerInfo(provider)返回路由展示元数据id必须等于providerproviderRetryPolicy(provider)返回路由级重试策略返回undefined则用默认策略imageRequestPricing(provider, model)提供方对请求图片计费时声明定价必须同步、无 I/OGenerateOptions包含 provider、model、适配器拥有的推理强度 idreasoningEffort、对话历史messages、系统提示词system、工具 schematools、生成参数、停止序列stop和AbortSignal。注意一个硬限制GenerateOptions的采样参数只有temperature/maxTokens/stop没有top_p、tool_choice或 penalty 字段见 dsh-llm README 的已知限制。把支持的字段映射到提供方 API提供方无法支持的字段应抛出带稳定 code 的LlmError而不是静默丢弃。关于resolveModel()服务会在stream()之前校验聚合结果并拒绝显式指定但不受支持的推理强度如果模型元数据里省略reasoning表示该模型没有可选的推理强度能力。推理元数据包含有序的不透明 id、展示名称和可选的配置默认值——保留适配器自己的权威列表包括上游能力 API 返回的off不要把这些值提升为核心枚举。最小适配器实现以下是 LLM adapters 指南中的最小实现代码中的my-llm-adapter、my-provider、my-model-v1是文档示例名替换为你自己的插件名、提供方路由和模型 idimport type { Context } from deepseek-ai/cordis import Schema from deepseek-ai/schemastery import { LlmAdapter, type GenerateOptions, type StreamChunk } from deepseek-ai/dsh-llm class MyAdapter extends LlmAdapter { private apiKey: string constructor(apiKey: string) { super() this.apiKey apiKey } async *stream(options: GenerateOptions): AsyncIterableStreamChunk { // 1. Convert options.messages to the provider format. // 2. Call the streaming API. // 3. Convert the response into StreamChunk values. } } export interface Config { apiKey: string providers: string[] } export const Config: SchemaConfig Schema.object({ apiKey: Schema.string().required(), providers: Schema.array(Schema.string()).required(), }) export const name my-llm-adapter export const inject [llm] export function apply(ctx: Context, config: Config) { const adapter new MyAdapter(config.apiKey) ctx.llm.registerAdapter(config.providers, adapter) }一个 Cordis 插件需要导出name、Configschema、inject和applyinject: [llm]声明对llm服务的依赖apply里完成注册。配置校验用deepseek-ai/schemastery它属于运行时依赖见 adding-a-package 手册对 package.json 的要求。StreamChunk 协议分片的顺序与形状stream()按以下协议产出分片。下面是指南中的文档示例展示一次含文本块和工具调用块的完整流import { ToolCallId, type StreamChunk } from deepseek-ai/dsh-llm async function* exampleChunks(): AsyncIterableStreamChunk { // 1. Start each content block with block-start. yield { type: block-start, index: 0, blockType: text } // 2. Stream text through text-delta. yield { type: text-delta, index: 0, text: Hello } yield { type: text-delta, index: 0, text: world } // 3. End each content block with block-end and the complete block. yield { type: block-end, index: 0, block: { type: text, text: Hello world }, } // 4. Tool-call block. yield { type: block-start, index: 1, blockType: tool-call } yield { type: tool-call-delta, index: 1, id: ToolCallId(call-123), name: bash, argumentsDelta: {command:ls}, } yield { type: block-end, index: 1, block: { type: tool-call, id: ToolCallId(call-123), name: bash, arguments: {command:ls}, }, } // 5. Token usage. yield { type: usage, usage: { inputTokens: 100, outputTokens: 50 } } // 6. Finish reason. yield { type: finish, reason: { kind: stop } } // Alternatively, { kind: tool-calls } requests tool execution. }分片类型的完整联合含reasoning-delta见 types.ts协议说明见子系统文档。必须遵守的关键规则每个block-start都有对应的block-endblock-end携带组装好的完整ContentBlock消费方不需要自己重新拼接增量。index从 0 开始递增标识内容块的顺序。tool-call-delta的argumentsDelta携带原始 JSON 文本增量可以一次给完也可以分多个分片工具参数从头到尾保持原始 JSON 字符串。usage必须在finish之前发出finish是最后一个分片其后不能再有任何内容。建议把这两者推迟到提供方的流结束标记处发出避免尾部的 usage 分片破坏顺序。usage的计数字段是互斥的inputTokens只含未缓存输入缓存命中单独报告提供方若把缓存折叠进单一 prompt 总数需要在适配器里拆出来。失败时你有两条合法路径要么从stream()中抛出异常传输/协议错误要么以finish { kind: error | aborted, failure }结束流提供方带内错误、无法中途抛出的场景。两条路径都收敛到同一个LlmFailure结构消费者按code路由从不依赖报错文本。错误处理LlmError、attributionHeaders 与中止信号适配器把传输和协议故障抛出带稳定 code 的LlmErroragent loop 会保留该错误和 code 用于诊断与策略不会自动转换普通Error。另外每个 provider HTTP 请求都必须合并attributionHeaders()并转发options.signal。指南给出的示例import { attributionHeaders, LlmAdapter, LlmError, type GenerateOptions, type StreamChunk, } from deepseek-ai/dsh-llm class HttpAdapter extends LlmAdapter { constructor(private readonly endpoint: string) { super() } async *stream(options: GenerateOptions): AsyncIterableStreamChunk { const response await fetch(this.endpoint, { method: POST, headers: { content-type: application/json, ...attributionHeaders(), }, body: JSON.stringify({ model: options.model, messages: options.messages }), ...options.signal ? { signal: options.signal } : {}, }) if (!response.ok) { throw new LlmError(Provider API error: ${response.status}, PROVIDER_HTTP_ERROR) } // A real adapter parses the response and emits the complete chunk sequence. yield { type: finish, reason: { kind: stop } } } }attributionHeaders()只映射标准的User-Agent头其内容是公开产品事实不含密钥、路径或会话 id。LlmError的构造参数是message, code, options?其中status必须是 100–599 的整数providerRetryAfterMs必须是正数见 LlmError 实现。registerAdapter注册与路由规则注册调用ctx.llm.registerAdapter([my-provider], adapter)第一个参数是该适配器处理的提供方路由列表。运行时的路由规则见 LlmRuntime 实现GenerateOptions.provider按路由选择已注册的适配器GenerateOptions.model直接传给适配器无需在生命周期启动时注册。路由重复会失败任一 provider 已有适配器时抛出DUPLICATE_ADAPTER且是 all-or-nothing整个注册回滚。空数组不合法初始注册至少需要一个 provider否则抛INVALID_ADAPTER。返回值是 disposer附带replace(providers)用同一适配器实例原子地替换路由集候选集先整体校验冲突时当前路由不受影响。注册随 fiber 销毁。在 cordis.yml 中挂载并验证指南给出的组合示例!!js process.env.MY_API_KEY是文档示例写法读取你启动环境中的环境变量MY_API_KEY按需替换成你自己的凭据来源- id: my-llm name: ./src/my-llm-adapter.ts config: apiKey: !!js process.env.MY_API_KEY providers: - my-provider - id: agent-loop name: deepseek-ai/dsh-agent-loop config: agents: - id: main provider: my-provider model: my-model-v1验证挂载是否成功dsh-llm README 给出两个文档化的检查点注册后ctx.llm.listProviders()按注册顺序报告已注册的路由——你的my-provider应当出现。发一次真实流式请求观察分片序列。README 中的消费示例文档示例值来自内置 DeepSeek 路由自定义适配器替换为你注册的 provider 路由和模型 idfor await (const chunk of ctx.llm.stream({ provider: deepseek-official, model: deepseek-v4-flash, messages: [createUserMessage({ content: [{ type: text, text: Hello }] })], })) { // chunks: block-start, text-delta, ..., usage, finish }流总是以恰好一个终止finish分片结束正常完成为{ kind: stop }或{ kind: tool-calls }请求执行工具失败为{ kind: error, failure }取消为{ kind: aborted, failure }。排错时按稳定 code 判断而不是报错文本现象code请求指向未注册的 providerNO_ADAPTER注册时路由已被占用DUPLICATE_ADAPTER请求中没有任何凭据MISSING_CREDENTIAL凭据格式非法INVALID_CREDENTIAL报错会指明该修哪条引用且不含密钥的任何部分提供方 401/403AUTH限流RATE_LIMIT上下文超限CONTEXT_WINDOW_EXCEEDED限制与参考实现一次适配器调用是一次提供方尝试。服务本身不重跑请求重试是dsh-llm-retry在 agent 失败步边界上执行策略的职责。模型目录是建议性的路由仍以注册的 provider 路由为准不要基于listModels()的缺失做请求校验。BlockAssembler只处理核心块类型插件自加的块类型如果没有block-end关闭blocks()会抛错。仓库中有两个完整的参考实现指南建议对比它们可以看到同一套 harness 契约如何在不同提供方 SDK 之上实现packages/llm/llm-deepseek — 直接fetchDeepSeek chat-completions 的适配器SSE 到StreamChunk的转换在 translate.tspackages/llm/llm-pi-ai — 基于库、走不同 API 格式的多提供方适配器。如果适配器要作为新的工作区包进入本仓库按 adding-a-package 手册补齐package.json、tsconfig.json、README 并注册到根配置然后执行手册中的验证序列pnpm install # registers the workspace pnpm run doc-sync pnpm run constraints pnpm run typecheck pnpm run lint pnpm run build pnpm run hygiene【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门