Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type channelMonitorV2RepoStub struct {
|
||||
config serviceChannelMonitorV2ConfigAlias
|
||||
users *ChannelMonitorV2List[ChannelMonitorV2UserRow]
|
||||
matrix *ChannelMonitorV2Matrix
|
||||
errors *ChannelMonitorV2List[ChannelMonitorV2ErrorRow]
|
||||
snap *ChannelMonitorV2Snapshot
|
||||
group ChannelMonitorV2GroupBy
|
||||
admin bool
|
||||
}
|
||||
|
||||
// Alias keeps composite literals readable without introducing another package.
|
||||
type serviceChannelMonitorV2ConfigAlias = ChannelMonitorV2Config
|
||||
|
||||
func (s *channelMonitorV2RepoStub) GetConfig(context.Context) (*ChannelMonitorV2Config, error) {
|
||||
cfg := ChannelMonitorV2Config(s.config)
|
||||
return &cfg, nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) UpdateConfig(context.Context, ChannelMonitorV2Config, int) (*ChannelMonitorV2Config, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) GetDimensions(context.Context, ChannelMonitorV2Filter, ChannelMonitorV2Config) (*ChannelMonitorV2Dimensions, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) GetSnapshot(_ context.Context, _ ChannelMonitorV2Filter, _ ChannelMonitorV2Config, admin bool) (*ChannelMonitorV2Snapshot, error) {
|
||||
s.admin = admin
|
||||
if s.snap == nil {
|
||||
return nil, nil
|
||||
}
|
||||
// Shallow copy so public redaction does not mutate fixture.
|
||||
cfg := s.snap.Config
|
||||
cfg.Platforms = append([]ChannelMonitorV2PlatformConfig(nil), s.snap.Config.Platforms...)
|
||||
for i := range cfg.Platforms {
|
||||
cfg.Platforms[i].Models = append([]string(nil), s.snap.Config.Platforms[i].Models...)
|
||||
}
|
||||
cfg.GroupIDs = append([]int64(nil), s.snap.Config.GroupIDs...)
|
||||
cfg.IgnoredErrorCategories = append([]string(nil), s.snap.Config.IgnoredErrorCategories...)
|
||||
out := *s.snap
|
||||
out.Config = cfg
|
||||
return &out, nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) GetModels(context.Context, ChannelMonitorV2Filter, ChannelMonitorV2Config, bool) (*ChannelMonitorV2List[ChannelMonitorV2ModelRow], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) GetMatrix(_ context.Context, _ ChannelMonitorV2Filter, _ ChannelMonitorV2Config, groupBy ChannelMonitorV2GroupBy, admin bool) (*ChannelMonitorV2Matrix, error) {
|
||||
s.group, s.admin = groupBy, admin
|
||||
return s.matrix, nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) GetErrors(_ context.Context, _ ChannelMonitorV2Filter, _ ChannelMonitorV2Config, admin bool) (*ChannelMonitorV2List[ChannelMonitorV2ErrorRow], error) {
|
||||
s.admin = admin
|
||||
if s.errors != nil {
|
||||
// Return a shallow copy so service redaction does not mutate the fixture.
|
||||
out := &ChannelMonitorV2List[ChannelMonitorV2ErrorRow]{
|
||||
Coverage: s.errors.Coverage,
|
||||
Items: append([]ChannelMonitorV2ErrorRow(nil), s.errors.Items...),
|
||||
}
|
||||
for i := range out.Items {
|
||||
if len(out.Items[i].Details) > 0 {
|
||||
out.Items[i].Details = append([]ChannelMonitorV2ErrorDetail(nil), out.Items[i].Details...)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) GetUsers(context.Context, ChannelMonitorV2Filter, ChannelMonitorV2Config, bool) (*ChannelMonitorV2List[ChannelMonitorV2UserRow], error) {
|
||||
return s.users, nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) RecomputeRange(context.Context, time.Time, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
func (s *channelMonitorV2RepoStub) GetAggregationWatermark(context.Context) (*ChannelMonitorV2AggregationWatermark, error) {
|
||||
return &ChannelMonitorV2AggregationWatermark{}, nil
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2BootstrapProgress(t *testing.T) {
|
||||
now := time.Date(2026, 8, 7, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
t.Run("no data shows active zero progress", func(t *testing.T) {
|
||||
b := ChannelMonitorV2BootstrapProgress(now, time.Time{}, false)
|
||||
require.NotNil(t, b)
|
||||
require.True(t, b.Active)
|
||||
require.Equal(t, 0, b.ProgressPercent)
|
||||
require.Equal(t, now.Add(-ChannelMonitorV2BootstrapProductWindow), b.TargetStart)
|
||||
})
|
||||
|
||||
t.Run("2h seed is small percent of 30d", func(t *testing.T) {
|
||||
covered := now.Add(-2 * time.Hour)
|
||||
b := ChannelMonitorV2BootstrapProgress(now, covered, true)
|
||||
require.NotNil(t, b)
|
||||
require.True(t, b.Active)
|
||||
require.Greater(t, b.ProgressPercent, 0)
|
||||
require.Less(t, b.ProgressPercent, 5)
|
||||
})
|
||||
|
||||
t.Run("30d covered hides bootstrap", func(t *testing.T) {
|
||||
covered := now.Add(-ChannelMonitorV2BootstrapProductWindow)
|
||||
b := ChannelMonitorV2BootstrapProgress(now, covered, true)
|
||||
require.Nil(t, b)
|
||||
})
|
||||
|
||||
t.Run("beyond 30d also hides", func(t *testing.T) {
|
||||
covered := now.Add(-60 * 24 * time.Hour)
|
||||
b := ChannelMonitorV2BootstrapProgress(now, covered, true)
|
||||
require.Nil(t, b)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2ParseFilterDefaultsAndBuckets(t *testing.T) {
|
||||
svc := &ChannelMonitorV2Service{now: func() time.Time { return time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) }}
|
||||
|
||||
filter, err := svc.ParseFilter("", []string{"openai", "openai", ""}, []string{"gpt-5"}, []int64{2, 1, 2, 0})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "90m", filter.Range)
|
||||
require.Equal(t, 5*time.Minute, filter.Bucket)
|
||||
require.Equal(t, []string{"openai"}, filter.Platforms)
|
||||
require.Equal(t, []int64{1, 2}, filter.GroupIDs)
|
||||
require.Equal(t, 90*time.Minute, filter.End.Sub(filter.Start))
|
||||
|
||||
filter, err = svc.ParseFilter("30d", nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 24*time.Hour, filter.Bucket)
|
||||
_, err = svc.ParseFilter("15d", nil, nil, nil)
|
||||
require.ErrorIs(t, err, ErrChannelMonitorV2InvalidRange)
|
||||
}
|
||||
|
||||
func TestParseChannelMonitorV2GroupBy(t *testing.T) {
|
||||
groupBy, err := ParseChannelMonitorV2GroupBy("")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ChannelMonitorV2GroupByPlatformGroup, groupBy)
|
||||
for _, value := range []ChannelMonitorV2GroupBy{ChannelMonitorV2GroupByPlatform, ChannelMonitorV2GroupByPlatformGroup, ChannelMonitorV2GroupByPlatformModel, ChannelMonitorV2GroupByPlatformGroupModel} {
|
||||
parsed, parseErr := ParseChannelMonitorV2GroupBy(string(value))
|
||||
require.NoError(t, parseErr)
|
||||
require.Equal(t, value, parsed)
|
||||
}
|
||||
_, err = ParseChannelMonitorV2GroupBy("group")
|
||||
require.ErrorIs(t, err, ErrChannelMonitorV2InvalidGroupBy)
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2MatrixForwardsGroupingAndAdminScope(t *testing.T) {
|
||||
want := &ChannelMonitorV2Matrix{GroupBy: ChannelMonitorV2GroupByPlatformModel}
|
||||
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, matrix: want}
|
||||
result, err := NewChannelMonitorV2Service(repo).Matrix(context.Background(), ChannelMonitorV2Filter{}, ChannelMonitorV2GroupByPlatformModel, true)
|
||||
require.NoError(t, err)
|
||||
require.Same(t, want, result)
|
||||
require.Equal(t, ChannelMonitorV2GroupByPlatformModel, repo.group)
|
||||
require.True(t, repo.admin)
|
||||
|
||||
_, err = NewChannelMonitorV2Service(repo).Matrix(context.Background(), ChannelMonitorV2Filter{}, "bad", false)
|
||||
require.ErrorIs(t, err, ErrChannelMonitorV2InvalidGroupBy)
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2ConfigValidation(t *testing.T) {
|
||||
cfg := ChannelMonitorV2Config{
|
||||
Platforms: []ChannelMonitorV2PlatformConfig{
|
||||
{Platform: " OpenAI ", Enabled: true, Models: []string{"gpt-5", "gpt-5", ""}},
|
||||
{Platform: "anthropic", Enabled: true},
|
||||
},
|
||||
GroupIDs: []int64{3, 1, 3},
|
||||
}
|
||||
require.NoError(t, normalizeChannelMonitorV2Config(&cfg))
|
||||
require.Equal(t, 300, cfg.RefreshIntervalSeconds)
|
||||
require.Equal(t, "anthropic", cfg.Platforms[0].Platform)
|
||||
require.Equal(t, []int64{1, 3}, cfg.GroupIDs)
|
||||
|
||||
cfg.RefreshIntervalSeconds = 120
|
||||
require.ErrorIs(t, normalizeChannelMonitorV2Config(&cfg), ErrChannelMonitorV2InvalidConfig)
|
||||
|
||||
cfg.RefreshIntervalSeconds = 60
|
||||
cfg.GroupIDs = []int64{0}
|
||||
require.ErrorIs(t, normalizeChannelMonitorV2Config(&cfg), ErrChannelMonitorV2InvalidConfig)
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2ErrorTaxonomyPriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in ChannelMonitorV2ErrorInput
|
||||
want string
|
||||
}{
|
||||
{"cyber", ChannelMonitorV2ErrorInput{ErrorType: "cyber_policy", StatusCode: 200}, "content_policy"},
|
||||
{"auth before forbidden", ChannelMonitorV2ErrorInput{StatusCode: 403, Message: "invalid API key"}, "authentication"},
|
||||
{"context", ChannelMonitorV2ErrorInput{Message: "maximum prompt length exceeded"}, "context_limit"},
|
||||
{"unsupported", ChannelMonitorV2ErrorInput{Message: "not supported by any configured account"}, "model_unsupported"},
|
||||
{"pool", ChannelMonitorV2ErrorInput{Message: "No available accounts"}, "account_pool_unavailable"},
|
||||
{"timeout", ChannelMonitorV2ErrorInput{Message: "error code: 524"}, "timeout"},
|
||||
{"upstream", ChannelMonitorV2ErrorInput{ErrorOwner: "provider", StatusCode: 502, UpstreamStatusCode: 502}, "upstream_5xx"},
|
||||
{"other", ChannelMonitorV2ErrorInput{StatusCode: 400, Message: "unknown"}, "other"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.want, ClassifyChannelMonitorV2Error(tt.in)) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2HealthBlendsErrorTTFTAndCache(t *testing.T) {
|
||||
// error 3%/5% → 40; ttft p50 2s → 100; cache 50% → 50
|
||||
// overall = (0.6*40 + 0.2*100 + 0.2*50) / 1.0 = 54 → warning
|
||||
p50 := int64(2000)
|
||||
p95 := int64(9000)
|
||||
thresholds := ChannelMonitorV2HealthThresholds{
|
||||
MinimumSample: 20,
|
||||
WarningErrorRate: 0.02,
|
||||
CriticalErrorRate: 0.05,
|
||||
TargetTTFTMs: 2500,
|
||||
WarningTTFTMs: 2501,
|
||||
CriticalTTFTMs: 6000,
|
||||
WarningCacheRate: 0.20,
|
||||
CriticalCacheRate: 0.05,
|
||||
ErrorWeight: 0.60,
|
||||
TTFTWeight: 0.20,
|
||||
CacheWeight: 0.20,
|
||||
}
|
||||
metrics := ChannelMonitorV2Metric{
|
||||
RequestCount: 100,
|
||||
ErrorRate: 0.03,
|
||||
CacheRate: 0.50,
|
||||
CacheRateDenominator: 100,
|
||||
TTFT: ChannelMonitorV2Latency{SampleCount: 100, P50Ms: &p50, P95Ms: &p95},
|
||||
}
|
||||
health := ChannelMonitorV2HealthForWithThresholds(metrics, thresholds)
|
||||
require.Equal(t, "warning", health.ErrorRate)
|
||||
require.Equal(t, "healthy", health.TTFT)
|
||||
require.Equal(t, "healthy", health.Cache)
|
||||
require.NotNil(t, health.Score)
|
||||
require.NotNil(t, health.CacheScore)
|
||||
require.InDelta(t, 50.0, *health.CacheScore, 0.01)
|
||||
require.InDelta(t, 54.0, *health.Score, 0.01)
|
||||
require.Equal(t, "warning", health.Overall)
|
||||
|
||||
// Perfect signals → 100
|
||||
p50OK := int64(1000)
|
||||
metrics.ErrorRate = 0
|
||||
metrics.CacheRate = 1
|
||||
metrics.TTFT.P50Ms = &p50OK
|
||||
health = ChannelMonitorV2HealthForWithThresholds(metrics, thresholds)
|
||||
require.Equal(t, "healthy", health.Overall)
|
||||
require.NotNil(t, health.Score)
|
||||
require.InDelta(t, 100.0, *health.Score, 0.01)
|
||||
|
||||
// Small samples stay unknown
|
||||
health = ChannelMonitorV2HealthFor(ChannelMonitorV2Metric{RequestCount: 2})
|
||||
require.Equal(t, "unknown", health.Overall)
|
||||
require.Nil(t, health.Score)
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2DefaultHealthThresholdsAreTolerant(t *testing.T) {
|
||||
p50 := int64(2500)
|
||||
health := ChannelMonitorV2HealthFor(ChannelMonitorV2Metric{
|
||||
RequestCount: 100,
|
||||
ErrorRate: 0.03,
|
||||
CacheRate: 0,
|
||||
CacheRateDenominator: 100,
|
||||
TTFT: ChannelMonitorV2Latency{SampleCount: 100, P50Ms: &p50},
|
||||
})
|
||||
require.Equal(t, "healthy", health.ErrorRate)
|
||||
require.Equal(t, "healthy", health.TTFT)
|
||||
require.Equal(t, "healthy", health.Cache)
|
||||
require.Equal(t, "healthy", health.Overall)
|
||||
require.NotNil(t, health.CacheScore)
|
||||
require.InDelta(t, 100.0, *health.CacheScore, 0.01)
|
||||
}
|
||||
|
||||
func TestErrorRateTTFTAndCacheScoreHelpers(t *testing.T) {
|
||||
require.InDelta(t, 100.0, errorRateScore(0, 0.05), 0.001)
|
||||
require.InDelta(t, 0.0, errorRateScore(0.05, 0.05), 0.001)
|
||||
require.InDelta(t, 40.0, errorRateScore(0.03, 0.05), 0.001)
|
||||
|
||||
require.InDelta(t, 100.0, ttftP50Score(2500, 2500, 6000), 0.001)
|
||||
require.InDelta(t, 100.0, ttftP50Score(1000, 2500, 6000), 0.001)
|
||||
require.InDelta(t, 0.0, ttftP50Score(6000, 2500, 6000), 0.001)
|
||||
require.InDelta(t, 50.0, ttftP50Score(4250, 2500, 6000), 0.001)
|
||||
|
||||
require.InDelta(t, 0.0, cacheRateScore(0), 0.001)
|
||||
require.InDelta(t, 100.0, cacheRateScore(1), 0.001)
|
||||
require.InDelta(t, 50.0, cacheRateScore(0.5), 0.001)
|
||||
require.Equal(t, "critical", cacheRateBand(0.02, 0.20, 0.05))
|
||||
require.Equal(t, "warning", cacheRateBand(0.10, 0.20, 0.05))
|
||||
require.Equal(t, "healthy", cacheRateBand(0.50, 0.20, 0.05))
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2UsersRemovesOtherUserIdentity(t *testing.T) {
|
||||
selfID, otherID := int64(7), int64(9)
|
||||
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, users: &ChannelMonitorV2List[ChannelMonitorV2UserRow]{Items: []ChannelMonitorV2UserRow{
|
||||
{UserID: &otherID, Email: "other@example.com", Username: "other"},
|
||||
{UserID: &selfID, Email: "self@example.com", Username: "self"},
|
||||
}}}
|
||||
result, err := NewChannelMonitorV2Service(repo).Users(context.Background(), ChannelMonitorV2Filter{}, selfID, false)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, result.Items[0].UserID)
|
||||
require.Empty(t, result.Items[0].Email)
|
||||
require.Equal(t, "Other user #1", result.Items[0].DisplayLabel)
|
||||
require.Equal(t, selfID, *result.Items[1].UserID)
|
||||
require.True(t, result.Items[1].IsSelf)
|
||||
require.Equal(t, "Me", result.Items[1].DisplayLabel)
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2UsersAppendsSelfWhenMissingFromRanking(t *testing.T) {
|
||||
selfID, otherID := int64(7), int64(9)
|
||||
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, users: &ChannelMonitorV2List[ChannelMonitorV2UserRow]{Items: []ChannelMonitorV2UserRow{
|
||||
{UserID: &otherID, Email: "other@example.com", Username: "other", Metrics: ChannelMonitorV2Metric{RequestCount: 10}},
|
||||
}}}
|
||||
result, err := NewChannelMonitorV2Service(repo).Users(context.Background(), ChannelMonitorV2Filter{}, selfID, false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result.Items, 2)
|
||||
self := result.Items[1]
|
||||
require.True(t, self.IsSelf)
|
||||
require.Equal(t, "Me", self.DisplayLabel)
|
||||
require.Equal(t, selfID, *self.UserID)
|
||||
require.Equal(t, 0, self.Rank) // unranked / no traffic in window
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2TopUsersKeepsSelfOutsideLimit(t *testing.T) {
|
||||
items := make([]ChannelMonitorV2UserRow, 0, 12)
|
||||
for i := 1; i <= 12; i++ {
|
||||
id := int64(i)
|
||||
items = append(items, ChannelMonitorV2UserRow{UserID: &id, Rank: i, DisplayLabel: fmt.Sprintf("u%d", i)})
|
||||
}
|
||||
// self is rank 12 (index 11)
|
||||
out := channelMonitorV2TopUsersWithSelf(items, 11, 10)
|
||||
require.Len(t, out, 11)
|
||||
require.Equal(t, int64(12), *out[10].UserID)
|
||||
require.Equal(t, 12, out[10].Rank)
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2ReadAPIsRejectDisabledConfig(t *testing.T) {
|
||||
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: false}}
|
||||
_, err := NewChannelMonitorV2Service(repo).Matrix(context.Background(), ChannelMonitorV2Filter{}, ChannelMonitorV2GroupByPlatform, false)
|
||||
require.ErrorIs(t, err, ErrChannelMonitorDisabled)
|
||||
}
|
||||
|
||||
func TestNormalizeChannelMonitorV2IgnoredCategories(t *testing.T) {
|
||||
got := normalizeChannelMonitorV2IgnoredCategories([]string{"timeout", "TIMEOUT", "not-a-real", "", "timeout", "other"})
|
||||
require.Equal(t, []string{"other", "timeout"}, got)
|
||||
}
|
||||
|
||||
func TestApplyIgnoredErrorsAdjustsRateOnly(t *testing.T) {
|
||||
// applyIgnoredErrors lives in repository package; mirror the scored-rate formula.
|
||||
// scored errors = total_errors - ignored; error_rate = scored / request_count
|
||||
// success_rate stays SuccessRequests / RequestCount (ignored ≠ success).
|
||||
m := ChannelMonitorV2Metric{RequestCount: 100, ErrorRequests: 20, SuccessRequests: 80, ErrorRate: 0.2, SuccessRate: 0.8}
|
||||
ignored := int64(5)
|
||||
counted := m.ErrorRequests - ignored
|
||||
errorRate := float64(counted) / float64(m.RequestCount)
|
||||
successRate := float64(m.SuccessRequests) / float64(m.RequestCount)
|
||||
require.InDelta(t, 0.15, errorRate, 0.0001)
|
||||
require.InDelta(t, 0.80, successRate, 0.0001)
|
||||
require.Equal(t, int64(20), m.ErrorRequests)
|
||||
require.Equal(t, int64(100), m.RequestCount)
|
||||
}
|
||||
|
||||
func TestErrorsForViewerStripsDetailsAndCountsForNonAdmin(t *testing.T) {
|
||||
fixture := &ChannelMonitorV2List[ChannelMonitorV2ErrorRow]{
|
||||
Items: []ChannelMonitorV2ErrorRow{{
|
||||
Category: "timeout",
|
||||
Count: 42,
|
||||
Rate: 0.4,
|
||||
Details: []ChannelMonitorV2ErrorDetail{{
|
||||
Platform: "anthropic",
|
||||
Model: "claude",
|
||||
ErrorType: "upstream",
|
||||
StatusCode: 504,
|
||||
UpstreamStatusCode: 504,
|
||||
Message: "gateway timeout sk-secret",
|
||||
Count: 12,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
repo := &channelMonitorV2RepoStub{config: ChannelMonitorV2Config{Enabled: true}, errors: fixture}
|
||||
svc := NewChannelMonitorV2Service(repo)
|
||||
|
||||
userList, err := svc.ErrorsForViewer(context.Background(), ChannelMonitorV2Filter{}, false)
|
||||
require.NoError(t, err)
|
||||
require.False(t, repo.admin)
|
||||
require.Len(t, userList.Items, 1)
|
||||
require.Zero(t, userList.Items[0].Count)
|
||||
require.Empty(t, userList.Items[0].Details)
|
||||
require.InDelta(t, 0.4, userList.Items[0].Rate, 0.0001)
|
||||
|
||||
adminList, err := svc.ErrorsForViewer(context.Background(), ChannelMonitorV2Filter{}, true)
|
||||
require.NoError(t, err)
|
||||
require.True(t, repo.admin)
|
||||
require.Equal(t, int64(42), adminList.Items[0].Count)
|
||||
require.Len(t, adminList.Items[0].Details, 1)
|
||||
require.Contains(t, adminList.Items[0].Details[0].Message, "gateway timeout")
|
||||
}
|
||||
|
||||
func TestSnapshotRedactsPublicConfigPolicyFields(t *testing.T) {
|
||||
updatedBy := int64(9)
|
||||
repo := &channelMonitorV2RepoStub{
|
||||
config: ChannelMonitorV2Config{Enabled: true},
|
||||
snap: &ChannelMonitorV2Snapshot{
|
||||
Config: ChannelMonitorV2Config{
|
||||
Version: 3,
|
||||
Enabled: true,
|
||||
RefreshIntervalSeconds: 300,
|
||||
Platforms: []ChannelMonitorV2PlatformConfig{
|
||||
{Platform: "openai", Enabled: true, Models: []string{"gpt-5"}},
|
||||
},
|
||||
GroupIDs: []int64{1, 2},
|
||||
IgnoredErrorCategories: []string{"timeout"},
|
||||
UpdatedBy: &updatedBy,
|
||||
},
|
||||
Metrics: ChannelMonitorV2Metric{RequestCount: 100, ErrorRate: 0.1, SuccessRate: 0.9, RPM: 5},
|
||||
},
|
||||
}
|
||||
svc := NewChannelMonitorV2Service(repo)
|
||||
snap, err := svc.Snapshot(context.Background(), ChannelMonitorV2Filter{}, false)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, snap.Config.GroupIDs)
|
||||
require.Empty(t, snap.Config.IgnoredErrorCategories)
|
||||
require.Nil(t, snap.Config.UpdatedBy)
|
||||
require.Empty(t, snap.Config.Platforms[0].Models)
|
||||
require.Equal(t, 300, snap.Config.RefreshIntervalSeconds)
|
||||
require.Zero(t, snap.Metrics.RequestCount)
|
||||
require.InDelta(t, 0.1, snap.Metrics.ErrorRate, 0.0001)
|
||||
require.Zero(t, snap.Metrics.RPM)
|
||||
}
|
||||
|
||||
func TestRedactChannelMonitorV2MetricKeepsRates(t *testing.T) {
|
||||
p50 := int64(100)
|
||||
m := ChannelMonitorV2Metric{
|
||||
SuccessRequests: 90, ErrorRequests: 10, RequestCount: 100,
|
||||
InputTokens: 1, OutputTokens: 2, CacheCreationTokens: 3, CacheReadTokens: 4,
|
||||
TokenCount: 10, RPM: 12, TPM: 34, ErrorRate: 0.1, SuccessRate: 0.9,
|
||||
CacheRate: 0.5, CacheRateNumerator: 4, CacheRateDenominator: 8,
|
||||
TTFT: ChannelMonitorV2Latency{SampleCount: 50, P50Ms: &p50},
|
||||
}
|
||||
upstream := int64(3)
|
||||
m.UpstreamAffectedRequests = &upstream
|
||||
redactChannelMonitorV2Metric(&m, false)
|
||||
require.Zero(t, m.RequestCount)
|
||||
require.Zero(t, m.ErrorRequests)
|
||||
require.Zero(t, m.TokenCount)
|
||||
require.Zero(t, m.CacheRateNumerator)
|
||||
require.Zero(t, m.TTFT.SampleCount)
|
||||
require.Nil(t, m.UpstreamAffectedRequests)
|
||||
require.InDelta(t, 0.1, m.ErrorRate, 0.0001)
|
||||
require.InDelta(t, 12.0, m.RPM, 0.0001)
|
||||
require.InDelta(t, 34.0, m.TPM, 0.0001)
|
||||
require.NotNil(t, m.TTFT.P50Ms)
|
||||
require.Equal(t, int64(100), *m.TTFT.P50Ms)
|
||||
|
||||
redactChannelMonitorV2Metric(&m, true)
|
||||
require.Zero(t, m.RPM)
|
||||
require.Zero(t, m.TPM)
|
||||
require.InDelta(t, 0.1, m.ErrorRate, 0.0001)
|
||||
}
|
||||
|
||||
func TestChannelMonitorV2HealthTTFTAtTargetIsHealthy(t *testing.T) {
|
||||
p50 := int64(2500)
|
||||
m := ChannelMonitorV2Metric{
|
||||
RequestCount: 100, ErrorRate: 0,
|
||||
CacheRate: 1, CacheRateDenominator: 100,
|
||||
TTFT: ChannelMonitorV2Latency{SampleCount: 100, P50Ms: &p50},
|
||||
}
|
||||
h := ChannelMonitorV2HealthFor(m)
|
||||
require.Equal(t, "healthy", h.TTFT)
|
||||
require.NotNil(t, h.TTFTScore)
|
||||
require.InDelta(t, 100.0, *h.TTFTScore, 0.01)
|
||||
}
|
||||
Reference in New Issue
Block a user