返回 DeepSeek-Reasonix
image.go
根目录 / internal / provider / image.go
1 package provider
2
3 import (
4 "errors"
5 "net/url"
6 "strings"
7 "unicode/utf8"
8
9 "reasonix/internal/attachment"
10 )
11
12 const (
13 // MaxInlineImageBytes is the official DeepSeek base64 / URL image cap (32 MiB).
14 MaxInlineImageBytes = 32 << 20
15 // MaxFileAPIImageBytes is the official DeepSeek Files API image cap (64 MiB).
16 MaxFileAPIImageBytes = 64 << 20
17 // MaxImageURLRunes is the official DeepSeek external image URL length cap.
18 MaxImageURLRunes = 8192
19 )
20
21 // ImageKind classifies a Message.Images entry for vision serializers.
22 type ImageKind int
23
24 const (
25 ImageNone ImageKind = iota
26 ImageDataURL
27 ImageHTTPURL
28 ImageFileID
29 )
30
31 // ClassifyImage reports how a stored image reference should appear on the wire.
32 // Older sessions only stored data URLs; HTTP URLs and Files API ids are additive.
33 func ClassifyImage(s string) ImageKind {
34 s = strings.TrimSpace(s)
35 if s == "" {
36 return ImageNone
37 }
38 if _, _, ok := ParseImageDataURL(s); ok {
39 return ImageDataURL
40 }
41 if IsImageFileID(s) {
42 return ImageFileID
43 }
44 if IsImageHTTPURL(s) {
45 return ImageHTTPURL
46 }
47 return ImageNone
48 }
49
50 // IsImageFileID reports a DeepSeek Files API id (file-api-…).
51 func IsImageFileID(s string) bool {
52 s = strings.TrimSpace(s)
53 rest, ok := strings.CutPrefix(s, "file-api-")
54 if !ok || rest == "" || len(rest) > 128 {
55 return false
56 }
57 for _, r := range rest {
58 if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-' {
59 continue
60 }
61 return false
62 }
63 return true
64 }
65
66 // IsImageHTTPURL reports a public http(s) image URL that official DeepSeek
67 // can fetch (extension-gated so arbitrary links are not treated as images).
68 func IsImageHTTPURL(s string) bool {
69 s = strings.TrimSpace(s)
70 if s == "" || utf8.RuneCountInString(s) > MaxImageURLRunes {
71 return false
72 }
73 u, err := url.Parse(s)
74 if err != nil || u.User != nil || u.Host == "" {
75 return false
76 }
77 switch strings.ToLower(u.Scheme) {
78 case "http", "https":
79 default:
80 return false
81 }
82 path := u.EscapedPath()
83 if i := strings.LastIndex(path, "."); i >= 0 {
84 path = path[i:]
85 } else {
86 return false
87 }
88 switch strings.ToLower(path) {
89 case ".jpg", ".jpeg", ".png", ".gif", ".webp":
90 return true
91 default:
92 return false
93 }
94 }
95
96 func (m Message) HasImagePayload() bool {
97 return len(m.Images) > 0 || len(m.ImageInputs) > 0
98 }
99
100 func (m Message) ValidateImageFields() error {
101 if len(m.Images) > 0 && len(m.ImageInputs) > 0 {
102 return errors.New("message must not combine images and image_inputs")
103 }
104 for _, in := range m.ImageInputs {
105 if err := in.Validate(); err != nil {
106 return err
107 }
108 }
109 return nil
110 }
111
112 func CloneImageInputs(in []attachment.ImageInput) []attachment.ImageInput {
113 return attachment.CloneImageInputs(in)
114 }
115
115 lines GO