DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

zhexuany /

zhexuany/dsh-stateful-responses

Verified

dsh plugin: use the stateful OpenAI Responses API (previous_response_id + delta input) instead of replaying the full conversation

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@8b9d400b

dsh-stateful-responses

CI License: MIT

让 dsh 调用 GPT 模型时使用有状态的 Responses API:每一步只把新增的 input item 和上一轮的 previous_response_id 发给上游,而不是把整段会话重放一遍。

插件只改写发往上游的请求 body,并且只在上游明确证明它保存了上一轮(响应 store: true 且带 response id)之后才发送 previous_response_id。任何不确定的情况都会退回全量请求,因此最坏行为与 未安装插件完全一致。

  • 目标运行时:DSH Harness / dsh 0.2.0-rc.2 及以上
  • 运行环境:Node.js >= 22.19.0,无第三方依赖
  • 上游要求:支持 store + previous_response_id 才有收益;不支持时自动降级为全量
  • 许可:MIT

目录

  • 1. 它解决什么问题
  • 2. 设计目标
  • 3. 架构
  • 4. 工作原理
  • 5. 安全模型
  • 6. 兼容性
  • 7. 安装
  • 8. 配置
  • 9. 可观测性与排错
  • 10. 开发与测试
  • 11. 已知限制
  • 12. 许可

1. 它解决什么问题

dsh 的 agent loop 在每一步都重新构造完整对话——system instructions、工具定义、历史消息、工具结果—— 一并交给 provider。对于官方 OpenAI Responses API 这类服务端有状态的接口,这会造成两处重复:

  1. 传输:每一步都把整段历史重新序列化上传;
  2. 计费:历史 token 在每一步都被重新计入 input。

Responses API 的 continuation 机制正是为此设计的:上游保存上一次 response 的输入与输出,客户端只需要 发送新增 item 和 previous_response_id。

dsh 当前在这里有一个缺口:

  • llm-pi-ai 的 api: openai-responses 适配负责协议转换,但没有 continuation 接线;
  • provider 注册是独占的(同一个 provider 不能再注册 adapter),插件无法从 provider 层接管;
  • 请求扩展只能往请求体里加字段,而"不再重放历史"是会话循环的行为,不是单个字段。

因此本插件把改写点放在进程内最后一层:globalThis.fetch。

2. 设计目标

  • 不修改 dsh 核心、不修改 provider 配置,也不重新实现 OpenAI 客户端;
  • 只在确定安全时改写:宁可发全量,也不让上游收到一个拼接不上的状态;
  • 自动降级:上游不支持时,行为与未安装插件完全一致(不丢上下文、不改变响应);
  • 可解释:日志能回答"为什么改写 / 为什么回退 / 为什么最近没有收益";
  • 有界:内存、链数量和并发语义都有上界,长时间运行不累积。

3. 架构

agent loop
  └── llm-pi-ai  (api: openai-responses)
        └── OpenAI SDK
              └── globalThis.fetch   ← dsh-stateful-responses 在这里拦截
                    └── upstream  POST /v1/responses
模块 职责
index.js 插件入口:name + apply(ctx, config),安装/卸载拦截器
lib/interceptor.js 核心:命中判定、请求改写、响应追踪、回退与降级
lib/chain.js 有界 LRU,保存会话链(response id + item 哈希序列)
lib/items.js item 规范化与哈希、"前缀扩展"判定
lib/sse.js SSE 终止事件(completed/failed/incomplete)收集
lib/config.js 严格配置校验
cordis.patch.yml bundle patch:以 insert 行挂载插件

选择 fetch 层而不是 adapter 层的原因:

  • pi-ai 的 OpenAI SDK 每次请求都使用全局 fetch,body 是 JSON 字符串——这是稳定的调用契约;
  • 不需要了解 pi-ai 内部实现,也不绑定其版本;
  • 插件卸载时可以完整恢复原 fetch,不留残余包装;
  • 不改任何 provider 配置,安装和回滚都只是增删一个 bundle。

4. 工作原理

4.1 命中条件

只有同时满足以下条件才处理,否则原样放行:

  • 方法为 POST;
  • URL 以 /responses 结尾,且命中配置的 baseURLs;
  • body 是 JSON 字符串,含字符串 model 与数组 input;
  • model 命中配置的 models(支持结尾 * 通配);
  • 调用方没有自带 previous_response_id。

4.2 会话链

链的键由请求的稳定要素组成:

