feat: enhance text protocol conversion and advanced custom routing (#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
This commit is contained in:
Calcium-Ion
2026-07-11 20:44:12 +08:00
committed by GitHub
parent 1250fb2eb5
commit c36418c863
106 changed files with 13345 additions and 4307 deletions
+62
View File
@@ -3,6 +3,7 @@ package common
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
@@ -36,6 +37,67 @@ func GetFullRequestURL(baseURL string, requestURL string, channelType int) strin
return fullRequestURL
}
func SanitizeURLForLog(rawURL string) string {
if rawURL == "" {
return rawURL
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return rawURL
}
query := parsedURL.Query()
if len(query) == 0 {
return rawURL
}
changed := false
for key := range query {
if isSensitiveURLQueryKey(key) {
query.Set(key, "***masked***")
changed = true
}
}
if !changed {
return rawURL
}
parsedURL.RawQuery = query.Encode()
return parsedURL.String()
}
func isSensitiveURLQueryKey(key string) bool {
normalized := strings.ToLower(strings.TrimSpace(key))
switch normalized {
case "key",
"api_key",
"api-key",
"apikey",
"x-api-key",
"access_token",
"refresh_token",
"id_token",
"token",
"authorization",
"auth",
"client_secret",
"secret",
"password",
"passwd",
"signature",
"sig",
"awsaccesskeyid",
"x-amz-credential",
"x-amz-security-token",
"x-amz-signature":
return true
}
return strings.Contains(normalized, "token") ||
strings.Contains(normalized, "secret") ||
strings.Contains(normalized, "signature")
}
func GetAPIVersion(c *gin.Context) string {
query := c.Request.URL.Query()
apiVersion := query.Get("api-version")
+45
View File
@@ -3,14 +3,59 @@ package common
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/QuantumNous/new-api/constant"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSanitizeURLForLogMasksSensitiveQueryValues(t *testing.T) {
rawURL := "https://example.test/v1beta/models/gemini:streamGenerateContent?alt=sse&key=sk-secret&access_token=ya29-secret&api-version=2024-02-01"
got := SanitizeURLForLog(rawURL)
assert.NotContains(t, got, "sk-secret")
assert.NotContains(t, got, "ya29-secret")
parsedURL, err := url.Parse(got)
require.NoError(t, err)
query := parsedURL.Query()
assert.Equal(t, "***masked***", query.Get("key"))
assert.Equal(t, "***masked***", query.Get("access_token"))
assert.Equal(t, "sse", query.Get("alt"))
assert.Equal(t, "2024-02-01", query.Get("api-version"))
}
func TestSanitizeURLForLogMasksAWSAndSecretLikeQueryKeys(t *testing.T) {
rawURL := "https://example.test/path?X-Amz-Credential=credential&X-Amz-Signature=signature&session_token=session&client_secret=secret&model=gpt-test"
got := SanitizeURLForLog(rawURL)
assert.NotContains(t, got, "X-Amz-Credential=credential")
assert.NotContains(t, got, "X-Amz-Signature=signature")
assert.NotContains(t, got, "session_token=session")
assert.NotContains(t, got, "client_secret=secret")
parsedURL, err := url.Parse(got)
require.NoError(t, err)
query := parsedURL.Query()
assert.Equal(t, "***masked***", query.Get("X-Amz-Credential"))
assert.Equal(t, "***masked***", query.Get("X-Amz-Signature"))
assert.Equal(t, "***masked***", query.Get("session_token"))
assert.Equal(t, "***masked***", query.Get("client_secret"))
assert.Equal(t, "gpt-test", query.Get("model"))
}
func TestSanitizeURLForLogKeepsURLWithoutSensitiveQuery(t *testing.T) {
rawURL := "https://example.test/v1/chat/completions?api-version=2024-02-01&alt=sse"
got := SanitizeURLForLog(rawURL)
assert.Equal(t, rawURL, got)
}
func TestValidateMultipartDirectNormalizesImageField(t *testing.T) {
gin.SetMode(gin.TestMode)
body := strings.NewReader(`{"model":"wan2.7-i2v","prompt":"animate","image":" https://example.com/first.png "}`)