Repository Wiki
deepseek-ai/deepseek-harness

模型适配器、路由与流式响应

本页说明模型提供商适配器如何接入 LLM 注册表、解析认证与模型目录,以及 DeepSeek 请求扩展如何在发送前准备字段并在请求成功后完成接受事务。文档依据 packages/llm 中已读取的实现,重点覆盖适配器边界、路由注册、取消传播、不可变请求字段与错误映射。

Purpose and Scope

本页面向需要新增或维护模型提供商、路由和流式请求前置逻辑的工程师,覆盖以下范围:

  • llm-deepseek-account 插件的生命周期、deepseek-account 路由、账号令牌解析和模型发现。
  • DeepSeekLlmApiExtensionRegistry 的字段注册、请求准备、取消处理、字段隔离和接受回调。
  • 适配器与 ctx.llm、deepseekAccount、DeepSeek 连接配置之间的真实依赖关系。
  • 配额、未登录、401 令牌失效和存储删除失败等实现中明确处理的失败路径。

本页不展开账号凭据存储本身、通用 llm 服务的完整实现、具体 UI 配置页面或所有其他模型供应商。对于凭据记录格式和通用认证桥接,参见 llm-pi-ai 相关页面;对于 Cordis 插件生命周期和应用启动编排,参见运行时与插件系统相关页面。

Overview

该能力由两类互补机制组成:

  1. 模型适配器与路由:llm-deepseek-account 作为 Cordis 插件注入 llm,把配置转换为运行时连接选项,向 ctx.llm 注册可配置 provider,并通过 registerDeepSeekProvider 提供认证、模型发现和请求所需的 DeepSeek 连接行为。
  2. 请求扩展注册表:DeepSeekLlmApiExtensionRegistry 管理由不同插件独立拥有的顶层请求字段。每个字段只能有一个 provider;请求阶段并行准备字段,结果被 structuredClone 后递归冻结;所有接受回调通过一次幂等的联合事务执行。

这种拆分的设计意图是把“如何到达某个模型端点”与“某个插件如何增加官方请求字段”解耦。适配器拥有认证和模型目录职责,扩展注册表则只负责请求字段生命周期,不把具体扩展逻辑硬编码进官方适配器。

Architecture

Loading diagram...

Sources:

图中的依赖均来自实现:apply 读取 ctx.deepseekAccount,调用 ctx.llm.registerConfigurableProviders 和 registerDeepSeekProvider;扩展注册表本身是 Cordis Service,以 deepseekLlmApiExtensions 注册到上下文中。适配器和扩展注册表通过请求边界协作,但扩展 provider 的具体业务不属于账号适配器。

适配器生命周期与路由注册

apply(ctx, config) 是账号适配器的入口。它首先创建惰性 options 函数:每次调用时把 plainOptions(config) 与 launchEnvironmentOf(ctx) 交给 resolveAdapterOptions。入口阶段立即调用一次 options(),使配置和启动环境在插件应用时就得到解析;后续认证和模型发现仍重新解析连接选项,而不是共享一个可能过期的快照。

随后,插件向 ctx.llm 注册一个可配置 provider:

  • 内部路由键是 deepseek-account。
  • 展示名是 DeepSeek Account。
  • settingsNs 使用当前 fiber entry 的 options id,若不存在则回退到插件名。
  • settingsPath 是空数组,表明该实现没有在此处追加嵌套设置路径。

之后 registerDeepSeekProvider 接收同一个 provider 键,并获得 options、resolveAuth、展示名和 discoverModels。因此路由的关键行为集中在 provider 配置对象中,而不是分散到调用方。

认证、模型发现与请求错误映射

认证流程

resolveAuth(connection) 从 ctx.get('deepseekAccount') 获取账号服务,然后以 connection.baseURL 为目标解析 token。没有 token 时抛出 LlmError,错误码为 ACCOUNT_SIGN_IN_REQUIRED,消息明确要求用户登录且请求目的地必须允许账号认证。

