Files
new-api/router/relay_router_test.go
T
Calcium-Ion 0ed497f066 feat(relay): hosted-tool conversion fidelity, reasoning normalization, and billing usage integrity (#7137)
* feat(relaykit): preserve hosted tools across conversions

- add protocol-neutral hosted-tool DTOs, conversion metadata, and loss policies
- bridge citations, grounding metadata, and hosted-tool stream lifecycles
- document the public conversion behavior and channel policy controls

* refactor(relaykit): normalize reasoning and thinking intent

- centralize provider-neutral reasoning intent, effort, and budget mappings
- parse model suffixes at the host entry boundary while preserving provider-owned tails
- keep adaptive Claude thinking and explicit zero-token compatibility consistent

* fix(billing): preserve authoritative usage across relay hops

- carry native BillingUsage sidecars through direct and streamed protocol bridges
- merge partial and terminal usage monotonically with safe fallback settlement
- retain cache metadata, penultimate usage, and per-call Gemini tool surcharges

* feat(relay): bridge Responses with Claude and Gemini protocols

- add direct request, response, and stream converters across supported relay formats
- expose Claude count_tokens and Chat-to-Responses compatibility endpoints
- carry conversion diagnostics through the host while retaining the curated public goldens

* fix(relay): wire relaykit conversions into host channels

- connect handlers, adaptors, and channel settings to the standalone conversion layer
- keep model mapping, pricing identity, retries, and provider-specific suffix behavior aligned
- ignore local audit artifacts and retain focused public regression coverage
2026-09-01 21:53:35 +08:00

141 lines
3.7 KiB
Go

package router
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListModelsSupportsOpenAIAndGeminiAuthentication(t *testing.T) {
setupRelayRouterTestDB(t)
user := model.User{
Username: "models-user",
Status: common.UserStatusEnabled,
Group: "default",
Quota: 100,
}
require.NoError(t, model.DB.Create(&user).Error)
require.NoError(t, model.DB.Create(&model.Token{
UserId: user.Id,
Key: "modelstestkey",
Status: common.TokenStatusEnabled,
ExpiredTime: -1,
UnlimitedQuota: true,
}).Error)
engine := gin.New()
SetRelayRouter(engine)
tests := []struct {
name string
path string
headerName string
expectedObject string
expectedField string
}{
{
name: "OpenAI bearer token",
path: "/v1/models",
headerName: "Authorization",
expectedObject: "list",
expectedField: "data",
},
{
name: "Gemini API key header",
path: "/v1/models",
headerName: "x-goog-api-key",
expectedField: "models",
},
{
name: "Gemini API key query",
path: "/v1/models?key=modelstestkey",
expectedField: "models",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, test.path, nil)
if test.headerName != "" {
value := "modelstestkey"
if test.headerName == "Authorization" {
value = "Bearer " + value
}
request.Header.Set(test.headerName, value)
}
engine.ServeHTTP(recorder, request)
require.Equal(t, http.StatusOK, recorder.Code)
var payload map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &payload))
assert.Contains(t, payload, test.expectedField)
assert.NotContains(t, payload, "error")
if test.expectedObject != "" {
assert.Equal(t, test.expectedObject, payload["object"])
}
})
}
}
func TestRelayRouterRegistersClaudeTokenCountingEndpoint(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
SetRelayRouter(engine)
for _, route := range engine.Routes() {
if route.Method == http.MethodPost && route.Path == "/v1/messages/count_tokens" {
return
}
}
t.Fatal("POST /v1/messages/count_tokens route is not registered")
}
func setupRelayRouterTestDB(t *testing.T) {
t.Helper()
gin.SetMode(gin.TestMode)
originalIsMasterNode := common.IsMasterNode
originalRedisEnabled := common.RedisEnabled
originalSQLitePath := common.SQLitePath
originalMainDatabaseType := common.MainDatabaseType()
originalLogDatabaseType := common.LogDatabaseType()
originalSQLDSN, hadSQLDSN := os.LookupEnv("SQL_DSN")
common.IsMasterNode = false
common.RedisEnabled = false
common.SQLitePath = fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
common.SetDatabaseTypes(common.DatabaseTypeSQLite, common.DatabaseTypeSQLite)
require.NoError(t, os.Setenv("SQL_DSN", "local"))
require.NoError(t, model.InitDB())
model.LOG_DB = model.DB
require.NoError(t, model.DB.AutoMigrate(&model.User{}, &model.Token{}, &model.Ability{}))
t.Cleanup(func() {
if sqlDB, err := model.DB.DB(); err == nil {
_ = sqlDB.Close()
}
common.IsMasterNode = originalIsMasterNode
common.RedisEnabled = originalRedisEnabled
common.SQLitePath = originalSQLitePath
common.SetDatabaseTypes(originalMainDatabaseType, originalLogDatabaseType)
if hadSQLDSN {
require.NoError(t, os.Setenv("SQL_DSN", originalSQLDSN))
} else {
require.NoError(t, os.Unsetenv("SQL_DSN"))
}
})
}