gemini_messages_compat_service.go 119 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798
  1. package service
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "crypto/rand"
  7. "encoding/base64"
  8. "encoding/hex"
  9. "encoding/json"
  10. "errors"
  11. "fmt"
  12. "io"
  13. "log"
  14. "math"
  15. mathrand "math/rand"
  16. "net/http"
  17. "regexp"
  18. "strings"
  19. "time"
  20. "github.com/Wei-Shaw/sub2api/internal/config"
  21. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  22. "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
  23. "github.com/Wei-Shaw/sub2api/internal/pkg/googleapi"
  24. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  25. "github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
  26. "github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
  27. "github.com/gin-gonic/gin"
  28. "github.com/tidwall/gjson"
  29. )
  30. const geminiStickySessionTTL = time.Hour
  31. const (
  32. geminiMaxRetries = 5
  33. geminiRetryBaseDelay = 1 * time.Second
  34. geminiRetryMaxDelay = 16 * time.Second
  35. )
  36. // Gemini tool calling now requires `thoughtSignature` in parts that include `functionCall`.
  37. // Many clients don't send it; we inject a known dummy signature to satisfy the validator.
  38. // Ref: https://ai.google.dev/gemini-api/docs/thought-signatures
  39. const geminiDummyThoughtSignature = "skip_thought_signature_validator"
  40. type GeminiMessagesCompatService struct {
  41. accountRepo AccountRepository
  42. groupRepo GroupRepository
  43. cache GatewayCache
  44. schedulerSnapshot *SchedulerSnapshotService
  45. tokenProvider *GeminiTokenProvider
  46. rateLimitService *RateLimitService
  47. httpUpstream HTTPUpstream
  48. antigravityGatewayService *AntigravityGatewayService
  49. cfg *config.Config
  50. responseHeaderFilter *responseheaders.CompiledHeaderFilter
  51. }
  52. func (s *GeminiMessagesCompatService) readUpstreamErrorBody(resp *http.Response) []byte {
  53. if resp == nil || resp.Body == nil {
  54. return nil
  55. }
  56. limit := gatewayUpstreamErrorBodyReadLimit
  57. if s != nil && s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody && s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes > int(limit) {
  58. limit = int64(s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
  59. }
  60. body, _ := io.ReadAll(io.LimitReader(resp.Body, limit))
  61. return body
  62. }
  63. func NewGeminiMessagesCompatService(
  64. accountRepo AccountRepository,
  65. groupRepo GroupRepository,
  66. cache GatewayCache,
  67. schedulerSnapshot *SchedulerSnapshotService,
  68. tokenProvider *GeminiTokenProvider,
  69. rateLimitService *RateLimitService,
  70. httpUpstream HTTPUpstream,
  71. antigravityGatewayService *AntigravityGatewayService,
  72. cfg *config.Config,
  73. ) *GeminiMessagesCompatService {
  74. return &GeminiMessagesCompatService{
  75. accountRepo: accountRepo,
  76. groupRepo: groupRepo,
  77. cache: cache,
  78. schedulerSnapshot: schedulerSnapshot,
  79. tokenProvider: tokenProvider,
  80. rateLimitService: rateLimitService,
  81. httpUpstream: httpUpstream,
  82. antigravityGatewayService: antigravityGatewayService,
  83. cfg: cfg,
  84. responseHeaderFilter: compileResponseHeaderFilter(cfg),
  85. }
  86. }
  87. // GetTokenProvider returns the token provider for OAuth accounts
  88. func (s *GeminiMessagesCompatService) GetTokenProvider() *GeminiTokenProvider {
  89. return s.tokenProvider
  90. }
  91. func (s *GeminiMessagesCompatService) SelectAccountForModel(ctx context.Context, groupID *int64, sessionHash string, requestedModel string) (*Account, error) {
  92. return s.SelectAccountForModelWithExclusions(ctx, groupID, sessionHash, requestedModel, nil)
  93. }
  94. func (s *GeminiMessagesCompatService) SelectAccountForModelWithExclusions(ctx context.Context, groupID *int64, sessionHash string, requestedModel string, excludedIDs map[int64]struct{}) (*Account, error) {
  95. // 1. 确定目标平台和调度模式
  96. // Determine target platform and scheduling mode
  97. platform, useMixedScheduling, hasForcePlatform, err := s.resolvePlatformAndSchedulingMode(ctx, groupID)
  98. if err != nil {
  99. return nil, err
  100. }
  101. cacheKey := "gemini:" + sessionHash
  102. // 2. 尝试粘性会话命中
  103. // Try sticky session hit
  104. if account := s.tryStickySessionHit(ctx, groupID, sessionHash, cacheKey, requestedModel, excludedIDs, platform, useMixedScheduling); account != nil {
  105. return s.hydrateSelectedAccount(ctx, account)
  106. }
  107. // 3. 查询可调度账户(强制平台模式:优先按分组查找,找不到再查全部)
  108. // Query schedulable accounts (force platform mode: try group first, fallback to all)
  109. accounts, err := s.listSchedulableAccountsOnce(ctx, groupID, platform, hasForcePlatform)
  110. if err != nil {
  111. return nil, fmt.Errorf("query accounts failed: %w", err)
  112. }
  113. // 强制平台模式下,分组中找不到账户时回退查询全部
  114. if len(accounts) == 0 && groupID != nil && hasForcePlatform {
  115. accounts, err = s.listSchedulableAccountsOnce(ctx, nil, platform, hasForcePlatform)
  116. if err != nil {
  117. return nil, fmt.Errorf("query accounts failed: %w", err)
  118. }
  119. }
  120. // 4. 按优先级 + LRU 选择最佳账号
  121. // Select best account by priority + LRU
  122. selected := s.selectBestGeminiAccount(ctx, accounts, requestedModel, excludedIDs, platform, useMixedScheduling)
  123. if selected == nil {
  124. if requestedModel != "" {
  125. return nil, fmt.Errorf("no available Gemini accounts supporting model: %s", requestedModel)
  126. }
  127. return nil, errors.New("no available Gemini accounts")
  128. }
  129. // 5. 设置粘性会话绑定
  130. // Set sticky session binding
  131. if sessionHash != "" {
  132. _ = s.cache.SetSessionAccountID(ctx, derefGroupID(groupID), cacheKey, selected.ID, geminiStickySessionTTL)
  133. }
  134. return s.hydrateSelectedAccount(ctx, selected)
  135. }
  136. // resolvePlatformAndSchedulingMode 解析目标平台和调度模式。
  137. // 返回:平台名称、是否使用混合调度、是否强制平台、错误。
  138. //
  139. // resolvePlatformAndSchedulingMode resolves target platform and scheduling mode.
  140. // Returns: platform name, whether to use mixed scheduling, whether force platform, error.
  141. func (s *GeminiMessagesCompatService) resolvePlatformAndSchedulingMode(ctx context.Context, groupID *int64) (platform string, useMixedScheduling bool, hasForcePlatform bool, err error) {
  142. // 优先检查 context 中的强制平台(/antigravity 路由)
  143. forcePlatform, hasForcePlatform := ctx.Value(ctxkey.ForcePlatform).(string)
  144. if hasForcePlatform && forcePlatform != "" {
  145. return forcePlatform, false, true, nil
  146. }
  147. if groupID != nil {
  148. // 根据分组 platform 决定查询哪种账号
  149. var group *Group
  150. if ctxGroup, ok := ctx.Value(ctxkey.Group).(*Group); ok && IsGroupContextValid(ctxGroup) && ctxGroup.ID == *groupID {
  151. group = ctxGroup
  152. } else {
  153. group, err = s.groupRepo.GetByIDLite(ctx, *groupID)
  154. if err != nil {
  155. return "", false, false, fmt.Errorf("get group failed: %w", err)
  156. }
  157. }
  158. // gemini 分组支持混合调度(包含启用了 mixed_scheduling 的 antigravity 账户)
  159. return group.Platform, group.Platform == PlatformGemini, false, nil
  160. }
  161. // 无分组时只使用原生 gemini 平台
  162. return PlatformGemini, true, false, nil
  163. }
  164. // tryStickySessionHit 尝试从粘性会话获取账号。
  165. // 如果命中且账号可用则返回账号;如果账号不可用则清理会话并返回 nil。
  166. //
  167. // tryStickySessionHit attempts to get account from sticky session.
  168. // Returns account if hit and usable; clears session and returns nil if account unavailable.
  169. func (s *GeminiMessagesCompatService) tryStickySessionHit(
  170. ctx context.Context,
  171. groupID *int64,
  172. sessionHash, cacheKey, requestedModel string,
  173. excludedIDs map[int64]struct{},
  174. platform string,
  175. useMixedScheduling bool,
  176. ) *Account {
  177. if sessionHash == "" {
  178. return nil
  179. }
  180. accountID, err := s.cache.GetSessionAccountID(ctx, derefGroupID(groupID), cacheKey)
  181. if err != nil || accountID <= 0 {
  182. return nil
  183. }
  184. if _, excluded := excludedIDs[accountID]; excluded {
  185. return nil
  186. }
  187. account, err := s.getSchedulableAccount(ctx, accountID)
  188. if err != nil {
  189. return nil
  190. }
  191. // 检查账号是否需要清理粘性会话
  192. // Check if sticky session should be cleared
  193. if shouldClearStickySession(account, requestedModel) {
  194. _ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), cacheKey)
  195. return nil
  196. }
  197. // 验证账号是否可用于当前请求
  198. // Verify account is usable for current request
  199. if !s.isAccountUsableForRequest(ctx, account, requestedModel, platform, useMixedScheduling) {
  200. return nil
  201. }
  202. // 刷新会话 TTL 并返回账号
  203. // Refresh session TTL and return account
  204. _ = s.cache.RefreshSessionTTL(ctx, derefGroupID(groupID), cacheKey, geminiStickySessionTTL)
  205. return account
  206. }
  207. // isAccountUsableForRequest 检查账号是否可用于当前请求。
  208. // 验证:模型调度、模型支持、平台匹配、速率限制预检。
  209. //
  210. // isAccountUsableForRequest checks if account is usable for current request.
  211. // Validates: model scheduling, model support, platform matching, rate limit precheck.
  212. func (s *GeminiMessagesCompatService) isAccountUsableForRequest(
  213. ctx context.Context,
  214. account *Account,
  215. requestedModel, platform string,
  216. useMixedScheduling bool,
  217. ) bool {
  218. return s.isAccountUsableForRequestWithPrecheck(ctx, account, requestedModel, platform, useMixedScheduling, nil)
  219. }
  220. func (s *GeminiMessagesCompatService) isAccountUsableForRequestWithPrecheck(
  221. ctx context.Context,
  222. account *Account,
  223. requestedModel, platform string,
  224. useMixedScheduling bool,
  225. precheckResult map[int64]bool,
  226. ) bool {
  227. // 检查模型调度能力
  228. // Check model scheduling capability
  229. if !account.IsSchedulableForModelWithContext(ctx, requestedModel) {
  230. return false
  231. }
  232. // 检查模型支持
  233. // Check model support
  234. if requestedModel != "" && !s.isModelSupportedByAccount(account, requestedModel) {
  235. return false
  236. }
  237. // 检查平台匹配
  238. // Check platform matching
  239. if !s.isAccountValidForPlatform(account, platform, useMixedScheduling) {
  240. return false
  241. }
  242. // 速率限制预检
  243. // Rate limit precheck
  244. if !s.passesRateLimitPreCheckWithCache(ctx, account, requestedModel, precheckResult) {
  245. return false
  246. }
  247. return true
  248. }
  249. // isAccountValidForPlatform 检查账号是否匹配目标平台。
  250. // 原生平台直接匹配;混合调度模式下 antigravity 需要启用 mixed_scheduling。
  251. //
  252. // isAccountValidForPlatform checks if account matches target platform.
  253. // Native platform matches directly; mixed scheduling mode requires antigravity to enable mixed_scheduling.
  254. func (s *GeminiMessagesCompatService) isAccountValidForPlatform(account *Account, platform string, useMixedScheduling bool) bool {
  255. if account.Platform == platform {
  256. return true
  257. }
  258. if useMixedScheduling && account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled() {
  259. return true
  260. }
  261. return false
  262. }
  263. func (s *GeminiMessagesCompatService) passesRateLimitPreCheckWithCache(ctx context.Context, account *Account, requestedModel string, precheckResult map[int64]bool) bool {
  264. if s.rateLimitService == nil || requestedModel == "" {
  265. return true
  266. }
  267. if precheckResult != nil {
  268. if ok, exists := precheckResult[account.ID]; exists {
  269. return ok
  270. }
  271. }
  272. ok, err := s.rateLimitService.PreCheckUsage(ctx, account, requestedModel)
  273. if err != nil {
  274. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini PreCheck] Account %d precheck error: %v", account.ID, err)
  275. }
  276. return ok
  277. }
  278. // selectBestGeminiAccount 从候选账号中选择最佳账号(优先级 + LRU + OAuth 优先)。
  279. // 返回 nil 表示无可用账号。
  280. //
  281. // selectBestGeminiAccount selects best account from candidates (priority + LRU + OAuth preferred).
  282. // Returns nil if no available account.
  283. func (s *GeminiMessagesCompatService) selectBestGeminiAccount(
  284. ctx context.Context,
  285. accounts []Account,
  286. requestedModel string,
  287. excludedIDs map[int64]struct{},
  288. platform string,
  289. useMixedScheduling bool,
  290. ) *Account {
  291. var selected *Account
  292. precheckResult := s.buildPreCheckUsageResultMap(ctx, accounts, requestedModel)
  293. for i := range accounts {
  294. acc := &accounts[i]
  295. // 跳过被排除的账号
  296. if _, excluded := excludedIDs[acc.ID]; excluded {
  297. continue
  298. }
  299. // 检查账号是否可用于当前请求
  300. if !s.isAccountUsableForRequestWithPrecheck(ctx, acc, requestedModel, platform, useMixedScheduling, precheckResult) {
  301. continue
  302. }
  303. // 选择最佳账号
  304. if selected == nil {
  305. selected = acc
  306. continue
  307. }
  308. if s.isBetterGeminiAccount(acc, selected) {
  309. selected = acc
  310. }
  311. }
  312. return selected
  313. }
  314. func (s *GeminiMessagesCompatService) buildPreCheckUsageResultMap(ctx context.Context, accounts []Account, requestedModel string) map[int64]bool {
  315. if s.rateLimitService == nil || requestedModel == "" || len(accounts) == 0 {
  316. return nil
  317. }
  318. candidates := make([]*Account, 0, len(accounts))
  319. for i := range accounts {
  320. candidates = append(candidates, &accounts[i])
  321. }
  322. result, err := s.rateLimitService.PreCheckUsageBatch(ctx, candidates, requestedModel)
  323. if err != nil {
  324. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini PreCheckBatch] failed: %v", err)
  325. }
  326. return result
  327. }
  328. // isBetterGeminiAccount 判断 candidate 是否比 current 更优。
  329. // 规则:优先级更高(数值更小)优先;同优先级时,未使用过的优先(OAuth > 非 OAuth),其次是最久未使用的。
  330. //
  331. // isBetterGeminiAccount checks if candidate is better than current.
  332. // Rules: higher priority (lower value) wins; same priority: never used (OAuth > non-OAuth) > least recently used.
  333. func (s *GeminiMessagesCompatService) isBetterGeminiAccount(candidate, current *Account) bool {
  334. // 优先级更高(数值更小)
  335. if candidate.Priority < current.Priority {
  336. return true
  337. }
  338. if candidate.Priority > current.Priority {
  339. return false
  340. }
  341. // 同优先级,比较最后使用时间
  342. switch {
  343. case candidate.LastUsedAt == nil && current.LastUsedAt != nil:
  344. // candidate 从未使用,优先
  345. return true
  346. case candidate.LastUsedAt != nil && current.LastUsedAt == nil:
  347. // current 从未使用,保持
  348. return false
  349. case candidate.LastUsedAt == nil && current.LastUsedAt == nil:
  350. // 都未使用,优先选择 OAuth 账号(更兼容 Code Assist 流程)
  351. return candidate.Type == AccountTypeOAuth && current.Type != AccountTypeOAuth
  352. default:
  353. // 都使用过,选择最久未使用的
  354. return candidate.LastUsedAt.Before(*current.LastUsedAt)
  355. }
  356. }
  357. // isModelSupportedByAccount 根据账户平台检查模型支持
  358. func (s *GeminiMessagesCompatService) isModelSupportedByAccount(account *Account, requestedModel string) bool {
  359. if account.Platform == PlatformAntigravity {
  360. if strings.TrimSpace(requestedModel) == "" {
  361. return true
  362. }
  363. return mapAntigravityModel(account, requestedModel) != ""
  364. }
  365. return account.IsModelSupported(requestedModel)
  366. }
  367. // GetAntigravityGatewayService 返回 AntigravityGatewayService
  368. func (s *GeminiMessagesCompatService) GetAntigravityGatewayService() *AntigravityGatewayService {
  369. return s.antigravityGatewayService
  370. }
  371. func (s *GeminiMessagesCompatService) getSchedulableAccount(ctx context.Context, accountID int64) (*Account, error) {
  372. if s.schedulerSnapshot != nil {
  373. return s.schedulerSnapshot.GetAccount(ctx, accountID)
  374. }
  375. return s.accountRepo.GetByID(ctx, accountID)
  376. }
  377. func (s *GeminiMessagesCompatService) hydrateSelectedAccount(ctx context.Context, account *Account) (*Account, error) {
  378. if account == nil {
  379. return nil, nil
  380. }
  381. hydrated := account
  382. if s.schedulerSnapshot != nil {
  383. var err error
  384. hydrated, err = s.schedulerSnapshot.GetAccount(ctx, account.ID)
  385. if err != nil {
  386. return nil, err
  387. }
  388. if hydrated == nil {
  389. return nil, fmt.Errorf("selected gemini account %d not found during hydration", account.ID)
  390. }
  391. }
  392. if err := ResolveRandomProxyFromSource(ctx, hydrated, s.accountRepo); err != nil {
  393. return nil, err
  394. }
  395. return hydrated, nil
  396. }
  397. func (s *GeminiMessagesCompatService) listSchedulableAccountsOnce(ctx context.Context, groupID *int64, platform string, hasForcePlatform bool) ([]Account, error) {
  398. var accounts []Account
  399. var err error
  400. if s.schedulerSnapshot != nil {
  401. accounts, _, err = s.schedulerSnapshot.ListSchedulableAccounts(ctx, groupID, platform, hasForcePlatform)
  402. } else {
  403. useMixedScheduling := platform == PlatformGemini && !hasForcePlatform
  404. queryPlatforms := []string{platform}
  405. if useMixedScheduling {
  406. queryPlatforms = []string{platform, PlatformAntigravity}
  407. }
  408. if groupID != nil {
  409. accounts, err = s.accountRepo.ListSchedulableByGroupIDAndPlatforms(ctx, *groupID, queryPlatforms)
  410. } else if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
  411. accounts, err = s.accountRepo.ListSchedulableByPlatforms(ctx, queryPlatforms)
  412. } else {
  413. accounts, err = s.accountRepo.ListSchedulableUngroupedByPlatforms(ctx, queryPlatforms)
  414. }
  415. }
  416. if err != nil {
  417. return accounts, err
  418. }
  419. return FilterTierPoolAccounts(accounts, TierPoolFromContext(ctx)), nil
  420. }
  421. func (s *GeminiMessagesCompatService) validateUpstreamBaseURL(raw string) (string, error) {
  422. if s.cfg != nil && !s.cfg.Security.URLAllowlist.Enabled {
  423. normalized, err := urlvalidator.ValidateURLFormat(raw, s.cfg.Security.URLAllowlist.AllowInsecureHTTP)
  424. if err != nil {
  425. return "", fmt.Errorf("invalid base_url: %w", err)
  426. }
  427. return normalized, nil
  428. }
  429. normalized, err := urlvalidator.ValidateHTTPSURL(raw, urlvalidator.ValidationOptions{
  430. AllowedHosts: s.cfg.Security.URLAllowlist.UpstreamHosts,
  431. RequireAllowlist: true,
  432. AllowPrivate: s.cfg.Security.URLAllowlist.AllowPrivateHosts,
  433. })
  434. if err != nil {
  435. return "", fmt.Errorf("invalid base_url: %w", err)
  436. }
  437. return normalized, nil
  438. }
  439. // HasAntigravityAccounts 检查是否有可用的 antigravity 账户
  440. func (s *GeminiMessagesCompatService) HasAntigravityAccounts(ctx context.Context, groupID *int64) (bool, error) {
  441. accounts, err := s.listSchedulableAccountsOnce(ctx, groupID, PlatformAntigravity, false)
  442. if err != nil {
  443. return false, err
  444. }
  445. return len(accounts) > 0, nil
  446. }
  447. // SelectAccountForAIStudioEndpoints selects an account that is likely to succeed against
  448. // generativelanguage.googleapis.com (e.g. GET /v1beta/models).
  449. //
  450. // Preference order:
  451. // 1) API key accounts (AI Studio)
  452. // 2) OAuth accounts without project_id (AI Studio OAuth)
  453. // 3) OAuth accounts explicitly marked as ai_studio
  454. // 4) Any remaining Gemini accounts (fallback)
  455. func (s *GeminiMessagesCompatService) SelectAccountForAIStudioEndpoints(ctx context.Context, groupID *int64) (*Account, error) {
  456. accounts, err := s.listSchedulableAccountsOnce(ctx, groupID, PlatformGemini, true)
  457. if err != nil {
  458. return nil, fmt.Errorf("query accounts failed: %w", err)
  459. }
  460. if len(accounts) == 0 {
  461. return nil, errors.New("no available Gemini accounts")
  462. }
  463. rank := func(a *Account) int {
  464. if a == nil {
  465. return 999
  466. }
  467. switch a.Type {
  468. case AccountTypeAPIKey:
  469. if strings.TrimSpace(a.GetCredential("api_key")) != "" {
  470. return 0
  471. }
  472. return 9
  473. case AccountTypeOAuth:
  474. if strings.TrimSpace(a.GetCredential("project_id")) == "" {
  475. return 1
  476. }
  477. if strings.TrimSpace(a.GetCredential("oauth_type")) == "ai_studio" {
  478. return 2
  479. }
  480. // Code Assist OAuth tokens often lack AI Studio scopes for models listing.
  481. return 3
  482. case AccountTypeServiceAccount:
  483. // Vertex service accounts use aiplatform.googleapis.com, not the AI Studio
  484. // endpoint (generativelanguage.googleapis.com), so they cannot serve these requests.
  485. return 999
  486. default:
  487. return 10
  488. }
  489. }
  490. var selected *Account
  491. for i := range accounts {
  492. acc := &accounts[i]
  493. if selected == nil {
  494. selected = acc
  495. continue
  496. }
  497. r1, r2 := rank(acc), rank(selected)
  498. if r1 < r2 {
  499. selected = acc
  500. continue
  501. }
  502. if r1 > r2 {
  503. continue
  504. }
  505. if acc.Priority < selected.Priority {
  506. selected = acc
  507. } else if acc.Priority == selected.Priority {
  508. switch {
  509. case acc.LastUsedAt == nil && selected.LastUsedAt != nil:
  510. selected = acc
  511. case acc.LastUsedAt != nil && selected.LastUsedAt == nil:
  512. // keep selected
  513. case acc.LastUsedAt == nil && selected.LastUsedAt == nil:
  514. if acc.Type == AccountTypeOAuth && selected.Type != AccountTypeOAuth {
  515. selected = acc
  516. }
  517. default:
  518. if acc.LastUsedAt.Before(*selected.LastUsedAt) {
  519. selected = acc
  520. }
  521. }
  522. }
  523. }
  524. if selected == nil {
  525. return nil, errors.New("no available Gemini accounts")
  526. }
  527. return s.hydrateSelectedAccount(ctx, selected)
  528. }
  529. func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Context, account *Account, body []byte) (*ForwardResult, error) {
  530. beginUpstreamResponseModelObservation(c)
  531. beginGeminiImageOutputObservation(c)
  532. startTime := time.Now()
  533. var req struct {
  534. Model string `json:"model"`
  535. Stream bool `json:"stream"`
  536. }
  537. if err := json.Unmarshal(body, &req); err != nil {
  538. return nil, fmt.Errorf("parse request: %w", err)
  539. }
  540. if strings.TrimSpace(req.Model) == "" {
  541. return nil, fmt.Errorf("missing model")
  542. }
  543. originalModel := req.Model
  544. mappedModel := req.Model
  545. if account.Type == AccountTypeAPIKey || account.Type == AccountTypeServiceAccount {
  546. mappedModel = account.GetMappedModel(req.Model)
  547. }
  548. geminiReq, err := convertClaudeMessagesToGeminiGenerateContent(body)
  549. if err != nil {
  550. return nil, s.writeClaudeError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
  551. }
  552. geminiReq = ensureGeminiFunctionCallThoughtSignatures(geminiReq)
  553. originalClaudeBody := body
  554. proxyURL := ""
  555. if account.ProxyID != nil && account.Proxy != nil {
  556. proxyURL = account.Proxy.URL()
  557. }
  558. var requestIDHeader string
  559. var buildReq func(ctx context.Context) (*http.Request, string, error)
  560. useUpstreamStream := req.Stream
  561. if account.Type == AccountTypeOAuth && !req.Stream && strings.TrimSpace(account.GetCredential("project_id")) != "" {
  562. // Code Assist's non-streaming generateContent may return no content; use streaming upstream and aggregate.
  563. useUpstreamStream = true
  564. }
  565. switch account.Type {
  566. case AccountTypeAPIKey:
  567. buildReq = func(ctx context.Context) (*http.Request, string, error) {
  568. apiKey := account.GetCredential("api_key")
  569. if strings.TrimSpace(apiKey) == "" {
  570. return nil, "", errors.New("gemini api_key not configured")
  571. }
  572. baseURL := account.GetGeminiBaseURL(geminicli.AIStudioBaseURL)
  573. normalizedBaseURL, err := s.validateUpstreamBaseURL(baseURL)
  574. if err != nil {
  575. return nil, "", err
  576. }
  577. action := "generateContent"
  578. if req.Stream {
  579. action = "streamGenerateContent"
  580. }
  581. fullURL, err := buildGeminiAIStudioModelActionURL(normalizedBaseURL, mappedModel, action, req.Stream)
  582. if err != nil {
  583. return nil, "", err
  584. }
  585. restGeminiReq := normalizeGeminiRequestForAIStudio(geminiReq)
  586. upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(restGeminiReq))
  587. if err != nil {
  588. return nil, "", err
  589. }
  590. upstreamReq.Header.Set("Content-Type", "application/json")
  591. upstreamReq.Header.Set("x-goog-api-key", apiKey)
  592. return upstreamReq, "x-request-id", nil
  593. }
  594. requestIDHeader = "x-request-id"
  595. case AccountTypeOAuth:
  596. buildReq = func(ctx context.Context) (*http.Request, string, error) {
  597. if s.tokenProvider == nil {
  598. return nil, "", errors.New("gemini token provider not configured")
  599. }
  600. accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
  601. if err != nil {
  602. return nil, "", err
  603. }
  604. projectID := strings.TrimSpace(account.GetCredential("project_id"))
  605. action := "generateContent"
  606. if useUpstreamStream {
  607. action = "streamGenerateContent"
  608. }
  609. // Two modes for OAuth:
  610. // 1. With project_id -> Code Assist API (wrapped request)
  611. // 2. Without project_id -> AI Studio API (direct OAuth, like API key but with Bearer token)
  612. if projectID != "" {
  613. // Mode 1: Code Assist API
  614. baseURL, err := s.validateUpstreamBaseURL(geminicli.GeminiCliBaseURL)
  615. if err != nil {
  616. return nil, "", err
  617. }
  618. fullURL := fmt.Sprintf("%s/v1internal:%s", strings.TrimRight(baseURL, "/"), action)
  619. if useUpstreamStream {
  620. fullURL += "?alt=sse"
  621. }
  622. wrapped := map[string]any{
  623. "model": mappedModel,
  624. "project": projectID,
  625. }
  626. var inner any
  627. if err := json.Unmarshal(geminiReq, &inner); err != nil {
  628. return nil, "", fmt.Errorf("failed to parse gemini request: %w", err)
  629. }
  630. wrapped["request"] = inner
  631. wrappedBytes, _ := json.Marshal(wrapped)
  632. upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(wrappedBytes))
  633. if err != nil {
  634. return nil, "", err
  635. }
  636. upstreamReq.Header.Set("Content-Type", "application/json")
  637. upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
  638. upstreamReq.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent)
  639. return upstreamReq, "x-request-id", nil
  640. } else {
  641. // Mode 2: AI Studio API with OAuth (like API key mode, but using Bearer token)
  642. baseURL := account.GetGeminiBaseURL(geminicli.AIStudioBaseURL)
  643. normalizedBaseURL, err := s.validateUpstreamBaseURL(baseURL)
  644. if err != nil {
  645. return nil, "", err
  646. }
  647. fullURL, err := buildGeminiAIStudioModelActionURL(normalizedBaseURL, mappedModel, action, useUpstreamStream)
  648. if err != nil {
  649. return nil, "", err
  650. }
  651. restGeminiReq := normalizeGeminiRequestForAIStudio(geminiReq)
  652. upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(restGeminiReq))
  653. if err != nil {
  654. return nil, "", err
  655. }
  656. upstreamReq.Header.Set("Content-Type", "application/json")
  657. upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
  658. return upstreamReq, "x-request-id", nil
  659. }
  660. }
  661. requestIDHeader = "x-request-id"
  662. case AccountTypeServiceAccount:
  663. buildReq = func(ctx context.Context) (*http.Request, string, error) {
  664. if s.tokenProvider == nil {
  665. return nil, "", errors.New("gemini token provider not configured")
  666. }
  667. accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
  668. if err != nil {
  669. return nil, "", err
  670. }
  671. action := "generateContent"
  672. if req.Stream {
  673. action = "streamGenerateContent"
  674. }
  675. fullURL, err := buildVertexGeminiURL(account.VertexProjectID(), account.VertexLocation(mappedModel), mappedModel, action, req.Stream)
  676. if err != nil {
  677. return nil, "", err
  678. }
  679. restGeminiReq := normalizeGeminiRequestForAIStudio(geminiReq)
  680. upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(restGeminiReq))
  681. if err != nil {
  682. return nil, "", err
  683. }
  684. upstreamReq.Header.Set("Content-Type", "application/json")
  685. upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
  686. return upstreamReq, "x-request-id", nil
  687. }
  688. requestIDHeader = "x-request-id"
  689. default:
  690. return nil, fmt.Errorf("unsupported account type: %s", account.Type)
  691. }
  692. var resp *http.Response
  693. signatureRetryStage := 0
  694. for attempt := 1; attempt <= geminiMaxRetries; attempt++ {
  695. upstreamReq, idHeader, err := buildReq(ctx)
  696. if err != nil {
  697. if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
  698. return nil, err
  699. }
  700. // Local build error: don't retry.
  701. if strings.Contains(err.Error(), "missing project_id") {
  702. return nil, s.writeClaudeError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
  703. }
  704. return nil, s.writeClaudeError(c, http.StatusBadGateway, "upstream_error", err.Error())
  705. }
  706. requestIDHeader = idHeader
  707. resp, err = s.httpUpstream.Do(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency)
  708. if err != nil {
  709. if local := AccountTrafficFailover(err); local != nil {
  710. return nil, local
  711. }
  712. safeErr := sanitizeUpstreamErrorMessage(err.Error())
  713. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  714. ProxyID: opsUpstreamProxyID(account),
  715. ProxyName: opsUpstreamProxyName(account),
  716. Platform: account.Platform,
  717. AccountID: account.ID,
  718. AccountName: account.Name,
  719. UpstreamStatusCode: 0,
  720. Kind: "request_error",
  721. Message: safeErr,
  722. })
  723. if attempt < geminiMaxRetries {
  724. logger.LegacyPrintf("service.gemini_messages_compat", "Gemini account %d: upstream request failed, retry %d/%d: %v", account.ID, attempt, geminiMaxRetries, err)
  725. sleepGeminiBackoff(attempt)
  726. continue
  727. }
  728. setOpsUpstreamError(c, 0, safeErr, "")
  729. return nil, s.writeClaudeError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed after retries: "+safeErr)
  730. }
  731. // Special-case: signature/thought_signature validation errors are not transient, but may be fixed by
  732. // downgrading Claude thinking/tool history to plain text (conservative two-stage retry).
  733. if resp.StatusCode == http.StatusBadRequest && signatureRetryStage < 2 {
  734. respBody := s.readUpstreamErrorBody(resp)
  735. _ = resp.Body.Close()
  736. if isGeminiSignatureRelatedError(respBody) {
  737. upstreamReqID := resp.Header.Get(requestIDHeader)
  738. if upstreamReqID == "" {
  739. upstreamReqID = resp.Header.Get("x-goog-request-id")
  740. }
  741. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  742. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  743. upstreamDetail := ""
  744. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  745. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  746. if maxBytes <= 0 {
  747. maxBytes = 2048
  748. }
  749. upstreamDetail = truncateString(string(respBody), maxBytes)
  750. }
  751. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  752. ProxyID: opsUpstreamProxyID(account),
  753. ProxyName: opsUpstreamProxyName(account),
  754. Platform: account.Platform,
  755. AccountID: account.ID,
  756. AccountName: account.Name,
  757. UpstreamStatusCode: resp.StatusCode,
  758. UpstreamRequestID: upstreamReqID,
  759. Kind: "signature_error",
  760. Message: upstreamMsg,
  761. Detail: upstreamDetail,
  762. })
  763. var strippedClaudeBody []byte
  764. stageName := ""
  765. // 路径说明:本处上游是 Gemini,但被剥离的 body 是 Anthropic 格式。传 originalModel
  766. // (客户端原 Anthropic model)而非 mappedModel(上游 Gemini model),让剥离逻辑按
  767. // 客户端请求的 Anthropic 子协议族判定(详见 ResolveThinkingProtocol 文档)。
  768. switch signatureRetryStage {
  769. case 0:
  770. // Stage 1: disable thinking + thinking->text
  771. strippedClaudeBody = FilterThinkingBlocksForRetry(originalClaudeBody, originalModel)
  772. stageName = "thinking-only"
  773. signatureRetryStage = 1
  774. default:
  775. // Stage 2: additionally downgrade tool_use/tool_result blocks to text
  776. strippedClaudeBody = FilterSignatureSensitiveBlocksForRetry(originalClaudeBody, originalModel)
  777. stageName = "thinking+tools"
  778. signatureRetryStage = 2
  779. }
  780. retryGeminiReq, txErr := convertClaudeMessagesToGeminiGenerateContent(strippedClaudeBody)
  781. if txErr == nil {
  782. logger.LegacyPrintf("service.gemini_messages_compat", "Gemini account %d: detected signature-related 400, retrying with downgraded Claude blocks (%s)", account.ID, stageName)
  783. geminiReq = retryGeminiReq
  784. // Consume one retry budget attempt and continue with the updated request payload.
  785. sleepGeminiBackoff(1)
  786. continue
  787. }
  788. }
  789. // Restore body for downstream error handling.
  790. resp = &http.Response{
  791. StatusCode: http.StatusBadRequest,
  792. Header: resp.Header.Clone(),
  793. Body: io.NopCloser(bytes.NewReader(respBody)),
  794. }
  795. break
  796. }
  797. // 错误策略优先:匹配则跳过重试直接处理。
  798. if matched, rebuilt := s.checkErrorPolicyInLoop(ctx, account, resp, mappedModel); matched {
  799. resp = rebuilt
  800. break
  801. } else {
  802. resp = rebuilt
  803. }
  804. if resp.StatusCode >= 400 && s.shouldRetryGeminiUpstreamError(account, resp.StatusCode) {
  805. respBody := s.readUpstreamErrorBody(resp)
  806. _ = resp.Body.Close()
  807. // Don't treat insufficient-scope as transient.
  808. if resp.StatusCode == 403 && isGeminiInsufficientScope(resp.Header, respBody) {
  809. resp = &http.Response{
  810. StatusCode: resp.StatusCode,
  811. Header: resp.Header.Clone(),
  812. Body: io.NopCloser(bytes.NewReader(respBody)),
  813. }
  814. break
  815. }
  816. if resp.StatusCode == 429 {
  817. // Mark as rate-limited early so concurrent requests avoid this account.
  818. s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  819. }
  820. if attempt < geminiMaxRetries {
  821. upstreamReqID := resp.Header.Get(requestIDHeader)
  822. if upstreamReqID == "" {
  823. upstreamReqID = resp.Header.Get("x-goog-request-id")
  824. }
  825. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  826. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  827. upstreamDetail := ""
  828. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  829. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  830. if maxBytes <= 0 {
  831. maxBytes = 2048
  832. }
  833. upstreamDetail = truncateString(string(respBody), maxBytes)
  834. }
  835. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  836. ProxyID: opsUpstreamProxyID(account),
  837. ProxyName: opsUpstreamProxyName(account),
  838. Platform: account.Platform,
  839. AccountID: account.ID,
  840. AccountName: account.Name,
  841. UpstreamStatusCode: resp.StatusCode,
  842. UpstreamRequestID: upstreamReqID,
  843. Kind: "retry",
  844. Message: upstreamMsg,
  845. Detail: upstreamDetail,
  846. })
  847. logger.LegacyPrintf("service.gemini_messages_compat", "Gemini account %d: upstream status %d, retry %d/%d", account.ID, resp.StatusCode, attempt, geminiMaxRetries)
  848. sleepGeminiBackoff(attempt)
  849. continue
  850. }
  851. // Final attempt: surface the upstream error body (mapped below) instead of a generic retry error.
  852. resp = &http.Response{
  853. StatusCode: resp.StatusCode,
  854. Header: resp.Header.Clone(),
  855. Body: io.NopCloser(bytes.NewReader(respBody)),
  856. }
  857. break
  858. }
  859. break
  860. }
  861. defer func() { _ = resp.Body.Close() }()
  862. if resp.StatusCode >= 400 {
  863. respBody := s.readUpstreamErrorBody(resp)
  864. // 统一错误策略:自定义错误码 + 临时不可调度
  865. if s.rateLimitService != nil {
  866. policy := s.rateLimitService.CheckErrorPolicy(ctx, account, resp.StatusCode, respBody, mappedModel)
  867. switch policy {
  868. case ErrorPolicySkipped:
  869. upstreamReqID := resp.Header.Get(requestIDHeader)
  870. if upstreamReqID == "" {
  871. upstreamReqID = resp.Header.Get("x-goog-request-id")
  872. }
  873. if failoverErr := s.skippedErrorPolicyFailoverError(c, account, resp.StatusCode, respBody, upstreamReqID); failoverErr != nil {
  874. return nil, failoverErr
  875. }
  876. if account.IsCustomErrorCodesEnabled() {
  877. return nil, s.writeGeminiCustomCodeSkippedError(c, account, resp.StatusCode, upstreamReqID, respBody, func() {
  878. _ = s.writeClaudeError(c, http.StatusInternalServerError, "api_error", geminiCustomCodeSkippedClientMessage)
  879. })
  880. }
  881. // 池模式:客户端写出与 ErrorPolicyNone 相同(按上游真实状态码映射),仅跳过账号状态标记。
  882. return nil, s.writeGeminiMappedError(c, account, resp.StatusCode, upstreamReqID, respBody)
  883. case ErrorPolicyMatched, ErrorPolicyTempUnscheduled:
  884. if policy == ErrorPolicyMatched {
  885. s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  886. }
  887. upstreamReqID := resp.Header.Get(requestIDHeader)
  888. if upstreamReqID == "" {
  889. upstreamReqID = resp.Header.Get("x-goog-request-id")
  890. }
  891. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  892. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  893. upstreamDetail := ""
  894. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  895. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  896. if maxBytes <= 0 {
  897. maxBytes = 2048
  898. }
  899. upstreamDetail = truncateString(string(respBody), maxBytes)
  900. }
  901. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  902. ProxyID: opsUpstreamProxyID(account),
  903. ProxyName: opsUpstreamProxyName(account),
  904. Platform: account.Platform,
  905. AccountID: account.ID,
  906. AccountName: account.Name,
  907. UpstreamStatusCode: resp.StatusCode,
  908. UpstreamRequestID: upstreamReqID,
  909. Kind: "failover",
  910. Message: upstreamMsg,
  911. Detail: upstreamDetail,
  912. })
  913. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: respBody}
  914. }
  915. }
  916. // ErrorPolicyNone → 原有逻辑
  917. s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  918. // 精确匹配服务端配置类 400 错误,触发 failover + 临时封禁
  919. if resp.StatusCode == http.StatusBadRequest {
  920. msg400 := strings.ToLower(strings.TrimSpace(extractUpstreamErrorMessage(respBody)))
  921. if isGoogleProjectConfigError(msg400) {
  922. upstreamReqID := resp.Header.Get(requestIDHeader)
  923. if upstreamReqID == "" {
  924. upstreamReqID = resp.Header.Get("x-goog-request-id")
  925. }
  926. upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(respBody)))
  927. upstreamDetail := ""
  928. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  929. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  930. if maxBytes <= 0 {
  931. maxBytes = 2048
  932. }
  933. upstreamDetail = truncateString(string(respBody), maxBytes)
  934. }
  935. log.Printf("[Gemini] status=400 google_config_error failover=true upstream_message=%q account=%d", upstreamMsg, account.ID)
  936. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  937. ProxyID: opsUpstreamProxyID(account),
  938. ProxyName: opsUpstreamProxyName(account),
  939. Platform: account.Platform,
  940. AccountID: account.ID,
  941. AccountName: account.Name,
  942. UpstreamStatusCode: resp.StatusCode,
  943. UpstreamRequestID: upstreamReqID,
  944. Kind: "failover",
  945. Message: upstreamMsg,
  946. Detail: upstreamDetail,
  947. })
  948. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: respBody, RetryableOnSameAccount: true}
  949. }
  950. }
  951. if s.shouldFailoverGeminiUpstreamError(resp.StatusCode) {
  952. upstreamReqID := resp.Header.Get(requestIDHeader)
  953. if upstreamReqID == "" {
  954. upstreamReqID = resp.Header.Get("x-goog-request-id")
  955. }
  956. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  957. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  958. upstreamDetail := ""
  959. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  960. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  961. if maxBytes <= 0 {
  962. maxBytes = 2048
  963. }
  964. upstreamDetail = truncateString(string(respBody), maxBytes)
  965. }
  966. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  967. ProxyID: opsUpstreamProxyID(account),
  968. ProxyName: opsUpstreamProxyName(account),
  969. Platform: account.Platform,
  970. AccountID: account.ID,
  971. AccountName: account.Name,
  972. UpstreamStatusCode: resp.StatusCode,
  973. UpstreamRequestID: upstreamReqID,
  974. Kind: "failover",
  975. Message: upstreamMsg,
  976. Detail: upstreamDetail,
  977. })
  978. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: respBody}
  979. }
  980. upstreamReqID := resp.Header.Get(requestIDHeader)
  981. if upstreamReqID == "" {
  982. upstreamReqID = resp.Header.Get("x-goog-request-id")
  983. }
  984. return nil, s.writeGeminiMappedError(c, account, resp.StatusCode, upstreamReqID, respBody)
  985. }
  986. requestID := resp.Header.Get(requestIDHeader)
  987. if requestID == "" {
  988. requestID = resp.Header.Get("x-goog-request-id")
  989. }
  990. if requestID != "" {
  991. c.Header("x-request-id", requestID)
  992. }
  993. var usage *ClaudeUsage
  994. var firstTokenMs *int
  995. if req.Stream {
  996. streamRes, err := s.handleStreamingResponse(c, resp, startTime, originalModel)
  997. if err != nil {
  998. return nil, err
  999. }
  1000. usage = streamRes.usage
  1001. firstTokenMs = streamRes.firstTokenMs
  1002. } else {
  1003. if useUpstreamStream {
  1004. collected, usageObj, err := collectGeminiSSE(resp.Body, true)
  1005. if err != nil {
  1006. return nil, s.writeClaudeError(c, http.StatusBadGateway, "upstream_error", "Failed to read upstream stream")
  1007. }
  1008. collectedBytes, _ := json.Marshal(collected)
  1009. upstreamResponseModelObserverFromContext(c).ObserveGemini(collectedBytes)
  1010. observeGeminiImageOutputs(c, collectedBytes)
  1011. claudeResp, usageObj2 := convertGeminiToClaudeMessage(collected, originalModel, collectedBytes, false)
  1012. c.JSON(http.StatusOK, claudeResp)
  1013. usage = usageObj2
  1014. if usageObj != nil && (usageObj.InputTokens > 0 || usageObj.OutputTokens > 0) {
  1015. usage = usageObj
  1016. }
  1017. } else {
  1018. usage, err = s.handleNonStreamingResponse(c, resp, originalModel)
  1019. if err != nil {
  1020. return nil, err
  1021. }
  1022. }
  1023. }
  1024. // 图片生成计费
  1025. imageInputSize := s.extractImageInputSize(body)
  1026. imageSize := normalizeOpenAIImageSizeTier(imageInputSize)
  1027. imageCount := resolveGeminiImageCount(c, originalModel, mappedModel)
  1028. return &ForwardResult{
  1029. RequestID: requestID,
  1030. UpstreamHeaders: resp.Header,
  1031. Usage: *usage,
  1032. Model: originalModel,
  1033. UpstreamModel: mappedModel,
  1034. UpstreamResponseModel: observedUpstreamResponseModel(c),
  1035. UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
  1036. Stream: req.Stream,
  1037. Duration: time.Since(startTime),
  1038. FirstTokenMs: firstTokenMs,
  1039. ImageCount: imageCount,
  1040. ImageSize: imageSize,
  1041. ImageInputSize: imageInputSize,
  1042. }, nil
  1043. }
  1044. func isGeminiSignatureRelatedError(respBody []byte) bool {
  1045. msg := strings.ToLower(strings.TrimSpace(extractAntigravityErrorMessage(respBody)))
  1046. if msg == "" {
  1047. msg = strings.ToLower(string(respBody))
  1048. }
  1049. return strings.Contains(msg, "thought_signature") || strings.Contains(msg, "signature")
  1050. }
  1051. func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin.Context, account *Account, originalModel string, action string, stream bool, body []byte) (*ForwardResult, error) {
  1052. beginUpstreamResponseModelObservation(c)
  1053. beginGeminiImageOutputObservation(c)
  1054. startTime := time.Now()
  1055. if strings.TrimSpace(originalModel) == "" {
  1056. return nil, s.writeGoogleError(c, http.StatusBadRequest, "Missing model in URL")
  1057. }
  1058. if strings.TrimSpace(action) == "" {
  1059. return nil, s.writeGoogleError(c, http.StatusBadRequest, "Missing action in URL")
  1060. }
  1061. if len(body) == 0 {
  1062. return nil, s.writeGoogleError(c, http.StatusBadRequest, "Request body is empty")
  1063. }
  1064. // 过滤掉 parts 为空的消息(Gemini API 不接受空 parts)
  1065. if filteredBody, err := filterEmptyPartsFromGeminiRequest(body); err == nil {
  1066. body = filteredBody
  1067. }
  1068. switch action {
  1069. case "generateContent", "streamGenerateContent", "countTokens":
  1070. // ok
  1071. default:
  1072. return nil, s.writeGoogleError(c, http.StatusNotFound, "Unsupported action: "+action)
  1073. }
  1074. // Some Gemini upstreams validate tool call parts strictly; ensure any `functionCall` part includes a
  1075. // `thoughtSignature` to avoid frequent INVALID_ARGUMENT 400s.
  1076. body = ensureGeminiFunctionCallThoughtSignatures(body)
  1077. mappedModel := originalModel
  1078. if account.Type == AccountTypeAPIKey || account.Type == AccountTypeServiceAccount {
  1079. mappedModel = account.GetMappedModel(originalModel)
  1080. }
  1081. proxyURL := ""
  1082. if account.ProxyID != nil && account.Proxy != nil {
  1083. proxyURL = account.Proxy.URL()
  1084. }
  1085. useUpstreamStream := stream
  1086. upstreamAction := action
  1087. if account.Type == AccountTypeOAuth && !stream && action == "generateContent" && strings.TrimSpace(account.GetCredential("project_id")) != "" {
  1088. // Code Assist's non-streaming generateContent may return no content; use streaming upstream and aggregate.
  1089. useUpstreamStream = true
  1090. upstreamAction = "streamGenerateContent"
  1091. }
  1092. forceAIStudio := action == "countTokens"
  1093. var requestIDHeader string
  1094. var buildReq func(ctx context.Context) (*http.Request, string, error)
  1095. switch account.Type {
  1096. case AccountTypeAPIKey:
  1097. buildReq = func(ctx context.Context) (*http.Request, string, error) {
  1098. apiKey := account.GetCredential("api_key")
  1099. if strings.TrimSpace(apiKey) == "" {
  1100. return nil, "", errors.New("gemini api_key not configured")
  1101. }
  1102. baseURL := account.GetGeminiBaseURL(geminicli.AIStudioBaseURL)
  1103. normalizedBaseURL, err := s.validateUpstreamBaseURL(baseURL)
  1104. if err != nil {
  1105. return nil, "", err
  1106. }
  1107. fullURL, err := buildGeminiAIStudioModelActionURL(normalizedBaseURL, mappedModel, upstreamAction, useUpstreamStream)
  1108. if err != nil {
  1109. return nil, "", err
  1110. }
  1111. upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(body))
  1112. if err != nil {
  1113. return nil, "", err
  1114. }
  1115. upstreamReq.Header.Set("Content-Type", "application/json")
  1116. upstreamReq.Header.Set("x-goog-api-key", apiKey)
  1117. return upstreamReq, "x-request-id", nil
  1118. }
  1119. requestIDHeader = "x-request-id"
  1120. case AccountTypeOAuth:
  1121. buildReq = func(ctx context.Context) (*http.Request, string, error) {
  1122. if s.tokenProvider == nil {
  1123. return nil, "", errors.New("gemini token provider not configured")
  1124. }
  1125. accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
  1126. if err != nil {
  1127. return nil, "", err
  1128. }
  1129. projectID := strings.TrimSpace(account.GetCredential("project_id"))
  1130. // Two modes for OAuth:
  1131. // 1. With project_id -> Code Assist API (wrapped request)
  1132. // 2. Without project_id -> AI Studio API (direct OAuth, like API key but with Bearer token)
  1133. if projectID != "" && !forceAIStudio {
  1134. // Mode 1: Code Assist API
  1135. baseURL, err := s.validateUpstreamBaseURL(geminicli.GeminiCliBaseURL)
  1136. if err != nil {
  1137. return nil, "", err
  1138. }
  1139. fullURL := fmt.Sprintf("%s/v1internal:%s", strings.TrimRight(baseURL, "/"), upstreamAction)
  1140. if useUpstreamStream {
  1141. fullURL += "?alt=sse"
  1142. }
  1143. wrapped := map[string]any{
  1144. "model": mappedModel,
  1145. "project": projectID,
  1146. }
  1147. var inner any
  1148. if err := json.Unmarshal(body, &inner); err != nil {
  1149. return nil, "", fmt.Errorf("failed to parse gemini request: %w", err)
  1150. }
  1151. wrapped["request"] = inner
  1152. wrappedBytes, _ := json.Marshal(wrapped)
  1153. upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(wrappedBytes))
  1154. if err != nil {
  1155. return nil, "", err
  1156. }
  1157. upstreamReq.Header.Set("Content-Type", "application/json")
  1158. upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
  1159. upstreamReq.Header.Set("User-Agent", geminicli.GeminiCLIUserAgent)
  1160. return upstreamReq, "x-request-id", nil
  1161. } else {
  1162. // Mode 2: AI Studio API with OAuth (like API key mode, but using Bearer token)
  1163. baseURL := account.GetGeminiBaseURL(geminicli.AIStudioBaseURL)
  1164. normalizedBaseURL, err := s.validateUpstreamBaseURL(baseURL)
  1165. if err != nil {
  1166. return nil, "", err
  1167. }
  1168. fullURL, err := buildGeminiAIStudioModelActionURL(normalizedBaseURL, mappedModel, upstreamAction, useUpstreamStream)
  1169. if err != nil {
  1170. return nil, "", err
  1171. }
  1172. upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(body))
  1173. if err != nil {
  1174. return nil, "", err
  1175. }
  1176. upstreamReq.Header.Set("Content-Type", "application/json")
  1177. upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
  1178. return upstreamReq, "x-request-id", nil
  1179. }
  1180. }
  1181. requestIDHeader = "x-request-id"
  1182. case AccountTypeServiceAccount:
  1183. buildReq = func(ctx context.Context) (*http.Request, string, error) {
  1184. if s.tokenProvider == nil {
  1185. return nil, "", errors.New("gemini token provider not configured")
  1186. }
  1187. accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
  1188. if err != nil {
  1189. return nil, "", err
  1190. }
  1191. fullURL, err := buildVertexGeminiURL(account.VertexProjectID(), account.VertexLocation(mappedModel), mappedModel, upstreamAction, useUpstreamStream)
  1192. if err != nil {
  1193. return nil, "", err
  1194. }
  1195. upstreamReq, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewReader(body))
  1196. if err != nil {
  1197. return nil, "", err
  1198. }
  1199. upstreamReq.Header.Set("Content-Type", "application/json")
  1200. upstreamReq.Header.Set("Authorization", "Bearer "+accessToken)
  1201. return upstreamReq, "x-request-id", nil
  1202. }
  1203. requestIDHeader = "x-request-id"
  1204. default:
  1205. return nil, s.writeGoogleError(c, http.StatusBadGateway, "Unsupported account type: "+account.Type)
  1206. }
  1207. var resp *http.Response
  1208. for attempt := 1; attempt <= geminiMaxRetries; attempt++ {
  1209. upstreamReq, idHeader, err := buildReq(ctx)
  1210. if err != nil {
  1211. if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
  1212. return nil, err
  1213. }
  1214. // Local build error: don't retry.
  1215. if strings.Contains(err.Error(), "missing project_id") {
  1216. return nil, s.writeGoogleError(c, http.StatusBadRequest, err.Error())
  1217. }
  1218. return nil, s.writeGoogleError(c, http.StatusBadGateway, err.Error())
  1219. }
  1220. requestIDHeader = idHeader
  1221. resp, err = s.httpUpstream.Do(WithAccountTrafficRequest(upstreamReq, account), proxyURL, account.ID, account.Concurrency)
  1222. if err != nil {
  1223. if local := AccountTrafficFailover(err); local != nil {
  1224. return nil, local
  1225. }
  1226. safeErr := sanitizeUpstreamErrorMessage(err.Error())
  1227. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1228. ProxyID: opsUpstreamProxyID(account),
  1229. ProxyName: opsUpstreamProxyName(account),
  1230. Platform: account.Platform,
  1231. AccountID: account.ID,
  1232. AccountName: account.Name,
  1233. UpstreamStatusCode: 0,
  1234. Kind: "request_error",
  1235. Message: safeErr,
  1236. })
  1237. if attempt < geminiMaxRetries {
  1238. logger.LegacyPrintf("service.gemini_messages_compat", "Gemini account %d: upstream request failed, retry %d/%d: %v", account.ID, attempt, geminiMaxRetries, err)
  1239. sleepGeminiBackoff(attempt)
  1240. continue
  1241. }
  1242. if action == "countTokens" {
  1243. estimated := estimateGeminiCountTokens(body)
  1244. c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated})
  1245. return &ForwardResult{
  1246. RequestID: "",
  1247. Usage: ClaudeUsage{},
  1248. Model: originalModel,
  1249. UpstreamModel: mappedModel,
  1250. Stream: false,
  1251. Duration: time.Since(startTime),
  1252. FirstTokenMs: nil,
  1253. }, nil
  1254. }
  1255. setOpsUpstreamError(c, 0, safeErr, "")
  1256. return nil, s.writeGoogleError(c, http.StatusBadGateway, "Upstream request failed after retries: "+safeErr)
  1257. }
  1258. // 错误策略优先:匹配则跳过重试直接处理。
  1259. if matched, rebuilt := s.checkErrorPolicyInLoop(ctx, account, resp, mappedModel); matched {
  1260. resp = rebuilt
  1261. break
  1262. } else {
  1263. resp = rebuilt
  1264. }
  1265. if resp.StatusCode >= 400 && s.shouldRetryGeminiUpstreamError(account, resp.StatusCode) {
  1266. respBody := s.readUpstreamErrorBody(resp)
  1267. _ = resp.Body.Close()
  1268. // Don't treat insufficient-scope as transient.
  1269. if resp.StatusCode == 403 && isGeminiInsufficientScope(resp.Header, respBody) {
  1270. resp = &http.Response{
  1271. StatusCode: resp.StatusCode,
  1272. Header: resp.Header.Clone(),
  1273. Body: io.NopCloser(bytes.NewReader(respBody)),
  1274. }
  1275. break
  1276. }
  1277. if resp.StatusCode == 429 {
  1278. s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  1279. }
  1280. if attempt < geminiMaxRetries {
  1281. upstreamReqID := resp.Header.Get(requestIDHeader)
  1282. if upstreamReqID == "" {
  1283. upstreamReqID = resp.Header.Get("x-goog-request-id")
  1284. }
  1285. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  1286. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  1287. upstreamDetail := ""
  1288. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  1289. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  1290. if maxBytes <= 0 {
  1291. maxBytes = 2048
  1292. }
  1293. upstreamDetail = truncateString(string(respBody), maxBytes)
  1294. }
  1295. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1296. ProxyID: opsUpstreamProxyID(account),
  1297. ProxyName: opsUpstreamProxyName(account),
  1298. Platform: account.Platform,
  1299. AccountID: account.ID,
  1300. AccountName: account.Name,
  1301. UpstreamStatusCode: resp.StatusCode,
  1302. UpstreamRequestID: upstreamReqID,
  1303. Kind: "retry",
  1304. Message: upstreamMsg,
  1305. Detail: upstreamDetail,
  1306. })
  1307. logger.LegacyPrintf("service.gemini_messages_compat", "Gemini account %d: upstream status %d, retry %d/%d", account.ID, resp.StatusCode, attempt, geminiMaxRetries)
  1308. sleepGeminiBackoff(attempt)
  1309. continue
  1310. }
  1311. if action == "countTokens" {
  1312. estimated := estimateGeminiCountTokens(body)
  1313. c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated})
  1314. return &ForwardResult{
  1315. RequestID: "",
  1316. Usage: ClaudeUsage{},
  1317. Model: originalModel,
  1318. UpstreamModel: mappedModel,
  1319. Stream: false,
  1320. Duration: time.Since(startTime),
  1321. FirstTokenMs: nil,
  1322. }, nil
  1323. }
  1324. // Final attempt: surface the upstream error body (passed through below) instead of a generic retry error.
  1325. resp = &http.Response{
  1326. StatusCode: resp.StatusCode,
  1327. Header: resp.Header.Clone(),
  1328. Body: io.NopCloser(bytes.NewReader(respBody)),
  1329. }
  1330. break
  1331. }
  1332. break
  1333. }
  1334. defer func() { _ = resp.Body.Close() }()
  1335. requestID := resp.Header.Get(requestIDHeader)
  1336. if requestID == "" {
  1337. requestID = resp.Header.Get("x-goog-request-id")
  1338. }
  1339. if requestID != "" {
  1340. c.Header("x-request-id", requestID)
  1341. }
  1342. isOAuth := account.Type == AccountTypeOAuth
  1343. if resp.StatusCode >= 400 {
  1344. respBody := s.readUpstreamErrorBody(resp)
  1345. // Best-effort fallback for OAuth tokens missing AI Studio scopes when calling countTokens.
  1346. // This avoids Gemini SDKs failing hard during preflight token counting.
  1347. // Checked before error policy so it always works regardless of custom error codes.
  1348. if action == "countTokens" && isOAuth && isGeminiInsufficientScope(resp.Header, respBody) {
  1349. estimated := estimateGeminiCountTokens(body)
  1350. c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated})
  1351. return &ForwardResult{
  1352. RequestID: requestID,
  1353. UpstreamHeaders: resp.Header,
  1354. Usage: ClaudeUsage{},
  1355. Model: originalModel,
  1356. UpstreamModel: mappedModel,
  1357. Stream: false,
  1358. Duration: time.Since(startTime),
  1359. FirstTokenMs: nil,
  1360. }, nil
  1361. }
  1362. // 统一错误策略:自定义错误码 + 临时不可调度
  1363. if s.rateLimitService != nil {
  1364. policy := s.rateLimitService.CheckErrorPolicy(ctx, account, resp.StatusCode, respBody, mappedModel)
  1365. switch policy {
  1366. case ErrorPolicySkipped:
  1367. if failoverErr := s.skippedErrorPolicyFailoverError(c, account, resp.StatusCode, respBody, requestID); failoverErr != nil {
  1368. return nil, failoverErr
  1369. }
  1370. if account.IsCustomErrorCodesEnabled() {
  1371. return nil, s.writeGeminiCustomCodeSkippedError(c, account, resp.StatusCode, requestID, respBody, func() {
  1372. _ = s.writeGoogleError(c, http.StatusInternalServerError, geminiCustomCodeSkippedClientMessage)
  1373. })
  1374. }
  1375. // 池模式:客户端写出与 ErrorPolicyNone 相同(状态码/响应体保真),仅跳过账号状态标记。
  1376. return nil, s.writeGeminiNativeUpstreamError(c, account, resp, respBody, requestID, isOAuth)
  1377. case ErrorPolicyMatched, ErrorPolicyTempUnscheduled:
  1378. if policy == ErrorPolicyMatched {
  1379. s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  1380. }
  1381. evBody := unwrapIfNeeded(isOAuth, respBody)
  1382. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(evBody))
  1383. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  1384. upstreamDetail := ""
  1385. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  1386. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  1387. if maxBytes <= 0 {
  1388. maxBytes = 2048
  1389. }
  1390. upstreamDetail = truncateString(string(evBody), maxBytes)
  1391. }
  1392. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1393. ProxyID: opsUpstreamProxyID(account),
  1394. ProxyName: opsUpstreamProxyName(account),
  1395. Platform: account.Platform,
  1396. AccountID: account.ID,
  1397. AccountName: account.Name,
  1398. UpstreamStatusCode: resp.StatusCode,
  1399. UpstreamRequestID: requestID,
  1400. Kind: "failover",
  1401. Message: upstreamMsg,
  1402. Detail: upstreamDetail,
  1403. })
  1404. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: respBody}
  1405. }
  1406. }
  1407. // ErrorPolicyNone → 原有逻辑
  1408. s.handleGeminiUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody)
  1409. // 精确匹配服务端配置类 400 错误,触发 failover + 临时封禁
  1410. if resp.StatusCode == http.StatusBadRequest {
  1411. msg400 := strings.ToLower(strings.TrimSpace(extractUpstreamErrorMessage(respBody)))
  1412. if isGoogleProjectConfigError(msg400) {
  1413. evBody := unwrapIfNeeded(isOAuth, respBody)
  1414. upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(evBody)))
  1415. upstreamDetail := ""
  1416. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  1417. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  1418. if maxBytes <= 0 {
  1419. maxBytes = 2048
  1420. }
  1421. upstreamDetail = truncateString(string(evBody), maxBytes)
  1422. }
  1423. log.Printf("[Gemini] status=400 google_config_error failover=true upstream_message=%q account=%d", upstreamMsg, account.ID)
  1424. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1425. ProxyID: opsUpstreamProxyID(account),
  1426. ProxyName: opsUpstreamProxyName(account),
  1427. Platform: account.Platform,
  1428. AccountID: account.ID,
  1429. AccountName: account.Name,
  1430. UpstreamStatusCode: resp.StatusCode,
  1431. UpstreamRequestID: requestID,
  1432. Kind: "failover",
  1433. Message: upstreamMsg,
  1434. Detail: upstreamDetail,
  1435. })
  1436. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: evBody, RetryableOnSameAccount: true}
  1437. }
  1438. }
  1439. if s.shouldFailoverGeminiUpstreamError(resp.StatusCode) {
  1440. evBody := unwrapIfNeeded(isOAuth, respBody)
  1441. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(evBody))
  1442. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  1443. upstreamDetail := ""
  1444. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  1445. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  1446. if maxBytes <= 0 {
  1447. maxBytes = 2048
  1448. }
  1449. upstreamDetail = truncateString(string(evBody), maxBytes)
  1450. }
  1451. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1452. ProxyID: opsUpstreamProxyID(account),
  1453. ProxyName: opsUpstreamProxyName(account),
  1454. Platform: account.Platform,
  1455. AccountID: account.ID,
  1456. AccountName: account.Name,
  1457. UpstreamStatusCode: resp.StatusCode,
  1458. UpstreamRequestID: requestID,
  1459. Kind: "failover",
  1460. Message: upstreamMsg,
  1461. Detail: upstreamDetail,
  1462. })
  1463. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: evBody}
  1464. }
  1465. return nil, s.writeGeminiNativeUpstreamError(c, account, resp, respBody, requestID, isOAuth)
  1466. }
  1467. var usage *ClaudeUsage
  1468. var firstTokenMs *int
  1469. if stream {
  1470. streamRes, err := s.handleNativeStreamingResponse(c, resp, startTime, isOAuth)
  1471. if err != nil {
  1472. return nil, err
  1473. }
  1474. usage = streamRes.usage
  1475. firstTokenMs = streamRes.firstTokenMs
  1476. } else {
  1477. if useUpstreamStream {
  1478. collected, usageObj, err := collectGeminiSSE(resp.Body, isOAuth)
  1479. if err != nil {
  1480. return nil, s.writeGoogleError(c, http.StatusBadGateway, "Failed to read upstream stream")
  1481. }
  1482. b, _ := json.Marshal(collected)
  1483. upstreamResponseModelObserverFromContext(c).ObserveGemini(b)
  1484. observeGeminiImageOutputs(c, b)
  1485. c.Data(http.StatusOK, "application/json", b)
  1486. usage = usageObj
  1487. } else {
  1488. usageResp, err := s.handleNativeNonStreamingResponse(c, resp, isOAuth)
  1489. if err != nil {
  1490. return nil, err
  1491. }
  1492. usage = usageResp
  1493. }
  1494. }
  1495. if usage == nil {
  1496. usage = &ClaudeUsage{}
  1497. }
  1498. // 图片生成计费
  1499. imageInputSize := s.extractImageInputSize(body)
  1500. imageSize := normalizeOpenAIImageSizeTier(imageInputSize)
  1501. imageCount := resolveGeminiImageCount(c, originalModel, mappedModel)
  1502. return &ForwardResult{
  1503. RequestID: requestID,
  1504. UpstreamHeaders: resp.Header,
  1505. Usage: *usage,
  1506. Model: originalModel,
  1507. UpstreamModel: mappedModel,
  1508. UpstreamResponseModel: observedUpstreamResponseModel(c),
  1509. UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
  1510. Stream: stream,
  1511. Duration: time.Since(startTime),
  1512. FirstTokenMs: firstTokenMs,
  1513. ImageCount: imageCount,
  1514. ImageSize: imageSize,
  1515. ImageInputSize: imageInputSize,
  1516. }, nil
  1517. }
  1518. // checkErrorPolicyInLoop 在重试循环内预检查错误策略。
  1519. // 返回 true 表示策略已匹配(调用者应 break),resp 已重建可直接使用。
  1520. // 返回 false 表示 ErrorPolicyNone,resp 已重建,调用者继续走重试逻辑。
  1521. func (s *GeminiMessagesCompatService) checkErrorPolicyInLoop(
  1522. ctx context.Context, account *Account, resp *http.Response, mappedModel string,
  1523. ) (matched bool, rebuilt *http.Response) {
  1524. if resp.StatusCode < 400 || s.rateLimitService == nil {
  1525. return false, resp
  1526. }
  1527. body := s.readUpstreamErrorBody(resp)
  1528. _ = resp.Body.Close()
  1529. rebuilt = &http.Response{
  1530. StatusCode: resp.StatusCode,
  1531. Header: resp.Header.Clone(),
  1532. Body: io.NopCloser(bytes.NewReader(body)),
  1533. }
  1534. policy := s.rateLimitService.CheckErrorPolicy(ctx, account, resp.StatusCode, body, mappedModel)
  1535. return policy != ErrorPolicyNone, rebuilt
  1536. }
  1537. func (s *GeminiMessagesCompatService) shouldRetryGeminiUpstreamError(account *Account, statusCode int) bool {
  1538. switch statusCode {
  1539. case 429, 500, 502, 503, 504, 529:
  1540. return true
  1541. case 403:
  1542. // GeminiCli OAuth occasionally returns 403 transiently (activation/quota propagation); allow retry.
  1543. if account == nil || account.Type != AccountTypeOAuth {
  1544. return false
  1545. }
  1546. oauthType := strings.ToLower(strings.TrimSpace(account.GetCredential("oauth_type")))
  1547. if oauthType == "" && strings.TrimSpace(account.GetCredential("project_id")) != "" {
  1548. // Legacy/implicit Code Assist OAuth accounts.
  1549. oauthType = "code_assist"
  1550. }
  1551. return oauthType == "code_assist"
  1552. default:
  1553. return false
  1554. }
  1555. }
  1556. func (s *GeminiMessagesCompatService) shouldFailoverGeminiUpstreamError(statusCode int) bool {
  1557. switch statusCode {
  1558. case 401, 403, 429, 529:
  1559. return true
  1560. default:
  1561. return statusCode >= 500
  1562. }
  1563. }
  1564. // skippedErrorPolicyFailoverError 命中 ErrorPolicySkipped(池模式、或自定义错误码未命中)
  1565. // 时构造 failover 错误:可 failover 的状态码返回 UpstreamFailoverError,交给 handler 层换号
  1566. // (池模式账号按 pool_mode_retry_count 先同账号重试);返回 nil 表示状态码不可 failover,
  1567. // 由调用方决定客户端写出。Skipped 只豁免账号状态标记,不豁免换号,与 OpenAI 网关路径一致。
  1568. func (s *GeminiMessagesCompatService) skippedErrorPolicyFailoverError(c *gin.Context, account *Account, statusCode int, respBody []byte, upstreamRequestID string) *UpstreamFailoverError {
  1569. if !s.shouldFailoverGeminiUpstreamError(statusCode) {
  1570. return nil
  1571. }
  1572. upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(respBody)))
  1573. upstreamDetail := s.upstreamErrorDetail(respBody)
  1574. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1575. ProxyID: opsUpstreamProxyID(account),
  1576. ProxyName: opsUpstreamProxyName(account),
  1577. Platform: account.Platform,
  1578. AccountID: account.ID,
  1579. AccountName: account.Name,
  1580. UpstreamStatusCode: statusCode,
  1581. UpstreamRequestID: upstreamRequestID,
  1582. Kind: "failover",
  1583. Message: upstreamMsg,
  1584. Detail: upstreamDetail,
  1585. })
  1586. return &UpstreamFailoverError{
  1587. StatusCode: statusCode,
  1588. ResponseBody: respBody,
  1589. RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(statusCode),
  1590. }
  1591. }
  1592. // geminiCustomCodeSkippedClientMessage 自定义错误码未命中时对客户端隐藏上游细节的固定文案,
  1593. // 与 OpenAI 网关路径同场景的文案一致。
  1594. const geminiCustomCodeSkippedClientMessage = "Upstream gateway error"
  1595. // upstreamErrorDetail 按配置截断上游错误响应体,用于 ops 错误日志的 Detail 字段;
  1596. // 未开启 LogUpstreamErrorBody 时返回空。
  1597. func (s *GeminiMessagesCompatService) upstreamErrorDetail(body []byte) string {
  1598. if s.cfg == nil || !s.cfg.Gateway.LogUpstreamErrorBody {
  1599. return ""
  1600. }
  1601. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  1602. if maxBytes <= 0 {
  1603. maxBytes = 2048
  1604. }
  1605. return truncateString(string(body), maxBytes)
  1606. }
  1607. // writeGeminiCustomCodeSkippedError 处理自定义错误码未命中且不可 failover 的上游错误:
  1608. // 客户端统一收到 500 + 固定文案(由 write 按端点格式写出),不透传上游细节;
  1609. // 上游真实状态码与错误信息仅记录到 ops 错误日志。
  1610. func (s *GeminiMessagesCompatService) writeGeminiCustomCodeSkippedError(c *gin.Context, account *Account, upstreamStatus int, upstreamRequestID string, body []byte, write func()) error {
  1611. upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(body)))
  1612. upstreamDetail := s.upstreamErrorDetail(body)
  1613. setOpsUpstreamError(c, upstreamStatus, upstreamMsg, upstreamDetail)
  1614. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1615. ProxyID: opsUpstreamProxyID(account),
  1616. ProxyName: opsUpstreamProxyName(account),
  1617. Platform: account.Platform,
  1618. AccountID: account.ID,
  1619. AccountName: account.Name,
  1620. UpstreamStatusCode: upstreamStatus,
  1621. UpstreamRequestID: upstreamRequestID,
  1622. Kind: "http_error",
  1623. Message: upstreamMsg,
  1624. Detail: upstreamDetail,
  1625. })
  1626. write()
  1627. if upstreamMsg == "" {
  1628. return fmt.Errorf("gemini upstream error: %d (not in custom error codes)", upstreamStatus)
  1629. }
  1630. return fmt.Errorf("gemini upstream error: %d (not in custom error codes) message=%s", upstreamStatus, upstreamMsg)
  1631. }
  1632. // writeGeminiNativeUpstreamError 将不可 failover 的上游错误按原始状态码与响应体透传给客户端,
  1633. // 并记录 ops 错误事件。状态码保真:下游据此区分请求级错误与可重试的链路故障。
  1634. func (s *GeminiMessagesCompatService) writeGeminiNativeUpstreamError(c *gin.Context, account *Account, resp *http.Response, respBody []byte, requestID string, isOAuth bool) error {
  1635. respBody = unwrapIfNeeded(isOAuth, respBody)
  1636. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(respBody))
  1637. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  1638. upstreamDetail := s.upstreamErrorDetail(respBody)
  1639. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  1640. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini] native upstream error %d: %s", resp.StatusCode, truncateForLog(respBody, s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes))
  1641. }
  1642. setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
  1643. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1644. ProxyID: opsUpstreamProxyID(account),
  1645. ProxyName: opsUpstreamProxyName(account),
  1646. Platform: account.Platform,
  1647. AccountID: account.ID,
  1648. AccountName: account.Name,
  1649. UpstreamStatusCode: resp.StatusCode,
  1650. UpstreamRequestID: requestID,
  1651. Kind: "http_error",
  1652. Message: upstreamMsg,
  1653. Detail: upstreamDetail,
  1654. })
  1655. contentType := resp.Header.Get("Content-Type")
  1656. if contentType == "" {
  1657. contentType = "application/json"
  1658. }
  1659. MarkResponseCommitted(c)
  1660. c.Data(resp.StatusCode, contentType, respBody)
  1661. if upstreamMsg == "" {
  1662. return fmt.Errorf("gemini upstream error: %d", resp.StatusCode)
  1663. }
  1664. return fmt.Errorf("gemini upstream error: %d message=%s", resp.StatusCode, upstreamMsg)
  1665. }
  1666. func sleepGeminiBackoff(attempt int) {
  1667. delay := geminiRetryBaseDelay * time.Duration(1<<uint(attempt-1))
  1668. if delay > geminiRetryMaxDelay {
  1669. delay = geminiRetryMaxDelay
  1670. }
  1671. // +/- 20% jitter
  1672. r := mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
  1673. jitter := time.Duration(float64(delay) * 0.2 * (r.Float64()*2 - 1))
  1674. sleepFor := delay + jitter
  1675. if sleepFor < 0 {
  1676. sleepFor = 0
  1677. }
  1678. time.Sleep(sleepFor)
  1679. }
  1680. var (
  1681. sensitiveQueryParamRegex = regexp.MustCompile(`(?i)([?&](?:key|client_secret|access_token|refresh_token)=)[^&"\s]+`)
  1682. retryInRegex = regexp.MustCompile(`Please retry in ([0-9.]+)s`)
  1683. )
  1684. func sanitizeUpstreamErrorMessage(msg string) string {
  1685. if msg == "" {
  1686. return msg
  1687. }
  1688. return sensitiveQueryParamRegex.ReplaceAllString(msg, `$1***`)
  1689. }
  1690. func (s *GeminiMessagesCompatService) writeGeminiMappedError(c *gin.Context, account *Account, upstreamStatus int, upstreamRequestID string, body []byte) error {
  1691. MarkResponseCommitted(c)
  1692. upstreamMsg := strings.TrimSpace(extractUpstreamErrorMessage(body))
  1693. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  1694. upstreamDetail := ""
  1695. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  1696. maxBytes := s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
  1697. if maxBytes <= 0 {
  1698. maxBytes = 2048
  1699. }
  1700. upstreamDetail = truncateString(string(body), maxBytes)
  1701. }
  1702. setOpsUpstreamError(c, upstreamStatus, upstreamMsg, upstreamDetail)
  1703. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  1704. ProxyID: opsUpstreamProxyID(account),
  1705. ProxyName: opsUpstreamProxyName(account),
  1706. Platform: account.Platform,
  1707. AccountID: account.ID,
  1708. AccountName: account.Name,
  1709. UpstreamStatusCode: upstreamStatus,
  1710. UpstreamRequestID: upstreamRequestID,
  1711. Kind: "http_error",
  1712. Message: upstreamMsg,
  1713. Detail: upstreamDetail,
  1714. })
  1715. if s.cfg != nil && s.cfg.Gateway.LogUpstreamErrorBody {
  1716. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini] upstream error %d: %s", upstreamStatus, truncateForLog(body, s.cfg.Gateway.LogUpstreamErrorBodyMaxBytes))
  1717. }
  1718. if status, errType, errMsg, matched := applyErrorPassthroughRule(
  1719. c,
  1720. PlatformGemini,
  1721. upstreamStatus,
  1722. body,
  1723. http.StatusBadGateway,
  1724. "upstream_error",
  1725. "Upstream request failed",
  1726. ); matched {
  1727. c.JSON(status, gin.H{
  1728. "type": "error",
  1729. "error": gin.H{"type": errType, "message": errMsg},
  1730. })
  1731. if upstreamMsg == "" {
  1732. upstreamMsg = errMsg
  1733. }
  1734. if upstreamMsg == "" {
  1735. return fmt.Errorf("upstream error: %d (passthrough rule matched)", upstreamStatus)
  1736. }
  1737. return fmt.Errorf("upstream error: %d (passthrough rule matched) message=%s", upstreamStatus, upstreamMsg)
  1738. }
  1739. var statusCode int
  1740. var errType, errMsg string
  1741. if mapped := mapGeminiErrorBodyToClaudeError(body); mapped != nil {
  1742. errType = mapped.Type
  1743. if mapped.Message != "" {
  1744. errMsg = mapped.Message
  1745. }
  1746. if mapped.StatusCode > 0 {
  1747. statusCode = mapped.StatusCode
  1748. }
  1749. }
  1750. switch upstreamStatus {
  1751. case 400:
  1752. if statusCode == 0 {
  1753. statusCode = http.StatusBadRequest
  1754. }
  1755. if errType == "" {
  1756. errType = "invalid_request_error"
  1757. }
  1758. // 400 是确定性的请求错误:回传上游 message(已脱敏),客户端据此定位非法字段。
  1759. if errMsg == "" {
  1760. errMsg = upstreamMsg
  1761. }
  1762. if errMsg == "" {
  1763. errMsg = "Invalid request"
  1764. }
  1765. case 401:
  1766. if statusCode == 0 {
  1767. statusCode = http.StatusBadGateway
  1768. }
  1769. if errType == "" {
  1770. errType = "authentication_error"
  1771. }
  1772. if errMsg == "" {
  1773. errMsg = "Upstream authentication failed, please contact administrator"
  1774. }
  1775. case 403:
  1776. if statusCode == 0 {
  1777. statusCode = http.StatusBadGateway
  1778. }
  1779. if errType == "" {
  1780. errType = "permission_error"
  1781. }
  1782. if errMsg == "" {
  1783. errMsg = "Upstream access forbidden, please contact administrator"
  1784. }
  1785. case 404:
  1786. if statusCode == 0 {
  1787. statusCode = http.StatusNotFound
  1788. }
  1789. if errType == "" {
  1790. errType = "not_found_error"
  1791. }
  1792. if errMsg == "" {
  1793. errMsg = "Resource not found"
  1794. }
  1795. case 429:
  1796. if statusCode == 0 {
  1797. statusCode = http.StatusTooManyRequests
  1798. }
  1799. if errType == "" {
  1800. errType = "rate_limit_error"
  1801. }
  1802. if errMsg == "" {
  1803. errMsg = "Upstream rate limit exceeded, please retry later"
  1804. }
  1805. case 529:
  1806. if statusCode == 0 {
  1807. statusCode = http.StatusServiceUnavailable
  1808. }
  1809. if errType == "" {
  1810. errType = "overloaded_error"
  1811. }
  1812. if errMsg == "" {
  1813. errMsg = "Upstream service overloaded, please retry later"
  1814. }
  1815. case 500, 502, 503, 504:
  1816. if statusCode == 0 {
  1817. statusCode = http.StatusBadGateway
  1818. }
  1819. if errType == "" {
  1820. switch upstreamStatus {
  1821. case 504:
  1822. errType = "timeout_error"
  1823. case 503:
  1824. errType = "overloaded_error"
  1825. default:
  1826. errType = "api_error"
  1827. }
  1828. }
  1829. if errMsg == "" {
  1830. errMsg = "Upstream service temporarily unavailable"
  1831. }
  1832. default:
  1833. if statusCode == 0 {
  1834. statusCode = http.StatusBadGateway
  1835. }
  1836. if errType == "" {
  1837. errType = "upstream_error"
  1838. }
  1839. if errMsg == "" {
  1840. errMsg = "Upstream request failed"
  1841. }
  1842. }
  1843. c.JSON(statusCode, gin.H{
  1844. "type": "error",
  1845. "error": gin.H{"type": errType, "message": errMsg},
  1846. })
  1847. if upstreamMsg == "" {
  1848. return fmt.Errorf("upstream error: %d", upstreamStatus)
  1849. }
  1850. return fmt.Errorf("upstream error: %d message=%s", upstreamStatus, upstreamMsg)
  1851. }
  1852. type claudeErrorMapping struct {
  1853. Type string
  1854. Message string
  1855. StatusCode int
  1856. }
  1857. func mapGeminiErrorBodyToClaudeError(body []byte) *claudeErrorMapping {
  1858. if len(body) == 0 {
  1859. return nil
  1860. }
  1861. var parsed struct {
  1862. Error struct {
  1863. Code int `json:"code"`
  1864. Message string `json:"message"`
  1865. Status string `json:"status"`
  1866. } `json:"error"`
  1867. }
  1868. if err := json.Unmarshal(body, &parsed); err != nil {
  1869. return nil
  1870. }
  1871. if strings.TrimSpace(parsed.Error.Status) == "" && parsed.Error.Code == 0 && strings.TrimSpace(parsed.Error.Message) == "" {
  1872. return nil
  1873. }
  1874. mapped := &claudeErrorMapping{
  1875. Type: mapGeminiStatusToClaudeErrorType(parsed.Error.Status),
  1876. Message: "",
  1877. }
  1878. if mapped.Type == "" {
  1879. mapped.Type = "upstream_error"
  1880. }
  1881. switch strings.ToUpper(strings.TrimSpace(parsed.Error.Status)) {
  1882. case "INVALID_ARGUMENT":
  1883. mapped.StatusCode = http.StatusBadRequest
  1884. case "NOT_FOUND":
  1885. mapped.StatusCode = http.StatusNotFound
  1886. case "RESOURCE_EXHAUSTED":
  1887. mapped.StatusCode = http.StatusTooManyRequests
  1888. default:
  1889. // Keep StatusCode unset and let HTTP status mapping decide.
  1890. }
  1891. // Keep messages generic by default; upstream error message can be long or include sensitive fragments.
  1892. return mapped
  1893. }
  1894. func mapGeminiStatusToClaudeErrorType(status string) string {
  1895. switch strings.ToUpper(strings.TrimSpace(status)) {
  1896. case "INVALID_ARGUMENT":
  1897. return "invalid_request_error"
  1898. case "PERMISSION_DENIED":
  1899. return "permission_error"
  1900. case "NOT_FOUND":
  1901. return "not_found_error"
  1902. case "RESOURCE_EXHAUSTED":
  1903. return "rate_limit_error"
  1904. case "UNAUTHENTICATED":
  1905. return "authentication_error"
  1906. case "UNAVAILABLE":
  1907. return "overloaded_error"
  1908. case "INTERNAL":
  1909. return "api_error"
  1910. case "DEADLINE_EXCEEDED":
  1911. return "timeout_error"
  1912. default:
  1913. return ""
  1914. }
  1915. }
  1916. type geminiStreamResult struct {
  1917. usage *ClaudeUsage
  1918. firstTokenMs *int
  1919. }
  1920. func (s *GeminiMessagesCompatService) handleNonStreamingResponse(c *gin.Context, resp *http.Response, originalModel string) (*ClaudeUsage, error) {
  1921. body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
  1922. if err != nil {
  1923. return nil, s.writeClaudeError(c, http.StatusBadGateway, "upstream_error", "Failed to read upstream response")
  1924. }
  1925. unwrappedBody, err := unwrapGeminiResponse(body)
  1926. if err != nil {
  1927. return nil, s.writeClaudeError(c, http.StatusBadGateway, "upstream_error", "Failed to parse upstream response")
  1928. }
  1929. observer := upstreamResponseModelObserverFromContext(c)
  1930. if observer == nil {
  1931. observer = beginUpstreamResponseModelObservation(c)
  1932. }
  1933. observer.ObserveGemini(unwrappedBody)
  1934. observeGeminiImageOutputs(c, unwrappedBody)
  1935. var geminiResp map[string]any
  1936. if err := json.Unmarshal(unwrappedBody, &geminiResp); err != nil {
  1937. return nil, s.writeClaudeError(c, http.StatusBadGateway, "upstream_error", "Failed to parse upstream response")
  1938. }
  1939. claudeResp, usage := convertGeminiToClaudeMessage(geminiResp, originalModel, unwrappedBody, false)
  1940. c.JSON(http.StatusOK, claudeResp)
  1941. return usage, nil
  1942. }
  1943. func (s *GeminiMessagesCompatService) handleStreamingResponse(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string) (*geminiStreamResult, error) {
  1944. c.Header("Content-Type", "text/event-stream")
  1945. c.Header("Cache-Control", "no-cache")
  1946. c.Header("Connection", "keep-alive")
  1947. c.Header("X-Accel-Buffering", "no")
  1948. c.Status(http.StatusOK)
  1949. flusher, ok := c.Writer.(http.Flusher)
  1950. if !ok {
  1951. return nil, errors.New("streaming not supported")
  1952. }
  1953. messageID := generateAnthropicMsgID()
  1954. messageStart := map[string]any{
  1955. "type": "message_start",
  1956. "message": map[string]any{
  1957. "id": messageID,
  1958. "type": "message",
  1959. "role": "assistant",
  1960. "model": originalModel,
  1961. "content": []any{},
  1962. "stop_reason": nil,
  1963. "stop_sequence": nil,
  1964. "usage": map[string]any{
  1965. "input_tokens": 0,
  1966. "output_tokens": 0,
  1967. },
  1968. },
  1969. }
  1970. writeSSE(c.Writer, "message_start", messageStart)
  1971. flusher.Flush()
  1972. var firstTokenMs *int
  1973. var usage ClaudeUsage
  1974. finishReason := ""
  1975. sawToolUse := false
  1976. nextBlockIndex := 0
  1977. openBlockIndex := -1
  1978. openBlockType := ""
  1979. seenText := ""
  1980. openToolIndex := -1
  1981. openToolID := ""
  1982. openToolName := ""
  1983. seenToolJSON := ""
  1984. reader := bufio.NewReader(resp.Body)
  1985. for {
  1986. line, err := reader.ReadString('\n')
  1987. if err != nil && !errors.Is(err, io.EOF) {
  1988. return nil, fmt.Errorf("stream read error: %w", err)
  1989. }
  1990. if !strings.HasPrefix(line, "data:") {
  1991. if errors.Is(err, io.EOF) {
  1992. break
  1993. }
  1994. continue
  1995. }
  1996. payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
  1997. if payload == "" || payload == "[DONE]" {
  1998. if errors.Is(err, io.EOF) {
  1999. break
  2000. }
  2001. continue
  2002. }
  2003. unwrappedBytes, err := unwrapGeminiResponse([]byte(payload))
  2004. if err != nil {
  2005. continue
  2006. }
  2007. observer := upstreamResponseModelObserverFromContext(c)
  2008. if observer == nil {
  2009. observer = beginUpstreamResponseModelObservation(c)
  2010. }
  2011. observer.ObserveGemini(unwrappedBytes)
  2012. observeGeminiImageOutputs(c, unwrappedBytes)
  2013. var geminiResp map[string]any
  2014. if err := json.Unmarshal(unwrappedBytes, &geminiResp); err != nil {
  2015. continue
  2016. }
  2017. if fr := extractGeminiFinishReason(geminiResp); fr != "" {
  2018. finishReason = fr
  2019. }
  2020. parts := extractGeminiParts(geminiResp)
  2021. for _, part := range parts {
  2022. if text, ok := part["text"].(string); ok && text != "" {
  2023. // Close an open tool_use block before starting text, mirroring
  2024. // the functionCall branch (which closes open text blocks) and
  2025. // the chat-completions sibling's closeOpenTool(). Otherwise a
  2026. // tool→text sequence keeps the tool_use block open while the
  2027. // text block starts, emitting overlapping Anthropic content
  2028. // blocks that violate the SSE contract.
  2029. if openToolIndex >= 0 {
  2030. writeSSE(c.Writer, "content_block_stop", map[string]any{
  2031. "type": "content_block_stop",
  2032. "index": openToolIndex,
  2033. })
  2034. openToolIndex = -1
  2035. openToolName = ""
  2036. seenToolJSON = ""
  2037. }
  2038. delta, newSeen := computeGeminiTextDelta(seenText, text)
  2039. seenText = newSeen
  2040. if delta == "" {
  2041. continue
  2042. }
  2043. if openBlockType != "text" {
  2044. if openBlockIndex >= 0 {
  2045. writeSSE(c.Writer, "content_block_stop", map[string]any{
  2046. "type": "content_block_stop",
  2047. "index": openBlockIndex,
  2048. })
  2049. }
  2050. openBlockType = "text"
  2051. openBlockIndex = nextBlockIndex
  2052. nextBlockIndex++
  2053. writeSSE(c.Writer, "content_block_start", map[string]any{
  2054. "type": "content_block_start",
  2055. "index": openBlockIndex,
  2056. "content_block": map[string]any{
  2057. "type": "text",
  2058. "text": "",
  2059. },
  2060. })
  2061. }
  2062. if firstTokenMs == nil {
  2063. ms := int(time.Since(startTime).Milliseconds())
  2064. firstTokenMs = &ms
  2065. }
  2066. writeSSE(c.Writer, "content_block_delta", map[string]any{
  2067. "type": "content_block_delta",
  2068. "index": openBlockIndex,
  2069. "delta": map[string]any{
  2070. "type": "text_delta",
  2071. "text": delta,
  2072. },
  2073. })
  2074. flusher.Flush()
  2075. continue
  2076. }
  2077. if fc, ok := part["functionCall"].(map[string]any); ok && fc != nil {
  2078. name, _ := fc["name"].(string)
  2079. args := fc["args"]
  2080. if strings.TrimSpace(name) == "" {
  2081. name = "tool"
  2082. }
  2083. // Close any open text block before tool_use.
  2084. if openBlockIndex >= 0 {
  2085. writeSSE(c.Writer, "content_block_stop", map[string]any{
  2086. "type": "content_block_stop",
  2087. "index": openBlockIndex,
  2088. })
  2089. openBlockIndex = -1
  2090. openBlockType = ""
  2091. }
  2092. // If we receive streamed tool args in pieces, keep a single tool block open and emit deltas.
  2093. if openToolIndex >= 0 && openToolName != name {
  2094. writeSSE(c.Writer, "content_block_stop", map[string]any{
  2095. "type": "content_block_stop",
  2096. "index": openToolIndex,
  2097. })
  2098. openToolIndex = -1
  2099. openToolName = ""
  2100. seenToolJSON = ""
  2101. }
  2102. if openToolIndex < 0 {
  2103. openToolID = "toolu_" + randomHex(8)
  2104. openToolIndex = nextBlockIndex
  2105. openToolName = name
  2106. nextBlockIndex++
  2107. sawToolUse = true
  2108. writeSSE(c.Writer, "content_block_start", map[string]any{
  2109. "type": "content_block_start",
  2110. "index": openToolIndex,
  2111. "content_block": map[string]any{
  2112. "type": "tool_use",
  2113. "id": openToolID,
  2114. "name": name,
  2115. "input": map[string]any{},
  2116. },
  2117. })
  2118. }
  2119. argsJSONText := "{}"
  2120. switch v := args.(type) {
  2121. case nil:
  2122. // keep default "{}"
  2123. case string:
  2124. if strings.TrimSpace(v) != "" {
  2125. argsJSONText = v
  2126. }
  2127. default:
  2128. if b, err := json.Marshal(args); err == nil && len(b) > 0 {
  2129. argsJSONText = string(b)
  2130. }
  2131. }
  2132. delta, newSeen := computeGeminiTextDelta(seenToolJSON, argsJSONText)
  2133. seenToolJSON = newSeen
  2134. if delta != "" {
  2135. writeSSE(c.Writer, "content_block_delta", map[string]any{
  2136. "type": "content_block_delta",
  2137. "index": openToolIndex,
  2138. "delta": map[string]any{
  2139. "type": "input_json_delta",
  2140. "partial_json": delta,
  2141. },
  2142. })
  2143. }
  2144. flusher.Flush()
  2145. }
  2146. }
  2147. if u := extractGeminiUsage(unwrappedBytes); u != nil {
  2148. usage = *u
  2149. }
  2150. // Process the final unterminated line at EOF as well.
  2151. if errors.Is(err, io.EOF) {
  2152. break
  2153. }
  2154. }
  2155. if openBlockIndex >= 0 {
  2156. writeSSE(c.Writer, "content_block_stop", map[string]any{
  2157. "type": "content_block_stop",
  2158. "index": openBlockIndex,
  2159. })
  2160. }
  2161. if openToolIndex >= 0 {
  2162. writeSSE(c.Writer, "content_block_stop", map[string]any{
  2163. "type": "content_block_stop",
  2164. "index": openToolIndex,
  2165. })
  2166. }
  2167. stopReason := mapGeminiFinishReasonToClaudeStopReason(finishReason)
  2168. if sawToolUse {
  2169. stopReason = "tool_use"
  2170. }
  2171. usageObj := map[string]any{
  2172. "output_tokens": usage.OutputTokens,
  2173. }
  2174. if usage.InputTokens > 0 {
  2175. usageObj["input_tokens"] = usage.InputTokens
  2176. }
  2177. writeSSE(c.Writer, "message_delta", map[string]any{
  2178. "type": "message_delta",
  2179. "delta": map[string]any{
  2180. "stop_reason": stopReason,
  2181. "stop_sequence": nil,
  2182. },
  2183. "usage": usageObj,
  2184. })
  2185. writeSSE(c.Writer, "message_stop", map[string]any{
  2186. "type": "message_stop",
  2187. })
  2188. flusher.Flush()
  2189. return &geminiStreamResult{usage: &usage, firstTokenMs: firstTokenMs}, nil
  2190. }
  2191. func writeSSE(w io.Writer, event string, data any) {
  2192. if event != "" {
  2193. _, _ = fmt.Fprintf(w, "event: %s\n", event)
  2194. }
  2195. b, _ := json.Marshal(data)
  2196. _, _ = fmt.Fprintf(w, "data: %s\n\n", string(b))
  2197. }
  2198. func randomHex(nBytes int) string {
  2199. b := make([]byte, nBytes)
  2200. _, _ = rand.Read(b)
  2201. return hex.EncodeToString(b)
  2202. }
  2203. // generateAnthropicMsgID 生成 Anthropic 官方格式的 message ID:msg_01 + 22 位 Base62
  2204. func generateAnthropicMsgID() string {
  2205. const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  2206. const idLen = 22
  2207. randomBytes := make([]byte, idLen)
  2208. if _, err := rand.Read(randomBytes); err != nil {
  2209. return "msg_01" + randomHex(11)
  2210. }
  2211. b := make([]byte, idLen)
  2212. for i := range b {
  2213. b[i] = charset[int(randomBytes[i])%len(charset)]
  2214. }
  2215. return "msg_01" + string(b)
  2216. }
  2217. func (s *GeminiMessagesCompatService) writeClaudeError(c *gin.Context, status int, errType, message string) error {
  2218. MarkResponseCommitted(c)
  2219. c.JSON(status, gin.H{
  2220. "type": "error",
  2221. "error": gin.H{"type": errType, "message": message},
  2222. })
  2223. return fmt.Errorf("%s", message)
  2224. }
  2225. func (s *GeminiMessagesCompatService) writeGoogleError(c *gin.Context, status int, message string) error {
  2226. MarkResponseCommitted(c)
  2227. c.JSON(status, gin.H{
  2228. "error": gin.H{
  2229. "code": status,
  2230. "message": message,
  2231. "status": googleapi.HTTPStatusToGoogleStatus(status),
  2232. },
  2233. })
  2234. return fmt.Errorf("%s", message)
  2235. }
  2236. func unwrapIfNeeded(isOAuth bool, raw []byte) []byte {
  2237. if !isOAuth {
  2238. return raw
  2239. }
  2240. inner, err := unwrapGeminiResponse(raw)
  2241. if err != nil {
  2242. return raw
  2243. }
  2244. return inner
  2245. }
  2246. func collectGeminiSSE(body io.Reader, isOAuth bool) (map[string]any, *ClaudeUsage, error) {
  2247. reader := bufio.NewReader(body)
  2248. var last map[string]any
  2249. var lastWithParts map[string]any
  2250. var collectedTextParts []string // Collect all text parts for aggregation
  2251. usage := &ClaudeUsage{}
  2252. for {
  2253. line, err := reader.ReadString('\n')
  2254. if len(line) > 0 {
  2255. trimmed := strings.TrimRight(line, "\r\n")
  2256. if strings.HasPrefix(trimmed, "data:") {
  2257. payload := strings.TrimSpace(strings.TrimPrefix(trimmed, "data:"))
  2258. switch payload {
  2259. case "", "[DONE]":
  2260. if payload == "[DONE]" {
  2261. return mergeCollectedTextParts(pickGeminiCollectResult(last, lastWithParts), collectedTextParts), usage, nil
  2262. }
  2263. default:
  2264. var parsed map[string]any
  2265. var rawBytes []byte
  2266. if isOAuth {
  2267. innerBytes, err := unwrapGeminiResponse([]byte(payload))
  2268. if err == nil {
  2269. rawBytes = innerBytes
  2270. _ = json.Unmarshal(innerBytes, &parsed)
  2271. }
  2272. } else {
  2273. rawBytes = []byte(payload)
  2274. _ = json.Unmarshal(rawBytes, &parsed)
  2275. }
  2276. if parsed != nil {
  2277. last = parsed
  2278. if u := extractGeminiUsage(rawBytes); u != nil {
  2279. usage = u
  2280. }
  2281. if parts := extractGeminiParts(parsed); len(parts) > 0 {
  2282. lastWithParts = parsed
  2283. // Collect text from each part for aggregation
  2284. for _, part := range parts {
  2285. if text, ok := part["text"].(string); ok && text != "" {
  2286. collectedTextParts = append(collectedTextParts, text)
  2287. }
  2288. }
  2289. }
  2290. }
  2291. }
  2292. }
  2293. }
  2294. if errors.Is(err, io.EOF) {
  2295. break
  2296. }
  2297. if err != nil {
  2298. return nil, nil, err
  2299. }
  2300. }
  2301. return mergeCollectedTextParts(pickGeminiCollectResult(last, lastWithParts), collectedTextParts), usage, nil
  2302. }
  2303. func pickGeminiCollectResult(last map[string]any, lastWithParts map[string]any) map[string]any {
  2304. if lastWithParts != nil {
  2305. return lastWithParts
  2306. }
  2307. if last != nil {
  2308. return last
  2309. }
  2310. return map[string]any{}
  2311. }
  2312. // mergeCollectedTextParts merges all collected text chunks into the final response.
  2313. // This fixes the issue where non-streaming responses only returned the last chunk
  2314. // instead of the complete aggregated text.
  2315. func mergeCollectedTextParts(response map[string]any, textParts []string) map[string]any {
  2316. if len(textParts) == 0 {
  2317. return response
  2318. }
  2319. // Join all text parts
  2320. mergedText := strings.Join(textParts, "")
  2321. // Deep copy response
  2322. result := make(map[string]any)
  2323. for k, v := range response {
  2324. result[k] = v
  2325. }
  2326. // Get or create candidates
  2327. candidates, ok := result["candidates"].([]any)
  2328. if !ok || len(candidates) == 0 {
  2329. candidates = []any{map[string]any{}}
  2330. }
  2331. // Get first candidate
  2332. candidate, ok := candidates[0].(map[string]any)
  2333. if !ok {
  2334. candidate = make(map[string]any)
  2335. candidates[0] = candidate
  2336. }
  2337. // Get or create content
  2338. content, ok := candidate["content"].(map[string]any)
  2339. if !ok {
  2340. content = map[string]any{"role": "model"}
  2341. candidate["content"] = content
  2342. }
  2343. // Get existing parts
  2344. existingParts, ok := content["parts"].([]any)
  2345. if !ok {
  2346. existingParts = []any{}
  2347. }
  2348. // Find and update first text part, or create new one
  2349. newParts := make([]any, 0, len(existingParts)+1)
  2350. textUpdated := false
  2351. for _, p := range existingParts {
  2352. pm, ok := p.(map[string]any)
  2353. if !ok {
  2354. newParts = append(newParts, p)
  2355. continue
  2356. }
  2357. if _, hasText := pm["text"]; hasText && !textUpdated {
  2358. // Replace with merged text
  2359. newPart := make(map[string]any)
  2360. for k, v := range pm {
  2361. newPart[k] = v
  2362. }
  2363. newPart["text"] = mergedText
  2364. newParts = append(newParts, newPart)
  2365. textUpdated = true
  2366. } else {
  2367. newParts = append(newParts, pm)
  2368. }
  2369. }
  2370. if !textUpdated {
  2371. newParts = append([]any{map[string]any{"text": mergedText}}, newParts...)
  2372. }
  2373. content["parts"] = newParts
  2374. result["candidates"] = candidates
  2375. return result
  2376. }
  2377. type geminiNativeStreamResult struct {
  2378. usage *ClaudeUsage
  2379. firstTokenMs *int
  2380. }
  2381. func isGeminiInsufficientScope(headers http.Header, body []byte) bool {
  2382. if strings.Contains(strings.ToLower(headers.Get("Www-Authenticate")), "insufficient_scope") {
  2383. return true
  2384. }
  2385. lower := strings.ToLower(string(body))
  2386. return strings.Contains(lower, "insufficient authentication scopes") || strings.Contains(lower, "access_token_scope_insufficient")
  2387. }
  2388. func estimateGeminiCountTokens(reqBody []byte) int {
  2389. total := 0
  2390. // systemInstruction.parts[].text
  2391. gjson.GetBytes(reqBody, "systemInstruction.parts").ForEach(func(_, part gjson.Result) bool {
  2392. if t := strings.TrimSpace(part.Get("text").String()); t != "" {
  2393. total += estimateTokensForText(t)
  2394. }
  2395. return true
  2396. })
  2397. // contents[].parts[].text
  2398. gjson.GetBytes(reqBody, "contents").ForEach(func(_, content gjson.Result) bool {
  2399. content.Get("parts").ForEach(func(_, part gjson.Result) bool {
  2400. if t := strings.TrimSpace(part.Get("text").String()); t != "" {
  2401. total += estimateTokensForText(t)
  2402. }
  2403. return true
  2404. })
  2405. return true
  2406. })
  2407. if total < 0 {
  2408. return 0
  2409. }
  2410. return total
  2411. }
  2412. func estimateTokensForText(s string) int {
  2413. s = strings.TrimSpace(s)
  2414. if s == "" {
  2415. return 0
  2416. }
  2417. runes := []rune(s)
  2418. if len(runes) == 0 {
  2419. return 0
  2420. }
  2421. ascii := 0
  2422. for _, r := range runes {
  2423. if r <= 0x7f {
  2424. ascii++
  2425. }
  2426. }
  2427. asciiRatio := float64(ascii) / float64(len(runes))
  2428. if asciiRatio >= 0.8 {
  2429. // Roughly 4 chars per token for English-like text.
  2430. return (len(runes) + 3) / 4
  2431. }
  2432. // For CJK-heavy text, approximate 1 rune per token.
  2433. return len(runes)
  2434. }
  2435. type UpstreamHTTPResult struct {
  2436. StatusCode int
  2437. Headers http.Header
  2438. Body []byte
  2439. }
  2440. func (s *GeminiMessagesCompatService) handleNativeNonStreamingResponse(c *gin.Context, resp *http.Response, isOAuth bool) (*ClaudeUsage, error) {
  2441. if s.cfg != nil && s.cfg.Gateway.GeminiDebugResponseHeaders {
  2442. logger.LegacyPrintf("service.gemini_messages_compat", "[GeminiAPI] ========== Response Headers ==========")
  2443. for key, values := range resp.Header {
  2444. if strings.HasPrefix(strings.ToLower(key), "x-ratelimit") {
  2445. logger.LegacyPrintf("service.gemini_messages_compat", "[GeminiAPI] %s: %v", key, values)
  2446. }
  2447. }
  2448. logger.LegacyPrintf("service.gemini_messages_compat", "[GeminiAPI] ========================================")
  2449. }
  2450. respBody, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError)
  2451. if err != nil {
  2452. return nil, err
  2453. }
  2454. if isOAuth {
  2455. unwrappedBody, uwErr := unwrapGeminiResponse(respBody)
  2456. if uwErr == nil {
  2457. respBody = unwrappedBody
  2458. }
  2459. }
  2460. observer := upstreamResponseModelObserverFromContext(c)
  2461. if observer == nil {
  2462. observer = beginUpstreamResponseModelObservation(c)
  2463. }
  2464. observer.ObserveGemini(respBody)
  2465. observeGeminiImageOutputs(c, respBody)
  2466. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  2467. contentType := resp.Header.Get("Content-Type")
  2468. if contentType == "" {
  2469. contentType = "application/json"
  2470. }
  2471. c.Data(resp.StatusCode, contentType, respBody)
  2472. if u := extractGeminiUsage(respBody); u != nil {
  2473. return u, nil
  2474. }
  2475. return &ClaudeUsage{}, nil
  2476. }
  2477. func (s *GeminiMessagesCompatService) handleNativeStreamingResponse(c *gin.Context, resp *http.Response, startTime time.Time, isOAuth bool) (*geminiNativeStreamResult, error) {
  2478. if s.cfg != nil && s.cfg.Gateway.GeminiDebugResponseHeaders {
  2479. logger.LegacyPrintf("service.gemini_messages_compat", "[GeminiAPI] ========== Streaming Response Headers ==========")
  2480. for key, values := range resp.Header {
  2481. if strings.HasPrefix(strings.ToLower(key), "x-ratelimit") {
  2482. logger.LegacyPrintf("service.gemini_messages_compat", "[GeminiAPI] %s: %v", key, values)
  2483. }
  2484. }
  2485. logger.LegacyPrintf("service.gemini_messages_compat", "[GeminiAPI] ====================================================")
  2486. }
  2487. if s.responseHeaderFilter != nil {
  2488. responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
  2489. }
  2490. c.Status(resp.StatusCode)
  2491. c.Header("Cache-Control", "no-cache")
  2492. c.Header("Connection", "keep-alive")
  2493. c.Header("X-Accel-Buffering", "no")
  2494. contentType := resp.Header.Get("Content-Type")
  2495. if contentType == "" {
  2496. contentType = "text/event-stream; charset=utf-8"
  2497. }
  2498. c.Header("Content-Type", contentType)
  2499. flusher, ok := c.Writer.(http.Flusher)
  2500. if !ok {
  2501. return nil, errors.New("streaming not supported")
  2502. }
  2503. reader := bufio.NewReader(resp.Body)
  2504. usage := &ClaudeUsage{}
  2505. observer := upstreamResponseModelObserverFromContext(c)
  2506. if observer == nil {
  2507. observer = beginUpstreamResponseModelObservation(c)
  2508. }
  2509. var firstTokenMs *int
  2510. for {
  2511. line, err := reader.ReadString('\n')
  2512. if len(line) > 0 {
  2513. trimmed := strings.TrimRight(line, "\r\n")
  2514. if strings.HasPrefix(trimmed, "data:") {
  2515. payload := strings.TrimSpace(strings.TrimPrefix(trimmed, "data:"))
  2516. // Keepalive / done markers
  2517. if payload == "" || payload == "[DONE]" {
  2518. _, _ = io.WriteString(c.Writer, line)
  2519. flusher.Flush()
  2520. } else {
  2521. var rawToWrite string
  2522. rawToWrite = payload
  2523. var rawBytes []byte
  2524. if isOAuth {
  2525. innerBytes, err := unwrapGeminiResponse([]byte(payload))
  2526. if err == nil {
  2527. rawToWrite = string(innerBytes)
  2528. rawBytes = innerBytes
  2529. }
  2530. } else {
  2531. rawBytes = []byte(payload)
  2532. }
  2533. if u := extractGeminiUsage(rawBytes); u != nil {
  2534. usage = u
  2535. }
  2536. observer.ObserveGemini(rawBytes)
  2537. observeGeminiImageOutputs(c, rawBytes)
  2538. if firstTokenMs == nil {
  2539. ms := int(time.Since(startTime).Milliseconds())
  2540. firstTokenMs = &ms
  2541. }
  2542. if isOAuth {
  2543. // SSE format requires double newline (\n\n) to separate events
  2544. _, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", rawToWrite)
  2545. } else {
  2546. // Pass-through for AI Studio responses.
  2547. _, _ = io.WriteString(c.Writer, line)
  2548. }
  2549. flusher.Flush()
  2550. }
  2551. } else {
  2552. _, _ = io.WriteString(c.Writer, line)
  2553. flusher.Flush()
  2554. }
  2555. }
  2556. if errors.Is(err, io.EOF) {
  2557. break
  2558. }
  2559. if err != nil {
  2560. return nil, err
  2561. }
  2562. }
  2563. return &geminiNativeStreamResult{usage: usage, firstTokenMs: firstTokenMs}, nil
  2564. }
  2565. // ForwardAIStudioGET forwards a GET request to AI Studio (generativelanguage.googleapis.com) for
  2566. // endpoints like /v1beta/models and /v1beta/models/{model}.
  2567. //
  2568. // This is used to support Gemini SDKs that call models listing endpoints before generation.
  2569. func (s *GeminiMessagesCompatService) ForwardAIStudioGET(ctx context.Context, account *Account, path string) (*UpstreamHTTPResult, error) {
  2570. if account == nil {
  2571. return nil, errors.New("account is nil")
  2572. }
  2573. // path 会被直接拼到上游 base URL 后面,因此按路径护栏逐片段校验,
  2574. // 见 upstream_path_guard.go。
  2575. sanitizedPath, ok := sanitizedUpstreamPathSuffix(path)
  2576. if !ok || sanitizedPath == "" {
  2577. return nil, errors.New("invalid path")
  2578. }
  2579. path = sanitizedPath
  2580. baseURL := account.GetGeminiBaseURL(geminicli.AIStudioBaseURL)
  2581. normalizedBaseURL, err := s.validateUpstreamBaseURL(baseURL)
  2582. if err != nil {
  2583. return nil, err
  2584. }
  2585. fullURL := strings.TrimRight(normalizedBaseURL, "/") + path
  2586. var proxyURL string
  2587. if account.ProxyID != nil && account.Proxy != nil {
  2588. proxyURL = account.Proxy.URL()
  2589. }
  2590. req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
  2591. if err != nil {
  2592. return nil, err
  2593. }
  2594. switch account.Type {
  2595. case AccountTypeAPIKey:
  2596. apiKey := strings.TrimSpace(account.GetCredential("api_key"))
  2597. if apiKey == "" {
  2598. return nil, errors.New("gemini api_key not configured")
  2599. }
  2600. req.Header.Set("x-goog-api-key", apiKey)
  2601. case AccountTypeOAuth:
  2602. if s.tokenProvider == nil {
  2603. return nil, errors.New("gemini token provider not configured")
  2604. }
  2605. accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
  2606. if err != nil {
  2607. return nil, err
  2608. }
  2609. req.Header.Set("Authorization", "Bearer "+accessToken)
  2610. default:
  2611. return nil, fmt.Errorf("unsupported account type: %s", account.Type)
  2612. }
  2613. resp, err := s.httpUpstream.Do(WithAccountTrafficRequest(req, account), proxyURL, account.ID, account.Concurrency)
  2614. if err != nil {
  2615. return nil, err
  2616. }
  2617. defer func() { _ = resp.Body.Close() }()
  2618. body, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
  2619. wwwAuthenticate := resp.Header.Get("Www-Authenticate")
  2620. filteredHeaders := responseheaders.FilterHeaders(resp.Header, s.responseHeaderFilter)
  2621. if wwwAuthenticate != "" {
  2622. filteredHeaders.Set("Www-Authenticate", wwwAuthenticate)
  2623. }
  2624. return &UpstreamHTTPResult{
  2625. StatusCode: resp.StatusCode,
  2626. Headers: filteredHeaders,
  2627. Body: body,
  2628. }, nil
  2629. }
  2630. // unwrapGeminiResponse 解包 Gemini OAuth 响应中的 response 字段
  2631. // 使用 gjson 零拷贝提取,避免完整 Unmarshal+Marshal
  2632. func unwrapGeminiResponse(raw []byte) ([]byte, error) {
  2633. result := gjson.GetBytes(raw, "response")
  2634. if result.Exists() && result.Type == gjson.JSON {
  2635. return []byte(result.Raw), nil
  2636. }
  2637. return raw, nil
  2638. }
  2639. func convertGeminiToClaudeMessage(geminiResp map[string]any, originalModel string, rawData []byte, includeInlineData bool) (map[string]any, *ClaudeUsage) {
  2640. usage := extractGeminiUsage(rawData)
  2641. if usage == nil {
  2642. usage = &ClaudeUsage{}
  2643. }
  2644. contentBlocks := make([]any, 0)
  2645. sawToolUse := false
  2646. if candidates, ok := geminiResp["candidates"].([]any); ok && len(candidates) > 0 {
  2647. if cand, ok := candidates[0].(map[string]any); ok {
  2648. if content, ok := cand["content"].(map[string]any); ok {
  2649. if parts, ok := content["parts"].([]any); ok {
  2650. for _, part := range parts {
  2651. pm, ok := part.(map[string]any)
  2652. if !ok {
  2653. continue
  2654. }
  2655. if text, ok := pm["text"].(string); ok && text != "" {
  2656. contentBlocks = append(contentBlocks, map[string]any{
  2657. "type": "text",
  2658. "text": text,
  2659. })
  2660. }
  2661. if inlineData, ok := pm["inlineData"].(map[string]any); includeInlineData && ok {
  2662. mimeType, _ := inlineData["mimeType"].(string)
  2663. data, _ := inlineData["data"].(string)
  2664. if isGeminiInlineImageMIMEType(mimeType) && isValidBase64(data) {
  2665. contentBlocks = append(contentBlocks, map[string]any{
  2666. "type": "text",
  2667. "text": fmt.Sprintf("![image](data:%s;base64,%s)", mimeType, data),
  2668. })
  2669. }
  2670. }
  2671. if fc, ok := pm["functionCall"].(map[string]any); ok {
  2672. name, _ := fc["name"].(string)
  2673. if strings.TrimSpace(name) == "" {
  2674. name = "tool"
  2675. }
  2676. args := fc["args"]
  2677. sawToolUse = true
  2678. contentBlocks = append(contentBlocks, map[string]any{
  2679. "type": "tool_use",
  2680. "id": "toolu_" + randomHex(8),
  2681. "name": name,
  2682. "input": args,
  2683. })
  2684. }
  2685. }
  2686. }
  2687. }
  2688. }
  2689. }
  2690. stopReason := mapGeminiFinishReasonToClaudeStopReason(extractGeminiFinishReason(geminiResp))
  2691. if sawToolUse {
  2692. stopReason = "tool_use"
  2693. }
  2694. resp := map[string]any{
  2695. "id": generateAnthropicMsgID(),
  2696. "type": "message",
  2697. "role": "assistant",
  2698. "model": originalModel,
  2699. "content": contentBlocks,
  2700. "stop_reason": stopReason,
  2701. "stop_sequence": nil,
  2702. "usage": map[string]any{
  2703. "input_tokens": usage.InputTokens,
  2704. "output_tokens": usage.OutputTokens,
  2705. },
  2706. }
  2707. return resp, usage
  2708. }
  2709. func isGeminiInlineImageMIMEType(mimeType string) bool {
  2710. switch mimeType {
  2711. case "image/gif", "image/jpeg", "image/png", "image/webp":
  2712. return true
  2713. default:
  2714. return false
  2715. }
  2716. }
  2717. func isValidBase64(data string) bool {
  2718. if data == "" {
  2719. return false
  2720. }
  2721. _, err := base64.StdEncoding.DecodeString(data)
  2722. return err == nil
  2723. }
  2724. func extractGeminiUsage(data []byte) *ClaudeUsage {
  2725. usage := gjson.GetBytes(data, "usageMetadata")
  2726. if !usage.Exists() {
  2727. return nil
  2728. }
  2729. prompt := int(usage.Get("promptTokenCount").Int())
  2730. cand := int(usage.Get("candidatesTokenCount").Int())
  2731. cached := int(usage.Get("cachedContentTokenCount").Int())
  2732. thoughts := int(usage.Get("thoughtsTokenCount").Int())
  2733. // 从 candidatesTokensDetails 提取 IMAGE 模态 token 数
  2734. imageTokens := 0
  2735. candidateDetails := usage.Get("candidatesTokensDetails")
  2736. if candidateDetails.Exists() {
  2737. candidateDetails.ForEach(func(_, detail gjson.Result) bool {
  2738. if detail.Get("modality").String() == "IMAGE" {
  2739. imageTokens = int(detail.Get("tokenCount").Int())
  2740. return false
  2741. }
  2742. return true
  2743. })
  2744. }
  2745. // 注意:Gemini 的 promptTokenCount 包含 cachedContentTokenCount,
  2746. // 但 Claude 的 input_tokens 不包含 cache_read_input_tokens,需要减去
  2747. return &ClaudeUsage{
  2748. InputTokens: prompt - cached,
  2749. OutputTokens: cand + thoughts,
  2750. CacheReadInputTokens: cached,
  2751. ImageOutputTokens: imageTokens,
  2752. }
  2753. }
  2754. func asInt(v any) (int, bool) {
  2755. switch t := v.(type) {
  2756. case float64:
  2757. return int(t), true
  2758. case int:
  2759. return t, true
  2760. case int64:
  2761. return int(t), true
  2762. case json.Number:
  2763. i, err := t.Int64()
  2764. if err != nil {
  2765. return 0, false
  2766. }
  2767. return int(i), true
  2768. default:
  2769. return 0, false
  2770. }
  2771. }
  2772. func (s *GeminiMessagesCompatService) handleGeminiUpstreamError(ctx context.Context, account *Account, statusCode int, headers http.Header, body []byte) {
  2773. // 遵守自定义错误码策略:未命中则跳过所有限流处理
  2774. if !account.ShouldHandleErrorCode(statusCode) {
  2775. return
  2776. }
  2777. if s.rateLimitService != nil && (statusCode == 401 || statusCode == 403 || statusCode == 529) {
  2778. s.rateLimitService.HandleUpstreamError(ctx, account, statusCode, headers, body)
  2779. return
  2780. }
  2781. if statusCode != 429 {
  2782. return
  2783. }
  2784. // 池模式账号不写账号级限流:账号留在池内,由 failover / 同号重试消化 429。
  2785. // 自定义错误码优先级高于池模式,开启后仍按其命中结果标记。
  2786. if account.IsPoolMode() && !account.IsCustomErrorCodesEnabled() {
  2787. return
  2788. }
  2789. oauthType := account.GeminiOAuthType()
  2790. tierID := account.GeminiTierID()
  2791. projectID := strings.TrimSpace(account.GetCredential("project_id"))
  2792. isCodeAssist := account.IsGeminiCodeAssist()
  2793. resetAt := ParseGeminiRateLimitResetTime(body)
  2794. if resetAt == nil {
  2795. // 根据账号类型使用不同的默认重置时间
  2796. var ra time.Time
  2797. if isCodeAssist || oauthType == "google_one" {
  2798. // Gemini CLI / Google One: fallback cooldown by tier
  2799. cooldown := geminiCooldownForTier(tierID)
  2800. if s.rateLimitService != nil {
  2801. cooldown = s.rateLimitService.GeminiCooldown(ctx, account)
  2802. }
  2803. ra = time.Now().Add(cooldown)
  2804. if isCodeAssist {
  2805. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini 429] Account %d (Code Assist, tier=%s, project=%s) rate limited, cooldown=%v", account.ID, tierID, projectID, time.Until(ra).Truncate(time.Second))
  2806. } else {
  2807. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini 429] Account %d (Google One OAuth, tier=%s, project=%s) rate limited, cooldown=%v", account.ID, tierID, projectID, time.Until(ra).Truncate(time.Second))
  2808. }
  2809. } else {
  2810. // API Key / AI Studio OAuth: PST 午夜
  2811. if ts := nextGeminiDailyResetUnix(); ts != nil {
  2812. ra = time.Unix(*ts, 0)
  2813. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini 429] Account %d (API Key/AI Studio, type=%s) rate limited, reset at PST midnight (%v)", account.ID, account.Type, ra)
  2814. } else {
  2815. // 兜底:5 分钟
  2816. ra = time.Now().Add(5 * time.Minute)
  2817. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini 429] Account %d rate limited, fallback to 5min", account.ID)
  2818. }
  2819. }
  2820. _ = s.accountRepo.SetRateLimited(ctx, account.ID, ra)
  2821. return
  2822. }
  2823. // 使用解析到的重置时间
  2824. resetTime := time.Unix(*resetAt, 0)
  2825. _ = s.accountRepo.SetRateLimited(ctx, account.ID, resetTime)
  2826. logger.LegacyPrintf("service.gemini_messages_compat", "[Gemini 429] Account %d rate limited until %v (oauth_type=%s, tier=%s)",
  2827. account.ID, resetTime, oauthType, tierID)
  2828. }
  2829. // ParseGeminiRateLimitResetTime 解析 Gemini 格式的 429 响应,返回重置时间的 Unix 时间戳
  2830. func ParseGeminiRateLimitResetTime(body []byte) *int64 {
  2831. // 第一阶段:gjson 结构化提取
  2832. errMsg := gjson.GetBytes(body, "error.message").String()
  2833. if looksLikeGeminiDailyQuota(errMsg) {
  2834. if ts := nextGeminiDailyResetUnix(); ts != nil {
  2835. return ts
  2836. }
  2837. }
  2838. // 遍历 error.details 查找 quotaResetDelay
  2839. var found *int64
  2840. gjson.GetBytes(body, "error.details").ForEach(func(_, detail gjson.Result) bool {
  2841. v := detail.Get("metadata.quotaResetDelay").String()
  2842. if v == "" {
  2843. return true
  2844. }
  2845. if dur, err := time.ParseDuration(v); err == nil {
  2846. // Use ceil to avoid undercounting fractional seconds (e.g. 10.1s should not become 10s),
  2847. // which can affect scheduling decisions around thresholds (like 10s).
  2848. ts := time.Now().Unix() + int64(math.Ceil(dur.Seconds()))
  2849. found = &ts
  2850. return false
  2851. }
  2852. return true
  2853. })
  2854. if found != nil {
  2855. return found
  2856. }
  2857. // 第二阶段:regex 回退匹配 "Please retry in Xs"
  2858. matches := retryInRegex.FindStringSubmatch(string(body))
  2859. if len(matches) == 2 {
  2860. if dur, err := time.ParseDuration(matches[1] + "s"); err == nil {
  2861. ts := time.Now().Unix() + int64(math.Ceil(dur.Seconds()))
  2862. return &ts
  2863. }
  2864. }
  2865. return nil
  2866. }
  2867. func looksLikeGeminiDailyQuota(message string) bool {
  2868. m := strings.ToLower(message)
  2869. if strings.Contains(m, "per day") || strings.Contains(m, "requests per day") || strings.Contains(m, "quota") && strings.Contains(m, "per day") {
  2870. return true
  2871. }
  2872. return false
  2873. }
  2874. func nextGeminiDailyResetUnix() *int64 {
  2875. reset := geminiDailyResetTime(time.Now())
  2876. ts := reset.Unix()
  2877. return &ts
  2878. }
  2879. func ensureGeminiFunctionCallThoughtSignatures(body []byte) []byte {
  2880. // Fast path: only run when functionCall is present.
  2881. if !bytes.Contains(body, []byte(`"functionCall"`)) {
  2882. return body
  2883. }
  2884. var payload map[string]any
  2885. if err := json.Unmarshal(body, &payload); err != nil {
  2886. return body
  2887. }
  2888. contentsAny, ok := payload["contents"].([]any)
  2889. if !ok || len(contentsAny) == 0 {
  2890. return body
  2891. }
  2892. modified := false
  2893. for _, c := range contentsAny {
  2894. cm, ok := c.(map[string]any)
  2895. if !ok {
  2896. continue
  2897. }
  2898. partsAny, ok := cm["parts"].([]any)
  2899. if !ok || len(partsAny) == 0 {
  2900. continue
  2901. }
  2902. for _, p := range partsAny {
  2903. pm, ok := p.(map[string]any)
  2904. if !ok || pm == nil {
  2905. continue
  2906. }
  2907. if fc, ok := pm["functionCall"].(map[string]any); !ok || fc == nil {
  2908. continue
  2909. }
  2910. ts, _ := pm["thoughtSignature"].(string)
  2911. if strings.TrimSpace(ts) == "" {
  2912. pm["thoughtSignature"] = geminiDummyThoughtSignature
  2913. modified = true
  2914. }
  2915. }
  2916. }
  2917. if !modified {
  2918. return body
  2919. }
  2920. b, err := json.Marshal(payload)
  2921. if err != nil {
  2922. return body
  2923. }
  2924. return b
  2925. }
  2926. func extractGeminiFinishReason(geminiResp map[string]any) string {
  2927. if candidates, ok := geminiResp["candidates"].([]any); ok && len(candidates) > 0 {
  2928. if cand, ok := candidates[0].(map[string]any); ok {
  2929. if fr, ok := cand["finishReason"].(string); ok {
  2930. return fr
  2931. }
  2932. }
  2933. }
  2934. return ""
  2935. }
  2936. func extractGeminiParts(geminiResp map[string]any) []map[string]any {
  2937. if candidates, ok := geminiResp["candidates"].([]any); ok && len(candidates) > 0 {
  2938. if cand, ok := candidates[0].(map[string]any); ok {
  2939. if content, ok := cand["content"].(map[string]any); ok {
  2940. if partsAny, ok := content["parts"].([]any); ok && len(partsAny) > 0 {
  2941. out := make([]map[string]any, 0, len(partsAny))
  2942. for _, p := range partsAny {
  2943. pm, ok := p.(map[string]any)
  2944. if !ok {
  2945. continue
  2946. }
  2947. out = append(out, pm)
  2948. }
  2949. return out
  2950. }
  2951. }
  2952. }
  2953. }
  2954. return nil
  2955. }
  2956. func computeGeminiTextDelta(seen, incoming string) (delta, newSeen string) {
  2957. incoming = strings.TrimSuffix(incoming, "\u0000")
  2958. if incoming == "" {
  2959. return "", seen
  2960. }
  2961. // Cumulative mode: incoming contains full text so far.
  2962. if strings.HasPrefix(incoming, seen) {
  2963. return strings.TrimPrefix(incoming, seen), incoming
  2964. }
  2965. // Duplicate/rewind: ignore.
  2966. if strings.HasPrefix(seen, incoming) {
  2967. return "", seen
  2968. }
  2969. // Delta mode: treat incoming as incremental chunk.
  2970. return incoming, seen + incoming
  2971. }
  2972. func mapGeminiFinishReasonToClaudeStopReason(finishReason string) string {
  2973. switch strings.ToUpper(strings.TrimSpace(finishReason)) {
  2974. case "MAX_TOKENS":
  2975. return "max_tokens"
  2976. case "STOP":
  2977. return "end_turn"
  2978. default:
  2979. return "end_turn"
  2980. }
  2981. }
  2982. func convertClaudeMessagesToGeminiGenerateContent(body []byte) ([]byte, error) {
  2983. var req map[string]any
  2984. if err := json.Unmarshal(body, &req); err != nil {
  2985. return nil, err
  2986. }
  2987. toolUseIDToName := make(map[string]string)
  2988. systemText := extractClaudeSystemText(req["system"])
  2989. contents, err := convertClaudeMessagesToGeminiContents(req["messages"], toolUseIDToName)
  2990. if err != nil {
  2991. return nil, err
  2992. }
  2993. out := make(map[string]any)
  2994. if systemText != "" {
  2995. out["systemInstruction"] = map[string]any{
  2996. "parts": []any{map[string]any{"text": systemText}},
  2997. }
  2998. }
  2999. out["contents"] = contents
  3000. if tools := convertClaudeToolsToGeminiTools(req["tools"]); tools != nil {
  3001. out["tools"] = tools
  3002. }
  3003. generationConfig := convertClaudeGenerationConfig(req)
  3004. if generationConfig != nil {
  3005. out["generationConfig"] = generationConfig
  3006. }
  3007. stripGeminiFunctionIDs(out)
  3008. return json.Marshal(out)
  3009. }
  3010. func stripGeminiFunctionIDs(req map[string]any) {
  3011. // Defensive cleanup: some upstreams reject unexpected `id` fields in functionCall/functionResponse.
  3012. contents, ok := req["contents"].([]any)
  3013. if !ok {
  3014. return
  3015. }
  3016. for _, c := range contents {
  3017. cm, ok := c.(map[string]any)
  3018. if !ok {
  3019. continue
  3020. }
  3021. contentParts, ok := cm["parts"].([]any)
  3022. if !ok {
  3023. continue
  3024. }
  3025. for _, p := range contentParts {
  3026. pm, ok := p.(map[string]any)
  3027. if !ok {
  3028. continue
  3029. }
  3030. if fc, ok := pm["functionCall"].(map[string]any); ok && fc != nil {
  3031. delete(fc, "id")
  3032. }
  3033. if fr, ok := pm["functionResponse"].(map[string]any); ok && fr != nil {
  3034. delete(fr, "id")
  3035. }
  3036. }
  3037. }
  3038. }
  3039. func extractClaudeSystemText(system any) string {
  3040. switch v := system.(type) {
  3041. case string:
  3042. return strings.TrimSpace(v)
  3043. case []any:
  3044. var parts []string
  3045. for _, p := range v {
  3046. pm, ok := p.(map[string]any)
  3047. if !ok {
  3048. continue
  3049. }
  3050. if t, _ := pm["type"].(string); t != "text" {
  3051. continue
  3052. }
  3053. if text, ok := pm["text"].(string); ok && strings.TrimSpace(text) != "" {
  3054. parts = append(parts, text)
  3055. }
  3056. }
  3057. return strings.TrimSpace(strings.Join(parts, "\n"))
  3058. default:
  3059. return ""
  3060. }
  3061. }
  3062. func convertClaudeMessagesToGeminiContents(messages any, toolUseIDToName map[string]string) ([]any, error) {
  3063. arr, ok := messages.([]any)
  3064. if !ok {
  3065. return nil, errors.New("messages must be an array")
  3066. }
  3067. out := make([]any, 0, len(arr))
  3068. for _, m := range arr {
  3069. mm, ok := m.(map[string]any)
  3070. if !ok {
  3071. continue
  3072. }
  3073. role, _ := mm["role"].(string)
  3074. role = strings.ToLower(strings.TrimSpace(role))
  3075. gRole := "user"
  3076. if role == "assistant" {
  3077. gRole = "model"
  3078. }
  3079. parts := make([]any, 0)
  3080. switch content := mm["content"].(type) {
  3081. case string:
  3082. // 字符串形式的 content,保留所有内容(包括空白)
  3083. parts = append(parts, map[string]any{"text": content})
  3084. case []any:
  3085. // 如果只有一个 block,不过滤空白(让上游 API 报错)
  3086. singleBlock := len(content) == 1
  3087. for _, block := range content {
  3088. bm, ok := block.(map[string]any)
  3089. if !ok {
  3090. continue
  3091. }
  3092. bt, _ := bm["type"].(string)
  3093. switch bt {
  3094. case "text":
  3095. if text, ok := bm["text"].(string); ok {
  3096. // 单个 block 时保留所有内容(包括空白)
  3097. // 多个 blocks 时过滤掉空白
  3098. if singleBlock || strings.TrimSpace(text) != "" {
  3099. parts = append(parts, map[string]any{"text": text})
  3100. }
  3101. }
  3102. case "tool_use":
  3103. id, _ := bm["id"].(string)
  3104. name, _ := bm["name"].(string)
  3105. if strings.TrimSpace(id) != "" && strings.TrimSpace(name) != "" {
  3106. toolUseIDToName[id] = name
  3107. }
  3108. signature, _ := bm["signature"].(string)
  3109. signature = strings.TrimSpace(signature)
  3110. if signature == "" {
  3111. signature = geminiDummyThoughtSignature
  3112. }
  3113. parts = append(parts, map[string]any{
  3114. "thoughtSignature": signature,
  3115. "functionCall": map[string]any{
  3116. "name": name,
  3117. "args": bm["input"],
  3118. },
  3119. })
  3120. case "tool_result":
  3121. toolUseID, _ := bm["tool_use_id"].(string)
  3122. name := toolUseIDToName[toolUseID]
  3123. if name == "" {
  3124. name = "tool"
  3125. }
  3126. parts = append(parts, map[string]any{
  3127. "functionResponse": map[string]any{
  3128. "name": name,
  3129. "response": map[string]any{
  3130. "content": extractClaudeContentText(bm["content"]),
  3131. },
  3132. },
  3133. })
  3134. case "image":
  3135. if src, ok := bm["source"].(map[string]any); ok {
  3136. if srcType, _ := src["type"].(string); srcType == "base64" {
  3137. mediaType, _ := src["media_type"].(string)
  3138. data, _ := src["data"].(string)
  3139. if mediaType != "" && data != "" {
  3140. parts = append(parts, map[string]any{
  3141. "inlineData": map[string]any{
  3142. "mimeType": mediaType,
  3143. "data": data,
  3144. },
  3145. })
  3146. }
  3147. }
  3148. }
  3149. default:
  3150. // best-effort: preserve unknown blocks as text
  3151. if b, err := json.Marshal(bm); err == nil {
  3152. parts = append(parts, map[string]any{"text": string(b)})
  3153. }
  3154. }
  3155. }
  3156. default:
  3157. // ignore
  3158. }
  3159. out = append(out, map[string]any{
  3160. "role": gRole,
  3161. "parts": parts,
  3162. })
  3163. }
  3164. return out, nil
  3165. }
  3166. func extractClaudeContentText(v any) string {
  3167. switch t := v.(type) {
  3168. case string:
  3169. return t
  3170. case []any:
  3171. var sb strings.Builder
  3172. for _, part := range t {
  3173. pm, ok := part.(map[string]any)
  3174. if !ok {
  3175. continue
  3176. }
  3177. if pm["type"] == "text" {
  3178. if text, ok := pm["text"].(string); ok {
  3179. _, _ = sb.WriteString(text)
  3180. }
  3181. }
  3182. }
  3183. return sb.String()
  3184. default:
  3185. b, _ := json.Marshal(t)
  3186. return string(b)
  3187. }
  3188. }
  3189. func convertClaudeToolsToGeminiTools(tools any) []any {
  3190. arr, ok := tools.([]any)
  3191. if !ok || len(arr) == 0 {
  3192. return nil
  3193. }
  3194. hasWebSearch := false
  3195. funcDecls := make([]any, 0, len(arr))
  3196. for _, t := range arr {
  3197. tm, ok := t.(map[string]any)
  3198. if !ok {
  3199. continue
  3200. }
  3201. if isClaudeWebSearchToolMap(tm) {
  3202. hasWebSearch = true
  3203. continue
  3204. }
  3205. var name, desc string
  3206. var params any
  3207. // 检查是否为 custom 类型工具 (MCP)
  3208. toolType, _ := tm["type"].(string)
  3209. if toolType == "custom" {
  3210. // Custom 格式: 从 custom 字段获取 description 和 input_schema
  3211. custom, ok := tm["custom"].(map[string]any)
  3212. if !ok {
  3213. continue
  3214. }
  3215. name, _ = tm["name"].(string)
  3216. desc, _ = custom["description"].(string)
  3217. params = custom["input_schema"]
  3218. } else {
  3219. // 标准格式: 从顶层字段获取
  3220. name, _ = tm["name"].(string)
  3221. desc, _ = tm["description"].(string)
  3222. params = tm["input_schema"]
  3223. }
  3224. if name == "" {
  3225. continue
  3226. }
  3227. // 为 nil params 提供默认值
  3228. if params == nil {
  3229. params = map[string]any{
  3230. "type": "object",
  3231. "properties": map[string]any{},
  3232. }
  3233. }
  3234. // 清理 JSON Schema
  3235. cleanedParams := cleanToolSchema(params)
  3236. funcDecls = append(funcDecls, map[string]any{
  3237. "name": name,
  3238. "description": desc,
  3239. "parameters": cleanedParams,
  3240. })
  3241. }
  3242. out := make([]any, 0, 2)
  3243. if len(funcDecls) > 0 {
  3244. out = append(out, map[string]any{
  3245. "functionDeclarations": funcDecls,
  3246. })
  3247. }
  3248. if hasWebSearch {
  3249. out = append(out, map[string]any{
  3250. "googleSearch": map[string]any{},
  3251. })
  3252. }
  3253. if len(out) == 0 {
  3254. return nil
  3255. }
  3256. return out
  3257. }
  3258. func normalizeGeminiRequestForAIStudio(body []byte) []byte {
  3259. var payload map[string]any
  3260. if err := json.Unmarshal(body, &payload); err != nil {
  3261. return body
  3262. }
  3263. tools, ok := payload["tools"].([]any)
  3264. if !ok || len(tools) == 0 {
  3265. return body
  3266. }
  3267. modified := false
  3268. for _, rawTool := range tools {
  3269. tool, ok := rawTool.(map[string]any)
  3270. if !ok {
  3271. continue
  3272. }
  3273. googleSearch, ok := tool["googleSearch"]
  3274. if !ok {
  3275. continue
  3276. }
  3277. if _, exists := tool["google_search"]; exists {
  3278. continue
  3279. }
  3280. tool["google_search"] = googleSearch
  3281. delete(tool, "googleSearch")
  3282. modified = true
  3283. }
  3284. if !modified {
  3285. return body
  3286. }
  3287. normalized, err := json.Marshal(payload)
  3288. if err != nil {
  3289. return body
  3290. }
  3291. return normalized
  3292. }
  3293. func isClaudeWebSearchToolMap(tool map[string]any) bool {
  3294. toolType, _ := tool["type"].(string)
  3295. // A function named web_search is still a client-side function. This is
  3296. // especially important for Chat Completions clients such as Hermes, whose
  3297. // built-in runtime tools are represented as ordinary function tools.
  3298. // Promote only explicitly typed server-side search tools to Gemini's
  3299. // built-in googleSearch tool.
  3300. return strings.HasPrefix(toolType, "web_search") || toolType == "google_search"
  3301. }
  3302. // cleanToolSchema 清理工具的 JSON Schema,移除 Gemini 不支持的字段
  3303. func cleanToolSchema(schema any) any {
  3304. if schema == nil {
  3305. return nil
  3306. }
  3307. switch v := schema.(type) {
  3308. case map[string]any:
  3309. cleaned := make(map[string]any)
  3310. for key, value := range v {
  3311. // 跳过不支持的字段
  3312. if key == "$schema" || key == "$id" || key == "$ref" ||
  3313. key == "$defs" || key == "definitions" ||
  3314. key == "additionalProperties" || key == "patternProperties" || key == "minLength" ||
  3315. key == "maxLength" || key == "minItems" || key == "maxItems" || key == "exclusiveMinimum" ||
  3316. key == "deprecated" {
  3317. continue
  3318. }
  3319. // 递归清理嵌套对象
  3320. cleaned[key] = cleanToolSchema(value)
  3321. }
  3322. if enum, ok := cleaned["enum"].([]any); ok {
  3323. if normalized, ok := normalizeGeminiEnum(enum); ok {
  3324. cleaned["enum"] = normalized
  3325. } else {
  3326. delete(cleaned, "enum")
  3327. }
  3328. }
  3329. // 规范化 type 字段为大写
  3330. if typeVal, ok := cleaned["type"].(string); ok {
  3331. cleaned["type"] = strings.ToUpper(typeVal)
  3332. } else if typeValues, ok := cleaned["type"].([]any); ok {
  3333. for _, typeValue := range typeValues {
  3334. typeName, ok := typeValue.(string)
  3335. if ok && !strings.EqualFold(typeName, "null") {
  3336. cleaned["type"] = strings.ToUpper(typeName)
  3337. break
  3338. }
  3339. }
  3340. if _, ok := cleaned["type"].([]any); ok {
  3341. delete(cleaned, "type")
  3342. }
  3343. }
  3344. if cleaned["type"] == "INTEGER" {
  3345. if minimum, ok := incrementIntegralSchemaBound(v["exclusiveMinimum"]); ok {
  3346. if existing, exists := cleaned["minimum"]; !exists || schemaNumberLess(existing, minimum) {
  3347. cleaned["minimum"] = minimum
  3348. }
  3349. }
  3350. }
  3351. return cleaned
  3352. case []any:
  3353. cleaned := make([]any, len(v))
  3354. for i, item := range v {
  3355. cleaned[i] = cleanToolSchema(item)
  3356. }
  3357. return cleaned
  3358. default:
  3359. return v
  3360. }
  3361. }
  3362. func normalizeGeminiEnum(values []any) ([]any, bool) {
  3363. normalized := make([]any, len(values))
  3364. for i, value := range values {
  3365. if stringValue, ok := value.(string); ok {
  3366. normalized[i] = stringValue
  3367. continue
  3368. }
  3369. switch value.(type) {
  3370. case nil, bool, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, json.Number:
  3371. encoded, err := json.Marshal(value)
  3372. if err != nil {
  3373. return nil, false
  3374. }
  3375. normalized[i] = string(encoded)
  3376. default:
  3377. return nil, false
  3378. }
  3379. }
  3380. return normalized, true
  3381. }
  3382. func incrementIntegralSchemaBound(value any) (any, bool) {
  3383. switch v := value.(type) {
  3384. case float64:
  3385. if math.IsNaN(v) || math.IsInf(v, 0) || v != math.Trunc(v) || v+1 <= v {
  3386. return nil, false
  3387. }
  3388. return v + 1, true
  3389. case int:
  3390. if v == math.MaxInt {
  3391. return nil, false
  3392. }
  3393. return v + 1, true
  3394. case int64:
  3395. if v == math.MaxInt64 {
  3396. return nil, false
  3397. }
  3398. return v + 1, true
  3399. case json.Number:
  3400. i, err := v.Int64()
  3401. if err != nil || i == math.MaxInt64 {
  3402. return nil, false
  3403. }
  3404. return json.Number(fmt.Sprintf("%d", i+1)), true
  3405. default:
  3406. return nil, false
  3407. }
  3408. }
  3409. func schemaNumberLess(left, right any) bool {
  3410. leftNumber, leftOK := schemaNumberFloat64(left)
  3411. rightNumber, rightOK := schemaNumberFloat64(right)
  3412. return leftOK && rightOK && leftNumber < rightNumber
  3413. }
  3414. func schemaNumberFloat64(value any) (float64, bool) {
  3415. switch v := value.(type) {
  3416. case float64:
  3417. return v, !math.IsNaN(v) && !math.IsInf(v, 0)
  3418. case int:
  3419. return float64(v), true
  3420. case int64:
  3421. return float64(v), true
  3422. case json.Number:
  3423. n, err := v.Float64()
  3424. return n, err == nil && !math.IsInf(n, 0)
  3425. default:
  3426. return 0, false
  3427. }
  3428. }
  3429. func convertClaudeGenerationConfig(req map[string]any) map[string]any {
  3430. out := make(map[string]any)
  3431. if mt, ok := asInt(req["max_tokens"]); ok && mt > 0 {
  3432. out["maxOutputTokens"] = mt
  3433. }
  3434. if temp, ok := req["temperature"].(float64); ok {
  3435. out["temperature"] = temp
  3436. }
  3437. if topP, ok := req["top_p"].(float64); ok {
  3438. out["topP"] = topP
  3439. }
  3440. if stopSeq, ok := req["stop_sequences"].([]any); ok && len(stopSeq) > 0 {
  3441. out["stopSequences"] = stopSeq
  3442. }
  3443. if len(out) == 0 {
  3444. return nil
  3445. }
  3446. return out
  3447. }
  3448. func (s *GeminiMessagesCompatService) extractImageInputSize(body []byte) string {
  3449. var req struct {
  3450. GenerationConfig *struct {
  3451. ImageConfig *struct {
  3452. ImageSize string `json:"imageSize"`
  3453. } `json:"imageConfig"`
  3454. } `json:"generationConfig"`
  3455. }
  3456. if err := json.Unmarshal(body, &req); err != nil {
  3457. return ""
  3458. }
  3459. if req.GenerationConfig != nil && req.GenerationConfig.ImageConfig != nil {
  3460. return strings.TrimSpace(req.GenerationConfig.ImageConfig.ImageSize)
  3461. }
  3462. return ""
  3463. }