Skip to content

feat: enhance text protocol conversion and advanced custom routing - #5825

Merged
Calcium-Ion merged 14 commits into
mainfrom
refactor/convert
Jul 11, 2026
Merged

feat: enhance text protocol conversion and advanced custom routing#5825
Calcium-Ion merged 14 commits into
mainfrom
refactor/convert

Conversation

@Calcium-Ion

@Calcium-Ion Calcium-Ion commented Jun 30, 2026

Copy link
Copy Markdown
Member

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

本 PR 整理并统一了文本协议转换链,将 OpenAI Chat、OpenAI Responses、Claude Messages、Gemini Chat 的 request / response 转换集中到 service/relayconvert,通过统一 registry 声明转换方向、质量、请求转换、响应转换和多步转换链路。原本散落在 channel 和 service 层的纯 DTO 转换逻辑被收拢到来源协议对应的内部包中,channel 层主要保留 HTTP、SSE、鉴权和分发逻辑。

同时增强了 Advanced Custom 渠道能力:支持同一入口路径按客户端 model 分流,支持 re: 正则模型匹配,允许 /v1/responses 同时路由到 OpenAI Chat 和 Gemini Chat;前端编辑器改为按入口路径分组,增加排序、兜底提示、converter 默认上游配置自动填充和更清晰的 converter 展示。

计费侧新增协议感知的 billing_usage 容器,用于在协议转换后保留真实上游 usage 语义。最终响应仍保持客户端入口协议的 usage 格式,但会额外携带 OpenAI、Claude 或 Gemini 的原始计费 usage,便于下游按真实协议模式计费。用量日志也增加了 billing path 展示,能区分普通上游返回、billing_usage 以及 estimated billing_usage。

此外,Advanced Custom 的 /v1/models endpoint 能力不再按渠道类型硬编码,而是根据实际配置的入口路径和模型匹配规则推断,并修正了启动时 pricing 预热早于 channel cache 初始化导致初始 endpoint 不准确的问题。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

已执行过的主要验证:

go test ./service/relayconvert/...
go test ./relay/channel/advancedcustom
go test ./relay/channel/openai ./relay/channel/claude ./relay/channel/gemini
go test ./dto ./model ./controller
go test ./service/... ./relay/...

cd web/default && bun run i18n:sync
cd web/default && bunx oxlint -c .oxlintrc.json src/features/channels/components/dialogs/advanced-custom-editor-dialog.tsx src/features/channels/lib/advanced-custom.ts src/features/usage-logs/components/dialogs/details-dialog.tsx src/features/usage-logs/types.ts
cd web/default && bun run typecheck

git diff --check

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Added model-aware “Advanced Custom” routing with per-route model matching (exact and `re:` regex) and model-aware endpoint-type inference.
  * Improved billing/usage reporting across OpenAI, Claude, and Gemini, including better estimated Gemini billing when upstream metadata is missing.

* **Bug Fixes**
  * Improved model-aware channel/ability selection for Advanced Custom, leading to more accurate pricing/endpoint behavior.
  * Sanitized logged URLs to mask sensitive query values.

* **UI/Documentation**
  * Updated the Advanced Custom editor to group routes by incoming path with safer reorder controls.
  * Enhanced usage logs with clearer “Billing Path” labeling and guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa2bdd1a-8922-4cde-9170-85e8d097363f

📥 Commits

Reviewing files that changed from the base of the PR and between df7c0a1 and 27a2764.

📒 Files selected for processing (18)
  • .github/workflows/docker-build.yml
  • controller/model_list_test.go
  • main.go
  • relay/channel/api_request.go
  • relay/channel/openai/helper.go
  • relay/common/relay_utils.go
  • relay/common/relay_utils_test.go
  • service/text_quota.go
  • service/text_quota_test.go
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/features/usage-logs/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json

Walkthrough

This PR centralizes request/response conversion in service/relayconvert, adds model-aware Advanced Custom routing and pricing inference, normalizes provider billing usage, and updates relay handlers, UI, logging, translations, and workflows.

Changes

Core conversion, routing, billing, and handler updates

Layer / File(s) Summary
Billing usage and quota normalization
dto/..., service/billing_usage.go, service/text_quota.go
Adds provider-specific billing DTOs, cloning and token-detection helpers, normalizes quota settlement, and records billing-path metadata.
Conversion registries and converters
service/relayconvert/..., service/request_converter.go, service/convert.go
Adds typed request/response registries, provider conversion implementations, streaming state handling, compatibility wrappers, and tests.
Advanced Custom routing and pricing
dto/channel_settings.go, model/..., middleware/distributor.go, controller/model_list_test.go
Adds model-scoped route matching, validation, endpoint inference, cache synchronization, and pricing-backed model-list coverage.
Relay migration
relay/channel/..., relay/...
Moves request and response conversion paths to relayconvert, updates streaming flows, and attaches provider billing usage.
URL logging safety
relay/common/relay_utils.go, relay/channel/api_request.go
Masks sensitive query values before API and websocket URLs are logged.

Web UI and operations

