| package handler |
|
|
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "net/http" |
| "net/url" |
| "os" |
| "strings" |
| "time" |
|
|
| "aurora/httpclient/bogdanfinn" |
| "aurora/internal/accounts" |
| "aurora/internal/chatgpt" |
| "aurora/internal/config" |
| chatgpt_types "aurora/typings/chatgpt" |
| officialtypes "aurora/typings/official" |
| "aurora/util" |
|
|
| fhttp "github.com/bogdanfinn/fhttp" |
| "github.com/bogdanfinn/websocket" |
| "github.com/gin-gonic/gin" |
| "github.com/google/uuid" |
| ) |
|
|
| var ErrNoAvailable = errors.New("no available account of the requested type") |
|
|
| func respondError(c *gin.Context, status int, err error) { |
| c.JSON(status, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "invalid_request_error", |
| "param": nil, |
| "code": http.StatusText(status), |
| }}) |
| } |
|
|
| |
| |
| |
| func resolveAccount(c *gin.Context, pool *accounts.Pool, cfg *config.Config, needsPaid bool) (*accounts.Account, int, error) { |
| authHeader := c.GetHeader("Authorization") |
|
|
| |
| payload := strings.TrimSpace(authHeader) |
| if len(payload) >= 7 && strings.EqualFold(payload[:7], "Bearer ") { |
| payload = strings.TrimSpace(payload[7:]) |
| } |
| parts := strings.SplitN(payload, ",", 2) |
| token := strings.TrimSpace(parts[0]) |
| teamAccountID := "" |
| if len(parts) > 1 { |
| teamAccountID = strings.TrimSpace(parts[1]) |
| } |
|
|
| |
| for _, header := range []string{"ChatGPT-Account-ID", "Chatgpt-Account-Id", "Team-Account-ID", "X-ChatGPT-Account-ID"} { |
| if value := strings.TrimSpace(c.GetHeader(header)); value != "" { |
| teamAccountID = value |
| break |
| } |
| } |
|
|
| expected := cfg.Authorization |
|
|
| |
| if token == "" || (expected != "" && token == expected) { |
| acct, err := pool.Acquire(accounts.TypeFree) |
| if err != nil || acct == nil { |
| |
| acct, err = pool.Acquire(accounts.TypeNoAuth) |
| } |
| if err != nil || acct == nil { |
| return nil, http.StatusUnauthorized, ErrNoAvailable |
| } |
| if needsPaid && acct.Type == accounts.TypeNoAuth { |
| return nil, http.StatusForbidden, errors.New("this endpoint requires a logged-in ChatGPT account") |
| } |
| return acct, http.StatusOK, nil |
| } |
|
|
| |
| if strings.HasPrefix(token, "eyJ") { |
| if !cfg.EnableExternalToken { |
| return nil, http.StatusUnauthorized, errors.New("external access token disabled (set ENABLE_EXTERNAL_TOKEN=true)") |
| } |
| userAgent := c.GetHeader("User-Agent") |
| proxyURL := cfg.ProxyURL |
| if proxyURL == "" { |
| proxyURL = cfg.HTTPProxy |
| } |
| acct := pool.GetOrCreateTempAccount(token, userAgent, proxyURL) |
| acct.TeamUserID = teamAccountID |
| return acct, http.StatusOK, nil |
| } |
|
|
| |
| if _, err := uuid.Parse(token); err == nil { |
| if needsPaid { |
| return nil, http.StatusForbidden, errors.New("this endpoint requires a paid ChatGPT account") |
| } |
| acct := accounts.NewAccount(token, accounts.TypeNoAuth, token) |
| if err := acct.InitClient(); err != nil { |
| return nil, http.StatusInternalServerError, err |
| } |
| acct.Status = accounts.StatusActive |
| return acct, http.StatusOK, nil |
| } |
|
|
| |
| if teamAccountID != "" || len(token) > 64 { |
| client := bogdanfinn.NewStdClient() |
| result, status, err := chatgpt.GETTokenForRefreshToken(client, token, cfg.ProxyURL) |
| if err != nil { |
| return nil, status, err |
| } |
| if data, ok := result.(map[string]interface{}); ok { |
| if accessToken, ok := data["access_token"].(string); ok && accessToken != "" { |
| acct := accounts.NewAccount(accessToken, accounts.TypeFree, accessToken) |
| acct.TeamUserID = teamAccountID |
| acct.Proxy = cfg.ProxyURL |
| acct.RefreshToken = token |
| if err := acct.InitClient(); err != nil { |
| return nil, http.StatusInternalServerError, err |
| } |
| acct.Status = accounts.StatusActive |
| return acct, http.StatusOK, nil |
| } |
| } |
| return nil, http.StatusBadRequest, errors.New("refresh token response did not include access_token") |
| } |
|
|
| |
| acct, err := pool.Acquire(accounts.TypeFree) |
| if err != nil { |
| return nil, http.StatusUnauthorized, ErrNoAvailable |
| } |
| if needsPaid && acct.Type == accounts.TypeNoAuth { |
| return nil, http.StatusForbidden, errors.New("this endpoint requires a logged-in ChatGPT account") |
| } |
| acct.LastUsed = time.Now() |
| return acct, http.StatusOK, nil |
| } |
|
|
| |
| |
| |
| |
| |
| func conversationClientOrder(client **bogdanfinn.TlsClient, account *accounts.Account, translatedRequest chatgpt_types.ChatGPTRequest, proxyUrl string, stream bool, state *chatgpt.ChatClientState, pool *accounts.Pool) (*http.Response, *websocket.Conn, *chatgpt.TurnStile, int, error) { |
| if state != nil { |
| state.ApplyToRequest(&translatedRequest) |
| } |
| turnTraceID := uuid.NewString() |
|
|
| (*client).SetCookies("https://chatgpt.com", chatgpt.BasicCookies) |
|
|
| turnStile, status, err := chatgpt.InitSentinelWithState(*client, account, proxyUrl, 0, state) |
| if err != nil { |
| |
| if status == http.StatusUnauthorized && pool != nil { |
| pool.ReportFailure(account) |
| } |
| return nil, nil, nil, status, err |
| } |
|
|
| chatgpt.POSTConversationInit(*client, account, state) |
|
|
| var wsConn *websocket.Conn |
| if chatgpt.RequiresConversationWebsocket(stream, translatedRequest.ThinkingEffort) && account.Type.Satisfies(accounts.CapWebSocket) { |
| wsConn, err = chatgpt.DialChatWebsocketWithStateAndProxy(*client, account, state, proxyUrl) |
| if err != nil { |
| return nil, nil, nil, http.StatusInternalServerError, err |
| } |
| } |
|
|
| conduitToken, err := chatgpt.PrepareConversationConduitFullWithSentinel(*client, translatedRequest, account, proxyUrl, turnTraceID, state, turnStile) |
| if err != nil { |
| if wsConn != nil { |
| wsConn.Close() |
| } |
| return nil, nil, nil, http.StatusInternalServerError, err |
| } |
|
|
| response, err := chatgpt.POSTconversationPreparedWithState(*client, translatedRequest, account, turnStile, proxyUrl, conduitToken, turnTraceID, state) |
| if err != nil { |
| if wsConn != nil { |
| wsConn.Close() |
| } |
| return nil, nil, nil, http.StatusInternalServerError, err |
| } |
| return response, wsConn, turnStile, http.StatusOK, nil |
| } |
|
|
| |
| func setupClientWithProxy(proxyUrl string) *bogdanfinn.TlsClient { |
| client := bogdanfinn.NewStdClient() |
| if proxyUrl != "" { |
| _ = client.SetProxy(proxyUrl) |
| } |
| return client |
| } |
|
|
| |
| func websocketProxyFunc(proxy string) (func(*fhttp.Request) (*url.URL, error), error) { |
| if proxy == "" { |
| return fhttp.ProxyFromEnvironment, nil |
| } |
| proxyURL, err := url.Parse(proxy) |
| if err != nil { |
| return nil, err |
| } |
| return fhttp.ProxyURL(proxyURL), nil |
| } |
|
|
| |
| func original_requestHasFiles(request officialtypes.APIRequest) bool { |
| for _, message := range request.Messages { |
| if len(message.Files()) > 0 { |
| return true |
| } |
| } |
| return false |
| } |
|
|
| |
| func toolCallingEnabled(tools []officialtypes.Tool, cfg *config.Config) bool { |
| if cfg != nil && !cfg.ToolCallingEnabled { |
| return false |
| } |
| return len(tools) > 0 |
| } |
|
|
| |
| func countMessagesTokens(messages []officialtypes.APIMessage) int { |
| total := 0 |
| for _, message := range messages { |
| total += util.CountToken(message.Text()) |
| } |
| return total |
| } |
|
|
| |
| func writeChatCompletionStreamDone(c *gin.Context, stopSent bool, model string, conversationID string) { |
| if !stopSent { |
| finalLine := officialtypes.StopChunkWithConversation("stop", model, conversationID) |
| c.Writer.WriteString("data: " + finalLine.String() + "\n\n") |
| c.Writer.Flush() |
| } |
| c.Writer.WriteString("data: [DONE]\n\n") |
| c.Writer.Flush() |
| } |
|
|
| |
| func looksLikeSandboxRefusal(text string) bool { |
| if text == "" { |
| return false |
| } |
| t := strings.ToLower(text) |
| markers := []string{ |
| "/mnt/data", "/workspace", "/home/oai", "filesystem isolado", "ambiente isolado", |
| "root linux", "linux/container", "container atual", "não tem acesso ao diret", |
| "nao tem acesso ao diret", "não está montado", "nao esta montado", |
| "não foi montado", "nao foi montado", "não existe neste ambiente", |
| "nao existe neste ambiente", "não pode continuar neste ambiente", |
| "não é possível ler", "nao e possivel ler", |
| "não foi possível abrir", "nao foi possivel abrir", |
| "não foi possível executar", "nao foi possivel executar", |
| "falha na interface de execução", "falha no parsing", |
| "inferência baseada na estrutura", "inferencia baseada na estrutura", |
| "baseada apenas na estrutura", |
| } |
| for _, m := range markers { |
| if strings.Contains(t, m) { |
| return true |
| } |
| } |
| return false |
| } |
|
|
| |
| func appendToolDebugLog(path string, attempt int, text string, calls []officialtypes.ToolCall) { |
| f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) |
| if err != nil { |
| return |
| } |
| defer f.Close() |
| callsJSON, _ := json.Marshal(calls) |
| fmt.Fprintf(f, "\n=== attempt %d ===\ntext: %s\ncalls: %s\n", attempt, text, string(callsJSON)) |
| } |
|
|
| |
|
|
| func responsesCreatedEvent(respID, model string) string { |
| evt := map[string]interface{}{ |
| "type": "response.created", |
| "response": map[string]interface{}{ |
| "id": respID, "object": "response", "created_at": time.Now().Unix(), |
| "model": model, "status": "in_progress", |
| }, |
| } |
| b, _ := json.Marshal(evt) |
| return string(b) |
| } |
|
|
| func responsesOutputItemAddedEvent(outputIndex int, itemID, itemType string) string { |
| evt := map[string]interface{}{ |
| "type": "response.output_item.added", |
| "output_index": outputIndex, |
| "item": map[string]interface{}{ |
| "id": itemID, "type": itemType, "status": "in_progress", |
| }, |
| } |
| b, _ := json.Marshal(evt) |
| return string(b) |
| } |
|
|
| func responsesOutputItemDoneEvent(outputIndex int, itemID, itemType, text string) string { |
| item := map[string]interface{}{ |
| "id": itemID, "type": itemType, "status": "completed", |
| } |
| if itemType == "message" { |
| item["role"] = "assistant" |
| item["content"] = []map[string]interface{}{ |
| {"type": "output_text", "text": text}, |
| } |
| } else if itemType == "reasoning" { |
| item["content"] = []map[string]interface{}{ |
| {"type": "reasoning_text", "text": text}, |
| } |
| } |
| evt := map[string]interface{}{ |
| "type": "response.output_item.done", |
| "output_index": outputIndex, |
| "item": item, |
| } |
| b, _ := json.Marshal(evt) |
| return string(b) |
| } |
|
|
| func responsesFailedEvent(msg string) string { |
| evt := map[string]interface{}{ |
| "type": "response.failed", |
| "response": map[string]interface{}{ |
| "error": map[string]interface{}{ |
| "message": msg, "type": "server_error", |
| }, |
| }, |
| } |
| b, _ := json.Marshal(evt) |
| return string(b) |
| } |
|
|
| func responsesCompletedEvent(resp officialtypes.ResponsesResponse) string { |
| evt := map[string]interface{}{ |
| "type": "response.completed", |
| "response": resp, |
| } |
| b, _ := json.Marshal(evt) |
| return string(b) |
| } |
|
|