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