Skip to content

Commit b6cae06

Browse files
authored
File (#264)
* up * 1 * test * up
1 parent 852b0f2 commit b6cae06

9 files changed

Lines changed: 839 additions & 54 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ tools/authenticator/.proxies.txt.swp
1313
/logs/
1414
/target/
1515
/bin/
16-
.gocache
16+
.gocache
17+
chatgpt2api

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,31 @@ curl --location 'http://你的服务器ip:8080/v1/chat/completions' \
5050
"stream": true
5151
}'
5252
```
53+
5354
### 支持codex的api
5455

56+
### 携带文件问答
57+
58+
```bash
59+
curl -X POST http://localhost:8080/v1/files \
60+
-H "Authorization: Bearer <你的key或access token>" \
61+
-F "purpose=assistants" \
62+
-F "file=@./test.pdf"
63+
然后带 file_id 问答:
64+
```
65+
66+
```bash
67+
{
68+
"model": "auto",
69+
"messages": [{
70+
"role": "user",
71+
"content": [
72+
{"type": "input_file", "file_id": "file-xxx"},
73+
{"type": "text", "text": "总结这个文件"}
74+
]
75+
}]
76+
}
77+
```
5578
### TTS 语音合成
5679

5780
```bash

conversion/requests/chatgpt/convert.go

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package chatgpt
22

33
import (
4+
backendchatgpt "aurora/internal/chatgpt"
45
"aurora/internal/tokens"
56
chatgpt_types "aurora/typings/chatgpt"
67
official_types "aurora/typings/official"
8+
"strings"
79
)
810

