Capture OpenAI-compatible reasoning and replay it in assistant history
ci / vet-and-test (pull_request) Successful in 51s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s

Why: Interleaved-thinking models (MiniMax, DeepSeek R1) require the
assistant's thinking to be passed back verbatim in message history; dropping
it degrades later turns. The product's P2-I1 replayed thinking only into the
request-side rebuild; with the session transcript becoming the authoritative
context (quality P2-I2), capture must live in the kernel so thinking persists
in the transcript and replays on every request.

Evidence: SELF - TestReasoningCaptureAndReplay covers capture (SSE
reasoning_content into Message.Thinking, content unpolluted) and replay
(<think> prefix on thinking assistants, untouched non-thinking messages);
go vet + go test ./... pass.

Impact: sse.go accumulates reasoning deltas into the terminal message's
Thinking; client.go toProviderMessages renders captured thinking as a
<think> block prefix for assistant messages. Anthropic path unchanged;
messages without thinking serialize exactly as before.
This commit is contained in:
2026-10-09 15:28:56 +00:00
parent 37c5e6fbd6
commit 3b92cb63b4
5 changed files with 78 additions and 6 deletions
+9 -1
View File
@@ -52,6 +52,7 @@ func ParseSSE(ctx context.Context, r io.Reader) <-chan Event {
func emitParsed(ctx context.Context, r io.Reader, out chan<- Event) {
var content strings.Builder
var reasoning strings.Builder
calls := map[int]*ToolCall{}
order := []int{}
var usage Usage
@@ -108,8 +109,12 @@ func emitParsed(ctx context.Context, r io.Reader, out chan<- Event) {
}
// reasoning_content(国产 OpenAI 兼容模型的思考链)与正文分流:
// 只作为 reasoning_delta 事件外发,不计入 Message.Content(与 runner
// stripThinkBlocks"正文无 think"语义对齐)。
// stripThinkBlocks"正文无 think"语义对齐)。同时累计进 reasoning——
// 交错思考模型(MiniMax/DeepSeek R1 等)要求历史 assistant 原样带回
// 思考内容,丢弃会降低后续轮质量;捕获进 Message.Thinking 随会话
// 转录持久化,请求序列化侧按需回放。
if choice.Delta.Reasoning != "" {
reasoning.WriteString(choice.Delta.Reasoning)
select {
case <-ctx.Done():
emitAbort(out, content.String(), ctx.Err())
@@ -166,6 +171,9 @@ func emitParsed(ctx context.Context, r io.Reader, out chan<- Event) {
order = nil
}
msg := Message{Role: RoleAssistant, Content: content.String(), StopReason: mapFinish(finished, len(order) > 0)}
if reasoning.Len() > 0 {
msg.Thinking = []ThinkingBlock{{Thinking: reasoning.String()}}
}
for _, idx := range order {
msg.ToolCalls = append(msg.ToolCalls, *calls[idx])
}