chain key = url + model + prompt_cache_key + hash(tools) + hash(instructions)

不同模型、不同工具集、不同 system instructions 会自然分链;prompt_cache_key(dsh 会把会话 id 传给 provider)让不同会话不串链。链存放在有界 LRU 中(maxChains,默认 32),超出后淘汰最久未使用的链。

4.3 item 身份

每个 input / output item 先规范化再取 sha256。规范化会剔除上游可以重新生成的易变字段: id、status、created_at、completed_at、annotations、logprobs、obfuscation。 这些字段只影响比较;真正发出去的永远是未经修改的 delta 原文,因此剔除不会破坏会话内容。

4.4 前缀扩展判定

只有当"本次请求的 item 序列是链中 item 序列的严格前缀扩展"时才允许续传:

  • 前 N 项哈希逐一相同;
  • 且本次至少多出 1 项。

否则说明会话分叉、被编辑或换了上下文,插件丢弃该链并按全量发送。

4.5 改写

命中时把 body 改成等价的增量请求:

{
  "model": "gpt-6.1-sol",
  "store": true,
  "previous_response_id": "resp_...",
  "input": [ "…only the new items…" ]
}

其余字段(tools、instructions、reasoning 参数、采样参数等)保持不变。

4.6 响应追踪

  • 流式(SSE):对 response.body 做 tee(),一份原样交给 pi-ai,另一份交给内部收集器解析终止事件;
  • 非流式(JSON):通过 response.clone().json() 读取,原响应不受影响。

无论哪种形式,插件都不会修改模型返回给 dsh 的字节内容。

4.7 链更新

只有响应满足 store === true 且带非空 id 时才更新链:

items = 链旧 items + 本次 delta + 本次响应 output 的 item 哈希

这条规则的直接推论是:链只在上游证明它保存了之后才存在。

4.8 失败与降级

情况 行为
完成响应 store !== true 该 baseURL 标记为不支持,清空全部链,之后永远全量(每个 baseURL 只提示一次)
响应 failed / cancelled 只丢弃当前链,不标记不支持——失败不能证明上游不存储
续传请求非 2xx,且错误文本匹配 previous_response_id / previous response / not found 丢弃链,用原始全量 body 重试一次
续传请求网络错误(非用户取消) 丢弃链,用原始全量 body 重试一次
解析、规划、追踪等内部异常 放行原请求,不打断模型调用

5. 安全模型

不变量

  1. 没有 store: true 的响应,永远不会生成 previous_response_id;
  2. 每次改写前都做 item 前缀校验,不匹配即全量;
  3. tools / instructions / model / URL / prompt_cache_key 任一变化都不命中旧链;
  4. 调用方自己设置的 previous_response_id 一律放行,不参与链管理;
  5. 对 pi-ai 可见的响应流不做任何修改(SSE 只读 tee 分支);
  6. 非 JSON 字符串 body(Request 对象、流式 body 等)一律放行;
  7. 插件不读取、不缓存、不转发任何凭据:它只包装 fetch,鉴权头仍由 OpenAI SDK 生成并原样发送。

为什么不会丢上下文

  • 能发 previous_response_id 的前提是链存在;链存在的前提是上游返回过 store: true 且有 id;
  • 发送的 input 是本次请求中"比链多出来的部分"的原文,不是插件重建的内容;
  • 链上的 items 由上游返回的 output 追加而成,与上游持有的状态同源;
  • 上述任一环不成立,插件就发全量。

最坏情况

  • 上游完全不支持状态:第一次响应后即被标记,后续全部走原路径——等价于未安装插件;
  • 上游支持但状态过期或被清理:续传被拒,插件用全量重试一次并重建链,用户无感;
  • 插件自身异常:请求原样放行。

隐私

插件会比较 prompt item,但不会持久化、不会写入日志、不会发送到第三方。日志只包含模型名、item 数量、 response id 等元数据,不包含 prompt 或补全内容。

6. 兼容性

  • dsh 0.2.0-rc.2 及以上(依赖 bundle patch 机制与 ctx.effect 生命周期);
  • Node.js >= 22.19.0(插件运行在 dsh host 进程内,不需要额外服务);
  • 仅覆盖 OpenAI Responses 协议(POST …/responses)。llm-deepseek 使用的 Messages 协议没有等价的 continuation 字段,不在覆盖范围内;
  • 上游必须能接受它自己返回过的 output item(包括 reasoning item 的 encrypted_content)在下一次请求中 被带回;否则前缀校验失败,插件退回全量——安全但没有收益。

