feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+63 -24
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"slices"
"strings"
"time"
@@ -75,23 +76,58 @@ func geminiResponseUsageText(response *dto.GeminiChatResponse) string {
if part.Text != "" {
text.WriteString(part.Text)
}
// Function calls are most of an agent turn's output.
if part.FunctionCall != nil {
text.WriteString(part.FunctionCall.FunctionName)
args, _ := common.Marshal(part.FunctionCall.Arguments)
text.Write(args)
}
}
}
return text.String()
}
func markGeminiGoogleSearchCall(c *gin.Context, response *dto.GeminiChatResponse) {
if c == nil || response == nil {
return
}
// geminiGroundingCounter accumulates Google Search grounding across one
// upstream response (every stream frame and candidate). Gemini 3 bills each
// unique non-empty search query ("we ignore the empty web search queries when
// counting unique queries", ai.google.dev/gemini-api/docs/google-search);
// Gemini 2.5 and older bill once per grounded prompt, counted here when the
// response carries any search query. Both quantities are recorded and the
// tool price table decides which one costs money for a model. Image-search
// queries count like web-search queries; Google documents no separate rule.
// Google documents no response field holding the billed count, so the unique
// query set is the best available evidence, not a guaranteed match.
type geminiGroundingCounter struct {
queries map[string]struct{}
}
func (g *geminiGroundingCounter) observe(response *dto.GeminiChatResponse) {
for _, candidate := range response.Candidates {
if candidate.GroundingMetadata != nil && len(candidate.GroundingMetadata.WebSearchQueries) > 0 {
c.Set("gemini_google_search_call", true)
return
metadata := candidate.GroundingMetadata
if metadata == nil {
continue
}
for _, query := range slices.Concat(metadata.WebSearchQueries, metadata.ImageSearchQueries) {
query = strings.TrimSpace(query)
if query == "" {
continue
}
if g.queries == nil {
g.queries = make(map[string]struct{})
}
g.queries[query] = struct{}{}
}
}
}
func (g *geminiGroundingCounter) commit(info *relaycommon.RelayInfo) {
if len(g.queries) == 0 {
return
}
info.SetBillableToolCount(dto.BuildInToolGoogleSearch, len(g.queries))
info.SetBillableToolCount(relaycommon.GoogleSearchGroundedPromptTool, 1)
}
func countGeminiBillableFunctionCalls(info *relaycommon.RelayInfo, response *dto.GeminiChatResponse) {
if info == nil || response == nil {
return
@@ -171,6 +207,7 @@ func geminiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
var hasBillableUsageMetadata bool
var streamErr error
var accumulatedUsageMetadata *dto.GeminiUsageMetadata
var grounding geminiGroundingCounter
responseText := strings.Builder{}
helper.StreamScannerHandler(c, resp, info, func(data string, sr *helper.StreamResult) {
@@ -197,7 +234,9 @@ func geminiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
info.StreamStatus.MarkCompleted()
}
markGeminiGoogleSearchCall(c, &geminiResponse)
// Commit on every frame so a client abort keeps the queries seen so far.
grounding.observe(&geminiResponse)
grounding.commit(info)
countGeminiBillableFunctionCalls(info, &geminiResponse)
// 统计图片数量
@@ -206,11 +245,9 @@ func geminiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
if part.InlineData != nil && part.InlineData.MimeType != "" {
imageCount++
}
if part.Text != "" {
responseText.WriteString(part.Text)
}
}
}
responseText.WriteString(geminiResponseUsageText(&geminiResponse))
// 更新使用量统计
if metadata := geminiResponse.GetUsageMetadata(); dto.HasGeminiUsageMetadataTokens(metadata) {
@@ -251,7 +288,7 @@ func geminiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
return usage, types.NewOpenAIError(streamErr, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
}
if info.StreamStatus != nil && !info.StreamStatus.IsNormalEnd() {
logger.LogWarn(c, fmt.Sprintf("Gemini stream ended unexpectedly: %s", info.StreamStatus.Summary()))
logger.LogWarn(c, common.LogText("Gemini stream ended unexpectedly: %s", info.StreamStatus.Summary()))
}
return usage, nil
@@ -367,7 +404,7 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
}
handleErr := handleFinalStream(c, info, response)
if handleErr != nil {
common.SysLog("send final response failed: " + handleErr.Error())
common.SysLog(common.LogText("send final response failed: %s", handleErr.Error()))
}
return usage, nil
}
@@ -385,7 +422,9 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
}
info.ObserveResponseModel(gjson.GetBytes(responseBody, "modelVersion").Str)
markGeminiGoogleSearchCall(c, &geminiResponse)
var grounding geminiGroundingCounter
grounding.observe(&geminiResponse)
grounding.commit(info)
countGeminiBillableFunctionCalls(info, &geminiResponse)
if len(geminiResponse.Candidates) == 0 {
usage := buildUsageFromGeminiResponse(c, info, &geminiResponse)
@@ -414,11 +453,11 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
case types.RelayFormatClaude:
c.JSON(newAPIError.StatusCode, gin.H{
"type": "error",
"error": newAPIError.ToClaudeError(),
"error": newAPIError.ToClaudeError(common.UpstreamAddresses(c)...),
})
default:
c.JSON(newAPIError.StatusCode, gin.H{
"error": newAPIError.ToOpenAIError(),
"error": newAPIError.ToOpenAIError(common.UpstreamAddresses(c)...),
})
}
return &usage, nil
@@ -558,10 +597,10 @@ type GeminiModelsResponse struct {
NextPageToken string `json:"nextPageToken"`
}
func FetchGeminiModels(baseURL, apiKey, proxyURL string) ([]string, error) {
client, err := service.GetHttpClientWithProxy(proxyURL)
func FetchGeminiModels(baseURL, apiKey string, setting dto.ChannelSettings) ([]string, error) {
client, err := service.GetHttpClientWithProxySettings(setting.Proxy, setting)
if err != nil {
return nil, fmt.Errorf("创建HTTP客户端失败: %v", err)
return nil, fmt.Errorf("failed to create HTTP client: %v", err)
}
allModels := make([]string, 0)
@@ -578,7 +617,7 @@ func FetchGeminiModels(baseURL, apiKey, proxyURL string) ([]string, error) {
request, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
cancel()
return nil, fmt.Errorf("创建请求失败: %v", err)
return nil, fmt.Errorf("failed to create request: %v", err)
}
request.Header.Set("x-goog-api-key", apiKey)
@@ -586,26 +625,26 @@ func FetchGeminiModels(baseURL, apiKey, proxyURL string) ([]string, error) {
response, err := client.Do(request)
if err != nil {
cancel()
return nil, fmt.Errorf("请求失败: %v", err)
return nil, fmt.Errorf("request failed: %v", err)
}
if response.StatusCode != http.StatusOK {
body, _ := io.ReadAll(response.Body)
response.Body.Close()
cancel()
return nil, fmt.Errorf("服务器返回错误 %d: %s", response.StatusCode, string(body))
return nil, fmt.Errorf("server returned error %d: %s", response.StatusCode, string(body))
}
body, err := io.ReadAll(response.Body)
response.Body.Close()
cancel()
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
return nil, fmt.Errorf("failed to read response: %v", err)
}
var modelsResponse GeminiModelsResponse
if err = common.Unmarshal(body, &modelsResponse); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
return nil, fmt.Errorf("failed to parse response: %v", err)
}
for _, model := range modelsResponse.Models {