@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-llm、dsh-credentials、dsh-settings、dsh-timeout、dsh-launch-environment、dsh-anonymous-user-id、dsh-invariants 与 cordis。它用原生 fetch 与 DeepSeek 平台通信——没有独立的平台 API 客户端包(BFF/"remotes"包是另一回事;见 API 网关)。
适配器
DeepSeekAdapter(packages/llm/llm-deepseek/src/adapter.ts)继承 LlmAdapter。它为注册名下的每个模型服务:harness 模型 id 就是线上模型 id(listModels/resolveModel 原样返回所配置的内容)。构造一个实例并绑定到按请求的解析钩子:
export interface DeepSeekAdapterOptions {
options: () => DeepSeekConnectionOptions // 校验过的连接事实,按操作解析
resolveApiKey: (connection) => Promise<string> // 无 key 时抛 LlmError MISSING_CREDENTIAL
resolveUserId: () => AnonymousUserId
}DeepSeekConnectionOptions 冻结一次解析的端点事实:baseURL(追加 /chat/completions)、一个 apiKeyEnv 凭据引用(绝不是字面 key)、defaults(thinking/effort)、maxTokens、defaultContextWindow、咨询性 models 目录、streamIdleTimeoutMs 与已解析的 retryPolicy。每次 stream() 都从同一份快照重新运行 options() 与 resolveApiKey(connection),因此进行中的流永远不会观察到配置变更,端点也绝不会与另一代的 key 配对。
Base URL 解析
apply()(在 packages/llm/llm-deepseek/src/index.ts)按此优先级解析 baseURL:
- 显式
config.baseURL(来自cordis.yml或llm-deepseek设置段); - 来自可信 launch-environment 层的
$DEEPSEEK_BASE_URL(让 checkout 可以把它的 agent 指向其 checkout 所针对的网关); - 公共常量
PUBLIC_BASE_URL = 'https://api.deepseek.com'。
resolveAdapterOptions() 即使在构造绕过 Schemastery 时(编程式构建)也会重新校验每个默认值/绑定,加载时响亮失败,或在实时编辑违反 schema 之外的绑定时保留最后一个好的设置快照(keep the last good configuration)。
暴露的模型
默认解析为两个 V4 模型,共享 1M-token 上下文窗口:
| 模型 id | 显示名 | 上下文 |
|---|---|---|
deepseek-v4-flash | DeepSeek-V4-Flash | 1,000,000 |
deepseek-v4-pro | DeepSeek-V4-Pro | 1,000,000 |
目录中可覆盖每个模型的 maxTokens 与 contextWindow;没有精确值的被选模型回退到 defaultContextWindow(默认 1,000,000)与 defaultMaxTokens(默认 256,000)。每条路由的 inputModalities 都是 ['text']——线上路由纯文本。
请求映射:harness → DeepSeek 线上
serializeRequest / serializeMessages(serialize.ts)把 harness 词汇映射到 POST {baseURL}/chat/completions 的 OpenAI 兼容请求体:
| Harness 概念 | DeepSeek 线上 |
|---|---|
Message.role | messages[].role(system/user/assistant/tool) |
| 用户消息中的一条工具结果 | 展开为独立的 {role:'tool', tool_call_id, content} 条目 |
assistant 内容 | content;无文本的 turn 发送 ""——绝不发送 null |
| assistant 推理 | reasoning_content(CoT 回传),只在工具调用 turn 上 |
| assistant 工具调用 | tool_calls[](id、type:'function'、function{name,arguments}) |
GenerateOptions.tools | 带 JSON-Schema parameters 的 tools[] |
thinking/reasoningEffort | 顶层 thinking:{type} + reasoning_effort(`'high' |
temperature / maxTokens / stop | temperature、max_tokens、stop |
| 流式 | 总是 stream:true、stream_options:{include_usage:true} |
图片被 assertTextOnly 拒绝(抛 UNSUPPORTED_CONTENT),发生在任何文本扁平化可能静默抹掉它们之前。resolveThinking 映射适配器自有的 effort:off ⇒ thinking:{type:'disabled'};high/max ⇒ thinking:{type:'enabled'} + effort;session-title 用途总是强制关闭 thinking,让标题调用快速返回可见文本。
流式与翻译流水线
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')mapFinishReason 把 stop → stop、tool_calls → tool-calls、length → max-tokens,任何无法识别的 reason(如 content_filter)映射为 error finish,错误码为大写后的 reason。
Token 核算
mapUsage 把 DeepSeek 的 prompt_tokens(包含缓存命中)转换为 harness 的不相交约定:
// 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):
- 向
ctx.credentials(凭据接缝)请求那个CredentialRef;能解析就用它; - 否则在未挂载凭据接缝时回退到
launchEnvironmentOf(ctx).get(ref)(shell 导出的.env层); - 否则抛
LlmError('MISSING_CREDENTIAL')。
assertUsableApiKey 会 trim 并拒绝空白/非 header 安全 key(错误码 INVALID_CREDENTIAL),且不回显任何秘密。文件存储的故事见 凭据管理 与 credentials-local provider($DSH_HOME 下的 .credentials.yaml + .env)。
错误处理摘要
| 线上信号 | LlmError 错误码 |
|---|---|
| HTTP 401 / 403 | AUTH |
配额超限响应体(error.code/type/message) | QUOTA |
| HTTP 429 | RATE_LIMIT |
| HTTP 400 + 上下文容量特征 | CONTEXT_WINDOW_EXCEEDED |
| HTTP 400 其他 | INVALID_REQUEST |
| HTTP ≥ 500 | SERVER |
| 其他非 2xx | HTTP_<status> |
Retry-After 变成 providerRetryAfterMs;x-request-id/x-deepseek-request-id 变成 requestId,两者都被运行中的重试策略使用。每次线上请求都会注入属性与匿名用户头(attributionHeaders()、x-deepseek-harness-user-id,外加 sessionId/compact 标记)。
配置键(cordis.yml / llm-deepseek 设置段)
| 键 | 默认 | 含义 |
|---|---|---|
apiKeyEnv | DEEPSEEK_API_KEY | 凭据引用(环境变量名) |
baseURL | $DEEPSEEK_BASE_URL → 公共端点 | 端点 base |
thinking | provider 默认 | 'enabled' | 'disabled' |
reasoningEffort | high | 'off' | 'high' | 'max' |
maxTokens | 256,000 | 每次请求的输出上限 |
defaultContextWindow | 1,000,000 | 回退上下文容量 |
models | V4 Flash + V4 Pro | 咨询性目录 |
streamIdleTimeoutMs | 300,000 | 空闲读取看门狗 |
retryPolicy | normal 默认 | provider 自有重试策略 |
通过 Config schemastery schema 注册(见 packages/llm/llm-deepseek/src/index.ts),经 installSettingsSection 挂载,使插件的 config 与 llm-deepseek 设置段同形。
延伸阅读
- LLM 层——本 provider 实现的
LlmAdapter/LlmRuntime契约。 - 设置系统——schemastery
Configschema 及其注册所经的设置接缝。 - Token 计量——
mapUsage的不相交用量如何喂给基线。 packages/llm/llm-deepseek/src/adapter.ts——DeepSeekAdapter、DeepSeekConnectionOptions、错误码映射。packages/llm/llm-deepseek/src/serialize.ts、sse.ts、translate.ts——请求/SSE/流流水线。packages/llm/llm-deepseek/src/index.ts——apply()、Config、resolveAdapterOptions、PUBLIC_BASE_URL。