7. 安装

插件是一个标准 dsh bundle:包内的 cordis.patch.yml 通过 insert 增加一个插件行。

  1. 把仓库放到任意目录,例如 ~/dev/dsh-stateful-responses。

  2. 在目标 profile(~/.dsh/profiles/<name>/package.json)中添加依赖与 bundle:

    {
      "dependencies": {
        "dsh-stateful-responses": "link:/home/you/dev/dsh-stateful-responses"
      },
      "dsh": {
        "profile": {
          "bundles": ["…", "dsh-stateful-responses"]
        }
      }
    }
    
  3. 在该 profile 目录执行 pnpm install。

  4. 重启 dsh(bundle 列表在启动时读取)。

插件默认不限定 baseURL 与模型(空数组 = 全部命中)。生产使用时建议把 baseURLs / models 收窄到真正支持 Responses 状态的上游与模型,避免向其它端点发送 store: true。

8. 配置

键 类型 默认 说明
enabled boolean true 关闭后不安装拦截器
forceStore boolean true 改写时强制 store: true
baseURLs string[] [] 只拦截这些 URL 前缀下的 /responses;空 = 全部
models string[] [] 只拦截这些模型 id;支持结尾 * 通配;空 = 全部
maxChains positive integer 32 最多保留多少条会话链
debug boolean false 输出每次改写 / 回退的决策日志

示例(cordis.patch.yml):

- id: stateful-responses
  name: dsh-stateful-responses
  config:
    enabled: true
    forceStore: true
    baseURLs: ["https://api.openai.com/v1"]
    models: ["gpt-*"]
    maxChains: 32
    debug: false

配置是严格的:未知键或类型错误会在激活时输出 warn,并且插件不会启用,而不是静默忽略。

9. 可观测性与排错

debug: true 时会输出:

  • rewriting request:本轮改写为增量(模型、item 数、delta 数、previous_response_id);
  • chain updated:链已推进(response id、item 总数、本轮是全量还是增量);
  • continuation rejected by upstream; retrying once with full history:续传被拒并已回退;
  • response did not complete; chain dropped:失败响应导致链丢弃(上游仍被视为可用)。

每个 baseURL 只会出现一次的 info 级提示:

upstream does not retain responses (store is not true); using full history from now on

启用时输出 stateful Responses API interceptor installed;卸载时输出 restoring global fetch 以及本次 进程累计统计:requests / continuations / deltaItems / fullItems / fallbacks / unsupportedBases / errors / chains。

没有命中任何条件时,插件不产生日志,也不改变请求。

10. 开发与测试

index.js              插件入口(name + apply)
lib/config.js         严格配置
lib/items.js          item 规范化与哈希
lib/chain.js          有界 LRU
lib/interceptor.js    fetch 拦截、改写、追踪、回退
lib/sse.js            SSE 终止事件收集
cordis.patch.yml      bundle patch(insert 行)
test/                 node:test 用例(本地 mock 上游)

运行测试:

npm test          # 等价于 node --test test/*.test.mjs

测试覆盖:

  • 无状态上游:全量转发并被标记,之后不再改写;
  • 有状态上游:连续两轮只发 delta,previous_response_id 正确串联;
  • 续传被拒:丢弃链并回退一次全量,重试成功后重新建链;
  • 流式上游:SSE 原样透传,下一轮仍能续传;
  • 非目标模型、调用方自带的 previous_response_id:完全不处理;
  • 会话分叉:全量重放并丢弃过期链;
  • prompt_cache_key 隔离;失败响应不误标记;配置校验;item 规范化;LRU 淘汰;SSE 分帧。

无第三方依赖,使用 Node.js 内置 node:test。

11. 已知限制

  • 收益完全取决于上游是否真的实现 Responses 存储;对无状态网关没有收益(但也没有副作用)。
  • 只减少"发往上游的 body";dsh 本地 session log 与 loop 仍然构建完整历史。
  • 同一链 key 的并发请求可能互相覆盖;下一次请求会因前缀不匹配而全量,随后自愈。
  • 不做内容压缩或摘要,不改变模型选择、工具集或 system instructions。
  • 不处理 Request 对象或流式请求体(pi-ai 当前不使用这些形式)。

12. 许可

MIT © Zhexuan Yang

—/ 5

No ratings yet

Verified DSH bundle

Commit 8b9d400b12f8

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout