返回 DeepSeek-Reasonix
reasoning_contract.go
根目录 / internal / provider / reasoning_contract.go
1 package provider
2
3 import (
4 "slices"
5 "strings"
6
7 "reasonix/internal/nilutil"
8 )
9
10 // ReasoningState distinguishes observed empty output from absent and unsafe output.
11 // The zero value preserves inference for sessions written before this field existed.
12 type ReasoningState string
13
14 const (
15 ReasoningEmpty ReasoningState = "empty"
16 ReasoningComplete ReasoningState = "complete"
17 ReasoningIncomplete ReasoningState = "incomplete"
18 ReasoningTruncated ReasoningState = "truncated"
19 )
20
21 // ThinkingBlock retains each Anthropic proof separately, including redacted blocks.
22 // Strings are immutable; old readers continue using the legacy single-block fields.
23 type ThinkingBlock struct {
24 Type string `json:"type"`
25 Thinking string `json:"thinking,omitempty"`
26 Signature string `json:"signature,omitempty"`
27 Data string `json:"data,omitempty"`
28 }
29
30 // ReasoningReplayCapabilities describes wire requirements independently of whether
31 // a particular assistant turn requires replay. Unknown endpoints keep legacy policy.
32 type ReasoningReplayCapabilities struct {
33 Format string
34 RequireSignature bool
35 EmptyFallback string
36 }
37
38 type ReasoningCapabilitiesProvider interface {
39 ReasoningReplayCapabilities() ReasoningReplayCapabilities
40 }
41
42 func ReplayCapabilities(p Provider) ReasoningReplayCapabilities {
43 if nilutil.IsNil(p) {
44 return ReasoningReplayCapabilities{}
45 }
46 if c, ok := p.(ReasoningCapabilitiesProvider); ok {
47 return c.ReasoningReplayCapabilities()
48 }
49 return ReasoningReplayCapabilities{}
50 }
51
52 // HasReplayableReasoning never treats a truncated or unfinished block as empty.
53 func HasReplayableReasoning(p Provider, m Message) bool {
54 if !completeReplayEvidence(m) {
55 return false
56 }
57 caps := ReplayCapabilities(p)
58 if caps.RequireSignature || caps.Format == "anthropic-thinking" {
59 if len(m.ThinkingBlocks) > 0 {
60 for _, b := range m.ThinkingBlocks {
61 if b.Type == "redacted_thinking" && b.Data != "" {
62 continue
63 }
64 if b.Type != "thinking" || (strings.TrimSpace(b.Signature) == "" && (caps.RequireSignature || strings.TrimSpace(b.Thinking) == "")) {
65 return false
66 }
67 }
68 return true
69 }
70 if caps.RequireSignature || m.ReasoningSignature != "" {
71 return strings.TrimSpace(m.ReasoningSignature) != ""
72 }
73 }
74 if caps.Format == "responses-items" && slices.ContainsFunc(m.ResponsesItems, IsReplayableResponsesReasoning) {
75 return true
76 }
77
78 return strings.TrimSpace(m.ReasoningContent) != ""
79 }
80
81 func CanReplayAssistantMessage(p Provider, m Message) bool {
82 decision := DecideReasoningReplay(p, m, true)
83 return decision == ReplayDirect || decision == ReplayCompatible
84 }
85
86 // ReplayDecision keeps protocol compatibility separate from response completeness.
87 type ReplayDecision string
88
89 const (
90 ReplayDirect ReplayDecision = "direct"
91 ReplayCompatible ReplayDecision = "compatible"
92 ReplayRecover ReplayDecision = "recover"
93 ReplayReject ReplayDecision = "reject"
94 )
95
96 // DecideReasoningReplay is the common adapter contract consumed by execution.
97 // Incomplete proof is never replaced with an empty field.
98 func DecideReasoningReplay(p Provider, m Message, complete bool) ReplayDecision {
99 if !RequiresAssistantReasoningReplay(p, m) {
100 return ReplayDirect
101 }
102 if !complete || !completeReplayEvidence(m) {
103 return ReplayReject
104 }
105 if HasReplayableReasoning(p, m) {
106 return ReplayDirect
107 }
108 if _, ok := compatibleReplayMessage(p, m); ok {
109 return ReplayCompatible
110 }
111 if AllowsEmptyReasoningFallback(p) {
112 return ReplayCompatible
113 }
114 return ReplayRecover
115 }
116
116 lines GO