911
func ConvertAPIRequest(api_request official_types.APIRequest, secret *tokens.Secret, proxy string) chatgpt_types.ChatGPTRequest {
@@ -20,11 +22,16 @@ func ConvertAPIRequest(api_request official_types.APIRequest, secret *tokens.Sec
2022
chatgpt_request.PluginIDs = api_request.PluginIDs
2123
chatgpt_request.Model = "gpt-4-plugins"
2224
}
23-
for _, api_message := range api_request.Messages {
24-
if api_message.Role == "system" {
25-
api_message.Role = "critic"
25+
for _, apiMessage := range api_request.Messages {
26+
if apiMessage.Role == "system" {
27+
apiMessage.Role = "critic"
2628
}
27-
chatgpt_request.AddMessage(api_message.Role, api_message.Content)
29+
parts, metadata := buildMessageParts(apiMessage)
30+
if len(metadata) > 0 {
31+
chatgpt_request.AddMultimodalMessage(apiMessage.Role, parts, metadata)
32+
continue
33+
}
34+
chatgpt_request.AddMessage(apiMessage.Role, apiMessage.Text())
2835
}
2936
return chatgpt_request
3037
}
@@ -35,3 +42,138 @@ func ConvertTTSAPIRequest(input string) chatgpt_types.ChatGPTRequest {
3542
chatgpt_request.AddAssistantMessage(input)
3643
return chatgpt_request
3744
}
45+
46+
func buildMessageParts(message official_types.APIMessage) ([]interface{}, map[string]interface{}) {
47+
text := message.Text()
48+
files := enrichFiles(message.Files())
49+
if len(files) == 0 {
50+
return []interface{}{text}, nil
51+
}
52+
53+
parts := make([]interface{}, 0, len(files)+1)
54+
attachments := make([]interface{}, 0, len(files))
55+
for _, file := range files {
56+
fileID := fileID(file)
57+
if fileID == "" {
58+
continue
59+
}
60+
if isImageFile(file) {
61+
part := map[string]interface{}{
62+
"content_type": "image_asset_pointer",
63+
"asset_pointer": "file-service://" + fileID,
64+
}
65+
if file.Size > 0 {
66+
part["size_bytes"] = file.Size
67+
}
68+
if file.Width > 0 {
69+
part["width"] = file.Width
70+
}
71+
if file.Height > 0 {
72+
part["height"] = file.Height
73+
}
74+
parts = append(parts, part)
75+
}
76+
77+
attachment := map[string]interface{}{
78+
"id": fileID,
79+
"size": file.Size,
80+
"name": fileName(file),
81+
"mime_type": fileMime(file),
82+
"mimeType": fileMime(file),
83+
"source": "library",
84+
"is_big_paste": false,
85+
}
86+
if file.Width > 0 {
87+
attachment["width"] = file.Width
88+
}
89+
if file.Height > 0 {
90+
attachment["height"] = file.Height
91+
}
92+
if file.LibraryFileID != "" {
93+
attachment["library_file_id"] = file.LibraryFileID
94+
}
95+
attachments = append(attachments, attachment)
96+
}
97+
if text != "" {
98+
parts = append(parts, text)
99+
}
100+
if len(parts) == 0 {
101+
parts = append(parts, text)
102+
}
103+
return parts, map[string]interface{}{
104+
"attachments": attachments,
105+
"developer_mode_connector_ids": []interface{}{},
106+
"selected_sources": []interface{}{},
107+
"selected_github_repos": []interface{}{},
108+
"selected_all_github_repos": false,
109+
"serialization_metadata": map[string]interface{}{"custom_symbol_offsets": []interface{}{}},
110+
}
111+
}
112+
113+
func enrichFiles(files []official_types.FileAttachment) []official_types.FileAttachment {
114+
enriched := make([]official_types.FileAttachment, 0, len(files))
115+
seen := make(map[string]bool)
116+
for _, file := range files {
117+
id := fileID(file)
118+
if id == "" || seen[id] {
119+
continue
120+
}
121+
if uploaded, ok := backendchatgpt.LookupUploadedFile(id); ok {
122+
if file.ID == "" {
123+
file.ID = uploaded.ID
124+
}
125+
if file.FileID == "" {
126+
file.FileID = uploaded.FileID
127+
}
128+
if file.Name == "" && file.FileName == "" && file.Filename == "" {
129+
file.Name = uploaded.Filename
130+
file.FileName = uploaded.Filename
131+
file.Filename = uploaded.Filename
132+
}
133+
if file.MimeType == "" && file.MIMEType == "" {
134+
file.MimeType = uploaded.MimeType
135+
file.MIMEType = uploaded.MimeType
136+
}
137+
if file.Size == 0 {
138+
file.Size = uploaded.Bytes
139+
}
140+
if file.LibraryFileID == "" {
141+
file.LibraryFileID = uploaded.LibraryFileID
142+
}
143+
}
144+
seen[id] = true
145+
enriched = append(enriched, file)
146+
}
147+
return enriched
148+
}
149+
150+
func fileID(file official_types.FileAttachment) string {
151+
if strings.TrimSpace(file.FileID) != "" {
152+
return strings.TrimSpace(file.FileID)
153+
}
154+
return strings.TrimSpace(file.ID)
155+
}
156+
157+
func fileName(file official_types.FileAttachment) string {
158+
for _, value := range []string{file.Name, file.FileName, file.Filename} {
159+
if strings.TrimSpace(value) != "" {
160+
return strings.TrimSpace(value)
161+
}
162+
}
163+
return fileID(file)
164+
}
165+
166+
func fileMime(file official_types.FileAttachment) string {
167+
if strings.TrimSpace(file.MimeType) != "" {
168+
return strings.TrimSpace(file.MimeType)
169+
}
170+
return strings.TrimSpace(file.MIMEType)
171+
}
172+
173+
func isImageFile(file official_types.FileAttachment) bool {
174+
if strings.HasPrefix(strings.ToLower(fileMime(file)), "image/") {
175+
return true
176+
}
177+
name := strings.ToLower(fileName(file))
178+
return strings.HasSuffix(name, ".png") || strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".jpeg") || strings.HasSuffix(name, ".webp") || strings.HasSuffix(name, ".gif")
179+
}

initialize/handlers.go

Lines changed: 119 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"io"
1414
"os"
1515
"strings"
16+
"time"
1617

1718
"github.com/gin-gonic/gin"
1819
"github.com/google/uuid"
@@ -170,16 +171,18 @@ func (h *Handler) nightmare(c *gin.Context) {
170171
}})
171172
return
172173
}
173-
proxyUrl := h.proxy.GetProxyIP()
174-
input_tokens := util.CountToken(original_request.Messages[0].Content)
175-
secret := h.token.GetSecret()
176-
authHeader := c.GetHeader("Authorization")
177-
if authHeader != "" {
178-
customAccessToken := strings.Replace(authHeader, "Bearer ", "", 1)
179-
if strings.HasPrefix(customAccessToken, "eyJhbGciOiJSUzI1NiI") {
180-
secret = h.token.GenerateTempToken(customAccessToken)
181-
}
174+
if len(original_request.Messages) == 0 {
175+
c.JSON(400, gin.H{"error": gin.H{
176+
"message": "Missing required parameter: messages",
177+
"type": "invalid_request_error",
178+
"param": "messages",
179+
"code": "missing_required_parameter",
180+
}})
181+
return
182182
}
183+
proxyUrl := h.proxy.GetProxyIP()
184+
input_tokens := countMessagesTokens(original_request.Messages)
185+
secret := h.secretFromAuthorization(c.GetHeader("Authorization"), original_requestHasFiles(original_request), false)
183186
if secret == nil {
184187
c.JSON(400, gin.H{"error": "Not Account Found."})
185188
c.Abort()
@@ -288,16 +291,9 @@ func (h *Handler) responses(c *gin.Context) {
288291
proxyUrl := h.proxy.GetProxyIP()
289292
input_tokens := 0
290293
for _, message := range original_request.Messages {
291-
input_tokens += util.CountToken(message.Content)
292-
}
293-
secret := h.token.GetSecret()
294-
authHeader := c.GetHeader("Authorization")
295-
if authHeader != "" {
296-
customAccessToken := strings.Replace(authHeader, "Bearer ", "", 1)
297-
if strings.HasPrefix(customAccessToken, "eyJhbGciOiJSUzI1NiI") {
298-
secret = h.token.GenerateTempToken(customAccessToken)
299-
}
294+
input_tokens += util.CountToken(message.Text())
300295
}
296+
secret := h.secretFromAuthorization(c.GetHeader("Authorization"), original_requestHasFiles(original_request), false)
301297
if secret == nil {
302298
c.JSON(400, gin.H{"error": "Not Account Found."})
303299
c.Abort()
@@ -509,6 +505,77 @@ func (h *Handler) imageGenerations(c *gin.Context) {
509505
c.JSON(200, officialtypes.NewImageGenerationResponse(data))
510506
}
511507

508+
func (h *Handler) files(c *gin.Context) {
509+
secret := h.secretFromAuthorization(c.GetHeader("Authorization"), true, true)
510+
if secret == nil || secret.Token == "" || secret.IsFree {
511+
c.JSON(400, gin.H{"error": gin.H{
512+
"message": "Files API requires a logged-in ChatGPT access token.",
513+
"type": "invalid_request_error",
514+
"param": nil,
515+
"code": "missing_access_token",
516+
}})
517+
return
518+
}
519+
520+
formFile, err := c.FormFile("file")
521+
if err != nil {
522+
c.JSON(400, gin.H{"error": gin.H{
523+
"message": "Missing required multipart field: file",
524+
"type": "invalid_request_error",
525+
"param": "file",
526+
"code": "missing_required_parameter",
527+
}})
528+
return
529+
}
530+
file, err := formFile.Open()
531+
if err != nil {
532+
c.JSON(400, gin.H{"error": gin.H{
533+
"message": err.Error(),
534+
"type": "invalid_request_error",
535+
"param": "file",
536+
"code": "file_open_error",
537+
}})
538+
return
539+
}
540+
defer file.Close()
541+
data, err := io.ReadAll(file)
542+
if err != nil {
543+
c.JSON(400, gin.H{"error": gin.H{
544+
"message": err.Error(),
545+
"type": "invalid_request_error",
546+
"param": "file",
547+
"code": "file_read_error",
548+
}})
549+
return
550+
}
551+
if len(data) == 0 {
552+
c.JSON(400, gin.H{"error": gin.H{
553+
"message": "Uploaded file is empty",
554+
"type": "invalid_request_error",
555+
"param": "file",
556+
"code": "empty_file",
557+
}})
558+
return
559+
}
560+
561+
contentType := formFile.Header.Get("Content-Type")
562+
client := bogdanfinn.NewStdClient()
563+
client.SetCookies("https://chatgpt.com", chatgpt.BasicCookies)
564+
uploaded, status, err := chatgpt.UploadFile(client, secret, h.proxy.GetProxyIP(), formFile.Filename, contentType, c.PostForm("purpose"), data)
565+
if err != nil {
566+
c.JSON(status, gin.H{"error": gin.H{
567+
"message": err.Error(),
568+
"type": "file_upload_error",
569+
"param": "file",
570+
"code": "file_upload_error",
571+
}})
572+
return
573+
}
574+
uploaded.CreatedAt = time.Now().Unix()
575+
chatgpt.RegisterUploadedFile(uploaded)
576+
c.JSON(200, uploaded)
577+
}
578+
512579
func (h *Handler) engines(c *gin.Context) {
513580
type ResData struct {
514581
ID string `json:"id"`
@@ -539,6 +606,40 @@ func (h *Handler) engines(c *gin.Context) {
539606
})
540607
}
541608

609+
func (h *Handler) secretFromAuthorization(authHeader string, needsPaid bool, allowFallbackPaid bool) *tokens.Secret {
610+
secret := h.token.GetSecret()
611+
if needsPaid || allowFallbackPaid {
612+
secret = h.token.GetPaidSecret()
613+
}
614+
if authHeader != "" {
615+
customAccessToken := strings.TrimSpace(strings.Replace(authHeader, "Bearer ", "", 1))
616+
if strings.HasPrefix(customAccessToken, "eyJhbGciOiJSUzI1NiI") {
617+
secret = h.token.GenerateTempToken(customAccessToken)
618+
}
619+
}
620+
if needsPaid && (secret == nil || secret.Token == "" || secret.IsFree) && !allowFallbackPaid {
621+
return nil
622+
}
623+
return secret
624+
}
625+
626+
func countMessagesTokens(messages []officialtypes.APIMessage) int {
627+
total := 0
628+
for _, message := range messages {
629+
total += util.CountToken(message.Text())
630+
}
631+
return total
632+
}
633+
634+
func original_requestHasFiles(request officialtypes.APIRequest) bool {
635+
for _, message := range request.Messages {
636+
if len(message.Files()) > 0 {
637+
return true
638+
}
639+
}
640+
return false
641+
}
642+
542643
var ttsFmtMap = map[string]string{
543644
"mp3": "mp3",
544645
"opus": "opus",

initialize/router.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ func RegisterRouter() *gin.Engine {
3535
router.OPTIONS("/v1/chat/completions", optionsHandler)
3636
router.OPTIONS("/v1/responses", optionsHandler)
3737
router.OPTIONS("/v1/images/generations", optionsHandler)
38+
router.OPTIONS("/v1/files", optionsHandler)
3839

3940
authGroup := router.Group("").Use(middlewares.Authorization)
4041
authGroup.POST("/v1/chat/completions", handler.nightmare)
4142
authGroup.POST("/v1/responses", handler.responses)
43+
authGroup.POST("/v1/files", handler.files)
4244
authGroup.GET("/v1/models", handler.engines)
4345
authGroup.POST("/backend-api/conversation", handler.chatgptConversation)
4446
authGroup.POST("/v1/images/generations", handler.imageGenerations)

0 commit comments

Comments
 (0)