返回 DeepSeek-Reasonix
files.go
根目录 / internal / provider / files.go
1 package provider
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "mime/multipart"
10 "net/http"
11 "net/textproto"
12 "path"
13 "strings"
14 "time"
15 "unicode/utf8"
16 )
17
18 const (
19 anthropicFilesBeta = "files-api-2025-04-14"
20 maxUploadFilename = 512
21 )
22
23 // FileUpload is a Files API image upload (purpose=user_data).
24 type FileUpload struct {
25 BaseURL string
26 APIKey string
27 AuthHeader bool
28 Protocol string // "anthropic" uses the Anthropic-compatible Files API
29 Filename string
30 Data []byte
31 Client *http.Client
32 }
33
34 type openaiFileObject struct {
35 ID string `json:"id"`
36 }
37
38 type anthropicFileObject struct {
39 ID string `json:"id"`
40 }
41
42 // UploadUserDataFile uploads an image and returns its file_id (file-api-…).
43 func UploadUserDataFile(ctx context.Context, u FileUpload) (string, error) {
44 if len(u.Data) == 0 || len(u.Data) > MaxFileAPIImageBytes {
45 return "", fmt.Errorf("files api image must be between 1 byte and 64 MiB")
46 }
47 if strings.TrimSpace(u.APIKey) == "" {
48 return "", fmt.Errorf("files api: missing api key")
49 }
50 filename := sanitizeUploadFilename(u.Filename)
51 endpoint, err := filesEndpoint(u.BaseURL, u.Protocol)
52 if err != nil {
53 return "", err
54 }
55 body := &bytes.Buffer{}
56 writer := multipart.NewWriter(body)
57 if err := writer.WriteField("purpose", "user_data"); err != nil {
58 return "", err
59 }
60 hdr := make(textproto.MIMEHeader)
61 hdr.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, escapeQuotes(filename)))
62 hdr.Set("Content-Type", "application/octet-stream")
63 part, err := writer.CreatePart(hdr)
64 if err != nil {
65 return "", err
66 }
67 if _, err := part.Write(u.Data); err != nil {
68 return "", err
69 }
70 if err := writer.Close(); err != nil {
71 return "", err
72 }
73 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
74 if err != nil {
75 return "", err
76 }
77 req.Header.Set("Content-Type", writer.FormDataContentType())
78 anthropic := strings.EqualFold(strings.TrimSpace(u.Protocol), "anthropic")
79 if anthropic {
80 if u.AuthHeader {
81 req.Header.Set("Authorization", "Bearer "+u.APIKey)
82 } else {
83 req.Header.Set("x-api-key", u.APIKey)
84 }
85 req.Header.Set("anthropic-beta", anthropicFilesBeta)
86 } else {
87 req.Header.Set("Authorization", "Bearer "+u.APIKey)
88 }
89 client := u.Client
90 if client == nil {
91 client = &http.Client{Timeout: 2 * time.Minute}
92 }
93 resp, err := client.Do(req)
94 if err != nil {
95 return "", fmt.Errorf("files api: %w", err)
96 }
97 defer resp.Body.Close()
98 raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
99 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
100 return "", fmt.Errorf("files api: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
101 }
102 id := parseUploadedFileID(raw, anthropic)
103 if !IsImageFileID(id) {
104 return "", fmt.Errorf("files api: missing file_id")
105 }
106 return id, nil
107 }
108
109 func parseUploadedFileID(raw []byte, anthropic bool) string {
110 if anthropic {
111 var obj anthropicFileObject
112 if json.Unmarshal(raw, &obj) == nil {
113 return strings.TrimSpace(obj.ID)
114 }
115 }
116 var obj openaiFileObject
117 if json.Unmarshal(raw, &obj) == nil {
118 return strings.TrimSpace(obj.ID)
119 }
120 return ""
121 }
122
123 func filesEndpoint(baseURL, protocol string) (string, error) {
124 base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
125 if base == "" {
126 return "", fmt.Errorf("files api: empty base url")
127 }
128 base = strings.TrimSuffix(base, "/v1")
129 if strings.EqualFold(strings.TrimSpace(protocol), "anthropic") {
130 return base + "/v1/files", nil
131 }
132 return base + "/files", nil
133 }
134
135 func sanitizeUploadFilename(name string) string {
136 name = path.Base(strings.ReplaceAll(strings.TrimSpace(name), "\\", "/"))
137 if name == "" || name == "." || name == "/" {
138 name = "image.png"
139 }
140 if utf8.RuneCountInString(name) > maxUploadFilename {
141 runes := []rune(name)
142 name = string(runes[:maxUploadFilename])
143 }
144 return name
145 }
146
147 func escapeQuotes(s string) string {
148 return strings.ReplaceAll(s, `"`, `\"`)
149 }
150
150 lines GO