feat: gemini-2.5-flash-image-preview 文本和图片输出计费 - #1677
Conversation
WalkthroughIntroduces image-output token accounting and billing for Gemini 2.5 flash image preview models across relay, billing, pricing, and UI. Renames a DTO for token details and adds candidate token details. Adjusts token counting logic, extends model support and ratios, adds operation pricing, and updates frontend rendering and logs. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Relay as Relay (Gemini handlers)
participant Billing as CompatibleHandler
participant Ops as operation_setting
participant Ratios as ratio_setting
participant DTO as dto/gemini
Client->>Relay: Request (model: gemini-2.5-flash-image-preview*)
Relay->>DTO: Parse usage metadata (PromptTokensDetails, CandidatesTokensDetails)
alt Streaming
Relay->>Relay: Sum IMAGE in CandidatesTokensDetails
else Non-stream
Relay->>Relay: Scan candidate parts for image inline data
end
Relay->>Relay: Subtract image-output tokens from completion/total
Relay->>Billing: Context gemini_image_tokens
Billing->>Ops: GetGeminiImageOutputPricePerMillionTokens(model)
Ops-->>Billing: price or 0
Billing->>Ratios: Get group/model ratios
Billing->>Billing: Compute image-output quota (decimal)
Billing-->>Client: Response + logs (image_output_token_count, image_output_price)
sequenceDiagram
autonumber
participant Counter as service/token_counter
participant Store as Model settings
participant Relay as Relay
Counter->>Store: Check model startsWith gemini-2.5-flash-image-preview
alt preview model
Counter->>Counter: getImageToken(file, model, isStream)
else other Gemini image
Counter->>Counter: +256 image tokens
end
Counter-->>Relay: Input token count
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/hooks/usage-logs/useUsageLogsData.jsx (1)
426-450: Align renderModelPrice call arguments to the updated signatureThe helper’s signature now declares:
export function renderModelPrice( …, image = false, imageRatio = 1.0, imageInputTokens = 0, ← new slot webSearch = false, …, imageOutputTokens = 0, imageOutputPrice = 0, )But the current invocation still passes the legacy
other?.image_outputas the 12th argument—so it’s being interpreted asimageInputTokens, shifting every subsequent argument out of place. To fix:• Remove the deprecated
other?.image_outputargument.
• Immediately afterother?.image_ratio, insertother?.image_input_token_count || 0so thatimageInputTokensis populated correctly.
• Leave the existingother?.image_output_token_countandother?.image_output_priceat the end (they already map to slots 22 and 23).No other call sites were found.
Locations to update:
- File: web/src/hooks/usage-logs/useUsageLogsData.jsx
- Lines: around 426–450 (the
renderModelPrice(invocation)Suggested diff:
@@ web/src/hooks/usage-logs/useUsageLogsData.jsx:426 - other?.image_ratio || 0, - other?.image_output || 0, + other?.image_ratio || 0, + other?.image_input_token_count || 0, other?.web_search || false, other?.web_search_call_count || 0, other?.web_search_price || 0, // …rest unchanged…This change ensures that:
imageInputTokensis correctly passed fromother.image_input_token_countimageOutputTokensandimageOutputPricestill map to the final two parameters- The deprecated
image_outputargument is dropped, preventing misaligned math
🧹 Nitpick comments (6)
setting/model_setting/gemini.go (1)
29-30: Addition looks correct; consider future-proofing with prefix matchExact-name inclusion is fine. If Google ships date-suffixed variants (e.g., gemini-2.5-flash-image-preview-YYYY-MM-DD), an exact equality check in IsGeminiModelSupportImagine will miss them. You could either add a small prefix check in IsGeminiModelSupportImagine or list additional variants here when they arrive.
setting/operation_setting/tools.go (1)
73-79: Price resolver is correct; small robustness nitPrefix check is correct for future date-suffixed variants. To be defensive against accidental casing, consider normalizing once.
Apply this minimal diff:
-func GetGeminiImageOutputPricePerMillionTokens(modelName string) float64 { - if strings.HasPrefix(modelName, "gemini-2.5-flash-image-preview") { +func GetGeminiImageOutputPricePerMillionTokens(modelName string) float64 { + name := strings.ToLower(modelName) + if strings.HasPrefix(name, "gemini-2.5-flash-image-preview") { return Gemini25FlashImagePreviewImageOutputPrice } return 0 }service/token_counter.go (1)
307-315: Gemini image counting exception is correct; minor hardening recommendedLogic correctly falls back to precise image token calc for gemini-2.5-flash-image-preview while keeping 256 for other Gemini formats. To avoid edge-case casing issues and magic numbers, consider:
- Normalize model once for prefix check.
- Lift 256 into a named const for readability.
- case types.FileTypeImage: - if info.RelayFormat == types.RelayFormatGemini && !strings.HasPrefix(model, "gemini-2.5-flash-image-preview") { - tkm += 256 + case types.FileTypeImage: + constFixedGeminiImg := 256 + modelLower := strings.ToLower(model) + if info.RelayFormat == types.RelayFormatGemini && !strings.HasPrefix(modelLower, "gemini-2.5-flash-image-preview") { + tkm += constFixedGeminiImg } else { token, err := getImageToken(file, model, info.IsStream) if err != nil { return 0, fmt.Errorf("error counting image token: %v", err) } tkm += token }If model remapping is used (upstream_model_name in logs), please confirm we want to key this decision off the original request model (ContextKeyOriginalModel) rather than the mapped upstream model. This impacts whether certain remapped Gemini calls take the 256 shortcut or the precise path.
relay/channel/gemini/relay-gemini-native.go (1)
49-64: Consider usingCandidatesTokensDetailsfor image output accountingThe current implementation counts image outputs by scanning content parts, but there's commented-out code that uses
CandidatesTokensDetails(lines 65-74). Since the DTO now supportsCandidatesTokensDetailswith modality information, consider using the more reliable API-provided token counts instead of hardcoding 1290 tokens per image.- if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") { - imageOutputCounts := 0 - for _, candidate := range geminiResponse.Candidates { - for _, part := range candidate.Content.Parts { - if part.InlineData != nil && strings.HasPrefix(part.InlineData.MimeType, "image/") { - imageOutputCounts++ - } - } - } - if imageOutputCounts != 0 { - usage.CompletionTokens = usage.CompletionTokens - imageOutputCounts*1290 - usage.TotalTokens = usage.TotalTokens - imageOutputCounts*1290 - c.Set("gemini_image_tokens", imageOutputCounts*1290) - } - } - - // if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") { - // for _, detail := range geminiResponse.UsageMetadata.CandidatesTokensDetails { - // if detail.Modality == "IMAGE" { - // usage.CompletionTokens = usage.CompletionTokens - detail.TokenCount - // usage.TotalTokens = usage.TotalTokens - detail.TokenCount - // c.Set("gemini_image_tokens", detail.TokenCount) - // } - // } - // } + if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") { + for _, detail := range geminiResponse.UsageMetadata.CandidatesTokensDetails { + if detail.Modality == "IMAGE" { + usage.CompletionTokens = usage.CompletionTokens - detail.TokenCount + usage.TotalTokens = usage.TotalTokens - detail.TokenCount + c.Set("gemini_image_tokens", detail.TokenCount) + } + } + }dto/gemini.go (1)
9-11: Minor import formatting adjustmentMoving the
github.com/gin-gonic/ginimport to a new line is fine, but consider grouping all external imports together for better consistency.web/src/helpers/render.jsx (1)
1083-1094: Consider better parameter organizationThe
renderModelPricefunction now has 16 parameters, which makes it difficult to use and maintain. Consider using an options object pattern to group related parameters.export function renderModelPrice( inputTokens, completionTokens, - modelRatio, - modelPrice = -1, - completionRatio, - groupRatio, - user_group_ratio, - cacheTokens = 0, - cacheRatio = 1.0, - image = false, - imageRatio = 1.0, - imageInputTokens = 0, - webSearch = false, - webSearchCallCount = 0, - webSearchPrice = 0, - fileSearch = false, - fileSearchCallCount = 0, - fileSearchPrice = 0, - audioInputSeperatePrice = false, - audioInputTokens = 0, - audioInputPrice = 0, - imageOutputTokens = 0, - imageOutputPrice = 0, + options = {} ) { + const { + modelRatio, + modelPrice = -1, + completionRatio, + groupRatio, + user_group_ratio, + cacheTokens = 0, + cacheRatio = 1.0, + image = false, + imageRatio = 1.0, + imageInputTokens = 0, + webSearch = false, + webSearchCallCount = 0, + webSearchPrice = 0, + fileSearch = false, + fileSearchCallCount = 0, + fileSearchPrice = 0, + audioInputSeperatePrice = false, + audioInputTokens = 0, + audioInputPrice = 0, + imageOutputTokens = 0, + imageOutputPrice = 0, + } = options;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
dto/gemini.go(2 hunks)relay/channel/gemini/relay-gemini-native.go(2 hunks)relay/compatible_handler.go(2 hunks)service/token_counter.go(1 hunks)setting/model_setting/gemini.go(1 hunks)setting/operation_setting/tools.go(2 hunks)setting/ratio_setting/model_ratio.go(2 hunks)web/src/helpers/render.jsx(7 hunks)web/src/hooks/usage-logs/useUsageLogsData.jsx(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
📚 Learning: 2025-06-21T03:37:41.726Z
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
Applied to files:
service/token_counter.gorelay/channel/gemini/relay-gemini-native.gosetting/operation_setting/tools.gorelay/compatible_handler.go
📚 Learning: 2025-08-05T17:14:17.246Z
Learnt from: neotf
PR: QuantumNous/new-api#1511
File: setting/ratio_setting/model_ratio.go:118-123
Timestamp: 2025-08-05T17:14:17.246Z
Learning: Claude models handle "-thinking" variants differently from Gemini models. For Claude models, only the base model (without "-thinking") gets an entry in defaultModelRatio map. The "-thinking" variants rely on the Claude relay handler stripping the suffix using strings.TrimSuffix(textRequest.Model, "-thinking") before looking up the ratio, so they automatically use the base model's ratio.
Applied to files:
setting/ratio_setting/model_ratio.go
🧬 Code graph analysis (2)
service/token_counter.go (1)
types/relay_format.go (2)
RelayFormat(3-3)RelayFormatGemini(8-8)
relay/compatible_handler.go (1)
setting/operation_setting/tools.go (1)
GetGeminiImageOutputPricePerMillionTokens(73-78)
🔇 Additional comments (8)
setting/ratio_setting/model_ratio.go (2)
181-182: Model ratio entry aligns with comment and existing flash pricing0.15 matches the annotated $0.30/1M tokens baseline and keeps parity with gemini-2.5-flash. No functional concerns.
297-302: Completion ratio value is consistent with hardcoded fallback8.3333333333 matches the existing 2.5/0.3 logic in getHardcodedCompletionModelRatio for flash. Explicit mapping here removes reliance on fallback; good for clarity.
setting/operation_setting/tools.go (1)
27-30: Adds image-output price constant: OKNaming and visibility are consistent with existing pricing constants.
relay/channel/gemini/relay-gemini-native.go (1)
166-174: LGTM! Consistent implementation with non-streaming handlerThe streaming implementation correctly uses
CandidatesTokensDetailsto account for image tokens, which is more reliable than counting image parts.dto/gemini.go (1)
271-283: LGTM! Well-structured type renaming and field additionsThe renaming from
GeminiPromptTokensDetailstoGeminiModalityTokenCountis more descriptive and accurately represents the shared structure for both prompt and candidate token details. The addition ofCandidatesTokensDetailsfield enables proper tracking of output tokens by modality.relay/compatible_handler.go (2)
427-430: LGTM! Image output billing integrated correctlyThe image output token count and price are properly added to the logging context when applicable. This ensures accurate billing tracking for image generation.
317-325: No additional validation required forgemini_image_tokensThe call to
ctx.GetInt("gemini_image_tokens")safely returns 0 when the key is unset (per the Gin Context behavior), so:
- If the native Gemini handler set a positive token count, that value will be used.
- If it wasn’t set (e.g. for non-image models or in the compatible handler),
dImageOutputTokensbecomes 0, yielding a zero quota without error.Because defaulting to zero tokens has no runtime side-effects, and the native handler always populates this key for supported image requests, no further checks are needed here.
web/src/helpers/render.jsx (1)
1197-1205: LGTM! Image output pricing display implemented correctlyThe UI properly displays image output pricing when both tokens and price are present, maintaining consistency with other pricing displays.
| if (image && imageInputTokens > 0) { | ||
| effectiveInputTokens = | ||
| inputTokens - imageOutputTokens + imageOutputTokens * imageRatio; | ||
| inputTokens - imageInputTokens + imageInputTokens * imageRatio; | ||
| } |
There was a problem hiding this comment.
Fix logical error in image input token handling
The condition checks imageInputTokens > 0 but the gating uses image && imageInputTokens > 0. This could lead to incorrect calculations if image is false but imageInputTokens is provided.
- if (image && imageInputTokens > 0) {
+ if (imageInputTokens > 0) {
effectiveInputTokens =
inputTokens - imageInputTokens + imageInputTokens * imageRatio;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (image && imageInputTokens > 0) { | |
| effectiveInputTokens = | |
| inputTokens - imageOutputTokens + imageOutputTokens * imageRatio; | |
| inputTokens - imageInputTokens + imageInputTokens * imageRatio; | |
| } | |
| if (imageInputTokens > 0) { | |
| effectiveInputTokens = | |
| inputTokens - imageInputTokens + imageInputTokens * imageRatio; | |
| } |
🤖 Prompt for AI Agents
In web/src/helpers/render.jsx around lines 1122-1125, the conditional currently
gates the image token adjustment with "image && imageInputTokens > 0" which
skips the calculation when an explicit imageInputTokens value exists but image
is false; change the condition to rely on the presence/value of imageInputTokens
instead (e.g. check that imageInputTokens is not null/undefined and > 0) and
then apply the effectiveInputTokens calculation so the adjustment runs whenever
imageInputTokens is provided and positive.
|
Hello, can this feature be merged into the main branch? |
…-image-preview-billing feat: gemini-2.5-flash-image-preview 文本和图片输出计费

Summary by CodeRabbit
New Features
Refactor
Chores