返回 DeepSeek-Reasonix
reasoning.go
根目录 / internal / provider / reasoning.go
1 package provider
2
3 import (
4 "fmt"
5 "slices"
6 "strings"
7 )
8
9 // ReasoningOption is an adapter-owned identifier, not a global effort enum.
10 type ReasoningOption struct {
11 ID string `json:"id"`
12 Name string `json:"name"`
13 Description string `json:"description,omitempty"`
14 }
15
16 // ReasoningCapability describes one resolved endpoint/model. Empty Default
17 // preserves provider-default behavior; an empty option list offers no control.
18 type ReasoningCapability struct {
19 Options []ReasoningOption `json:"options"`
20 Default string `json:"default,omitempty"`
21 // Unknown distinguishes missing metadata from a declared lack of controls.
22 Unknown bool `json:"unknown,omitempty"`
23 }
24
25 type ReasoningProvider interface{ ReasoningCapability() ReasoningCapability }
26
27 // UnsupportedReasoningEffort is returned before provider I/O. Never clamp an
28 // explicit selection or silently replace it with the configured default.
29 type UnsupportedReasoningEffort struct {
30 Model, Effort string
31 Supported []string
32 Unknown bool
33 }
34
35 func (e *UnsupportedReasoningEffort) Error() string {
36 if e.Unknown {
37 return fmt.Sprintf("UNKNOWN_MODEL_REASONING: reasoning levels for model %q are not declared; cannot validate %q. Select auto or configure this model's reasoning_protocol and supported_efforts", e.Model, e.Effort)
38 }
39 return fmt.Sprintf("UNSUPPORTED_REASONING_EFFORT: model %q does not support %q (supported: %v)", e.Model, e.Effort, e.Supported)
40 }
41
42 // UnknownReasoning allows provider-default requests without inventing controls.
43 func UnknownReasoning() ReasoningCapability {
44 return ReasoningCapability{Options: []ReasoningOption{}, Unknown: true}
45 }
46
47 func (c ReasoningCapability) State() string {
48 if c.Unknown {
49 return "unknown"
50 }
51 if len(c.Options) == 0 {
52 return "unsupported"
53 }
54 return "supported"
55 }
56 func (c ReasoningCapability) IDs() []string {
57 ids := make([]string, 0, len(c.Options))
58 for _, option := range c.Options {
59 ids = append(ids, option.ID)
60 }
61 return ids
62 }
63 func (c ReasoningCapability) Clone() ReasoningCapability {
64 c.Options = slices.Clone(c.Options)
65 return c
66 }
67 func (c ReasoningCapability) Validate(model, effort string) error {
68 ids := c.IDs()
69 seen := map[string]bool{}
70 for _, id := range ids {
71 if id == "" || id == "auto" || seen[id] {
72 return fmt.Errorf("INVALID_MODEL_REASONING: model %q has invalid or repeated effort ID %q", model, id)
73 }
74 seen[id] = true
75 }
76 if c.Default != "" && !slices.Contains(ids, c.Default) {
77 return &UnsupportedReasoningEffort{Model: model, Effort: c.Default, Supported: ids, Unknown: c.Unknown}
78 }
79 if effort == "" {
80 return nil
81 }
82 if !slices.Contains(c.IDs(), effort) {
83 return &UnsupportedReasoningEffort{Model: model, Effort: effort, Supported: c.IDs(), Unknown: c.Unknown}
84 }
85 return nil
86 }
87 func ReasoningOptions(def string, ids ...string) ReasoningCapability {
88 c := ReasoningCapability{Options: make([]ReasoningOption, 0, len(ids)), Default: def}
89 for _, id := range ids {
90 c.Options = append(c.Options, ReasoningOption{ID: id, Name: id})
91 }
92 return c
93 }
94
95 // DeclaredReasoning replaces fallback vocabulary only when a deployment has
96 // explicitly declared it. The adapter still owns the serializer and policy.
97 func DeclaredReasoning(cfg Config, fallback ReasoningCapability) ReasoningCapability {
98 ids, _ := cfg.Extra["supported_efforts"].([]string)
99 clean := make([]string, 0, len(ids))
100 for _, id := range ids {
101 if strings.TrimSpace(id) != "" && strings.TrimSpace(id) != "auto" && !slices.Contains(clean, id) {
102 clean = append(clean, id)
103 }
104 }
105 ids = clean
106 if len(ids) > 0 {
107 fallback = ReasoningOptions(ids[0], ids...)
108 }
109 if def, _ := cfg.Extra["default_effort"].(string); def != "" {
110 fallback.Default = def
111 }
112 return fallback
113 }
114
115 // Reasoning factories register alongside adapter factories during init. They
116 // consume non-secret configuration and must not perform I/O.
117 var reasoningRegistry = map[string]func(Config) ReasoningCapability{}
118
119 func RegisterReasoning(kind string, resolve func(Config) ReasoningCapability) {
120 if _, exists := reasoningRegistry[kind]; exists {
121 panic("duplicate reasoning adapter: " + kind)
122 }
123 reasoningRegistry[kind] = resolve
124 }
125 func ReasoningForConfig(kind string, cfg Config) ReasoningCapability {
126 if resolve, ok := reasoningRegistry[kind]; ok {
127 return resolve(cfg).Clone()
128 }
129 return UnknownReasoning()
130 }
131
132 // RestrictReasoning keeps deployment declarations within a fixed wire protocol.
133 func RestrictReasoning(c ReasoningCapability, ids ...string) ReasoningCapability {
134 options := make([]ReasoningOption, 0, len(c.Options))
135 for _, option := range c.Options {
136 if slices.Contains(ids, option.ID) {
137 options = append(options, option)
138 }
139 }
140 c.Options = options
141 return c
142 }
143
144 // PreferredReasoning is for host-owned automatic policies only. Explicit user
145 // selections must use Validate and report rejection, never this fallback.
146 func PreferredReasoning(p Provider, id string) string {
147 if owner, ok := p.(ReasoningProvider); ok && owner.ReasoningCapability().Validate("", id) == nil {
148 return id
149 }
150 return ""
151 }
152
152 lines GO