dsh-llm-finish-reason-tolerance:让缺失 finish_reason 的流式响应正常收尾

前言

给智能体接 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.jsondsh.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.yamlllm-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、apiopenai-completionsbaseURL 写完整的 …/api/v2/cortex/v1
2、OpenAI SDK 会自动用 apiKeyEnv 指向的环境变量做 Authorization: Bearer 认证,不需要额外配置 headers;
3、插件只解决缺失的 finish_reasondeepseek-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 。

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye