| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- package service
- import (
- "strings"
- "github.com/gin-gonic/gin"
- "github.com/tidwall/gjson"
- )
- // geminiImageOutputCounterKey 是请求级内联图片计数器挂在 gin.Context 上的键。
- const geminiImageOutputCounterKey = "gemini_image_output_counter"
- // geminiImageOutputCounter 记录一次转发里 Gemini 上游真正回吐的内联图片数量。
- //
- // 取「单个 payload 内的最大值」而不是累加:Gemini 兼容上游的 SSE 分片可能是
- // 累积式的(同一份内容在后续 chunk 里重复整段回来——本文件同目录的
- // computeGeminiTextDelta 就是为此存在的),逐 chunk 累加会把同一张图算很多次。
- // 计费上宁可少算不可多算,所以用 max 兜底:
- // - 非流式:整份响应体只观测一次,max 即真实张数;
- // - 累积式流:最后一个 chunk 含全部图片,max 仍是真实张数;
- // - 增量式流且多图分散在不同 chunk:会低估到 1,与改动前的模型名启发式同值,
- // 不构成回退。
- type geminiImageOutputCounter struct {
- count int
- }
- // beginGeminiImageOutputObservation 在每次 Forward 开头重置计数器。
- // failover 会拿同一个 gin.Context 重跑转发,不重置就会把上一个账号的图数带进来。
- func beginGeminiImageOutputObservation(c *gin.Context) *geminiImageOutputCounter {
- if c == nil {
- return nil
- }
- counter := &geminiImageOutputCounter{}
- c.Set(geminiImageOutputCounterKey, counter)
- return counter
- }
- func geminiImageOutputCounterFromContext(c *gin.Context) *geminiImageOutputCounter {
- if c == nil {
- return nil
- }
- value, ok := c.Get(geminiImageOutputCounterKey)
- if !ok {
- return nil
- }
- counter, _ := value.(*geminiImageOutputCounter)
- return counter
- }
- // observeGeminiImageOutputs 观测一段上游响应(整份或单个 chunk)里的内联图片。
- // 调用点与 upstreamResponseModelObserver.ObserveGemini 一一对应——那里拿得到
- // 解包后的上游响应体,这里需要的是同一份字节。
- func observeGeminiImageOutputs(c *gin.Context, payload []byte) {
- counter := geminiImageOutputCounterFromContext(c)
- if counter == nil {
- return
- }
- if count := countGeminiInlineImageOutputs(payload); count > counter.count {
- counter.count = count
- }
- }
- func observedGeminiImageOutputs(c *gin.Context) int {
- counter := geminiImageOutputCounterFromContext(c)
- if counter == nil {
- return 0
- }
- return counter.count
- }
- // resolveGeminiImageCount 决定本次请求按几张图计费。
- //
- // 优先用上游真正返回的内联图片数:走 GeminiMessagesCompatService 的账号多是
- // API Key + 自定义模型映射,客户端请求名和上游模型名都可能是站长自取的别名
- // (issue #5358 里的 nana-banana-2),isImageGenerationModel 的白名单必然判不出,
- // 于是 ImageCount=0,calculateRecordUsageCost 整条按次计费分支不触发,
- // 生图请求全部记 $0。
- //
- // 只有响应里数不出图时(例如上游用 fileData 引用而非 inlineData 回图,或聚合
- // 函数丢掉了图片 part)才退回既有的模型名启发式,保证老行为不回退;这里额外
- // 也认映射后的上游模型名,与 shouldSkipCodexPlanGatedImageModelCooldown 对
- // requestedModel / modelKey 双取的口径一致。
- func resolveGeminiImageCount(c *gin.Context, originalModel, mappedModel string) int {
- if observed := observedGeminiImageOutputs(c); observed > 0 {
- return observed
- }
- if isImageGenerationModel(originalModel) || isImageGenerationModel(mappedModel) {
- return 1
- }
- return 0
- }
- // countGeminiInlineImageOutputs 统计一段 Gemini 响应 JSON 里的内联图片 part。
- // Gemini REST 回 camelCase 的 inlineData,官方 SDK 与部分中转会回 snake_case
- // 的 inline_data,两种都要认。
- func countGeminiInlineImageOutputs(payload []byte) int {
- if len(payload) == 0 || !gjson.ValidBytes(payload) {
- return 0
- }
- count := 0
- gjson.GetBytes(payload, "candidates").ForEach(func(_, candidate gjson.Result) bool {
- candidate.Get("content.parts").ForEach(func(_, part gjson.Result) bool {
- if geminiPartIsInlineImage(part) {
- count++
- }
- return true
- })
- return true
- })
- return count
- }
- func geminiPartIsInlineImage(part gjson.Result) bool {
- inline := part.Get("inlineData")
- if !inline.Exists() {
- inline = part.Get("inline_data")
- }
- if !inline.Exists() {
- return false
- }
- mimeType := inline.Get("mimeType")
- if !mimeType.Exists() {
- mimeType = inline.Get("mime_type")
- }
- if !isGeminiInlineImageMIMEType(strings.ToLower(strings.TrimSpace(mimeType.String()))) {
- return false
- }
- // 只认真的带上了 base64 数据的 part,空壳 part 不计费。
- return strings.TrimSpace(inline.Get("data").String()) != ""
- }
|