2026-10-10 22:21:28 +08:00
package handler
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
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/gin-gonic/gin"
"go.uber.org/zap"
)
// geminiCachedContentCreateMaxAttempts 限制创建缓存时实际发往上游的账号数。
const geminiCachedContentCreateMaxAttempts = 3
// geminiCachedContentPersistTimeout 是上游操作完成后落库的超时;落库与客户端连接脱钩。
const geminiCachedContentPersistTimeout = 15 * time . Second
const geminiCachedContentUnavailableMessage = "The account holding this cached content is temporarily unavailable"
// geminiCachedContentsPreamble 校验显式缓存管理接口的公共前置条件:gemini 分组、非 composite 分组、功能已装配。
func ( h * GatewayHandler ) geminiCachedContentsPreamble ( c * gin . Context , component string ) ( * service . APIKey , middleware . AuthSubject , * zap . Logger , bool ) {
apiKey , ok := middleware . GetAPIKeyFromContext ( c )
if ! ok || apiKey == nil {
googleError ( c , http . StatusUnauthorized , "Invalid API key" )
return nil , middleware . AuthSubject {}, nil , false
}
authSubject , ok := middleware . GetAuthSubjectFromContext ( c )
if ! ok {
googleError ( c , http . StatusInternalServerError , "User context not found" )
return nil , middleware . AuthSubject {}, nil , false
}
if h . geminiCachedContentService == nil {
2026-10-10 22:09:14 +08:00
googlePlatformError ( c , http . StatusNotFound , "Explicit context caching is not enabled" )
2026-10-10 22:21:28 +08:00
return nil , middleware . AuthSubject {}, nil , false
}
if apiKey . Group == nil || apiKey . GroupID == nil || apiKey . Group . Platform == service . PlatformComposite {
googleError ( c , http . StatusBadRequest , "Explicit context caching is not supported for this API key group" )
return nil , middleware . AuthSubject {}, nil , false
}
if effectiveAPIKeyPlatform ( c , apiKey ) != service . PlatformGemini {
googleError ( c , http . StatusBadRequest , "API key group platform is not gemini" )
return nil , middleware . AuthSubject {}, nil , false
}
reqLog := requestLogger (
c ,
component ,
zap . Int64 ( "user_id" , authSubject . UserID ),
zap . Int64 ( "api_key_id" , apiKey . ID ),
zap . Any ( "group_id" , apiKey . GroupID ),
)
return apiKey , authSubject , reqLog , true
}
func readGeminiCachedContentBody ( c * gin . Context ) ([] byte , bool ) {
body , err := pkghttputil . ReadRequestBodyWithPrealloc ( c . Request )
if err != nil {
if maxErr , ok := extractMaxBytesError ( err ); ok {
googleError ( c , http . StatusRequestEntityTooLarge , buildBodyTooLargeMessage ( maxErr . Limit ))
return nil , false
}
googleError ( c , http . StatusBadRequest , "Failed to read request body" )
return nil , false
}
if len ( body ) == 0 {
googleError ( c , http . StatusBadRequest , "Request body is empty" )
return nil , false
}
return body , true
}
func ( h * GatewayHandler ) checkGeminiCachedContentBilling ( c * gin . Context , reqLog * zap . Logger , apiKey * service . APIKey , subscription * service . UserSubscription ) bool {
if err := h . billingCacheService . CheckBillingEligibility ( c . Request . Context (), apiKey . User , apiKey , apiKey . Group , subscription , service . QuotaPlatform ( c . Request . Context (), apiKey )); err != nil {
reqLog . Info ( "gemini.cached_content.billing_eligibility_check_failed" , zap . Error ( err ))
2026-10-10 22:09:14 +08:00
status , code , message , retryAfter := billingErrorDetails ( err )
2026-10-10 22:21:28 +08:00
if retryAfter > 0 {
c . Header ( "Retry-After" , strconv . Itoa ( retryAfter ))
}
2026-10-10 22:09:14 +08:00
googleErrorWithType ( c , status , code , "" , message )
2026-10-10 22:21:28 +08:00
return false
}
return true
}
// GeminiCachedContentsCreate POST /v1beta/cachedContents
// 创建请求内一次性收取缓存 token 的输入费与整个有效期的存储费。
func ( h * GatewayHandler ) GeminiCachedContentsCreate ( c * gin . Context ) {
apiKey , authSubject , reqLog , ok := h . geminiCachedContentsPreamble ( c , "handler.gemini_v1beta.cached_contents.create" )
if ! ok {
return
}
svc := h . geminiCachedContentService
body , ok := readGeminiCachedContentBody ( c )
if ! ok {
return
}
req , err := service . ParseGeminiCachedContentCreateRequest ( body , svc . Now (), svc . MaxTTL ())
if err != nil {
googleError ( c , http . StatusBadRequest , err . Error ())
return
}
modelName := req . Model
reqLog = reqLog . With ( zap . String ( "model" , modelName ))
setOpsRequestContext ( c , modelName , false )
setOpsEndpointContext ( c , "" , int16 ( service . RequestTypeFromLegacy ( false , false )))
pricingCtx , pricingAt := service . WithGatewayTokenRequestPricing ( c . Request . Context ())
c . Request = c . Request . WithContext ( pricingCtx )
if decision := h . checkSecurityAudit ( c , reqLog , apiKey , authSubject , service . ContentModerationProtocolGemini , modelName , body ); decision != nil && ! decision . AllowNextStage {
googleSecurityAuditError ( c , decision )
return
}
channelMapping , _ := h . gatewayService . ResolveChannelMappingAndRestrict ( c . Request . Context (), apiKey . GroupID , modelName )
reqModel := modelName
if channelMapping . Mapped {
modelName = channelMapping . MappedModel
}
subscription , _ := middleware . GetSubscriptionFromContext ( c )
geminiConcurrency := NewConcurrencyHelper ( h . concurrencyHelper . concurrencyService , SSEPingFormatNone , 0 )
streamStarted := false
userReleaseFunc , err := geminiConcurrency . AcquireUserSlotWithWait ( c , authSubject . UserID , authSubject . Concurrency , false , & streamStarted )
if err != nil {
reqLog . Warn ( "gemini.cached_content.user_slot_acquire_failed" , zap . Error ( err ))
googleConcurrencyError ( c , err , "user" )
return
}
userReleaseFunc = wrapReleaseOnDone ( c . Request . Context (), userReleaseFunc )
if userReleaseFunc != nil {
defer userReleaseFunc ()
}
if ! h . checkGeminiCachedContentBilling ( c , reqLog , apiKey , subscription ) {
return
}
inflightRelease , err := reserveInflightBalance ( c , h . billingCacheService , h . gatewayService , apiKey , subscription , tokenInflightEstimate ( modelName , body ))
if err != nil {
reqLog . Info ( "gemini.cached_content.inflight_reservation_rejected" , zap . Error ( err ))
2026-10-10 22:09:14 +08:00
status , code , message , retryAfter := billingErrorDetails ( err )
2026-10-10 22:21:28 +08:00
if retryAfter > 0 {
c . Header ( "Retry-After" , strconv . Itoa ( retryAfter ))
}
2026-10-10 22:09:14 +08:00
googleErrorWithType ( c , status , code , "" , message )
2026-10-10 22:21:28 +08:00
return
}
defer inflightRelease ()
excluded , err := h . gatewayService . GeminiExplicitCacheExclusions ( c . Request . Context (), apiKey . GroupID , 0 )
if err != nil {
reqLog . Warn ( "gemini.cached_content.list_accounts_failed" , zap . Error ( err ))
googleError ( c , http . StatusServiceUnavailable , "No available Gemini accounts support explicit context caching" )
return
}
var lastUpstreamErr * service . GeminiCachedContentUpstreamError
var ttlRejectedLimit time . Duration
for attempts := 0 ; attempts < geminiCachedContentCreateMaxAttempts ; {
selection , err := h . gatewayService . SelectAccountWithLoadAwareness ( c . Request . Context (), apiKey . GroupID , "" , modelName , excluded , "" , int64 ( 0 ))
if err != nil {
if failoverClientGone ( c ) {
return
}
reqLog . Info ( "gemini.cached_content.account_select_exhausted" , zap . Error ( err ))
break
}
account := selection . Account
if ! service . IsGeminiExplicitCacheAccount ( account ) {
if selection . Acquired && selection . ReleaseFunc != nil {
selection . ReleaseFunc ()
}
excluded [ account . ID ] = struct {}{}
continue
}
ttl , ttlAllowed := req . TTLForAccount ( account )
if ! ttlAllowed {
if selection . Acquired && selection . ReleaseFunc != nil {
selection . ReleaseFunc ()
}
excluded [ account . ID ] = struct {}{}
ttlRejectedLimit = max ( ttlRejectedLimit , account . GeminiExplicitCacheUpstreamMaxTTL ())
continue
}
accountReleaseFunc , ok := h . acquireGeminiCachedContentAccountSlot ( c , reqLog , geminiConcurrency , selection )
if ! ok {
return
}
admissionCtx := service . ContextWithSelectionProfitGate ( c . Request . Context (), selection )
latest , vetoed , reason := h . gatewayService . GatewayProfitControlVetoLatest ( admissionCtx , account )
if vetoed {
accountReleaseFunc ()
reqLog . Debug ( "gemini.cached_content.account_profit_vetoed" , zap . Int64 ( "account_id" , account . ID ), zap . String ( "reason" , reason ))
excluded [ account . ID ] = struct {}{}
continue
}
account = latest
attempts ++
setOpsSelectedAccount ( c , account . ID , account . Platform )
mappedModel := account . GetMappedModel ( modelName )
usageFields := clientRequestedUsageFields ( c , channelMapping , reqModel , mappedModel )
if ! h . gatewayService . CacheStoragePriced ( c . Request . Context (), & service . RecordUsageInput {
Result : & service . ForwardResult { Model : modelName , UpstreamModel : mappedModel , CacheStorageTokenHours : 1 },
APIKey : apiKey ,
ChannelUsageFields : usageFields ,
}) {
accountReleaseFunc ()
2026-10-10 22:09:14 +08:00
googlePlatformError ( c , http . StatusBadRequest , fmt . Sprintf ( "Explicit context caching is not available for model %s: storage pricing is not configured" , modelName ))
2026-10-10 22:21:28 +08:00
return
}
upstreamModel , err := service . GeminiCachedContentUpstreamModelResource ( account , mappedModel )
if err != nil {
accountReleaseFunc ()
reqLog . Warn ( "gemini.cached_content.upstream_model_invalid" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
excluded [ account . ID ] = struct {}{}
continue
}
publicID , err := service . NewGeminiCachedContentPublicID ()
if err != nil {
accountReleaseFunc ()
googleError ( c , http . StatusInternalServerError , "Failed to allocate cached content id" )
return
}
upstream , err := svc . CreateUpstream ( c . Request . Context (), account , req , mappedModel , publicID , ttl )
accountReleaseFunc ()
if err != nil {
var upErr * service . GeminiCachedContentUpstreamError
if errors . As ( err , & upErr ) {
reqLog . Info ( "gemini.cached_content.upstream_create_failed" , zap . Int64 ( "account_id" , account . ID ), zap . Int ( "status" , upErr . StatusCode ))
if upErr . RetryableOnOtherAccount () {
lastUpstreamErr = upErr
excluded [ account . ID ] = struct {}{}
continue
}
2026-10-11 01:37:04 +08:00
h . writeGeminiCachedContentUpstreamError ( c , upErr )
2026-10-10 22:21:28 +08:00
return
}
// 上游已返回成功但响应不可用:不换号重建,能识别出资源名时删除上游缓存。
var malformed * service . GeminiCachedContentMalformedResponseError
if errors . As ( err , & malformed ) {
reqLog . Error ( "gemini.cached_content.upstream_create_malformed" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
if malformed . Name != "" {
h . cleanupGeminiCachedContentUpstream ( reqLog , account , malformed . Name )
}
googleError ( c , http . StatusBadGateway , "Upstream returned an invalid cached content response" )
return
}
if failoverClientGone ( c ) {
return
}
reqLog . Warn ( "gemini.cached_content.upstream_create_error" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
excluded [ account . ID ] = struct {}{}
continue
}
// 缓存 token 数在上游创建后才确定:按确定金额校验余额 / 额度并预留,不足时删除上游缓存,
// 只收取已发生的缓存 token 输入费。
tokens := upstream . TotalTokenCount
storageTokenHours := service . GeminiCachedContentStorageTokenHours ( tokens , ttl )
amount := h . gatewayService . QuoteUsageCost ( c . Request . Context (), & service . RecordUsageInput {
Result : & service . ForwardResult {
Model : modelName , UpstreamModel : mappedModel ,
Usage : service . ClaudeUsage { InputTokens : int ( tokens )}, CacheStorageTokenHours : storageTokenHours ,
},
APIKey : apiKey , User : apiKey . User , Subscription : subscription , PricingAt : pricingAt , ChannelUsageFields : usageFields ,
})
inflightRelease ()
chargeDone , err := h . reserveGeminiCachedContentCharge ( c , apiKey , subscription , amount )
if err != nil {
reqLog . Info ( "gemini.cached_content.storage_charge_rejected" , zap . Float64 ( "amount" , amount ), zap . Error ( err ))
h . cleanupGeminiCachedContentUpstream ( reqLog , account , upstream . Name )
h . submitGeminiCachedContentUsage ( c , apiKey , account , subscription , pricingAt ,
2026-10-11 01:37:04 +08:00
service . GeminiCachedContentCreateUsageRequestID ( publicID ), modelName , mappedModel , int ( tokens ), 0 , usageFields , body )
2026-10-10 22:21:28 +08:00
writeGeminiCachedContentBillingError ( c , err )
return
}
defer chargeDone ()
expireTime := service . ClampGeminiCachedContentExpire ( upstream . ExpireTime , svc . Now (). Add ( ttl ))
record := & service . GeminiCachedContent {
PublicID : publicID ,
UserID : authSubject . UserID ,
APIKeyID : apiKey . ID ,
GroupID : * apiKey . GroupID ,
AccountID : account . ID ,
UpstreamName : upstream . Name ,
Model : modelName ,
UpstreamModel : upstreamModel ,
DisplayName : req . DisplayName ,
TotalTokenCount : tokens ,
ExpireTime : expireTime ,
2026-10-11 01:37:04 +08:00
ChannelUsage : usageFields ,
2026-10-10 22:21:28 +08:00
}
persistCtx , cancelPersist := geminiCachedContentPersistContext ( c )
err = svc . Save ( persistCtx , record )
cancelPersist ()
if err != nil {
reqLog . Error ( "gemini.cached_content.save_failed" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
h . cleanupGeminiCachedContentUpstream ( reqLog , account , record . UpstreamName )
googleError ( c , http . StatusInternalServerError , "Failed to persist cached content" )
return
}
h . submitGeminiCachedContentUsage ( c , apiKey , account , subscription , pricingAt ,
2026-10-11 01:37:04 +08:00
service . GeminiCachedContentCreateUsageRequestID ( publicID ), modelName , mappedModel , int ( tokens ), storageTokenHours , usageFields , body )
2026-10-10 22:21:28 +08:00
c . JSON ( http . StatusOK , service . GeminiCachedContentView ( record ))
return
}
if lastUpstreamErr != nil {
2026-10-11 01:37:04 +08:00
h . writeGeminiCachedContentUpstreamError ( c , lastUpstreamErr )
2026-10-10 22:21:28 +08:00
return
}
if ttlRejectedLimit > 0 {
googleError ( c , http . StatusBadRequest , fmt . Sprintf ( "ttl exceeds the maximum allowed %ds by the upstream accounts" , int64 ( ttlRejectedLimit . Seconds ())))
return
}
markOpsRoutingCapacityLimited ( c )
googleError ( c , http . StatusServiceUnavailable , "No available Gemini accounts support explicit context caching" )
}
// GeminiCachedContentsList GET /v1beta/cachedContents
// 只返回当前 API Key 在当前分组下仍有效的缓存,不透传上游 list(同一账号上其他租户的缓存不可见)。
func ( h * GatewayHandler ) GeminiCachedContentsList ( c * gin . Context ) {
apiKey , _ , reqLog , ok := h . geminiCachedContentsPreamble ( c , "handler.gemini_v1beta.cached_contents.list" )
if ! ok {
return
}
pageSize := service . GeminiCachedContentListDefaultPageSize
if raw := strings . TrimSpace ( c . Query ( "pageSize" )); raw != "" {
n , err := strconv . Atoi ( raw )
if err != nil || n < 0 {
googleError ( c , http . StatusBadRequest , "pageSize must be a non-negative integer" )
return
}
if n > 0 {
pageSize = min ( n , service . GeminiCachedContentListMaxPageSize )
}
}
beforeID := int64 ( 0 )
if raw := strings . TrimSpace ( c . Query ( "pageToken" )); raw != "" {
id , ok := decodeGeminiCachedContentPageToken ( raw )
if ! ok {
googleError ( c , http . StatusBadRequest , "Invalid pageToken" )
return
}
beforeID = id
}
records , err := h . geminiCachedContentService . List ( c . Request . Context (), apiKey . ID , * apiKey . GroupID , beforeID , pageSize + 1 )
if err != nil {
reqLog . Error ( "gemini.cached_content.list_failed" , zap . Error ( err ))
googleError ( c , http . StatusInternalServerError , "Failed to list cached contents" )
return
}
resp := gin . H {}
if len ( records ) > pageSize {
records = records [: pageSize ]
resp [ "nextPageToken" ] = encodeGeminiCachedContentPageToken ( records [ len ( records ) - 1 ]. ID )
}
if len ( records ) > 0 {
views := make ([] map [ string ] any , 0 , len ( records ))
for _ , record := range records {
views = append ( views , service . GeminiCachedContentView ( record ))
}
resp [ "cachedContents" ] = views
}
c . JSON ( http . StatusOK , resp )
}
// GeminiCachedContentsGet GET /v1beta/cachedContents/:cacheID
func ( h * GatewayHandler ) GeminiCachedContentsGet ( c * gin . Context ) {
apiKey , _ , reqLog , ok := h . geminiCachedContentsPreamble ( c , "handler.gemini_v1beta.cached_contents.get" )
if ! ok {
return
}
record , ok := h . resolveGeminiCachedContentOrWrite ( c , reqLog , apiKey , c . Param ( "cacheID" ))
if ! ok {
return
}
c . JSON ( http . StatusOK , service . GeminiCachedContentView ( record ))
}
// GeminiCachedContentsPatch PATCH /v1beta/cachedContents/:cacheID
// 只允许修改有效期;延长部分在本次请求内按增量时长收取存储费,缩短不退费。
2026-10-12 17:14:56 +08:00
// 同一缓存的修改串行执行:上游修改、本地写回与计费都在修改意图的租约内完成,并发的修改返回 409。
// 上游结果未知或本地写回失败的修改不计费,租约过期后由恢复流程把上游有效期回滚到修改前的到期时间。
2026-10-10 22:21:28 +08:00
func ( h * GatewayHandler ) GeminiCachedContentsPatch ( c * gin . Context ) {
apiKey , _ , reqLog , ok := h . geminiCachedContentsPreamble ( c , "handler.gemini_v1beta.cached_contents.patch" )
if ! ok {
return
}
svc := h . geminiCachedContentService
body , ok := readGeminiCachedContentBody ( c )
if ! ok {
return
}
record , ok := h . resolveGeminiCachedContentOrWrite ( c , reqLog , apiKey , c . Param ( "cacheID" ))
if ! ok {
return
}
account , err := svc . BoundAccount ( c . Request . Context (), record )
if err != nil {
reqLog . Error ( "gemini.cached_content.load_account_failed" , zap . Int64 ( "account_id" , record . AccountID ), zap . Error ( err ))
googleError ( c , http . StatusInternalServerError , "Failed to load cached content account" )
return
}
if account == nil {
googleError ( c , http . StatusServiceUnavailable , geminiCachedContentUnavailableMessage )
return
}
ttl , err := service . ParseGeminiCachedContentPatchRequest ( body , c . Query ( "updateMask" ), svc . Now (), svc . MaxTTLForAccount ( account ))
if err != nil {
googleError ( c , http . StatusBadRequest , err . Error ())
return
}
setOpsRequestContext ( c , record . Model , false )
pricingCtx , pricingAt := service . WithGatewayTokenRequestPricing ( c . Request . Context ())
c . Request = c . Request . WithContext ( pricingCtx )
subscription , _ := middleware . GetSubscriptionFromContext ( c )
if extension := svc . Now (). Add ( ttl ). Sub ( record . ExpireTime ); extension > 0 {
usage := & service . RecordUsageInput {
Result : & service . ForwardResult {
Model : record . Model , UpstreamModel : record . UpstreamModelID (),
CacheStorageTokenHours : service . GeminiCachedContentStorageTokenHours ( record . TotalTokenCount , extension ),
},
APIKey : apiKey , User : apiKey . User , Subscription : subscription , PricingAt : pricingAt ,
2026-10-11 01:37:04 +08:00
ChannelUsageFields : record . ChannelUsage ,
2026-10-10 22:21:28 +08:00
}
if ! h . gatewayService . CacheStoragePriced ( c . Request . Context (), usage ) {
2026-10-10 22:09:14 +08:00
googlePlatformError ( c , http . StatusBadRequest , fmt . Sprintf ( "Explicit context caching is not available for model %s: storage pricing is not configured" , record . Model ))
2026-10-10 22:21:28 +08:00
return
}
if ! h . checkGeminiCachedContentBilling ( c , reqLog , apiKey , subscription ) {
return
}
chargeDone , err := h . reserveGeminiCachedContentCharge ( c , apiKey , subscription , h . gatewayService . QuoteUsageCost ( c . Request . Context (), usage ))
if err != nil {
reqLog . Info ( "gemini.cached_content.storage_charge_rejected" , zap . Error ( err ))
writeGeminiCachedContentBillingError ( c , err )
return
}
defer chargeDone ()
}
setOpsSelectedAccount ( c , account . ID , account . Platform )
2026-10-12 17:14:56 +08:00
mutation , err := svc . BeginMutation ( c . Request . Context (), record )
if err != nil {
switch {
case errors . Is ( err , service . ErrGeminiCachedContentNotFound ):
googleError ( c , http . StatusForbidden , service . GeminiCachedContentNotFoundMessage )
case errors . Is ( err , service . ErrGeminiCachedContentMutationConflict ):
reqLog . Info ( "gemini.cached_content.mutation_conflict" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
writeGeminiCachedContentMutationConflict ( c )
default :
reqLog . Error ( "gemini.cached_content.begin_mutation_failed" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
googleError ( c , http . StatusInternalServerError , "Failed to update cached content" )
}
return
}
2026-10-10 22:21:28 +08:00
upstream , err := svc . PatchUpstream ( c . Request . Context (), account , record , ttl )
if err != nil {
var upErr * service . GeminiCachedContentUpstreamError
2026-10-12 17:14:56 +08:00
upstreamRejected := errors . As ( err , & upErr )
if upstreamRejected || errors . Is ( err , service . ErrGeminiCachedContentRequestNotSent ) {
// 上游有效期未变:清除修改意图。
persistCtx , cancelPersist := geminiCachedContentPersistContext ( c )
if releaseErr := svc . ReleaseMutation ( persistCtx , mutation ); releaseErr != nil {
reqLog . Warn ( "gemini.cached_content.release_mutation_failed" , zap . Error ( releaseErr ))
}
cancelPersist ()
}
if upstreamRejected {
2026-10-10 22:21:28 +08:00
if upErr . NotFound () {
if forgetErr := svc . Forget ( c . Request . Context (), record ); forgetErr != nil {
reqLog . Warn ( "gemini.cached_content.forget_failed" , zap . Error ( forgetErr ))
}
}
2026-10-11 01:37:04 +08:00
h . writeGeminiCachedContentUpstreamError ( c , upErr )
2026-10-10 22:21:28 +08:00
return
}
reqLog . Warn ( "gemini.cached_content.upstream_patch_error" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
googleError ( c , http . StatusBadGateway , "Upstream request failed" )
return
}
expireTime := service . ClampGeminiCachedContentExpire ( upstream . ExpireTime , svc . Now (). Add ( ttl ))
persistCtx , cancelPersist := geminiCachedContentPersistContext ( c )
2026-10-12 17:14:56 +08:00
err = svc . CommitMutation ( persistCtx , mutation , expireTime )
2026-10-10 22:21:28 +08:00
cancelPersist ()
if err != nil {
2026-10-12 17:14:56 +08:00
reqLog . Error ( "gemini.cached_content.commit_mutation_failed" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
if errors . Is ( err , service . ErrGeminiCachedContentMutationConflict ) {
writeGeminiCachedContentMutationConflict ( c )
2026-10-10 22:21:28 +08:00
return
}
googleError ( c , http . StatusInternalServerError , "Failed to persist cached content expiration" )
return
}
2026-10-12 17:14:56 +08:00
// 提交前库中的到期时间一直是修改前的值:按本次实际推进的时长收费。
if extended := expireTime . Sub ( mutation . Baseline ()); extended > 0 {
2026-10-10 22:21:28 +08:00
h . submitGeminiCachedContentUsage ( c , apiKey , account , subscription , pricingAt ,
2026-10-11 01:37:04 +08:00
service . GeminiCachedContentPatchUsageRequestID ( record . PublicID ), record . Model , record . UpstreamModelID (), 0 ,
service . GeminiCachedContentStorageTokenHours ( record . TotalTokenCount , extended ), record . ChannelUsage , body )
2026-10-10 22:21:28 +08:00
}
c . JSON ( http . StatusOK , service . GeminiCachedContentView ( record ))
}
// GeminiCachedContentsDelete DELETE /v1beta/cachedContents/:cacheID
// 删除不退还已收取的存储费。
func ( h * GatewayHandler ) GeminiCachedContentsDelete ( c * gin . Context ) {
apiKey , _ , reqLog , ok := h . geminiCachedContentsPreamble ( c , "handler.gemini_v1beta.cached_contents.delete" )
if ! ok {
return
}
svc := h . geminiCachedContentService
record , ok := h . resolveGeminiCachedContentOrWrite ( c , reqLog , apiKey , c . Param ( "cacheID" ))
if ! ok {
return
}
account , err := svc . BoundAccount ( c . Request . Context (), record )
if err != nil {
reqLog . Error ( "gemini.cached_content.load_account_failed" , zap . Int64 ( "account_id" , record . AccountID ), zap . Error ( err ))
googleError ( c , http . StatusInternalServerError , "Failed to load cached content account" )
return
}
if account != nil {
setOpsSelectedAccount ( c , account . ID , account . Platform )
if err := svc . DeleteUpstream ( c . Request . Context (), account , record ); err != nil {
var upErr * service . GeminiCachedContentUpstreamError
if ! errors . As ( err , & upErr ) {
reqLog . Warn ( "gemini.cached_content.upstream_delete_error" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
googleError ( c , http . StatusBadGateway , "Upstream request failed" )
return
}
if ! upErr . NotFound () {
2026-10-11 01:37:04 +08:00
h . writeGeminiCachedContentUpstreamError ( c , upErr )
2026-10-10 22:21:28 +08:00
return
}
}
}
persistCtx , cancelPersist := geminiCachedContentPersistContext ( c )
err = svc . Forget ( persistCtx , record )
cancelPersist ()
if err != nil {
reqLog . Error ( "gemini.cached_content.forget_failed" , zap . Error ( err ))
googleError ( c , http . StatusInternalServerError , "Failed to delete cached content" )
return
}
c . JSON ( http . StatusOK , gin . H {})
}
func ( h * GatewayHandler ) resolveGeminiCachedContentOrWrite ( c * gin . Context , reqLog * zap . Logger , apiKey * service . APIKey , cacheID string ) ( * service . GeminiCachedContent , bool ) {
record , err := h . geminiCachedContentService . Resolve ( c . Request . Context (), apiKey . ID , apiKey . GroupID , cacheID )
if err != nil {
if errors . Is ( err , service . ErrGeminiCachedContentNotFound ) {
googleError ( c , http . StatusForbidden , service . GeminiCachedContentNotFoundMessage )
return nil , false
}
reqLog . Error ( "gemini.cached_content.lookup_failed" , zap . Error ( err ))
googleError ( c , http . StatusInternalServerError , "Failed to load cached content" )
return nil , false
}
return record , true
}
// resolveGeminiCachedContentBinding 处理 generateContent / countTokens 请求对显式缓存的引用:
// 校验归属与模型后把引用改写为上游资源名,返回绑定记录;请求未引用缓存时返回 nil。
// 返回 false 表示已写出错误响应。
func ( h * GatewayHandler ) resolveGeminiCachedContentBinding ( c * gin . Context , reqLog * zap . Logger , apiKey * service . APIKey , modelName string , body [] byte ) ( * service . GeminiCachedContent , [] byte , bool ) {
ref , err := service . FindGeminiCachedContentReference ( body )
if err != nil {
googleError ( c , http . StatusBadRequest , err . Error ())
return nil , nil , false
}
if ref == nil {
return nil , body , true
}
if middleware . HasForcePlatform ( c ) || h . geminiCachedContentService == nil ||
apiKey . Group == nil || apiKey . Group . Platform == service . PlatformComposite {
googleError ( c , http . StatusBadRequest , "cachedContent is not supported on this route" )
return nil , nil , false
}
record , err := h . geminiCachedContentService . Resolve ( c . Request . Context (), apiKey . ID , apiKey . GroupID , ref . Name )
if err != nil {
if errors . Is ( err , service . ErrGeminiCachedContentNotFound ) {
googleError ( c , http . StatusForbidden , service . GeminiCachedContentNotFoundMessage )
return nil , nil , false
}
reqLog . Error ( "gemini.cached_content.lookup_failed" , zap . Error ( err ))
googleError ( c , http . StatusInternalServerError , "Failed to load cached content" )
return nil , nil , false
}
if record . Model != modelName {
googleError ( c , http . StatusBadRequest , fmt . Sprintf (
"Model used by GenerateContent request (models/%s) and CachedContent (models/%s) has to be the same." , modelName , record . Model ))
return nil , nil , false
}
rewritten , err := service . RewriteGeminiCachedContentReference ( body , ref , record . UpstreamName )
if err != nil {
googleError ( c , http . StatusBadRequest , "Invalid cachedContent reference" )
return nil , nil , false
}
return record , rewritten , true
}
// acquireGeminiCachedContentAccountSlot 获取选中账号的并发槽(含等待计划);返回 false 表示已写出错误响应。
func ( h * GatewayHandler ) acquireGeminiCachedContentAccountSlot ( c * gin . Context , reqLog * zap . Logger , helper * ConcurrencyHelper , selection * service . AccountSelectionResult ) ( func (), bool ) {
if selection . Acquired {
release := selection . ReleaseFunc
if release == nil {
release = func () {}
}
return wrapReleaseOnDone ( c . Request . Context (), release ), true
}
account := selection . Account
if selection . WaitPlan == nil {
markOpsRoutingCapacityLimited ( c )
googleError ( c , http . StatusServiceUnavailable , "No available Gemini accounts" )
return nil , false
}
canWait , err := helper . IncrementAccountWaitCount ( c . Request . Context (), account . ID , selection . WaitPlan . MaxWaiting )
if err != nil {
reqLog . Warn ( "gemini.cached_content.account_wait_counter_increment_failed" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( err ))
} else if ! canWait {
googleError ( c , http . StatusTooManyRequests , "Too many pending requests, please retry later" )
return nil , false
}
streamStarted := false
release , acquireErr := helper . AcquireAccountSlotWithWaitTimeout ( c , account . ID , selection . WaitPlan . MaxConcurrency , selection . WaitPlan . Timeout , false , & streamStarted )
if err == nil {
helper . DecrementAccountWaitCount ( c . Request . Context (), account . ID )
}
if acquireErr != nil {
reqLog . Warn ( "gemini.cached_content.account_slot_acquire_failed" , zap . Int64 ( "account_id" , account . ID ), zap . Error ( acquireErr ))
googleConcurrencyError ( c , acquireErr , "account" )
return nil , false
}
if release == nil {
release = func () {}
}
return wrapReleaseOnDone ( c . Request . Context (), release ), true
}
2026-10-11 01:37:04 +08:00
// submitGeminiCachedContentUsage 记录缓存创建 / 延长的计费:inputTokens 按输入价计,storageTokenHours 按计费模型的存储单价计;
// requestID 是带固定前缀的费用行标识(见 GeminiCachedContentUsageRequestIDPrefix),落库时不被请求 id 覆盖。
2026-10-10 22:21:28 +08:00
func ( h * GatewayHandler ) submitGeminiCachedContentUsage (
c * gin . Context ,
apiKey * service . APIKey ,
account * service . Account ,
subscription * service . UserSubscription ,
pricingAt time . Time ,
2026-10-11 01:37:04 +08:00
requestID string ,
2026-10-10 22:21:28 +08:00
model string ,
upstreamModel string ,
inputTokens int ,
storageTokenHours float64 ,
usageFields service . ChannelUsageFields ,
payload [] byte ,
) {
if inputTokens <= 0 && storageTokenHours <= 0 {
return
}
if upstreamModel == model {
upstreamModel = ""
}
result := & service . ForwardResult {
2026-10-11 01:37:04 +08:00
RequestID : requestID ,
2026-10-10 22:21:28 +08:00
Model : model ,
UpstreamModel : upstreamModel ,
Usage : service . ClaudeUsage { InputTokens : inputTokens },
CacheStorageTokenHours : storageTokenHours ,
}
inboundEndpoint := GetInboundEndpoint ( c )
upstreamEndpoint := GetUpstreamEndpoint ( c , account . Platform )
userAgent := c . GetHeader ( "User-Agent" )
clientIP := ip . GetClientIP ( c )
requestPayloadHash := service . HashUsageRequestPayload ( payload )
quotaPlatform := service . QuotaPlatform ( c . Request . Context (), apiKey )
h . submitMandatoryUsageRecordTask ( c . Request . Context (), func ( ctx context . Context ) {
if err := h . gatewayService . RecordUsage ( ctx , & service . RecordUsageInput {
Result : result ,
APIKey : apiKey ,
User : apiKey . User ,
Account : account ,
Subscription : subscription ,
PricingAt : pricingAt ,
InboundEndpoint : inboundEndpoint ,
UpstreamEndpoint : upstreamEndpoint ,
UserAgent : userAgent ,
IPAddress : clientIP ,
RequestPayloadHash : requestPayloadHash ,
APIKeyService : h . apiKeyService ,
QuotaPlatform : quotaPlatform ,
ChannelUsageFields : usageFields ,
}); err != nil {
logger . L (). With (
zap . String ( "component" , "handler.gemini_v1beta.cached_contents" ),
zap . Int64 ( "api_key_id" , apiKey . ID ),
zap . Int64 ( "account_id" , account . ID ),
zap . String ( "request_id" , result . RequestID ),
). Error ( "gemini.cached_content.record_usage_failed" , zap . Error ( err ))
}
})
}
// geminiCachedContentPersistContext 返回与客户端连接脱钩的落库 context:上游操作已生效时,客户端断开不应让本地记录缺失。
func geminiCachedContentPersistContext ( c * gin . Context ) ( context . Context , context . CancelFunc ) {
return context . WithTimeout ( context . WithoutCancel ( c . Request . Context ()), geminiCachedContentPersistTimeout )
}
// cleanupGeminiCachedContentUpstream 尽力删除已在上游创建、但网关不会登记的缓存,避免其持续产生存储费。
func ( h * GatewayHandler ) cleanupGeminiCachedContentUpstream ( reqLog * zap . Logger , account * service . Account , upstreamName string ) {
ctx , cancel := context . WithTimeout ( context . Background (), geminiCachedContentPersistTimeout )
defer cancel ()
if err := h . geminiCachedContentService . DeleteUpstream ( ctx , account , & service . GeminiCachedContent { UpstreamName : upstreamName }); err != nil {
reqLog . Error ( "gemini.cached_content.orphan_cleanup_failed" , zap . Int64 ( "account_id" , account . ID ), zap . String ( "upstream_name" , upstreamName ), zap . Error ( err ))
}
}
// reserveGeminiCachedContentCharge 校验余额 / 订阅额度 / API Key 额度足以支付确定金额 amount(倍率后),
2026-10-12 17:14:56 +08:00
// 并按该金额在用户、订阅、API Key 维度登记在途预留(不受预留上限封顶,见 ReserveFixedCharge)。
// 返回的 done 须在 handler 结束时调用;预留随计费任务交接,落库后释放。
2026-10-10 22:21:28 +08:00
func ( h * GatewayHandler ) reserveGeminiCachedContentCharge ( c * gin . Context , apiKey * service . APIKey , subscription * service . UserSubscription , amount float64 ) ( func (), error ) {
ctx := c . Request . Context ()
2026-10-12 17:14:56 +08:00
res , err := h . billingCacheService . ReserveFixedCharge ( ctx , apiKey . User , apiKey , apiKey . Group , subscription , amount )
2026-10-10 22:21:28 +08:00
if err != nil {
return inflightNoop , err
}
if res == nil {
return inflightNoop , nil
}
c . Request = c . Request . WithContext ( service . WithInflightReservation ( ctx , res ))
return res . HandlerDone , nil
}
2026-10-12 17:14:56 +08:00
// writeGeminiCachedContentMutationConflict 回写同一缓存的修改冲突(Google 并发冲突语义 ABORTED),客户端可稍后重试。
func writeGeminiCachedContentMutationConflict ( c * gin . Context ) {
c . JSON ( http . StatusConflict , gin . H {
"error" : gin . H {
"code" : http . StatusConflict ,
"message" : "Another update to this cached content is in progress, please retry later" ,
"status" : "ABORTED" ,
},
})
}
2026-10-10 22:21:28 +08:00
func writeGeminiCachedContentBillingError ( c * gin . Context , err error ) {
2026-10-10 22:09:14 +08:00
status , code , message , retryAfter := billingErrorDetails ( err )
2026-10-10 22:21:28 +08:00
if retryAfter > 0 {
c . Header ( "Retry-After" , strconv . Itoa ( retryAfter ))
}
2026-10-10 22:09:14 +08:00
googleErrorWithType ( c , status , code , "" , message )
2026-10-10 22:21:28 +08:00
}
2026-10-11 01:37:04 +08:00
// writeGeminiCachedContentUpstreamError 回写上游错误:缓存不存在统一返回标准 403(各上游形态不同且可能含上游资源 ID);
// 账号凭据或权限类错误不是客户端问题,统一改为 502;限流 / 不可用按普通 Gemini 路径处理(透传规则、通用映射),
// 只有上游网关账号的这类响应反映持有缓存的上游账号状态,原样回写。
func ( h * GatewayHandler ) writeGeminiCachedContentUpstreamError ( c * gin . Context , upErr * service . GeminiCachedContentUpstreamError ) {
2026-10-10 22:21:28 +08:00
status := upErr . StatusCode
message := strings . TrimSpace ( extractGoogleErrorMessage ( upErr . Body ))
service . SetOpsUpstreamError ( c , status , message , "" )
if upErr . NotFound () {
2026-10-10 22:09:14 +08:00
service . MarkOpsRequestScopedError ( c , "permission_error" )
2026-10-10 22:21:28 +08:00
c . Data ( http . StatusForbidden , "application/json" , [] byte ( service . GeminiCachedContentNotFoundResponse ))
return
}
2026-10-11 01:37:04 +08:00
if ( status == http . StatusTooManyRequests || status >= http . StatusInternalServerError ) && ! upErr . UpstreamGateway {
h . handleGeminiFailoverExhausted ( c , & service . UpstreamFailoverError { StatusCode : status , ResponseBody : upErr . Body })
return
}
2026-10-10 22:21:28 +08:00
if status == http . StatusUnauthorized || status == http . StatusForbidden {
googleError ( c , http . StatusBadGateway , "Upstream account rejected the cached content request" )
return
}
if len ( upErr . Body ) == 0 || ! json . Valid ( upErr . Body ) {
if message == "" {
message = http . StatusText ( status )
}
googleError ( c , status , message )
return
}
c . Data ( status , "application/json" , upErr . Body )
}
func extractGoogleErrorMessage ( body [] byte ) string {
var envelope struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
if err := json . Unmarshal ( body , & envelope ); err != nil {
return ""
}
return envelope . Error . Message
}
func encodeGeminiCachedContentPageToken ( id int64 ) string {
return base64 . RawURLEncoding . EncodeToString ([] byte ( strconv . FormatInt ( id , 10 )))
}
func decodeGeminiCachedContentPageToken ( raw string ) ( int64 , bool ) {
decoded , err := base64 . RawURLEncoding . DecodeString ( raw )
if err != nil {
return 0 , false
}
id , err := strconv . ParseInt ( string ( decoded ), 10 , 64 )
if err != nil || id <= 0 {
return 0 , false
}
return id , true
}
// mergeAccountIDSets 返回两个账号 ID 集合的并集(不修改入参)。
func mergeAccountIDSets ( a , b map [ int64 ] struct {}) map [ int64 ] struct {} {
out := make ( map [ int64 ] struct {}, len ( a ) + len ( b ))
for id := range a {
out [ id ] = struct {}{}
}
for id := range b {
out [ id ] = struct {}{}
}
return out
}
2026-10-11 01:37:04 +08:00
// handleGeminiCachedContentFailoverExhausted 处理绑定缓存的请求在持有账号上重试耗尽:没有其他账号可换。
// 上游网关账号的限流 / 不可用响应反映持有缓存的上游账号状态,原样返回供客户端判断;
// 其余(含官方账号)仍按普通 Gemini 路径处理:透传规则、通用映射。
2026-10-10 22:21:28 +08:00
func ( h * GatewayHandler ) handleGeminiCachedContentFailoverExhausted ( c * gin . Context , failoverErr * service . UpstreamFailoverError ) {
2026-10-11 01:37:04 +08:00
if failoverErr == nil || ! failoverErr . BoundUpstreamPassthrough || len ( failoverErr . ResponseBody ) == 0 || ! json . Valid ( failoverErr . ResponseBody ) {
2026-10-10 22:21:28 +08:00
h . handleGeminiFailoverExhausted ( c , failoverErr )
return
}
service . SetOpsUpstreamError ( c , failoverErr . StatusCode , service . ExtractUpstreamErrorMessage ( failoverErr . ResponseBody ), "" )
c . Data ( failoverErr . StatusCode , "application/json" , failoverErr . ResponseBody )
}