Skip to content
Merged

File #264

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ tools/authenticator/.proxies.txt.swp
/logs/
/target/
/bin/
.gocache
.gocache
chatgpt2api
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,31 @@ curl --location 'http://你的服务器ip:8080/v1/chat/completions' \
"stream": true
}'
```

### 支持codex的api

### 携带文件问答

```bash
curl -X POST http://localhost:8080/v1/files \
-H "Authorization: Bearer <你的key或access token>" \
-F "purpose=assistants" \
-F "file=@./test.pdf"
然后带 file_id 问答:
```

Comment on lines +58 to +65
```bash
{
"model": "auto",
"messages": [{
"role": "user",
"content": [
{"type": "input_file", "file_id": "file-xxx"},
{"type": "text", "text": "总结这个文件"}
]
}]
}
```
### TTS 语音合成

```bash
Expand Down
150 changes: 146 additions & 4 deletions conversion/requests/chatgpt/convert.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package chatgpt

import (
backendchatgpt "aurora/internal/chatgpt"
"aurora/internal/tokens"
chatgpt_types "aurora/typings/chatgpt"
official_types "aurora/typings/official"
"strings"
)

func ConvertAPIRequest(api_request official_types.APIRequest, secret *tokens.Secret, proxy string) chatgpt_types.ChatGPTRequest {
Expand All @@ -20,11 +22,16 @@ func ConvertAPIRequest(api_request official_types.APIRequest, secret *tokens.Sec
chatgpt_request.PluginIDs = api_request.PluginIDs
chatgpt_request.Model = "gpt-4-plugins"
}
for _, api_message := range api_request.Messages {
if api_message.Role == "system" {
api_message.Role = "critic"
for _, apiMessage := range api_request.Messages {
if apiMessage.Role == "system" {
apiMessage.Role = "critic"
}
chatgpt_request.AddMessage(api_message.Role, api_message.Content)
parts, metadata := buildMessageParts(apiMessage)
if len(metadata) > 0 {
chatgpt_request.AddMultimodalMessage(apiMessage.Role, parts, metadata)
continue
}
chatgpt_request.AddMessage(apiMessage.Role, apiMessage.Text())
}
return chatgpt_request
}
Expand All @@ -35,3 +42,138 @@ func ConvertTTSAPIRequest(input string) chatgpt_types.ChatGPTRequest {
chatgpt_request.AddAssistantMessage(input)
return chatgpt_request
}

func buildMessageParts(message official_types.APIMessage) ([]interface{}, map[string]interface{}) {
text := message.Text()
files := enrichFiles(message.Files())
if len(files) == 0 {
return []interface{}{text}, nil
}

parts := make([]interface{}, 0, len(files)+1)
attachments := make([]interface{}, 0, len(files))
for _, file := range files {
fileID := fileID(file)
if fileID == "" {
continue
}
if isImageFile(file) {
part := map[string]interface{}{
"content_type": "image_asset_pointer",
"asset_pointer": "file-service://" + fileID,
}
if file.Size > 0 {
part["size_bytes"] = file.Size
}
if file.Width > 0 {
part["width"] = file.Width
}
if file.Height > 0 {
part["height"] = file.Height
}
parts = append(parts, part)
}

attachment := map[string]interface{}{
"id": fileID,
"size": file.Size,
"name": fileName(file),
"mime_type": fileMime(file),
"mimeType": fileMime(file),
"source": "library",
"is_big_paste": false,
}
if file.Width > 0 {
attachment["width"] = file.Width
}
if file.Height > 0 {
attachment["height"] = file.Height
}
if file.LibraryFileID != "" {
attachment["library_file_id"] = file.LibraryFileID
}
attachments = append(attachments, attachment)
}
if text != "" {
parts = append(parts, text)
}
if len(parts) == 0 {
parts = append(parts, text)
}
return parts, map[string]interface{}{
"attachments": attachments,
"developer_mode_connector_ids": []interface{}{},
"selected_sources": []interface{}{},
"selected_github_repos": []interface{}{},
"selected_all_github_repos": false,
"serialization_metadata": map[string]interface{}{"custom_symbol_offsets": []interface{}{}},
}
}

func enrichFiles(files []official_types.FileAttachment) []official_types.FileAttachment {
enriched := make([]official_types.FileAttachment, 0, len(files))
seen := make(map[string]bool)
for _, file := range files {
id := fileID(file)
if id == "" || seen[id] {
continue
}
if uploaded, ok := backendchatgpt.LookupUploadedFile(id); ok {
if file.ID == "" {
file.ID = uploaded.ID
}
if file.FileID == "" {
file.FileID = uploaded.FileID
}
if file.Name == "" && file.FileName == "" && file.Filename == "" {
file.Name = uploaded.Filename
file.FileName = uploaded.Filename
file.Filename = uploaded.Filename
}
if file.MimeType == "" && file.MIMEType == "" {
file.MimeType = uploaded.MimeType
file.MIMEType = uploaded.MimeType
}
if file.Size == 0 {
file.Size = uploaded.Bytes
}
if file.LibraryFileID == "" {
file.LibraryFileID = uploaded.LibraryFileID
}
}
seen[id] = true
enriched = append(enriched, file)
}
return enriched
}

func fileID(file official_types.FileAttachment) string {
if strings.TrimSpace(file.FileID) != "" {
return strings.TrimSpace(file.FileID)
}
return strings.TrimSpace(file.ID)
}

func fileName(file official_types.FileAttachment) string {
for _, value := range []string{file.Name, file.FileName, file.Filename} {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return fileID(file)
}

func fileMime(file official_types.FileAttachment) string {
if strings.TrimSpace(file.MimeType) != "" {
return strings.TrimSpace(file.MimeType)
}
return strings.TrimSpace(file.MIMEType)
}

func isImageFile(file official_types.FileAttachment) bool {
if strings.HasPrefix(strings.ToLower(fileMime(file)), "image/") {
return true
}
name := strings.ToLower(fileName(file))
return strings.HasSuffix(name, ".png") || strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".jpeg") || strings.HasSuffix(name, ".webp") || strings.HasSuffix(name, ".gif")
}
137 changes: 119 additions & 18 deletions initialize/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"io"
"os"
"strings"
"time"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
Expand Down Expand Up @@ -170,16 +171,18 @@ func (h *Handler) nightmare(c *gin.Context) {
}})
return
}
proxyUrl := h.proxy.GetProxyIP()
input_tokens := util.CountToken(original_request.Messages[0].Content)
secret := h.token.GetSecret()
authHeader := c.GetHeader("Authorization")
if authHeader != "" {
customAccessToken := strings.Replace(authHeader, "Bearer ", "", 1)
if strings.HasPrefix(customAccessToken, "eyJhbGciOiJSUzI1NiI") {
secret = h.token.GenerateTempToken(customAccessToken)
}
if len(original_request.Messages) == 0 {
c.JSON(400, gin.H{"error": gin.H{
"message": "Missing required parameter: messages",
"type": "invalid_request_error",
"param": "messages",
"code": "missing_required_parameter",
}})
return
}
proxyUrl := h.proxy.GetProxyIP()
input_tokens := countMessagesTokens(original_request.Messages)
secret := h.secretFromAuthorization(c.GetHeader("Authorization"), original_requestHasFiles(original_request), false)
if secret == nil {
c.JSON(400, gin.H{"error": "Not Account Found."})
c.Abort()
Expand Down Expand Up @@ -288,16 +291,9 @@ func (h *Handler) responses(c *gin.Context) {
proxyUrl := h.proxy.GetProxyIP()
input_tokens := 0
for _, message := range original_request.Messages {
input_tokens += util.CountToken(message.Content)
}
secret := h.token.GetSecret()
authHeader := c.GetHeader("Authorization")
if authHeader != "" {
customAccessToken := strings.Replace(authHeader, "Bearer ", "", 1)
if strings.HasPrefix(customAccessToken, "eyJhbGciOiJSUzI1NiI") {
secret = h.token.GenerateTempToken(customAccessToken)
}
input_tokens += util.CountToken(message.Text())
}
secret := h.secretFromAuthorization(c.GetHeader("Authorization"), original_requestHasFiles(original_request), false)
if secret == nil {
c.JSON(400, gin.H{"error": "Not Account Found."})
c.Abort()
Expand Down Expand Up @@ -509,6 +505,77 @@ func (h *Handler) imageGenerations(c *gin.Context) {
c.JSON(200, officialtypes.NewImageGenerationResponse(data))
}

func (h *Handler) files(c *gin.Context) {
secret := h.secretFromAuthorization(c.GetHeader("Authorization"), true, true)
if secret == nil || secret.Token == "" || secret.IsFree {
c.JSON(400, gin.H{"error": gin.H{
"message": "Files API requires a logged-in ChatGPT access token.",
"type": "invalid_request_error",
"param": nil,
"code": "missing_access_token",
}})
return
}

formFile, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": gin.H{
"message": "Missing required multipart field: file",
"type": "invalid_request_error",
"param": "file",
"code": "missing_required_parameter",
}})
return
}
file, err := formFile.Open()
if err != nil {
c.JSON(400, gin.H{"error": gin.H{
"message": err.Error(),
"type": "invalid_request_error",
"param": "file",
"code": "file_open_error",
}})
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
c.JSON(400, gin.H{"error": gin.H{
"message": err.Error(),
"type": "invalid_request_error",
"param": "file",
"code": "file_read_error",
}})
return
}
if len(data) == 0 {
c.JSON(400, gin.H{"error": gin.H{
"message": "Uploaded file is empty",
"type": "invalid_request_error",
"param": "file",
"code": "empty_file",
}})
return
}
Comment on lines +541 to +559

contentType := formFile.Header.Get("Content-Type")
client := bogdanfinn.NewStdClient()
client.SetCookies("https://chatgpt.com", chatgpt.BasicCookies)
uploaded, status, err := chatgpt.UploadFile(client, secret, h.proxy.GetProxyIP(), formFile.Filename, contentType, c.PostForm("purpose"), data)
if err != nil {
c.JSON(status, gin.H{"error": gin.H{
"message": err.Error(),
"type": "file_upload_error",
"param": "file",
"code": "file_upload_error",
}})
return
}
uploaded.CreatedAt = time.Now().Unix()
chatgpt.RegisterUploadedFile(uploaded)
c.JSON(200, uploaded)
}

