Skip to content

feat: Support Vertex AI Embeddings and Gemini Cached Content Tokens - #2488

Closed
Simba98 wants to merge 4851 commits into
QuantumNous:mainfrom
Simba98:main
Closed

feat: Support Vertex AI Embeddings and Gemini Cached Content Tokens#2488
Simba98 wants to merge 4851 commits into
QuantumNous:mainfrom
Simba98:main

Conversation

@Simba98

@Simba98 Simba98 commented Dec 21, 2025

Copy link
Copy Markdown

This PR introduces two key improvements to the Vertex AI and Gemini channels:

  1. Vertex AI Embedding Support
  2. Gemini Cached Content Tokens
  3. Fix Maas model on global region URL

Summary by CodeRabbit

  • New Features

    • Tracks cached-content tokens in usage metadata and surfaces cached-token counts in usage reports.
    • Adds end-to-end support for Vertex AI embeddings, including request transformation and streaming-friendly embedding responses.
  • Bug Fixes

    • Quota calculation now subtracts cached tokens for Gemini and Vertex AI channels to correct token accounting.

✏️ Tip: You can customize this high-level summary in your review settings.

LeonDevLifeLog and others added 30 commits November 6, 2025 15:23
feat:  EditTokenModal 中针对用户创建的 token 默认无限额度
feat: add environment variable switch for critical rate limit
…80p-image

fix: trim suffix p for jimeng image model
…odisable

fix(channel): 当没有可用密钥时返回错误而不是第一个密钥
… new sections for partners, acknowledgments, and deployment instructions
- 更新中文README.md中的语言链接
- 完全重写英文README.en.md,包含所有详细功能说明
- 完全重写法文README.fr.md,确保内容一致性
- 完全重写日文README.ja.md,提供完整的项目说明

所有语言版本现在具有:
- 相同的结构和格式
- 一致的语言导航
- 完整的功能特性和部署指南
- 统一的环境变量配置说明
…ence

修复viduq2不支持参考生视频的问题
…annel

feat: replicate channel flux model
seefs001 and others added 14 commits December 19, 2025 21:39
…-response-id

fix: 模型设置增加针对Vertex渠道过滤content[].part[].functionResponse.id的选项,默认启用
- Replace legacy `docs.newapi.pro` paths with the new `/{lang}/docs/...` structure across all README translations
- Point key sections (installation, env vars, API, support, features) to their new locations
- Ensure language-specific links use the correct locale prefix (zh/en/ja) and keep FR aligned with English routes
Keep new-site links (/{lang}/docs/...) where matching pages exist in the current docs repo
Revert links that have no equivalent in the new docs to the legacy paths on doc.newapi.pro:
Google Gemini Chat
Midjourney-Proxy image docs
Suno music docs
Apply the same rule consistently across all README translations (zh/en/ja/fr)
…te-doc-links-new-routing

🔗 docs(readme): update documentation links to new site routing
Copilot AI review requested due to automatic review settings December 21, 2025 16:37
@coderabbitai

coderabbitai Bot commented Dec 21, 2025

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds cached-content token tracking from Gemini metadata into usage, routes Gemini embedding requests through a Vertex-compatible transform, introduces a Vertex embedding response type and handler that aggregates tokens, and adjusts quota accounting to subtract cached tokens for Gemini and VertexAi channels.

Changes

