fix(openai-compat): Responses→Chat 桥接按 reasoning item id 缓存回注 reasoning_content

修复 #5520:Codex 经 force_chat_completions 桥接到 DeepSeek thinking 上游时,
历史中的 encrypted-only reasoning item(summary 为空 + 不透明 encrypted_content,
远程 compaction / 跨会话恢复后常见)取不出明文,后续 assistant 消息缺
reasoning_content,DeepSeek 400 "The reasoning_content in the thinking mode
must be passed back to the API",客户端仅看到通用 502。

reasoning item 的 id 一定会被客户端回传,以其为 key 做服务端缓存:

- GatewayCache 新增 Set/GetReasoningContent(Redis,默认 TTL 7 天)
- 响应侧:流式扫 response.output_item.done、非流式扫 output,把 reasoning
  全文按 item id 写缓存;客户端断连后 drain 期间用 detached ctx 仍会写完
- 请求侧:apicompat 新增 ResponsesToChatCompletionsRequestWithOptions 与
  ReasoningContentByID 钩子,encrypted-only item 查缓存补回 pendingReasoning;
  缓存 miss/出错一律 fail-open 维持原行为
- 自愈:历史里带明文 summary 的 reasoning item 顺手刷新缓存,覆盖 Redis
  flush / 跨实例漂移

测试:apicompat(命中恢复/miss 保持原样/明文优先)、service 端到端(流式
写缓存、请求侧回注+自愈)、repository miniredis 存取。
This commit is contained in:
lbyxiaolizi
2026-08-17 16:48:36 +08:00
parent e330c243a8
commit 612436a5a7
15 changed files with 582 additions and 6 deletions
@@ -131,6 +131,48 @@ func (c *gatewayCache) ReleaseGrokVideoBilled(ctx context.Context, key string) e
var _ service.CyberSessionBlockStore = (*gatewayCache)(nil)
var _ service.LiveCallStore = (*gatewayCache)(nil)
const reasoningContentPrefix = "reasoning_content:"
// reasoningContentDefaultTTL 是 reasoning 缓存的默认过期时间。Codex 会话可能
// 跨多天恢复,取 7 天;调用方传入非正 TTL 时兜底。
const reasoningContentDefaultTTL = 7 * 24 * time.Hour
// SetReasoningContent 按 reasoning item id 缓存 reasoning 全文。
// itemID 或 content 为空时直接返回 nil(无可缓存内容,属正常情况而非错误)。
func (c *gatewayCache) SetReasoningContent(ctx context.Context, itemID string, content string, ttl time.Duration) error {
if c == nil || c.rdb == nil {
return errors.New("gateway cache unavailable")
}
itemID = strings.TrimSpace(itemID)
if itemID == "" || content == "" {
return nil
}
if ttl <= 0 {
ttl = reasoningContentDefaultTTL
}
return c.rdb.Set(ctx, reasoningContentPrefix+itemID, content, ttl).Err()
}
// GetReasoningContent 返回缓存的 reasoning 全文;未命中返回
// service.ErrReasoningContentNotFound。
func (c *gatewayCache) GetReasoningContent(ctx context.Context, itemID string) (string, error) {
if c == nil || c.rdb == nil {
return "", errors.New("gateway cache unavailable")
}
itemID = strings.TrimSpace(itemID)
if itemID == "" {
return "", service.ErrReasoningContentNotFound
}
val, err := c.rdb.Get(ctx, reasoningContentPrefix+itemID).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return "", service.ErrReasoningContentNotFound
}
return "", err
}
return val, nil
}
const cyberSessionBlockPrefix = "cyber_session_block:"
// SetCyberSessionBlocked 把被 cyber_policy 命中的会话写入屏蔽表(TTL 自动过期)。