成功时返回:

  • headers:把 token 放到 x-dsh-auth-token。
  • onRequestError:把通用请求失败转换为账号路由语义。

这使认证凭据只在适配器边界转换为 DeepSeek 请求需要的 header,调用者无需了解账号服务的 token 获取细节。

模型发现

discoverModels 先解析当前连接选项,然后调用 resolveAuth 验证账号是否可用于目标地址。若错误码是 ACCOUNT_SIGN_IN_REQUIRED,返回空数组而不是让模型目录加载失败;其他错误继续抛出。认证成功后,将 connection.models 中每个模型映射为 catalogModelInfo(provider, model)。

因此“未登录”在模型发现阶段表现为“没有可展示模型”,而不是异常;真实请求阶段仍由 resolveAuth 强制认证。这种差异避免目录 UI 因缺少账号而进入错误态,同时不放松实际推理请求的安全要求。

错误转换

onRequestError 只转换 LlmError。非 LlmError 原样返回,避免适配器错误处理吞掉未知异常。对于已知错误:

  • 通用配额错误 QUOTA_EXCEEDED_CODE 被包装成 ACCOUNT_QUOTA_EXCEEDED_CODE,并保留原始 failure 信息与 cause。
  • 非 401 的错误原样返回。
  • 401 被转换为 ACCOUNT_TOKEN_INVALID,随后尝试调用 account.rejectToken(token) 清除失效令牌。
  • 令牌清除失败被捕获并忽略,因为存储清理失败不能替代真正的推理请求失败。

DeepSeek 请求扩展注册表

DeepSeekLlmApiExtensionRegistry 继承 Cordis Service,服务名为 deepseekLlmApiExtensions,内部用 Map<string, ErasedProvider> 保存字段 provider。类型层通过 declaration merging 把该服务暴露在 Context.deepseekLlmApiExtensions 上。

注册与作用域

register(field, provider) 的关键约束如下:

  1. 字段名不能为空,且必须是 trim 后不变的字符串;空字符串或带首尾空白会立即抛错。
  2. 同一顶层字段不能重复注册;重复注册会抛出包含字段名的错误。
  3. 注册动作通过 ctx.effect 绑定 Cordis effect 生命周期。
  4. 返回的 disposer 删除该字段,因此插件卸载时不会留下陈旧 provider。

注册的“每字段唯一”规则保证了最终请求不会出现多个插件争夺同一顶层 JSON 字段,也让字段所有权可以由类型映射 DeepSeekLlmApiExtensionMap 表达。

请求准备

prepare(request) 先检查 request.signal,然后复制当前 provider entries,并用 Promise.all 并行调用每个 provider 的 prepare。如果准备工作是异步的,abortable 会让外层等待同时受取消信号控制:请求已经取消时立刻拒绝;正常完成后再次检查 signal,避免在竞态窗口中继续使用结果。

每个非 undefined 结果都会经过 structuredClone 和 freezeJson:这会切断 provider 对发送值的可变引用,并递归冻结对象或数组。最终 fields 对象本身也被冻结。provider 可以同时返回 accept 回调;这些回调被绑定到结果对象后收集起来,但不会在 prepare 内立即执行。

返回值包含 fields 和 accept。accept 使用闭包中的 acceptance 缓存,因此多次调用只会创建一次 acceptAll Promise。acceptAll 对所有回调执行 Promise.allSettled,先等待全部回调结束,再按失败数量抛出单个原始错误或 AggregateError。这避免第一个失败导致其他接受回调尚未完成,从而留下部分提交状态。

Loading diagram...

Source: index.ts

该时序图反映实现中的两个重要边界:取消只控制请求准备阶段的等待,接受阶段则统一等待所有回调;而字段值在返回前已经与 provider 的可变对象隔离。

Usage Examples

账号 provider 的最小注册