func (h *Handler) engines(c *gin.Context) {
type ResData struct {
ID string `json:"id"`
Expand Down Expand Up @@ -539,6 +606,40 @@ func (h *Handler) engines(c *gin.Context) {
})
}

func (h *Handler) secretFromAuthorization(authHeader string, needsPaid bool, allowFallbackPaid bool) *tokens.Secret {
secret := h.token.GetSecret()
if needsPaid || allowFallbackPaid {
secret = h.token.GetPaidSecret()
}
if authHeader != "" {
customAccessToken := strings.TrimSpace(strings.Replace(authHeader, "Bearer ", "", 1))
if strings.HasPrefix(customAccessToken, "eyJhbGciOiJSUzI1NiI") {
secret = h.token.GenerateTempToken(customAccessToken)
}
}
if needsPaid && (secret == nil || secret.Token == "" || secret.IsFree) && !allowFallbackPaid {
return nil
}
return secret
}

func countMessagesTokens(messages []officialtypes.APIMessage) int {
total := 0
for _, message := range messages {
total += util.CountToken(message.Text())
}
return total
}

func original_requestHasFiles(request officialtypes.APIRequest) bool {
for _, message := range request.Messages {
if len(message.Files()) > 0 {
return true
}
}
return false
}

var ttsFmtMap = map[string]string{
"mp3": "mp3",
"opus": "opus",
Expand Down
2 changes: 2 additions & 0 deletions initialize/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ func RegisterRouter() *gin.Engine {
router.OPTIONS("/v1/chat/completions", optionsHandler)
router.OPTIONS("/v1/responses", optionsHandler)
router.OPTIONS("/v1/images/generations", optionsHandler)
router.OPTIONS("/v1/files", optionsHandler)

authGroup := router.Group("").Use(middlewares.Authorization)
authGroup.POST("/v1/chat/completions", handler.nightmare)
authGroup.POST("/v1/responses", handler.responses)
authGroup.POST("/v1/files", handler.files)
authGroup.GET("/v1/models", handler.engines)
authGroup.POST("/backend-api/conversation", handler.chatgptConversation)
authGroup.POST("/v1/images/generations", handler.imageGenerations)
Expand Down
Loading
Loading