前言¶
给智能体接 OpenAI 兼容模型时,有一个不太显眼但很致命的兼容性问题:部分网关的流式响应只推送内容增量和结束标记,从头到尾不发 finish_reason 字段。DSH 的模型客户端 pi-ai 会把这种流判定为截断(Stream ended without finish_reason),宿主再把它映射成一个 TRANSPORT 错误 finish。结果是:每次请求内容都完整送达了,回合却以失败告终。
典型的例子是 Snowflake Cortex 的 REST 网关(*.snowflakecomputing.com/api/v2/cortex/v1):它的 Chat Completions SSE 流会推送内容增量和一个终止的 data: [DONE],但从不发 finish_reason——纯文本如此,工具调用也如此(非流式响应返回的是 finish_reason: "")。
要绕开它,要么改 pi-ai 源码,要么在网关前架一层代理补字段,都要额外维护一处。下面介绍的 dsh-llm-finish-reason-tolerance 选了第三条路:在 harness 侧把这条特定终态错误改写成内容本应得到的成功 finish。
这是什么¶
dsh-llm-finish-reason-tolerance 是一个 DeepSeek Harness(DSH)宿主插件,由 michael-han-il 维护,当前版本 0.2.0,MIT 许可证。它做的事很克制:只针对「流式响应结束但没有 finish_reason 且内容已送达」这一种终态错误做改写,其余一切原样透传。
实现上它没有修改 pi-ai、dsh-llm 或 dsh-llm-pi-ai 的任何代码,纯靠 harness 的挂载机制工作。
工作原理¶
插件监听 harness 的 llm/stream 瀑布流,全局注册并前置,它返回的可迭代对象就是消费方实际迭代的那一个,因此能包装每一个模型流。改写规则分三种情况:
1、已交付 tool-call 块 → 终态 finish 改为 { kind: 'tool-calls' };
2、已交付文本内容 → 终态 finish 改为 { kind: 'stop' };
3、未交付任何内容,或是其他错误 → 原样透传。
改写是双重门控的:pi-ai 的终态错误消息必须恰好是 Stream ended without finish_reason,且已有内容交付。真正的中途截断在 pi-ai 里表现为不同的错误消息,不会被这条规则掩盖。
安装与启用¶
先确认环境:需要 DeepSeek Harness(较新的部署已随附 @deepseek-ai/cordis ≥ 4 与 @deepseek-ai/schemastery ≥ 3),Node ≥ 20。
这个插件是宿主级、单实例的:必须安装进一个 profile(如 web)并挂载在宿主组合中,不要挂进 agent preset。
Bundle 安装(推荐)¶
包以 profile bundle 形式发布,自带 cordis.patch.yml 补丁层(通过 package.json 里的 dsh.bundle.patch 声明),安装即挂载,不需要手动编辑宿主组合:
dsh plugin --profile web add /path/to/dsh-llm-finish-reason-tolerance-0.2.0.tgz
这一条命令做了三件事:
1、pnpm 把包装进 profile 的依赖树;
2、dsh plugin CLI 检测到 dsh.bundle.patch,自动把包追加进 profile 的 package.json 中 dsh.profile.bundles 列表:
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-llm-finish-reason-tolerance"] } }
3、下次启动时,bundle 自带的补丁层把 llm-finish-reason-tolerance 行插入宿主组合。
其他 profile 换成对应名字即可,例如 dsh plugin --profile tui add …。注意官方文档给出的安装形式是本地 tgz 路径,没有提供从 npm 或 GitHub 直接安装的命令。
Plain 安装(备选)¶
如果偏好普通依赖方式,先在 profile 目录(如 ~/.dsh/profiles/web)装包:
pnpm add /path/to/dsh-llm-finish-reason-tolerance-0.2.0.tgz
再在 ~/.dsh/profiles/<profile>/cordis.patch.yml 里手动添加挂载行。profile 补丁层是一组补丁操作的列表,新行必须包在 insert: 里——裸的 id: 行是对已有条目的覆盖操作,匹配不到就静默跳过:
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- insert:
- id: llm-finish-reason-tolerance
name: dsh-llm-finish-reason-tolerance
config:
providers:
- snowflake-cortex
两种安装方式不要混用,否则挂载行会被插入两次。
重启¶
无论哪种方式,安装后必须重启 harness——插件是新模块,只在启动时挂载。重启后,插件列表里会出现 llm-finish-reason-tolerance。
配置¶
只有一个配置项:
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
providers |
string[] |
[] |
本容错行为生效的 provider 路由键(对应 llm-pi-ai 设置段里的命名)。[] 表示所有 provider。 |
Bundle 安装默认带的是 providers: [],即对所有 provider 路由生效。要限定到特定路由,在你自己的 profile 补丁层覆盖这一行的 config:
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: llm-finish-reason-tolerance
config:
providers:
- snowflake-cortex
能这样覆盖,是因为 bundle 层先应用,它插入的行可以被后续层定位。注意 config 是整体替换,所以要写全量。Plain 安装则直接改 insert: 行里的 providers 值即可。
配合 Snowflake Cortex¶
Snowflake 的 provider 要配置在 ~/.dsh/settings.yaml 的 llm-pi-ai 设置段下,路由键与插件的 providers 列表对应。默认空列表对所有路由生效,无需匹配;如果限定了范围,路由键必须精确一致(如 snowflake-cortex)。
# ~/.dsh/settings.yaml
llm-pi-ai:
providers:
snowflake-cortex:
displayName: Snowflake Cortex (SG)
apiKeyEnv: SNOWFLAKE_CORTEX_API_KEY
api: openai-completions
baseURL: https://<account>.snowflakecomputing.com/api/v2/cortex/v1
models:
- id: claude-sonnet-5
name: Claude Sonnet 5
# 例如你账号里可用的其他模型
# - id: deepseek-r1
# name: DeepSeek R1
几点说明:
1、api 用 openai-completions,baseURL 写完整的 …/api/v2/cortex/v1;
2、OpenAI SDK 会自动用 apiKeyEnv 指向的环境变量做 Authorization: Bearer 认证,不需要额外配置 headers;
3、插件只解决缺失的 finish_reason;deepseek-r1 在该端点已弃用,装不装插件都无法使用。
适用场景与注意事项¶
适合的场景很明确:你的 DSH 接了会省略 finish_reason 的 OpenAI 兼容网关(典型是 Snowflake Cortex),每次请求内容都到、回合却以 TRANSPORT 错误收场。装上插件、重启,回合就能正常完成。
安全性方面,改写条件收得很紧:
1、只在 pi-ai 终态错误恰为 Stream ended without finish_reason 且已有内容交付时触发;
2、鉴权、限流、配额、真正的中途截断、空响应等错误全部原样透传;
3、既没有 finish_reason 也没有内容的响应,保留原来的错误 finish。
需要自己留意的几点:
1、插件以当前 dsh 进程的权限运行,安装前建议先审查源码和许可证(本项目为 MIT,仓库地址见文末);
2、宿主级单实例,装进 profile,不要挂进 agent preset;
3、两种安装方式不可混用。
想参与开发的话,仓库结构很直接:lib/index.js 是插件本体(纯 ESM,无构建步骤,harness 直接加载),test/finish-reason.test.mjs 是自包含的包装器测试。常用命令:
npm test # node --test,用合成的分块流测试包装逻辑
npm pack # 构建分发包
小结¶
一句话总结:当你接的 OpenAI 兼容网关不发 finish_reason 时,这个插件把「内容送达但回合失败」变成「内容送达、回合正常完成」,且只改写这一种情况,不掩盖其他任何错误。安装成本也低——bundle 形式一条命令,重启即生效。
插件页在社区目录:https://www.skillhub.cn/plugins/michael-han-il/dsh-llm-finish-reason-tolerance (目录页将其归在「模型推理」分类下;该目录是独立站点,与 DeepSeek、幻方无官方从属关系)。源码与 README:https://github.com/michael-han-il/dsh-llm-finish-reason-tolerance 。