以下是仓库中 apply 的核心注册路径。它展示了配置解析、LLM provider 注册以及 DeepSeek provider 行为如何组合在一起;代码未引入文档外的 API。

typescript
1export function apply(ctx: Context, config: Config): void { 2 const options = () => resolveAdapterOptions(plainOptions(config), launchEnvironmentOf(ctx)) 3 options() 4 const resolveAuth = async (connection: ResolvedDeepSeekOptions): Promise<DeepSeekRequestAuth> => { 5 const account = ctx.get('deepseekAccount') 6 const token = await account?.resolveToken(connection.baseURL) 7 if (token === undefined) throw new LlmError('Sign in to DeepSeek to use the account provider. The request destination must allow account authentication.', 'ACCOUNT_SIGN_IN_REQUIRED') 8 return { 9 headers: { 'x-dsh-auth-token': token }, 10 onRequestError: async (error) => { 11 if (!(error instanceof LlmError)) return error 12 if (error.code === QUOTA_EXCEEDED_CODE) { 13 return new LlmError(error.message, ACCOUNT_QUOTA_EXCEEDED_CODE, { ...error.failure, cause: error }) 14 } 15 if (error.failure.status !== 401) return error 16 const rejected = new LlmError(error.message, 'ACCOUNT_TOKEN_INVALID', { ...error.failure, cause: error }) 17 try { await account?.rejectToken(token) } 18 catch (_credentialRemovalFailed) { /* Storage failure cannot replace the inference failure. */ } 19 return rejected 20 }, 21 } 22 } 23 ctx.llm.registerConfigurableProviders([ 24 { provider: PROVIDER, displayName: 'DeepSeek Account', settingsNs: ctx.fiber.entry?.options.id ?? name, settingsPath: [] }, 25 ]) 26 registerDeepSeekProvider(ctx, PROVIDER, { 27 options, resolveAuth, providerName: 'DeepSeek Account', 28 discoverModels: async (provider) => { 29 const connection = options() 30 try { await resolveAuth(connection) } 31 catch (error) { 32 if (error instanceof LlmError && error.code === 'ACCOUNT_SIGN_IN_REQUIRED') return [] 33 throw error 34 } 35 return connection.models.map(model => catalogModelInfo(provider, model)) 36 }, 37 }) 38}

Source: index.ts

扩展字段的准备与接受

扩展 registry 的调用方可以先准备字段,再在请求生命周期合适的成功点调用 accept。下面的实现片段展示 registry 如何并行执行 provider、隔离字段值并延迟接受回调;accept 的具体调用时机由请求调用方决定,当前已读取源码未定义更多上层调用细节。

typescript
1async prepare(request: DeepSeekLlmApiExtensionRequest): Promise<PreparedDeepSeekLlmApiExtensions> { 2 request.signal.throwIfAborted() 3 const entries = [...this.providers.entries()] 4 const prepared = await abortable(Promise.all(entries.map(async ([field, provider]) => ({ 5 field, 6 result: await provider.prepare(request), 7 }))), request.signal) 8 const fields: Record<string, DeepSeekLlmApiJson> = Object.create(null) as Record<string, DeepSeekLlmApiJson> 9 const callbacks: Array<() => void | Promise<void>> = [] 10 for (const { field, result } of prepared) { 11 if (result === undefined) continue 12 fields[field] = freezeJson(structuredClone(result.value)) 13 const accept = result.accept 14 if (accept !== undefined) callbacks.push(accept.bind(result)) 15 } 16 Object.freeze(fields) 17 let acceptance: Promise<void> | undefined 18 return { 19 fields, 20 accept: () => acceptance ??= acceptAll(callbacks), 21 } 22}

Source: index.ts

注册字段并获得 disposer

该片段体现字段所有权和 effect 作用域:成功注册后,返回的函数负责释放字段;重复字段会在 effect 执行时拒绝。

