Skip to content

@deepseek-ai/dsh-llm-deepseek 是 harness LLM 接缝的参考实现:一个直接 fetch、OpenAI 兼容的 chat-completions 适配器,在 ctx.llm 上注册唯一的 provider 路由 deepseek-official。它纯粹是传输层——序列化、SSE 解码与流翻译——而连接事实与凭据通过注册插件所拥有的 thunk 按请求解析。

描述
@deepseek-ai/dsh-llm-deepseek"DeepSeek chat-completions adapter for the DeepSeek Harness LLM seam"

它依赖 eventsource-parser@^3.1.0(SSE 分帧)与 @deepseek-ai/schemastery(配置 schema);peer 依赖覆盖 dsh-llmdsh-credentialsdsh-settingsdsh-timeoutdsh-launch-environmentdsh-anonymous-user-iddsh-invariantscordis。它用原生 fetch 与 DeepSeek 平台通信——没有独立的平台 API 客户端包(BFF/"remotes"包是另一回事;见 API 网关)。

适配器

DeepSeekAdapterpackages/llm/llm-deepseek/src/adapter.ts)继承 LlmAdapter。它为注册名下的每个模型服务:harness 模型 id 就是线上模型 idlistModels/resolveModel 原样返回所配置的内容)。构造一个实例并绑定到按请求的解析钩子:

ts
export interface DeepSeekAdapterOptions {
  options: () => DeepSeekConnectionOptions   // 校验过的连接事实,按操作解析
  resolveApiKey: (connection) => Promise<string>  // 无 key 时抛 LlmError MISSING_CREDENTIAL
  resolveUserId: () => AnonymousUserId
}

DeepSeekConnectionOptions 冻结一次解析的端点事实:baseURL(追加 /chat/completions)、一个 apiKeyEnv 凭据引用(绝不是字面 key)、defaults(thinking/effort)、maxTokensdefaultContextWindow、咨询性 models 目录、streamIdleTimeoutMs 与已解析的 retryPolicy。每次 stream()从同一份快照重新运行 options()resolveApiKey(connection),因此进行中的流永远不会观察到配置变更,端点也绝不会与另一代的 key 配对。

Base URL 解析

apply()(在 packages/llm/llm-deepseek/src/index.ts)按此优先级解析 baseURL

  1. 显式 config.baseURL(来自 cordis.ymlllm-deepseek 设置段);
  2. 来自可信 launch-environment 层$DEEPSEEK_BASE_URL(让 checkout 可以把它的 agent 指向其 checkout 所针对的网关);
  3. 公共常量 PUBLIC_BASE_URL = 'https://api.deepseek.com'

resolveAdapterOptions() 即使在构造绕过 Schemastery 时(编程式构建)也会重新校验每个默认值/绑定,加载时响亮失败,或在实时编辑违反 schema 之外的绑定时保留最后一个好的设置快照(keep the last good configuration)。

暴露的模型

默认解析为两个 V4 模型,共享 1M-token 上下文窗口:

模型 id显示名上下文
deepseek-v4-flashDeepSeek-V4-Flash1,000,000
deepseek-v4-proDeepSeek-V4-Pro1,000,000

目录中可覆盖每个模型的 maxTokenscontextWindow;没有精确值的被选模型回退到 defaultContextWindow(默认 1,000,000)与 defaultMaxTokens(默认 256,000)。每条路由的 inputModalities 都是 ['text']——线上路由纯文本。

请求映射:harness → DeepSeek 线上

serializeRequest / serializeMessagesserialize.ts)把 harness 词汇映射到 POST {baseURL}/chat/completions 的 OpenAI 兼容请求体:

