Files
sub2api/backend/internal/repository/gateway_cache_reasoning_test.go
lbyxiaolizi 612436a5a7 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 存取。
2026-08-17 16:48:36 +08:00

42 lines
1.4 KiB
Go

package repository
import (
"context"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)
func TestGatewayCacheReasoningContent(t *testing.T) {
mr := miniredis.RunT(t)
client := redis.NewClient(&redis.Options{Addr: mr.Addr()})
cache := NewGatewayCache(client)
ctx := context.Background()
// 未命中返回哨兵错误,区别于真实读取失败。
_, err := cache.GetReasoningContent(ctx, "item_missing")
require.ErrorIs(t, err, service.ErrReasoningContentNotFound)
// 写入后可读回。
require.NoError(t, cache.SetReasoningContent(ctx, "item_abc", "think hard", time.Minute))
got, err := cache.GetReasoningContent(ctx, "item_abc")
require.NoError(t, err)
require.Equal(t, "think hard", got)
// ttl<=0 时兜底为默认 7 天。
require.NoError(t, cache.SetReasoningContent(ctx, "item_ttl", "x", 0))
ttl := mr.TTL(reasoningContentPrefix + "item_ttl")
require.Greater(t, ttl, 6*24*time.Hour)
require.LessOrEqual(t, ttl, reasoningContentDefaultTTL)
// 空 itemID / 空 content 是 no-op(无可缓存内容不算错误)。
require.NoError(t, cache.SetReasoningContent(ctx, "", "x", time.Minute))
require.NoError(t, cache.SetReasoningContent(ctx, "item_empty", "", time.Minute))
_, err = cache.GetReasoningContent(ctx, "item_empty")
require.ErrorIs(t, err, service.ErrReasoningContentNotFound)
}