feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user