typescript
1register<K extends keyof DeepSeekLlmApiExtensionMap>( 2 field: K, 3 provider: DeepSeekLlmApiExtensionProvider<DeepSeekLlmApiExtensionMap[K]>, 4): () => Promise<void> { 5 const fieldName = field as string 6 if (fieldName.length === 0 || fieldName.trim() !== fieldName) { 7 throw new Error('deepseek-llm-api-extensions: field must be a non-blank trimmed string') 8 } 9 const providers = this.providers 10 const erased = provider as ErasedProvider 11 const dispose = this.ctx.effect(() => { 12 if (providers.has(fieldName)) { 13 throw new Error(`deepseek-llm-api-extensions: field ${JSON.stringify(fieldName)} is already registered`) 14 } 15 providers.set(fieldName, erased) 16 return () => { 17 providers.delete(fieldName) 18 } 19 }, `deepseekLlmApiExtensions.register(${JSON.stringify(fieldName)})`) 20 return dispose 21}

Source: index.ts

Configuration Options

已读取的账号适配器实现将配置类型导出为 Config,并通过 plainOptions(config) 交给 resolveAdapterOptions。具体配置字段和默认值定义在 llm-deepseek-account/src/config.ts,但本页受源文件读取预算限制未读取该文件,因此不能可靠列出字段、类型或默认值。当前可确认的运行时事实如下:

项目类型/来源默认值说明
Config./config.ts 导出的类型源码未在本页读取由 plainOptions(config) 转换为适配器选项
launchEnvironmentOf(ctx)Cordis 上下文运行环境由运行时提供与配置共同传给 resolveAdapterOptions
connection.baseURLResolvedDeepSeekOptions由解析后的连接决定用作账号 token 解析的目标地址
connection.modelsResolvedDeepSeekOptions由解析后的连接提供用于模型发现和 catalogModelInfo 映射

因此不要根据本页推断未读取的配置键或默认 URL;新增配置时应以 config.ts 和 resolveAdapterOptions 的实现为准。

API Reference

apply(ctx: Context, config: Config): void

账号插件入口。它解析适配器选项,注册 DeepSeek Account 可配置 provider,并安装认证与模型发现行为。

  • ctx:Cordis Context,提供 llm、deepseekAccount、fiber entry 和启动环境。
  • config:插件配置,传入 plainOptions。
  • 返回值:无;注册行为由上下文与 provider 注册机制持有。
  • 可能失败:配置解析或 provider 注册阶段的异常会向调用方传播;认证错误在请求或模型发现路径中按实现转换。

DeepSeekLlmApiExtensionRegistry.register(field, provider): () => Promise<void>

为一个声明合并的顶层请求字段注册唯一 provider,并返回异步 disposer。

  • field:keyof DeepSeekLlmApiExtensionMap,运行时必须是非空且无首尾空白的字符串。
  • provider:实现 prepare(request) 的字段 provider,可返回字段值与可选 accept 回调。
  • 返回值:释放该字段注册的异步函数。
  • 可能失败:字段非法时抛出校验错误;字段已被注册时抛出重复注册错误。

DeepSeekLlmApiExtensionRegistry.prepare(request): Promise<PreparedDeepSeekLlmApiExtensions>

并行准备当前已注册字段,并返回冻结的字段集合和幂等接受事务。

  • request:包含序列化请求事实和 AbortSignal 的 DeepSeekLlmApiExtensionRequest。
  • 返回值:fields 与 accept;未返回值的 provider 不会出现在 fields 中。
  • 取消行为:在 provider 工作期间或完成后检测 signal;取消会拒绝等待过程。
  • 一致性行为:字段值经过 structuredClone 和递归冻结;accept() 的多次调用共享同一个 Promise。

DeepSeekLlmApiExtensionRegistry.acceptAll(callbacks): Promise<void>

这是模块内部函数,不是导出的公共 API。它使用 Promise.allSettled 等待所有接受回调,单个失败时抛出原始原因,多个失败时抛出 AggregateError。

Failure Modes、边界条件与并发

适配器侧

  • 未登录:resolveToken 返回 undefined 时抛出 ACCOUNT_SIGN_IN_REQUIRED。模型发现专门把该错误转为空模型列表;请求认证不静默放行。
  • 配额耗尽:通用 QUOTA_EXCEEDED_CODE 被重分类为账号专用错误码,同时保留 cause,便于上层区分账号额度与其他 provider 额度。
  • 令牌失效:401 会转换为 ACCOUNT_TOKEN_INVALID,并尝试删除 token。删除失败不会覆盖推理失败,避免凭据存储故障掩盖真正的 HTTP 认证结果。
  • 未知异常:非 LlmError 不被重写,保留原异常类型和语义。

扩展注册表侧

  • 字段非法或冲突:空字段、首尾有空白的字段和重复字段都会拒绝注册。由于检查位于 effect 注册逻辑中,失败不会留下该字段的有效 map 条目。
  • 取消竞态:prepare 在开始前检查 signal,并通过 abortable 竞速 provider 工作与 abort Promise;工作完成后再次检查 signal。这覆盖了 provider 完成与取消几乎同时发生的窗口。
  • 部分接受失败:acceptAll 不在第一个回调失败时立即返回,而是等待所有回调 settled,再决定抛出单一错误或 AggregateError。
  • 重复调用 accept:闭包缓存 acceptance Promise,使多次调用不会重复执行副作用回调。

性能与运维注意事项

扩展 provider 的 prepare 使用 Promise.all 并行执行,等待时间主要受最慢 provider 影响,而不是所有 provider 延迟之和;代价是任一未被取消的 provider 拒绝会使整体准备失败。字段通过 structuredClone 和递归冻结获得隔离,换取了额外复制与遍历成本;这适合请求级、结构化扩展字段,但不应把大型可变对象当作扩展值而忽略复制开销。

provider 列表在 prepare 开始时从 Map 快照复制,因此准备期间的注册或释放不会改变本次遍历集合。注册本身由 Cordis effect 管理,插件卸载路径应执行 disposer,避免旧 provider 继续参与后续请求。

账号适配器每次 options() 调用都重新计算解析后的连接选项。实现明确选择了动态解析,而不是缓存连接对象;如果运行环境或配置可以在插件生命周期内变化,这能避免长期使用旧连接,但也意味着调用方不应假定 discoverModels 与后续请求必然共享同一连接快照。

Extension Points

新增 DeepSeek 账号行为时,首选复用 registerDeepSeekProvider 的 options、resolveAuth 和 discoverModels 边界,而不是在 apply 外部直接读取 token。新增请求顶层字段时,使用 ctx.deepseekLlmApiExtensions.register,为字段提供独立的 prepare 和可选 accept;不要绕过 registry 直接修改最终请求对象,因为 registry 才负责唯一字段所有权、克隆冻结、取消传播和接受事务。

当前已读取源码没有显示流式 chunk 聚合、HTTP transport 或具体 accept 调用方的实现,因此这些行为在本页不做推断。若要扩展流式响应本身,应先定位 registerDeepSeekProvider 的实际发送实现和请求调用方,再补充端到端时序。

Tests and Verification Boundary

当前源材料包含测试文件路径和仓库级测试策略线索,但本页未读取具体 provider 或 registry 测试实现。因此不能据此宣称某个错误码、取消竞态或重复接受行为已有测试覆盖。建议验证时至少针对源码明确的契约建立测试:未登录模型发现返回空列表、401 后令牌拒绝、重复字段注册、abort 竞态、字段深冻结以及多个 acceptance failure 的 AggregateError 行为。

Sources

(2 files)
packages/llm/deepseek-llm-api-extensions/src
packages/llm/llm-deepseek-account/src