返回 DeepSeek-Reasonix
vision_images.go
根目录 / internal / control / vision_images.go
1 package control
2
3 import (
4 "context"
5 "encoding/base64"
6 "fmt"
7 "path"
8 "path/filepath"
9 "strings"
10 "time"
11
12 "reasonix/internal/config"
13 "reasonix/internal/provider"
14 "reasonix/internal/provider/openai"
15 )
16
17 var (
18 inlineImageLimit = provider.MaxInlineImageBytes
19 uploadVisionFile = provider.UploadUserDataFile
20 )
21
22 func classifyVisionToken(tok string) (ref, bool) {
23 tok = strings.TrimSpace(tok)
24 if provider.IsImageHTTPURL(tok) {
25 return ref{kind: refRemoteImage, path: tok, raw: tok}, true
26 }
27 if provider.IsImageFileID(tok) {
28 return ref{kind: refFileID, path: tok, raw: tok}, true
29 }
30 return ref{}, false
31 }
32
33 func bareVisionRefs(line string) []ref {
34 var refs []ref
35 seen := map[string]bool{}
36 for tok := range strings.FieldsSeq(line) {
37 tok = strings.Trim(tok, `<>"'`)
38 tok = strings.TrimRight(tok, ".,;!?)]}")
39 if seen[tok] {
40 continue
41 }
42 r, ok := classifyVisionToken(tok)
43 if !ok {
44 continue
45 }
46 seen[tok] = true
47 refs = append(refs, r)
48 }
49 return refs
50 }
51
52 func (c *Controller) visionRefImageValue(r ref, baseDir string) (string, error) {
53 switch r.kind {
54 case refRemoteImage, refFileID:
55 return r.path, nil
56 case refImage, refFile:
57 return c.visionLocalImageValue(r.path, baseDir)
58 default:
59 return "", fmt.Errorf("reference is not an image")
60 }
61 }
62
63 func imageAttachmentNote(path string, vision bool) string {
64 if vision {
65 return "[The image at @" + path + " is attached as visual input. Look at the image directly; do not OCR or read the file unless the user asks.]"
66 }
67 return "[image attachment available at @" + path + "; sent as direct model image input only when the selected model supports vision. Text-only models can still use an available OCR/image/vision tool with this local path; image bytes are not inlined into prompt text.]"
68 }
69
70 func imageFileRefNote(displayPath, mime string, size int64, attached, vision bool) string {
71 if attached && vision {
72 return fmt.Sprintf("[image file %s, mime=%s, %d bytes — attached as visual input. Look at the image directly; do not OCR or read the file unless the user asks.]", displayPath, mime, size)
73 }
74 if attached {
75 return fmt.Sprintf("[image file %s, mime=%s, %d bytes — sent as direct model image input only when the selected model supports vision. Text-only models can still use an available OCR/image/vision tool with this local path; image bytes are not inlined into prompt text.]", displayPath, mime, size)
76 }
77 return fmt.Sprintf("[image file %s, mime=%s, %d bytes — not sent as direct model image input because no workspace root is available. Use a workspace-scoped file reference, image attachment, or an available OCR/image/vision tool with a readable local path.]", displayPath, mime, size)
78 }
79
80 func (c *Controller) visionLocalImageValue(pathName, baseDir string) (string, error) {
81 var (
82 dataURL string
83 err error
84 )
85 if isAttachmentRef(filepath.ToSlash(pathName)) {
86 dataURL, err = visionImageDataURLInRoot(baseDir, pathName)
87 } else {
88 dataURL, err = visionFileImageDataURL(pathName, baseDir)
89 }
90 if err != nil {
91 return "", err
92 }
93 return c.maybeUploadDataURL(pathName, dataURL)
94 }
95
96 func (c *Controller) maybeUploadDataURL(filename, dataURL string) (string, error) {
97 _, payload, ok := provider.ParseImageDataURL(dataURL)
98 if !ok {
99 return dataURL, nil
100 }
101 raw, err := base64.StdEncoding.DecodeString(payload)
102 if err != nil || len(raw) <= inlineImageLimit {
103 return dataURL, nil
104 }
105 id, err := c.uploadOfficialVisionFile(filename, raw)
106 if err == nil {
107 return id, nil
108 }
109 if len(raw) > provider.MaxInlineImageBytes {
110 return "", err
111 }
112 return dataURL, nil
113 }
114
115 func (c *Controller) uploadOfficialVisionFile(filename string, data []byte) (string, error) {
116 if c == nil {
117 return "", fmt.Errorf("files api: no session")
118 }
119 cfg, err := config.LoadForRoot(c.workspaceRoot)
120 if err != nil {
121 return "", err
122 }
123 ref := c.selection.ref
124 if ref == "" {
125 ref = cfg.DefaultModel
126 }
127 entry, ok := cfg.ResolveModel(ref)
128 if !ok || !openai.IsDeepSeek(entry.BaseURL) {
129 return "", fmt.Errorf("files api requires official DeepSeek")
130 }
131 protocol := "openai"
132 if strings.EqualFold(entry.Kind, "anthropic") {
133 protocol = "anthropic"
134 }
135 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
136 defer cancel()
137 return uploadVisionFile(ctx, provider.FileUpload{
138 BaseURL: entry.BaseURL,
139 APIKey: entry.APIKey(),
140 AuthHeader: entry.AuthHeader,
141 Protocol: protocol,
142 Filename: path.Base(filename),
143 Data: data,
144 })
145 }
146
146 lines GO