现象
/v1/responses 的 handleResponses 里,原始请求体 respReq 在 echoOpts 构造完之后就没有任何引用了,但它是用 let respReq 声明的,一直活到请求结束。
对比 handleChatCompletions —— 那边在 7488662 之后已经是 openaiReq = null,问题只出在新端点上。
const echoOpts = {
instructions: respReq.instructions === undefined ? null : respReq.instructions,
...
tools: respReq.tools || [], // ← respReq 最后一次被读
};
const ccBody = buildCcRequest(chatReq);
const promptCacheKey = chatReq.prompt_cache_key;
chatReq = null; // 已经释放了转换后的树
// ← 但没释放原始树
为什么这里值得单独一提
respReq 是原始 input items 形态,chatReq 是从它转换出来的。两者都在内存里时:
respReq.input 持有完整的 input item 数组(Codex 多轮对话时可达 MB 级)
chatReq.messages 又持有一份等价的、重新组织过的结构
也就是说这条路径比 chat 路径多一棵树,而 release 只做了一半。
关于收益(先把预期压下来)
我把这个改动单独量过,峰值 RSS 上没有可测收益:
|
run1 |
run2 |
run3 |
| 不释放 |
+265 MB |
+274 MB |
+314 MB |
释放 respReq |
+281 MB |
+273 MB |
+282 MB |
8MB body × 8 并发,慢上游把生命周期拉长到数秒,60ms 采样。
原因是峰值与驻留不是一回事 —— 放大发生在 parse + 重建 + JSON.stringify 期间,那时两棵树按定义都活着。释放只降低驻留,而 OOM 看的是峰值。
所以这条应该按 清理(少留一棵已经无用的树)而不是内存优化来评估。
建议
const ccBody = buildCcRequest(chatReq);
const promptCacheKey = chatReq.prompt_cache_key;
+ // respReq 是 Codex 送来的完整会话,比 chatReq 还大一份
chatReq = null;
+ respReq = null;
我已经把它并进了 #23(那条 PR 现在覆盖三条路径:chat / messages / responses),如果你想单独处理也可以直接从那边摘。
现象
/v1/responses的handleResponses里,原始请求体respReq在echoOpts构造完之后就没有任何引用了,但它是用let respReq声明的,一直活到请求结束。对比
handleChatCompletions—— 那边在7488662之后已经是openaiReq = null,问题只出在新端点上。为什么这里值得单独一提
respReq是原始 input items 形态,chatReq是从它转换出来的。两者都在内存里时:respReq.input持有完整的 input item 数组(Codex 多轮对话时可达 MB 级)chatReq.messages又持有一份等价的、重新组织过的结构也就是说这条路径比 chat 路径多一棵树,而 release 只做了一半。
关于收益(先把预期压下来)
我把这个改动单独量过,峰值 RSS 上没有可测收益:
respReq8MB body × 8 并发,慢上游把生命周期拉长到数秒,60ms 采样。
原因是峰值与驻留不是一回事 —— 放大发生在 parse + 重建 +
JSON.stringify期间,那时两棵树按定义都活着。释放只降低驻留,而 OOM 看的是峰值。所以这条应该按 清理(少留一棵已经无用的树)而不是内存优化来评估。
建议
我已经把它并进了 #23(那条 PR 现在覆盖三条路径:chat / messages / responses),如果你想单独处理也可以直接从那边摘。