Harness 概念DeepSeek 线上
Message.rolemessages[].rolesystem/user/assistant/tool
用户消息中的一条工具结果展开为独立的 {role:'tool', tool_call_id, content} 条目
assistant 内容content;无文本的 turn 发送 ""——绝不发送 null
assistant 推理reasoning_content(CoT 回传),只在工具调用 turn 上
assistant 工具调用tool_calls[]idtype:'function'function{name,arguments}
GenerateOptions.tools带 JSON-Schema parameterstools[]
thinking/reasoningEffort顶层 thinking:{type} + reasoning_effort(`'high'
temperature / maxTokens / stoptemperaturemax_tokensstop
流式总是 stream:truestream_options:{include_usage:true}

图片被 assertTextOnly 拒绝(抛 UNSUPPORTED_CONTENT),发生在任何文本扁平化可能静默抹掉它们之前。resolveThinking 映射适配器自有的 effort:offthinking:{type:'disabled'}high/maxthinking:{type:'enabled'} + effort;session-title 用途总是强制关闭 thinking,让标题调用快速返回可见文本。

流式与翻译流水线

text
 fetch(baseURL/chat/completions)
   → HTTP 200 text/event-stream body
   → parseSse(): TextDecoder → EventSourceParserStream(eventsource-parser)
        · 产出每个事件的 `data` payload;最后产出字面量 "[DONE]"
        · 没有 [DONE] 就 EOF  ⇒  抛 LlmError('STREAM_CLOSED')
   → translate(): 每个 content/reasoning/tool 索引一个带状态的 harness block
        · reasoning_content → reasoning-delta(首个空帧不打开 block)
        · content           → text-delta
        · tool_calls[]      → tool-call-delta(片段按索引拼接)
        · finish_reason     → finish reason(stop|tool_calls|length)
        · usage             → usage chunk(推迟到 [DONE];保留最新)
   → adapter.stream(): 用空闲看门狗 AbortSignal 包裹读取
        · streamIdleTimeoutMs(默认 300_000)内无读取  ⇒  LlmError('TIMEOUT')
        · 调用方 abort                                     ⇒  LlmError('ABORTED')
        · 传输失败                                         ⇒  LlmError('TRANSPORT')

mapFinishReasonstop → stoptool_calls → tool-callslength → max-tokens,任何无法识别的 reason(如 content_filter)映射为 error finish,错误码为大写后的 reason。

Token 核算

mapUsage 把 DeepSeek 的 prompt_tokens包含缓存命中)转换为 harness 的不相交约定:

ts
// translate.ts —— DeepSeek 的 prompt_tokens = 缓存命中 + 缓存未命中
const cacheRead = usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens
{
  inputTokens: usage.prompt_tokens - (cacheRead ?? 0),
  outputTokens: usage.completion_tokens,
  ...cacheRead !== undefined ? { cacheReadTokens: cacheRead } : {},
  ...reasoning !== undefined ? { reasoningTokens: reasoning } : {},
}

API key 从何而来

provider 按环境变量名引用凭据——apiKeyEnv,默认 DEEPSEEK_API_KEY——绝不把字面 key 存在配置里。每次请求,resolveApiKey(connection)

  1. ctx.credentials(凭据接缝)请求那个 CredentialRef;能解析就用它;
  2. 否则在未挂载凭据接缝时回退到 launchEnvironmentOf(ctx).get(ref)(shell 导出的 .env 层);
  3. 否则抛 LlmError('MISSING_CREDENTIAL')

assertUsableApiKey 会 trim 并拒绝空白/非 header 安全 key(错误码 INVALID_CREDENTIAL),且不回显任何秘密。文件存储的故事见 凭据管理credentials-local provider($DSH_HOME 下的 .credentials.yaml + .env)。

错误处理摘要

线上信号LlmError 错误码
HTTP 401 / 403AUTH
配额超限响应体(error.code/type/messageQUOTA
HTTP 429RATE_LIMIT
HTTP 400 + 上下文容量特征CONTEXT_WINDOW_EXCEEDED
HTTP 400 其他INVALID_REQUEST
HTTP ≥ 500SERVER
其他非 2xxHTTP_<status>

Retry-After 变成 providerRetryAfterMsx-request-id/x-deepseek-request-id 变成 requestId,两者都被运行中的重试策略使用。每次线上请求都会注入属性与匿名用户头(attributionHeaders()x-deepseek-harness-user-id,外加 sessionId/compact 标记)。

配置键(cordis.yml / llm-deepseek 设置段)

默认含义
apiKeyEnvDEEPSEEK_API_KEY凭据引用(环境变量名)
baseURL$DEEPSEEK_BASE_URL → 公共端点端点 base
thinkingprovider 默认'enabled' | 'disabled'
reasoningEfforthigh'off' | 'high' | 'max'
maxTokens256,000每次请求的输出上限
defaultContextWindow1,000,000回退上下文容量
modelsV4 Flash + V4 Pro咨询性目录
streamIdleTimeoutMs300,000空闲读取看门狗
retryPolicynormal 默认provider 自有重试策略

通过 Config schemastery schema 注册(见 packages/llm/llm-deepseek/src/index.ts),经 installSettingsSection 挂载,使插件的 config 与 llm-deepseek 设置段同形。

延伸阅读

  • LLM 层——本 provider 实现的 LlmAdapter/LlmRuntime 契约。
  • 设置系统——schemastery Config schema 及其注册所经的设置接缝。
  • Token 计量——mapUsage 的不相交用量如何喂给基线。
  • packages/llm/llm-deepseek/src/adapter.ts——DeepSeekAdapterDeepSeekConnectionOptions、错误码映射。
  • packages/llm/llm-deepseek/src/serialize.tssse.tstranslate.ts——请求/SSE/流流水线。
  • packages/llm/llm-deepseek/src/index.ts——apply()ConfigresolveAdapterOptionsPUBLIC_BASE_URL