返回 DeepSeek-Reasonix
image_request.go
根目录 / internal / control / image_request.go
1 package control
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "path"
8 "strings"
9
10 "reasonix/internal/attachment"
11 "reasonix/internal/config"
12 "reasonix/internal/i18n"
13 "reasonix/internal/imageinput"
14 "reasonix/internal/provider"
15 "reasonix/internal/provider/openai"
16 )
17
18 func (c *Controller) StageImage(ctx context.Context, displayName, mime, dataURL string) (attachment.DraftCredential, error) {
19 ctx, cancel := c.NewAttachmentOperationContext(ctx)
20 defer cancel()
21 scope := c.attachmentScope()
22 svc := c.attachmentService()
23 prepared, err := svc.PrepareBatch(ctx, []attachment.Source{{DisplayName: displayName, DeclaredMIME: mime, DataURL: dataURL}})
24 if err != nil {
25 return attachment.DraftCredential{}, err
26 }
27 refs, err := svc.CommitBatch(ctx, prepared)
28 if err != nil {
29 return attachment.DraftCredential{}, err
30 }
31 if err := ctx.Err(); err != nil {
32 return attachment.DraftCredential{}, err
33 }
34 if scope != c.attachmentScope() {
35 return attachment.DraftCredential{}, attachment.Error{Code: attachment.CodeChanged, Message: "attachment owner changed; please retry"}
36 }
37 return svc.Drafts().Issue(scope, refs[0]), nil
38 }
39
40 func (c *Controller) ReadDraftImage(ctx context.Context, draftID string) (attachment.DraftCredential, []byte, error) {
41 draft, ok := c.attachmentService().Drafts().Lookup(c.attachmentScope(), draftID)
42 if !ok {
43 return attachment.DraftCredential{}, nil, attachment.Error{Code: attachment.CodeMissing, Message: "draft credential is not valid", Retry: true}
44 }
45 raw, err := c.attachmentService().ReadVerified(ctx, draft.Ref)
46 return draft, raw, err
47 }
48
49 func (c *Controller) ReleaseDraftImage(draftID string) {
50 c.attachmentService().Drafts().Release(c.attachmentScope(), draftID)
51 }
52
53 // ReadSessionAttachment returns one bounded range of an admitted original.
54 // digest is the only client-supplied identity; size and integrity come from
55 // this session's content graph.
56 func (c *Controller) ReadSessionAttachment(ctx context.Context, digest string, offset, length int64) ([]byte, int64, error) {
57 if c == nil {
58 return nil, 0, attachment.Error{Code: attachment.CodeMissing, Message: "attachment is not authorized for this session"}
59 }
60 service := c.SessionService()
61 ref, bound := c.SessionRef()
62 if service == nil || service.Query() == nil || !bound {
63 return nil, 0, attachment.Error{Code: attachment.CodeMissing, Message: "attachment is not authorized for this session"}
64 }
65 return service.Query().ReadSessionAttachment(ctx, ref, digest, offset, length)
66 }
67
68 func (c *Controller) PersistToolImages(ctx context.Context, images []string) ([]attachment.ImageInput, error) {
69 if len(images) == 0 {
70 return nil, nil
71 }
72 out := make([]attachment.ImageInput, 0, len(images))
73 var sources []attachment.Source
74 var slots []int
75 for _, image := range images {
76 switch provider.ClassifyImage(image) {
77 case provider.ImageHTTPURL:
78 out = append(out, attachment.ImageInput{Kind: attachment.KindURL, URL: image})
79 case provider.ImageFileID:
80 out = append(out, attachment.ImageInput{Kind: attachment.KindFiles, FilesID: image})
81 default:
82 slots = append(slots, len(out))
83 out = append(out, attachment.ImageInput{})
84 sources = append(sources, attachment.Source{DataURL: image})
85 }
86 }
87 if len(sources) == 0 {
88 return out, nil
89 }
90 svc := c.attachmentService()
91 prepared, err := svc.PrepareBatch(ctx, sources)
92 if err != nil {
93 return nil, err
94 }
95 refs, err := svc.CommitBatch(ctx, prepared)
96 if err != nil {
97 return nil, err
98 }
99 for i, ref := range refs {
100 item := ref
101 out[slots[i]] = attachment.ImageInput{Kind: attachment.KindAttachment, Attachment: &item}
102 }
103 return out, nil
104 }
105
106 func (c *Controller) ResolveRequestImages(ctx context.Context, msgs []provider.Message) ([]provider.Message, error) {
107 if c == nil {
108 return msgs, nil
109 }
110 return c.ResolveRequestImagesForModel(ctx, msgs, c.selection.ref, c.imageInputEnabled())
111 }
112
113 // ImageRequestRoute is request-local and must never be persisted in history.
114 type ImageRequestRoute struct {
115 Model string
116 BaseURL string
117 APIKey string
118 AuthHeader bool
119 Protocol string
120 }
121
122 func (c *Controller) captureImageRoutes(cfg *config.Config) {
123 c.imageRoutes = make(map[string]ImageRequestRoute)
124 for _, entry := range cfg.Providers {
125 protocol := "openai"
126 if strings.EqualFold(entry.Kind, "anthropic") {
127 protocol = "anthropic"
128 }
129 route := ImageRequestRoute{BaseURL: entry.BaseURL, APIKey: entry.APIKey(), AuthHeader: entry.AuthHeader, Protocol: protocol}
130 c.imageRoutes[entry.Name] = route
131 }
132 if prefix, _, ok := strings.Cut(cfg.DefaultModel, "/"); ok {
133 c.imageRoutes[""] = c.imageRoutes[prefix]
134 }
135 c.imageRoutesReady = true
136 }
137
138 func (c *Controller) imageRequestRoute(model string) (ImageRequestRoute, error) {
139 c.imageRoutesMu.Lock()
140 defer c.imageRoutesMu.Unlock()
141 if !c.imageRoutesReady {
142 cfg, err := config.LoadForRootReadOnly(c.workspaceRoot)
143 if err != nil {
144 // Failed reads are not a usable snapshot. A corrected configuration
145 // must take effect on the next attempt without rebuilding the task.
146 return ImageRequestRoute{}, err
147 }
148 c.captureImageRoutes(cfg)
149 }
150 prefix, _, _ := strings.Cut(model, "/")
151 route := c.imageRoutes[prefix]
152 route.Model = model
153 return route, nil
154 }
155
156 func (c *Controller) ResolveRequestImagesForModel(ctx context.Context, msgs []provider.Message, model string, native bool) ([]provider.Message, error) {
157 if c == nil {
158 return msgs, nil
159 }
160 route, err := c.imageRequestRoute(model)
161 if err != nil {
162 return nil, err
163 }
164 out := append([]provider.Message(nil), msgs...)
165 currentTurn := lastImageRequestTurn(msgs)
166 for i := range out {
167 if out[i].LocalOnly {
168 continue
169 }
170 if err := out[i].ValidateImageFields(); err != nil {
171 if i >= currentTurn {
172 return nil, fmt.Errorf("%s: %w", i18n.M.ImageRequestRecovery, err)
173 }
174 if len(out[i].Images) > 0 && len(out[i].ImageInputs) > 0 {
175 positions := make([]int, len(out[i].Images)+len(out[i].ImageInputs))
176 for j := range positions {
177 positions[j] = j + 1
178 }
179 noteUnavailableImages(&out[i], positions)
180 out[i].Images, out[i].ImageInputs = nil, nil
181 continue
182 }
183 // Per-input validation below preserves healthy historical siblings.
184 }
185 if len(out[i].ImageInputs) == 0 {
186 continue
187 }
188 if !native {
189 if out[i].VisionSummary == nil {
190 svc := imageinput.New(imageinput.Config{Model: c.visionModel, Resolve: c.visionProviderResolver, Select: c.visionModelSelector})
191 if c.executor != nil && c.executor.ImageInput() != nil {
192 svc = c.executor.ImageInput()
193 }
194 target, err := svc.SelectModel(model, nil)
195 if err != nil {
196 return nil, imageRequestFailure(ctx, err)
197 }
198 visionRoute, err := c.imageRequestRoute(target)
199 if err != nil {
200 return nil, imageRequestFailure(ctx, err)
201 }
202 images, missing, err := c.resolveReplayImages(ctx, out[i].ImageInputs, visionRoute, i < currentTurn)
203 if err != nil {
204 return nil, imageRequestFailure(ctx, err)
205 }
206 noteUnavailableImages(&out[i], missing)
207 if len(images) == 0 {
208 out[i].ImageInputs = nil
209 continue
210 }
211 summary, err := svc.UnderstandSelected(ctx, target, images, nil, c.sink)
212 if err != nil {
213 return nil, imageRequestFailure(ctx, err)
214 }
215 out[i].Content = imageinput.AppendSummary(out[i].Content, summary)
216 }
217 out[i].ImageInputs = nil
218 continue
219 }
220 resolved, missing, err := c.resolveReplayImages(ctx, out[i].ImageInputs, route, i < currentTurn)
221 if err != nil {
222 return nil, imageRequestFailure(ctx, err)
223 }
224 noteUnavailableImages(&out[i], missing)
225 out[i].Images = resolved
226 out[i].ImageInputs = nil
227 }
228 return out, nil
229 }
230
231 func (c *Controller) resolveImageInputsForRoute(ctx context.Context, inputs []attachment.ImageInput, route ImageRequestRoute) ([]string, error) {
232 out := make([]string, 0, len(inputs))
233 for i, in := range inputs {
234 if err := ctx.Err(); err != nil {
235 return nil, err
236 }
237 if err := in.Validate(); err != nil {
238 return nil, err
239 }
240 switch in.Kind {
241 case attachment.KindURL:
242 out = append(out, in.URL)
243 case attachment.KindFiles:
244 out = append(out, in.FilesID)
245 case attachment.KindAttachment:
246 value, err := c.wireImageFromRefForRoute(ctx, *in.Attachment, route)
247 if err != nil {
248 var item attachment.Error
249 if errors.As(err, &item) {
250 item.Index = i + 1
251 var readErr imageReadError
252 if errors.As(err, &readErr) {
253 return nil, imageReadError{item}
254 }
255 return nil, item
256 }
257 return nil, err
258 }
259 out = append(out, value)
260 }
261 }
262 return out, nil
263 }
264
265 func (c *Controller) wireImageFromRefForRoute(ctx context.Context, ref attachment.AttachmentRef, route ImageRequestRoute) (string, error) {
266 svc := c.attachmentService()
267 variant, err := svc.PrepareVariant(ctx, ref, attachment.VariantPolicyV1)
268 if err != nil {
269 return "", imageReadError{err}
270 }
271 if len(variant.Bytes) <= inlineImageLimit {
272 return attachment.DataURL(variant.MIME, variant.Bytes), nil
273 }
274 id, err := uploadImageForRoute(ctx, route, ref.DisplayName, variant.Bytes)
275 if err == nil {
276 return id, nil
277 }
278 if ctx.Err() != nil {
279 return "", ctx.Err()
280 }
281 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
282 return "", err
283 }
284 if len(variant.Bytes) > provider.MaxInlineImageBytes {
285 return "", err
286 }
287 return attachment.DataURL(variant.MIME, variant.Bytes), nil
288 }
289
290 func uploadImageForRoute(ctx context.Context, route ImageRequestRoute, filename string, data []byte) (string, error) {
291 if err := ctx.Err(); err != nil {
292 return "", err
293 }
294 if !openai.IsDeepSeek(route.BaseURL) {
295 return "", errFilesAPI()
296 }
297 return uploadVisionFile(ctx, provider.FileUpload{
298 BaseURL: route.BaseURL,
299 APIKey: route.APIKey,
300 AuthHeader: route.AuthHeader,
301 Protocol: route.Protocol,
302 Filename: path.Base(filename),
303 Data: data,
304 })
305 }
306
307 func errFilesAPI() error { return fmt.Errorf("files api requires official DeepSeek") }
308
308 lines GO