| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569 |
- package service
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "net/http"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/gin-gonic/gin"
- )
- // ForwardGemini 转发 Gemini 协议请求
- //
- // 限流处理流程:
- //
- // 请求 → antigravityRetryLoop → 预检查(remaining>0? → 切换账号) → 发送上游
- // ├─ 成功 → 正常返回
- // └─ 429/503 → handleSmartRetry
- // ├─ retryDelay >= 7s → 设置模型限流 + 清除粘性绑定 → 切换账号
- // └─ retryDelay < 7s → 等待后重试 1 次
- // ├─ 成功 → 正常返回
- // └─ 失败 → 设置模型限流 + 清除粘性绑定 → 切换账号
- type ForwardGeminiOption func(*forwardGeminiOptions)
- type forwardGeminiOptions struct {
- groupID int64
- sessionHash string
- }
- func WithForwardGeminiSession(groupID int64, sessionHash string) ForwardGeminiOption {
- return func(opts *forwardGeminiOptions) {
- opts.groupID = groupID
- opts.sessionHash = sessionHash
- }
- }
- func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Context, account *Account, originalModel string, action string, stream bool, body []byte, isStickySession bool, options ...ForwardGeminiOption) (*ForwardResult, error) {
- beginUpstreamResponseModelObservation(c)
- startTime := time.Now()
- forwardOpts := forwardGeminiOptions{}
- for _, apply := range options {
- if apply != nil {
- apply(&forwardOpts)
- }
- }
- sessionID := getSessionID(c)
- prefix := logPrefix(sessionID, account.Name)
- if strings.TrimSpace(originalModel) == "" {
- return nil, s.writeGoogleError(c, http.StatusBadRequest, "Missing model in URL")
- }
- if strings.TrimSpace(action) == "" {
- return nil, s.writeGoogleError(c, http.StatusBadRequest, "Missing action in URL")
- }
- if len(body) == 0 {
- return nil, s.writeGoogleError(c, http.StatusBadRequest, "Request body is empty")
- }
- // 解析请求以获取 image_size(用于图片计费)
- imageInputSize := s.extractImageInputSize(body)
- imageSize := normalizeOpenAIImageSizeTier(imageInputSize)
- switch action {
- case "generateContent", "streamGenerateContent":
- // ok
- case "countTokens":
- // 直接返回空值,不透传上游
- c.JSON(http.StatusOK, map[string]any{"totalTokens": 0})
- return &ForwardResult{
- RequestID: "",
- Usage: ClaudeUsage{},
- Model: originalModel,
- Stream: false,
- Duration: time.Since(startTime),
- FirstTokenMs: nil,
- }, nil
- default:
- return nil, s.writeGoogleError(c, http.StatusNotFound, "Unsupported action: "+action)
- }
- mappedModel := s.getMappedModel(account, originalModel)
- if mappedModel == "" {
- MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate)
- return nil, s.writeGoogleError(c, http.StatusForbidden, fmt.Sprintf("model %s not in whitelist", originalModel))
- }
- forwardedModel := mappedModel
- // 获取 access_token
- if s.tokenProvider == nil {
- return nil, s.writeGoogleError(c, http.StatusBadGateway, "Antigravity token provider not configured")
- }
- accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
- if err != nil {
- return nil, &UpstreamFailoverError{
- StatusCode: http.StatusBadGateway,
- ResponseBody: []byte(`{"error":{"message":"Failed to get upstream access token","status":"UNAVAILABLE"}}`),
- }
- }
- projectID, err := resolveAntigravityProjectID(account)
- if err != nil {
- _ = s.writeGoogleError(c, http.StatusBadRequest, err.Error())
- return nil, err
- }
- // 代理 URL
- proxyURL := ""
- if account.ProxyID != nil && account.Proxy != nil {
- proxyURL = account.Proxy.URL()
- }
- // Antigravity 上游要求必须包含身份提示词,注入到请求中
- injectedBody, err := injectIdentityPatchToGeminiRequest(body)
- if err != nil {
- return nil, s.writeGoogleError(c, http.StatusBadRequest, "Invalid request body")
- }
- // 清理 Schema
- if cleanedBody, err := cleanGeminiRequest(injectedBody); err == nil {
- injectedBody = cleanedBody
- logger.LegacyPrintf("service.antigravity_gateway", "[Antigravity] Cleaned request schema in forwarded request for account %s", account.Name)
- } else {
- logger.LegacyPrintf("service.antigravity_gateway", "[Antigravity] Failed to clean schema: %v", err)
- }
- // 包装请求
- wrappedBody, err := s.wrapV1InternalRequest(projectID, mappedModel, injectedBody)
- if err != nil {
- return nil, s.writeGoogleError(c, http.StatusInternalServerError, "Failed to build upstream request")
- }
- // Antigravity 上游只支持流式请求,统一使用 streamGenerateContent
- // 如果客户端请求非流式,在响应处理阶段会收集完整流式响应后返回
- upstreamAction := "streamGenerateContent"
- // 执行带重试的请求
- result, err := s.antigravityRetryLoop(antigravityRetryLoopParams{
- ctx: ctx,
- prefix: prefix,
- account: account,
- proxyURL: proxyURL,
- accessToken: accessToken,
- action: upstreamAction,
- body: wrappedBody,
- c: c,
- httpUpstream: s.httpUpstream,
- settingService: s.settingService,
- accountRepo: s.accountRepo,
- handleError: s.handleUpstreamError,
- requestedModel: originalModel,
- isStickySession: isStickySession, // ForwardGemini 由上层判断粘性会话
- groupID: forwardOpts.groupID,
- sessionHash: forwardOpts.sessionHash,
- })
- if err != nil {
- // 检查是否是账号切换信号,转换为 UpstreamFailoverError 让 Handler 切换账号
- if switchErr, ok := IsAntigravityAccountSwitchError(err); ok {
- return nil, &UpstreamFailoverError{
- StatusCode: http.StatusServiceUnavailable,
- ForceCacheBilling: switchErr.IsStickySession,
- }
- }
- // 区分客户端取消和真正的上游失败,返回更准确的错误消息
- if c.Request.Context().Err() != nil {
- return nil, s.writeGoogleError(c, http.StatusBadGateway, "Client disconnected before upstream response")
- }
- return nil, s.writeGoogleError(c, http.StatusBadGateway, "Upstream request failed after retries")
- }
- resp := result.resp
- defer func() {
- if resp != nil && resp.Body != nil {
- _ = resp.Body.Close()
- }
- }()
- // 处理错误响应
- if resp.StatusCode >= 400 {
- respBody := s.readUpstreamErrorBody(resp)
- contentType := resp.Header.Get("Content-Type")
- // 尽早关闭原始响应体,释放连接;后续逻辑仍可能需要读取 body,因此用内存副本重新包装。
- _ = resp.Body.Close()
- resp.Body = io.NopCloser(bytes.NewReader(respBody))
- // 模型兜底:模型不存在且开启 fallback 时,自动用 fallback 模型重试一次
- if s.settingService != nil && s.settingService.IsModelFallbackEnabled(ctx) &&
- isModelNotFoundError(resp.StatusCode, respBody) {
- fallbackModel := s.settingService.GetFallbackModel(ctx, PlatformAntigravity)
- if fallbackModel != "" && fallbackModel != mappedModel {
- logger.LegacyPrintf("service.antigravity_gateway", "[Antigravity] Model not found (%s), retrying with fallback model %s (account: %s)", mappedModel, fallbackModel, account.Name)
- fallbackWrapped, err := s.wrapV1InternalRequest(projectID, fallbackModel, injectedBody)
- if err == nil {
- fallbackReq, err := antigravity.NewAPIRequest(ctx, upstreamAction, accessToken, fallbackWrapped)
- if err == nil {
- fallbackResp, err := s.httpUpstream.Do(WithAccountTrafficRequest(fallbackReq, account), proxyURL, account.ID, account.Concurrency)
- if err == nil && fallbackResp.StatusCode < 400 {
- _ = resp.Body.Close()
- resp = fallbackResp
- forwardedModel = fallbackModel
- } else if fallbackResp != nil {
- _ = fallbackResp.Body.Close()
- }
- }
- }
- }
- }
- // Gemini 原生请求中的 thoughtSignature 可能来自旧上下文/旧账号,触发上游严格校验后返回
- // "Corrupted thought signature."。检测到此类 400 时,将 thoughtSignature 清理为 dummy 值后重试一次。
- signatureCheckBody := respBody
- if unwrapped, unwrapErr := s.unwrapV1InternalResponse(respBody); unwrapErr == nil && len(unwrapped) > 0 {
- signatureCheckBody = unwrapped
- }
- if resp.StatusCode == http.StatusBadRequest &&
- s.settingService != nil &&
- s.settingService.IsSignatureRectifierEnabled(ctx) &&
- isSignatureRelatedError(signatureCheckBody) &&
- bytes.Contains(injectedBody, []byte(`"thoughtSignature"`)) {
- upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractAntigravityErrorMessage(signatureCheckBody)))
- upstreamDetail := s.getUpstreamErrorDetail(signatureCheckBody)
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: resp.Header.Get("x-request-id"),
- Kind: "signature_error",
- Message: upstreamMsg,
- Detail: upstreamDetail,
- })
- logger.LegacyPrintf("service.antigravity_gateway", "Antigravity Gemini account %d: detected signature-related 400, retrying with cleaned thought signatures", account.ID)
- cleanedInjectedBody := CleanGeminiNativeThoughtSignatures(injectedBody)
- retryWrappedBody, wrapErr := s.wrapV1InternalRequest(projectID, mappedModel, cleanedInjectedBody)
- if wrapErr == nil {
- retryResult, retryErr := s.antigravityRetryLoop(antigravityRetryLoopParams{
- ctx: ctx,
- prefix: prefix,
- account: account,
- proxyURL: proxyURL,
- accessToken: accessToken,
- action: upstreamAction,
- body: retryWrappedBody,
- c: c,
- httpUpstream: s.httpUpstream,
- settingService: s.settingService,
- accountRepo: s.accountRepo,
- handleError: s.handleUpstreamError,
- requestedModel: originalModel,
- isStickySession: isStickySession,
- groupID: forwardOpts.groupID,
- sessionHash: forwardOpts.sessionHash,
- })
- if retryErr == nil {
- retryResp := retryResult.resp
- if retryResp.StatusCode < 400 {
- resp = retryResp
- } else {
- retryRespBody := s.readUpstreamErrorBody(retryResp)
- _ = retryResp.Body.Close()
- retryOpsBody := retryRespBody
- if retryUnwrapped, unwrapErr := s.unwrapV1InternalResponse(retryRespBody); unwrapErr == nil && len(retryUnwrapped) > 0 {
- retryOpsBody = retryUnwrapped
- }
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: retryResp.StatusCode,
- UpstreamRequestID: retryResp.Header.Get("x-request-id"),
- Kind: "signature_retry",
- Message: sanitizeUpstreamErrorMessage(strings.TrimSpace(extractAntigravityErrorMessage(retryOpsBody))),
- Detail: s.getUpstreamErrorDetail(retryOpsBody),
- })
- respBody = retryRespBody
- resp = &http.Response{
- StatusCode: retryResp.StatusCode,
- Header: retryResp.Header.Clone(),
- Body: io.NopCloser(bytes.NewReader(retryRespBody)),
- }
- contentType = resp.Header.Get("Content-Type")
- }
- } else {
- if switchErr, ok := IsAntigravityAccountSwitchError(retryErr); ok {
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: http.StatusServiceUnavailable,
- Kind: "failover",
- Message: sanitizeUpstreamErrorMessage(retryErr.Error()),
- })
- return nil, &UpstreamFailoverError{
- StatusCode: http.StatusServiceUnavailable,
- ForceCacheBilling: switchErr.IsStickySession,
- }
- }
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: 0,
- Kind: "signature_retry_request_error",
- Message: sanitizeUpstreamErrorMessage(retryErr.Error()),
- })
- logger.LegacyPrintf("service.antigravity_gateway", "Antigravity Gemini account %d: signature retry request failed: %v", account.ID, retryErr)
- }
- } else {
- logger.LegacyPrintf("service.antigravity_gateway", "Antigravity Gemini account %d: signature retry wrap failed: %v", account.ID, wrapErr)
- }
- }
- // fallback 成功:继续按正常响应处理
- if resp.StatusCode < 400 {
- goto handleSuccess
- }
- requestID := resp.Header.Get("x-request-id")
- if requestID != "" {
- c.Header("x-request-id", requestID)
- }
- unwrapped, unwrapErr := s.unwrapV1InternalResponse(respBody)
- unwrappedForOps := unwrapped
- if unwrapErr != nil || len(unwrappedForOps) == 0 {
- unwrappedForOps = respBody
- }
- s.handleUpstreamError(ctx, prefix, account, resp.StatusCode, resp.Header, respBody, originalModel, forwardOpts.groupID, forwardOpts.sessionHash, isStickySession)
- upstreamMsg := strings.TrimSpace(extractAntigravityErrorMessage(unwrappedForOps))
- upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
- upstreamDetail := s.getUpstreamErrorDetail(unwrappedForOps)
- // Always record upstream context for Ops error logs, even when we will failover.
- setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
- // 精确匹配服务端配置类 400 错误,触发同账号重试 + failover
- if resp.StatusCode == http.StatusBadRequest && isGoogleProjectConfigError(strings.ToLower(upstreamMsg)) {
- log.Printf("%s status=400 google_config_error failover=true upstream_message=%q account=%d", prefix, upstreamMsg, account.ID)
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: requestID,
- Kind: "failover",
- Message: upstreamMsg,
- Detail: upstreamDetail,
- })
- return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: unwrappedForOps, RetryableOnSameAccount: true}
- }
- if s.shouldFailoverUpstreamError(resp.StatusCode) {
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: requestID,
- Kind: "failover",
- Message: upstreamMsg,
- Detail: upstreamDetail,
- })
- return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: unwrappedForOps}
- }
- if contentType == "" {
- contentType = "application/json"
- }
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- ProxyID: opsUpstreamProxyID(account),
- ProxyName: opsUpstreamProxyName(account),
- Platform: account.Platform,
- AccountID: account.ID,
- AccountName: account.Name,
- UpstreamStatusCode: resp.StatusCode,
- UpstreamRequestID: requestID,
- Kind: "http_error",
- Message: upstreamMsg,
- Detail: upstreamDetail,
- })
- logger.LegacyPrintf("service.antigravity_gateway", "[antigravity-Forward] upstream error status=%d body=%s", resp.StatusCode, truncateForLog(unwrappedForOps, 500))
- MarkResponseCommitted(c)
- c.Data(resp.StatusCode, contentType, unwrappedForOps)
- return nil, fmt.Errorf("antigravity upstream error: %d", resp.StatusCode)
- }
- handleSuccess:
- requestID := resp.Header.Get("x-request-id")
- if requestID != "" {
- c.Header("x-request-id", requestID)
- }
- var usage *ClaudeUsage
- var firstTokenMs *int
- var clientDisconnect bool
- if stream {
- // 客户端要求流式,直接透传
- streamRes, err := s.handleGeminiStreamingResponse(c, resp, startTime)
- if err != nil {
- logger.LegacyPrintf("service.antigravity_gateway", "%s status=stream_error error=%v", prefix, err)
- return nil, err
- }
- usage = streamRes.usage
- firstTokenMs = streamRes.firstTokenMs
- clientDisconnect = streamRes.clientDisconnect
- } else {
- // 客户端要求非流式,收集流式响应后返回
- streamRes, err := s.handleGeminiStreamToNonStreaming(c, resp, startTime)
- if err != nil {
- logger.LegacyPrintf("service.antigravity_gateway", "%s status=stream_collect_error error=%v", prefix, err)
- return nil, err
- }
- usage = streamRes.usage
- firstTokenMs = streamRes.firstTokenMs
- }
- if usage == nil {
- usage = &ClaudeUsage{}
- }
- // 判断是否为图片生成模型
- imageCount := 0
- if isImageGenerationModel(mappedModel) {
- // Gemini 图片生成 API 每次请求只生成一张图片(API 限制)
- imageCount = 1
- }
- return &ForwardResult{
- RequestID: requestID,
- UpstreamHeaders: resp.Header,
- Usage: *usage,
- Model: originalModel,
- UpstreamModel: forwardedModel,
- UpstreamResponseModel: observedUpstreamResponseModel(c),
- UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
- Stream: stream,
- Duration: time.Since(startTime),
- FirstTokenMs: firstTokenMs,
- ClientDisconnect: clientDisconnect,
- ImageCount: imageCount,
- ImageSize: imageSize,
- ImageInputSize: imageInputSize,
- }, nil
- }
- // cleanGeminiRequest 清理 Gemini 请求体中的 Schema
- func cleanGeminiRequest(body []byte) ([]byte, error) {
- var payload map[string]any
- if err := json.Unmarshal(body, &payload); err != nil {
- return nil, err
- }
- modified := false
- // 1. 清理 Tools
- if tools, ok := payload["tools"].([]any); ok && len(tools) > 0 {
- for _, t := range tools {
- toolMap, ok := t.(map[string]any)
- if !ok {
- continue
- }
- // function_declarations (snake_case) or functionDeclarations (camelCase)
- var funcs []any
- if f, ok := toolMap["functionDeclarations"].([]any); ok {
- funcs = f
- } else if f, ok := toolMap["function_declarations"].([]any); ok {
- funcs = f
- }
- if len(funcs) == 0 {
- continue
- }
- for _, f := range funcs {
- funcMap, ok := f.(map[string]any)
- if !ok {
- continue
- }
- if params, ok := funcMap["parameters"].(map[string]any); ok {
- antigravity.DeepCleanUndefined(params)
- cleaned := antigravity.CleanJSONSchema(params)
- funcMap["parameters"] = cleaned
- modified = true
- }
- }
- }
- }
- if !modified {
- return body, nil
- }
- return json.Marshal(payload)
- }
- // filterEmptyPartsFromGeminiRequest 过滤掉 parts 为空的消息
- // Gemini API 不接受空 parts,需要在请求前过滤
- func filterEmptyPartsFromGeminiRequest(body []byte) ([]byte, error) {
- var payload map[string]any
- if err := json.Unmarshal(body, &payload); err != nil {
- return nil, err
- }
- contents, ok := payload["contents"].([]any)
- if !ok || len(contents) == 0 {
- return body, nil
- }
- filtered := make([]any, 0, len(contents))
- modified := false
- for _, c := range contents {
- contentMap, ok := c.(map[string]any)
- if !ok {
- filtered = append(filtered, c)
- continue
- }
- parts, hasParts := contentMap["parts"]
- if !hasParts {
- filtered = append(filtered, c)
- continue
- }
- partsSlice, ok := parts.([]any)
- if !ok {
- filtered = append(filtered, c)
- continue
- }
- // 跳过 parts 为空数组的消息
- if len(partsSlice) == 0 {
- modified = true
- continue
- }
- filtered = append(filtered, c)
- }
- if !modified {
- return body, nil
- }
- payload["contents"] = filtered
- return json.Marshal(payload)
- }
|