fix: 修复 golangci-lint 报告的 36 个问题
This commit is contained in:
@@ -4643,32 +4643,6 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost(
|
|||||||
return s.billingService.CalculateCostWithServiceTier(billingModel, tokens, multiplier, serviceTier)
|
return s.billingService.CalculateCostWithServiceTier(billingModel, tokens, multiplier, serviceTier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OpenAIGatewayService) calculateOpenAIImageTokenCost(
|
|
||||||
ctx context.Context,
|
|
||||||
apiKey *APIKey,
|
|
||||||
billingModel string,
|
|
||||||
multiplier float64,
|
|
||||||
tokens UsageTokens,
|
|
||||||
serviceTier string,
|
|
||||||
sizeTier string,
|
|
||||||
) (*CostBreakdown, error) {
|
|
||||||
if s.resolver != nil && apiKey.Group != nil {
|
|
||||||
gid := apiKey.Group.ID
|
|
||||||
return s.billingService.CalculateCostUnified(CostInput{
|
|
||||||
Ctx: ctx,
|
|
||||||
Model: billingModel,
|
|
||||||
GroupID: &gid,
|
|
||||||
Tokens: tokens,
|
|
||||||
RequestCount: 1,
|
|
||||||
SizeTier: sizeTier,
|
|
||||||
RateMultiplier: multiplier,
|
|
||||||
ServiceTier: serviceTier,
|
|
||||||
Resolver: s.resolver,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return s.billingService.CalculateCostWithServiceTier(billingModel, tokens, multiplier, serviceTier)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OpenAIGatewayService) calculateOpenAIImageCost(
|
func (s *OpenAIGatewayService) calculateOpenAIImageCost(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
billingModel string,
|
billingModel string,
|
||||||
@@ -4718,17 +4692,6 @@ func (s *OpenAIGatewayService) resolveOpenAIChannelPricing(ctx context.Context,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasOpenAIImageUsageTokens(result *OpenAIForwardResult) bool {
|
|
||||||
if result == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return result.Usage.InputTokens > 0 ||
|
|
||||||
result.Usage.OutputTokens > 0 ||
|
|
||||||
result.Usage.CacheCreationInputTokens > 0 ||
|
|
||||||
result.Usage.CacheReadInputTokens > 0 ||
|
|
||||||
result.Usage.ImageOutputTokens > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseCodexRateLimitHeaders extracts Codex usage limits from response headers.
|
// ParseCodexRateLimitHeaders extracts Codex usage limits from response headers.
|
||||||
// Exported for use in ratelimit_service when handling OpenAI 429 responses.
|
// Exported for use in ratelimit_service when handling OpenAI 429 responses.
|
||||||
func ParseCodexRateLimitHeaders(headers http.Header) *OpenAICodexUsageSnapshot {
|
func ParseCodexRateLimitHeaders(headers http.Header) *OpenAICodexUsageSnapshot {
|
||||||
|
|||||||
@@ -5,27 +5,22 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha3"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyurl"
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
|
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -38,18 +33,11 @@ const (
|
|||||||
openAIImagesGenerationsURL = "https://api.openai.com/v1/images/generations"
|
openAIImagesGenerationsURL = "https://api.openai.com/v1/images/generations"
|
||||||
openAIImagesEditsURL = "https://api.openai.com/v1/images/edits"
|
openAIImagesEditsURL = "https://api.openai.com/v1/images/edits"
|
||||||
|
|
||||||
openAIChatGPTStartURL = "https://chatgpt.com/"
|
openAIChatGPTStartURL = "https://chatgpt.com/"
|
||||||
openAIChatGPTFilesURL = "https://chatgpt.com/backend-api/files"
|
openAIChatGPTFilesURL = "https://chatgpt.com/backend-api/files"
|
||||||
openAIChatGPTConversationInitURL = "https://chatgpt.com/backend-api/conversation/init"
|
openAIImageBackendUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||||
openAIChatGPTConversationURL = "https://chatgpt.com/backend-api/f/conversation"
|
openAIImageMaxDownloadBytes = 20 << 20 // 20MB per image download
|
||||||
openAIChatGPTConversationPrepareURL = "https://chatgpt.com/backend-api/f/conversation/prepare"
|
openAIImageMaxUploadPartSize = 20 << 20 // 20MB per multipart upload part
|
||||||
openAIChatGPTChatRequirementsURL = "https://chatgpt.com/backend-api/sentinel/chat-requirements"
|
|
||||||
|
|
||||||
openAIImageBackendUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
||||||
openAIImageRequirementsDiff = "0fffff"
|
|
||||||
openAIImageLifecycleTimeout = 2 * time.Minute
|
|
||||||
openAIImageMaxDownloadBytes = 20 << 20 // 20MB per image download
|
|
||||||
openAIImageMaxUploadPartSize = 20 << 20 // 20MB per multipart upload part
|
|
||||||
openAIImagesResponsesMainModel = "gpt-5.4-mini"
|
openAIImagesResponsesMainModel = "gpt-5.4-mini"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -885,413 +873,6 @@ func extractOpenAIImageCountFromJSONBytes(body []byte) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveOpenAIProxyURL(account *Account) string {
|
|
||||||
if account != nil && account.ProxyID != nil && account.Proxy != nil {
|
|
||||||
return account.Proxy.URL()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOpenAIBackendAPIClient(proxyURL string) (*req.Client, error) {
|
|
||||||
client := req.C().
|
|
||||||
SetTimeout(180 * time.Second).
|
|
||||||
ImpersonateChrome()
|
|
||||||
trimmed, _, err := proxyurl.Parse(proxyURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if trimmed != "" {
|
|
||||||
client.SetProxyURL(trimmed)
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OpenAIGatewayService) buildOpenAIBackendAPIHeaders(account *Account, token string) (http.Header, error) {
|
|
||||||
deviceID, sessionID := s.ensureOpenAIImageSessionCredentials(context.Background(), account)
|
|
||||||
headers := make(http.Header)
|
|
||||||
headers.Set("Authorization", "Bearer "+token)
|
|
||||||
headers.Set("Accept", "application/json")
|
|
||||||
headers.Set("Origin", "https://chatgpt.com")
|
|
||||||
headers.Set("Referer", "https://chatgpt.com/")
|
|
||||||
headers.Set("Sec-Fetch-Dest", "empty")
|
|
||||||
headers.Set("Sec-Fetch-Mode", "cors")
|
|
||||||
headers.Set("Sec-Fetch-Site", "same-origin")
|
|
||||||
headers.Set("User-Agent", openAIImageBackendUserAgent)
|
|
||||||
if customUA := strings.TrimSpace(account.GetOpenAIUserAgent()); customUA != "" {
|
|
||||||
headers.Set("User-Agent", customUA)
|
|
||||||
}
|
|
||||||
if chatgptAccountID := strings.TrimSpace(account.GetChatGPTAccountID()); chatgptAccountID != "" {
|
|
||||||
headers.Set("chatgpt-account-id", chatgptAccountID)
|
|
||||||
}
|
|
||||||
if deviceID != "" {
|
|
||||||
headers.Set("oai-device-id", deviceID)
|
|
||||||
headers.Set("Cookie", "oai-did="+deviceID)
|
|
||||||
}
|
|
||||||
if sessionID != "" {
|
|
||||||
headers.Set("oai-session-id", sessionID)
|
|
||||||
}
|
|
||||||
return headers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OpenAIGatewayService) ensureOpenAIImageSessionCredentials(ctx context.Context, account *Account) (string, string) {
|
|
||||||
if account == nil {
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
deviceID := account.GetOpenAIDeviceID()
|
|
||||||
sessionID := account.GetOpenAISessionID()
|
|
||||||
if deviceID != "" && sessionID != "" {
|
|
||||||
return deviceID, sessionID
|
|
||||||
}
|
|
||||||
|
|
||||||
updates := map[string]any{}
|
|
||||||
if deviceID == "" {
|
|
||||||
deviceID = uuid.NewString()
|
|
||||||
updates["openai_device_id"] = deviceID
|
|
||||||
}
|
|
||||||
if sessionID == "" {
|
|
||||||
sessionID = uuid.NewString()
|
|
||||||
updates["openai_session_id"] = sessionID
|
|
||||||
}
|
|
||||||
if account.Extra == nil {
|
|
||||||
account.Extra = map[string]any{}
|
|
||||||
}
|
|
||||||
for key, value := range updates {
|
|
||||||
account.Extra[key] = value
|
|
||||||
}
|
|
||||||
if len(updates) == 0 || s == nil || s.accountRepo == nil {
|
|
||||||
return deviceID, sessionID
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := s.accountRepo.UpdateExtra(updateCtx, account.ID, updates); err != nil {
|
|
||||||
logger.LegacyPrintf("service.openai_gateway", "persist openai image session creds failed: account=%d err=%v", account.ID, err)
|
|
||||||
}
|
|
||||||
return deviceID, sessionID
|
|
||||||
}
|
|
||||||
|
|
||||||
func bootstrapOpenAIBackendAPI(ctx context.Context, client *req.Client, headers http.Header) error {
|
|
||||||
resp, err := client.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
DisableAutoReadResponse().
|
|
||||||
SetHeaders(headerToMap(headers)).
|
|
||||||
Get(openAIChatGPTStartURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp != nil && resp.Body != nil {
|
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initializeOpenAIImageConversation(ctx context.Context, client *req.Client, headers http.Header) error {
|
|
||||||
payload := map[string]any{
|
|
||||||
"gizmo_id": nil,
|
|
||||||
"requested_default_model": nil,
|
|
||||||
"conversation_id": nil,
|
|
||||||
"timezone_offset_min": openAITimezoneOffsetMinutes(),
|
|
||||||
"system_hints": []string{"picture_v2"},
|
|
||||||
}
|
|
||||||
resp, err := client.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetHeaders(headerToMap(headers)).
|
|
||||||
SetBodyJsonMarshal(payload).
|
|
||||||
Post(openAIChatGPTConversationInitURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !resp.IsSuccessState() {
|
|
||||||
return newOpenAIImageStatusError(resp, "conversation init failed")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type openAIChatRequirements struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
Turnstile struct {
|
|
||||||
Required bool `json:"required"`
|
|
||||||
} `json:"turnstile"`
|
|
||||||
Arkose struct {
|
|
||||||
Required bool `json:"required"`
|
|
||||||
} `json:"arkose"`
|
|
||||||
ProofOfWork struct {
|
|
||||||
Required bool `json:"required"`
|
|
||||||
Seed string `json:"seed"`
|
|
||||||
Difficulty string `json:"difficulty"`
|
|
||||||
} `json:"proofofwork"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchOpenAIChatRequirements(ctx context.Context, client *req.Client, headers http.Header) (*openAIChatRequirements, error) {
|
|
||||||
var lastErr error
|
|
||||||
for _, payload := range []map[string]any{
|
|
||||||
{"p": nil},
|
|
||||||
{"p": generateOpenAIRequirementsToken(headers.Get("User-Agent"))},
|
|
||||||
} {
|
|
||||||
var result openAIChatRequirements
|
|
||||||
resp, err := client.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetHeaders(headerToMap(headers)).
|
|
||||||
SetBodyJsonMarshal(payload).
|
|
||||||
SetSuccessResult(&result).
|
|
||||||
Post(openAIChatGPTChatRequirementsURL)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if resp.IsSuccessState() && strings.TrimSpace(result.Token) != "" {
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
lastErr = newOpenAIImageStatusError(resp, "chat-requirements failed")
|
|
||||||
}
|
|
||||||
if lastErr == nil {
|
|
||||||
lastErr = fmt.Errorf("chat-requirements failed")
|
|
||||||
}
|
|
||||||
return nil, lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareOpenAIImageConversation(
|
|
||||||
ctx context.Context,
|
|
||||||
client *req.Client,
|
|
||||||
headers http.Header,
|
|
||||||
prompt string,
|
|
||||||
parentMessageID string,
|
|
||||||
chatToken string,
|
|
||||||
proofToken string,
|
|
||||||
) (string, error) {
|
|
||||||
messageID := uuid.NewString()
|
|
||||||
payload := map[string]any{
|
|
||||||
"action": "next",
|
|
||||||
"client_prepare_state": "success",
|
|
||||||
"fork_from_shared_post": false,
|
|
||||||
"parent_message_id": parentMessageID,
|
|
||||||
"model": "auto",
|
|
||||||
"timezone_offset_min": openAITimezoneOffsetMinutes(),
|
|
||||||
"timezone": openAITimezoneName(),
|
|
||||||
"conversation_mode": map[string]any{"kind": "primary_assistant"},
|
|
||||||
"system_hints": []string{"picture_v2"},
|
|
||||||
"supports_buffering": true,
|
|
||||||
"supported_encodings": []string{"v1"},
|
|
||||||
"partial_query": map[string]any{
|
|
||||||
"id": messageID,
|
|
||||||
"author": map[string]any{"role": "user"},
|
|
||||||
"content": map[string]any{
|
|
||||||
"content_type": "text",
|
|
||||||
"parts": []string{coalesceOpenAIFileName(prompt, "Generate an image.")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"client_contextual_info": map[string]any{
|
|
||||||
"app_name": "chatgpt.com",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
prepareHeaders := cloneHTTPHeader(headers)
|
|
||||||
prepareHeaders.Set("Accept", "*/*")
|
|
||||||
prepareHeaders.Set("Content-Type", "application/json")
|
|
||||||
if strings.TrimSpace(chatToken) != "" {
|
|
||||||
prepareHeaders.Set("openai-sentinel-chat-requirements-token", strings.TrimSpace(chatToken))
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(proofToken) != "" {
|
|
||||||
prepareHeaders.Set("openai-sentinel-proof-token", strings.TrimSpace(proofToken))
|
|
||||||
}
|
|
||||||
var result struct {
|
|
||||||
ConduitToken string `json:"conduit_token"`
|
|
||||||
}
|
|
||||||
resp, err := client.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetHeaders(headerToMap(prepareHeaders)).
|
|
||||||
SetBodyJsonMarshal(payload).
|
|
||||||
SetSuccessResult(&result).
|
|
||||||
Post(openAIChatGPTConversationPrepareURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !resp.IsSuccessState() {
|
|
||||||
return "", newOpenAIImageStatusError(resp, "conversation prepare failed")
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(result.ConduitToken), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type openAIUploadedImage struct {
|
|
||||||
FileID string
|
|
||||||
FileName string
|
|
||||||
FileSize int
|
|
||||||
MimeType string
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
}
|
|
||||||
|
|
||||||
func uploadOpenAIImageFiles(ctx context.Context, client *req.Client, headers http.Header, uploads []OpenAIImagesUpload) ([]openAIUploadedImage, error) {
|
|
||||||
if len(uploads) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
results := make([]openAIUploadedImage, 0, len(uploads))
|
|
||||||
for i := range uploads {
|
|
||||||
item := uploads[i]
|
|
||||||
fileName := coalesceOpenAIFileName(item.FileName, "image.png")
|
|
||||||
payload := map[string]any{
|
|
||||||
"file_name": fileName,
|
|
||||||
"file_size": len(item.Data),
|
|
||||||
"use_case": "multimodal",
|
|
||||||
}
|
|
||||||
var created struct {
|
|
||||||
FileID string `json:"file_id"`
|
|
||||||
UploadURL string `json:"upload_url"`
|
|
||||||
}
|
|
||||||
resp, err := client.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetHeaders(headerToMap(headers)).
|
|
||||||
SetBodyJsonMarshal(payload).
|
|
||||||
SetSuccessResult(&created).
|
|
||||||
Post(openAIChatGPTFilesURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !resp.IsSuccessState() || strings.TrimSpace(created.FileID) == "" || strings.TrimSpace(created.UploadURL) == "" {
|
|
||||||
return nil, newOpenAIImageStatusError(resp, "create upload slot failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadHeaders := map[string]string{
|
|
||||||
"Content-Type": coalesceOpenAIFileName(item.ContentType, "application/octet-stream"),
|
|
||||||
"Origin": "https://chatgpt.com",
|
|
||||||
"x-ms-blob-type": "BlockBlob",
|
|
||||||
"x-ms-version": "2020-04-08",
|
|
||||||
"User-Agent": headers.Get("User-Agent"),
|
|
||||||
}
|
|
||||||
putResp, err := client.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetHeaders(uploadHeaders).
|
|
||||||
SetBody(item.Data).
|
|
||||||
DisableAutoReadResponse().
|
|
||||||
Put(created.UploadURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if putResp.Response != nil && putResp.Body != nil {
|
|
||||||
_, _ = io.Copy(io.Discard, putResp.Body)
|
|
||||||
_ = putResp.Body.Close()
|
|
||||||
}
|
|
||||||
if putResp.StatusCode < 200 || putResp.StatusCode >= 300 {
|
|
||||||
return nil, newOpenAIImageStatusError(putResp, "upload image bytes failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadedResp, err := client.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetHeaders(headerToMap(headers)).
|
|
||||||
SetBodyJsonMarshal(map[string]any{}).
|
|
||||||
Post(fmt.Sprintf("%s/%s/uploaded", openAIChatGPTFilesURL, created.FileID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !uploadedResp.IsSuccessState() {
|
|
||||||
return nil, newOpenAIImageStatusError(uploadedResp, "mark upload complete failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, openAIUploadedImage{
|
|
||||||
FileID: created.FileID,
|
|
||||||
FileName: fileName,
|
|
||||||
FileSize: len(item.Data),
|
|
||||||
MimeType: coalesceOpenAIFileName(item.ContentType, "application/octet-stream"),
|
|
||||||
Width: item.Width,
|
|
||||||
Height: item.Height,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func coalesceOpenAIFileName(value string, fallback string) string {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value == "" {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildOpenAIImageConversationRequest(parsed *OpenAIImagesRequest, parentMessageID string, uploads []openAIUploadedImage) map[string]any {
|
|
||||||
parts := []any{coalesceOpenAIFileName(parsed.Prompt, "Generate an image.")}
|
|
||||||
attachments := make([]map[string]any, 0, len(uploads))
|
|
||||||
if len(uploads) > 0 {
|
|
||||||
parts = make([]any, 0, len(uploads)+1)
|
|
||||||
for _, upload := range uploads {
|
|
||||||
parts = append(parts, map[string]any{
|
|
||||||
"content_type": "image_asset_pointer",
|
|
||||||
"asset_pointer": "file-service://" + upload.FileID,
|
|
||||||
"size_bytes": upload.FileSize,
|
|
||||||
"width": upload.Width,
|
|
||||||
"height": upload.Height,
|
|
||||||
})
|
|
||||||
attachment := map[string]any{
|
|
||||||
"id": upload.FileID,
|
|
||||||
"mimeType": upload.MimeType,
|
|
||||||
"name": upload.FileName,
|
|
||||||
"size": upload.FileSize,
|
|
||||||
}
|
|
||||||
if upload.Width > 0 {
|
|
||||||
attachment["width"] = upload.Width
|
|
||||||
}
|
|
||||||
if upload.Height > 0 {
|
|
||||||
attachment["height"] = upload.Height
|
|
||||||
}
|
|
||||||
attachments = append(attachments, attachment)
|
|
||||||
}
|
|
||||||
parts = append(parts, coalesceOpenAIFileName(parsed.Prompt, "Edit this image."))
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := "text"
|
|
||||||
if len(uploads) > 0 {
|
|
||||||
contentType = "multimodal_text"
|
|
||||||
}
|
|
||||||
metadata := map[string]any{
|
|
||||||
"developer_mode_connector_ids": []any{},
|
|
||||||
"selected_github_repos": []any{},
|
|
||||||
"selected_all_github_repos": false,
|
|
||||||
"system_hints": []string{"picture_v2"},
|
|
||||||
"serialization_metadata": map[string]any{
|
|
||||||
"custom_symbol_offsets": []any{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
message := map[string]any{
|
|
||||||
"id": uuid.NewString(),
|
|
||||||
"author": map[string]any{"role": "user"},
|
|
||||||
"content": map[string]any{
|
|
||||||
"content_type": contentType,
|
|
||||||
"parts": parts,
|
|
||||||
},
|
|
||||||
"metadata": metadata,
|
|
||||||
"create_time": float64(time.Now().UnixMilli()) / 1000,
|
|
||||||
}
|
|
||||||
if len(attachments) > 0 {
|
|
||||||
metadata["attachments"] = attachments
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]any{
|
|
||||||
"action": "next",
|
|
||||||
"client_prepare_state": "sent",
|
|
||||||
"parent_message_id": parentMessageID,
|
|
||||||
"model": "auto",
|
|
||||||
"timezone_offset_min": openAITimezoneOffsetMinutes(),
|
|
||||||
"timezone": openAITimezoneName(),
|
|
||||||
"conversation_mode": map[string]any{"kind": "primary_assistant"},
|
|
||||||
"enable_message_followups": true,
|
|
||||||
"system_hints": []string{"picture_v2"},
|
|
||||||
"supports_buffering": true,
|
|
||||||
"supported_encodings": []string{"v1"},
|
|
||||||
"paragen_cot_summary_display_override": "allow",
|
|
||||||
"force_parallel_switch": "auto",
|
|
||||||
"client_contextual_info": map[string]any{
|
|
||||||
"is_dark_mode": false,
|
|
||||||
"time_since_loaded": 200,
|
|
||||||
"page_height": 900,
|
|
||||||
"page_width": 1440,
|
|
||||||
"pixel_ratio": 1,
|
|
||||||
"screen_height": 1080,
|
|
||||||
"screen_width": 1920,
|
|
||||||
"app_name": "chatgpt.com",
|
|
||||||
},
|
|
||||||
"messages": []any{message},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type openAIImagePointerInfo struct {
|
type openAIImagePointerInfo struct {
|
||||||
Pointer string
|
Pointer string
|
||||||
DownloadURL string
|
DownloadURL string
|
||||||
@@ -1300,51 +881,6 @@ type openAIImagePointerInfo struct {
|
|||||||
Prompt string
|
Prompt string
|
||||||
}
|
}
|
||||||
|
|
||||||
type openAIImageToolMessage struct {
|
|
||||||
MessageID string
|
|
||||||
CreateTime float64
|
|
||||||
PointerInfos []openAIImagePointerInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func readOpenAIImageConversationStream(resp *req.Response, startTime time.Time) (string, []openAIImagePointerInfo, OpenAIUsage, *int, error) {
|
|
||||||
if resp == nil || resp.Body == nil {
|
|
||||||
return "", nil, OpenAIUsage{}, nil, fmt.Errorf("empty conversation response")
|
|
||||||
}
|
|
||||||
reader := bufio.NewReader(resp.Body)
|
|
||||||
var (
|
|
||||||
conversationID string
|
|
||||||
firstTokenMs *int
|
|
||||||
usage OpenAIUsage
|
|
||||||
pointers []openAIImagePointerInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if strings.TrimSpace(line) != "" && firstTokenMs == nil {
|
|
||||||
ms := int(time.Since(startTime).Milliseconds())
|
|
||||||
firstTokenMs = &ms
|
|
||||||
}
|
|
||||||
if data, ok := extractOpenAISSEDataLine(strings.TrimRight(line, "\r\n")); ok && data != "" && data != "[DONE]" {
|
|
||||||
dataBytes := []byte(data)
|
|
||||||
if conversationID == "" {
|
|
||||||
conversationID = strings.TrimSpace(gjson.GetBytes(dataBytes, "v.conversation_id").String())
|
|
||||||
if conversationID == "" {
|
|
||||||
conversationID = strings.TrimSpace(gjson.GetBytes(dataBytes, "conversation_id").String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mergeOpenAIUsage(&usage, dataBytes)
|
|
||||||
pointers = mergeOpenAIImagePointerInfos(pointers, collectOpenAIImagePointers(dataBytes))
|
|
||||||
}
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, OpenAIUsage{}, firstTokenMs, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return conversationID, pointers, usage, firstTokenMs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectOpenAIImagePointers(body []byte) []openAIImagePointerInfo {
|
func collectOpenAIImagePointers(body []byte) []openAIImagePointerInfo {
|
||||||
if len(body) == 0 {
|
if len(body) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -1470,222 +1006,6 @@ func mergeOpenAIImagePointerInfo(existing, next openAIImagePointerInfo) openAIIm
|
|||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasOpenAIFileServicePointerInfos(items []openAIImagePointerInfo) bool {
|
|
||||||
for _, item := range items {
|
|
||||||
if strings.HasPrefix(item.Pointer, "file-service://") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func countOpenAIFileServicePointerInfos(items []openAIImagePointerInfo) int {
|
|
||||||
count := 0
|
|
||||||
for _, item := range items {
|
|
||||||
if strings.HasPrefix(item.Pointer, "file-service://") {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func countOpenAIDirectImageAssets(items []openAIImagePointerInfo) int {
|
|
||||||
count := 0
|
|
||||||
for _, item := range items {
|
|
||||||
if strings.TrimSpace(item.DownloadURL) != "" || strings.TrimSpace(item.B64JSON) != "" {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func preferOpenAIFileServicePointerInfos(items []openAIImagePointerInfo) []openAIImagePointerInfo {
|
|
||||||
if !hasOpenAIFileServicePointerInfos(items) {
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
out := make([]openAIImagePointerInfo, 0, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
if strings.HasPrefix(item.Pointer, "file-service://") {
|
|
||||||
out = append(out, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractOpenAIImageToolMessages(mapping map[string]any) []openAIImageToolMessage {
|
|
||||||
if len(mapping) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := make([]openAIImageToolMessage, 0, 4)
|
|
||||||
for messageID, raw := range mapping {
|
|
||||||
node, _ := raw.(map[string]any)
|
|
||||||
if node == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
message, _ := node["message"].(map[string]any)
|
|
||||||
if message == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
author, _ := message["author"].(map[string]any)
|
|
||||||
metadata, _ := message["metadata"].(map[string]any)
|
|
||||||
content, _ := message["content"].(map[string]any)
|
|
||||||
if author == nil || metadata == nil || content == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if role, _ := author["role"].(string); role != "tool" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if asyncTaskType, _ := metadata["async_task_type"].(string); asyncTaskType != "image_gen" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if contentType, _ := content["content_type"].(string); contentType != "multimodal_text" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
prompt := ""
|
|
||||||
if title, _ := metadata["image_gen_title"].(string); strings.TrimSpace(title) != "" {
|
|
||||||
prompt = strings.TrimSpace(title)
|
|
||||||
}
|
|
||||||
item := openAIImageToolMessage{MessageID: messageID}
|
|
||||||
if createTime, ok := message["create_time"].(float64); ok {
|
|
||||||
item.CreateTime = createTime
|
|
||||||
}
|
|
||||||
parts, _ := content["parts"].([]any)
|
|
||||||
for _, part := range parts {
|
|
||||||
switch value := part.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
if assetPointer, _ := value["asset_pointer"].(string); strings.TrimSpace(assetPointer) != "" {
|
|
||||||
for _, pointer := range openAIImagePointerMatches([]byte(assetPointer)) {
|
|
||||||
item.PointerInfos = append(item.PointerInfos, openAIImagePointerInfo{
|
|
||||||
Pointer: pointer,
|
|
||||||
Prompt: prompt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case string:
|
|
||||||
for _, pointer := range openAIImagePointerMatches([]byte(value)) {
|
|
||||||
item.PointerInfos = append(item.PointerInfos, openAIImagePointerInfo{
|
|
||||||
Pointer: pointer,
|
|
||||||
Prompt: prompt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(item.PointerInfos) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
item.PointerInfos = mergeOpenAIImagePointerInfos(nil, item.PointerInfos)
|
|
||||||
out = append(out, item)
|
|
||||||
}
|
|
||||||
sort.Slice(out, func(i, j int) bool {
|
|
||||||
return out[i].CreateTime < out[j].CreateTime
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func pollOpenAIImageConversation(ctx context.Context, client *req.Client, headers http.Header, conversationID string) ([]openAIImagePointerInfo, error) {
|
|
||||||
conversationID = strings.TrimSpace(conversationID)
|
|
||||||
if conversationID == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
deadline := time.Now().Add(90 * time.Second)
|
|
||||||
interval := 3 * time.Second
|
|
||||||
previewWait := 15 * time.Second
|
|
||||||
var (
|
|
||||||
lastErr error
|
|
||||||
firstToolAt time.Time
|
|
||||||
)
|
|
||||||
for time.Now().Before(deadline) {
|
|
||||||
resp, err := client.R().
|
|
||||||
SetContext(ctx).
|
|
||||||
SetHeaders(headerToMap(headers)).
|
|
||||||
DisableAutoReadResponse().
|
|
||||||
Get(fmt.Sprintf("https://chatgpt.com/backend-api/conversation/%s", conversationID))
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
} else {
|
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
||||||
body, readErr := io.ReadAll(resp.Body)
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
if readErr != nil {
|
|
||||||
lastErr = readErr
|
|
||||||
goto waitNextPoll
|
|
||||||
}
|
|
||||||
pointers := mergeOpenAIImagePointerInfos(nil, collectOpenAIImagePointers(body))
|
|
||||||
var decoded map[string]any
|
|
||||||
if err := json.Unmarshal(body, &decoded); err == nil {
|
|
||||||
if mapping, _ := decoded["mapping"].(map[string]any); len(mapping) > 0 {
|
|
||||||
toolMessages := extractOpenAIImageToolMessages(mapping)
|
|
||||||
if len(toolMessages) > 0 && firstToolAt.IsZero() {
|
|
||||||
firstToolAt = time.Now()
|
|
||||||
}
|
|
||||||
for _, msg := range toolMessages {
|
|
||||||
pointers = mergeOpenAIImagePointerInfos(pointers, msg.PointerInfos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasOpenAIFileServicePointerInfos(pointers) {
|
|
||||||
return preferOpenAIFileServicePointerInfos(pointers), nil
|
|
||||||
}
|
|
||||||
if len(pointers) > 0 && !firstToolAt.IsZero() && time.Since(firstToolAt) >= previewWait {
|
|
||||||
return pointers, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
statusErr := newOpenAIImageStatusError(resp, "conversation poll failed")
|
|
||||||
if isOpenAIImageTransientConversationNotFoundError(statusErr) {
|
|
||||||
lastErr = statusErr
|
|
||||||
goto waitNextPoll
|
|
||||||
}
|
|
||||||
return nil, statusErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
waitNextPoll:
|
|
||||||
timer := time.NewTimer(interval)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
if !timer.Stop() {
|
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case <-timer.C:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildOpenAIImageResponse(
|
|
||||||
ctx context.Context,
|
|
||||||
client *req.Client,
|
|
||||||
headers http.Header,
|
|
||||||
conversationID string,
|
|
||||||
pointers []openAIImagePointerInfo,
|
|
||||||
) ([]byte, int, error) {
|
|
||||||
type responseItem struct {
|
|
||||||
B64JSON string `json:"b64_json"`
|
|
||||||
RevisedPrompt string `json:"revised_prompt,omitempty"`
|
|
||||||
}
|
|
||||||
items := make([]responseItem, 0, len(pointers))
|
|
||||||
for _, pointer := range pointers {
|
|
||||||
data, err := resolveOpenAIImageBytes(ctx, client, headers, conversationID, pointer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
items = append(items, responseItem{
|
|
||||||
B64JSON: base64.StdEncoding.EncodeToString(data),
|
|
||||||
RevisedPrompt: pointer.Prompt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
payload := map[string]any{
|
|
||||||
"created": time.Now().Unix(),
|
|
||||||
"data": items,
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return body, len(items), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveOpenAIImageBytes(
|
func resolveOpenAIImageBytes(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
client *req.Client,
|
client *req.Client,
|
||||||
@@ -1805,17 +1125,6 @@ func isLikelyOpenAIImageDownloadURL(raw string) bool {
|
|||||||
strings.Contains(lower, ".webp")
|
strings.Contains(lower, ".webp")
|
||||||
}
|
}
|
||||||
|
|
||||||
func detachOpenAIImageLifecycleContext(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
|
|
||||||
base := context.Background()
|
|
||||||
if ctx != nil {
|
|
||||||
base = context.WithoutCancel(ctx)
|
|
||||||
}
|
|
||||||
if timeout <= 0 {
|
|
||||||
return base, func() {}
|
|
||||||
}
|
|
||||||
return context.WithTimeout(base, timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchOpenAIImageDownloadURL(
|
func fetchOpenAIImageDownloadURL(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
client *req.Client,
|
client *req.Client,
|
||||||
@@ -1910,10 +1219,6 @@ func downloadOpenAIImageBytes(ctx context.Context, client *req.Client, headers h
|
|||||||
return io.ReadAll(io.LimitReader(resp.Body, openAIImageMaxDownloadBytes))
|
return io.ReadAll(io.LimitReader(resp.Body, openAIImageMaxDownloadBytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleOpenAIImageBackendError(resp *req.Response) error {
|
|
||||||
return newOpenAIImageStatusError(resp, "backend-api request failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
type openAIImageStatusError struct {
|
type openAIImageStatusError struct {
|
||||||
StatusCode int
|
StatusCode int
|
||||||
Message string
|
Message string
|
||||||
@@ -1981,23 +1286,6 @@ func newOpenAIImageStatusError(resp *req.Response, fallback string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOpenAIImageSyntheticStatusError(statusCode int, message string, requestURL string) *openAIImageStatusError {
|
|
||||||
message = sanitizeUpstreamErrorMessage(strings.TrimSpace(message))
|
|
||||||
if message == "" {
|
|
||||||
message = "openai image backend request failed"
|
|
||||||
}
|
|
||||||
var body []byte
|
|
||||||
if payload, err := json.Marshal(map[string]string{"detail": message}); err == nil {
|
|
||||||
body = payload
|
|
||||||
}
|
|
||||||
return &openAIImageStatusError{
|
|
||||||
StatusCode: statusCode,
|
|
||||||
Message: message,
|
|
||||||
ResponseBody: body,
|
|
||||||
URL: strings.TrimSpace(requestURL),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOpenAIImageTransientConversationNotFoundError(err error) bool {
|
func isOpenAIImageTransientConversationNotFoundError(err error) bool {
|
||||||
statusErr, ok := err.(*openAIImageStatusError)
|
statusErr, ok := err.(*openAIImageStatusError)
|
||||||
if !ok || statusErr == nil || statusErr.StatusCode != http.StatusNotFound {
|
if !ok || statusErr == nil || statusErr.StatusCode != http.StatusNotFound {
|
||||||
@@ -2017,58 +1305,6 @@ func isOpenAIImageTransientConversationNotFoundError(err error) bool {
|
|||||||
return strings.Contains(bodyMsg, "conversation") && strings.Contains(bodyMsg, "not found")
|
return strings.Contains(bodyMsg, "conversation") && strings.Contains(bodyMsg, "not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OpenAIGatewayService) wrapOpenAIImageBackendError(
|
|
||||||
ctx context.Context,
|
|
||||||
c *gin.Context,
|
|
||||||
account *Account,
|
|
||||||
err error,
|
|
||||||
) error {
|
|
||||||
var statusErr *openAIImageStatusError
|
|
||||||
if !errors.As(err, &statusErr) || statusErr == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
upstreamMsg := sanitizeUpstreamErrorMessage(statusErr.Message)
|
|
||||||
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
|
|
||||||
Platform: account.Platform,
|
|
||||||
AccountID: account.ID,
|
|
||||||
AccountName: account.Name,
|
|
||||||
UpstreamStatusCode: statusErr.StatusCode,
|
|
||||||
UpstreamRequestID: statusErr.RequestID,
|
|
||||||
UpstreamURL: safeUpstreamURL(statusErr.URL),
|
|
||||||
Kind: "request_error",
|
|
||||||
Message: upstreamMsg,
|
|
||||||
})
|
|
||||||
setOpsUpstreamError(c, statusErr.StatusCode, upstreamMsg, "")
|
|
||||||
|
|
||||||
if s.shouldFailoverOpenAIUpstreamResponse(statusErr.StatusCode, upstreamMsg, statusErr.ResponseBody) {
|
|
||||||
if s.rateLimitService != nil {
|
|
||||||
s.rateLimitService.HandleUpstreamError(ctx, account, statusErr.StatusCode, statusErr.ResponseHeaders, statusErr.ResponseBody)
|
|
||||||
}
|
|
||||||
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
|
|
||||||
Platform: account.Platform,
|
|
||||||
AccountID: account.ID,
|
|
||||||
AccountName: account.Name,
|
|
||||||
UpstreamStatusCode: statusErr.StatusCode,
|
|
||||||
UpstreamRequestID: statusErr.RequestID,
|
|
||||||
UpstreamURL: safeUpstreamURL(statusErr.URL),
|
|
||||||
Kind: "failover",
|
|
||||||
Message: upstreamMsg,
|
|
||||||
})
|
|
||||||
retryableOnSameAccount := account.IsPoolMode() && isPoolModeRetryableStatus(statusErr.StatusCode)
|
|
||||||
if strings.Contains(strings.ToLower(statusErr.Message), "unsupported challenge") {
|
|
||||||
retryableOnSameAccount = false
|
|
||||||
}
|
|
||||||
return &UpstreamFailoverError{
|
|
||||||
StatusCode: statusErr.StatusCode,
|
|
||||||
ResponseBody: statusErr.ResponseBody,
|
|
||||||
RetryableOnSameAccount: retryableOnSameAccount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneHTTPHeader(src http.Header) http.Header {
|
func cloneHTTPHeader(src http.Header) http.Header {
|
||||||
dst := make(http.Header, len(src))
|
dst := make(http.Header, len(src))
|
||||||
for key, values := range src {
|
for key, values := range src {
|
||||||
@@ -2093,110 +1329,6 @@ func headerToMap(header http.Header) map[string]string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func openAITimezoneOffsetMinutes() int {
|
|
||||||
_, offset := time.Now().Zone()
|
|
||||||
return offset / 60
|
|
||||||
}
|
|
||||||
|
|
||||||
func openAITimezoneName() string {
|
|
||||||
return time.Now().Location().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateOpenAIRequirementsToken(userAgent string) string {
|
|
||||||
config := []any{
|
|
||||||
"core" + strconv.Itoa(3008),
|
|
||||||
time.Now().UTC().Format(time.RFC1123),
|
|
||||||
nil,
|
|
||||||
0.123456,
|
|
||||||
coalesceOpenAIFileName(strings.TrimSpace(userAgent), openAIImageBackendUserAgent),
|
|
||||||
nil,
|
|
||||||
"prod-openai-images",
|
|
||||||
"en-US",
|
|
||||||
"en-US,en",
|
|
||||||
0,
|
|
||||||
"navigator.webdriver",
|
|
||||||
"location",
|
|
||||||
"document.body",
|
|
||||||
float64(time.Now().UnixMilli()) / 1000,
|
|
||||||
uuid.NewString(),
|
|
||||||
"",
|
|
||||||
8,
|
|
||||||
time.Now().Unix(),
|
|
||||||
}
|
|
||||||
answer, solved := generateOpenAIChallengeAnswer(strconv.FormatInt(time.Now().UnixNano(), 10), openAIImageRequirementsDiff, config)
|
|
||||||
if solved {
|
|
||||||
return "gAAAAAC" + answer
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateOpenAIChallengeAnswer(seed string, difficulty string, config []any) (string, bool) {
|
|
||||||
diffBytes, err := hex.DecodeString(difficulty)
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
p1 := []byte(jsonCompactSlice(config[:3], true))
|
|
||||||
p2 := []byte(jsonCompactSlice(config[4:9], false))
|
|
||||||
p3 := []byte(jsonCompactSlice(config[10:], false))
|
|
||||||
seedBytes := []byte(seed)
|
|
||||||
|
|
||||||
for i := 0; i < 100000; i++ {
|
|
||||||
payload := fmt.Sprintf("%s%d,%s,%d,%s", p1, i, p2, i>>1, p3)
|
|
||||||
encoded := base64.StdEncoding.EncodeToString([]byte(payload))
|
|
||||||
sum := sha3.Sum512(append(seedBytes, []byte(encoded)...))
|
|
||||||
if bytes.Compare(sum[:len(diffBytes)], diffBytes) <= 0 {
|
|
||||||
return encoded, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonCompactSlice(values []any, trimSuffixComma bool) string {
|
|
||||||
raw, _ := json.Marshal(values)
|
|
||||||
text := string(raw)
|
|
||||||
if trimSuffixComma {
|
|
||||||
return strings.TrimSuffix(text, "]")
|
|
||||||
}
|
|
||||||
return strings.TrimPrefix(text, "[")
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateOpenAIProofToken(required bool, seed string, difficulty string, userAgent string) string {
|
|
||||||
if !required || strings.TrimSpace(seed) == "" || strings.TrimSpace(difficulty) == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
screen := 3008
|
|
||||||
if len(seed)%2 == 0 {
|
|
||||||
screen = 4010
|
|
||||||
}
|
|
||||||
proofToken := []any{
|
|
||||||
screen,
|
|
||||||
time.Now().UTC().Format(time.RFC1123),
|
|
||||||
nil,
|
|
||||||
0,
|
|
||||||
coalesceOpenAIFileName(strings.TrimSpace(userAgent), openAIImageBackendUserAgent),
|
|
||||||
"https://chatgpt.com/",
|
|
||||||
"dpl=openai-images",
|
|
||||||
"en",
|
|
||||||
"en-US",
|
|
||||||
nil,
|
|
||||||
"plugins[object PluginArray]",
|
|
||||||
"_reactListening",
|
|
||||||
"alert",
|
|
||||||
}
|
|
||||||
diffLen := len(difficulty)
|
|
||||||
for i := 0; i < 100000; i++ {
|
|
||||||
proofToken[3] = i
|
|
||||||
raw, _ := json.Marshal(proofToken)
|
|
||||||
encoded := base64.StdEncoding.EncodeToString(raw)
|
|
||||||
sum := sha3.Sum512([]byte(seed + encoded))
|
|
||||||
if strings.Compare(hex.EncodeToString(sum[:])[:diffLen], difficulty) <= 0 {
|
|
||||||
return "gAAAAAB" + encoded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fallbackBase := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%q", seed)))
|
|
||||||
return "gAAAAABwQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + fallbackBase
|
|
||||||
}
|
|
||||||
|
|
||||||
func dedupeStrings(values []string) []string {
|
func dedupeStrings(values []string) []string {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -821,7 +821,7 @@ func (s *OpenAIGatewayService) forwardOpenAIImagesOAuth(
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
usage OpenAIUsage
|
usage OpenAIUsage
|
||||||
imageCount = parsed.N
|
imageCount int
|
||||||
firstTokenMs *int
|
firstTokenMs *int
|
||||||
)
|
)
|
||||||
if parsed.Stream {
|
if parsed.Stream {
|
||||||
|
|||||||
Reference in New Issue
Block a user