| package handler |
|
|
| import ( |
| "bytes" |
| "context" |
| "crypto/sha256" |
| "encoding/hex" |
| "encoding/json" |
| "errors" |
| "net/http" |
| "regexp" |
| "strings" |
|
|
| "github.com/Wei-Shaw/sub2api/internal/domain" |
| "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity" |
| "github.com/Wei-Shaw/sub2api/internal/pkg/gemini" |
| "github.com/Wei-Shaw/sub2api/internal/pkg/googleapi" |
| pkghttputil "github.com/Wei-Shaw/sub2api/internal/pkg/httputil" |
| "github.com/Wei-Shaw/sub2api/internal/pkg/ip" |
| "github.com/Wei-Shaw/sub2api/internal/pkg/logger" |
| "github.com/Wei-Shaw/sub2api/internal/server/middleware" |
| "github.com/Wei-Shaw/sub2api/internal/service" |
| "github.com/google/uuid" |
|
|
| "github.com/gin-gonic/gin" |
| "go.uber.org/zap" |
| ) |
|
|
| |
| |
| var geminiCLITmpDirRegex = regexp.MustCompile(`/\.gemini/tmp/([A-Fa-f0-9]{64})`) |
|
|
| |
| |
| func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) { |
| apiKey, ok := middleware.GetAPIKeyFromContext(c) |
| if !ok || apiKey == nil { |
| googleError(c, http.StatusUnauthorized, "Invalid API key") |
| return |
| } |
| |
| forcePlatform, hasForcePlatform := middleware.GetForcePlatformFromContext(c) |
| if !hasForcePlatform && (apiKey.Group == nil || apiKey.Group.Platform != service.PlatformGemini) { |
| googleError(c, http.StatusBadRequest, "API key group platform is not gemini") |
| return |
| } |
|
|
| |
| if forcePlatform == service.PlatformAntigravity { |
| c.JSON(http.StatusOK, antigravity.FallbackGeminiModelsList()) |
| return |
| } |
|
|
| account, err := h.geminiCompatService.SelectAccountForAIStudioEndpoints(c.Request.Context(), apiKey.GroupID) |
| if err != nil { |
| |
| hasAntigravity, _ := h.geminiCompatService.HasAntigravityAccounts(c.Request.Context(), apiKey.GroupID) |
| if hasAntigravity { |
| |
| c.JSON(http.StatusOK, gemini.FallbackModelsList()) |
| return |
| } |
| googleError(c, http.StatusServiceUnavailable, "No available Gemini accounts: "+err.Error()) |
| return |
| } |
|
|
| res, err := h.geminiCompatService.ForwardAIStudioGET(c.Request.Context(), account, "/v1beta/models") |
| if err != nil { |
| googleError(c, http.StatusBadGateway, err.Error()) |
| return |
| } |
| if shouldFallbackGeminiModels(res) { |
| c.JSON(http.StatusOK, gemini.FallbackModelsList()) |
| return |
| } |
| writeUpstreamResponse(c, res) |
| } |
|
|
| |
| |
| func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) { |
| apiKey, ok := middleware.GetAPIKeyFromContext(c) |
| if !ok || apiKey == nil { |
| googleError(c, http.StatusUnauthorized, "Invalid API key") |
| return |
| } |
| |
| forcePlatform, hasForcePlatform := middleware.GetForcePlatformFromContext(c) |
| if !hasForcePlatform && (apiKey.Group == nil || apiKey.Group.Platform != service.PlatformGemini) { |
| googleError(c, http.StatusBadRequest, "API key group platform is not gemini") |
| return |
| } |
|
|
| modelName := strings.TrimSpace(c.Param("model")) |
| if modelName == "" { |
| googleError(c, http.StatusBadRequest, "Missing model in URL") |
| return |
| } |
|
|
| |
| if forcePlatform == service.PlatformAntigravity { |
| c.JSON(http.StatusOK, antigravity.FallbackGeminiModel(modelName)) |
| return |
| } |
|
|
| account, err := h.geminiCompatService.SelectAccountForAIStudioEndpoints(c.Request.Context(), apiKey.GroupID) |
| if err != nil { |
| |
| hasAntigravity, _ := h.geminiCompatService.HasAntigravityAccounts(c.Request.Context(), apiKey.GroupID) |
| if hasAntigravity { |
| |
| c.JSON(http.StatusOK, gemini.FallbackModel(modelName)) |
| return |
| } |
| googleError(c, http.StatusServiceUnavailable, "No available Gemini accounts: "+err.Error()) |
| return |
| } |
|
|
| res, err := h.geminiCompatService.ForwardAIStudioGET(c.Request.Context(), account, "/v1beta/models/"+modelName) |
| if err != nil { |
| googleError(c, http.StatusBadGateway, err.Error()) |
| return |
| } |
| if shouldFallbackGeminiModels(res) { |
| c.JSON(http.StatusOK, gemini.FallbackModel(modelName)) |
| return |
| } |
| writeUpstreamResponse(c, res) |
| } |
|
|
| |
| |
| |
| func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) { |
| apiKey, ok := middleware.GetAPIKeyFromContext(c) |
| if !ok || apiKey == nil { |
| googleError(c, http.StatusUnauthorized, "Invalid API key") |
| return |
| } |
| authSubject, ok := middleware.GetAuthSubjectFromContext(c) |
| if !ok { |
| googleError(c, http.StatusInternalServerError, "User context not found") |
| return |
| } |
| reqLog := requestLogger( |
| c, |
| "handler.gemini_v1beta.models", |
| zap.Int64("user_id", authSubject.UserID), |
| zap.Int64("api_key_id", apiKey.ID), |
| zap.Any("group_id", apiKey.GroupID), |
| ) |
|
|
| |
| if !middleware.HasForcePlatform(c) { |
| if apiKey.Group == nil || apiKey.Group.Platform != service.PlatformGemini { |
| googleError(c, http.StatusBadRequest, "API key group platform is not gemini") |
| return |
| } |
| } |
|
|
| modelName, action, err := parseGeminiModelAction(strings.TrimPrefix(c.Param("modelAction"), "/")) |
| if err != nil { |
| googleError(c, http.StatusNotFound, err.Error()) |
| return |
| } |
|
|
| stream := action == "streamGenerateContent" |
| reqLog = reqLog.With(zap.String("model", modelName), zap.String("action", action), zap.Bool("stream", stream)) |
|
|
| body, err := pkghttputil.ReadRequestBodyWithPrealloc(c.Request) |
| if err != nil { |
| if maxErr, ok := extractMaxBytesError(err); ok { |
| googleError(c, http.StatusRequestEntityTooLarge, buildBodyTooLargeMessage(maxErr.Limit)) |
| return |
| } |
| googleError(c, http.StatusBadRequest, "Failed to read request body") |
| return |
| } |
| if len(body) == 0 { |
| googleError(c, http.StatusBadRequest, "Request body is empty") |
| return |
| } |
|
|
| setOpsRequestContext(c, modelName, stream, body) |
|
|
| |
| subscription, _ := middleware.GetSubscriptionFromContext(c) |
|
|
| |
| geminiConcurrency := NewConcurrencyHelper(h.concurrencyHelper.concurrencyService, SSEPingFormatNone, 0) |
|
|
| |
| maxWait := service.CalculateMaxWait(authSubject.Concurrency) |
| canWait, err := geminiConcurrency.IncrementWaitCount(c.Request.Context(), authSubject.UserID, maxWait) |
| waitCounted := false |
| if err != nil { |
| reqLog.Warn("gemini.user_wait_counter_increment_failed", zap.Error(err)) |
| } else if !canWait { |
| reqLog.Info("gemini.user_wait_queue_full", zap.Int("max_wait", maxWait)) |
| googleError(c, http.StatusTooManyRequests, "Too many pending requests, please retry later") |
| return |
| } |
| if err == nil && canWait { |
| waitCounted = true |
| } |
| defer func() { |
| if waitCounted { |
| geminiConcurrency.DecrementWaitCount(c.Request.Context(), authSubject.UserID) |
| } |
| }() |
|
|
| |
| streamStarted := false |
| if h.errorPassthroughService != nil { |
| service.BindErrorPassthroughService(c, h.errorPassthroughService) |
| } |
| userReleaseFunc, err := geminiConcurrency.AcquireUserSlotWithWait(c, authSubject.UserID, authSubject.Concurrency, stream, &streamStarted) |
| if err != nil { |
| reqLog.Warn("gemini.user_slot_acquire_failed", zap.Error(err)) |
| googleError(c, http.StatusTooManyRequests, err.Error()) |
| return |
| } |
| if waitCounted { |
| geminiConcurrency.DecrementWaitCount(c.Request.Context(), authSubject.UserID) |
| waitCounted = false |
| } |
| |
| userReleaseFunc = wrapReleaseOnDone(c.Request.Context(), userReleaseFunc) |
| if userReleaseFunc != nil { |
| defer userReleaseFunc() |
| } |
|
|
| |
| if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription); err != nil { |
| reqLog.Info("gemini.billing_eligibility_check_failed", zap.Error(err)) |
| status, _, message := billingErrorDetails(err) |
| googleError(c, status, message) |
| return |
| } |
|
|
| |
| |
| sessionHash := extractGeminiCLISessionHash(c, body) |
| if sessionHash == "" { |
| |
| parsedReq, _ := service.ParseGatewayRequest(body, domain.PlatformGemini) |
| if parsedReq != nil { |
| parsedReq.SessionContext = &service.SessionContext{ |
| ClientIP: ip.GetClientIP(c), |
| UserAgent: c.GetHeader("User-Agent"), |
| APIKeyID: apiKey.ID, |
| } |
| } |
| sessionHash = h.gatewayService.GenerateSessionHash(parsedReq) |
| } |
| sessionKey := sessionHash |
| if sessionHash != "" { |
| sessionKey = "gemini:" + sessionHash |
| } |
|
|
| |
| var sessionBoundAccountID int64 |
| if sessionKey != "" { |
| sessionBoundAccountID, _ = h.gatewayService.GetCachedSessionAccountID(c.Request.Context(), apiKey.GroupID, sessionKey) |
| if sessionBoundAccountID > 0 { |
| prefetchedGroupID := int64(0) |
| if apiKey.GroupID != nil { |
| prefetchedGroupID = *apiKey.GroupID |
| } |
| ctx := service.WithPrefetchedStickySession(c.Request.Context(), sessionBoundAccountID, prefetchedGroupID, h.metadataBridgeEnabled()) |
| c.Request = c.Request.WithContext(ctx) |
| } |
| } |
|
|
| |
| |
| var geminiDigestChain string |
| var geminiPrefixHash string |
| var geminiSessionUUID string |
| var matchedDigestChain string |
| useDigestFallback := sessionBoundAccountID == 0 |
|
|
| if useDigestFallback { |
| |
| var geminiReq antigravity.GeminiRequest |
| if err := json.Unmarshal(body, &geminiReq); err == nil && len(geminiReq.Contents) > 0 { |
| |
| geminiDigestChain = service.BuildGeminiDigestChain(&geminiReq) |
| if geminiDigestChain != "" { |
| |
| userAgent := c.GetHeader("User-Agent") |
| clientIP := ip.GetClientIP(c) |
| platform := "" |
| if apiKey.Group != nil { |
| platform = apiKey.Group.Platform |
| } |
| geminiPrefixHash = service.GenerateGeminiPrefixHash( |
| authSubject.UserID, |
| apiKey.ID, |
| clientIP, |
| userAgent, |
| platform, |
| modelName, |
| ) |
|
|
| |
| foundUUID, foundAccountID, foundMatchedChain, found := h.gatewayService.FindGeminiSession( |
| c.Request.Context(), |
| derefGroupID(apiKey.GroupID), |
| geminiPrefixHash, |
| geminiDigestChain, |
| ) |
| if found { |
| matchedDigestChain = foundMatchedChain |
| sessionBoundAccountID = foundAccountID |
| geminiSessionUUID = foundUUID |
| reqLog.Info("gemini.digest_fallback_matched", |
| zap.String("session_uuid_prefix", safeShortPrefix(foundUUID, 8)), |
| zap.Int64("account_id", foundAccountID), |
| zap.String("digest_chain", truncateDigestChain(geminiDigestChain)), |
| ) |
|
|
| |
| |
| if sessionKey == "" { |
| sessionKey = service.GenerateGeminiDigestSessionKey(geminiPrefixHash, foundUUID) |
| } |
| _ = h.gatewayService.BindStickySession(c.Request.Context(), apiKey.GroupID, sessionKey, foundAccountID) |
| } else { |
| |
| geminiSessionUUID = uuid.New().String() |
| |
| if sessionKey == "" { |
| sessionKey = service.GenerateGeminiDigestSessionKey(geminiPrefixHash, geminiSessionUUID) |
| } |
| } |
| } |
| } |
| } |
|
|
| |
| hasBoundSession := sessionKey != "" && sessionBoundAccountID > 0 |
| cleanedForUnknownBinding := false |
|
|
| fs := NewFailoverState(h.maxAccountSwitchesGemini, hasBoundSession) |
|
|
| |
| |
| if h.gatewayService.IsSingleAntigravityAccountGroup(c.Request.Context(), apiKey.GroupID) { |
| ctx := service.WithSingleAccountRetry(c.Request.Context(), true, h.metadataBridgeEnabled()) |
| c.Request = c.Request.WithContext(ctx) |
| } |
|
|
| for { |
| selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionKey, modelName, fs.FailedAccountIDs, "") |
| if err != nil { |
| if len(fs.FailedAccountIDs) == 0 { |
| googleError(c, http.StatusServiceUnavailable, "No available Gemini accounts: "+err.Error()) |
| return |
| } |
| action := fs.HandleSelectionExhausted(c.Request.Context()) |
| switch action { |
| case FailoverContinue: |
| ctx := service.WithSingleAccountRetry(c.Request.Context(), true, h.metadataBridgeEnabled()) |
| c.Request = c.Request.WithContext(ctx) |
| continue |
| case FailoverCanceled: |
| return |
| default: |
| h.handleGeminiFailoverExhausted(c, fs.LastFailoverErr) |
| return |
| } |
| } |
| account := selection.Account |
| setOpsSelectedAccount(c, account.ID, account.Platform) |
|
|
| |
| |
| if sessionBoundAccountID > 0 && sessionBoundAccountID != account.ID { |
| reqLog.Info("gemini.sticky_session_account_switched", |
| zap.Int64("from_account_id", sessionBoundAccountID), |
| zap.Int64("to_account_id", account.ID), |
| zap.Bool("clean_thought_signature", true), |
| ) |
| body = service.CleanGeminiNativeThoughtSignatures(body) |
| sessionBoundAccountID = account.ID |
| } else if sessionKey != "" && sessionBoundAccountID == 0 && !cleanedForUnknownBinding && bytes.Contains(body, []byte(`"thoughtSignature"`)) { |
| |
| |
| reqLog.Info("gemini.sticky_session_binding_missing", |
| zap.Bool("clean_thought_signature", true), |
| ) |
| body = service.CleanGeminiNativeThoughtSignatures(body) |
| cleanedForUnknownBinding = true |
| sessionBoundAccountID = account.ID |
| } else if sessionBoundAccountID == 0 { |
| |
| sessionBoundAccountID = account.ID |
| } |
|
|
| |
| accountReleaseFunc := selection.ReleaseFunc |
| if !selection.Acquired { |
| if selection.WaitPlan == nil { |
| googleError(c, http.StatusServiceUnavailable, "No available Gemini accounts") |
| return |
| } |
| accountWaitCounted := false |
| canWait, err := geminiConcurrency.IncrementAccountWaitCount(c.Request.Context(), account.ID, selection.WaitPlan.MaxWaiting) |
| if err != nil { |
| reqLog.Warn("gemini.account_wait_counter_increment_failed", zap.Int64("account_id", account.ID), zap.Error(err)) |
| } else if !canWait { |
| reqLog.Info("gemini.account_wait_queue_full", |
| zap.Int64("account_id", account.ID), |
| zap.Int("max_waiting", selection.WaitPlan.MaxWaiting), |
| ) |
| googleError(c, http.StatusTooManyRequests, "Too many pending requests, please retry later") |
| return |
| } |
| if err == nil && canWait { |
| accountWaitCounted = true |
| } |
| defer func() { |
| if accountWaitCounted { |
| geminiConcurrency.DecrementAccountWaitCount(c.Request.Context(), account.ID) |
| } |
| }() |
|
|
| accountReleaseFunc, err = geminiConcurrency.AcquireAccountSlotWithWaitTimeout( |
| c, |
| account.ID, |
| selection.WaitPlan.MaxConcurrency, |
| selection.WaitPlan.Timeout, |
| stream, |
| &streamStarted, |
| ) |
| if err != nil { |
| reqLog.Warn("gemini.account_slot_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err)) |
| googleError(c, http.StatusTooManyRequests, err.Error()) |
| return |
| } |
| if accountWaitCounted { |
| geminiConcurrency.DecrementAccountWaitCount(c.Request.Context(), account.ID) |
| accountWaitCounted = false |
| } |
| if err := h.gatewayService.BindStickySession(c.Request.Context(), apiKey.GroupID, sessionKey, account.ID); err != nil { |
| reqLog.Warn("gemini.bind_sticky_session_failed", zap.Int64("account_id", account.ID), zap.Error(err)) |
| } |
| } |
| |
| accountReleaseFunc = wrapReleaseOnDone(c.Request.Context(), accountReleaseFunc) |
|
|
| |
| var result *service.ForwardResult |
| requestCtx := c.Request.Context() |
| if fs.SwitchCount > 0 { |
| requestCtx = service.WithAccountSwitchCount(requestCtx, fs.SwitchCount, h.metadataBridgeEnabled()) |
| } |
| if account.Platform == service.PlatformAntigravity && account.Type != service.AccountTypeAPIKey { |
| result, err = h.antigravityGatewayService.ForwardGemini(requestCtx, c, account, modelName, action, stream, body, hasBoundSession) |
| } else { |
| result, err = h.geminiCompatService.ForwardNative(requestCtx, c, account, modelName, action, stream, body) |
| } |
| if accountReleaseFunc != nil { |
| accountReleaseFunc() |
| } |
| if err != nil { |
| var failoverErr *service.UpstreamFailoverError |
| if errors.As(err, &failoverErr) { |
| failoverAction := fs.HandleFailoverError(c.Request.Context(), h.gatewayService, account.ID, account.Platform, failoverErr) |
| switch failoverAction { |
| case FailoverContinue: |
| continue |
| case FailoverExhausted: |
| h.handleGeminiFailoverExhausted(c, fs.LastFailoverErr) |
| return |
| case FailoverCanceled: |
| return |
| } |
| } |
| |
| reqLog.Error("gemini.forward_failed", zap.Int64("account_id", account.ID), zap.Error(err)) |
| return |
| } |
|
|
| |
| userAgent := c.GetHeader("User-Agent") |
| clientIP := ip.GetClientIP(c) |
|
|
| |
| if useDigestFallback && geminiDigestChain != "" && geminiPrefixHash != "" { |
| if err := h.gatewayService.SaveGeminiSession( |
| c.Request.Context(), |
| derefGroupID(apiKey.GroupID), |
| geminiPrefixHash, |
| geminiDigestChain, |
| geminiSessionUUID, |
| account.ID, |
| matchedDigestChain, |
| ); err != nil { |
| reqLog.Warn("gemini.digest_session_save_failed", zap.Int64("account_id", account.ID), zap.Error(err)) |
| } |
| } |
|
|
| |
| requestPayloadHash := service.HashUsageRequestPayload(body) |
| inboundEndpoint := GetInboundEndpoint(c) |
| upstreamEndpoint := GetUpstreamEndpoint(c, account.Platform) |
| h.submitUsageRecordTask(func(ctx context.Context) { |
| if err := h.gatewayService.RecordUsageWithLongContext(ctx, &service.RecordUsageLongContextInput{ |
| Result: result, |
| APIKey: apiKey, |
| User: apiKey.User, |
| Account: account, |
| Subscription: subscription, |
| InboundEndpoint: inboundEndpoint, |
| UpstreamEndpoint: upstreamEndpoint, |
| UserAgent: userAgent, |
| IPAddress: clientIP, |
| RequestPayloadHash: requestPayloadHash, |
| LongContextThreshold: 200000, |
| LongContextMultiplier: 2.0, |
| ForceCacheBilling: fs.ForceCacheBilling, |
| APIKeyService: h.apiKeyService, |
| }); err != nil { |
| logger.L().With( |
| zap.String("component", "handler.gemini_v1beta.models"), |
| zap.Int64("user_id", authSubject.UserID), |
| zap.Int64("api_key_id", apiKey.ID), |
| zap.Any("group_id", apiKey.GroupID), |
| zap.String("model", modelName), |
| zap.Int64("account_id", account.ID), |
| ).Error("gemini.record_usage_failed", zap.Error(err)) |
| } |
| }) |
| reqLog.Debug("gemini.request_completed", |
| zap.Int64("account_id", account.ID), |
| zap.Int("switch_count", fs.SwitchCount), |
| ) |
| return |
| } |
| } |
|
|
| func parseGeminiModelAction(rest string) (model string, action string, err error) { |
| rest = strings.TrimSpace(rest) |
| if rest == "" { |
| return "", "", &pathParseError{"missing path"} |
| } |
|
|
| |
| if i := strings.Index(rest, ":"); i > 0 && i < len(rest)-1 { |
| return rest[:i], rest[i+1:], nil |
| } |
|
|
| |
| if i := strings.Index(rest, "/"); i > 0 && i < len(rest)-1 { |
| return rest[:i], rest[i+1:], nil |
| } |
|
|
| return "", "", &pathParseError{"invalid model action path"} |
| } |
|
|
| func (h *GatewayHandler) handleGeminiFailoverExhausted(c *gin.Context, failoverErr *service.UpstreamFailoverError) { |
| if failoverErr == nil { |
| googleError(c, http.StatusBadGateway, "Upstream request failed") |
| return |
| } |
|
|
| statusCode := failoverErr.StatusCode |
| responseBody := failoverErr.ResponseBody |
|
|
| |
| if h.errorPassthroughService != nil && len(responseBody) > 0 { |
| if rule := h.errorPassthroughService.MatchRule(service.PlatformGemini, statusCode, responseBody); rule != nil { |
| |
| respCode := statusCode |
| if !rule.PassthroughCode && rule.ResponseCode != nil { |
| respCode = *rule.ResponseCode |
| } |
|
|
| |
| msg := service.ExtractUpstreamErrorMessage(responseBody) |
| if !rule.PassthroughBody && rule.CustomMessage != nil { |
| msg = *rule.CustomMessage |
| } |
|
|
| if rule.SkipMonitoring { |
| c.Set(service.OpsSkipPassthroughKey, true) |
| } |
|
|
| googleError(c, respCode, msg) |
| return |
| } |
| } |
|
|
| |
| upstreamMsg := service.ExtractUpstreamErrorMessage(responseBody) |
| service.SetOpsUpstreamError(c, statusCode, upstreamMsg, "") |
|
|
| |
| status, message := mapGeminiUpstreamError(statusCode) |
| googleError(c, status, message) |
| } |
|
|
| func mapGeminiUpstreamError(statusCode int) (int, string) { |
| switch statusCode { |
| case 401: |
| return http.StatusBadGateway, "Upstream authentication failed, please contact administrator" |
| case 403: |
| return http.StatusBadGateway, "Upstream access forbidden, please contact administrator" |
| case 429: |
| return http.StatusTooManyRequests, "Upstream rate limit exceeded, please retry later" |
| case 529: |
| return http.StatusServiceUnavailable, "Upstream service overloaded, please retry later" |
| case 500, 502, 503, 504: |
| return http.StatusBadGateway, "Upstream service temporarily unavailable" |
| default: |
| return http.StatusBadGateway, "Upstream request failed" |
| } |
| } |
|
|
| type pathParseError struct{ msg string } |
|
|
| func (e *pathParseError) Error() string { return e.msg } |
|
|
| func googleError(c *gin.Context, status int, message string) { |
| c.JSON(status, gin.H{ |
| "error": gin.H{ |
| "code": status, |
| "message": message, |
| "status": googleapi.HTTPStatusToGoogleStatus(status), |
| }, |
| }) |
| } |
|
|
| func writeUpstreamResponse(c *gin.Context, res *service.UpstreamHTTPResult) { |
| if res == nil { |
| googleError(c, http.StatusBadGateway, "Empty upstream response") |
| return |
| } |
| for k, vv := range res.Headers { |
| |
| if strings.EqualFold(k, "Content-Length") || strings.EqualFold(k, "Transfer-Encoding") || strings.EqualFold(k, "Connection") { |
| continue |
| } |
| for _, v := range vv { |
| c.Writer.Header().Add(k, v) |
| } |
| } |
| contentType := res.Headers.Get("Content-Type") |
| if contentType == "" { |
| contentType = "application/json" |
| } |
| c.Data(res.StatusCode, contentType, res.Body) |
| } |
|
|
| func shouldFallbackGeminiModels(res *service.UpstreamHTTPResult) bool { |
| if res == nil { |
| return true |
| } |
| if res.StatusCode != http.StatusUnauthorized && res.StatusCode != http.StatusForbidden { |
| return false |
| } |
| if strings.Contains(strings.ToLower(res.Headers.Get("Www-Authenticate")), "insufficient_scope") { |
| return true |
| } |
| if strings.Contains(strings.ToLower(string(res.Body)), "insufficient authentication scopes") { |
| return true |
| } |
| if strings.Contains(strings.ToLower(string(res.Body)), "access_token_scope_insufficient") { |
| return true |
| } |
| return false |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func extractGeminiCLISessionHash(c *gin.Context, body []byte) string { |
| |
| match := geminiCLITmpDirRegex.FindSubmatch(body) |
| if len(match) < 2 { |
| return "" |
| } |
| tmpDirHash := string(match[1]) |
|
|
| |
| privilegedUserID := strings.TrimSpace(c.GetHeader("x-gemini-api-privileged-user-id")) |
|
|
| |
| if privilegedUserID != "" { |
| |
| combined := privilegedUserID + ":" + tmpDirHash |
| hash := sha256.Sum256([]byte(combined)) |
| return hex.EncodeToString(hash[:]) |
| } |
|
|
| |
| return tmpDirHash |
| } |
|
|
| |
| func truncateDigestChain(chain string) string { |
| if len(chain) <= 50 { |
| return chain |
| } |
| return chain[:50] + "..." |
| } |
|
|
| |
| |
| func safeShortPrefix(value string, n int) string { |
| if n <= 0 || len(value) <= n { |
| return value |
| } |
| return value[:n] |
| } |
|
|
| |
| func derefGroupID(groupID *int64) int64 { |
| if groupID == nil { |
| return 0 |
| } |
| return *groupID |
| } |
|
|