Cohort / File(s) Change Summary
Gemini cached token tracking
dto/gemini.go, relay/channel/gemini/relay-gemini-native.go, relay/channel/gemini/relay-gemini.go
Added CachedContentTokenCount to GeminiUsageMetadata and propagate it into usage.PromptTokensDetails.CachedTokens in both streaming and non-stream Gemini handlers.
Vertex embedding transform & handler
relay/channel/vertex/adaptor.go, relay/channel/vertex/relay-vertex.go
Transform Gemini embedding requests into Vertex-compatible instances payloads (aggregate content parts, forward optional task_type/title, handle output_dimensionality) and add VertexEmbeddingResponse + vertexEmbeddingHandler to unmarshal responses, aggregate token counts, and stream the original response body.
Quota accounting update
service/quota.go
In PostClaudeConsumeQuota, subtract cached tokens from prompt tokens for Gemini and VertexAi channel types when computing quota consumption.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Relay as Relay (Vertex Adaptor)
    participant VertexAPI as Vertex AI
    participant Handler as vertexEmbeddingHandler
    participant Quota as Quota Service

    Client->>Relay: POST Gemini embedding request
    Relay->>Relay: Read & parse Gemini embedding body
    Relay->>Relay: Build Vertex `instances` (aggregate content, task_type/title, params)
    Relay->>VertexAPI: POST Vertex-compatible embedding request
    VertexAPI-->>Relay: JSON embedding response
    Relay->>Handler: Route response to vertexEmbeddingHandler
    Handler->>Handler: Unmarshal VertexEmbeddingResponse, aggregate token counts
    Handler-->>Client: Stream original response body and attach usage metadata
    Client->>Quota: Report usage (includes cached tokens)
    Quota->>Quota: Subtract cached tokens for Gemini/VertexAi when consuming quota
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect relay/channel/vertex/adaptor.go for correct parsing and edge cases (multi-part inputs, optional fields, output dimensionality).
  • Verify VertexEmbeddingResponse structure and token aggregation in relay/channel/vertex/relay-vertex.go.
  • Confirm cached-token propagation in relay/channel/gemini/*.
  • Validate service/quota.go branch logic to avoid negative or double-counting.

Possibly related PRs

Suggested reviewers

  • seefs001

Poem

🐰
I nibble cached crumbs from a Gemini tree,
fold them into vectors, light and free,
Vertex hums, tokens counted with care,
quotas trimmed softly — hop here and there,
relay rabbit scampers, code neat as a pea.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main changes: adding support for Vertex AI embeddings and Gemini cached content tokens, which are the primary objectives mentioned in the PR description.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb554c and 70fd738.

📒 Files selected for processing (1)
  • relay/channel/vertex/adaptor.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • relay/channel/vertex/adaptor.go

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
relay/channel/vertex/relay-vertex.go (2)

63-67: Consider using common.Unmarshal for consistency.

Other handlers in the codebase (e.g., GeminiEmbeddingHandler in relay-gemini.go) use common.Unmarshal instead of json.Unmarshal. Using the common utility ensures consistent behavior across the codebase.

🔎 Proposed fix
 	var vertexResponse VertexEmbeddingResponse
-	err = json.Unmarshal(responseBody, &vertexResponse)
+	err = common.Unmarshal(responseBody, &vertexResponse)
 	if err != nil {
 		return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
 	}

37-49: Anonymous nested structs reduce reusability.

The VertexEmbeddingResponse uses anonymous nested structs. While functional, extracting named types would improve code readability and enable reuse if these structures are needed elsewhere.

This is a minor style consideration and can be addressed later if the response format needs to be referenced in other parts of the codebase.

relay/channel/vertex/adaptor.go (1)

375-421: Consider extracting duplicate instance-building logic.

The code for building instances from embedding requests is duplicated between the batch and single request paths. Consider extracting this into a helper function to improve maintainability.

🔎 Example refactor
func buildEmbeddingInstance(content dto.GeminiChatContent, taskType, title string) map[string]interface{} {
	instance := make(map[string]interface{})
	contentStr := ""
	for _, part := range content.Parts {
		if part.Text != "" {
			contentStr += part.Text
		}
	}
	instance["content"] = contentStr
	if taskType != "" {
		instance["task_type"] = taskType
	}
	if title != "" {
		instance["title"] = title
	}
	return instance
}

Then use it in both paths:

 			if err := json.Unmarshal(bodyBytes, &req); err != nil {
 				return nil, fmt.Errorf("failed to unmarshal batch embedding request: %w", err)
 			}
 			for _, r := range req.Requests {
-				instance := make(map[string]interface{})
-				content := ""
-				for _, part := range r.Content.Parts {
-					if part.Text != "" {
-						content += part.Text
-					}
-				}
-				instance["content"] = content
-				if r.TaskType != "" {
-					instance["task_type"] = r.TaskType
-				}
-				if r.Title != "" {
-					instance["title"] = r.Title
-				}
+				instance := buildEmbeddingInstance(r.Content, r.TaskType, r.Title)
 				instances = append(instances, instance)
 			}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afd9c29 and ae48977.

📒 Files selected for processing (6)
  • dto/gemini.go (1 hunks)
  • relay/channel/gemini/relay-gemini-native.go (1 hunks)
  • relay/channel/gemini/relay-gemini.go (2 hunks)
  • relay/channel/vertex/adaptor.go (5 hunks)
  • relay/channel/vertex/relay-vertex.go (2 hunks)
  • service/quota.go (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: RedwindA
Repo: QuantumNous/new-api PR: 1537
File: relay/gemini_handler.go:330-342
Timestamp: 2025-08-08T17:12:43.157Z
Learning: In the new-api repository, the `GeminiEmbeddingHandler` function in `relay/gemini_handler.go` is designed specifically for native Gemini embedding requests and therefore does not require the `ConvertGeminiRequest` step that is used in the chat handler. The embedding requests are already in the native Gemini format and don't need conversion.
Learnt from: 9Ninety
Repo: QuantumNous/new-api PR: 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-08-08T17:12:43.157Z
Learnt from: RedwindA
Repo: QuantumNous/new-api PR: 1537
File: relay/gemini_handler.go:330-342
Timestamp: 2025-08-08T17:12:43.157Z
Learning: In the new-api repository, the `GeminiEmbeddingHandler` function in `relay/gemini_handler.go` is designed specifically for native Gemini embedding requests and therefore does not require the `ConvertGeminiRequest` step that is used in the chat handler. The embedding requests are already in the native Gemini format and don't need conversion.

Applied to files:

  • relay/channel/vertex/relay-vertex.go
  • relay/channel/vertex/adaptor.go
  • relay/channel/gemini/relay-gemini.go
📚 Learning: 2025-08-05T17:14:17.246Z
Learnt from: neotf
Repo: QuantumNous/new-api PR: 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:

  • relay/channel/vertex/adaptor.go
📚 Learning: 2025-06-21T03:37:41.726Z
Learnt from: 9Ninety
Repo: QuantumNous/new-api PR: 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:

  • relay/channel/gemini/relay-gemini-native.go
  • relay/channel/gemini/relay-gemini.go
  • service/quota.go
📚 Learning: 2025-06-18T12:20:25.779Z
Learnt from: neotf
Repo: QuantumNous/new-api PR: 1120
File: service/quota.go:0-0
Timestamp: 2025-06-18T12:20:25.779Z
Learning: For OpenRouter integration: cacheTokens returned from upstream always belong to [0, promptTokens], meaning cacheTokens will never exceed the original promptTokens value. This constraint ensures that operations like `promptTokens -= cacheTokens` will not result in negative values.

Applied to files:

  • service/quota.go
🧬 Code graph analysis (2)
relay/channel/vertex/adaptor.go (3)
dto/gemini.go (2)
  • GeminiBatchEmbeddingRequest (423-425)
  • GeminiEmbeddingRequest (391-397)
common/json.go (2)
  • Unmarshal (9-11)
  • Marshal (21-23)
logger/logger.go (1)
  • LogDebug (69-76)
service/quota.go (1)
constant/channel.go (2)
  • ChannelTypeGemini (28-28)
  • ChannelTypeVertexAi (41-41)
🔇 Additional comments (9)
relay/channel/gemini/relay-gemini-native.go (1)

46-46: Cached token propagation looks correct.

The assignment follows the established pattern used for ReasoningTokens on line 45, consistently propagating the new CachedContentTokenCount field from the Gemini API response into the usage structure.

relay/channel/gemini/relay-gemini.go (2)

1105-1105: Streaming handler cached token propagation is consistent.

The assignment correctly propagates CachedContentTokenCount within the streaming usage metadata update block, following the same pattern as the ReasoningTokens assignment on line 1104.


1240-1240: Non-streaming handler cached token propagation is consistent.

The assignment in the chat handler follows the same pattern as the streaming handler and native handler, ensuring cached tokens are properly tracked across all Gemini response paths.

dto/gemini.go (1)

348-355: DTO field addition for cached content token tracking looks good.

The new CachedContentTokenCount field follows the existing naming conventions and type patterns in GeminiUsageMetadata. The JSON tag cachedContentTokenCount aligns with the Gemini API response format.

relay/channel/vertex/relay-vertex.go (1)

74-82: Verify if Vertex embedding response should be converted to OpenAI format.

The GeminiEmbeddingHandler in relay-gemini.go converts the Gemini embedding response to the OpenAI embedding format (dto.OpenAIEmbeddingResponse) before returning. This handler returns the raw Vertex response body via IOCopyBytesGracefully.

If clients expect a consistent OpenAI-compatible embedding response format, consider converting the Vertex response similarly:

openAIResponse := dto.OpenAIEmbeddingResponse{
    Object: "list",
    Data:   make([]dto.OpenAIEmbeddingResponseItem, 0, len(vertexResponse.Predictions)),
    Model:  info.UpstreamModelName,
}
for i, prediction := range vertexResponse.Predictions {
    openAIResponse.Data = append(openAIResponse.Data, dto.OpenAIEmbeddingResponseItem{
        Object:    "embedding",
        Embedding: prediction.Embeddings.Values,
        Index:     i,
    })
}
openAIResponse.Usage = *usage

If the raw Vertex format is intentional for native Vertex API passthrough, this is fine as-is.

service/quota.go (1)

270-272: Remove this review. The Gemini/VertexAi API design guarantees that cachedContentTokenCount is always a subset of promptTokenCount since cached content forms a prefix within the prompt. No bounds check is needed.

Likely an incorrect or invalid review comment.

relay/channel/vertex/adaptor.go (3)

4-4: LGTM!

The new imports are properly utilized in the embedding transformation logic.

Also applies to: 14-14


231-233: LGTM!

The condition correctly identifies embedding models and routes them to use the "predict" suffix, consistent with how imagen models are handled.


450-452: No issues found. The info.RequestURLPath is properly populated from c.Request.URL.String() before reaching the DoResponse function, and the routing logic correctly identifies and directs embedding requests to vertexEmbeddingHandler.

Comment on lines +365 to +426
if a.RequestMode == RequestModeGemini && strings.Contains(c.Request.URL.Path, "embed") {
bodyBytes, err := io.ReadAll(requestBody)
if err != nil {
return nil, err
}

var newBodyBytes []byte
vertexReq := make(map[string]interface{})
instances := make([]interface{}, 0)

if info.IsGeminiBatchEmbedding {
var req dto.GeminiBatchEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err == nil {
for _, r := range req.Requests {
instance := make(map[string]interface{})
content := ""
for _, part := range r.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if r.TaskType != "" {
instance["task_type"] = r.TaskType
}
if r.Title != "" {
instance["title"] = r.Title
}
instances = append(instances, instance)
}
}
} else {
var req dto.GeminiEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err == nil {
instance := make(map[string]interface{})
content := ""
for _, part := range req.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if req.TaskType != "" {
instance["task_type"] = req.TaskType
}
if req.Title != "" {
instance["title"] = req.Title
}
instances = append(instances, instance)

if req.OutputDimensionality > 0 {
vertexReq["parameters"] = map[string]interface{}{
"outputDimensionality": req.OutputDimensionality,
}
}
}
}
vertexReq["instances"] = instances
newBodyBytes, _ = json.Marshal(vertexReq)
requestBody = bytes.NewReader(newBodyBytes)
logger.LogDebug(c, "Vertex Embedding request body: "+string(newBodyBytes))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate that instances array is non-empty.

After processing the request, there's no validation that the instances array contains at least one element. Sending an empty instances array would result in an invalid API request.

🔎 Proposed fix
 		}
 		vertexReq["instances"] = instances
+		if len(instances) == 0 {
+			return nil, errors.New("no valid embedding content found in request")
+		}
 		newBodyBytes, _ = json.Marshal(vertexReq)
🤖 Prompt for AI Agents
In relay/channel/vertex/adaptor.go around lines 365 to 426, after building the
instances slice validate that it contains at least one element and fail fast if
not: if len(instances) == 0 return an error (or an HTTP 400 style error)
indicating "empty instances in embedding request" instead of marshaling and
forwarding an empty instances array; ensure the error is returned consistently
with the surrounding function's error handling and log a debug/warn message with
context before returning.


if info.IsGeminiBatchEmbedding {
var req dto.GeminiBatchEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle unmarshaling errors explicitly.

The unmarshaling errors are silently ignored with err == nil checks. If unmarshaling fails, the code proceeds with an empty instances array, which could result in invalid API requests.

🔎 Proposed fix
 		if info.IsGeminiBatchEmbedding {
 			var req dto.GeminiBatchEmbeddingRequest
-			if err := json.Unmarshal(bodyBytes, &req); err == nil {
+			if err := json.Unmarshal(bodyBytes, &req); err != nil {
+				return nil, fmt.Errorf("failed to unmarshal batch embedding request: %w", err)
+			}
 				for _, r := range req.Requests {
 					instance := make(map[string]interface{})
 					content := ""
 					for _, part := range r.Content.Parts {
 						if part.Text != "" {
 							content += part.Text
 						}
 					}
 					instance["content"] = content
 					if r.TaskType != "" {
 						instance["task_type"] = r.TaskType
 					}
 					if r.Title != "" {
 						instance["title"] = r.Title
 					}
 					instances = append(instances, instance)
 				}
-			}
 		} else {
 			var req dto.GeminiEmbeddingRequest
-			if err := json.Unmarshal(bodyBytes, &req); err == nil {
+			if err := json.Unmarshal(bodyBytes, &req); err != nil {
+				return nil, fmt.Errorf("failed to unmarshal embedding request: %w", err)
+			}
 				instance := make(map[string]interface{})
 				content := ""
 				for _, part := range req.Content.Parts {
 					if part.Text != "" {
 						content += part.Text
 					}
 				}
 				instance["content"] = content
 				if req.TaskType != "" {
 					instance["task_type"] = req.TaskType
 				}
 				if req.Title != "" {
 					instance["title"] = req.Title
 				}
 				instances = append(instances, instance)
 
 				if req.OutputDimensionality > 0 {
 					vertexReq["parameters"] = map[string]interface{}{
 						"outputDimensionality": req.OutputDimensionality,
 					}
 				}
-			}
 		}

Also applies to: 398-398

🤖 Prompt for AI Agents
In relay/channel/vertex/adaptor.go around lines 377 and 398, the json.Unmarshal
errors are currently ignored (using only err == nil) which lets execution
continue with empty/invalid data; change both blocks to explicitly check for err
!= nil, log the unmarshaling error with context, and return an appropriate HTTP
error response (e.g., 400 Bad Request or an internal error) instead of
proceeding, so invalid JSON does not result in invalid API requests.

}
}
vertexReq["instances"] = instances
newBodyBytes, _ = json.Marshal(vertexReq)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle the Marshal error.

The error from json.Marshal is ignored. If marshaling fails, newBodyBytes will be nil or empty, leading to an invalid request.

🔎 Proposed fix
-		newBodyBytes, _ = json.Marshal(vertexReq)
+		newBodyBytes, err = json.Marshal(vertexReq)
+		if err != nil {
+			return nil, fmt.Errorf("failed to marshal vertex embedding request: %w", err)
+		}
📝 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.

Suggested change
newBodyBytes, _ = json.Marshal(vertexReq)
newBodyBytes, err = json.Marshal(vertexReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal vertex embedding request: %w", err)
}
🤖 Prompt for AI Agents
In relay/channel/vertex/adaptor.go around line 423, json.Marshal's error is
currently ignored when marshaling vertexReq into newBodyBytes; update the code
to capture the returned error, handle it (e.g., log/processLogger.Error or
return the error up the call stack), and abort building/sending the request when
marshaling fails. Concretely: assign both newBodyBytes and err from
json.Marshal, check if err != nil, then return an appropriate error or respond
with a failure (and log the marshaling error) instead of proceeding with a
nil/empty body.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for Vertex AI embeddings and Gemini cached content tokens to improve API compatibility and token accounting accuracy.

  • Added support for transforming Gemini embedding requests to Vertex AI's prediction API format
  • Implemented cached content token tracking in Gemini responses for accurate quota calculation
  • Added vertex embedding response handler with token counting from API statistics

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
service/quota.go Added cached token deduction logic for Gemini and Vertex AI channels in quota calculation
relay/channel/vertex/relay-vertex.go Added vertex embedding response handler with token count extraction from predictions
relay/channel/vertex/adaptor.go Implemented embedding request transformation from Gemini format to Vertex AI format with proper routing logic
relay/channel/gemini/relay-gemini.go Added cached content token tracking to both streaming and non-streaming chat handlers
relay/channel/gemini/relay-gemini-native.go Added cached content token tracking to native Gemini text generation handler
dto/gemini.go Added CachedContentTokenCount field to GeminiUsageMetadata structure for tracking cached tokens

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +377 to +418
if err := json.Unmarshal(bodyBytes, &req); err == nil {
for _, r := range req.Requests {
instance := make(map[string]interface{})
content := ""
for _, part := range r.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if r.TaskType != "" {
instance["task_type"] = r.TaskType
}
if r.Title != "" {
instance["title"] = r.Title
}
instances = append(instances, instance)
}
}
} else {
var req dto.GeminiEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err == nil {
instance := make(map[string]interface{})
content := ""
for _, part := range req.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if req.TaskType != "" {
instance["task_type"] = req.TaskType
}
if req.Title != "" {
instance["title"] = req.Title
}
instances = append(instances, instance)

if req.OutputDimensionality > 0 {
vertexReq["parameters"] = map[string]interface{}{
"outputDimensionality": req.OutputDimensionality,
}

Copilot AI Dec 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error from json.Unmarshal is silently ignored with 'if err == nil'. If unmarshaling fails, the code continues with an empty instances array, which will result in an empty vertexReq being sent to the API. This could lead to API errors or unexpected behavior. Consider returning an error if unmarshaling fails instead of silently proceeding.

Suggested change
if err := json.Unmarshal(bodyBytes, &req); err == nil {
for _, r := range req.Requests {
instance := make(map[string]interface{})
content := ""
for _, part := range r.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if r.TaskType != "" {
instance["task_type"] = r.TaskType
}
if r.Title != "" {
instance["title"] = r.Title
}
instances = append(instances, instance)
}
}
} else {
var req dto.GeminiEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err == nil {
instance := make(map[string]interface{})
content := ""
for _, part := range req.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if req.TaskType != "" {
instance["task_type"] = req.TaskType
}
if req.Title != "" {
instance["title"] = req.Title
}
instances = append(instances, instance)
if req.OutputDimensionality > 0 {
vertexReq["parameters"] = map[string]interface{}{
"outputDimensionality": req.OutputDimensionality,
}
if err := json.Unmarshal(bodyBytes, &req); err != nil {
return nil, err
}
for _, r := range req.Requests {
instance := make(map[string]interface{})
content := ""
for _, part := range r.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if r.TaskType != "" {
instance["task_type"] = r.TaskType
}
if r.Title != "" {
instance["title"] = r.Title
}
instances = append(instances, instance)
}
} else {
var req dto.GeminiEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err != nil {
return nil, err
}
instance := make(map[string]interface{})
content := ""
for _, part := range req.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if req.TaskType != "" {
instance["task_type"] = req.TaskType
}
if req.Title != "" {
instance["title"] = req.Title
}
instances = append(instances, instance)
if req.OutputDimensionality > 0 {
vertexReq["parameters"] = map[string]interface{}{
"outputDimensionality": req.OutputDimensionality,

Copilot uses AI. Check for mistakes.
Comment on lines +377 to +418
if err := json.Unmarshal(bodyBytes, &req); err == nil {
for _, r := range req.Requests {
instance := make(map[string]interface{})
content := ""
for _, part := range r.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if r.TaskType != "" {
instance["task_type"] = r.TaskType
}
if r.Title != "" {
instance["title"] = r.Title
}
instances = append(instances, instance)
}
}
} else {
var req dto.GeminiEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err == nil {
instance := make(map[string]interface{})
content := ""
for _, part := range req.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if req.TaskType != "" {
instance["task_type"] = req.TaskType
}
if req.Title != "" {
instance["title"] = req.Title
}
instances = append(instances, instance)

if req.OutputDimensionality > 0 {
vertexReq["parameters"] = map[string]interface{}{
"outputDimensionality": req.OutputDimensionality,
}

Copilot AI Dec 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error from json.Unmarshal is silently ignored with 'if err == nil'. If unmarshaling fails, the code continues with an empty instances array, which will result in an empty vertexReq being sent to the API. This could lead to API errors or unexpected behavior. Consider returning an error if unmarshaling fails instead of silently proceeding.

Suggested change
if err := json.Unmarshal(bodyBytes, &req); err == nil {
for _, r := range req.Requests {
instance := make(map[string]interface{})
content := ""
for _, part := range r.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if r.TaskType != "" {
instance["task_type"] = r.TaskType
}
if r.Title != "" {
instance["title"] = r.Title
}
instances = append(instances, instance)
}
}
} else {
var req dto.GeminiEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err == nil {
instance := make(map[string]interface{})
content := ""
for _, part := range req.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if req.TaskType != "" {
instance["task_type"] = req.TaskType
}
if req.Title != "" {
instance["title"] = req.Title
}
instances = append(instances, instance)
if req.OutputDimensionality > 0 {
vertexReq["parameters"] = map[string]interface{}{
"outputDimensionality": req.OutputDimensionality,
}
if err := json.Unmarshal(bodyBytes, &req); err != nil {
return nil, fmt.Errorf("failed to unmarshal GeminiBatchEmbeddingRequest: %w", err)
}
for _, r := range req.Requests {
instance := make(map[string]interface{})
content := ""
for _, part := range r.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if r.TaskType != "" {
instance["task_type"] = r.TaskType
}
if r.Title != "" {
instance["title"] = r.Title
}
instances = append(instances, instance)
}
} else {
var req dto.GeminiEmbeddingRequest
if err := json.Unmarshal(bodyBytes, &req); err != nil {
return nil, fmt.Errorf("failed to unmarshal GeminiEmbeddingRequest: %w", err)
}
instance := make(map[string]interface{})
content := ""
for _, part := range req.Content.Parts {
if part.Text != "" {
content += part.Text
}
}
instance["content"] = content
if req.TaskType != "" {
instance["task_type"] = req.TaskType
}
if req.Title != "" {
instance["title"] = req.Title
}
instances = append(instances, instance)
if req.OutputDimensionality > 0 {
vertexReq["parameters"] = map[string]interface{}{
"outputDimensionality": req.OutputDimensionality,

Copilot uses AI. Check for mistakes.
}
}
vertexReq["instances"] = instances
newBodyBytes, _ = json.Marshal(vertexReq)

Copilot AI Dec 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error returned from json.Marshal is ignored using the blank identifier. If marshaling fails, newBodyBytes will be nil, which could cause a panic or unexpected behavior when creating the bytes.NewReader. Consider handling this error appropriately by returning it to the caller.

Suggested change
newBodyBytes, _ = json.Marshal(vertexReq)
newBodyBytes, err = json.Marshal(vertexReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal vertex embedding request: %w", err)
}

Copilot uses AI. Check for mistakes.
@seefs001 seefs001 added the ready to merge It will eventually merge, requiring a final check. label Dec 26, 2025
@nickhelion

Copy link
Copy Markdown

真的很需要这个特性

@UnlastingR

Copy link
Copy Markdown

这个功能现在得到支持了吗?pr为何被关闭了?

@Calcium-Ion

Calcium-Ion commented Apr 8, 2026

Copy link
Copy Markdown
Member

pr因为main分支force push被自动关闭了,目前支持了缓存token读取,但是Vertex AI Embedding还不支持

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready to merge It will eventually merge, requiring a final check.

Projects

None yet

Development

Successfully merging this pull request may close these issues.