Layer / File(s) Summary
Advanced Custom editor
web/default/src/features/channels/...
Groups routes by incoming path and adds model-rule, fallback, reorder, and validation controls.
Usage billing-path display
web/default/src/features/usage-logs/...
Adds billing-path types and renders localized billing-path labels in usage details.
Translations and workflows
web/default/src/i18n/locales/*, .github/workflows/*
Adds routing and billing translations and pins workflow actions to commit SHAs.

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related PRs

Suggested reviewers: seefs001, creamlike1024

Poem

🐰 Routes hop neatly, tokens glow,
converters guide each stream’s flow.
Billing paths now clearly shine,
while masked URLs stay safe in line.
Hop hop—the UI blooms anew!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main changes: text protocol conversion refactoring and Advanced Custom routing improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/convert

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.

@Calcium-Ion Calcium-Ion changed the title Refactor/convert Refactor text protocol converters and advanced custom routing Jun 30, 2026
@Calcium-Ion Calcium-Ion changed the title Refactor text protocol converters and advanced custom routing feat: unify text protocol converters and advanced custom routing Jun 30, 2026
@Calcium-Ion Calcium-Ion changed the title feat: unify text protocol converters and advanced custom routing feat: enhance text protocol conversion and advanced custom routing Jun 30, 2026

@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.

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/default/src/features/channels/lib/advanced-custom.ts (1)

301-323: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Recompute upstream defaults when changing a route group path.

updateGroupIncomingPath only changes incoming_path and possibly resets converter; it leaves the old upstream_path and auth. Changing a native group from OpenAI Chat to Claude/Gemini can save a route with the new incoming path but stale upstream/auth defaults.

Suggested fix
       if (!groupRouteIndexes.has(routeIndex)) return route
       const converter = route.converter || 'none'
+      const nextConverter = isAdvancedCustomIncomingPathAllowed(
+        resolvedIncomingPath,
+        converter
+      )
+        ? converter
+        : 'none'
+      const defaults = getAdvancedCustomConverterDefaults(
+        nextConverter,
+        resolvedIncomingPath
+      )
       return {
         ...route,
         incoming_path: resolvedIncomingPath,
-        converter: isAdvancedCustomIncomingPathAllowed(
-          resolvedIncomingPath,
-          converter
-        )
-          ? converter
-          : 'none',
+        converter: nextConverter,
+        upstream_path: defaults.upstream_path,
+        auth: defaults.auth,
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/channels/lib/advanced-custom.ts` around lines 301 -
323, `updateGroupIncomingPath` only updates the route’s incoming_path, so
changing a group’s path can leave stale upstream_path and auth from the previous
preset. Update the logic in `updateGroupIncomingPath` (and any helper it uses
for native route defaults) to recompute the full route defaults for the selected
group, including upstream_path, converter, and auth, based on the matching
entries in the advanced route definitions.
🟠 Major comments (29)
dto/channel_settings.go-291-296 (1)

291-296: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Persist the defaulted converter back to the route.

Line 291 copies c.Routes[i], so the advancedCustomConverterNone default on Line 296 is only used during validation. Later callers that receive the matched route still see an empty Converter.

Proposed fix
-		route := c.Routes[i]
-		route.IncomingPath = strings.TrimSpace(route.IncomingPath)
-		upstreamPath := strings.TrimSpace(route.UpstreamPath)
-		route.Converter = strings.TrimSpace(route.Converter)
+		route := &c.Routes[i]
+		route.IncomingPath = strings.TrimSpace(route.IncomingPath)
+		route.UpstreamPath = strings.TrimSpace(route.UpstreamPath)
+		upstreamPath := route.UpstreamPath
+		route.Converter = strings.TrimSpace(route.Converter)
 		if route.Converter == "" {
 			route.Converter = advancedCustomConverterNone
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dto/channel_settings.go` around lines 291 - 296, The route validation logic
in the channel settings flow is only defaulting Converter on a local copy of
c.Routes[i], so the empty value is never persisted back to the stored route.
Update the route-handling code in the validation/matching path that trims
IncomingPath, UpstreamPath, and Converter so the default
advancedCustomConverterNone is written back into c.Routes[i] (or otherwise
returned in the matched route object) before continuing, ensuring later callers
see the defaulted Converter value.
dto/channel_settings.go-136-139 (1)

136-139: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prefer exact path matches before Gemini generate→stream fallback.

Line 137 calls matchAdvancedCustomIncomingPath, where a configured :generateContent route also matches :streamGenerateContent. If a generic generate route appears before a stream-specific route, streaming requests are captured by the generate route before the exact stream route is inspected.

Proposed direction
 func (c *AdvancedCustomConfig) MatchPathForModel(requestPath string, model string) (AdvancedCustomRoute, bool) {
 	if c == nil {
 		return AdvancedCustomRoute{}, false
 	}
 	model = strings.TrimSpace(model)
+
+	for _, route := range c.Routes {
+		if matchAdvancedCustomIncomingPathTemplate(strings.TrimSpace(route.IncomingPath), requestPath) &&
+			matchAdvancedCustomRouteModel(route.Models, model) {
+			return route, true
+		}
+	}
+
 	for _, route := range c.Routes {
 		if matchAdvancedCustomIncomingPath(strings.TrimSpace(route.IncomingPath), requestPath) &&
 			matchAdvancedCustomRouteModel(route.Models, model) {
 			return route, true
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dto/channel_settings.go` around lines 136 - 139, The route lookup in the
channel settings matching flow is too permissive because
`matchAdvancedCustomIncomingPath` lets a `:generateContent` route match
`:streamGenerateContent`, so a generic route can win before a stream-specific
one is checked. Update the matching logic in the route selection path around
`Routes` iteration to prefer exact incoming-path matches first, then allow the
Gemini generate→stream fallback only if no exact stream route matches, keeping
the existing model check via `matchAdvancedCustomRouteModel`.
service/billing_usage.go-154-193 (1)

154-193: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Populate normalized Gemini InputTokens.

usageFromGeminiBillingUsage() computes the provider-normalized input total but leaves InputTokens at zero. Downstream, PostTextConsumeQuota() only writes other["input_tokens_total"] when billingUsage.InputTokens > 0, so Gemini billing-usage paths never emit that field after this refactor.

Suggested fix
 func usageFromGeminiBillingUsage(billingUsage *dto.BillingUsage) *dto.Usage {
 	metadata := *billingUsage.GeminiUsageMetadata
 	promptTokens := metadata.PromptTokenCount + metadata.ToolUsePromptTokenCount
 	usage := &dto.Usage{
-		PromptTokens:     promptTokens,
+		PromptTokens:     promptTokens,
+		InputTokens:      promptTokens,
 		CompletionTokens: metadata.CandidatesTokenCount + metadata.ThoughtsTokenCount,
 		TotalTokens:      metadata.TotalTokenCount,
 		UsageSemantic:    dto.BillingUsageSemanticGemini,
 		UsageSource:      dto.BillingUsageSourceGeminiChat,
 		BillingUsage:     dto.CloneBillingUsage(billingUsage),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/billing_usage.go` around lines 154 - 193, The Gemini normalization in
usageFromGeminiBillingUsage leaves Usage.InputTokens unset even though it
already computes the normalized prompt total; set InputTokens on the returned
dto.Usage from the same provider-normalized input token value used for prompt
usage. Keep the change localized to usageFromGeminiBillingUsage and ensure
PostTextConsumeQuota can see a non-zero BillingUsage.InputTokens so it emits
input_tokens_total for Gemini paths.
relay/channel/gemini/relay-gemini.go-40-62 (1)

40-62: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Count non-text Gemini parts in the fallback usage estimator.

Line 45 only includes part.Text. When Gemini omits UsageMetadata, tool-call / function-response outputs fall through to ResponseText2Usage(...) as an empty string, so these responses can be billed with 0 completion tokens even though the model produced output.

Suggested fix
 func geminiResponseUsageText(response *dto.GeminiChatResponse) string {
 	if response == nil {
 		return ""
 	}
 	var text strings.Builder
 	for _, candidate := range response.Candidates {
 		for _, part := range candidate.Content.Parts {
-			if part.Text != "" {
-				text.WriteString(part.Text)
-			}
+			switch {
+			case part.Text != "":
+				text.WriteString(part.Text)
+			case part.FunctionCall != nil:
+				if payload, err := common.Marshal(part.FunctionCall); err == nil {
+					text.Write(payload)
+				}
+			case part.FunctionResponse != nil:
+				if payload, err := common.Marshal(part.FunctionResponse); err == nil {
+					text.Write(payload)
+				}
+			}
 		}
 	}
 	return text.String()
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/gemini/relay-gemini.go` around lines 40 - 62, The fallback
usage estimator in geminiResponseUsageText only concatenates part.Text, so
non-text Gemini parts are dropped and can produce zero completion tokens when
UsageMetadata is missing. Update geminiResponseUsageText and/or
buildUsageFromGeminiResponse to account for tool-call and function-response
parts from GeminiChatResponse.Candidates.Content.Parts, converting those outputs
into a non-empty fallback string before calling ResponseText2Usage so usage is
estimated from all model output, not just text.
service/relayconvert/internal/oai_chat/to_claude_messages_req.go-33-44 (1)

33-44: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject malformed request shapes instead of panicking.

Line 41 and Line 219 both use unchecked type assertions on client JSON. A payload like tools[].function.parameters.type: {} or stop: [1] will panic inside conversion instead of returning a normal validation error.

Proposed fix
 	for _, tool := range textRequest.Tools {
 		if params, ok := tool.Function.Parameters.(map[string]any); ok {
 			claudeTool := dto.Tool{
 				Name:        tool.Function.Name,
 				Description: tool.Function.Description,
 			}
 			claudeTool.InputSchema = make(map[string]interface{})
-			if params["type"] != nil {
-				claudeTool.InputSchema["type"] = params["type"].(string)
+			if rawType, exists := params["type"]; exists {
+				typeName, ok := rawType.(string)
+				if !ok {
+					return nil, fmt.Errorf("tool %q has non-string schema type", tool.Function.Name)
+				}
+				claudeTool.InputSchema["type"] = typeName
 			}
 	if textRequest.Stop != nil {
 		switch stop := textRequest.Stop.(type) {
 		case string:
 			claudeRequest.StopSequences = []string{stop}
 		case []interface{}:
 			stopSequences := make([]string, 0)
 			for _, item := range stop {
-				stopSequences = append(stopSequences, item.(string))
+				value, ok := item.(string)
+				if !ok {
+					return nil, fmt.Errorf("stop sequences must be strings")
+				}
+				stopSequences = append(stopSequences, value)
 			}
 			claudeRequest.StopSequences = stopSequences
 		}
 	}

Also applies to: 212-220

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_claude_messages_req.go` around
lines 33 - 44, The conversion in toClaudeMessagesReq should not panic on
malformed client JSON; replace the unchecked type assertions in the tools loop
and stop handling with safe type checks and return a validation error when
fields like parameters.type or stop have the wrong shape. Update the conversion
paths around the tool parsing in toClaudeMessagesReq and the stop-field handling
near the later conversion block so they validate types before assigning into
dto.Tool/InputSchema or Claude request fields.
service/relayconvert/internal/oai_chat/to_gemini_chat_req.go-61-68 (1)

61-68: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Only skip ApplyThinkingConfig when a thinking override was actually provided.

Line 63 sets adaptorWithExtraBody = true for any extra_body.google object on non--nothinking models. If the caller only sends image_config, Lines 151-153 still skip sharedgemini.ApplyThinkingConfig, so the normal default/model thinking behavior disappears for an unrelated option.

Proposed fix
 		if googleBody, ok := extraBody["google"].(map[string]interface{}); ok {
 			if !strings.HasSuffix(upstreamModelName, "-nothinking") {
-				adaptorWithExtraBody = true
 				if _, hasErrorParam := googleBody["thinkingConfig"]; hasErrorParam {
 					return nil, errors.New("extra_body.google.thinkingConfig is not supported, use extra_body.google.thinking_config instead")
 				}
@@
 					if hasThinkingConfig {
+						adaptorWithExtraBody = true
 						if geminiRequest.GenerationConfig.ThinkingConfig == nil {
 							geminiRequest.GenerationConfig.ThinkingConfig = &tempThinkingConfig
 						} else {

Also applies to: 104-116, 151-153

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_gemini_chat_req.go` around lines 61
- 68, The thinking-config guard in to_gemini_chat_req.go is too broad:
`adaptorWithExtraBody` is being set for any `extra_body.google` payload, which
causes `sharedgemini.ApplyThinkingConfig` to be skipped even when only unrelated
fields like `image_config` are present. Update the logic around `googleBody`,
`thinkingConfig`, and the `ApplyThinkingConfig` call so the skip only happens
when an actual thinking override is supplied (for example, `thinking_config` or
the legacy `thinkingConfig` error path), and keep the default/model thinking
behavior for other extra body options.
service/relayconvert/internal/oai_chat/to_claude_messages_req.go-229-235 (1)

229-235: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the normalized role when the incoming role is empty.

Line 231 writes "user" back into textRequest.Messages[i], but fmtMessage.Role on Line 234 still reads from the stale message range copy. Empty-role messages therefore stay empty in formatMessages, which can produce invalid Claude roles later.

Proposed fix
 	for i, message := range textRequest.Messages {
-		if message.Role == "" {
-			textRequest.Messages[i].Role = "user"
-		}
+		role := message.Role
+		if role == "" {
+			role = "user"
+			textRequest.Messages[i].Role = role
+		}
 		fmtMessage := dto.Message{
-			Role:    message.Role,
+			Role:    role,
 			Content: message.Content,
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_claude_messages_req.go` around
lines 229 - 235, The normalized role is being written back to
textRequest.Messages[i] in formatMessages, but dto.Message.Role still uses the
stale loop variable message, so empty roles remain empty. Update formatMessages
to populate fmtMessage.Role from the normalized value in textRequest.Messages[i]
(or otherwise re-read the updated slice element) so messages with missing roles
become "user" before building Claude messages.
service/relayconvert/internal/oai_chat/to_gemini_chat_req.go-311-358 (1)

311-358: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the text after the last inline markdown image.

Once the loop starts consuming ![...](data:...) segments, it keeps slicing text, but the remaining tail is never appended after the final image. Inputs like before ![img](data:...) after drop the trailing " after" completely.

Proposed fix
 				for {
 					startIdx := strings.Index(text, "![")
 					if startIdx == -1 {
 						break
@@
 					parts = append(parts, imgPart)
 					text = text[closeIdx+1:]
 				}
+				if hasMarkdownImage && text != "" {
+					parts = append(parts, dto.GeminiPart{
+						Text: text,
+					})
+				}
 				if !hasMarkdownImage {
 					parts = append(parts, dto.GeminiPart{
 						Text: part.Text,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_gemini_chat_req.go` around lines
311 - 358, The inline markdown image parsing in toGeminiChatReq is discarding
the trailing text after the last `![...](data:...)` segment. Update the loop
that slices `text` to preserve any remaining tail after the final image by
appending the leftover text to `parts` once no more image matches are found,
using the existing `text`, `parts`, and `hasMarkdownImage` handling in
`toGeminiChatReq`.
service/relayconvert/internal/shared/claude/cache.go-3-8 (1)

3-8: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Normalize the overflow path as well.

When tokens5m + tokens1h > totalTokens, Lines 4-8 still return a split whose sum exceeds totalTokens. That makes the “normalized” result inconsistent and can overcount cache-creation tokens downstream.

Proposed fix
 func NormalizeCacheCreationSplit(totalTokens int, tokens5m int, tokens1h int) (int, int) {
+	if totalTokens <= 0 {
+		return 0, 0
+	}
+	if tokens1h < 0 {
+		tokens1h = 0
+	}
+	if tokens1h > totalTokens {
+		return 0, totalTokens
+	}
 	remainder := totalTokens - tokens5m - tokens1h
 	if remainder < 0 {
-		remainder = 0
+		return totalTokens - tokens1h, tokens1h
 	}
 	return tokens5m + remainder, tokens1h
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/shared/claude/cache.go` around lines 3 - 8,
Normalize the overflow path in NormalizeCacheCreationSplit: when tokens5m +
tokens1h exceeds totalTokens, the returned split must be clamped so the sum
never goes above totalTokens. Update the logic in NormalizeCacheCreationSplit to
reduce the 5m and/or 1h values proportionally or by a clear priority rule,
instead of only zeroing the remainder, so the normalized result is always
bounded by totalTokens.
dto/gemini.go-486-493 (1)

486-493: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve billing-only Gemini metadata here.

GetUsageMetadata() currently returns nil unless HasUsageMetadata is set or token counters are non-zero. That drops responses built in memory with only UsageMetadata.BillingUsage, and both the Gemini relay path and service/relayconvert/response_registry.go rely on this accessor before deciding whether to fall back to estimated usage. The result is that preserved upstream billing data can be discarded on the way into quota/billing normalization.

Suggested fix
func (r *GeminiChatResponse) GetUsageMetadata() *GeminiUsageMetadata {
	if r == nil {
		return nil
	}
-	if r.HasUsageMetadata || HasGeminiUsageMetadataTokens(&r.UsageMetadata) {
+	if r.HasUsageMetadata || HasGeminiUsageMetadataTokens(&r.UsageMetadata) || r.UsageMetadata.BillingUsage != nil {
		return &r.UsageMetadata
	}
	return nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dto/gemini.go` around lines 486 - 493, GetUsageMetadata in GeminiChatResponse
is filtering out billing-only metadata by only returning UsageMetadata when
HasUsageMetadata is set or token counters are present. Update this accessor to
preserve any populated UsageMetadata, including BillingUsage, so the Gemini
relay path and service/relayconvert/response_registry.go can keep upstream
billing data instead of falling back to estimated usage. Keep the nil receiver
guard, but remove the token-only gating and make the check rely on the metadata
field itself.
service/relayconvert/internal/oai_responses/to_claude_messages_req.go-196-217 (1)

196-217: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject non-image/non-PDF inputs here.

Lines 196-216 route input_file, input_audio, and input_video through Claude's image/document blocks only. Anything that is not a PDF becomes image, so MP3/MP4/CSV uploads are serialized with an invalid Claude block type and will fail upstream.

Suggested fix
-			if strings.HasPrefix(mimeType, "application/pdf") {
-				claudePart.Type = "document"
-			} else {
-				claudePart.Type = "image"
-			}
+			normalized := strings.ToLower(mimeType)
+			switch {
+			case strings.HasPrefix(normalized, "image/"):
+				claudePart.Type = "image"
+			case normalized == "application/pdf":
+				claudePart.Type = "document"
+			default:
+				return nil, fmt.Errorf("mime type %q is not supported for Claude input", mimeType)
+			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_responses/to_claude_messages_req.go` around
lines 196 - 217, The media conversion in toClaudeMessagesReq is incorrectly
treating every non-PDF `input_file`, `input_audio`, and `input_video` as an
`image`, which sends invalid Claude block types upstream. Update the switch
handling around `ContentPartToFileSource` and `relaymedia.ResolveBase64Data` to
only allow supported image/PDF inputs for `dto.ClaudeMediaMessage`, and
explicitly reject or skip non-image/non-PDF MIME types (such as audio, video, or
generic files) instead of mapping them to `image`.
service/relayconvert/internal/oai_responses/to_gemini_chat_req.go-178-186 (1)

178-186: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Propagate invalid json_schema errors.

Once line 178 switches Gemini into JSON response mode, an unmarshal failure on lines 183-185 should reject the request. Returning nil here silently drops the schema and forwards a less constrained request than the client asked for.

Suggested fix
 	var jsonSchema dto.FormatJsonSchema
 	if err := common.Unmarshal(responseFormat.JsonSchema, &jsonSchema); err != nil {
-		return nil
+		return fmt.Errorf("invalid text.format.json_schema: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_responses/to_gemini_chat_req.go` around
lines 178 - 186, The JSON response setup in toGeminiChatReq should not silently
ignore an invalid json_schema after setting ResponseMimeType in the Gemini
request. In the logic around responseFormat.JsonSchema and common.Unmarshal,
change the unmarshal failure path to return an error instead of nil so the
request is rejected; keep the fix localized to the schema handling in
toGeminiChatReq and preserve the existing valid-schema flow.
service/relayconvert/internal/gemini_chat/to_oai_chat_req.go-33-34 (1)

33-34: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep tool-call IDs stable across the whole converted request.

toolCalls is reset for each Gemini content block, but FunctionResponse reuses len(toolCalls) to build ToolCallId. That means a later tool response usually points at call_0 instead of the earlier assistant tool call, so the OpenAI message sequence loses the tool/result pairing.

Also applies to: 61-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/gemini_chat/to_oai_chat_req.go` around lines 33
- 34, The tool-call ID generation in toOAIChatReq is unstable because toolCalls
is recreated for each Gemini content block, so FunctionResponse ends up reusing
len(toolCalls) and producing call_0 for later tool results. Update the
conversion logic in toOAIChatReq (including the FunctionCall and
FunctionResponse handling paths) to use a request-wide counter or preserved
mapping so each tool call gets a stable, unique ToolCallId across all content
blocks and the assistant/tool message pairing remains intact.
service/relayconvert/internal/jsonutil/stringify.go-9-14 (1)

9-14: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't fall back to Go formatting from a JSON helper.

fmt.Sprintf("%v", v) can return strings like map[a:b], which are not valid JSON. This helper is used to serialize function arguments and tool responses on the Gemini→OpenAI path, so a marshal failure here turns into a malformed upstream payload instead of a conversion error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/jsonutil/stringify.go` around lines 9 - 14, The
ToJSONString helper currently falls back to fmt.Sprintf("%v", v) on marshal
failure, which can emit non-JSON output and leak malformed payloads into the
Gemini→OpenAI conversion path. Update ToJSONString in stringif y.go to avoid Go
formatting entirely: when common.Marshal fails, return a JSON-safe result or
propagate the failure through the surrounding conversion flow so callers can
surface an error instead of using invalid serialized data. Keep the fix
localized to ToJSONString and any immediate callers that depend on it for
function arguments or tool responses.
service/relayconvert/internal/claude_messages/to_oai_chat_req.go-149-160 (1)

149-160: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve assistant text when the Claude message also contains tool_use.

This branch keeps toolCalls but discards mediaMessages whenever both exist. Claude assistant turns can interleave text and tool calls, and OpenAI assistant messages support sending both, so the current conversion silently drops prompt-relevant text.

Proposed fix
 			if len(toolCalls) > 0 {
 				openAIMessage.SetToolCalls(toolCalls)
 			}
-			if len(mediaMessages) > 0 && len(toolCalls) == 0 {
+			if len(mediaMessages) > 0 {
 				openAIMessage.SetMediaContent(mediaMessages)
 			}

Also applies to: 199-204

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/claude_messages/to_oai_chat_req.go` around
lines 149 - 160, The conversion logic in to_oai_chat_req is dropping assistant
text whenever tool_use is present because the toolCalls branch wins over
mediaMessages. Update the Claude-to-OpenAI mapping so mixed assistant turns keep
both text and tool calls: preserve the accumulated mediaMessages alongside
toolCalls instead of discarding them, and ensure the same handling is applied in
the companion branch around the later media/tool_use conversion path. Use the
existing dto.ToolCallRequest, dto.MediaContent, and message-building flow to
locate and adjust the merge logic.
service/relayconvert/internal/gemini_chat/to_oai_chat_req.go-51-60 (1)

51-60: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Only map Gemini file_data to image_url for image MIME types.

This converter tags every FileData part as OpenAI image_url, but the shared Gemini helpers in this PR explicitly allow non-image MIME types such as PDF, audio, and video. Those requests will be turned into invalid OpenAI chat content instead of being rejected or converted via a supported path.

Proposed fix
 			} else if part.FileData != nil {
+				if !strings.HasPrefix(part.FileData.MimeType, "image/") {
+					return nil, fmt.Errorf("unsupported Gemini file mime type for OpenAI chat conversion: %s", part.FileData.MimeType)
+				}
 				mediaContent := dto.MediaContent{
 					Type: "image_url",
 					ImageUrl: &dto.MessageImageUrl{
 						Url:      part.FileData.FileUri,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/gemini_chat/to_oai_chat_req.go` around lines 51
- 60, The Gemini-to-OpenAI converter in to_oai_chat_req should not map every
FileData part to dto.MediaContent{Type: "image_url"}; update the FileData
handling branch to inspect part.FileData.MimeType and only create an image_url
payload for image MIME types. For non-image MIME types (for example PDF, audio,
video), either reject the part with a clear error or route it through a
supported conversion path, so the logic in the file_data branch of the converter
does not produce invalid OpenAI chat content.
service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go-72-74 (1)

72-74: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't relabel image-only prompt tokens as text tokens.

This fallback fires whenever text/audio are zero, even if ImageTokens is already populated. For image-only prompts it duplicates the full prompt total into TextTokens, which corrupts the modality breakdown used downstream for usage/billing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go` around lines
72 - 74, The fallback in the usage normalization logic is too broad and relabels
image-only prompts as text tokens. Update the check in the gemini chat response
conversion path around the usage details handling so it only copies
`PromptTokens` into `TextTokens` when there are truly no modality-specific
tokens set, and explicitly exclude cases where `ImageTokens` is already
populated. Use the `usage.PromptTokensDetails` fields in `to_oai_chat_resp` to
preserve the correct modality breakdown for downstream billing.
service/relayconvert/internal/oai_chat/to_gemini_chat_resp.go-57-83 (1)

57-83: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve OpenAI reasoning content as Gemini Thought parts.

Both converters ignore ReasoningContent, and the stream precheck also drops reasoning-only chunks as “empty”. That breaks round-tripping with service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go, which already maps Gemini thought parts into OpenAI reasoning content.

Also applies to: 97-109, 165-195

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_gemini_chat_resp.go` around lines
57 - 83, The OpenAI-to-Gemini conversion in toGeminiChatResp currently drops
ReasoningContent, so update the response mapping to preserve it as Gemini
Thought parts alongside text and function calls. Add the same handling in the
stream precheck/empty-chunk path and any other affected converter blocks
referenced in this diff so reasoning-only messages are not discarded. Use the
existing structures in choice.Message, dto.GeminiPart, and the related converter
logic to keep round-tripping compatible with to_oai_chat_resp.go.
service/relayconvert/internal/oai_chat/to_claude_messages_resp.go-281-290 (1)

281-290: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Process the current delta before deferring close on finish_reason.

This early return happens before chosenChoice.Delta is converted. If an upstream sends final content/tool data in the same chunk as finish_reason and postpones usage to a later chunk, that last delta is silently discarded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_claude_messages_resp.go` around
lines 281 - 290, The finish_reason handling in toClaudeMessagesResp is returning
too early in the doneChunk branch, which drops any final Delta content/tool data
in the same chunk. In to_claude_messages_resp.go, update the logic around
chosenChoice.FinishReason so the current chosenChoice.Delta is converted and
appended before deferring closure for usage, and only return early after
processing that delta when openAIResponse.Usage is still nil. Keep the fix
localized to the doneChunk handling in toClaudeMessagesResp.
service/relayconvert/internal/oai_chat/to_claude_messages_resp.go-134-171 (1)

134-171: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The first streaming tool-call chunk drops every tool after index 0.

The SendResponseCount == 1 branch selects a single toolCall and never iterates the rest of openAIResponse.Choices[0].Delta.ToolCalls. If the opener chunk carries multiple parallel tool calls, only the first Claude tool_use block is emitted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_claude_messages_resp.go` around
lines 134 - 171, The first streaming tool-call chunk only emits the first tool
call and ignores any additional parallel calls. Update the SendResponseCount ==
1 handling in to_claude_messages_resp.go to iterate over
openAIResponse.Choices[0].Delta.ToolCalls (and the fallback GetFirstToolCall
path if needed) and append a Claude content_block_start for each tool call,
preserving each tool’s ID, Function.Name, and Arguments instead of hardcoding a
single toolCall at index 0.
service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go-86-87 (1)

86-87: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Track tool-call finish state per candidate, not per response.

isToolCall is declared outside the candidate loop and never reset. After the first tool-call candidate, every later choice is forced to tool_calls even when that candidate only contains plain text.

Also applies to: 158-178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go` around lines
86 - 87, The tool-call finish state is being carried across candidates because
isToolCall is initialized before the candidate loop and reused in toOAIChatResp,
which causes later candidates to inherit tool_calls incorrectly. Update the
candidate processing logic in toOAIChatResp so the tool-call check is computed
and reset per candidate (and likewise for the related response-building path
mentioned in the later block), using each candidate’s own finish/parts data
instead of a shared flag.
service/relayconvert/internal/oai_chat/to_claude_messages_resp.go-42-48 (1)

42-48: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't double-count cached prompt tokens in synthesized Claude usage.

PromptTokens is already the OpenAI-side total, including cached prompt details. Copying it into InputTokens and also populating CacheReadInputTokens / CacheCreationInputTokens inflates Anthropic usage whenever cache fields are present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_claude_messages_resp.go` around
lines 42 - 48, The Claude usage synthesis in toClaudeMessagesResp is
double-counting cached prompt tokens by setting ClaudeUsage.InputTokens from
oaiUsage.PromptTokens while also filling CacheReadInputTokens and
CacheCreationInputTokens. Update the mapping in this function so InputTokens
reflects only the non-cached prompt portion when PromptTokensDetails is present,
and keep the cache fields as the explicit cached amounts; use the existing
oaiUsage and ClaudeUsage symbols to adjust the conversion logic without
inflating billingUsage.
service/relayconvert/internal/claude_messages/to_oai_chat_resp.go-124-142 (1)

124-142: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Accumulate Claude text/thinking blocks instead of overwriting them.

This loop replaces responseText and thinkingContent on each matching block, so a Claude message with multiple text or thinking parts only returns the last one after conversion.

Suggested fix
-	for _, message := range claudeResponse.Content {
+	var textBuilder strings.Builder
+	var thinkingBuilder strings.Builder
+	for _, message := range claudeResponse.Content {
 		switch message.Type {
 		case "tool_use":
 			args, _ := common.Marshal(message.Input)
 			tools = append(tools, dto.ToolCallResponse{
@@
 		case "thinking":
 			if message.Thinking != nil {
-				thinkingContent = *message.Thinking
+				thinkingBuilder.WriteString(*message.Thinking)
 			}
 		case "text":
-			responseText = message.GetText()
+			textBuilder.WriteString(message.GetText())
 		}
 	}
+	responseText = textBuilder.String()
+	thinkingContent = thinkingBuilder.String()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/claude_messages/to_oai_chat_resp.go` around
lines 124 - 142, The loop in to_oai_chat_resp.go overwrites responseText and
thinkingContent for each Claude content block, so only the last text/thinking
segment survives. Update the claudeResponse.Content handling in the conversion
logic to accumulate all "text" blocks into responseText and all "thinking"
blocks into thinkingContent instead of replacing prior values, while keeping the
existing tool_use handling unchanged.
service/relayconvert/internal/oai_responses/to_oai_chat_resp.go-131-140 (1)

131-140: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep absent responses usage nil.

When resp.Usage is nil, this helper still returns &dto.Usage{}. That makes a responses payload with no accounting data look like a zero-token usage record, and registry callers will treat it as present. Please return nil for nil input here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_responses/to_oai_chat_resp.go` around lines
131 - 140, The UsageFromResponsesUsage helper currently turns a nil input into
an empty dto.Usage, which makes absent responses usage look present. Update
UsageFromResponsesUsage to return nil immediately when src is nil, and keep the
existing field cloning/population logic only for non-nil inputs so registry
callers can distinguish missing usage from a real zero-valued record.
service/relayconvert/internal/oai_responses/to_oai_chat_resp.go-201-207 (1)

201-207: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't fall back to reasoning text as assistant content.

This fallback appends every non-empty c.Text, including reasoning/summary_text items. A chat→responses→chat round-trip with empty assistant content but non-empty reasoning will come back with the reasoning duplicated into both Message.Content and Message.ReasoningContent.

Possible fix
-	for _, out := range resp.Output {
-		for _, c := range out.Content {
-			if c.Text != "" {
-				sb.WriteString(c.Text)
-			}
-		}
-	}
+	for _, out := range resp.Output {
+		if out.Type != responsesOutputTypeMessage {
+			continue
+		}
+		if out.Role != "" && out.Role != "assistant" {
+			continue
+		}
+		for _, c := range out.Content {
+			if c.Type == "output_text" && c.Text != "" {
+				sb.WriteString(c.Text)
+			}
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_responses/to_oai_chat_resp.go` around lines
201 - 207, The chat-content reconstruction in toOAIChatResp is incorrectly
appending every non-empty Output.Content.Text, which can pull in
reasoning/summary text as assistant content. Update the loop in the chat
response builder to only collect actual assistant message text and explicitly
skip reasoning-related content types so Message.Content is not duplicated from
Message.ReasoningContent on round-trips.
service/relayconvert/internal/oai_chat/to_oai_responses_resp.go-111-153 (1)

111-153: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Treat empty chat usage as absent.

dto.OpenAITextResponse.Usage is a value, so an omitted upstream usage arrives here as an all-zero struct. Returning a non-nil *dto.Usage for that case turns “usage missing” into “0 tokens”, and downstream registry code will treat it as a real accounting snapshot. On multi-hop conversions that can wipe preserved BillingUsage/token totals with zeros.

Possible fix
func UsageFromChatUsage(src *dto.Usage) *dto.Usage {
-	usage := &dto.Usage{}
-	if src == nil {
-		return usage
-	}
+	if src == nil {
+		return nil
+	}
+	if !dto.HasOpenAIUsageTokens(src) &&
+		src.BillingUsage == nil &&
+		src.Cost == 0 &&
+		src.PromptTokensDetails == (dto.InputTokenDetails{}) &&
+		src.CompletionTokenDetails == (dto.OutputTokenDetails{}) &&
+		src.ClaudeCacheCreation5mTokens == 0 &&
+		src.ClaudeCacheCreation1hTokens == 0 {
+		return nil
+	}
+	usage := &dto.Usage{}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/internal/oai_chat/to_oai_responses_resp.go` around lines
111 - 153, Treat an all-zero chat usage as absent in UsageFromChatUsage: when
src is non-nil but every usage field is zero, return nil instead of constructing
an empty dto.Usage. Update the conversion logic in UsageFromChatUsage to detect
this “empty upstream usage” case before populating BillingUsage, token counts,
or details, so downstream code in the relay conversion path preserves existing
accounting data instead of overwriting it with zeros.
service/relayconvert/request_registry.go-325-344 (1)

325-344: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid running Responses→Gemini preprocessing twice.

For the direct OpenAI Responses→Gemini registry path, prepareRequestForStep prepares the request before the step, then convertOpenAIResponsesRequestToGeminiChat prepares it again. Keep this normalization in one layer to avoid duplicated/non-idempotent transformations.

🐛 Proposed fix
 func convertOpenAIResponsesRequestToGeminiChat(c *gin.Context, info *relaycommon.RelayInfo, request any) (any, error) {
 	responsesRequest, err := oairesponses.OpenAIResponsesRequestFromAny(request)
 	if err != nil {
 		return nil, err
 	}
-
-	prepared, err := oairesponses.PrepareOpenAIResponsesRequest(*responsesRequest)
-	if err != nil {
-		return nil, err
-	}
-	return oairesponses.OpenAIResponsesRequestToGeminiChat(c, &prepared, info)
+	return oairesponses.OpenAIResponsesRequestToGeminiChat(c, responsesRequest, info)
 }

Also applies to: 475-485

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/request_registry.go` around lines 325 - 344, Avoid
double-normalizing OpenAI Responses requests on the direct Responses-to-Gemini
path: `prepareRequestForStep` is already calling
`oairesponses.PrepareOpenAIResponsesRequest`, and
`convertOpenAIResponsesRequestToGeminiChat` should not prepare the same payload
again. Update one of these layers so the normalization happens only once,
keeping the registry path in `request_registry.go` and the Gemini converter
consistent for both the direct flow and the shared helper used elsewhere.
service/relayconvert/response_registry.go-468-503 (1)

468-503: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Suppress nil stream conversions before returning a result.

StreamResponseOpenAI2Gemini can return nil for empty leading stream chunks, but executeStatelessStreamResponseSpec still wraps that as a successful ResponseResult with Value == nil. That can make callers write an invalid/empty stream payload instead of skipping the chunk.

🐛 Proposed fix
 		var err error
 		current, usage, err = step.ConvertStream(c, info, current)
 		if err != nil {
 			return nil, err
 		}
+		if len(streamValuesFromAny(current)) == 0 {
+			return nil, nil
+		}
 		resultSteps = append(resultSteps, ResponseStep{
 			Converter: step.ID,

Also applies to: 908-914

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/response_registry.go` around lines 468 - 503,
`executeStatelessStreamResponseSpec` currently returns a successful
`ResponseResult` even when a stream converter like `StreamResponseOpenAI2Gemini`
produces a nil `current` for empty leading chunks. Update the loop in
`executeStatelessStreamResponseSpec` to detect a nil stream conversion result
and suppress it by returning no result for that chunk instead of wrapping it in
`ResponseResult`; keep the existing step validation and error handling intact,
and ensure callers can skip empty stream payloads rather than emitting an
invalid nil value.
service/relayconvert/request_compat.go-47-48 (1)

47-48: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply Responses→Gemini preprocessing in the compatibility wrapper.

Line 48 bypasses the preparation that the registry applies before OpenAI Responses→Gemini conversion, so direct callers of this public wrapper can get different request normalization than ConvertRequest.

🐛 Proposed fix
 import (
+	"errors"
+
 	"github.com/QuantumNous/new-api/dto"
 	relaycommon "github.com/QuantumNous/new-api/relay/common"
@@
 func OpenAIResponsesRequestToGeminiChat(c *gin.Context, req *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo) (*dto.GeminiChatRequest, error) {
-	return oairesponses.OpenAIResponsesRequestToGeminiChat(c, req, info)
+	if req == nil {
+		return nil, errors.New("OpenAI responses request is nil")
+	}
+	prepared, err := oairesponses.PrepareOpenAIResponsesRequest(*req)
+	if err != nil {
+		return nil, err
+	}
+	return oairesponses.OpenAIResponsesRequestToGeminiChat(c, &prepared, info)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/relayconvert/request_compat.go` around lines 47 - 48, The
OpenAIResponsesRequestToGeminiChat compatibility wrapper bypasses the same
preprocessing that ConvertRequest applies before Responses-to-Gemini conversion,
so direct callers can get inconsistent normalization. Update
OpenAIResponsesRequestToGeminiChat in request_compat.go to run the same
Responses→Gemini preprocessing path used by the registry (via the relevant
oairesponses/registry helper) before delegating to the Gemini conversion, while
keeping the existing function signature and delegation structure intact.
🟡 Minor comments (3)
web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx-1030-1035 (1)

1030-1035: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hide decorative billing-path icons from assistive tech.

These icons are redundant with the adjacent text label, so add aria-hidden='true'. As per coding guidelines, decorative icons should be hidden from assistive tech.

-                    <Monitor className='size-3 text-blue-500' />
+                    <Monitor className='size-3 text-blue-500' aria-hidden='true' />
...
-                    <Cloud className='size-3 text-emerald-500' />
+                    <Cloud className='size-3 text-emerald-500' aria-hidden='true' />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx`
around lines 1030 - 1035, Hide the decorative billing-path icons in the usage
log details dialog from assistive tech by adding aria-hidden='true' to the
Monitor and Cloud icon renders inside the details dialog component. Update the
conditional icon block in details-dialog.tsx so the icons remain visually shown
next to the label but are excluded from accessibility APIs.

Source: Coding guidelines

web/default/src/features/channels/components/dialogs/advanced-custom-editor-dialog.tsx-762-773 (1)

762-773: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Localize incoming-path option labels.

Line 769 renders option.label directly in the new route group selector. Wrap it with t() like the other user-facing labels in this component. As per coding guidelines, React UI text must support i18n with useTranslation() and t().

-                      <span>{option.label}</span>
+                      <span>{t(option.label)}</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/channels/components/dialogs/advanced-custom-editor-dialog.tsx`
around lines 762 - 773, The incoming-path option label in the advanced custom
editor dialog is still rendered as raw UI text. Update the SelectItem rendering
in advanced-custom-editor-dialog.tsx so the option label is passed through the
existing i18n flow using useTranslation() and t(), matching the other localized
labels in this component; keep option.value as-is and only localize the
displayed label text.

Source: Coding guidelines

web/default/src/features/channels/lib/advanced-custom.ts-724-731 (1)

724-731: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add regex syntax validation to match backend validation.

The frontend only rejects empty regex patterns but the backend also validates regex syntax with regexp.Compile(). Patterns like re:[ currently pass frontend validation but fail at runtime. Add a try-catch block to test the regex pattern before save, using the same validation as the backend:

Backend validation (Go)
// ./dto/channel_settings.go lines 383-387
if pattern == "" {
  return fmt.Errorf("regex is empty")
}
if _, err := regexp.Compile(pattern); err != nil {
  return fmt.Errorf("regex is invalid")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/channels/lib/advanced-custom.ts` around lines 724 -
731, The regex validation in advanced-custom.ts only checks for empty patterns,
so it should also validate regex syntax to match backend behavior. In the model
validation flow where getAdvancedCustomModelRuleKind and
getAdvancedCustomRegexModelPattern are used, add a try-catch around compiling
the extracted pattern and return a validation message when compilation fails,
alongside the existing empty-pattern check. Keep the check in the same
save/validation path so invalid regexes like malformed patterns are rejected
before persistence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49aa5a81-8b09-464e-a37f-547640f89e26

📥 Commits

Reviewing files that changed from the base of the PR and between e514db2 and e45397d.

📒 Files selected for processing (98)
  • controller/model_list_test.go
  • dto/billing_usage.go
  • dto/billing_usage_test.go
  • dto/channel_settings.go
  • dto/channel_settings_test.go
  • dto/claude.go
  • dto/gemini.go
  • dto/gemini_response_test.go
  • dto/openai_response.go
  • main.go
  • middleware/distributor.go
  • model/ability.go
  • model/channel_cache.go
  • model/pricing.go
  • model/pricing_endpoint_test.go
  • relay/channel/advancedcustom/adaptor.go
  • relay/channel/advancedcustom/adaptor_test.go
  • relay/channel/ali/adaptor.go
  • relay/channel/api_request.go
  • relay/channel/aws/adaptor.go
  • relay/channel/claude/adaptor.go
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go
  • relay/channel/gemini/adaptor.go
  • relay/channel/gemini/relay-gemini-native.go
  • relay/channel/gemini/relay-gemini.go
  • relay/channel/gemini/relay_gemini_usage_test.go
  • relay/channel/gemini/relay_responses.go
  • relay/channel/openai/adaptor.go
  • relay/channel/openai/chat_via_responses.go
  • relay/channel/openai/helper.go
  • relay/channel/openai/relay-openai.go
  • relay/channel/openai/responses_via_chat.go
  • relay/channel/vertex/adaptor.go
  • relay/chat_completions_via_responses.go
  • relay/claude_handler.go
  • relay/common/relay_utils.go
  • relay/common/relay_utils_test.go
  • relay/gemini_handler.go
  • service/billing_usage.go
  • service/convert.go
  • service/convert_test.go
  • service/relayconvert/internal/claude_messages/to_oai_chat_req.go
  • service/relayconvert/internal/claude_messages/to_oai_chat_resp.go
  • service/relayconvert/internal/gemini_chat/to_oai_chat_req.go
  • service/relayconvert/internal/gemini_chat/to_oai_chat_resp.go
  • service/relayconvert/internal/jsonutil/stringify.go
  • service/relayconvert/internal/matcher/regex.go
  • service/relayconvert/internal/media/media.go
  • service/relayconvert/internal/meta/relay_info.go
  • service/relayconvert/internal/oai_chat/to_claude_messages_req.go
  • service/relayconvert/internal/oai_chat/to_claude_messages_resp.go
  • service/relayconvert/internal/oai_chat/to_claude_messages_resp_test.go
  • service/relayconvert/internal/oai_chat/to_gemini_chat_req.go
  • service/relayconvert/internal/oai_chat/to_gemini_chat_resp.go
  • service/relayconvert/internal/oai_chat/to_gemini_chat_resp_test.go
  • service/relayconvert/internal/oai_chat/to_oai_responses_policy.go
  • service/relayconvert/internal/oai_chat/to_oai_responses_req.go
  • service/relayconvert/internal/oai_chat/to_oai_responses_req_test.go
  • service/relayconvert/internal/oai_chat/to_oai_responses_resp.go
  • service/relayconvert/internal/oai_chat/to_oai_responses_resp_test.go
  • service/relayconvert/internal/oai_chat/to_oai_responses_stream_resp.go
  • service/relayconvert/internal/oai_responses/req_helpers.go
  • service/relayconvert/internal/oai_responses/to_claude_messages_req.go
  • service/relayconvert/internal/oai_responses/to_gemini_chat_req.go
  • service/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go
  • service/relayconvert/internal/oai_responses/to_oai_chat_req.go
  • service/relayconvert/internal/oai_responses/to_oai_chat_req_test.go
  • service/relayconvert/internal/oai_responses/to_oai_chat_resp.go
  • service/relayconvert/internal/oai_responses/to_oai_chat_resp_test.go
  • service/relayconvert/internal/oai_responses/to_oai_chat_stream_resp.go
  • service/relayconvert/internal/shared/claude/cache.go
  • service/relayconvert/internal/shared/claude/tool_choice.go
  • service/relayconvert/internal/shared/gemini/request.go
  • service/relayconvert/internal/shared/gemini/schema.go
  • service/relayconvert/media.go
  • service/relayconvert/request_compat.go
  • service/relayconvert/request_registry.go
  • service/relayconvert/request_registry_test.go
  • service/relayconvert/response_compat.go
  • service/relayconvert/response_registry.go
  • service/relayconvert/response_registry_test.go
  • service/relayconvert/text_converter_registry.go
  • service/relayconvert/text_converter_registry_test.go
  • service/request_converter.go
  • service/text_quota.go
  • service/text_quota_test.go
  • web/default/src/features/channels/components/dialogs/advanced-custom-editor-dialog.tsx
  • web/default/src/features/channels/lib/advanced-custom.ts
  • web/default/src/features/channels/types.ts
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/features/usage-logs/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json

# Conflicts:
#	relay/common/relay_utils_test.go
#	web/default/src/features/channels/components/dialogs/advanced-custom-editor-dialog.tsx
#	web/default/src/features/channels/lib/advanced-custom.ts
- estimate Gemini completion tokens locally when billable usageMetadata is
  prompt-only but output content was received (e.g. client aborts the stream
  before the final chunk), and rebuild the attached billing_usage as estimated
  so settlement does not bill zero output tokens
- guard NewClaudeMessagesBillingUsage against all-zero ClaudeUsage, matching
  the OpenAI/Gemini constructors, so a zero billing_usage cannot override a
  non-zero top-level usage during settlement
- cache compiled advanced-custom route model regexes; they run on the request
  hot path and were recompiled per request
- move the effectiveBillingUsage remap to PostTextConsumeQuota only, and
  document that calculateTextQuotaSummary expects remapped usage
- document the updatePricingLock -> channelSyncLock lock ordering that
  InitChannelCache/CacheUpdateChannel rely on, and the aux-struct pitfall in
  GeminiChatResponse.UnmarshalJSON
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@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.

🧹 Nitpick comments (4)
.github/workflows/electron-build.yml (1)

136-136: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Optional: replace softprops/action-gh-release with gh release CLI.

zizmor flags this as functionality already provided by the runner via the GitHub CLI, removing a third-party action dependency. Low priority given the current pinned SHA is already reasonably secure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/electron-build.yml at line 136, Replace the release step
that uses softprops/action-gh-release in the Electron build workflow with the
GitHub CLI release flow instead. Update the release job/step to use gh release
so the existing workflow logic still creates/uploads the release artifacts
without depending on the third-party action, and keep the surrounding release
configuration in the same workflow block.

Source: Linters/SAST tools

.github/workflows/release.yml (2)

67-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: softprops/action-gh-release duplicates runner-provided gh functionality.

zizmor notes this functionality is already available via the pre-installed gh CLI, so the third-party action could be replaced with a gh release create/gh release upload script step. Current usage works fine; this is a stylistic simplification only.

Also applies to: 122-122, 179-179

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 67, The release workflow still relies
on softprops/action-gh-release in multiple places, but this can be simplified by
using the runner’s built-in gh CLI instead. Update the release job steps that
currently use the action to equivalent gh release create and gh release upload
commands, keeping the existing release behavior and tags/assets handling intact.

Source: Linters/SAST tools


29-29: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Disable default caching on setup-bun/setup-go in the release jobs.

Static analysis flags cache-poisoning risk: these actions enable dependency caching by default. For a release pipeline that produces publicly distributed binaries, a cache poisoned by an earlier (possibly untrusted) workflow run sharing the same cache key could get restored here.

🔒️ Proposed fix: disable caching for release builds
       - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
         with:
           go-version: '>=1.25.1'
+          cache: false

Apply similarly to the oven-sh/setup-bun steps if it exposes a cache-disable input.

Also applies to: 51-51, 88-88, 111-111, 146-146, 168-168

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml at line 29, The release workflow’s setup steps
are leaving dependency caching enabled by default, which should be disabled for
all release jobs. Update the `setup-bun` and `setup-go` usages in the release
workflow to explicitly turn off caching via their cache-disable inputs or
equivalent flags, and apply the same change consistently to every matching setup
step in the workflow so the release pipeline never restores shared caches.

Source: Linters/SAST tools

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

55-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate magic number 1400 for the image-token completion estimate heuristic.

Consider extracting this into a shared named constant (e.g. geminiImageCompletionTokenEstimate) to avoid drift between the two call sites.

Also applies to: 183-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/gemini/relay-gemini.go` at line 55, The Gemini image completion
estimate currently uses the same magic number at multiple call sites, which
risks divergence over time. Extract the shared heuristic value from the code
paths in the Gemini relay logic (including the completion token assignment in
the relevant Gemini helper/methods) into a named constant such as
geminiImageCompletionTokenEstimate, and replace both literal uses with that
constant so the estimate stays consistent in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/electron-build.yml:
- Line 136: Replace the release step that uses softprops/action-gh-release in
the Electron build workflow with the GitHub CLI release flow instead. Update the
release job/step to use gh release so the existing workflow logic still
creates/uploads the release artifacts without depending on the third-party
action, and keep the surrounding release configuration in the same workflow
block.

In @.github/workflows/release.yml:
- Line 67: The release workflow still relies on softprops/action-gh-release in
multiple places, but this can be simplified by using the runner’s built-in gh
CLI instead. Update the release job steps that currently use the action to
equivalent gh release create and gh release upload commands, keeping the
existing release behavior and tags/assets handling intact.
- Line 29: The release workflow’s setup steps are leaving dependency caching
enabled by default, which should be disabled for all release jobs. Update the
`setup-bun` and `setup-go` usages in the release workflow to explicitly turn off
caching via their cache-disable inputs or equivalent flags, and apply the same
change consistently to every matching setup step in the workflow so the release
pipeline never restores shared caches.

In `@relay/channel/gemini/relay-gemini.go`:
- Line 55: The Gemini image completion estimate currently uses the same magic
number at multiple call sites, which risks divergence over time. Extract the
shared heuristic value from the code paths in the Gemini relay logic (including
the completion token assignment in the relevant Gemini helper/methods) into a
named constant such as geminiImageCompletionTokenEstimate, and replace both
literal uses with that constant so the estimate stays consistent in one place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e7bcd7d3-71c5-4331-ba76-fd69c9f4f16e

📥 Commits

Reviewing files that changed from the base of the PR and between a599a6a and df7c0a1.

📒 Files selected for processing (15)
  • .github/workflows/docker-build.yml
  • .github/workflows/docker-image-branch.yml
  • .github/workflows/electron-build.yml
  • .github/workflows/release.yml
  • dto/billing_usage.go
  • dto/billing_usage_test.go
  • dto/channel_settings.go
  • dto/channel_settings_test.go
  • dto/gemini.go
  • model/channel_cache.go
  • model/pricing.go
  • relay/channel/gemini/relay-gemini.go
  • relay/channel/gemini/relay_gemini_usage_test.go
  • service/text_quota.go
  • service/text_quota_test.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • dto/billing_usage_test.go
  • .github/workflows/docker-build.yml
  • model/pricing.go
  • dto/gemini.go
  • dto/billing_usage.go
  • service/text_quota_test.go
  • model/channel_cache.go
  • dto/channel_settings_test.go
  • dto/channel_settings.go

# Conflicts:
#	web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
#	web/default/src/i18n/locales/en.json
#	web/default/src/i18n/locales/fr.json
#	web/default/src/i18n/locales/ja.json
#	web/default/src/i18n/locales/ru.json
#	web/default/src/i18n/locales/vi.json
#	web/default/src/i18n/locales/zh.json
@Calcium-Ion
Calcium-Ion merged commit c36418c into main Jul 11, 2026
1 check was pending
agogo233 added a commit to agogo233/new-api that referenced this pull request Jul 13, 2026
* upstream/main: (56 commits)
  fix: list only channel models in unset price models tab
  fix: harden unset price models tab batch copy, feedback, and memo equality
  feat: add unset price models tab to model pricing settings (QuantumNous#6124)
  feat: enhance model search functionality with status and sync filters
  fix: bound uncached remainder by prompt-max(cached,write) and forward compact prompt_cache_key
  feat: bill OpenAI cache_write_tokens at cache-creation price with zero clamp
  feat: enhance text protocol conversion and advanced custom routing (QuantumNous#5825)
  fix: adjust margin for StatusBadge component in logs columns
  feat: enhance stale instance handling and update theme colors
  feat: update theme colors
  feat(timing): add timing metrics display for stream logs and enhance localization
  revert: restore StatusBadge horizontal padding
  revert: undo t0ng7u UI design-system refactor
  ✨ feat(web): polish themed data views and add task log details
  feat(image): enhance image stream handling with client disconnect logic and billing adjustments
  fix(billing): improve quota handling and error reporting for pre-consume operations
  fix(billing): reject saturated pre-consume quota
  🐛 fix: Fontsource asset resolution across workspace layouts
  fix: sync codex field (QuantumNous#6018)
  chore(deps): bump golang.org/x/crypto from 0.51.0 to 0.52.0 (QuantumNous#6096)
  ...
@coderabbitai coderabbitai Bot mentioned this pull request Jul 13, 2026
11 tasks
lansine pushed a commit to evin-pubb/new-api-fork that referenced this pull request Jul 14, 2026
…uantumNous#5825)

* refactor: consolidate relay protocol converters

* refactor relayconvert text converters

* feat: refine relay converters and advanced custom routing

* refactor: enhance logging and add thought signature handling for Gemini requests

* refactor: enhance channel cache and pricing endpoint handling for advanced custom models

* feat: preserve billing usage semantics

* feat: add protocol-aware billing usage

* Delete useless files

* chore: update action versions in workflow files

* chore: update Docker action versions in workflow files

* fix: harden billing usage settlement and hot-path route matching

- estimate Gemini completion tokens locally when billable usageMetadata is
  prompt-only but output content was received (e.g. client aborts the stream
  before the final chunk), and rebuild the attached billing_usage as estimated
  so settlement does not bill zero output tokens
- guard NewClaudeMessagesBillingUsage against all-zero ClaudeUsage, matching
  the OpenAI/Gemini constructors, so a zero billing_usage cannot override a
  non-zero top-level usage during settlement
- cache compiled advanced-custom route model regexes; they run on the request
  hot path and were recompiled per request
- move the effectiveBillingUsage remap to PostTextConsumeQuota only, and
  document that calculateTextQuotaSummary expects remapped usage
- document the updatePricingLock -> channelSyncLock lock ordering that
  InitChannelCache/CacheUpdateChannel rely on, and the aux-struct pitfall in
  GeminiChatResponse.UnmarshalJSON
JacksonsY added a commit to JacksonsY/new-api that referenced this pull request Jul 15, 2026
PR QuantumNous#6177(隐藏未定价模型)只更新了 controller/model_list_test.go,漏了上游
model/pricing_endpoint_test.go(来自 PR QuantumNous#5825)——后者用未定价模型验证 endpoint 类型推断,
依赖「未定价模型出现在 GetPricing」。PR 后非自用模式下这些模型被排除,7 个测试挂。
在共享 helper resetPricingEndpointTestTables 临时开启自用模式(带还原),让未定价测试模型
仍以默认档暴露、复刻 PR 前可见性;endpoint 推断断言不受影响。合并上游若其补了同一测试需
留意此处。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Calcium-Ion
Calcium-Ion deleted the refactor/convert branch July 21, 2026 06:46
zhaodechao2008 pushed a commit to zhaodechao2008/new-api that referenced this pull request Jul 27, 2026
…uantumNous#5825)

* refactor: consolidate relay protocol converters

* refactor relayconvert text converters

* feat: refine relay converters and advanced custom routing

* refactor: enhance logging and add thought signature handling for Gemini requests

* refactor: enhance channel cache and pricing endpoint handling for advanced custom models

* feat: preserve billing usage semantics

* feat: add protocol-aware billing usage

* Delete useless files

* chore: update action versions in workflow files

* chore: update Docker action versions in workflow files

* fix: harden billing usage settlement and hot-path route matching

- estimate Gemini completion tokens locally when billable usageMetadata is
  prompt-only but output content was received (e.g. client aborts the stream
  before the final chunk), and rebuild the attached billing_usage as estimated
  so settlement does not bill zero output tokens
- guard NewClaudeMessagesBillingUsage against all-zero ClaudeUsage, matching
  the OpenAI/Gemini constructors, so a zero billing_usage cannot override a
  non-zero top-level usage during settlement
- cache compiled advanced-custom route model regexes; they run on the request
  hot path and were recompiled per request
- move the effectiveBillingUsage remap to PostTextConsumeQuota only, and
  document that calculateTextQuotaSummary expects remapped usage
- document the updatePricingLock -> channelSyncLock lock ordering that
  InitChannelCache/CacheUpdateChannel rely on, and the aux-struct pitfall in
  GeminiChatResponse.UnmarshalJSON
330079598 pushed a commit to 330079598/new-api that referenced this pull request Aug 19, 2026
…uantumNous#5825)

* refactor: consolidate relay protocol converters

* refactor relayconvert text converters

* feat: refine relay converters and advanced custom routing

* refactor: enhance logging and add thought signature handling for Gemini requests

* refactor: enhance channel cache and pricing endpoint handling for advanced custom models

* feat: preserve billing usage semantics

* feat: add protocol-aware billing usage

* Delete useless files

* chore: update action versions in workflow files

* chore: update Docker action versions in workflow files

* fix: harden billing usage settlement and hot-path route matching

- estimate Gemini completion tokens locally when billable usageMetadata is
  prompt-only but output content was received (e.g. client aborts the stream
  before the final chunk), and rebuild the attached billing_usage as estimated
  so settlement does not bill zero output tokens
- guard NewClaudeMessagesBillingUsage against all-zero ClaudeUsage, matching
  the OpenAI/Gemini constructors, so a zero billing_usage cannot override a
  non-zero top-level usage during settlement
- cache compiled advanced-custom route model regexes; they run on the request
  hot path and were recompiled per request
- move the effectiveBillingUsage remap to PostTextConsumeQuota only, and
  document that calculateTextQuotaSummary expects remapped usage
- document the updatePricingLock -> channelSyncLock lock ordering that
  InitChannelCache/CacheUpdateChannel rely on, and the aux-struct pitfall in
  GeminiChatResponse.UnmarshalJSON
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant