feat(web): 合并 Ledger 首页与统计重构

This commit is contained in:
tbphp committed 2026-07-31 11:00:17 +08:00
commit 5933bbbc6f
184 files changed
+13836 -5082

No files matched your search

+1 -7
View File
@@ -264,13 +264,7 @@ make build
Full local quality gates:
```console
corepack pnpm --dir web install --frozen-lockfile
corepack pnpm --dir web run lint
corepack pnpm --dir web run format
corepack pnpm --dir web run type-check
corepack pnpm --dir web run build
go build -o gpt-load .
go test . ./internal/...
make check
```
Frontend unit tests and browser E2E tests are not part of the project workflow. Frontend verification consists of dependency installation, linting, formatting, type-checking, and building.
+1 -7
View File
@@ -263,13 +263,7 @@ make build
完整的本地质量门禁:
```console
corepack pnpm --dir web install --frozen-lockfile
corepack pnpm --dir web run lint
corepack pnpm --dir web run format
corepack pnpm --dir web run type-check
corepack pnpm --dir web run build
go build -o gpt-load .
go test . ./internal/...
make check
```
项目工作流不包含前端单元测试或浏览器 E2E;前端验证范围为依赖安装、lint、format、type-check 与 build。
+1 -7
View File
@@ -264,13 +264,7 @@ make build
ローカルの完全な品質ゲート:
```console
corepack pnpm --dir web install --frozen-lockfile
corepack pnpm --dir web run lint
corepack pnpm --dir web run format
corepack pnpm --dir web run type-check
corepack pnpm --dir web run build
go build -o gpt-load .
go test . ./internal/...
make check
```
プロジェクトのワークフローには、フロントエンドのユニットテストとブラウザE2Eテストを含めません。フロントエンドの検証範囲は、依存関係のインストール、lint、format、type-check、buildです。
+3 -2
View File
@@ -370,10 +370,11 @@ func TestAppStartUsesEncryptionForSchemaV1UpgradeBeforeRuntimeLoad(t *testing.T)
t.Fatalf("Encrypt() error = %v", err)
}
if err := db.Exec(`INSERT INTO access_keys
(name,key_value,key_hash,status,filters,rpm_limit,daily_cost_limit,monthly_cost_limit)
VALUES (?,?,?,?,?,?,?,?)`,
(name,key_value,key_hash,status,filters,rpm_limit,daily_cost_limit,monthly_cost_limit,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
"legacy", ciphertext, keyService.Hash(plaintext), "active",
`{"groups":[],"protocols":[],"models":[]}`, 0, 0, 0,
"2026-07-30T00:00:00Z", "2026-07-30T00:00:00Z",
).Error; err != nil {
t.Fatalf("insert legacy AccessKey: %v", err)
}
+3
View File
@@ -69,6 +69,9 @@ func BuildContainer() (*dig.Container, error) {
func(service *requestlog.Service) control.UsageStatReader {
return service
},
func(service *requestlog.Service) control.HomeStatisticsReader {
return service
},
func(service *requestlog.Service) control.RequestLogStatsReader {
return service
},
+36
View File
@@ -125,6 +125,42 @@ func TestBuildContainerWiresUsageReaderToSingletonRequestLogService(t *testing.T
}
}
func TestBuildContainerWiresHomeStatisticsReaderToSingletonRequestLogService(
t *testing.T,
) {
t.Setenv("AUTH_KEY", "test-auth-key")
t.Setenv("DATA_DIR", t.TempDir())
t.Setenv("DATABASE_DSN", ":memory:")
t.Setenv("ENCRYPTION_KEY", "test-master-key-long")
dependencyContainer, err := BuildContainer()
if err != nil {
t.Fatalf("BuildContainer() error = %v", err)
}
err = dependencyContainer.Invoke(func(
service *requestlog.Service,
usageReader control.UsageStatReader,
homeReader control.HomeStatisticsReader,
db *gorm.DB,
) {
sqlDB, dbErr := db.DB()
if dbErr == nil {
t.Cleanup(func() { _ = sqlDB.Close() })
}
if usageReader != service || homeReader != service {
t.Fatalf(
"statistics readers = %T/%T, want singleton %p",
usageReader,
homeReader,
service,
)
}
})
if err != nil {
t.Fatalf("resolve Home statistics reader graph: %v", err)
}
}
func TestBuildContainerWiresSingletonPriceRuntime(t *testing.T) {
t.Setenv("AUTH_KEY", "test-auth-key")
t.Setenv("DATA_DIR", t.TempDir())
+1 -1
View File
@@ -77,7 +77,7 @@ func (s *Service) CreateAccessKeyIdempotent(
metadata, err := mapAccessKeyMetadataRow(accessKeyMetadataRow{
ID: row.ID, Name: row.Name, KeySuffix: row.KeySuffix,
Status: row.Status, Filters: row.Filters, RPMLimit: row.RPMLimit,
CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
CreatedAtMS: row.CreatedAtMS, UpdatedAtMS: row.UpdatedAtMS,
})
if err != nil {
return idempotentMutationResult{}, err
+50 -29
View File
@@ -9,7 +9,6 @@ import (
"fmt"
"io"
"strings"
"time"
"gorm.io/gorm"
@@ -57,14 +56,14 @@ type AccessKeyUpdateRequest struct {
}
type AccessKeyMetadata struct {
ID uint `json:"id"`
Name string `json:"name"`
MaskedKey string `json:"masked_key"`
Status state.AccessKeyStatus `json:"status"`
Filters AccessKeyFilters `json:"filters"`
RPMLimit int64 `json:"rpm_limit"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint `json:"id"`
Name string `json:"name"`
MaskedKey string `json:"masked_key"`
Status state.AccessKeyStatus `json:"status"`
Filters AccessKeyFilters `json:"filters"`
RPMLimit int64 `json:"rpm_limit"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type AccessKeyCreateResult struct {
@@ -80,20 +79,20 @@ type AccessKeyOption struct {
}
type AccessKeyRevealResult struct {
ID uint `json:"id"`
Key string `json:"key"`
RevealedAt time.Time `json:"revealed_at"`
ID uint `json:"id"`
Key string `json:"key"`
RevealedAtMS int64 `json:"revealed_at_ms"`
}
type accessKeyMetadataRow struct {
ID uint
Name string
KeySuffix string
Status string
Filters models.JSON
RPMLimit int64
CreatedAt time.Time
UpdatedAt time.Time
ID uint
Name string
KeySuffix string
Status string
Filters models.JSON
RPMLimit int64
CreatedAtMS int64
UpdatedAtMS int64
}
const accessKeyPrefix = "sk-gl-"
@@ -159,7 +158,7 @@ func (s *Service) CreateAccessKey(
metadata, err := mapAccessKeyMetadataRow(accessKeyMetadataRow{
ID: row.ID, Name: row.Name, KeySuffix: row.KeySuffix,
Status: row.Status, Filters: row.Filters, RPMLimit: row.RPMLimit,
CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
CreatedAtMS: row.CreatedAtMS, UpdatedAtMS: row.UpdatedAtMS,
})
if err != nil {
return err
@@ -182,7 +181,7 @@ func (s *Service) ListAccessKeys(ctx context.Context) ([]AccessKeyMetadata, erro
Model(&models.AccessKey{}).
Select(
"id", "name", "key_suffix", "status", "filters", "rpm_limit",
"created_at", "updated_at",
"created_at_ms", "updated_at_ms",
).
Order("id ASC").
Scan(&rows).Error; err != nil {
@@ -246,7 +245,7 @@ func (s *Service) UpdateAccessKey(
if err := tx.Model(&models.AccessKey{}).
Select(
"id", "name", "key_suffix", "status", "filters", "rpm_limit",
"created_at", "updated_at",
"created_at_ms", "updated_at_ms",
).
Where("id = ?", id).
Take(&row).Error; err != nil {
@@ -302,7 +301,7 @@ func (s *Service) UpdateAccessKey(
if err := tx.Model(&models.AccessKey{}).
Select(
"id", "name", "key_suffix", "status", "filters", "rpm_limit",
"created_at", "updated_at",
"created_at_ms", "updated_at_ms",
).
Where("id = ?", row.ID).
Take(&row).Error; err != nil {
@@ -365,14 +364,32 @@ func (s *Service) RevealAccessKey(
app_errors.ErrInternalServer,
)
}
revealedAtMS, err := safeEpochMilliseconds(s.now())
if err != nil {
return AccessKeyRevealResult{}, app_errors.ErrInternalServer
}
return AccessKeyRevealResult{
ID: row.ID,
Key: plaintext,
RevealedAt: s.now().UTC(),
ID: row.ID,
Key: plaintext,
RevealedAtMS: revealedAtMS,
}, nil
}
func mapAccessKeyMetadataRow(row accessKeyMetadataRow) (AccessKeyMetadata, error) {
if err := validateSafeMilliseconds(row.CreatedAtMS); err != nil {
return AccessKeyMetadata{}, fmt.Errorf(
"access key %d has invalid created_at_ms: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
if err := validateSafeMilliseconds(row.UpdatedAtMS); err != nil {
return AccessKeyMetadata{}, fmt.Errorf(
"access key %d has invalid updated_at_ms: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
status := state.AccessKeyStatus(row.Status)
if status != state.AccessKeyStatusActive && status != state.AccessKeyStatusDisabled {
return AccessKeyMetadata{}, fmt.Errorf(
@@ -398,12 +415,16 @@ func mapAccessKeyMetadataRow(row accessKeyMetadataRow) (AccessKeyMetadata, error
}
return AccessKeyMetadata{
ID: row.ID, Name: row.Name,
MaskedKey: accessKeyPrefix + "••••••••" + row.KeySuffix,
MaskedKey: maskedAccessKey(row.KeySuffix),
Status: status, Filters: filters, RPMLimit: row.RPMLimit,
CreatedAt: row.CreatedAt.UTC(), UpdatedAt: row.UpdatedAt.UTC(),
CreatedAtMS: row.CreatedAtMS, UpdatedAtMS: row.UpdatedAtMS,
}, nil
}
func maskedAccessKey(suffix string) string {
return accessKeyPrefix + "****" + suffix
}
func validAccessKeySuffix(value string) bool {
if len(value) != 4 {
return false
+2 -2
View File
@@ -48,7 +48,7 @@ func TestAccessKeyMetadataListAndUpdateNeverDecryptCiphertext(t *testing.T) {
t.Fatalf("ListAccessKeys() error = %v", err)
}
if len(list) != 1 || list[0].ID != created.ID ||
list[0].MaskedKey != "sk-gl-••••••••0000" {
list[0].MaskedKey != "sk-gl-****0000" {
t.Fatalf("ListAccessKeys() = %#v", list)
}
encoded, err := json.Marshal(list)
@@ -175,7 +175,7 @@ func TestRevealAccessKeyIsTheExplicitMetadataDecryptPath(t *testing.T) {
t.Fatalf("RevealAccessKey() error = %v", err)
}
if revealed.ID != created.ID || revealed.Key != created.Key ||
revealed.RevealedAt.IsZero() {
revealed.RevealedAtMS <= 0 {
t.Fatalf("RevealAccessKey() = %#v", revealed)
}
if spy.decryptCalls != 1 {
+2 -2
View File
@@ -225,9 +225,9 @@ func TestListAccessKeysReturnsMaskedMetadataInIDOrderWithoutDecrypting(t *testin
}
if len(listed) != 2 ||
listed[0].ID != first.ID ||
listed[0].MaskedKey != "sk-gl-••••••••0e0f" ||
listed[0].MaskedKey != "sk-gl-****0e0f" ||
listed[1].ID != second.ID ||
listed[1].MaskedKey != "sk-gl-••••••••1e1f" {
listed[1].MaskedKey != "sk-gl-****1e1f" {
t.Fatalf("ListAccessKeys() = %#v", listed)
}
+30 -13
View File
@@ -35,7 +35,7 @@ func TestEnsureInitialStatePublishesBuiltinPricesWithAndWithoutMarker(t *testing
t.Fatalf("EnsureInitialState() error = %v", err)
}
assertPublishedPrice(t, fixture.priceRuntime, "gpt-4o", pricing.SourceBuiltin, 2.5)
assertPublishedPrice(t, fixture.priceRuntime, "gpt-4o", pricing.SourceBuiltin, 2_500_000_000)
if got := fixture.manager.Current().Revision; got != beforeRevision {
t.Fatalf("ConfigSnapshot revision = %d, want unchanged %d", got, beforeRevision)
}
@@ -44,15 +44,18 @@ func TestEnsureInitialStatePublishesBuiltinPricesWithAndWithoutMarker(t *testing
t.Run("existing marker and user row", func(t *testing.T) {
fixture := newServiceFixture(t)
zero := 0.0
output := 12.0
zero := int64(0)
output := int64(12_000_000_000)
if err := fixture.db.Create(&models.SystemSetting{
Key: bootstrapMarkerForTest, Value: "true",
}).Error; err != nil {
t.Fatalf("create marker: %v", err)
}
if err := fixture.db.Create(&models.ModelPrice{
Pattern: "gpt-*", InputPrice: &zero, OutputPrice: &output, Source: "user",
Pattern: "gpt-*",
InputPriceNanoUSDPerMillionTokens: &zero,
OutputPriceNanoUSDPerMillionTokens: &output,
Source: "user",
}).Error; err != nil {
t.Fatalf("create user ModelPrice: %v", err)
}
@@ -84,7 +87,7 @@ func TestEnsureInitialStatePublishesBuiltinPricesWithAndWithoutMarker(t *testing
t.Fatalf("second EnsureInitialState() error = %v", err)
}
assertPublishedPrice(t, second.priceRuntime, "gpt-4o", pricing.SourceBuiltin, 2.5)
assertPublishedPrice(t, second.priceRuntime, "gpt-4o", pricing.SourceBuiltin, 2_500_000_000)
if got := second.manager.Current().Revision; got != beforeRevision {
t.Fatalf("ConfigSnapshot revision = %d, want unchanged %d", got, beforeRevision)
}
@@ -93,9 +96,11 @@ func TestEnsureInitialStatePublishesBuiltinPricesWithAndWithoutMarker(t *testing
func TestEnsureInitialStateRejectsInvalidPersistedModelPrice(t *testing.T) {
fixture := newServiceFixture(t)
negative := -1.0
value := int64(1_000_000_000)
if err := fixture.db.Create(&models.ModelPrice{
Pattern: "invalid-price", InputPrice: &negative, Source: "user",
Pattern: "invalid?pattern",
InputPriceNanoUSDPerMillionTokens: &value,
Source: "user",
}).Error; err != nil {
t.Fatalf("create invalid persisted ModelPrice: %v", err)
}
@@ -128,9 +133,21 @@ func TestEnsureInitialStateRejectsNonUserPersistedModelPrice(t *testing.T) {
}
})
if err := fixture.db.Exec(`
INSERT INTO model_prices (pattern, output_price, source)
VALUES (?, ?, ?)
`, privatePattern, 73456.125, privateSource).Error; err != nil {
INSERT INTO model_prices (
pattern,
output_price_nano_usd_per_million_tokens,
source,
created_at_ms,
updated_at_ms
)
VALUES (?, ?, ?, ?, ?)
`,
privatePattern,
int64(73_456_125_000_000),
privateSource,
time.Now().UnixMilli(),
time.Now().UnixMilli(),
).Error; err != nil {
t.Fatalf("insert invalid persisted ModelPrice source: %v", err)
}
if err := fixture.db.Exec("PRAGMA ignore_check_constraints = OFF").Error; err != nil {
@@ -141,7 +158,7 @@ func TestEnsureInitialStateRejectsNonUserPersistedModelPrice(t *testing.T) {
if !errors.Is(err, app_errors.ErrInternalServer) {
t.Fatalf("EnsureInitialState() error = %v, want internal error", err)
}
for _, privateValue := range []string{privatePattern, privateSource, "73456.125"} {
for _, privateValue := range []string{privatePattern, privateSource, "73456125000000"} {
if strings.Contains(err.Error(), privateValue) {
t.Fatalf("EnsureInitialState() error leaked persisted value %q: %v", privateValue, err)
}
@@ -428,7 +445,7 @@ func assertPublishedPrice(
runtime *PriceRuntime,
model string,
wantSource pricing.Source,
wantInput float64,
wantInput pricing.NanoUSD,
) {
t.Helper()
table := runtime.Load()
@@ -440,7 +457,7 @@ func assertPublishedPrice(
t.Fatalf("PriceTable.Match(%q) missed", model)
}
if rule.Source != wantSource || !rule.Prices.UncachedInput.Set ||
rule.Prices.UncachedInput.Value != wantInput {
rule.Prices.UncachedInput.NanoUSDPerMillion != wantInput {
t.Fatalf("PriceTable.Match(%q) = %+v", model, rule)
}
}
+3 -2
View File
@@ -238,14 +238,15 @@ func TestDeleteGroupAcceptsRegistryAlreadyAtTargetStateAndRetainsHistory(t *test
fixture.stats.RecordFailure(key.ID, health.FailureCategoryAmbiguous, 0, now)
beforeStats := fixture.stats.Snapshot(key.ID, now)
if err := fixture.db.Create(&models.RequestLog{
ID: "delete-history-log", CreatedAt: now, AccessKeyID: 1,
ID: "delete-history-log", CompletedAtMS: now.UnixMilli(), AccessKeyID: 1,
Protocol: "openai-completions", ClientModel: "model", UpstreamModel: "model",
Status: "success", StatusCode: http.StatusOK,
}).Error; err != nil {
t.Fatal(err)
}
if err := fixture.db.Create(&models.UsageStat{
HourBucket: now, GroupID: groupID, Model: "model", RequestCount: 1,
BucketStartMS: now.UnixMilli(), AccessKeyID: 1,
GroupID: groupID, Model: "model", RequestCount: 1, SuccessCount: 1,
}).Error; err != nil {
t.Fatal(err)
}
+43 -32
View File
@@ -36,9 +36,9 @@ type healthGroupResponse struct {
}
type healthRecoveryResponse struct {
Automatic bool `json:"automatic"`
Mode string `json:"mode"`
At *time.Time `json:"at"`
Automatic bool `json:"automatic"`
Mode string `json:"mode"`
AtMS *int64 `json:"at_ms"`
}
type healthProblemKeyResponse struct {
@@ -48,7 +48,7 @@ type healthProblemKeyResponse struct {
Mask string `json:"mask"`
LastFailureCategory string `json:"last_failure_category"`
LastStatusCode *int `json:"last_status_code"`
CooldownUntil *time.Time `json:"cooldown_until,omitempty"`
CooldownUntilMS *int64 `json:"cooldown_until_ms"`
FailureCount int `json:"failure_count"`
RecentSuccessCount uint64 `json:"recent_success_count"`
RecentFailureCount uint64 `json:"recent_failure_count"`
@@ -59,24 +59,24 @@ type healthProblemKeyResponse struct {
}
type requestLogHealthResponse struct {
EnqueuedTotal uint64 `json:"enqueued_total"`
PersistedTotal uint64 `json:"persisted_total"`
DroppedNotRunningTotal uint64 `json:"dropped_not_running_total"`
DroppedQueueFullTotal uint64 `json:"dropped_queue_full_total"`
DroppedStoppingTotal uint64 `json:"dropped_stopping_total"`
DroppedPersistFailedTotal uint64 `json:"dropped_persist_failed_total"`
DroppedShutdownTotal uint64 `json:"dropped_shutdown_total"`
DroppedTotal uint64 `json:"dropped_total"`
WriteFailureTotal uint64 `json:"write_failure_total"`
RetentionDeleteFailureTotal uint64 `json:"retention_delete_failure_total"`
QueueDepth int `json:"queue_depth"`
QueueCapacity int `json:"queue_capacity"`
LastWriteFailureAt *time.Time `json:"last_write_failure_at"`
LastRetentionFailureAt *time.Time `json:"last_retention_failure_at"`
EnqueuedTotal uint64 `json:"enqueued_total"`
PersistedTotal uint64 `json:"persisted_total"`
DroppedNotRunningTotal uint64 `json:"dropped_not_running_total"`
DroppedQueueFullTotal uint64 `json:"dropped_queue_full_total"`
DroppedStoppingTotal uint64 `json:"dropped_stopping_total"`
DroppedPersistFailedTotal uint64 `json:"dropped_persist_failed_total"`
DroppedShutdownTotal uint64 `json:"dropped_shutdown_total"`
DroppedTotal uint64 `json:"dropped_total"`
WriteFailureTotal uint64 `json:"write_failure_total"`
RetentionDeleteFailureTotal uint64 `json:"retention_delete_failure_total"`
QueueDepth int `json:"queue_depth"`
QueueCapacity int `json:"queue_capacity"`
LastWriteFailureAtMS *int64 `json:"last_write_failure_at_ms"`
LastRetentionFailureAtMS *int64 `json:"last_retention_failure_at_ms"`
}
type runtimeHealthResponse struct {
ObservedAt time.Time `json:"observed_at"`
ObservedAtMS int64 `json:"observed_at_ms"`
Version string `json:"version"`
UptimeSeconds int64 `json:"uptime_seconds"`
SnapshotRevision uint64 `json:"snapshot_revision"`
@@ -132,14 +132,6 @@ func addHealthCount(counts *healthCountsResponse, bucket healthBucket) {
}
}
func optionalUTC(value time.Time) *time.Time {
if value.IsZero() {
return nil
}
value = value.UTC()
return &value
}
func cloneInt(value *int) *int {
if value == nil {
return nil
@@ -205,8 +197,12 @@ func (service *Service) RuntimeHealth() (runtimeHealthResponse, error) {
app_errors.ErrInternalServer,
)
}
observedAtMS, err := safeEpochMilliseconds(observation.observedAt)
if err != nil {
return runtimeHealthResponse{}, fmt.Errorf("map runtime health observed_at_ms: %w", err)
}
result := runtimeHealthResponse{
ObservedAt: observation.observedAt,
ObservedAtMS: observedAtMS,
SnapshotRevision: observation.snapshot.Revision,
StatsWindowSeconds: int64(health.StatsWindow / time.Second),
Groups: []healthGroupResponse{},
@@ -261,11 +257,18 @@ func (service *Service) RuntimeHealth() (runtimeHealthResponse, error) {
WeightAuto: key.WeightAuto,
}
if bucket == healthBucketCooldown {
detail.CooldownUntil = optionalUTC(key.CooldownUntil)
cooldownUntilMS, err := optionalSafeEpochMilliseconds(key.CooldownUntil)
if err != nil {
return runtimeHealthResponse{}, fmt.Errorf(
"map runtime health cooldown_until_ms: %w",
err,
)
}
detail.CooldownUntilMS = cooldownUntilMS
detail.Recovery = healthRecoveryResponse{
Automatic: true,
Mode: "cooldown_expiry",
At: optionalUTC(key.CooldownUntil),
AtMS: cooldownUntilMS,
}
result.CooldownKeys = append(result.CooldownKeys, detail)
} else {
@@ -305,6 +308,14 @@ func mapRequestLogHealth(stats requestlog.Stats) (requestLogHealthResponse, erro
stats.QueueCapacity < 0 || uint64(stats.QueueCapacity) > uint64(maxSafeInteger) {
return requestLogHealthResponse{}, fmt.Errorf("map request log health: unsafe queue")
}
lastWriteFailureAtMS, err := optionalSafeEpochMilliseconds(stats.LastWriteFailureAt)
if err != nil {
return requestLogHealthResponse{}, fmt.Errorf("map last write failure timestamp: %w", err)
}
lastRetentionFailureAtMS, err := optionalSafeEpochMilliseconds(stats.LastRetentionFailureAt)
if err != nil {
return requestLogHealthResponse{}, fmt.Errorf("map last retention failure timestamp: %w", err)
}
return requestLogHealthResponse{
EnqueuedTotal: stats.EnqueuedTotal,
PersistedTotal: stats.PersistedTotal,
@@ -318,8 +329,8 @@ func mapRequestLogHealth(stats requestlog.Stats) (requestLogHealthResponse, erro
RetentionDeleteFailureTotal: stats.RetentionDeleteFailureTotal,
QueueDepth: stats.QueueDepth,
QueueCapacity: stats.QueueCapacity,
LastWriteFailureAt: optionalUTC(stats.LastWriteFailureAt),
LastRetentionFailureAt: optionalUTC(stats.LastRetentionFailureAt),
LastWriteFailureAtMS: lastWriteFailureAtMS,
LastRetentionFailureAtMS: lastRetentionFailureAtMS,
}, nil
}
+8 -7
View File
@@ -106,7 +106,7 @@ func TestRuntimeHealthReturnsMutuallyExclusiveCurrentState(t *testing.T) {
if err != nil {
t.Fatalf("RuntimeHealth() error = %v", err)
}
if !got.ObservedAt.Equal(now) || got.SnapshotRevision != fixture.manager.Current().Revision ||
if got.ObservedAtMS != now.UnixMilli() || got.SnapshotRevision != fixture.manager.Current().Revision ||
got.StatsWindowSeconds != 300 {
t.Fatalf("observation metadata = %#v", got)
}
@@ -147,12 +147,13 @@ func TestRuntimeHealthReturnsMutuallyExclusiveCurrentState(t *testing.T) {
*got.BlacklistedKeys[0].LastStatusCode != 401 ||
got.BlacklistedKeys[0].ConsecutiveFailureCount != 1 ||
got.BlacklistedKeys[0].Recovery.Mode != "validation_probe" ||
got.BlacklistedKeys[0].Recovery.At != nil {
got.BlacklistedKeys[0].Recovery.AtMS != nil {
t.Fatalf("blacklisted details = %#v", got.BlacklistedKeys)
}
if got.RequestLog.DroppedTotal != 2 ||
got.RequestLog.LastWriteFailureAt == nil ||
got.RequestLog.LastRetentionFailureAt != nil {
got.RequestLog.LastWriteFailureAtMS == nil ||
*got.RequestLog.LastWriteFailureAtMS != now.Add(-time.Minute).UnixMilli() ||
got.RequestLog.LastRetentionFailureAtMS != nil {
t.Fatalf("request log stats = %#v", got.RequestLog)
}
}
@@ -285,8 +286,8 @@ func TestRuntimeHealthJSONOmitsScoresCredentialsAndZeroTimes(t *testing.T) {
"retention_delete_failure_total": {},
"queue_depth": {},
"queue_capacity": {},
"last_write_failure_at": {},
"last_retention_failure_at": {},
"last_write_failure_at_ms": {},
"last_retention_failure_at_ms": {},
}
if !reflect.DeepEqual(requestLogFields, wantRequestLogFields) {
t.Fatalf("request_log fields = %#v, want %#v", requestLogFields, wantRequestLogFields)
@@ -422,7 +423,7 @@ func TestRuntimeHealthEndpointRequiresManagementAuthentication(t *testing.T) {
if err := json.Unmarshal(success.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode authenticated response: %v", err)
}
if envelope.Code != 0 || !envelope.Data.ObservedAt.Equal(healthNow()) ||
if envelope.Code != 0 || envelope.Data.ObservedAtMS != healthNow().UnixMilli() ||
envelope.Data.StatsWindowSeconds != 300 {
t.Fatalf("authenticated envelope = %#v", envelope)
}
+357
View File
@@ -0,0 +1,357 @@
package control
import (
"context"
"fmt"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gpt-load/internal/platform/epochms"
app_errors "gpt-load/internal/platform/errors"
"gpt-load/internal/platform/response"
"gpt-load/internal/platform/version"
"gpt-load/internal/protocol"
"gpt-load/internal/state"
"gpt-load/internal/storage/models"
)
type HomeInventory struct {
GroupCount int64 `json:"group_count"`
UpstreamKeyCount int64 `json:"upstream_key_count"`
AvailableUpstreamKeyCount int64 `json:"available_upstream_key_count"`
ModelCount int64 `json:"model_count"`
}
type HomeAccessKey struct {
ID uint `json:"id"`
Name string `json:"name"`
MaskedKey string `json:"masked_key"`
Protocols []protocol.Protocol `json:"protocols"`
}
type HomeBase struct {
Inventory HomeInventory `json:"inventory"`
AccessKeys []HomeAccessKey `json:"access_keys"`
}
type homeResponse struct {
ServerNowMS int64 `json:"server_now_ms"`
StartedAtMS int64 `json:"started_at_ms"`
Version string `json:"version"`
HomeBase
}
type homeUpstreamKeyRow struct {
ID uint
GroupID uint
Status models.UpstreamKeyStatus
}
type homeAccessKeyRow struct {
ID uint
Name string
KeySuffix string
Filters models.JSON
}
type homeReadRows struct {
groupCount int64
upstreamKeys []homeUpstreamKeyRow
accessKeys []homeAccessKeyRow
}
func (s *Service) ReadHomeBase(
ctx context.Context,
nowMS int64,
) (HomeBase, error) {
if s == nil || s.db == nil || s.manager == nil || s.registrySnapshot == nil {
return HomeBase{}, fmt.Errorf(
"read home base: dependencies unavailable: %w",
app_errors.ErrInternalServer,
)
}
if ctx == nil {
ctx = context.Background()
}
if err := validateSafeMilliseconds(nowMS); err != nil {
return HomeBase{}, fmt.Errorf(
"read home base: invalid observation time: %w",
app_errors.ErrInternalServer,
)
}
now, err := epochms.ToTime(nowMS)
if err != nil {
return HomeBase{}, fmt.Errorf(
"read home base: invalid observation time: %w",
app_errors.ErrInternalServer,
)
}
s.writeMu.RLock()
defer s.writeMu.RUnlock()
snapshot := s.manager.Current()
if snapshot == nil {
return HomeBase{}, fmt.Errorf(
"read home base: runtime snapshot unavailable: %w",
app_errors.ErrInternalServer,
)
}
runtimeKeys := s.registrySnapshot()
rows, err := s.readHomeRows(ctx)
if err != nil {
return HomeBase{}, app_errors.ParseDBError(err)
}
inventory := HomeInventory{
GroupCount: rows.groupCount,
UpstreamKeyCount: int64(len(rows.upstreamKeys)),
ModelCount: countHomeModels(snapshot),
}
inventory.AvailableUpstreamKeyCount, err = countAvailableHomeKeys(
snapshot,
rows.upstreamKeys,
runtimeKeys,
now,
)
if err != nil {
return HomeBase{}, err
}
if err := validateHomeInventory(inventory); err != nil {
return HomeBase{}, err
}
accessKeys, err := mapHomeAccessKeys(rows.accessKeys)
if err != nil {
return HomeBase{}, err
}
return HomeBase{
Inventory: inventory,
AccessKeys: accessKeys,
}, nil
}
func (s *Server) handleHome(c *gin.Context) {
if c.Request.URL.RawQuery != "" || c.Request.URL.ForceQuery {
writeServiceError(c, "home", app_errors.ErrBadRequest)
return
}
serverNowMS, err := safeEpochMilliseconds(s.now())
if err != nil {
writeServiceError(c, "home", err)
return
}
startedAtMS, err := safeEpochMilliseconds(s.startedAt)
if err != nil {
writeServiceError(c, "home", err)
return
}
base, err := s.service.ReadHomeBase(c.Request.Context(), serverNowMS)
if err != nil {
writeServiceError(c, "home", err)
return
}
response.SuccessI18n(c, "common.success", homeResponse{
ServerNowMS: serverNowMS,
StartedAtMS: startedAtMS,
Version: version.Version,
HomeBase: base,
})
}
func (s *Service) readHomeRows(
ctx context.Context,
) (homeReadRows, error) {
var result homeReadRows
err := s.withReadSnapshot(ctx, func(tx *gorm.DB) error {
if err := tx.Model(&models.Group{}).Count(&result.groupCount).Error; err != nil {
return fmt.Errorf("count home groups: %w", err)
}
if err := tx.Model(&models.UpstreamKey{}).
Select("id", "group_id", "status").
Order("id ASC").
Find(&result.upstreamKeys).Error; err != nil {
return fmt.Errorf("query home upstream keys: %w", err)
}
if err := tx.Model(&models.AccessKey{}).
Select("id", "name", "key_suffix", "filters").
Where("status = ?", state.AccessKeyStatusActive).
Order("id ASC").
Find(&result.accessKeys).Error; err != nil {
return fmt.Errorf("query home access keys: %w", err)
}
return nil
})
if err != nil {
return homeReadRows{}, err
}
return result, nil
}
func countHomeModels(snapshot *state.ConfigSnapshot) int64 {
names := make(map[string]struct{})
for _, group := range snapshot.Groups {
for _, model := range group.Models {
name := strings.TrimSpace(model.Alias)
if name == "" {
name = strings.TrimSpace(model.ID)
}
if name != "" {
names[name] = struct{}{}
}
}
}
return int64(len(names))
}
func countAvailableHomeKeys(
snapshot *state.ConfigSnapshot,
persisted []homeUpstreamKeyRow,
runtime []state.KeyRuntimeView,
now time.Time,
) (int64, error) {
runtimeByID := make(map[uint]state.KeyRuntimeView, len(runtime))
for _, key := range runtime {
if key.ID == 0 {
return 0, fmt.Errorf(
"count available home keys: runtime key has zero id: %w",
app_errors.ErrInternalServer,
)
}
if _, duplicate := runtimeByID[key.ID]; duplicate {
return 0, fmt.Errorf(
"count available home keys: duplicate runtime key %d: %w",
key.ID,
app_errors.ErrInternalServer,
)
}
runtimeByID[key.ID] = key
}
seen := make(map[uint]struct{}, len(persisted))
var available int64
for _, row := range persisted {
if row.ID == 0 {
return 0, fmt.Errorf(
"count available home keys: persisted key has zero id: %w",
app_errors.ErrInternalServer,
)
}
if _, duplicate := seen[row.ID]; duplicate {
return 0, fmt.Errorf(
"count available home keys: duplicate persisted key %d: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
seen[row.ID] = struct{}{}
group, groupExists := snapshot.GroupCatalog[row.GroupID]
key, keyExists := runtimeByID[row.ID]
if !groupExists || !keyExists || key.GroupID != row.GroupID {
return 0, fmt.Errorf(
"count available home key %d: runtime configuration mismatch: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
var status state.KeyStatus
switch row.Status {
case models.UpstreamKeyStatusActive:
status = state.KeyStatusActive
case models.UpstreamKeyStatusDisabled:
status = state.KeyStatusDisabled
default:
return 0, fmt.Errorf(
"count available home key %d: invalid persisted status: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
if key.Status != status {
return 0, fmt.Errorf(
"count available home key %d: runtime status mismatch: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
if group.Enabled &&
status == state.KeyStatusActive &&
key.RuntimeState(now) == state.KeyRuntimeAvailable {
available++
}
}
if len(runtimeByID) != len(seen) {
return 0, fmt.Errorf(
"count available home keys: runtime key set mismatch: %w",
app_errors.ErrInternalServer,
)
}
return available, nil
}
func mapHomeAccessKeys(rows []homeAccessKeyRow) ([]HomeAccessKey, error) {
result := make([]HomeAccessKey, 0, len(rows))
for _, row := range rows {
if uint64(row.ID) > uint64(maxSafeInteger) {
return nil, fmt.Errorf(
"map home access key %d: unsafe id: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
if !validAccessKeySuffix(row.KeySuffix) {
return nil, fmt.Errorf(
"map home access key %d: invalid persisted suffix: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
filters, err := decodeStoredAccessKeyFilters(row.Filters)
if err != nil {
return nil, fmt.Errorf(
"map home access key %d filters: %w",
row.ID,
app_errors.ErrInternalServer,
)
}
protocols := filters.Protocols
if len(protocols) == 0 {
protocols = protocol.DataPlaneProtocols()
} else {
protocols = append([]protocol.Protocol(nil), protocols...)
}
result = append(result, HomeAccessKey{
ID: row.ID, Name: row.Name,
MaskedKey: maskedAccessKey(row.KeySuffix),
Protocols: protocols,
})
}
return result, nil
}
func validateHomeInventory(value HomeInventory) error {
for _, field := range []struct {
name string
value int64
}{
{name: "group count", value: value.GroupCount},
{name: "upstream key count", value: value.UpstreamKeyCount},
{
name: "available upstream key count",
value: value.AvailableUpstreamKeyCount,
},
{name: "model count", value: value.ModelCount},
} {
if field.value < 0 || field.value > maxSafeInteger {
return fmt.Errorf(
"map home inventory %s: unsafe integer: %w",
field.name,
app_errors.ErrInternalServer,
)
}
}
return nil
}
+385
View File
@@ -0,0 +1,385 @@
package control
import (
"context"
"fmt"
"net/url"
"github.com/gin-gonic/gin"
"gpt-load/internal/platform/epochms"
app_errors "gpt-load/internal/platform/errors"
"gpt-load/internal/platform/response"
"gpt-load/internal/requestlog"
)
const homeStatisticsRankingLimit = 5
type HomeStatisticsReader interface {
QueryHomeStatistics(
context.Context,
requestlog.HomeStatisticsQuery,
) (requestlog.HomeStatisticsReport, error)
}
type homeStatisticsAggregateResponse struct {
RequestCount int64 `json:"request_count"`
SuccessCount int64 `json:"success_count"`
FailureCount int64 `json:"failure_count"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
UsageMissingCount int64 `json:"usage_missing_count"`
PartialCount int64 `json:"partial_count"`
UnpricedRequestCount int64 `json:"unpriced_request_count"`
}
type homeStatisticsSeriesResponse struct {
BucketStartMS int64 `json:"bucket_start_ms"`
BucketEndMS int64 `json:"bucket_end_ms"`
RequestCount int64 `json:"request_count"`
FailureCount int64 `json:"failure_count"`
}
type homeStatisticsRefResponse struct {
ID uint `json:"id"`
Name *string `json:"name"`
Deleted bool `json:"deleted"`
}
type homeModelRankingResponse struct {
Model string `json:"model"`
Group homeStatisticsRefResponse `json:"group"`
RequestCount int64 `json:"request_count"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
}
type homeGroupRankingResponse struct {
Group homeStatisticsRefResponse `json:"group"`
RequestCount int64 `json:"request_count"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
}
type homeAccessKeyRankingResponse struct {
AccessKey homeStatisticsRefResponse `json:"access_key"`
RequestCount int64 `json:"request_count"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
}
type homeStatisticsRankingsResponse struct {
Models []homeModelRankingResponse `json:"models"`
Groups []homeGroupRankingResponse `json:"groups"`
AccessKeys []homeAccessKeyRankingResponse `json:"access_keys"`
}
type homeStatisticsResponse struct {
Range requestlog.HomeStatisticsRange `json:"range"`
Granularity requestlog.UsageGranularity `json:"granularity"`
FromMS int64 `json:"from_ms"`
ToMS int64 `json:"to_ms"`
ObservedAtMS int64 `json:"observed_at_ms"`
Summary homeStatisticsAggregateResponse `json:"summary"`
Series []homeStatisticsSeriesResponse `json:"series"`
Rankings homeStatisticsRankingsResponse `json:"rankings"`
}
func (s *Service) QueryHomeStatistics(
ctx context.Context,
query requestlog.HomeStatisticsQuery,
) (requestlog.HomeStatisticsReport, error) {
if s == nil || s.homeStatistics == nil {
return requestlog.HomeStatisticsReport{}, app_errors.ErrInternalServer
}
report, err := s.homeStatistics.QueryHomeStatistics(ctx, query)
if err != nil {
return requestlog.HomeStatisticsReport{}, app_errors.ParseDBError(err)
}
return report, nil
}
func (s *Server) handleHomeStatistics(c *gin.Context) {
observedAtMS, err := safeEpochMilliseconds(s.now())
if err != nil {
writeServiceError(c, "home_statistics", err)
return
}
query, apiErr := parseHomeStatisticsQuery(
c.Request.URL.RawQuery,
observedAtMS,
)
if apiErr != nil {
writeServiceError(c, "home_statistics", apiErr)
return
}
report, err := s.service.QueryHomeStatistics(
c.Request.Context(),
query,
)
if err != nil {
writeServiceError(c, "home_statistics", err)
return
}
result, err := mapHomeStatisticsResponse(query, report)
if err != nil {
writeServiceError(c, "home_statistics", err)
return
}
response.SuccessI18n(c, "common.success", result)
}
func parseHomeStatisticsQuery(
rawQuery string,
observedAtMS int64,
) (requestlog.HomeStatisticsQuery, *app_errors.APIError) {
if err := validateSafeMilliseconds(observedAtMS); err != nil {
return requestlog.HomeStatisticsQuery{}, app_errors.ErrInternalServer
}
values, err := url.ParseQuery(rawQuery)
if err != nil {
return requestlog.HomeStatisticsQuery{}, app_errors.ErrBadRequest
}
for key, entries := range values {
if key != "range" || len(entries) != 1 {
return requestlog.HomeStatisticsQuery{}, app_errors.ErrBadRequest
}
}
rangeValue := requestlog.HomeStatistics24H
if entries, exists := values["range"]; exists {
rangeValue = requestlog.HomeStatisticsRange(entries[0])
}
width, count, _, ok := homeStatisticsRangeContract(rangeValue)
if !ok {
return requestlog.HomeStatisticsQuery{}, app_errors.ErrValidation
}
fromMS, toMS, err := epochms.WindowEndingAt(
observedAtMS,
width,
count,
)
if err != nil ||
validateSafeMilliseconds(fromMS) != nil ||
validateSafeMilliseconds(toMS) != nil {
return requestlog.HomeStatisticsQuery{}, app_errors.ErrInternalServer
}
return requestlog.HomeStatisticsQuery{
Range: rangeValue,
ObservedAtMS: observedAtMS,
}, nil
}
func mapHomeStatisticsResponse(
query requestlog.HomeStatisticsQuery,
report requestlog.HomeStatisticsReport,
) (homeStatisticsResponse, error) {
width, count, granularity, ok := homeStatisticsRangeContract(query.Range)
if !ok {
return homeStatisticsResponse{}, fmt.Errorf(
"map home statistics: invalid query range",
)
}
fromMS, toMS, err := epochms.WindowEndingAt(
query.ObservedAtMS,
width,
count,
)
if err != nil {
return homeStatisticsResponse{}, fmt.Errorf(
"map home statistics window: %w",
err,
)
}
if report.Range != query.Range ||
report.ObservedAtMS != query.ObservedAtMS ||
report.FromMS != fromMS ||
report.ToMS != toMS ||
report.Granularity != granularity {
return homeStatisticsResponse{}, fmt.Errorf(
"map home statistics: report metadata mismatch",
)
}
for _, value := range []int64{
report.FromMS,
report.ToMS,
report.ObservedAtMS,
} {
if err := validateSafeMilliseconds(value); err != nil {
return homeStatisticsResponse{}, fmt.Errorf(
"map home statistics time: %w",
err,
)
}
}
summary, err := mapHomeStatisticsAggregate(report.Summary)
if err != nil {
return homeStatisticsResponse{}, err
}
if len(report.Series) != count {
return homeStatisticsResponse{}, fmt.Errorf(
"map home statistics: series length mismatch",
)
}
if len(report.TopModels) > homeStatisticsRankingLimit ||
len(report.TopGroups) > homeStatisticsRankingLimit ||
len(report.TopAccessKeys) > homeStatisticsRankingLimit {
return homeStatisticsResponse{}, fmt.Errorf(
"map home statistics: ranking limit exceeded",
)
}
result := homeStatisticsResponse{
Range: report.Range,
Granularity: report.Granularity,
FromMS: report.FromMS,
ToMS: report.ToMS,
ObservedAtMS: report.ObservedAtMS,
Summary: summary,
Series: make([]homeStatisticsSeriesResponse, 0, count),
Rankings: homeStatisticsRankingsResponse{
Models: make([]homeModelRankingResponse, 0, len(report.TopModels)),
Groups: make([]homeGroupRankingResponse, 0, len(report.TopGroups)),
AccessKeys: make(
[]homeAccessKeyRankingResponse,
0,
len(report.TopAccessKeys),
),
},
}
for index, point := range report.Series {
wantStartMS := fromMS + int64(index)*width
if point.BucketStartMS != wantStartMS ||
point.BucketEndMS != wantStartMS+width {
return homeStatisticsResponse{}, fmt.Errorf(
"map home statistics: non-dense series",
)
}
if _, err := mapHomeStatisticsAggregate(point.UsageAggregate); err != nil {
return homeStatisticsResponse{}, err
}
result.Series = append(result.Series, homeStatisticsSeriesResponse{
BucketStartMS: point.BucketStartMS,
BucketEndMS: point.BucketEndMS,
RequestCount: point.RequestCount,
FailureCount: point.FailureCount,
})
}
for _, row := range report.TopModels {
aggregate, err := mapHomeStatisticsAggregate(row.UsageAggregate)
if err != nil {
return homeStatisticsResponse{}, err
}
group, err := mapHomeStatisticsRef(row.Group)
if err != nil {
return homeStatisticsResponse{}, err
}
result.Rankings.Models = append(
result.Rankings.Models,
homeModelRankingResponse{
Model: row.Model, Group: group,
RequestCount: aggregate.RequestCount,
TotalTokens: aggregate.TotalTokens,
EstimatedCostNanoUSD: aggregate.EstimatedCostNanoUSD,
},
)
}
for _, row := range report.TopGroups {
aggregate, err := mapHomeStatisticsAggregate(row.UsageAggregate)
if err != nil {
return homeStatisticsResponse{}, err
}
group, err := mapHomeStatisticsRef(row.Group)
if err != nil {
return homeStatisticsResponse{}, err
}
result.Rankings.Groups = append(
result.Rankings.Groups,
homeGroupRankingResponse{
Group: group, RequestCount: aggregate.RequestCount,
TotalTokens: aggregate.TotalTokens,
EstimatedCostNanoUSD: aggregate.EstimatedCostNanoUSD,
},
)
}
for _, row := range report.TopAccessKeys {
aggregate, err := mapHomeStatisticsAggregate(row.UsageAggregate)
if err != nil {
return homeStatisticsResponse{}, err
}
accessKey, err := mapHomeStatisticsRef(row.AccessKey)
if err != nil {
return homeStatisticsResponse{}, err
}
result.Rankings.AccessKeys = append(
result.Rankings.AccessKeys,
homeAccessKeyRankingResponse{
AccessKey: accessKey,
RequestCount: aggregate.RequestCount,
TotalTokens: aggregate.TotalTokens,
EstimatedCostNanoUSD: aggregate.EstimatedCostNanoUSD,
},
)
}
return result, nil
}
func homeStatisticsRangeContract(
value requestlog.HomeStatisticsRange,
) (int64, int, requestlog.UsageGranularity, bool) {
switch value {
case requestlog.HomeStatistics24H:
return epochms.MillisecondsPerHour, 24, requestlog.UsageGranularityHour, true
case requestlog.HomeStatistics30D:
return epochms.MillisecondsPerDay, 30, requestlog.UsageGranularityDay, true
default:
return 0, 0, "", false
}
}
func mapHomeStatisticsAggregate(
source requestlog.UsageAggregate,
) (homeStatisticsAggregateResponse, error) {
mapped, err := mapUsageAggregate(source)
if err != nil {
return homeStatisticsAggregateResponse{}, fmt.Errorf(
"map home statistics aggregate: %w",
err,
)
}
if source.SuccessCount > maxSafeInteger-source.FailureCount ||
source.SuccessCount+source.FailureCount != source.RequestCount {
return homeStatisticsAggregateResponse{}, fmt.Errorf(
"map home statistics aggregate: invalid request count",
)
}
return homeStatisticsAggregateResponse{
RequestCount: mapped.RequestCount,
SuccessCount: mapped.SuccessCount,
FailureCount: mapped.FailureCount,
TotalTokens: mapped.TotalTokens,
EstimatedCostNanoUSD: mapped.EstimatedCostNanoUSD,
UsageMissingCount: mapped.UsageMissingCount,
PartialCount: mapped.PartialCount,
UnpricedRequestCount: mapped.UnpricedRequestCount,
}, nil
}
func mapHomeStatisticsRef(
source requestlog.HomeStatisticsRef,
) (homeStatisticsRefResponse, error) {
if uint64(source.ID) > uint64(maxSafeInteger) ||
source.Deleted && source.Name != nil ||
!source.Deleted && source.Name == nil {
return homeStatisticsRefResponse{}, fmt.Errorf(
"map home statistics reference: invalid reference",
)
}
var name *string
if source.Name != nil {
cloned := *source.Name
name = &cloned
}
return homeStatisticsRefResponse{
ID: source.ID, Name: name, Deleted: source.Deleted,
}, nil
}
+616
View File
@@ -0,0 +1,616 @@
package control
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"gpt-load/internal/platform/config"
"gpt-load/internal/platform/epochms"
"gpt-load/internal/platform/version"
"gpt-load/internal/requestlog"
)
func TestHomeBaseHTTPUsesAuthenticationEnvelopeAndServerClock(t *testing.T) {
initControlI18n(t)
fixture := newServiceFixture(t)
server := NewServer(
&config.Config{AuthKey: "test-auth-key"},
fixture.service,
)
now := time.Date(2026, time.July, 30, 12, 34, 56, 789, time.UTC)
startedAt := time.Date(2026, time.July, 29, 1, 2, 3, 456, time.UTC)
server.now = func() time.Time { return now }
server.startedAt = startedAt
engine := gin.New()
server.RegisterRoutes(engine)
unauthorized := performHomeRequest(engine, "/api/home", "")
if unauthorized.Code != http.StatusUnauthorized {
t.Fatalf(
"unauthorized GET /api/home = %d %s",
unauthorized.Code,
unauthorized.Body.String(),
)
}
recorder := performHomeRequest(engine, "/api/home", "test-auth-key")
if recorder.Code != http.StatusOK {
t.Fatalf(
"GET /api/home = %d %s, want 200",
recorder.Code,
recorder.Body.String(),
)
}
var envelope struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
ServerNowMS int64 `json:"server_now_ms"`
StartedAtMS int64 `json:"started_at_ms"`
Version string `json:"version"`
Inventory HomeInventory `json:"inventory"`
AccessKeys []HomeAccessKey `json:"access_keys"`
} `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode home response: %v", err)
}
if envelope.Code != 0 || envelope.Message == "" ||
envelope.Data.ServerNowMS != now.UnixMilli() ||
envelope.Data.StartedAtMS != startedAt.UnixMilli() ||
envelope.Data.Version != version.Version ||
envelope.Data.Inventory != (HomeInventory{}) ||
envelope.Data.AccessKeys == nil ||
len(envelope.Data.AccessKeys) != 0 {
t.Fatalf("home response = %#v", envelope)
}
assertManagementWireObject(
t,
envelope.Data,
[]string{
"server_now_ms",
"started_at_ms",
"version",
"inventory",
"access_keys",
},
)
}
func TestHomeBaseHTTPRejectsEveryQueryBeforeReading(t *testing.T) {
initControlI18n(t)
fixture := newServiceFixture(t)
fixture.service.db = nil
server := NewServer(
&config.Config{AuthKey: "test-auth-key"},
fixture.service,
)
engine := gin.New()
server.RegisterRoutes(engine)
for _, target := range []string{
"/api/home?unknown=1",
"/api/home?range=24h",
"/api/home?unknown=",
} {
recorder := performHomeRequest(engine, target, "test-auth-key")
assertHomeHTTPError(t, recorder, http.StatusBadRequest, "BAD_REQUEST")
}
}
func TestParseHomeStatisticsQueryIsStrictAndDefaultsTo24Hours(t *testing.T) {
const observedAtMS = int64(1_785_412_345_678)
tests := []struct {
rawQuery string
want requestlog.HomeStatisticsRange
code string
}{
{rawQuery: "", want: requestlog.HomeStatistics24H},
{rawQuery: "range=24h", want: requestlog.HomeStatistics24H},
{rawQuery: "range=30d", want: requestlog.HomeStatistics30D},
{rawQuery: "range=", code: "VALIDATION_FAILED"},
{rawQuery: "range=1h", code: "VALIDATION_FAILED"},
{rawQuery: "range=24h&range=30d", code: "BAD_REQUEST"},
{rawQuery: "unknown=1", code: "BAD_REQUEST"},
{rawQuery: "range=24h&unknown=1", code: "BAD_REQUEST"},
}
for _, test := range tests {
t.Run(test.rawQuery, func(t *testing.T) {
query, apiErr := parseHomeStatisticsQuery(
test.rawQuery,
observedAtMS,
)
if test.code != "" {
if apiErr == nil || apiErr.Code != test.code {
t.Fatalf(
"parseHomeStatisticsQuery() error = %v, want %s",
apiErr,
test.code,
)
}
return
}
if apiErr != nil {
t.Fatalf("parseHomeStatisticsQuery() error = %v", apiErr)
}
if query.Range != test.want ||
query.ObservedAtMS != observedAtMS {
t.Fatalf("query = %#v", query)
}
})
}
for _, observedAtMS := range []int64{-1, maxSafeInteger + 1} {
if _, apiErr := parseHomeStatisticsQuery("", observedAtMS); apiErr == nil ||
apiErr.Code != "INTERNAL_SERVER_ERROR" {
t.Fatalf(
"parseHomeStatisticsQuery(observed=%d) error = %v",
observedAtMS,
apiErr,
)
}
}
}
func TestHomeStatisticsHTTPDefaultsToDense24HoursAndMapsExactWire(t *testing.T) {
now := time.Date(2026, time.July, 30, 12, 34, 56, 789, time.UTC)
currentGroupName := "current-group"
currentAccessKeyName := "current-access"
reader := &recordingHomeStatisticsReader{
fn: func(query requestlog.HomeStatisticsQuery) requestlog.HomeStatisticsReport {
report := homeStatisticsReportForQuery(t, query)
report.Summary = requestlog.UsageAggregate{
RequestCount: 8, SuccessCount: 6, FailureCount: 2,
UncachedInputTokens: 11, CacheReadTokens: 12,
CacheWrite5MTokens: 13, CacheWrite1HTokens: 14,
OutputTokens: 15, EstimatedCostNanoUSD: 58_200_000_000,
UsageMissingCount: 1, PartialCount: 2,
UnpricedRequestCount: 3,
}
report.Series[0].UsageAggregate = requestlog.UsageAggregate{
RequestCount: 4,
SuccessCount: 3,
FailureCount: 1,
}
report.TopModels = []requestlog.HomeModelRanking{{
Group: requestlog.HomeStatisticsRef{
ID: 7, Name: &currentGroupName,
},
Model: "sonnet-4.5",
UsageAggregate: requestlog.UsageAggregate{
RequestCount: 3, SuccessCount: 3, UncachedInputTokens: 20,
OutputTokens: 30, EstimatedCostNanoUSD: 18_400_000_000,
},
}}
report.TopGroups = []requestlog.HomeGroupRanking{{
Group: requestlog.HomeStatisticsRef{
ID: 9, Deleted: true,
},
UsageAggregate: requestlog.UsageAggregate{
RequestCount: 4, SuccessCount: 4, UncachedInputTokens: 40,
OutputTokens: 50, EstimatedCostNanoUSD: 24_100_000_000,
},
}}
report.TopAccessKeys = []requestlog.HomeAccessKeyRanking{{
AccessKey: requestlog.HomeStatisticsRef{
ID: 3, Name: &currentAccessKeyName,
},
UsageAggregate: requestlog.UsageAggregate{
RequestCount: 5, SuccessCount: 5, UncachedInputTokens: 60,
OutputTokens: 70, EstimatedCostNanoUSD: 28_440_000_000,
},
}}
return report
},
}
engine := newHomeStatisticsTestEngine(t, now, reader)
recorder := performHomeRequest(
engine,
"/api/home/statistics",
"test-auth-key",
)
if recorder.Code != http.StatusOK {
t.Fatalf(
"GET /api/home/statistics = %d %s",
recorder.Code,
recorder.Body.String(),
)
}
if len(reader.queries) != 1 ||
reader.queries[0].Range != requestlog.HomeStatistics24H ||
reader.queries[0].ObservedAtMS != now.UnixMilli() {
t.Fatalf("QueryHomeStatistics calls = %#v", reader.queries)
}
var envelope struct {
Code int `json:"code"`
Data struct {
Range string `json:"range"`
Granularity requestlog.UsageGranularity `json:"granularity"`
FromMS int64 `json:"from_ms"`
ToMS int64 `json:"to_ms"`
ObservedAtMS int64 `json:"observed_at_ms"`
Summary struct {
RequestCount int64 `json:"request_count"`
SuccessCount int64 `json:"success_count"`
FailureCount int64 `json:"failure_count"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
UsageMissingCount int64 `json:"usage_missing_count"`
PartialCount int64 `json:"partial_count"`
UnpricedRequestCount int64 `json:"unpriced_request_count"`
} `json:"summary"`
Series []struct {
BucketStartMS int64 `json:"bucket_start_ms"`
BucketEndMS int64 `json:"bucket_end_ms"`
RequestCount int64 `json:"request_count"`
FailureCount int64 `json:"failure_count"`
} `json:"series"`
Rankings struct {
Models []struct {
Model string `json:"model"`
Group struct {
ID uint `json:"id"`
Name *string `json:"name"`
Deleted bool `json:"deleted"`
} `json:"group"`
RequestCount int64 `json:"request_count"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
} `json:"models"`
Groups []struct {
Group struct {
ID uint `json:"id"`
Name *string `json:"name"`
Deleted bool `json:"deleted"`
} `json:"group"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
} `json:"groups"`
AccessKeys []struct {
AccessKey struct {
ID uint `json:"id"`
Name *string `json:"name"`
Deleted bool `json:"deleted"`
} `json:"access_key"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
} `json:"access_keys"`
} `json:"rankings"`
} `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode home statistics response: %v", err)
}
if envelope.Code != 0 ||
envelope.Data.Range != "24h" ||
envelope.Data.Granularity != requestlog.UsageGranularityHour ||
envelope.Data.ObservedAtMS != now.UnixMilli() ||
envelope.Data.ToMS-envelope.Data.FromMS !=
24*epochms.MillisecondsPerHour ||
envelope.Data.Summary.RequestCount != 8 ||
envelope.Data.Summary.SuccessCount != 6 ||
envelope.Data.Summary.FailureCount != 2 ||
envelope.Data.Summary.TotalTokens != 65 ||
envelope.Data.Summary.EstimatedCostNanoUSD != "58200000000" ||
envelope.Data.Summary.UsageMissingCount != 1 ||
envelope.Data.Summary.PartialCount != 2 ||
envelope.Data.Summary.UnpricedRequestCount != 3 ||
len(envelope.Data.Series) != 24 ||
envelope.Data.Series[0].RequestCount != 4 ||
envelope.Data.Series[0].FailureCount != 1 ||
len(envelope.Data.Rankings.Models) != 1 ||
envelope.Data.Rankings.Models[0].Model != "sonnet-4.5" ||
envelope.Data.Rankings.Models[0].Group.Name == nil ||
*envelope.Data.Rankings.Models[0].Group.Name != currentGroupName ||
envelope.Data.Rankings.Models[0].EstimatedCostNanoUSD != "18400000000" ||
len(envelope.Data.Rankings.Groups) != 1 ||
envelope.Data.Rankings.Groups[0].Group.ID != 9 ||
envelope.Data.Rankings.Groups[0].Group.Name != nil ||
!envelope.Data.Rankings.Groups[0].Group.Deleted ||
envelope.Data.Rankings.Groups[0].EstimatedCostNanoUSD != "24100000000" ||
len(envelope.Data.Rankings.AccessKeys) != 1 ||
envelope.Data.Rankings.AccessKeys[0].AccessKey.Name == nil ||
*envelope.Data.Rankings.AccessKeys[0].AccessKey.Name != currentAccessKeyName ||
envelope.Data.Rankings.AccessKeys[0].EstimatedCostNanoUSD != "28440000000" {
t.Fatalf("home statistics response = %#v", envelope.Data)
}
if strings.Contains(recorder.Body.String(), "2026-") {
t.Fatalf("response exposes RFC 3339 timestamp: %s", recorder.Body.String())
}
var rawEnvelope struct {
Data map[string]json.RawMessage `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &rawEnvelope); err != nil {
t.Fatalf("decode raw home statistics response: %v", err)
}
for _, forbidden := range []string{"health", "timezone", "link"} {
if _, exists := rawEnvelope.Data[forbidden]; exists {
t.Fatalf(
"home statistics response exposes %q: %s",
forbidden,
recorder.Body.String(),
)
}
}
assertManagementWireObject(
t,
envelope.Data,
[]string{
"range",
"granularity",
"from_ms",
"to_ms",
"observed_at_ms",
"summary",
"series",
"rankings",
},
)
}
func TestHomeStatisticsHTTPAcceptsOnlyOneSupportedRange(t *testing.T) {
now := time.Date(2026, time.July, 30, 12, 0, 0, 0, time.UTC)
reader := &recordingHomeStatisticsReader{}
engine := newHomeStatisticsTestEngine(t, now, reader)
recorder := performHomeRequest(
engine,
"/api/home/statistics?range=30d",
"test-auth-key",
)
if recorder.Code != http.StatusOK {
t.Fatalf("30d response = %d %s", recorder.Code, recorder.Body.String())
}
if len(reader.queries) != 1 ||
reader.queries[0].Range != requestlog.HomeStatistics30D {
t.Fatalf("30d queries = %#v", reader.queries)
}
var response struct {
Data struct {
Granularity requestlog.UsageGranularity `json:"granularity"`
Series []json.RawMessage `json:"series"`
} `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("decode 30d response: %v", err)
}
if response.Data.Granularity != requestlog.UsageGranularityDay ||
len(response.Data.Series) != 30 {
t.Fatalf("30d response = %#v", response.Data)
}
validCalls := len(reader.queries)
for _, target := range []string{
"/api/home/statistics?unknown=1",
"/api/home/statistics?range=24h&range=30d",
"/api/home/statistics?range=1h",
"/api/home/statistics?range=",
} {
recorder := performHomeRequest(engine, target, "test-auth-key")
if recorder.Code != http.StatusBadRequest {
t.Fatalf(
"%s response = %d %s",
target,
recorder.Code,
recorder.Body.String(),
)
}
}
if len(reader.queries) != validCalls {
t.Fatalf(
"QueryHomeStatistics calls = %d after invalid queries, want %d",
len(reader.queries),
validCalls,
)
}
}
func TestHomeStatisticsHTTPFailsClosedOnUnsafeDataAndReadError(t *testing.T) {
now := time.Date(2026, time.July, 30, 12, 0, 0, 0, time.UTC)
t.Run("unsafe aggregate", func(t *testing.T) {
reader := &recordingHomeStatisticsReader{
fn: func(query requestlog.HomeStatisticsQuery) requestlog.HomeStatisticsReport {
report := homeStatisticsReportForQuery(t, query)
report.Summary.RequestCount = maxSafeInteger + 1
return report
},
}
recorder := performHomeRequest(
newHomeStatisticsTestEngine(t, now, reader),
"/api/home/statistics",
"test-auth-key",
)
assertHomeHTTPError(
t,
recorder,
http.StatusInternalServerError,
"INTERNAL_SERVER_ERROR",
)
})
t.Run("too many rankings", func(t *testing.T) {
reader := &recordingHomeStatisticsReader{
fn: func(query requestlog.HomeStatisticsQuery) requestlog.HomeStatisticsReport {
report := homeStatisticsReportForQuery(t, query)
report.TopGroups = make([]requestlog.HomeGroupRanking, 6)
return report
},
}
recorder := performHomeRequest(
newHomeStatisticsTestEngine(t, now, reader),
"/api/home/statistics",
"test-auth-key",
)
assertHomeHTTPError(
t,
recorder,
http.StatusInternalServerError,
"INTERNAL_SERVER_ERROR",
)
})
t.Run("reader error", func(t *testing.T) {
reader := &recordingHomeStatisticsReader{
err: errors.New("private database detail"),
}
recorder := performHomeRequest(
newHomeStatisticsTestEngine(t, now, reader),
"/api/home/statistics",
"test-auth-key",
)
assertHomeHTTPError(
t,
recorder,
http.StatusInternalServerError,
"DATABASE_ERROR",
)
if strings.Contains(recorder.Body.String(), "private database detail") {
t.Fatalf("response leaks reader error: %s", recorder.Body.String())
}
})
}
type recordingHomeStatisticsReader struct {
queries []requestlog.HomeStatisticsQuery
report requestlog.HomeStatisticsReport
fn func(requestlog.HomeStatisticsQuery) requestlog.HomeStatisticsReport
err error
}
func (reader *recordingHomeStatisticsReader) QueryHomeStatistics(
_ context.Context,
query requestlog.HomeStatisticsQuery,
) (requestlog.HomeStatisticsReport, error) {
reader.queries = append(reader.queries, query)
if reader.err != nil {
return requestlog.HomeStatisticsReport{}, reader.err
}
if reader.fn != nil {
return reader.fn(query), nil
}
if reader.report.Range != "" {
return reader.report, nil
}
return homeStatisticsReportForQuery(nil, query), nil
}
func homeStatisticsReportForQuery(
t *testing.T,
query requestlog.HomeStatisticsQuery,
) requestlog.HomeStatisticsReport {
if t != nil {
t.Helper()
}
var width int64
var count int
var granularity requestlog.UsageGranularity
switch query.Range {
case requestlog.HomeStatistics24H:
width = epochms.MillisecondsPerHour
count = 24
granularity = requestlog.UsageGranularityHour
case requestlog.HomeStatistics30D:
width = epochms.MillisecondsPerDay
count = 30
granularity = requestlog.UsageGranularityDay
default:
if t != nil {
t.Fatalf("unsupported test range %q", query.Range)
}
return requestlog.HomeStatisticsReport{}
}
fromMS, toMS, err := epochms.WindowEndingAt(
query.ObservedAtMS,
width,
count,
)
if err != nil {
if t != nil {
t.Fatalf("WindowEndingAt() error = %v", err)
}
return requestlog.HomeStatisticsReport{}
}
report := requestlog.HomeStatisticsReport{
Range: query.Range,
ObservedAtMS: query.ObservedAtMS,
FromMS: fromMS,
ToMS: toMS,
Granularity: granularity,
Series: make([]requestlog.UsageSeriesPoint, 0, count),
TopModels: []requestlog.HomeModelRanking{},
TopGroups: []requestlog.HomeGroupRanking{},
TopAccessKeys: []requestlog.HomeAccessKeyRanking{},
}
for startMS := fromMS; startMS < toMS; startMS += width {
report.Series = append(report.Series, requestlog.UsageSeriesPoint{
BucketStartMS: startMS,
BucketEndMS: startMS + width,
})
}
return report
}
func newHomeStatisticsTestEngine(
t *testing.T,
now time.Time,
reader *recordingHomeStatisticsReader,
) *gin.Engine {
t.Helper()
initControlI18n(t)
fixture := newServiceFixture(t)
fixture.service.homeStatistics = reader
server := NewServer(
&config.Config{AuthKey: "test-auth-key"},
fixture.service,
)
server.now = func() time.Time { return now }
engine := gin.New()
server.RegisterRoutes(engine)
return engine
}
func performHomeRequest(
engine *gin.Engine,
target string,
authKey string,
) *httptest.ResponseRecorder {
request := httptest.NewRequest(http.MethodGet, target, nil)
if authKey != "" {
request.Header.Set("Authorization", "Bearer "+authKey)
}
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
return recorder
}
func assertHomeHTTPError(
t *testing.T,
recorder *httptest.ResponseRecorder,
status int,
code string,
) {
t.Helper()
var envelope struct {
Code string `json:"code"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode error response: %v; body=%s", err, recorder.Body.String())
}
if recorder.Code != status || envelope.Code != code {
t.Fatalf(
"error response = %d/%q, want %d/%q; body=%s",
recorder.Code,
envelope.Code,
status,
code,
recorder.Body.String(),
)
}
}
+307
View File
@@ -0,0 +1,307 @@
package control
import (
"context"
"reflect"
"sync/atomic"
"testing"
"time"
"gorm.io/gorm"
"gpt-load/internal/protocol"
"gpt-load/internal/state"
stateloader "gpt-load/internal/state/loader"
"gpt-load/internal/storage"
"gpt-load/internal/storage/models"
)
func TestReadHomeBaseUsesPersistedAndRuntimeSnapshots(t *testing.T) {
fixture := newServiceFixture(t)
now := time.Date(2026, time.July, 30, 12, 0, 0, 0, time.UTC)
nowMS := now.UnixMilli()
enabled := validControlGroup("home-enabled")
enabled.Models = models.JSON(`[
{"id":"gpt-4o","alias":"client-primary"},
{"id":"client-secondary"},
{"id":"ignored-empty","alias":" "}
]`)
enabledTwo := validControlGroup("home-enabled-two")
enabledTwo.Models = models.JSON(`[
{"id":"upstream-duplicate","alias":"client-primary"},
{"id":"client-secondary"},
{"id":"client-third","alias":"client-third"}
]`)
disabled := validControlGroup("home-disabled")
disabled.Enabled = false
disabled.Models = models.JSON(`[{"id":"disabled-model"}]`)
for _, group := range []*models.Group{enabled, enabledTwo, disabled} {
if err := fixture.db.Create(group).Error; err != nil {
t.Fatalf("create group %q: %v", group.Name, err)
}
}
if err := fixture.db.Model(disabled).Update("enabled", false).Error; err != nil {
t.Fatalf("disable group: %v", err)
}
keys := []models.UpstreamKey{
{
ID: 1, GroupID: enabled.ID, KeyValue: "cipher-1", KeyHash: "hash-1",
Status: models.UpstreamKeyStatusActive,
},
{
ID: 2, GroupID: enabled.ID, KeyValue: "cipher-2", KeyHash: "hash-2",
Status: models.UpstreamKeyStatusActive,
},
{
ID: 3, GroupID: enabledTwo.ID, KeyValue: "cipher-3", KeyHash: "hash-3",
Status: models.UpstreamKeyStatusDisabled,
},
{
ID: 4, GroupID: disabled.ID, KeyValue: "cipher-4", KeyHash: "hash-4",
Status: models.UpstreamKeyStatusActive,
},
}
if err := fixture.db.Create(&keys).Error; err != nil {
t.Fatalf("create upstream keys: %v", err)
}
if err := fixture.registry.Replace([]state.KeyEntry{
{
ID: 1, GroupID: enabled.ID, Status: state.KeyStatusActive,
EncryptedValue: "cipher-1",
},
{
ID: 2, GroupID: enabled.ID, Status: state.KeyStatusActive,
CooldownUntil: now.Add(time.Hour), EncryptedValue: "cipher-2",
},
{
ID: 3, GroupID: enabledTwo.ID, Status: state.KeyStatusDisabled,
EncryptedValue: "cipher-3",
},
{
ID: 4, GroupID: disabled.ID, Status: state.KeyStatusActive,
EncryptedValue: "cipher-4",
},
}); err != nil {
t.Fatalf("registry.Replace() error = %v", err)
}
accessKeys := []models.AccessKey{
{
ID: 9, Name: "all protocols", KeyValue: "cipher-access-9",
KeyHash: "access-hash-9", KeySuffix: "c0de",
Status: string(state.AccessKeyStatusActive),
Filters: models.JSON(
`{"groups":[],"protocols":[],"models":[]}`,
),
},
{
ID: 3, Name: "filtered", KeyValue: "cipher-access-3",
KeyHash: "access-hash-3", KeySuffix: "88ab",
Status: string(state.AccessKeyStatusActive),
Filters: models.JSON(
`{"groups":[],"protocols":["gemini","openai-completions","gemini"],"models":[]}`,
),
},
{
ID: 5, Name: "disabled", KeyValue: "cipher-access-5",
KeyHash: "access-hash-5", KeySuffix: "dead",
Status: string(state.AccessKeyStatusDisabled),
Filters: models.JSON(
`{"groups":[],"protocols":[],"models":[]}`,
),
},
}
if err := fixture.db.Create(&accessKeys).Error; err != nil {
t.Fatalf("create access keys: %v", err)
}
input, err := stateloader.BuildCompileInput(context.Background(), fixture.db)
if err != nil {
t.Fatalf("BuildCompileInput() error = %v", err)
}
if _, err := fixture.manager.Publish(input); err != nil {
t.Fatalf("manager.Publish() error = %v", err)
}
registrySnapshotCalls := 0
fixture.service.registrySnapshot = func() []state.KeyRuntimeView {
registrySnapshotCalls++
return fixture.registry.Snapshot()
}
got, err := fixture.service.ReadHomeBase(context.Background(), nowMS)
if err != nil {
t.Fatalf("ReadHomeBase() error = %v", err)
}
want := HomeBase{
Inventory: HomeInventory{
GroupCount: 3,
UpstreamKeyCount: 4,
AvailableUpstreamKeyCount: 1,
ModelCount: 4,
},
AccessKeys: []HomeAccessKey{
{
ID: 3, Name: "filtered",
MaskedKey: "sk-gl-****88ab",
Protocols: []protocol.Protocol{
protocol.OpenAICompletions,
protocol.Gemini,
},
},
{
ID: 9, Name: "all protocols",
MaskedKey: "sk-gl-****c0de",
Protocols: protocol.DataPlaneProtocols(),
},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ReadHomeBase() = %#v, want %#v", got, want)
}
if registrySnapshotCalls != 1 {
t.Fatalf("registry Snapshot calls = %d, want 1", registrySnapshotCalls)
}
}
func TestReadHomeBaseFailsClosed(t *testing.T) {
t.Run("invalid time", func(t *testing.T) {
fixture := newServiceFixture(t)
for _, nowMS := range []int64{-1, maxSafeInteger + 1} {
if _, err := fixture.service.ReadHomeBase(
context.Background(),
nowMS,
); err == nil {
t.Fatalf("ReadHomeBase(nowMS=%d) error = nil", nowMS)
}
}
})
t.Run("invalid stored filter", func(t *testing.T) {
fixture := newServiceFixture(t)
row := models.AccessKey{
Name: "corrupt", KeyValue: "cipher", KeyHash: "corrupt-hash",
KeySuffix: "cafe", Status: string(state.AccessKeyStatusActive),
Filters: models.JSON(`{"unknown":[]}`),
}
if err := fixture.db.Create(&row).Error; err != nil {
t.Fatalf("create access key: %v", err)
}
if _, err := fixture.service.ReadHomeBase(
context.Background(),
1,
); err == nil {
t.Fatal("ReadHomeBase() error = nil")
}
})
t.Run("query failure", func(t *testing.T) {
fixture := newServiceFixture(t)
fixture.service.db = nil
if _, err := fixture.service.ReadHomeBase(
context.Background(),
1,
); err == nil {
t.Fatal("ReadHomeBase() error = nil")
}
})
t.Run("unsafe inventory", func(t *testing.T) {
if err := validateHomeInventory(HomeInventory{
GroupCount: maxSafeInteger + 1,
}); err == nil {
t.Fatal("validateHomeInventory() error = nil")
}
})
}
func TestReadHomeBaseKeepsDatabaseRowsInOneReadSnapshot(t *testing.T) {
fixture, dsn := newFileServiceFixture(t)
group := validControlGroup("home-snapshot")
if err := fixture.db.Create(group).Error; err != nil {
t.Fatalf("create group: %v", err)
}
input, err := stateloader.BuildCompileInput(t.Context(), fixture.db)
if err != nil {
t.Fatalf("BuildCompileInput() error = %v", err)
}
if _, err := fixture.manager.Publish(input); err != nil {
t.Fatalf("manager.Publish() error = %v", err)
}
writer, err := storage.Open(dsn)
if err != nil {
t.Fatalf("storage.Open(writer) error = %v", err)
}
writerSQL, err := writer.DB()
if err != nil {
t.Fatalf("writer.DB() error = %v", err)
}
t.Cleanup(func() {
if err := writerSQL.Close(); err != nil {
t.Errorf("close writer database: %v", err)
}
})
var held atomic.Bool
firstSelectDone := make(chan struct{})
releaseRead := make(chan struct{})
const callbackName = "test:hold-home-after-group-count"
if err := fixture.db.Callback().Query().
After("gorm:query").
Register(callbackName, func(tx *gorm.DB) {
if tx.Statement.Table == "groups" && held.CompareAndSwap(false, true) {
close(firstSelectDone)
<-releaseRead
}
}); err != nil {
t.Fatalf("register query callback: %v", err)
}
t.Cleanup(func() {
fixture.db.Callback().Query().Remove(callbackName)
})
type readResult struct {
value HomeBase
err error
}
readDone := make(chan readResult, 1)
go func() {
value, err := fixture.service.ReadHomeBase(t.Context(), 1)
readDone <- readResult{value: value, err: err}
}()
select {
case <-firstSelectDone:
case <-time.After(time.Second):
t.Fatal("ReadHomeBase did not complete the first SELECT")
}
newAccessKey := models.AccessKey{
Name: "newer", KeyValue: "cipher", KeyHash: "newer-hash",
KeySuffix: "abcd", Status: string(state.AccessKeyStatusActive),
Filters: models.JSON(
`{"groups":[],"protocols":[],"models":[]}`,
),
}
if err := writer.Create(&newAccessKey).Error; err != nil {
t.Fatalf("WAL writer create access key: %v", err)
}
close(releaseRead)
select {
case result := <-readDone:
if result.err != nil {
t.Fatalf("ReadHomeBase() error = %v", result.err)
}
if result.value.Inventory.GroupCount != 1 ||
len(result.value.AccessKeys) != 0 {
t.Fatalf(
"ReadHomeBase() mixed database versions: %#v",
result.value,
)
}
case <-time.After(time.Second):
t.Fatal("ReadHomeBase did not finish")
}
}
+7
View File
@@ -43,6 +43,13 @@ func (s *Server) HTTPModule() httproute.Module {
MethodNotAllowed: controlMethodNotAllowed,
Routes: []httproute.Route{
controlRoute("control.auth.session", http.MethodGet, "/auth/session", s.handleAuthSession),
controlRoute("control.home", http.MethodGet, "/home", s.handleHome),
controlRoute(
"control.home.statistics",
http.MethodGet,
"/home/statistics",
s.handleHomeStatistics,
),
controlRoute("control.health", http.MethodGet, "/health", s.handleRuntimeHealth),
controlRoute("control.logs.list", http.MethodGet, "/logs", s.handleListRequestLogs),
controlRoute("control.usage", http.MethodGet, "/usage", s.handleUsage),
+30 -18
View File
@@ -9,11 +9,11 @@ import (
"errors"
"fmt"
"io"
"time"
"gorm.io/gorm"
"gpt-load/internal/platform/canonicaljson"
"gpt-load/internal/platform/epochms"
app_errors "gpt-load/internal/platform/errors"
stateloader "gpt-load/internal/state/loader"
"gpt-load/internal/storage/models"
@@ -61,7 +61,7 @@ type operationExpiredData struct {
OperationID string `json:"operation_id"`
OperationKind operationKind `json:"operation_kind"`
ResourceIdentity string `json:"resource_identity"`
CompletedAt time.Time `json:"completed_at"`
CompletedAtMS int64 `json:"completed_at_ms"`
}
func operationRequiredStages(kind operationKind) ([]operationStage, error) {
@@ -205,7 +205,10 @@ func (s *Service) executeIdempotentOperation(
app_errors.ErrInternalServer,
)
}
now := s.now().UTC()
nowMS, timeErr := epochms.FromTime(s.now())
if timeErr != nil {
return app_errors.ErrInternalServer
}
operation = models.ControlOperation{
OperationID: operationID,
IdempotencyKey: input.IdempotencyKey,
@@ -216,8 +219,8 @@ func (s *Service) executeIdempotentOperation(
CanonicalResult: append([]byte(nil), mutationResult.CanonicalResult...),
RequiredStages: models.JSON(encodedStages),
LastCompletedStage: string(operationStageDBCommitted),
CreatedAt: now,
UpdatedAt: now,
CreatedAtMS: nowMS,
UpdatedAtMS: nowMS,
}
if err := tx.Create(&operation).Error; err != nil {
return app_errors.ParseDBError(err)
@@ -248,8 +251,11 @@ func (s *Service) replayIdempotentOperationLocked(
if err := validateOperationComparator(operation, input); err != nil {
return idempotentOperationResult{}, err
}
if operation.CompactedAt != nil {
if operation.CompletedAt == nil {
if operation.CompactedAtMS != nil {
if operation.CompletedAtMS == nil {
return idempotentOperationResult{}, app_errors.ErrInternalServer
}
if err := validateSafeMilliseconds(*operation.CompletedAtMS); err != nil {
return idempotentOperationResult{}, app_errors.ErrInternalServer
}
return idempotentOperationResult{}, app_errors.NewAPIErrorWithData(
@@ -258,7 +264,7 @@ func (s *Service) replayIdempotentOperationLocked(
OperationID: operation.OperationID,
OperationKind: operationKind(operation.OperationKind),
ResourceIdentity: operation.ResourceIdentity,
CompletedAt: operation.CompletedAt.UTC(),
CompletedAtMS: *operation.CompletedAtMS,
},
)
}
@@ -268,7 +274,7 @@ func (s *Service) replayIdempotentOperationLocked(
return idempotentOperationResult{}, s.operationIncompleteError(*operation)
}
}
if operation.CompletedAt == nil || len(operation.CanonicalResult) == 0 {
if operation.CompletedAtMS == nil || len(operation.CanonicalResult) == 0 {
return idempotentOperationResult{}, app_errors.ErrInternalServer
}
return idempotentOperationResult{
@@ -392,14 +398,17 @@ func (s *Service) advanceOperationStageLocked(
}
}
previous := operation.LastCompletedStage
now := s.now().UTC()
nowMS, timeErr := epochms.FromTime(s.now())
if timeErr != nil {
return app_errors.ErrInternalServer
}
updates := map[string]any{
"last_completed_stage": string(stage),
"failed_stage": "",
"updated_at": now,
"updated_at_ms": nowMS,
}
if stage == operationStageCompleted {
updates["completed_at"] = now
updates["completed_at_ms"] = nowMS
}
err := s.withControlTransaction(ctx, func(tx *gorm.DB) error {
result := tx.Model(&models.ControlOperation{}).
@@ -422,9 +431,9 @@ func (s *Service) advanceOperationStageLocked(
}
operation.LastCompletedStage = string(stage)
operation.FailedStage = ""
operation.UpdatedAt = now
operation.UpdatedAtMS = nowMS
if stage == operationStageCompleted {
operation.CompletedAt = &now
operation.CompletedAtMS = &nowMS
}
return nil
}
@@ -434,18 +443,21 @@ func (s *Service) recordOperationFailureLocked(
operation *models.ControlOperation,
stage operationStage,
) error {
now := s.now().UTC()
nowMS, timeErr := epochms.FromTime(s.now())
if timeErr != nil {
return app_errors.ErrInternalServer
}
err := s.withControlTransaction(ctx, func(tx *gorm.DB) error {
return tx.Model(&models.ControlOperation{}).
Where("commit_sequence = ?", operation.CommitSequence).
Updates(map[string]any{
"failed_stage": string(stage),
"updated_at": now,
"failed_stage": string(stage),
"updated_at_ms": nowMS,
}).Error
})
if err == nil {
operation.FailedStage = string(stage)
operation.UpdatedAt = now
operation.UpdatedAtMS = nowMS
}
return err
}
@@ -159,7 +159,7 @@ func TestExecuteIdempotentOperationCommitsResultAndReplaysWithoutMutation(t *tes
}
if row.CommitSequence == 0 || row.ResourceIdentity != "access-key:7" ||
row.LastCompletedStage != string(operationStageCompleted) ||
row.CompletedAt == nil || row.CompactedAt != nil {
row.CompletedAtMS == nil || row.CompactedAtMS != nil {
t.Fatalf("operation row = %#v", row)
}
var stages []operationStage
+81 -36
View File
@@ -8,6 +8,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gpt-load/internal/platform/epochms"
app_errors "gpt-load/internal/platform/errors"
"gpt-load/internal/pricing"
"gpt-load/internal/storage/models"
@@ -15,11 +16,11 @@ import (
type ModelPriceInput struct {
Pattern string
UncachedInput *float64
CacheRead *float64
CacheWrite5M *float64
CacheWrite1H *float64
Output *float64
UncachedInput *pricing.NanoUSD
CacheRead *pricing.NanoUSD
CacheWrite5M *pricing.NanoUSD
CacheWrite1H *pricing.NanoUSD
Output *pricing.NanoUSD
}
func (s *Service) UpsertModelPrice(ctx context.Context, input ModelPriceInput) error {
@@ -27,27 +28,30 @@ func (s *Service) UpsertModelPrice(ctx context.Context, input ModelPriceInput) e
return app_errors.ErrValidation
}
return s.writePriceTable(ctx, func(tx *gorm.DB) error {
now := s.now()
row := models.ModelPrice{
Pattern: input.Pattern,
InputPrice: input.UncachedInput,
OutputPrice: input.Output,
CacheReadPrice: input.CacheRead,
CacheWrite5MPrice: input.CacheWrite5M,
CacheWrite1HPrice: input.CacheWrite1H,
Source: string(pricing.SourceUser),
UpdatedAt: now,
nowMS, err := safeEpochMilliseconds(s.now())
if err != nil {
return app_errors.ErrInternalServer
}
err := tx.Clauses(clause.OnConflict{
row := models.ModelPrice{
Pattern: input.Pattern,
InputPriceNanoUSDPerMillionTokens: nanoUSDStoragePointer(input.UncachedInput),
OutputPriceNanoUSDPerMillionTokens: nanoUSDStoragePointer(input.Output),
CacheReadPriceNanoUSDPerMillionTokens: nanoUSDStoragePointer(input.CacheRead),
CacheWrite5MPriceNanoUSDPerMillionTokens: nanoUSDStoragePointer(input.CacheWrite5M),
CacheWrite1HPriceNanoUSDPerMillionTokens: nanoUSDStoragePointer(input.CacheWrite1H),
Source: string(pricing.SourceUser),
UpdatedAtMS: nowMS,
}
err = tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "pattern"}},
DoUpdates: clause.Assignments(map[string]any{
"input_price": input.UncachedInput,
"output_price": input.Output,
"cache_read_price": input.CacheRead,
"cache_write_5m_price": input.CacheWrite5M,
"cache_write_1h_price": input.CacheWrite1H,
"source": string(pricing.SourceUser),
"updated_at": now,
"input_price_nano_usd_per_million_tokens": nanoUSDStoragePointer(input.UncachedInput),
"output_price_nano_usd_per_million_tokens": nanoUSDStoragePointer(input.Output),
"cache_read_price_nano_usd_per_million_tokens": nanoUSDStoragePointer(input.CacheRead),
"cache_write_5m_price_nano_usd_per_million_tokens": nanoUSDStoragePointer(input.CacheWrite5M),
"cache_write_1h_price_nano_usd_per_million_tokens": nanoUSDStoragePointer(input.CacheWrite1H),
"source": string(pricing.SourceUser),
"updated_at_ms": nowMS,
}),
}).Create(&row).Error
if err != nil {
@@ -88,13 +92,25 @@ func (s *Service) ListModelPrices(ctx context.Context) (modelPriceListResponse,
Overrides: make([]modelPriceRuleResponse, 0, len(rows)),
}
for _, rule := range builtinRules {
result.Builtin = append(result.Builtin, newModelPriceRuleResponse(rule))
response, err := newModelPriceRuleResponse(rule)
if err != nil {
return modelPriceListResponse{}, err
}
result.Builtin = append(result.Builtin, response)
}
for _, row := range rows {
if row.Source != string(pricing.SourceUser) {
return modelPriceListResponse{}, app_errors.ErrInternalServer
}
result.Overrides = append(result.Overrides, newModelPriceRuleResponse(modelPriceRule(row)))
rule, err := modelPriceRule(row)
if err != nil {
return modelPriceListResponse{}, err
}
response, err := newModelPriceRuleResponse(rule)
if err != nil {
return modelPriceListResponse{}, err
}
result.Overrides = append(result.Overrides, response)
}
return result, nil
}
@@ -139,7 +155,11 @@ func loadPriceTable(ctx context.Context, tx *gorm.DB) (*pricing.Table, error) {
app_errors.ErrInternalServer,
)
}
rules = append(rules, modelPriceRule(row))
rule, err := modelPriceRule(row)
if err != nil {
return nil, err
}
rules = append(rules, rule)
}
table, err := pricing.Compile(rules)
if err != nil {
@@ -162,25 +182,50 @@ func modelPriceInputRule(input ModelPriceInput) pricing.Rule {
}
}
func modelPriceRule(row models.ModelPrice) pricing.Rule {
func modelPriceRule(row models.ModelPrice) (pricing.Rule, error) {
updatedAt, err := epochms.ToTime(row.UpdatedAtMS)
if err != nil {
return pricing.Rule{}, fmt.Errorf(
"validate persisted model price time: %w",
app_errors.ErrInternalServer,
)
}
return pricing.Rule{
Pattern: row.Pattern,
Prices: pricing.Prices{
UncachedInput: priceFromPointer(row.InputPrice),
CacheRead: priceFromPointer(row.CacheReadPrice),
CacheWrite5M: priceFromPointer(row.CacheWrite5MPrice),
CacheWrite1H: priceFromPointer(row.CacheWrite1HPrice),
Output: priceFromPointer(row.OutputPrice),
UncachedInput: priceFromStoragePointer(row.InputPriceNanoUSDPerMillionTokens),
CacheRead: priceFromStoragePointer(row.CacheReadPriceNanoUSDPerMillionTokens),
CacheWrite5M: priceFromStoragePointer(row.CacheWrite5MPriceNanoUSDPerMillionTokens),
CacheWrite1H: priceFromStoragePointer(row.CacheWrite1HPriceNanoUSDPerMillionTokens),
Output: priceFromStoragePointer(row.OutputPriceNanoUSDPerMillionTokens),
},
Source: pricing.SourceUser,
SourceURL: "",
UpdatedAt: row.UpdatedAt,
}
UpdatedAt: updatedAt,
}, nil
}
func priceFromPointer(value *float64) pricing.Price {
func priceFromPointer(value *pricing.NanoUSD) pricing.Price {
if value == nil {
return pricing.Price{}
}
return pricing.Price{Value: *value, Set: true}
return pricing.Price{NanoUSDPerMillion: *value, Set: true}
}
func priceFromStoragePointer(value *int64) pricing.Price {
if value == nil {
return pricing.Price{}
}
return pricing.Price{
NanoUSDPerMillion: pricing.NanoUSD(*value),
Set: true,
}
}
func nanoUSDStoragePointer(value *pricing.NanoUSD) *int64 {
if value == nil {
return nil
}
result := int64(*value)
return &result
}
+50 -56
View File
@@ -3,7 +3,6 @@ package control
import (
"context"
"errors"
"math"
"strings"
"sync"
"testing"
@@ -26,16 +25,16 @@ func TestPriceRuntimePublishesImmutableTablesAtomically(t *testing.T) {
oldTable := mustCompilePriceTable(t, pricing.Rule{
Pattern: "atomic-model",
Prices: pricing.Prices{
UncachedInput: pricing.Price{Value: 1, Set: true},
Output: pricing.Price{Value: 2, Set: true},
UncachedInput: pricing.Price{NanoUSDPerMillion: 1, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 2, Set: true},
},
Source: pricing.SourceUser,
})
newTable := mustCompilePriceTable(t, pricing.Rule{
Pattern: "atomic-model",
Prices: pricing.Prices{
UncachedInput: pricing.Price{Value: 10, Set: true},
Output: pricing.Price{Value: 20, Set: true},
UncachedInput: pricing.Price{NanoUSDPerMillion: 10, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 20, Set: true},
},
Source: pricing.SourceUser,
})
@@ -63,8 +62,8 @@ func TestPriceRuntimePublishesImmutableTablesAtomically(t *testing.T) {
errs <- "published table did not match"
return
}
input := rule.Prices.UncachedInput.Value
output := rule.Prices.Output.Value
input := rule.Prices.UncachedInput.NanoUSDPerMillion
output := rule.Prices.Output.NanoUSDPerMillion
if (input != 1 || output != 2) && (input != 10 || output != 20) {
errs <- "reader observed a partial table"
return
@@ -103,7 +102,9 @@ func TestUpsertModelPricePublishesCompleteUserOverride(t *testing.T) {
fixture := newServiceFixture(t)
mustEnsureInitialPrices(t, fixture)
input, cacheRead, cacheWrite5M, cacheWrite1H, output := 9.1, 9.2, 9.3, 9.4, 9.5
input, cacheRead, cacheWrite5M, cacheWrite1H, output := pricing.NanoUSD(9_100_000_000),
pricing.NanoUSD(9_200_000_000), pricing.NanoUSD(9_300_000_000),
pricing.NanoUSD(9_400_000_000), pricing.NanoUSD(9_500_000_000)
err := fixture.service.UpsertModelPrice(context.Background(), ModelPriceInput{
Pattern: "gpt-*",
UncachedInput: &input,
@@ -122,11 +123,11 @@ func TestUpsertModelPricePublishesCompleteUserOverride(t *testing.T) {
t.Fatal("published PriceTable missed gpt-4o")
}
wantPrices := pricing.Prices{
UncachedInput: pricing.Price{Value: input, Set: true},
CacheRead: pricing.Price{Value: cacheRead, Set: true},
CacheWrite5M: pricing.Price{Value: cacheWrite5M, Set: true},
CacheWrite1H: pricing.Price{Value: cacheWrite1H, Set: true},
Output: pricing.Price{Value: output, Set: true},
UncachedInput: pricing.Price{NanoUSDPerMillion: input, Set: true},
CacheRead: pricing.Price{NanoUSDPerMillion: cacheRead, Set: true},
CacheWrite5M: pricing.Price{NanoUSDPerMillion: cacheWrite5M, Set: true},
CacheWrite1H: pricing.Price{NanoUSDPerMillion: cacheWrite1H, Set: true},
Output: pricing.Price{NanoUSDPerMillion: output, Set: true},
}
if rule.Source != pricing.SourceUser || rule.Pattern != "gpt-*" ||
rule.Prices != wantPrices {
@@ -146,7 +147,7 @@ func TestUpsertModelPriceWritesNullAndExplicitZero(t *testing.T) {
fixture := newServiceFixture(t)
mustEnsureInitialPrices(t, fixture)
first := 1.0
first := pricing.NanoUSD(1_000_000_000)
if err := fixture.service.UpsertModelPrice(context.Background(), ModelPriceInput{
Pattern: "round-trip-model",
UncachedInput: &first,
@@ -158,8 +159,8 @@ func TestUpsertModelPriceWritesNullAndExplicitZero(t *testing.T) {
t.Fatalf("first UpsertModelPrice() error = %v", err)
}
zero := 0.0
output := 2.0
zero := pricing.NanoUSD(0)
output := pricing.NanoUSD(2_000_000_000)
if err := fixture.service.UpsertModelPrice(context.Background(), ModelPriceInput{
Pattern: "round-trip-model",
UncachedInput: nil,
@@ -175,53 +176,44 @@ func TestUpsertModelPriceWritesNullAndExplicitZero(t *testing.T) {
if err := fixture.db.Where("pattern = ?", "round-trip-model").Take(&row).Error; err != nil {
t.Fatalf("query round-trip ModelPrice: %v", err)
}
if row.InputPrice != nil || row.CacheWrite5MPrice != nil {
t.Fatalf("nullable columns = input:%v cache_write_5m:%v, want nil", row.InputPrice, row.CacheWrite5MPrice)
if row.InputPriceNanoUSDPerMillionTokens != nil ||
row.CacheWrite5MPriceNanoUSDPerMillionTokens != nil {
t.Fatalf(
"nullable columns = input:%v cache_write_5m:%v, want nil",
row.InputPriceNanoUSDPerMillionTokens,
row.CacheWrite5MPriceNanoUSDPerMillionTokens,
)
}
if row.CacheReadPrice == nil || *row.CacheReadPrice != 0 ||
row.CacheWrite1HPrice == nil || *row.CacheWrite1HPrice != 0 ||
row.OutputPrice == nil || *row.OutputPrice != output {
if row.CacheReadPriceNanoUSDPerMillionTokens == nil ||
*row.CacheReadPriceNanoUSDPerMillionTokens != 0 ||
row.CacheWrite1HPriceNanoUSDPerMillionTokens == nil ||
*row.CacheWrite1HPriceNanoUSDPerMillionTokens != 0 ||
row.OutputPriceNanoUSDPerMillionTokens == nil ||
*row.OutputPriceNanoUSDPerMillionTokens != int64(output) {
t.Fatalf("explicit numeric columns = %+v", row)
}
rule, ok := fixture.priceRuntime.Load().Match("round-trip-model")
if !ok || rule.Prices.UncachedInput.Set || rule.Prices.CacheWrite5M.Set ||
!rule.Prices.CacheRead.Set || rule.Prices.CacheRead.Value != 0 ||
!rule.Prices.CacheWrite1H.Set || rule.Prices.CacheWrite1H.Value != 0 {
!rule.Prices.CacheRead.Set || rule.Prices.CacheRead.NanoUSDPerMillion != 0 ||
!rule.Prices.CacheWrite1H.Set || rule.Prices.CacheWrite1H.NanoUSDPerMillion != 0 {
t.Fatalf("published nullable/zero prices = %+v, matched=%v", rule.Prices, ok)
}
}
func TestUpsertModelPriceRejectsInvalidInputBeforePersistence(t *testing.T) {
one := 1.0
nan := math.NaN()
positiveInfinity := math.Inf(1)
negativeInfinity := math.Inf(-1)
negative := -1.0
one := pricing.NanoUSD(1_000_000_000)
negative := pricing.NanoUSD(-1)
tests := []struct {
name string
input ModelPriceInput
}{
{
name: "NaN with another valid price",
name: "negative input with another valid price",
input: ModelPriceInput{
Pattern: "private-invalid-pattern",
CacheRead: &nan,
Output: &one,
},
},
{
name: "positive infinity",
input: ModelPriceInput{
Pattern: "private-invalid-pattern",
Output: &positiveInfinity,
},
},
{
name: "negative infinity",
input: ModelPriceInput{
Pattern: "private-invalid-pattern",
Output: &negativeInfinity,
Pattern: "private-invalid-pattern",
UncachedInput: &negative,
Output: &one,
},
},
{
@@ -264,7 +256,7 @@ func TestUpsertModelPriceFailureRollsBackAndKeepsRuntime(t *testing.T) {
mustEnsureInitialPrices(t, fixture)
beforeTable := fixture.priceRuntime.Load()
value := 1.0
value := pricing.NanoUSD(1_000_000_000)
err := fixture.service.UpsertModelPrice(context.Background(), ModelPriceInput{
Pattern: "invalid?pattern", UncachedInput: &value,
})
@@ -291,7 +283,7 @@ func TestUpsertModelPriceFailureRollsBackAndKeepsRuntime(t *testing.T) {
t.Fatalf("create rejection trigger: %v", err)
}
value := 1.0
value := pricing.NanoUSD(1_000_000_000)
err := fixture.service.UpsertModelPrice(context.Background(), ModelPriceInput{
Pattern: "rejected-model", Output: &value,
})
@@ -310,7 +302,7 @@ func TestUpsertModelPriceFailureRollsBackAndKeepsRuntime(t *testing.T) {
beforeTable := fixture.priceRuntime.Load()
releaseReader := holdRollbackJournalReadLock(t, fixture.db, dsn)
value := 1.0
value := pricing.NanoUSD(1_000_000_000)
err := fixture.service.UpsertModelPrice(context.Background(), ModelPriceInput{
Pattern: "commit-failure-model", Output: &value,
})
@@ -329,7 +321,7 @@ func TestResetModelPriceRestoresBuiltinAndIsIdempotent(t *testing.T) {
fixture := newServiceFixture(t)
mustEnsureInitialPrices(t, fixture)
input, output := 99.0, 100.0
input, output := pricing.NanoUSD(99_000_000_000), pricing.NanoUSD(100_000_000_000)
if err := fixture.service.UpsertModelPrice(context.Background(), ModelPriceInput{
Pattern: "gpt-4o", UncachedInput: &input, Output: &output,
}); err != nil {
@@ -340,7 +332,7 @@ func TestResetModelPriceRestoresBuiltinAndIsIdempotent(t *testing.T) {
if err := fixture.service.ResetModelPrice(context.Background(), "gpt-4o"); err != nil {
t.Fatalf("ResetModelPrice() error = %v", err)
}
assertPublishedPrice(t, fixture.priceRuntime, "gpt-4o", pricing.SourceBuiltin, 2.5)
assertPublishedPrice(t, fixture.priceRuntime, "gpt-4o", pricing.SourceBuiltin, 2_500_000_000)
assertModelPriceCount(t, fixture, 0)
beforeTable := fixture.priceRuntime.Load()
@@ -350,7 +342,7 @@ func TestResetModelPriceRestoresBuiltinAndIsIdempotent(t *testing.T) {
if fixture.priceRuntime.Load() == nil || fixture.priceRuntime.Load() == beforeTable {
t.Fatal("idempotent reset did not publish the precompiled complete table")
}
assertPublishedPrice(t, fixture.priceRuntime, "gpt-4o", pricing.SourceBuiltin, 2.5)
assertPublishedPrice(t, fixture.priceRuntime, "gpt-4o", pricing.SourceBuiltin, 2_500_000_000)
assertModelPriceCount(t, fixture, 0)
}
@@ -373,7 +365,7 @@ func TestModelPriceWritesDoNotPublishConfigSnapshot(t *testing.T) {
fixture := newServiceFixture(t)
mustEnsureInitialPrices(t, fixture)
beforeSnapshot := fixture.manager.Current()
value := 3.0
value := pricing.NanoUSD(3_000_000_000)
if err := fixture.service.UpsertModelPrice(context.Background(), ModelPriceInput{
Pattern: "snapshot-model", Output: &value,
@@ -394,7 +386,7 @@ func TestModelPriceWritesDoNotPublishConfigSnapshot(t *testing.T) {
func TestModelPriceConcurrentReadersSeeCompleteTables(t *testing.T) {
fixture := newServiceFixture(t)
mustEnsureInitialPrices(t, fixture)
input, output := 40.0, 80.0
input, output := pricing.NanoUSD(40_000_000_000), pricing.NanoUSD(80_000_000_000)
override := ModelPriceInput{
Pattern: "gpt-4o", UncachedInput: &input, Output: &output,
}
@@ -422,9 +414,11 @@ func TestModelPriceConcurrentReadersSeeCompleteTables(t *testing.T) {
}
prices := rule.Prices
builtin := rule.Source == pricing.SourceBuiltin &&
prices.UncachedInput.Value == 2.5 && prices.Output.Value == 10
prices.UncachedInput.NanoUSDPerMillion == 2_500_000_000 &&
prices.Output.NanoUSDPerMillion == 10_000_000_000
user := rule.Source == pricing.SourceUser &&
prices.UncachedInput.Value == input && prices.Output.Value == output
prices.UncachedInput.NanoUSDPerMillion == input &&
prices.Output.NanoUSDPerMillion == output
if !builtin && !user {
errs <- "concurrent Match observed partial prices"
return
+59 -33
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"net/url"
"time"
"github.com/gin-gonic/gin"
@@ -15,11 +14,19 @@ import (
)
type modelPriceValuesResponse struct {
UncachedInput *float64 `json:"uncached_input"`
CacheRead *float64 `json:"cache_read"`
CacheWrite5M *float64 `json:"cache_write_5m"`
CacheWrite1H *float64 `json:"cache_write_1h"`
Output *float64 `json:"output"`
InputPrice *string `json:"input_price_usd_per_million_tokens"`
OutputPrice *string `json:"output_price_usd_per_million_tokens"`
CacheReadPrice *string `json:"cache_read_price_usd_per_million_tokens"`
CacheWrite5MPrice *string `json:"cache_write_5m_price_usd_per_million_tokens"`
CacheWrite1HPrice *string `json:"cache_write_1h_price_usd_per_million_tokens"`
}
type modelPriceValuesInput struct {
UncachedInput *pricing.NanoUSD
CacheRead *pricing.NanoUSD
CacheWrite5M *pricing.NanoUSD
CacheWrite1H *pricing.NanoUSD
Output *pricing.NanoUSD
}
type modelPricePolicyResponse struct {
@@ -33,7 +40,7 @@ type modelPriceRuleResponse struct {
Source pricing.Source `json:"source"`
Prices modelPriceValuesResponse `json:"prices"`
SourceURL *string `json:"source_url"`
UpdatedAt time.Time `json:"updated_at"`
UpdatedAtMS int64 `json:"updated_at_ms"`
PricingPolicy *modelPricePolicyResponse `json:"pricing_policy"`
}
@@ -82,48 +89,59 @@ func (request *modelPriceUpsertRequest) UnmarshalJSON(data []byte) error {
return nil
}
func decodeModelPriceValues(data json.RawMessage) (modelPriceValuesResponse, error) {
func decodeModelPriceValues(data json.RawMessage) (modelPriceValuesInput, error) {
trimmed := bytes.TrimSpace(data)
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) || trimmed[0] != '{' {
return modelPriceValuesResponse{}, fmt.Errorf("model price prices must be an object")
return modelPriceValuesInput{}, fmt.Errorf("model price prices must be an object")
}
var object map[string]json.RawMessage
if err := json.Unmarshal(trimmed, &object); err != nil {
return modelPriceValuesResponse{}, err
return modelPriceValuesInput{}, err
}
if len(object) != 5 {
return modelPriceValuesResponse{}, fmt.Errorf("model price prices must contain five values")
return modelPriceValuesInput{}, fmt.Errorf("model price prices must contain five values")
}
keys := []string{
"uncached_input", "cache_read", "cache_write_5m", "cache_write_1h", "output",
"input_price_usd_per_million_tokens",
"output_price_usd_per_million_tokens",
"cache_read_price_usd_per_million_tokens",
"cache_write_5m_price_usd_per_million_tokens",
"cache_write_1h_price_usd_per_million_tokens",
}
values := make([]*float64, len(keys))
values := make([]*pricing.NanoUSD, len(keys))
for index, key := range keys {
raw, exists := object[key]
if !exists {
return modelPriceValuesResponse{}, fmt.Errorf("model price prices must contain %s", key)
return modelPriceValuesInput{}, fmt.Errorf("model price prices must contain %s", key)
}
value, err := decodeNullableModelPrice(raw)
if err != nil {
return modelPriceValuesResponse{}, fmt.Errorf("decode model price %s: %w", key, err)
return modelPriceValuesInput{}, fmt.Errorf("decode model price %s: %w", key, err)
}
values[index] = value
}
return modelPriceValuesResponse{
UncachedInput: values[0], CacheRead: values[1], CacheWrite5M: values[2],
CacheWrite1H: values[3], Output: values[4],
return modelPriceValuesInput{
UncachedInput: values[0],
Output: values[1],
CacheRead: values[2],
CacheWrite5M: values[3],
CacheWrite1H: values[4],
}, nil
}
func decodeNullableModelPrice(raw json.RawMessage) (*float64, error) {
func decodeNullableModelPrice(raw json.RawMessage) (*pricing.NanoUSD, error) {
if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return nil, nil
}
var value float64
var value string
if err := json.Unmarshal(raw, &value); err != nil {
return nil, err
}
return &value, nil
parsed, err := pricing.ParseUSD(value)
if err != nil {
return nil, err
}
return &parsed, nil
}
func (s *Server) handleListModelPrices(c *gin.Context) {
@@ -183,15 +201,19 @@ func parseModelPricePattern(rawQuery string) (string, *app_errors.APIError) {
return patternValues[0], nil
}
func newModelPriceRuleResponse(rule pricing.Rule) modelPriceRuleResponse {
func newModelPriceRuleResponse(rule pricing.Rule) (modelPriceRuleResponse, error) {
updatedAtMS, err := safeEpochMilliseconds(rule.UpdatedAt)
if err != nil {
return modelPriceRuleResponse{}, app_errors.ErrInternalServer
}
result := modelPriceRuleResponse{
Pattern: rule.Pattern, Source: rule.Source, UpdatedAt: rule.UpdatedAt,
Pattern: rule.Pattern, Source: rule.Source, UpdatedAtMS: updatedAtMS,
Prices: modelPriceValuesResponse{
UncachedInput: modelPriceValuePointer(rule.Prices.UncachedInput),
CacheRead: modelPriceValuePointer(rule.Prices.CacheRead),
CacheWrite5M: modelPriceValuePointer(rule.Prices.CacheWrite5M),
CacheWrite1H: modelPriceValuePointer(rule.Prices.CacheWrite1H),
Output: modelPriceValuePointer(rule.Prices.Output),
InputPrice: modelPriceValuePointer(rule.Prices.UncachedInput),
OutputPrice: modelPriceValuePointer(rule.Prices.Output),
CacheReadPrice: modelPriceValuePointer(rule.Prices.CacheRead),
CacheWrite5MPrice: modelPriceValuePointer(rule.Prices.CacheWrite5M),
CacheWrite1HPrice: modelPriceValuePointer(rule.Prices.CacheWrite1H),
},
}
if rule.SourceURL != "" {
@@ -201,17 +223,21 @@ func newModelPriceRuleResponse(rule pricing.Rule) modelPriceRuleResponse {
if rule.LongContextPolicy != nil {
result.PricingPolicy = &modelPricePolicyResponse{
InputThresholdTokens: rule.LongContextPolicy.InputThresholdTokens,
InputMultiplier: rule.LongContextPolicy.InputMultiplier,
OutputMultiplier: rule.LongContextPolicy.OutputMultiplier,
InputMultiplier: modelPriceMultiplier(rule.LongContextPolicy.InputMultiplier),
OutputMultiplier: modelPriceMultiplier(rule.LongContextPolicy.OutputMultiplier),
}
}
return result
return result, nil
}
func modelPriceValuePointer(price pricing.Price) *float64 {
func modelPriceValuePointer(price pricing.Price) *string {
if !price.Set {
return nil
}
value := price.Value
value := pricing.FormatUSD(price.NanoUSDPerMillion)
return &value
}
func modelPriceMultiplier(value pricing.Multiplier) float64 {
return float64(value.Numerator) / float64(value.Denominator)
}
+45 -31
View File
@@ -22,8 +22,8 @@ func TestModelPriceAPIListsStableBuiltinAndUserRules(t *testing.T) {
fixture.service.now = func() time.Time {
return time.Date(2026, time.July, 27, 12, 30, 0, 0, time.UTC)
}
zero := 0.0
output := 7.5
zero := pricing.NanoUSD(0)
output := pricing.NanoUSD(7_500_000_000)
if err := fixture.service.UpsertModelPrice(t.Context(), ModelPriceInput{
Pattern: "z-user", UncachedInput: &zero, Output: &output,
}); err != nil {
@@ -83,19 +83,24 @@ func TestModelPriceAPIListsStableBuiltinAndUserRules(t *testing.T) {
}
if builtin == nil || builtin.Source != string(pricing.SourceBuiltin) || builtin.SourceURL == nil ||
*builtin.SourceURL != "https://developers.openai.com/api/docs/pricing" ||
builtin.UpdatedAt.Format(time.RFC3339) != "2026-07-26T00:00:00Z" {
builtin.UpdatedAtMS != time.Date(2026, time.July, 26, 0, 0, 0, 0, time.UTC).UnixMilli() {
t.Fatalf("builtin response = %#v", builtin)
}
if len(builtin.Prices) != 5 || builtin.Prices["cache_write_5m"] != nil || builtin.Prices["cache_write_1h"] != nil {
if len(builtin.Prices) != 5 ||
builtin.Prices["cache_write_5m_price_usd_per_million_tokens"] != nil ||
builtin.Prices["cache_write_1h_price_usd_per_million_tokens"] != nil {
t.Fatalf("builtin prices = %#v, want five values including null cache writes", builtin.Prices)
}
if userA == nil || userA.Source != string(pricing.SourceUser) || userA.SourceURL != nil ||
userA.UpdatedAt.Format(time.RFC3339) != "2026-07-27T12:30:00Z" {
userA.UpdatedAtMS != time.Date(2026, time.July, 27, 12, 30, 0, 0, time.UTC).UnixMilli() {
t.Fatalf("a-user response = %#v", userA)
}
if userZ == nil || len(userZ.Prices) != 5 || userZ.Prices["uncached_input"] == nil ||
*userZ.Prices["uncached_input"] != 0 || userZ.Prices["cache_read"] != nil ||
userZ.Prices["output"] == nil || *userZ.Prices["output"] != 7.5 {
if userZ == nil || len(userZ.Prices) != 5 ||
userZ.Prices["input_price_usd_per_million_tokens"] == nil ||
*userZ.Prices["input_price_usd_per_million_tokens"] != "0" ||
userZ.Prices["cache_read_price_usd_per_million_tokens"] != nil ||
userZ.Prices["output_price_usd_per_million_tokens"] == nil ||
*userZ.Prices["output_price_usd_per_million_tokens"] != "7.5" {
t.Fatalf("z-user prices = %#v", userZ)
}
}
@@ -103,7 +108,7 @@ func TestModelPriceAPIListsStableBuiltinAndUserRules(t *testing.T) {
func TestModelPriceAPIListsReadOnlyPricingPolicy(t *testing.T) {
fixture := newServiceFixture(t)
mustEnsureInitialPrices(t, fixture)
output := 7.5
output := pricing.NanoUSD(7_500_000_000)
if err := fixture.service.UpsertModelPrice(t.Context(), ModelPriceInput{
Pattern: "user-model", Output: &output,
}); err != nil {
@@ -178,33 +183,39 @@ func TestModelPriceAPIPutStrictlyReplacesFivePrices(t *testing.T) {
mustEnsureInitialPrices(t, fixture)
engine := newModelPriceTestEngine(t, fixture)
valid := `{"pattern":"replace-model","prices":{"uncached_input":1,"cache_read":2,"cache_write_5m":3,"cache_write_1h":4,"output":5}}`
valid := `{"pattern":"replace-model","prices":{"input_price_usd_per_million_tokens":"1","output_price_usd_per_million_tokens":"5","cache_read_price_usd_per_million_tokens":"2","cache_write_5m_price_usd_per_million_tokens":"3","cache_write_1h_price_usd_per_million_tokens":"4"}}`
first := serveModelPriceRequest(engine, http.MethodPut, "/api/model-prices", valid, true)
if first.Code != http.StatusOK {
t.Fatalf("first PUT = %d %s", first.Code, first.Body.String())
}
replacement := `{"pattern":"replace-model","prices":{"uncached_input":null,"cache_read":0,"cache_write_5m":null,"cache_write_1h":null,"output":9}}`
replacement := `{"pattern":"replace-model","prices":{"input_price_usd_per_million_tokens":null,"output_price_usd_per_million_tokens":"9","cache_read_price_usd_per_million_tokens":"0","cache_write_5m_price_usd_per_million_tokens":null,"cache_write_1h_price_usd_per_million_tokens":null}}`
second := serveModelPriceRequest(engine, http.MethodPut, "/api/model-prices", replacement, true)
if second.Code != http.StatusOK {
t.Fatalf("replacement PUT = %d %s", second.Code, second.Body.String())
}
var row struct {
InputPrice, CacheReadPrice, CacheWrite5MPrice, CacheWrite1HPrice, OutputPrice *float64
}
if err := fixture.db.Table("model_prices").Where("pattern = ?", "replace-model").Take(&row).Error; err != nil {
var row models.ModelPrice
if err := fixture.db.Where("pattern = ?", "replace-model").Take(&row).Error; err != nil {
t.Fatalf("read replacement row: %v", err)
}
if row.InputPrice != nil || row.CacheReadPrice == nil || *row.CacheReadPrice != 0 ||
row.CacheWrite5MPrice != nil || row.CacheWrite1HPrice != nil || row.OutputPrice == nil || *row.OutputPrice != 9 {
if row.InputPriceNanoUSDPerMillionTokens != nil ||
row.CacheReadPriceNanoUSDPerMillionTokens == nil ||
*row.CacheReadPriceNanoUSDPerMillionTokens != 0 ||
row.CacheWrite5MPriceNanoUSDPerMillionTokens != nil ||
row.CacheWrite1HPriceNanoUSDPerMillionTokens != nil ||
row.OutputPriceNanoUSDPerMillionTokens == nil ||
*row.OutputPriceNanoUSDPerMillionTokens != 9_000_000_000 {
t.Fatalf("replacement row = %#v", row)
}
for _, body := range []string{
`{"pattern":"strict-model","prices":{"uncached_input":1,"cache_read":2,"cache_write_5m":3,"cache_write_1h":4,"output":5},"unknown":true}`,
`{"pattern":"strict-model","prices":{"uncached_input":1,"cache_read":2,"cache_write_5m":3,"cache_write_1h":4,"output":5},"pricing_policy":null}`,
`{"pattern":"strict-model","pattern":"other","prices":{"uncached_input":1,"cache_read":2,"cache_write_5m":3,"cache_write_1h":4,"output":5}}`,
`{"pattern":"strict-model","prices":{"uncached_input":1,"cache_read":2,"cache_read":3,"cache_write_5m":3,"cache_write_1h":4,"output":5}}`,
`{"pattern":"strict-model","prices":{"uncached_input":1,"cache_read":2,"cache_write_5m":3,"cache_write_1h":4}}`,
strings.TrimSuffix(valid, "}") + `,"unknown":true}`,
strings.TrimSuffix(valid, "}") + `,"pricing_policy":null}`,
strings.Replace(valid, `"pattern":"replace-model"`, `"pattern":"strict-model","pattern":"other"`, 1),
strings.Replace(valid, `"cache_read_price_usd_per_million_tokens":"2"`, `"cache_read_price_usd_per_million_tokens":"2","cache_read_price_usd_per_million_tokens":"3"`, 1),
strings.Replace(valid, `,"cache_write_1h_price_usd_per_million_tokens":"4"`, "", 1),
strings.Replace(valid, `"1"`, "1", 1),
strings.Replace(valid, `"1"`, `"1e0"`, 1),
strings.Replace(valid, `"1"`, `" 1"`, 1),
valid + ` {}`,
} {
recorder := serveModelPriceRequest(engine, http.MethodPut, "/api/model-prices", body, true)
@@ -215,7 +226,7 @@ func TestModelPriceAPIPutStrictlyReplacesFivePrices(t *testing.T) {
func TestModelPriceAPIDeleteValidatesSinglePatternAndIsIdempotent(t *testing.T) {
fixture := newServiceFixture(t)
mustEnsureInitialPrices(t, fixture)
value := 99.0
value := pricing.NanoUSD(99_000_000_000)
if err := fixture.service.UpsertModelPrice(t.Context(), ModelPriceInput{Pattern: "gpt-4o", Output: &value}); err != nil {
t.Fatalf("seed override: %v", err)
}
@@ -239,7 +250,8 @@ func TestModelPriceAPIDeleteValidatesSinglePatternAndIsIdempotent(t *testing.T)
}
}
rule, ok := fixture.priceRuntime.Load().Match("gpt-4o")
if !ok || rule.Source != pricing.SourceBuiltin || rule.Prices.Output.Value != 10 {
if !ok || rule.Source != pricing.SourceBuiltin ||
rule.Prices.Output.NanoUSDPerMillion != 10_000_000_000 {
t.Fatalf("runtime after idempotent DELETE = %#v, %t", rule, ok)
}
}
@@ -259,9 +271,11 @@ func TestModelPriceAPIRequiresAuthAndGetWaitsForPriceWrite(t *testing.T) {
fixture.service.writeMu.Unlock()
}
}()
value := 88.0
value := int64(88_000_000_000)
if err := fixture.db.Create(&models.ModelPrice{
Pattern: "locked-write-model", OutputPrice: &value, Source: string(pricing.SourceUser),
Pattern: "locked-write-model",
OutputPriceNanoUSDPerMillionTokens: &value,
Source: string(pricing.SourceUser),
}).Error; err != nil {
t.Fatalf("persist locked write row: %v", err)
}
@@ -335,11 +349,11 @@ func assertModelPriceAPIError(t *testing.T, recorder *httptest.ResponseRecorder,
}
type modelPriceListTestResponse struct {
Pattern string `json:"pattern"`
Source string `json:"source"`
Prices map[string]*float64 `json:"prices"`
SourceURL *string `json:"source_url"`
UpdatedAt time.Time `json:"updated_at"`
Pattern string `json:"pattern"`
Source string `json:"source"`
Prices map[string]*string `json:"prices"`
SourceURL *string `json:"source_url"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
func assertModelPriceResponseSorted(t *testing.T, rules []modelPriceListTestResponse) {
+12 -7
View File
@@ -335,8 +335,11 @@ func TestSettingsMutationAudit(t *testing.T) {
func TestModelPriceMutationAudit(t *testing.T) {
const upsertBody = `{"pattern":"audit-model","prices":{` +
`"uncached_input":1,"cache_read":null,` +
`"cache_write_5m":null,"cache_write_1h":null,"output":2}}`
`"input_price_usd_per_million_tokens":"1",` +
`"output_price_usd_per_million_tokens":"2",` +
`"cache_read_price_usd_per_million_tokens":null,` +
`"cache_write_5m_price_usd_per_million_tokens":null,` +
`"cache_write_1h_price_usd_per_million_tokens":null}}`
t.Run("upsert success", func(t *testing.T) {
fixture := newServiceFixture(t)
@@ -397,9 +400,11 @@ func TestModelPriceMutationAudit(t *testing.T) {
method: http.MethodPut,
path: "/api/model-prices",
body: `{"pattern":"","prices":{` +
`"uncached_input":1,"cache_read":null,` +
`"cache_write_5m":null,` +
`"cache_write_1h":null,"output":2}}`,
`"input_price_usd_per_million_tokens":"1",` +
`"output_price_usd_per_million_tokens":"2",` +
`"cache_read_price_usd_per_million_tokens":null,` +
`"cache_write_5m_price_usd_per_million_tokens":null,` +
`"cache_write_1h_price_usd_per_million_tokens":null}}`,
},
)
assertMutationEvent(
@@ -1198,8 +1203,8 @@ func assertAccessKeyAuditLogExcludes(
func seedAuditModelPrice(t *testing.T, fixture serviceFixture) {
t.Helper()
uncachedInput := 1.0
output := 2.0
uncachedInput := pricing.NanoUSD(1_000_000_000)
output := pricing.NanoUSD(2_000_000_000)
if err := fixture.service.UpsertModelPrice(
t.Context(),
ModelPriceInput{
+17 -8
View File
@@ -12,6 +12,7 @@ import (
"gorm.io/gorm"
"gpt-load/internal/platform/canonicaljson"
"gpt-load/internal/platform/epochms"
app_errors "gpt-load/internal/platform/errors"
"gpt-load/internal/platform/utils"
"gpt-load/internal/storage/models"
@@ -45,7 +46,7 @@ func (s *Service) recoverPendingOperationsLocked(
beforeCommitSequence uint64,
) (*models.ControlOperation, error) {
query := s.db.WithContext(ctx).
Where("completed_at IS NULL").
Where("completed_at_ms IS NULL").
Order("commit_sequence ASC")
if beforeCommitSequence != 0 {
query = query.Where("commit_sequence < ?", beforeCommitSequence)
@@ -108,19 +109,27 @@ func (s *Service) compactCompletedOperationsLocked(
) (int64, error) {
now = now.UTC()
cutoff := now.Add(-operationResultRetention)
nowMS, err := epochms.FromTime(now)
if err != nil {
return 0, app_errors.ErrInternalServer
}
cutoffMS, err := epochms.FromTime(cutoff)
if err != nil {
cutoffMS = 0
}
var rowsAffected int64
err := s.withControlTransaction(ctx, func(tx *gorm.DB) error {
err = s.withControlTransaction(ctx, func(tx *gorm.DB) error {
result := tx.Model(&models.ControlOperation{}).
Where("completed_at IS NOT NULL").
Where("completed_at <= ?", cutoff).
Where("compacted_at IS NULL").
Where("completed_at_ms IS NOT NULL").
Where("completed_at_ms <= ?", cutoffMS).
Where("compacted_at_ms IS NULL").
Updates(map[string]any{
"canonical_result": gorm.Expr("NULL"),
"required_stages": gorm.Expr("NULL"),
"last_completed_stage": "",
"failed_stage": "",
"compacted_at": now,
"updated_at": now,
"compacted_at_ms": nowMS,
"updated_at_ms": nowMS,
})
if result.Error != nil {
return app_errors.ParseDBError(result.Error)
@@ -223,7 +232,7 @@ func validateRecoverableOperation(operation *models.ControlOperation) error {
operation.DigestVersion != 1 ||
len(operation.RequestDigest) != 32 ||
!operationKind(operation.OperationKind).valid() ||
operation.CompactedAt != nil {
operation.CompactedAtMS != nil {
return fmt.Errorf("invalid durable operation comparator: %w", app_errors.ErrInternalServer)
}
if err := validateIdempotencyKey(operation.OperationID); err != nil {
+3 -3
View File
@@ -236,8 +236,8 @@ func TestDrainCommittedOperationsFailsClosedOnUnsupportedDigestVersion(t *testin
CanonicalResult: []byte(`{"id":1}`),
RequiredStages: models.JSON(stages),
LastCompletedStage: string(operationStageDBCommitted),
CreatedAt: now,
UpdatedAt: now,
CreatedAtMS: now.UnixMilli(),
UpdatedAtMS: now.UnixMilli(),
}
if err := fixture.db.Create(&row).Error; err != nil {
t.Fatalf("seed unsupported operation: %v", err)
@@ -314,7 +314,7 @@ func TestCompactCompletedOperationsKeepsPermanentComparatorTombstone(t *testing.
if err := fixture.db.First(&row).Error; err != nil {
t.Fatalf("read compacted operation: %v", err)
}
if row.CompactedAt == nil || len(row.CanonicalResult) != 0 ||
if row.CompactedAtMS == nil || len(row.CanonicalResult) != 0 ||
len(row.RequiredStages) != 0 || row.LastCompletedStage != "" ||
row.OperationID == "" || row.ResourceIdentity != "access-key:7" ||
len(row.RequestDigest) != 32 {
+91 -79
View File
@@ -5,12 +5,12 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"regexp"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -25,7 +25,7 @@ import (
const (
defaultRequestLogLimit = 50
maxRequestLogLimit = 200
requestLogCursorV1 = 1
requestLogCursorV2 = 2
)
var canonicalLowercaseUUIDv4 = regexp.MustCompile(
@@ -37,9 +37,9 @@ type RequestLogReader interface {
}
type requestLogCursorPayload struct {
Version int `json:"v"`
CompletedAt string `json:"completed_at"`
RequestID string `json:"request_id"`
Version int `json:"v"`
CompletedAtMS int64 `json:"completed_at_ms"`
RequestID string `json:"request_id"`
}
type requestLogAccessKeyResponse struct {
@@ -65,28 +65,28 @@ type requestLogAttemptResponse struct {
}
type requestLogItemResponse struct {
RequestID string `json:"request_id"`
CompletedAt string `json:"completed_at"`
AccessKey requestLogAccessKeyResponse `json:"access_key"`
Protocol string `json:"protocol"`
ClientModel *string `json:"client_model"`
UpstreamModel *string `json:"upstream_model"`
Status telemetry.RequestStatus `json:"status"`
StatusCode int `json:"status_code"`
DurationMs int64 `json:"duration_ms"`
ErrorCode string `json:"error_code"`
ErrorSummary string `json:"error_summary"`
AffinityHit bool `json:"affinity_hit"`
Attempts []requestLogAttemptResponse `json:"attempts"`
GroupID *uint `json:"group_id"`
UsageState usage.State `json:"usage_state"`
CostState pricing.CostState `json:"cost_state"`
UncachedInputTokens int64 `json:"uncached_input_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWrite5MTokens int64 `json:"cache_write_5m_tokens"`
CacheWrite1HTokens int64 `json:"cache_write_1h_tokens"`
OutputTokens int64 `json:"output_tokens"`
EstimatedCostUSD json.Number `json:"estimated_cost_usd"`
RequestID string `json:"request_id"`
CompletedAtMS int64 `json:"completed_at_ms"`
AccessKey requestLogAccessKeyResponse `json:"access_key"`
Protocol string `json:"protocol"`
ClientModel *string `json:"client_model"`
UpstreamModel *string `json:"upstream_model"`
Status telemetry.RequestStatus `json:"status"`
StatusCode int `json:"status_code"`
DurationMs int64 `json:"duration_ms"`
ErrorCode string `json:"error_code"`
ErrorSummary string `json:"error_summary"`
AffinityHit bool `json:"affinity_hit"`
Attempts []requestLogAttemptResponse `json:"attempts"`
GroupID *uint `json:"group_id"`
UsageState usage.State `json:"usage_state"`
CostState pricing.CostState `json:"cost_state"`
UncachedInputTokens int64 `json:"uncached_input_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWrite5MTokens int64 `json:"cache_write_5m_tokens"`
CacheWrite1HTokens int64 `json:"cache_write_1h_tokens"`
OutputTokens int64 `json:"output_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
}
type requestLogListResponse struct {
@@ -133,7 +133,7 @@ func parseRequestLogQuery(rawQuery string) (requestlog.ListQuery, *app_errors.AP
return requestlog.ListQuery{}, app_errors.ErrBadRequest
}
allowed := map[string]struct{}{
"from": {}, "to": {}, "group_id": {}, "model": {}, "access_key_id": {},
"from_ms": {}, "to_ms": {}, "group_id": {}, "model": {}, "access_key_id": {},
"status": {}, "request_id": {}, "limit": {}, "cursor": {},
}
for key, value := range values {
@@ -143,23 +143,21 @@ func parseRequestLogQuery(rawQuery string) (requestlog.ListQuery, *app_errors.AP
}
query := requestlog.ListQuery{Limit: defaultRequestLogLimit}
if value, ok := singleQueryValue(values, "from"); ok {
parsed, err := time.Parse(time.RFC3339Nano, value)
if value, ok := singleQueryValue(values, "from_ms"); ok {
parsed, err := parseCanonicalSafeMilliseconds(value)
if err != nil {
return requestlog.ListQuery{}, app_errors.ErrBadRequest
}
parsed = parsed.UTC()
query.From = &parsed
query.FromMS = &parsed
}
if value, ok := singleQueryValue(values, "to"); ok {
parsed, err := time.Parse(time.RFC3339Nano, value)
if value, ok := singleQueryValue(values, "to_ms"); ok {
parsed, err := parseCanonicalSafeMilliseconds(value)
if err != nil {
return requestlog.ListQuery{}, app_errors.ErrBadRequest
}
parsed = parsed.UTC()
query.To = &parsed
query.ToMS = &parsed
}
if query.From != nil && query.To != nil && !query.From.Before(*query.To) {
if query.FromMS != nil && query.ToMS != nil && *query.FromMS >= *query.ToMS {
return requestlog.ListQuery{}, app_errors.ErrValidation
}
if value, ok := singleQueryValue(values, "group_id"); ok {
@@ -201,14 +199,17 @@ func parseRequestLogQuery(rawQuery string) (requestlog.ListQuery, *app_errors.AP
query.RequestID = value
}
if value, ok := singleQueryValue(values, "limit"); ok {
parsed, err := strconv.Atoi(value)
parsed, err := parseCanonicalSafeUint(value)
if err != nil {
if errors.Is(err, errUnsafeCanonicalUint) {
return requestlog.ListQuery{}, app_errors.ErrValidation
}
return requestlog.ListQuery{}, app_errors.ErrBadRequest
}
if parsed < 1 || parsed > maxRequestLogLimit {
return requestlog.ListQuery{}, app_errors.ErrValidation
}
query.Limit = parsed
query.Limit = int(parsed)
}
if value, ok := singleQueryValue(values, "cursor"); ok {
cursor, err := decodeRequestLogCursor(value)
@@ -229,11 +230,14 @@ func singleQueryValue(values url.Values, key string) (string, bool) {
}
func parseRequestLogID(value string) (uint, *app_errors.APIError) {
parsed, err := strconv.ParseUint(value, 10, strconv.IntSize)
parsed, err := parseCanonicalSafeUint(value)
if err != nil {
if errors.Is(err, errUnsafeCanonicalUint) {
return 0, app_errors.ErrValidation
}
return 0, app_errors.ErrBadRequest
}
if parsed == 0 {
if parsed == 0 || uint64(uint(parsed)) != parsed {
return 0, app_errors.ErrValidation
}
return uint(parsed), nil
@@ -251,19 +255,18 @@ func decodeRequestLogCursor(encoded string) (*requestlog.Cursor, error) {
if err != nil {
return nil, err
}
if payload.Version != requestLogCursorV1 {
if payload.Version != requestLogCursorV2 {
return nil, fmt.Errorf("unsupported request log cursor version")
}
completedAt, err := time.Parse(time.RFC3339Nano, payload.CompletedAt)
if err != nil || payload.CompletedAt != completedAt.UTC().Format(time.RFC3339Nano) {
return nil, fmt.Errorf("invalid request log cursor completed_at")
if err := validateSafeMilliseconds(payload.CompletedAtMS); err != nil {
return nil, fmt.Errorf("invalid request log cursor completed_at_ms: %w", err)
}
if !canonicalLowercaseUUIDv4.MatchString(payload.RequestID) {
return nil, fmt.Errorf("invalid request log cursor request_id")
}
return &requestlog.Cursor{
CompletedAt: completedAt.UTC(),
RequestID: payload.RequestID,
CompletedAtMS: payload.CompletedAtMS,
RequestID: payload.RequestID,
}, nil
}
@@ -326,14 +329,16 @@ func errorsIsEOF(err error) bool {
}
func encodeRequestLogCursor(cursor requestlog.Cursor) (string, error) {
completedAt := cursor.CompletedAt.UTC()
if err := validateSafeMilliseconds(cursor.CompletedAtMS); err != nil {
return "", fmt.Errorf("encode request log cursor: invalid completed_at_ms: %w", err)
}
if !canonicalLowercaseUUIDv4.MatchString(cursor.RequestID) {
return "", fmt.Errorf("encode request log cursor: invalid request ID")
}
payload := requestLogCursorPayload{
Version: requestLogCursorV1,
CompletedAt: completedAt.Format(time.RFC3339Nano),
RequestID: cursor.RequestID,
Version: requestLogCursorV2,
CompletedAtMS: cursor.CompletedAtMS,
RequestID: cursor.RequestID,
}
raw, err := json.Marshal(payload)
if err != nil {
@@ -347,6 +352,12 @@ func mapRequestLogListResponse(page requestlog.Page) (requestLogListResponse, er
Items: make([]requestLogItemResponse, 0, len(page.Items)),
}
for _, record := range page.Items {
if err := validateSafeMilliseconds(record.CompletedAtMS); err != nil {
return requestLogListResponse{}, fmt.Errorf(
"map request log completed_at_ms: %w",
err,
)
}
usageCost, err := mapRequestLogUsageCost(record)
if err != nil {
return requestLogListResponse{}, err
@@ -370,32 +381,32 @@ func mapRequestLogListResponse(page requestlog.Page) (requestLogListResponse, er
})
}
result.Items = append(result.Items, requestLogItemResponse{
RequestID: record.RequestID,
CompletedAt: record.CompletedAt.UTC().Format(time.RFC3339Nano),
RequestID: record.RequestID,
CompletedAtMS: record.CompletedAtMS,
AccessKey: requestLogAccessKeyResponse{
ID: record.AccessKey.ID,
Name: record.AccessKey.Name,
Deleted: record.AccessKey.Deleted,
},
Protocol: string(record.Protocol),
ClientModel: nullableRequestLogModel(record.ClientModel),
UpstreamModel: nullableRequestLogModel(record.UpstreamModel),
Status: record.Status,
StatusCode: record.StatusCode,
DurationMs: record.DurationMs,
ErrorCode: record.ErrorCode,
ErrorSummary: record.ErrorSummary,
AffinityHit: record.AffinityHit,
Attempts: attempts,
GroupID: usageCost.groupID,
UsageState: record.UsageState,
CostState: record.CostState,
UncachedInputTokens: record.UncachedInputTokens,
CacheReadTokens: record.CacheReadTokens,
CacheWrite5MTokens: record.CacheWrite5MTokens,
CacheWrite1HTokens: record.CacheWrite1HTokens,
OutputTokens: record.OutputTokens,
EstimatedCostUSD: usageCost.estimatedCostUSD,
Protocol: string(record.Protocol),
ClientModel: nullableRequestLogModel(record.ClientModel),
UpstreamModel: nullableRequestLogModel(record.UpstreamModel),
Status: record.Status,
StatusCode: record.StatusCode,
DurationMs: record.DurationMs,
ErrorCode: record.ErrorCode,
ErrorSummary: record.ErrorSummary,
AffinityHit: record.AffinityHit,
Attempts: attempts,
GroupID: usageCost.groupID,
UsageState: record.UsageState,
CostState: record.CostState,
UncachedInputTokens: record.UncachedInputTokens,
CacheReadTokens: record.CacheReadTokens,
CacheWrite5MTokens: record.CacheWrite5MTokens,
CacheWrite1HTokens: record.CacheWrite1HTokens,
OutputTokens: record.OutputTokens,
EstimatedCostNanoUSD: usageCost.estimatedCostNanoUSD,
})
}
if page.NextCursor != nil {
@@ -416,15 +427,15 @@ func nullableRequestLogModel(value string) *string {
}
type requestLogUsageCostResponse struct {
groupID *uint
estimatedCostUSD json.Number
groupID *uint
estimatedCostNanoUSD string
}
func mapRequestLogUsageCost(record requestlog.Record) (requestLogUsageCostResponse, error) {
if err := requestlog.ValidateUsageCostState(
record.UsageState,
record.CostState,
record.EstimatedCostUSD,
record.EstimatedCostNanoUSD,
); err != nil {
return requestLogUsageCostResponse{}, fmt.Errorf("map request log usage/cost: %w", err)
}
@@ -442,12 +453,13 @@ func mapRequestLogUsageCost(record requestlog.Record) (requestLogUsageCostRespon
if uint64(record.GroupID) > uint64(maxSafeInteger) {
return requestLogUsageCostResponse{}, fmt.Errorf("map request log final Group ID: unsafe value")
}
cost, err := mapEstimatedCostUSD(record.EstimatedCostUSD)
if err != nil {
return requestLogUsageCostResponse{}, err
if record.EstimatedCostNanoUSD < 0 {
return requestLogUsageCostResponse{}, fmt.Errorf(
"map request log usage/cost: negative estimated cost",
)
}
result := requestLogUsageCostResponse{
estimatedCostUSD: cost,
estimatedCostNanoUSD: strconv.FormatInt(record.EstimatedCostNanoUSD, 10),
}
if record.GroupID != 0 {
groupID := record.GroupID
+99 -59
View File
@@ -5,7 +5,6 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"math"
"net/http"
"net/http/httptest"
"net/url"
@@ -26,7 +25,7 @@ import (
func TestRequestLogEndpointRejectsUnknownDuplicateAndMalformedQueries(t *testing.T) {
validCursor := encodeTestCursorPayload(
`{"v":1,"completed_at":"2026-07-24T12:00:00Z","request_id":"00000000-0000-4000-8000-000000000001"}`,
`{"v":2,"completed_at_ms":1784894400000,"request_id":"00000000-0000-4000-8000-000000000001"}`,
)
tests := []struct {
name string
@@ -35,25 +34,41 @@ func TestRequestLogEndpointRejectsUnknownDuplicateAndMalformedQueries(t *testing
{name: "unknown", query: "unknown=value"},
{name: "duplicate", query: "limit=1&limit=2"},
{name: "number", query: "limit=not-a-number"},
{name: "time", query: "from=not-a-time"},
{name: "old time query", query: "from=1784894400000"},
{name: "time", query: "from_ms=not-a-time"},
{name: "time leading zero", query: "from_ms=01784894400000"},
{name: "time plus", query: "from_ms=%2B1784894400000"},
{name: "time negative", query: "from_ms=-1"},
{name: "time unsafe", query: "from_ms=9007199254740992"},
{name: "group", query: "group_id=-1"},
{name: "group leading zero", query: "group_id=01"},
{name: "group plus", query: "group_id=%2B1"},
{name: "group whitespace", query: "group_id=%201"},
{name: "group duplicate", query: "group_id=1&group_id=2"},
{name: "access key", query: "access_key_id=1.5"},
{name: "access key leading zero", query: "access_key_id=01"},
{name: "access key plus", query: "access_key_id=%2B1"},
{name: "access key whitespace", query: "access_key_id=%201"},
{name: "access key duplicate", query: "access_key_id=1&access_key_id=2"},
{name: "limit leading zero", query: "limit=01"},
{name: "limit plus", query: "limit=%2B1"},
{name: "limit whitespace", query: "limit=%201"},
{name: "request UUID uppercase", query: "request_id=00000000-0000-4000-8000-00000000ABCD"},
{name: "request UUID version", query: "request_id=00000000-0000-3000-8000-000000000001"},
{name: "cursor base64", query: "cursor=%25%25%25"},
{name: "cursor percent encoded newline", query: "cursor=" + validCursor + "%0A"},
{name: "cursor JSON", query: "cursor=" + encodeTestCursorPayload(`{"v":1`)},
{name: "cursor unknown field", query: "cursor=" + encodeTestCursorPayload(
`{"v":1,"completed_at":"2026-07-24T12:00:00Z","request_id":"00000000-0000-4000-8000-000000000001","extra":true}`,
`{"v":2,"completed_at_ms":1784894400000,"request_id":"00000000-0000-4000-8000-000000000001","extra":true}`,
)},
{name: "cursor version", query: "cursor=" + encodeTestCursorPayload(
`{"v":2,"completed_at":"2026-07-24T12:00:00Z","request_id":"00000000-0000-4000-8000-000000000001"}`,
{name: "cursor old version", query: "cursor=" + encodeTestCursorPayload(
`{"v":1,"completed_at":"2026-07-24T12:00:00Z","request_id":"00000000-0000-4000-8000-000000000001"}`,
)},
{name: "cursor non UTC time", query: "cursor=" + encodeTestCursorPayload(
`{"v":1,"completed_at":"2026-07-24T20:00:00+08:00","request_id":"00000000-0000-4000-8000-000000000001"}`,
{name: "cursor unsafe timestamp", query: "cursor=" + encodeTestCursorPayload(
`{"v":2,"completed_at_ms":9007199254740992,"request_id":"00000000-0000-4000-8000-000000000001"}`,
)},
{name: "cursor UUID", query: "cursor=" + encodeTestCursorPayload(
`{"v":1,"completed_at":"2026-07-24T12:00:00Z","request_id":"00000000-0000-3000-8000-000000000001"}`,
`{"v":2,"completed_at_ms":1784894400000,"request_id":"00000000-0000-3000-8000-000000000001"}`,
)},
}
@@ -71,7 +86,7 @@ func TestRequestLogEndpointRejectsUnknownDuplicateAndMalformedQueries(t *testing
}
func TestRequestLogEndpointRejectsInvalidDomainValues(t *testing.T) {
equalTime := url.QueryEscape("2026-07-24T12:00:00Z")
equalTime := "1784894400000"
tests := []struct {
name string
query string
@@ -79,10 +94,12 @@ func TestRequestLogEndpointRejectsInvalidDomainValues(t *testing.T) {
{name: "limit zero", query: "limit=0"},
{name: "limit above maximum", query: "limit=201"},
{name: "group zero", query: "group_id=0"},
{name: "group unsafe", query: "group_id=9007199254740992"},
{name: "access key zero", query: "access_key_id=0"},
{name: "access key unsafe", query: "access_key_id=9007199254740992"},
{name: "empty model", query: "model="},
{name: "equal range", query: "from=" + equalTime + "&to=" + equalTime},
{name: "reversed range", query: "from=2026-07-24T13%3A00%3A00Z&to=2026-07-24T12%3A00%3A00Z"},
{name: "equal range", query: "from_ms=" + equalTime + "&to_ms=" + equalTime},
{name: "reversed range", query: "from_ms=1784898000000&to_ms=1784894400000"},
{name: "unknown status", query: "status=unknown"},
}
@@ -99,19 +116,43 @@ func TestRequestLogEndpointRejectsInvalidDomainValues(t *testing.T) {
}
}
func TestRequestLogEndpointAcceptsCanonicalNumericBoundaries(t *testing.T) {
if strconv.IntSize != 64 {
t.Skip("maximum JavaScript safe integer does not fit uint on this architecture")
}
reader := &recordingRequestLogReader{}
engine := newRequestLogTestEngine(t, reader)
recorder := performRequestLogRequest(
engine,
"test-auth-key",
"group_id=9007199254740991&access_key_id=9007199254740991&limit=200",
)
if recorder.Code != http.StatusOK {
t.Fatalf("response = %d %s, want 200", recorder.Code, recorder.Body.String())
}
if len(reader.queries) != 1 || reader.queries[0].GroupID == nil ||
reader.queries[0].AccessKeyID == nil ||
uint64(*reader.queries[0].GroupID) != uint64(maxSafeInteger) ||
uint64(*reader.queries[0].AccessKeyID) != uint64(maxSafeInteger) ||
reader.queries[0].Limit != maxRequestLogLimit {
t.Fatalf("List() queries = %#v", reader.queries)
}
}
func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
completedAt := time.Date(2026, time.July, 24, 12, 0, 0, 123456789, time.UTC)
completedAtMS := completedAt.UnixMilli()
nextCursor := &requestlog.Cursor{
CompletedAt: completedAt,
RequestID: "00000000-0000-4000-8000-000000000502",
CompletedAtMS: completedAtMS,
RequestID: "00000000-0000-4000-8000-000000000502",
}
currentName := "Renamed Access Key"
reader := &recordingRequestLogReader{
pages: []requestlog.Page{
{
Items: []requestlog.Record{{
RequestID: "00000000-0000-4000-8000-000000000501",
CompletedAt: completedAt,
RequestID: "00000000-0000-4000-8000-000000000501",
CompletedAtMS: completedAtMS,
AccessKey: requestlog.AccessKeyRef{
ID: 41, Name: &currentName,
},
@@ -145,8 +186,8 @@ func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
engine := newRequestLogTestEngine(t, reader)
query := strings.Join([]string{
"from=2026-07-24T19%3A00%3A00%2B08%3A00",
"to=2026-07-24T21%3A00%3A00%2B08%3A00",
"from_ms=1784890800000",
"to_ms=1784898000000",
"group_id=12",
"model=client-model",
"access_key_id=41",
@@ -161,9 +202,9 @@ func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
t.Fatalf("Reader calls = %d, want one", len(reader.queries))
}
gotQuery := reader.queries[0]
if gotQuery.Limit != 50 || gotQuery.From == nil || gotQuery.To == nil ||
gotQuery.From.Format(time.RFC3339Nano) != "2026-07-24T11:00:00Z" ||
gotQuery.To.Format(time.RFC3339Nano) != "2026-07-24T13:00:00Z" ||
if gotQuery.Limit != 50 || gotQuery.FromMS == nil || gotQuery.ToMS == nil ||
*gotQuery.FromMS != 1784890800000 ||
*gotQuery.ToMS != 1784898000000 ||
gotQuery.GroupID == nil || *gotQuery.GroupID != 12 ||
gotQuery.AccessKeyID == nil || *gotQuery.AccessKeyID != 41 ||
gotQuery.ClientModel != "client-model" ||
@@ -211,8 +252,8 @@ func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
if err := json.Unmarshal(decodedCursor, &payload); err != nil {
t.Fatalf("next_cursor JSON: %v", err)
}
if len(payload) != 3 || payload["v"] != float64(1) ||
payload["completed_at"] != "2026-07-24T12:00:00.123456789Z" ||
if len(payload) != 3 || payload["v"] != float64(2) ||
payload["completed_at_ms"] != float64(completedAtMS) ||
payload["request_id"] != nextCursor.RequestID {
t.Fatalf("next_cursor payload = %#v", payload)
}
@@ -226,7 +267,7 @@ func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
t.Fatalf("second response = %d %s", second.Code, second.Body.String())
}
if len(reader.queries) != 2 || reader.queries[1].Cursor == nil ||
!reader.queries[1].Cursor.CompletedAt.Equal(completedAt) ||
reader.queries[1].Cursor.CompletedAtMS != completedAtMS ||
reader.queries[1].Cursor.RequestID != nextCursor.RequestID {
t.Fatalf("decoded cursor query = %#v", reader.queries)
}
@@ -248,20 +289,20 @@ func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
func TestRequestLogEndpointProjectsUsageCostAndNullGroupZero(t *testing.T) {
reader := &recordingRequestLogReader{pages: []requestlog.Page{{Items: []requestlog.Record{
{
RequestID: "00000000-0000-4000-8000-000000000603",
CompletedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
Protocol: protocol.OpenAICompletions,
Status: telemetry.RequestStatusSuccess,
GroupID: 0,
UsageState: usage.StateComplete,
CostState: pricing.CostStatePriced,
UncachedInputTokens: 1,
CacheReadTokens: 2,
CacheWrite5MTokens: 3,
CacheWrite1HTokens: 4,
OutputTokens: 5,
EstimatedCostUSD: 0.1234567890123,
Attempts: []requestlog.Attempt{},
RequestID: "00000000-0000-4000-8000-000000000603",
CompletedAtMS: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC).UnixMilli(),
Protocol: protocol.OpenAICompletions,
Status: telemetry.RequestStatusSuccess,
GroupID: 0,
UsageState: usage.StateComplete,
CostState: pricing.CostStatePriced,
UncachedInputTokens: 1,
CacheReadTokens: 2,
CacheWrite5MTokens: 3,
CacheWrite1HTokens: 4,
OutputTokens: 5,
EstimatedCostNanoUSD: 123_456_789_012,
Attempts: []requestlog.Attempt{},
},
}}}}
recorder := performRequestLogRequest(newRequestLogTestEngine(t, reader), "test-auth-key", "")
@@ -271,15 +312,15 @@ func TestRequestLogEndpointProjectsUsageCostAndNullGroupZero(t *testing.T) {
var envelope struct {
Data struct {
Items []struct {
GroupID *uint `json:"group_id"`
UsageState string `json:"usage_state"`
CostState string `json:"cost_state"`
UncachedInputTokens int64 `json:"uncached_input_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWrite5MTokens int64 `json:"cache_write_5m_tokens"`
CacheWrite1HTokens int64 `json:"cache_write_1h_tokens"`
OutputTokens int64 `json:"output_tokens"`
EstimatedCostUSD json.Number `json:"estimated_cost_usd"`
GroupID *uint `json:"group_id"`
UsageState string `json:"usage_state"`
CostState string `json:"cost_state"`
UncachedInputTokens int64 `json:"uncached_input_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWrite5MTokens int64 `json:"cache_write_5m_tokens"`
CacheWrite1HTokens int64 `json:"cache_write_1h_tokens"`
OutputTokens int64 `json:"output_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
} `json:"items"`
} `json:"data"`
}
@@ -293,7 +334,7 @@ func TestRequestLogEndpointProjectsUsageCostAndNullGroupZero(t *testing.T) {
if item.GroupID != nil || item.UsageState != "complete" || item.CostState != "priced" ||
item.UncachedInputTokens != 1 || item.CacheReadTokens != 2 ||
item.CacheWrite5MTokens != 3 || item.CacheWrite1HTokens != 4 ||
item.OutputTokens != 5 || item.EstimatedCostUSD.String() != "0.123456789012" {
item.OutputTokens != 5 || item.EstimatedCostNanoUSD != "123456789012" {
t.Fatalf("usage/cost projection = %#v", item)
}
}
@@ -347,8 +388,7 @@ func TestRequestLogUsageCostProjectionRejectsUnsafeValues(t *testing.T) {
{name: "cost state", mutate: func(record *requestlog.Record) { record.CostState = "invalid" }},
{name: "negative token", mutate: func(record *requestlog.Record) { record.OutputTokens = -1 }},
{name: "unsafe token", mutate: func(record *requestlog.Record) { record.OutputTokens = maxSafeInteger + 1 }},
{name: "negative cost", mutate: func(record *requestlog.Record) { record.EstimatedCostUSD = -0.1 }},
{name: "infinite cost", mutate: func(record *requestlog.Record) { record.EstimatedCostUSD = math.Inf(1) }},
{name: "negative cost", mutate: func(record *requestlog.Record) { record.EstimatedCostNanoUSD = -1 }},
{
name: "missing usage cannot be priced",
mutate: func(record *requestlog.Record) {
@@ -366,7 +406,7 @@ func TestRequestLogUsageCostProjectionRejectsUnsafeValues(t *testing.T) {
name: "unpriced cost must be zero",
mutate: func(record *requestlog.Record) {
record.CostState = pricing.CostStateUnpriced
record.EstimatedCostUSD = 0.1
record.EstimatedCostNanoUSD = 1
},
},
}
@@ -428,7 +468,7 @@ func TestRequestLogEndpointFailsClosedOnCorruptReaderUsageCost(t *testing.T) {
name: "unpriced nonzero cost",
mutate: func(record *requestlog.Record) {
record.CostState = pricing.CostStateUnpriced
record.EstimatedCostUSD = 0.01
record.EstimatedCostNanoUSD = 1
},
},
}
@@ -447,11 +487,11 @@ func TestRequestLogEndpointFailsClosedOnCorruptReaderUsageCost(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
record := requestlog.Record{
RequestID: "00000000-0000-4000-8000-000000000605",
CompletedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
UsageState: usage.StateComplete,
CostState: pricing.CostStatePriced,
Attempts: []requestlog.Attempt{},
RequestID: "00000000-0000-4000-8000-000000000605",
CompletedAtMS: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC).UnixMilli(),
UsageState: usage.StateComplete,
CostState: pricing.CostStatePriced,
Attempts: []requestlog.Attempt{},
}
test.mutate(&record)
reader := &recordingRequestLogReader{
@@ -550,7 +590,7 @@ func encodeTestCursorPayload(payload string) string {
}
func Example_requestLogOpaqueCursor() {
payload := `{"v":1,"completed_at":"2026-07-24T12:00:00Z","request_id":"00000000-0000-4000-8000-000000000001"}`
payload := `{"v":2,"completed_at_ms":1784894400000,"request_id":"00000000-0000-4000-8000-000000000001"}`
fmt.Println(encodeTestCursorPayload(payload))
// Output: eyJ2IjoxLCJjb21wbGV0ZWRfYXQiOiIyMDI2LTA3LTI0VDEyOjAwOjAwWiIsInJlcXVlc3RfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEifQ
// Output: eyJ2IjoyLCJjb21wbGV0ZWRfYXRfbXMiOjE3ODQ4OTQ0MDAwMDAsInJlcXVlc3RfaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwMDEifQ
}
+18 -8
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -34,7 +33,7 @@ type routeInspectKeyResponse struct {
WeightManual *int `json:"weight_manual"`
WeightAuto int `json:"weight_auto"`
EffectiveWeight int64 `json:"effective_weight"`
CooldownUntil *time.Time `json:"cooldown_until"`
CooldownUntilMS *int64 `json:"cooldown_until_ms"`
}
type routeInspectGroupResponse struct {
@@ -49,7 +48,7 @@ type routeInspectGroupResponse struct {
}
type routeInspectResponse struct {
ObservedAt time.Time `json:"observed_at"`
ObservedAtMS int64 `json:"observed_at_ms"`
SnapshotRevision uint64 `json:"snapshot_revision"`
Protocol protocol.Protocol `json:"protocol"`
ExternalModel *string `json:"external_model"`
@@ -118,7 +117,7 @@ func (service *Service) InspectRoute(
request,
accessKey,
explanation,
), nil
)
}
func mapRouteInspectResponse(
@@ -126,9 +125,13 @@ func mapRouteInspectResponse(
request routeInspectRequest,
accessKey state.AccessKeyView,
explanation scheduler.Inspection,
) routeInspectResponse {
) (routeInspectResponse, error) {
observedAtMS, err := safeEpochMilliseconds(observation.observedAt)
if err != nil {
return routeInspectResponse{}, fmt.Errorf("map route inspection observed_at_ms: %w", err)
}
result := routeInspectResponse{
ObservedAt: observation.observedAt,
ObservedAtMS: observedAtMS,
SnapshotRevision: observation.snapshot.Revision,
Protocol: request.Protocol,
ExternalModel: cloneRouteModel(request.ExternalModel),
@@ -151,6 +154,13 @@ func mapRouteInspectResponse(
Keys: []routeInspectKeyResponse{},
}
for _, key := range group.Keys {
cooldownUntilMS, err := optionalSafeEpochMilliseconds(key.CooldownUntil)
if err != nil {
return routeInspectResponse{}, fmt.Errorf(
"map route inspection cooldown_until_ms: %w",
err,
)
}
groupResponse.Keys = append(groupResponse.Keys, routeInspectKeyResponse{
KeyID: key.KeyID,
Available: key.Available,
@@ -158,12 +168,12 @@ func mapRouteInspectResponse(
WeightManual: cloneInt(key.WeightManual),
WeightAuto: key.WeightAuto,
EffectiveWeight: key.EffectiveWeight,
CooldownUntil: optionalUTC(key.CooldownUntil),
CooldownUntilMS: cooldownUntilMS,
})
}
result.Groups = append(result.Groups, groupResponse)
}
return result
return result, nil
}
func cloneRouteModel(value *string) *string {
+19 -21
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"reflect"
"strconv"
"strings"
"sync/atomic"
"testing"
@@ -333,7 +334,7 @@ func TestRouteInspectEndpointReturnsCurrentSafeExplanation(t *testing.T) {
`{"protocol":"openai-completions","external_model":"public-model","access_key_id":10}`,
)
got := decodeRouteInspectSuccess(t, recorder)
if !got.ObservedAt.Equal(now) ||
if got.ObservedAtMS != now.UnixMilli() ||
got.SnapshotRevision != fixture.manager.Current().Revision ||
got.Protocol != protocol.OpenAICompletions ||
routeModelValue(got.ExternalModel) != "public-model" ||
@@ -360,14 +361,14 @@ func TestRouteInspectEndpointReturnsCurrentSafeExplanation(t *testing.T) {
if !available.Available || available.ReasonCode != nil ||
available.WeightManual == nil || *available.WeightManual != 25 ||
available.WeightAuto != 90 || available.EffectiveWeight != 50*25 ||
available.CooldownUntil != nil {
available.CooldownUntilMS != nil {
t.Fatalf("available key = %#v", available)
}
cooldown := primary.Keys[1]
if cooldown.Available || cooldown.WeightManual != nil ||
cooldown.WeightAuto != 40 || cooldown.EffectiveWeight != 0 ||
cooldown.CooldownUntil == nil ||
!cooldown.CooldownUntil.Equal(now.Add(time.Minute).UTC()) {
cooldown.CooldownUntilMS == nil ||
*cooldown.CooldownUntilMS != now.Add(time.Minute).UnixMilli() {
t.Fatalf("cooldown key = %#v", cooldown)
}
assertRouteReason(t, cooldown.ReasonCode, scheduler.ReasonKeyCooldown)
@@ -381,12 +382,12 @@ func TestRouteInspectEndpointReturnsCurrentSafeExplanation(t *testing.T) {
backup.Keys[0].WeightManual != nil ||
backup.Keys[0].WeightAuto != 30 ||
backup.Keys[0].EffectiveWeight != 20*30 ||
backup.Keys[0].CooldownUntil != nil {
backup.Keys[0].CooldownUntilMS != nil {
t.Fatalf("backup group = %#v", backup)
}
body := recorder.Body.String()
if strings.Count(body, `"reason_code":null`) != 5 ||
strings.Count(body, `"cooldown_until":null`) != 2 {
strings.Count(body, `"cooldown_until_ms":null`) != 2 {
t.Fatalf("success response must preserve explicit nulls: %s", body)
}
lower := strings.ToLower(body)
@@ -473,7 +474,7 @@ func TestRouteInspectEndpointReturnsFilterExplanations(t *testing.T) {
`{"protocol":"openai-completions","external_model":"public-model","access_key_id":10}`,
)
got := decodeRouteInspectSuccess(t, recorder)
if !got.ObservedAt.Equal(now) ||
if got.ObservedAtMS != now.UnixMilli() ||
got.SnapshotRevision != fixture.manager.Current().Revision ||
got.Routable {
t.Fatalf("filter response = %#v", got)
@@ -533,7 +534,7 @@ func TestRouteInspectEndpointReturnsNoRouteTargetExplanation(t *testing.T) {
`{"protocol":"openai-completions","external_model":"missing-model","access_key_id":10}`,
)
got := decodeRouteInspectSuccess(t, recorder)
if !got.ObservedAt.Equal(now) ||
if got.ObservedAtMS != now.UnixMilli() ||
got.SnapshotRevision != fixture.manager.Current().Revision ||
got.Routable || got.Groups == nil || len(got.Groups) != 0 {
t.Fatalf("no-target response = %#v", got)
@@ -598,7 +599,7 @@ func TestRouteInspectEndpointReturnsNoAvailableKeyExplanation(t *testing.T) {
`{"protocol":"openai-completions","external_model":"public-model","access_key_id":10}`,
)
got := decodeRouteInspectSuccess(t, recorder)
if !got.ObservedAt.Equal(now) ||
if got.ObservedAtMS != now.UnixMilli() ||
got.SnapshotRevision != fixture.manager.Current().Revision ||
got.Routable {
t.Fatalf("unavailable response = %#v", got)
@@ -639,25 +640,22 @@ func TestRouteInspectEndpointReturnsNoAvailableKeyExplanation(t *testing.T) {
t.Fatalf("key %d manual weight = %v, want %d", index, key.WeightManual, *wantManual[index])
}
if index == 3 {
if key.CooldownUntil == nil ||
!key.CooldownUntil.Equal(cooldownAt.UTC()) {
t.Fatalf("cooldown = %v, want %v", key.CooldownUntil, cooldownAt.UTC())
if key.CooldownUntilMS == nil ||
*key.CooldownUntilMS != cooldownAt.UnixMilli() {
t.Fatalf("cooldown = %v, want %v", key.CooldownUntilMS, cooldownAt.UnixMilli())
}
if key.CooldownUntil.Location() != time.UTC {
t.Fatalf("cooldown location = %v, want UTC", key.CooldownUntil.Location())
}
} else if key.CooldownUntil != nil {
t.Fatalf("key %d cooldown = %v, want nil", index, key.CooldownUntil)
} else if key.CooldownUntilMS != nil {
t.Fatalf("key %d cooldown = %v, want nil", index, key.CooldownUntilMS)
}
}
if strings.Count(recorder.Body.String(), `"cooldown_until":null`) != 3 {
if strings.Count(recorder.Body.String(), `"cooldown_until_ms":null`) != 3 {
t.Fatalf("non-cooldown keys must encode null cooldown: %s", recorder.Body.String())
}
wantCooldownJSON := `"cooldown_until":"` +
cooldownAt.UTC().Format(time.RFC3339) + `"`
wantCooldownJSON := `"cooldown_until_ms":` +
strconv.FormatInt(cooldownAt.UnixMilli(), 10)
if !strings.Contains(recorder.Body.String(), wantCooldownJSON) {
t.Fatalf(
"cooldown must encode as UTC: got %s, want %s",
"cooldown must encode as milliseconds: got %s, want %s",
recorder.Body.String(),
wantCooldownJSON,
)
+50 -5
View File
@@ -28,6 +28,46 @@ import (
"gpt-load/internal/storage/models"
)
func TestServerHomeRoutesUseExactManagementContracts(t *testing.T) {
fixture := newServiceFixture(t)
module := NewServer(
&config.Config{AuthKey: "test-auth-key"},
fixture.service,
).HTTPModule()
type routeContract struct {
method string
path string
}
want := map[string]routeContract{
"control.home": {
method: http.MethodGet,
path: "/home",
},
"control.home.statistics": {
method: http.MethodGet,
path: "/home/statistics",
},
}
seen := make(map[string]int, len(want))
for _, route := range module.Routes {
contract, exists := want[route.Name]
if !exists {
continue
}
seen[route.Name]++
if len(route.Methods) != 1 ||
route.Methods[0] != contract.method ||
route.Path != contract.path {
t.Fatalf("route %q = %#v, want %#v", route.Name, route, contract)
}
}
for name := range want {
if seen[name] != 1 {
t.Fatalf("route %q occurrences = %d, want 1", name, seen[name])
}
}
}
func TestSystemInfoHTTPContract(t *testing.T) {
initControlI18n(t)
fixture := newServiceFixture(t)
@@ -210,9 +250,13 @@ func TestControlMutationRejectsDuplicateJSONWithoutSideEffects(t *testing.T) {
name: "model price",
method: http.MethodPut,
path: "/api/model-prices",
body: `{"pattern":"duplicate-model","prices":{"uncached_input":1,` +
`"cache_read":2,"cache_read":3,"cache_write_5m":4,` +
`"cache_write_1h":5,"output":6}}`,
body: `{"pattern":"duplicate-model","prices":{` +
`"input_price_usd_per_million_tokens":"1",` +
`"output_price_usd_per_million_tokens":"6",` +
`"cache_read_price_usd_per_million_tokens":"2",` +
`"cache_read_price_usd_per_million_tokens":"3",` +
`"cache_write_5m_price_usd_per_million_tokens":"4",` +
`"cache_write_1h_price_usd_per_million_tokens":"5"}}`,
},
{
name: "route inspector",
@@ -2084,8 +2128,8 @@ func TestSettingsHTTPFiltersPrivateRowsAndDoesNotLogValues(t *testing.T) {
t.Fatalf("update response = %d %s", update.Code, update.Body.String())
}
for _, row := range []models.SystemSetting{
{Key: defaultAccessKeyMarker, Value: `"bootstrap-marker-distinctive"`, UpdatedAt: time.Now().UTC()},
{Key: unknownKey, Value: `"unknown-value"`, UpdatedAt: time.Now().UTC()},
{Key: defaultAccessKeyMarker, Value: `"bootstrap-marker-distinctive"`, UpdatedAtMS: time.Now().UnixMilli()},
{Key: unknownKey, Value: `"unknown-value"`, UpdatedAtMS: time.Now().UnixMilli()},
} {
if err := fixture.db.Create(&row).Error; err != nil {
t.Fatal(err)
@@ -2127,6 +2171,7 @@ func TestSettingsHTTPFiltersPrivateRowsAndDoesNotLogValues(t *testing.T) {
fixture.service.dialects,
fixture.service.requestLogs,
fixture.service.usageStats,
fixture.service.homeStatistics,
fixture.stats,
fixture.requestLogStats,
)
+6 -2
View File
@@ -31,11 +31,13 @@ type Service struct {
db *gorm.DB
manager *state.Manager
registry *state.KeyRegistry
registrySnapshot func() []state.KeyRuntimeView
priceRuntime *PriceRuntime
encryption encryption.Service
dialects dialect.Set
requestLogs RequestLogReader
usageStats UsageStatReader
homeStatistics HomeStatisticsReader
stats *health.StatsStore
requestLogStats RequestLogStatsReader
modelDiscoveryTimeout time.Duration
@@ -62,6 +64,7 @@ func NewService(
dialects dialect.Set,
requestLogs RequestLogReader,
usageStats UsageStatReader,
homeStatistics HomeStatisticsReader,
stats *health.StatsStore,
requestLogStats RequestLogStatsReader,
) *Service {
@@ -69,8 +72,8 @@ func NewService(
db: db, manager: manager, registry: registry,
priceRuntime: priceRuntime,
encryption: encryptionService, dialects: dialects, requestLogs: requestLogs,
usageStats: usageStats,
stats: stats, requestLogStats: requestLogStats,
usageStats: usageStats, homeStatistics: homeStatistics,
stats: stats, requestLogStats: requestLogStats,
modelDiscoveryTimeout: defaultModelDiscoveryTimeout,
random: rand.Reader,
operationRandom: rand.Reader,
@@ -79,6 +82,7 @@ func NewService(
}
service.publishSnapshot = manager.Publish
service.reconcileRegistryGroup = registry.ReconcileGroup
service.registrySnapshot = registry.Snapshot
return service
}
+9 -4
View File
@@ -13,6 +13,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gpt-load/internal/platform/epochms"
app_errors "gpt-load/internal/platform/errors"
"gpt-load/internal/state"
stateloader "gpt-load/internal/state/loader"
@@ -220,13 +221,17 @@ func (s *Service) applySettingUpdates(
}
continue
}
updatedAtMS, err := epochms.FromTime(s.now())
if err != nil {
return app_errors.ErrInternalServer
}
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "key"}},
DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at"}),
DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at_ms"}),
}).Create(&models.SystemSetting{
Key: update.key,
Value: *update.value,
UpdatedAt: s.now().UTC(),
Key: update.key,
Value: *update.value,
UpdatedAtMS: updatedAtMS,
}).Error; err != nil {
return app_errors.ParseDBError(err)
}
+1
View File
@@ -216,6 +216,7 @@ func TestSettingsETagIgnoresOtherResourcesAndSurvivesRuntimeReload(t *testing.T)
fixture.service.dialects,
fixture.service.requestLogs,
fixture.service.usageStats,
fixture.service.homeStatistics,
fixture.stats,
fixture.requestLogStats,
)
+4 -4
View File
@@ -95,8 +95,8 @@ func TestGetSettingsFiltersAndSortsPublicRuntimeOverrides(t *testing.T) {
t.Fatal(err)
}
for _, row := range []models.SystemSetting{
{Key: "unknown_public_setting", Value: `"hidden"`, UpdatedAt: time.Now().UTC()},
{Key: models.InternalSystemSettingPrefix + "hidden", Value: `"marker"`, UpdatedAt: time.Now().UTC()},
{Key: "unknown_public_setting", Value: `"hidden"`, UpdatedAtMS: time.Now().UnixMilli()},
{Key: models.InternalSystemSettingPrefix + "hidden", Value: `"marker"`, UpdatedAtMS: time.Now().UnixMilli()},
} {
if err := fixture.db.Create(&row).Error; err != nil {
t.Fatal(err)
@@ -251,8 +251,8 @@ func TestUpdateSettingsCanonicalizesValuesAndReturnsEffectiveHeaderRules(t *test
t.Fatalf("persisted value = %q, want %q", row.Value, wantValue)
}
wantTime := time.Date(2026, time.July, 24, 4, 30, 0, 0, time.UTC)
if !row.UpdatedAt.Equal(wantTime) {
t.Fatalf("updated_at = %s, want %s", row.UpdatedAt, wantTime)
if row.UpdatedAtMS != wantTime.UnixMilli() {
t.Fatalf("updated_at_ms = %d, want %d", row.UpdatedAtMS, wantTime.UnixMilli())
}
}
+1
View File
@@ -134,6 +134,7 @@ func newServiceFixtureWithDSN(t *testing.T, dsn string) serviceFixture {
dialect.NewSet(),
nil,
nil,
nil,
stats,
requestLogStats,
),
+11 -8
View File
@@ -23,7 +23,7 @@ type UpstreamKeyResponse struct {
WeightManual *int `json:"weight_manual"`
WeightAuto int `json:"weight_auto"`
Blacklisted bool `json:"blacklisted"`
CooldownUntil *time.Time `json:"cooldown_until"`
CooldownUntilMS *int64 `json:"cooldown_until_ms"`
FailureCount int `json:"failure_count"`
}
@@ -163,9 +163,12 @@ func (s *Service) mapGroupKeys(
app_errors.ErrInternalServer,
)
}
var cooldownUntil *time.Time
var cooldownUntilMS *int64
if view.CooldownUntil.After(observation.observedAt) {
cooldownUntil = optionalUTC(view.CooldownUntil)
cooldownUntilMS, err = optionalSafeEpochMilliseconds(view.CooldownUntil)
if err != nil {
return nil, fmt.Errorf("map upstream key cooldown_until_ms: %w", err)
}
}
result = append(result, UpstreamKeyResponse{
ID: row.ID, GroupID: row.GroupID,
@@ -176,11 +179,11 @@ func (s *Service) mapGroupKeys(
view,
observation.observedAt,
)),
WeightManual: cloneInt(view.WeightManual),
WeightAuto: view.WeightAuto,
Blacklisted: view.Blacklisted,
CooldownUntil: cooldownUntil,
FailureCount: view.FailureCount,
WeightManual: cloneInt(view.WeightManual),
WeightAuto: view.WeightAuto,
Blacklisted: view.Blacklisted,
CooldownUntilMS: cooldownUntilMS,
FailureCount: view.FailureCount,
})
}
return result, nil
+8 -8
View File
@@ -137,8 +137,8 @@ func TestListGroupKeysReturnsMaskedStableRuntimeView(t *testing.T) {
if got[0].Mask != "sk-a****wxyz" ||
got[0].EffectiveStatus != "cooldown" ||
got[0].WeightAuto != 42 ||
got[0].CooldownUntil == nil ||
!got[0].CooldownUntil.Equal(now.Add(time.Minute)) ||
got[0].CooldownUntilMS == nil ||
*got[0].CooldownUntilMS != now.Add(time.Minute).UnixMilli() ||
got[0].FailureCount != 1 {
t.Fatalf("long response = %#v", got[0])
}
@@ -146,7 +146,7 @@ func TestListGroupKeysReturnsMaskedStableRuntimeView(t *testing.T) {
got[1].Status != state.KeyStatusDisabled ||
got[1].EffectiveStatus != "disabled" ||
!got[1].Blacklisted ||
got[1].CooldownUntil != nil ||
got[1].CooldownUntilMS != nil ||
got[1].FailureCount != 3 {
t.Fatalf("short response = %#v", got[1])
}
@@ -184,7 +184,7 @@ func TestListGroupKeysEffectiveStatusPriorityAndCooldownEquality(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if got[0].EffectiveStatus != "available" || got[0].CooldownUntil != nil {
if got[0].EffectiveStatus != "available" || got[0].CooldownUntilMS != nil {
t.Fatalf("cooldown equality = %#v", got[0])
}
@@ -200,7 +200,7 @@ func TestListGroupKeysEffectiveStatusPriorityAndCooldownEquality(t *testing.T) {
t.Fatal(err)
}
if got[0].EffectiveStatus != "disabled" || !got[0].Blacklisted ||
got[0].CooldownUntil == nil {
got[0].CooldownUntilMS == nil {
t.Fatalf("disabled priority/raw state = %#v", got[0])
}
}
@@ -1086,7 +1086,7 @@ func TestUpdateGroupKeyHTTPSuccessThreeStateFields(t *testing.T) {
!equalOptionalWeight(envelope.Data.WeightManual, test.wantWeight) ||
envelope.Data.WeightAuto != state.DefaultWeight ||
envelope.Data.Blacklisted ||
envelope.Data.CooldownUntil != nil ||
envelope.Data.CooldownUntilMS != nil ||
envelope.Data.FailureCount != 0 {
t.Fatalf("PUT success envelope = %#v", envelope)
}
@@ -1665,7 +1665,7 @@ func TestUpdateGroupKeyChangesConfigAndPreservesAutomaticRuntime(t *testing.T) {
if got.Status != state.KeyStatusDisabled ||
got.WeightManual == nil || *got.WeightManual != 0 ||
got.WeightAuto != 41 || !got.Blacklisted ||
got.FailureCount != 3 || got.CooldownUntil == nil {
got.FailureCount != 3 || got.CooldownUntilMS == nil {
t.Fatalf("updated response = %#v", got)
}
if fixture.manager.Current() != beforeSnapshot {
@@ -1688,7 +1688,7 @@ func TestUpdateGroupKeyChangesConfigAndPreservesAutomaticRuntime(t *testing.T) {
}
if got.WeightManual != nil || got.WeightAuto != 41 ||
!got.Blacklisted || got.FailureCount != 3 ||
got.CooldownUntil == nil {
got.CooldownUntilMS == nil {
t.Fatalf("re-enabled response = %#v", got)
}
var persisted models.UpstreamKey
+137 -76
View File
@@ -2,18 +2,17 @@ package control
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"net/url"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/gin-gonic/gin"
"gpt-load/internal/platform/epochms"
app_errors "gpt-load/internal/platform/errors"
"gpt-load/internal/platform/response"
"gpt-load/internal/requestlog"
@@ -23,7 +22,6 @@ const (
usageRange24Hours = "24h"
usageRange30Days = "30d"
usageBreakdownMax = 100
maxSafeInteger = int64(9007199254740991)
)
type UsageStatReader interface {
@@ -31,24 +29,24 @@ type UsageStatReader interface {
}
type usageAggregateResponse struct {
RequestCount int64 `json:"request_count"`
SuccessCount int64 `json:"success_count"`
FailureCount int64 `json:"failure_count"`
UncachedInputTokens int64 `json:"uncached_input_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWrite5MTokens int64 `json:"cache_write_5m_tokens"`
CacheWrite1HTokens int64 `json:"cache_write_1h_tokens"`
OutputTokens int64 `json:"output_tokens"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostUSD json.Number `json:"estimated_cost_usd"`
UsageMissingCount int64 `json:"usage_missing_count"`
PartialCount int64 `json:"partial_count"`
UnpricedRequestCount int64 `json:"unpriced_request_count"`
RequestCount int64 `json:"request_count"`
SuccessCount int64 `json:"success_count"`
FailureCount int64 `json:"failure_count"`
UncachedInputTokens int64 `json:"uncached_input_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWrite5MTokens int64 `json:"cache_write_5m_tokens"`
CacheWrite1HTokens int64 `json:"cache_write_1h_tokens"`
OutputTokens int64 `json:"output_tokens"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
UsageMissingCount int64 `json:"usage_missing_count"`
PartialCount int64 `json:"partial_count"`
UnpricedRequestCount int64 `json:"unpriced_request_count"`
}
type usageSeriesResponse struct {
BucketStart string `json:"bucket_start"`
BucketEnd string `json:"bucket_end"`
BucketStartMS int64 `json:"bucket_start_ms"`
BucketEndMS int64 `json:"bucket_end_ms"`
usageAggregateResponse
}
@@ -59,19 +57,18 @@ type usageBreakdownResponse struct {
}
type usageCollectionHealthResponse struct {
Scope string `json:"scope"`
DroppedTotal uint64 `json:"dropped_total"`
WriteFailureTotal uint64 `json:"write_failure_total"`
LastWriteFailureAt *time.Time `json:"last_write_failure_at"`
Scope string `json:"scope"`
DroppedTotal uint64 `json:"dropped_total"`
WriteFailureTotal uint64 `json:"write_failure_total"`
LastWriteFailureAtMS *int64 `json:"last_write_failure_at_ms"`
}
type usageResponse struct {
Range string `json:"range"`
Granularity requestlog.UsageGranularity `json:"granularity"`
Timezone string `json:"timezone"`
From string `json:"from"`
To string `json:"to"`
ObservedAt string `json:"observed_at"`
FromMS int64 `json:"from_ms"`
ToMS int64 `json:"to_ms"`
ObservedAtMS int64 `json:"observed_at_ms"`
Summary usageAggregateResponse `json:"summary"`
Series []usageSeriesResponse `json:"series"`
Breakdown []usageBreakdownResponse `json:"breakdown"`
@@ -96,8 +93,12 @@ func (service *Service) QueryUsage(
}
func (server *Server) handleUsage(c *gin.Context) {
observedAt := server.service.now().UTC()
query, apiErr := parseUsageQuery(c.Request.URL.RawQuery, observedAt)
observedAtMS, err := safeEpochMilliseconds(server.service.now())
if err != nil {
writeServiceError(c, "usage", err)
return
}
query, apiErr := parseUsageQuery(c.Request.URL.RawQuery, observedAtMS)
if apiErr != nil {
writeServiceError(c, "usage", apiErr)
return
@@ -107,7 +108,7 @@ func (server *Server) handleUsage(c *gin.Context) {
writeServiceError(c, "usage", err)
return
}
result, err := server.service.mapUsageResponse(observedAt, query, report)
result, err := server.service.mapUsageResponse(observedAtMS, query, report)
if err != nil {
writeServiceError(c, "usage", err)
return
@@ -115,13 +116,15 @@ func (server *Server) handleUsage(c *gin.Context) {
response.SuccessI18n(c, "common.success", result)
}
func parseUsageQuery(rawQuery string, observedAt time.Time) (requestlog.UsageQuery, *app_errors.APIError) {
func parseUsageQuery(rawQuery string, observedAtMS int64) (requestlog.UsageQuery, *app_errors.APIError) {
values, err := url.ParseQuery(rawQuery)
if err != nil {
return requestlog.UsageQuery{}, app_errors.ErrBadRequest
}
allowed := map[string]struct{}{
"range": {},
"from_ms": {},
"to_ms": {},
"group_id": {},
"model": {},
"breakdown_order": {},
@@ -132,7 +135,9 @@ func parseUsageQuery(rawQuery string, observedAt time.Time) (requestlog.UsageQue
}
}
observedAt = observedAt.UTC()
if err := validateSafeMilliseconds(observedAtMS); err != nil {
return requestlog.UsageQuery{}, app_errors.ErrInternalServer
}
query := requestlog.UsageQuery{
Limit: usageBreakdownMax,
BreakdownOrder: requestlog.UsageBreakdownOrderRequests,
@@ -141,20 +146,65 @@ func parseUsageQuery(rawQuery string, observedAt time.Time) (requestlog.UsageQue
if value, ok := singleQueryValue(values, "range"); ok {
rangeValue = value
}
switch rangeValue {
case usageRange24Hours:
currentHour := observedAt.Truncate(time.Hour)
query.From = currentHour.Add(-23 * time.Hour)
query.To = currentHour.Add(time.Hour)
query.Granularity = requestlog.UsageGranularityHour
case usageRange30Days:
currentDay := time.Date(observedAt.Year(), observedAt.Month(), observedAt.Day(), 0, 0, 0, 0, time.UTC)
query.From = currentDay.AddDate(0, 0, -29)
query.To = currentDay.AddDate(0, 0, 1)
query.Granularity = requestlog.UsageGranularityDay
default:
fromValue, hasFrom := singleQueryValue(values, "from_ms")
toValue, hasTo := singleQueryValue(values, "to_ms")
if hasFrom != hasTo {
return requestlog.UsageQuery{}, app_errors.ErrValidation
}
if hasFrom {
if _, hasRange := singleQueryValue(values, "range"); hasRange {
return requestlog.UsageQuery{}, app_errors.ErrValidation
}
fromMS, err := parseCanonicalSafeMilliseconds(fromValue)
if err != nil {
return requestlog.UsageQuery{}, app_errors.ErrBadRequest
}
toMS, err := parseCanonicalSafeMilliseconds(toValue)
if err != nil {
return requestlog.UsageQuery{}, app_errors.ErrBadRequest
}
if fromMS >= toMS {
return requestlog.UsageQuery{}, app_errors.ErrValidation
}
query.FromMS = fromMS
query.ToMS = toMS
if toMS-fromMS <= epochms.MillisecondsPerDay {
query.Granularity = requestlog.UsageGranularityHour
} else {
query.Granularity = requestlog.UsageGranularityDay
}
rangeValue = ""
}
if !hasFrom {
switch rangeValue {
case usageRange24Hours:
fromMS, toMS, err := epochms.WindowEndingAt(
observedAtMS,
epochms.MillisecondsPerHour,
24,
)
if err != nil {
return requestlog.UsageQuery{}, app_errors.ErrInternalServer
}
query.FromMS = fromMS
query.ToMS = toMS
query.Granularity = requestlog.UsageGranularityHour
case usageRange30Days:
fromMS, toMS, err := epochms.WindowEndingAt(
observedAtMS,
epochms.MillisecondsPerDay,
30,
)
if err != nil {
return requestlog.UsageQuery{}, app_errors.ErrInternalServer
}
query.FromMS = fromMS
query.ToMS = toMS
query.Granularity = requestlog.UsageGranularityDay
default:
return requestlog.UsageQuery{}, app_errors.ErrValidation
}
}
if value, ok := singleQueryValue(values, "group_id"); ok {
groupID, apiErr := parseUsageGroupID(value)
if apiErr != nil {
@@ -195,18 +245,21 @@ func validUsageModel(value string) bool {
}
func parseUsageGroupID(value string) (uint, *app_errors.APIError) {
parsed, err := strconv.ParseUint(value, 10, 64)
parsed, err := parseCanonicalSafeUint(value)
if err != nil {
if errors.Is(err, errUnsafeCanonicalUint) {
return 0, app_errors.ErrValidation
}
return 0, app_errors.ErrBadRequest
}
if parsed == 0 || parsed > uint64(maxSafeInteger) || uint64(uint(parsed)) != parsed {
if parsed == 0 || uint64(uint(parsed)) != parsed {
return 0, app_errors.ErrValidation
}
return uint(parsed), nil
}
func (service *Service) mapUsageResponse(
observedAt time.Time,
observedAtMS int64,
query requestlog.UsageQuery,
report requestlog.UsageReport,
) (usageResponse, error) {
@@ -241,18 +294,30 @@ func (service *Service) mapUsageResponse(
default:
return usageResponse{}, fmt.Errorf("map usage response: invalid granularity")
}
lastWriteFailureAtMS, err := optionalSafeEpochMilliseconds(stats.LastWriteFailureAt)
if err != nil {
return usageResponse{}, fmt.Errorf("map usage collection health: %w", err)
}
if err := validateSafeMilliseconds(query.FromMS); err != nil {
return usageResponse{}, fmt.Errorf("map usage from_ms: %w", err)
}
if err := validateSafeMilliseconds(query.ToMS); err != nil {
return usageResponse{}, fmt.Errorf("map usage to_ms: %w", err)
}
if err := validateSafeMilliseconds(observedAtMS); err != nil {
return usageResponse{}, fmt.Errorf("map usage observed_at_ms: %w", err)
}
result := usageResponse{
Range: rangeValue,
Granularity: query.Granularity,
Timezone: "UTC",
From: query.From.UTC().Format(time.RFC3339Nano),
To: query.To.UTC().Format(time.RFC3339Nano),
ObservedAt: observedAt.UTC().Format(time.RFC3339Nano),
Range: rangeValue,
Granularity: query.Granularity,
FromMS: query.FromMS,
ToMS: query.ToMS,
ObservedAtMS: observedAtMS,
CollectionHealth: usageCollectionHealthResponse{
Scope: "current_process",
DroppedTotal: stats.DroppedTotal,
WriteFailureTotal: stats.WriteFailureTotal,
LastWriteFailureAt: optionalUTC(stats.LastWriteFailureAt),
Scope: "current_process",
DroppedTotal: stats.DroppedTotal,
WriteFailureTotal: stats.WriteFailureTotal,
LastWriteFailureAtMS: lastWriteFailureAtMS,
},
Summary: summary,
Series: make([]usageSeriesResponse, 0, len(report.Series)),
@@ -262,19 +327,25 @@ func (service *Service) mapUsageResponse(
BreakdownGroupCount: report.BreakdownGroupCount,
}
for _, point := range report.Series {
if err := validateSafeMilliseconds(point.BucketStartMS); err != nil {
return usageResponse{}, fmt.Errorf("map usage bucket_start_ms: %w", err)
}
if err := validateSafeMilliseconds(point.BucketEndMS); err != nil {
return usageResponse{}, fmt.Errorf("map usage bucket_end_ms: %w", err)
}
aggregate, err := mapUsageAggregate(point.UsageAggregate)
if err != nil {
return usageResponse{}, err
}
result.Series = append(result.Series, usageSeriesResponse{
BucketStart: point.BucketStart.UTC().Format(time.RFC3339Nano),
BucketEnd: point.BucketEnd.UTC().Format(time.RFC3339Nano),
BucketStartMS: point.BucketStartMS,
BucketEndMS: point.BucketEndMS,
usageAggregateResponse: aggregate,
})
}
for _, row := range report.Breakdown {
if row.GroupID == 0 || uint64(row.GroupID) > uint64(maxSafeInteger) || row.Model == "" {
return usageResponse{}, fmt.Errorf("map usage breakdown: invalid group or model")
if uint64(row.GroupID) > uint64(maxSafeInteger) {
return usageResponse{}, fmt.Errorf("map usage breakdown: unsafe group")
}
aggregate, err := mapUsageAggregate(row.UsageAggregate)
if err != nil {
@@ -309,16 +380,16 @@ func mapUsageAggregate(source requestlog.UsageAggregate) (usageAggregateResponse
if err != nil {
return usageAggregateResponse{}, err
}
cost, err := mapEstimatedCostUSD(source.Cost)
if err != nil {
return usageAggregateResponse{}, err
if source.EstimatedCostNanoUSD < 0 {
return usageAggregateResponse{}, fmt.Errorf("map usage cost: negative value")
}
return usageAggregateResponse{
RequestCount: source.RequestCount, SuccessCount: source.SuccessCount, FailureCount: source.FailureCount,
UncachedInputTokens: source.UncachedInputTokens, CacheReadTokens: source.CacheReadTokens,
CacheWrite5MTokens: source.CacheWrite5MTokens, CacheWrite1HTokens: source.CacheWrite1HTokens,
OutputTokens: source.OutputTokens, TotalTokens: totalTokens, EstimatedCostUSD: cost,
UsageMissingCount: source.UsageMissingCount, PartialCount: source.PartialCount,
OutputTokens: source.OutputTokens, TotalTokens: totalTokens,
EstimatedCostNanoUSD: strconv.FormatInt(source.EstimatedCostNanoUSD, 10),
UsageMissingCount: source.UsageMissingCount, PartialCount: source.PartialCount,
UnpricedRequestCount: source.UnpricedRequestCount,
}, nil
}
@@ -333,13 +404,3 @@ func checkedUsageTokenTotal(tokens ...int64) (int64, error) {
}
return total, nil
}
func mapEstimatedCostUSD(value float64) (json.Number, error) {
if value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
return "", fmt.Errorf("map usage cost: invalid value")
}
if value == 0 {
value = 0
}
return json.Number(strconv.FormatFloat(value, 'g', 12, 64)), nil
}
+137 -69
View File
@@ -4,10 +4,10 @@ import (
"context"
"encoding/json"
"errors"
"math"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
@@ -16,6 +16,7 @@ import (
"gpt-load/internal/platform/config"
"gpt-load/internal/requestlog"
"gpt-load/internal/storage/models"
)
func TestUsageAPIRouteUsesManagementAuthentication(t *testing.T) {
@@ -66,19 +67,20 @@ func TestParseUsageQueryUsesFixedUTCAlignedWindows(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
query, apiErr := parseUsageQuery(test.rawQuery, test.observedAt)
query, apiErr := parseUsageQuery(test.rawQuery, test.observedAt.UnixMilli())
if apiErr != nil {
t.Fatalf("parseUsageQuery() error = %v", apiErr)
}
if !query.From.Equal(test.wantFrom) || !query.To.Equal(test.wantTo) ||
if query.FromMS != test.wantFrom.UnixMilli() ||
query.ToMS != test.wantTo.UnixMilli() ||
query.Granularity != test.granularity {
t.Fatalf(
"parseUsageQuery() window = %s to %s (%s), want %s to %s (%s)",
query.From.Format(time.RFC3339Nano),
query.To.Format(time.RFC3339Nano),
"parseUsageQuery() window = %d to %d (%s), want %d to %d (%s)",
query.FromMS,
query.ToMS,
query.Granularity,
test.wantFrom.Format(time.RFC3339Nano),
test.wantTo.Format(time.RFC3339Nano),
test.wantFrom.UnixMilli(),
test.wantTo.UnixMilli(),
test.granularity,
)
}
@@ -106,8 +108,8 @@ func TestUsageAPIDefaultsToFixedUTCAligned24HoursAndReturnsZeroArrays(t *testing
}
query := reader.queries[0]
if query.Granularity != requestlog.UsageGranularityHour ||
query.From.Format(time.RFC3339Nano) != "2026-07-26T05:00:00Z" ||
query.To.Format(time.RFC3339Nano) != "2026-07-27T05:00:00Z" ||
query.FromMS != time.Date(2026, time.July, 26, 5, 0, 0, 0, time.UTC).UnixMilli() ||
query.ToMS != time.Date(2026, time.July, 27, 5, 0, 0, 0, time.UTC).UnixMilli() ||
query.GroupID != nil || query.Model != "" || query.Limit != 100 ||
query.BreakdownOrder != requestlog.UsageBreakdownOrderRequests {
t.Fatalf("default UsageQuery = %#v", query)
@@ -115,21 +117,20 @@ func TestUsageAPIDefaultsToFixedUTCAligned24HoursAndReturnsZeroArrays(t *testing
var envelope struct {
Code int `json:"code"`
Data struct {
Range string `json:"range"`
Granularity requestlog.UsageGranularity `json:"granularity"`
Timezone string `json:"timezone"`
From string `json:"from"`
To string `json:"to"`
ObservedAt string `json:"observed_at"`
Series []json.RawMessage `json:"series"`
Breakdown []json.RawMessage `json:"breakdown"`
Order requestlog.UsageBreakdownOrder `json:"breakdown_order"`
GroupCount int64 `json:"breakdown_group_count"`
Health struct {
Scope string `json:"scope"`
DroppedTotal uint64 `json:"dropped_total"`
WriteFailureTotal uint64 `json:"write_failure_total"`
LastWriteFailureAt *time.Time `json:"last_write_failure_at"`
Range string `json:"range"`
Granularity requestlog.UsageGranularity `json:"granularity"`
FromMS int64 `json:"from_ms"`
ToMS int64 `json:"to_ms"`
ObservedAtMS int64 `json:"observed_at_ms"`
Series []json.RawMessage `json:"series"`
Breakdown []json.RawMessage `json:"breakdown"`
Order requestlog.UsageBreakdownOrder `json:"breakdown_order"`
GroupCount int64 `json:"breakdown_group_count"`
Health struct {
Scope string `json:"scope"`
DroppedTotal uint64 `json:"dropped_total"`
WriteFailureTotal uint64 `json:"write_failure_total"`
LastWriteFailureAtMS *int64 `json:"last_write_failure_at_ms"`
} `json:"collection_health"`
} `json:"data"`
}
@@ -138,10 +139,9 @@ func TestUsageAPIDefaultsToFixedUTCAligned24HoursAndReturnsZeroArrays(t *testing
}
if envelope.Code != 0 || envelope.Data.Range != "24h" ||
envelope.Data.Granularity != requestlog.UsageGranularityHour ||
envelope.Data.Timezone != "UTC" ||
envelope.Data.From != "2026-07-26T05:00:00Z" ||
envelope.Data.To != "2026-07-27T05:00:00Z" ||
envelope.Data.ObservedAt != "2026-07-27T04:34:56.000000789Z" ||
envelope.Data.FromMS != time.Date(2026, time.July, 26, 5, 0, 0, 0, time.UTC).UnixMilli() ||
envelope.Data.ToMS != time.Date(2026, time.July, 27, 5, 0, 0, 0, time.UTC).UnixMilli() ||
envelope.Data.ObservedAtMS != now.UnixMilli() ||
envelope.Data.Series == nil || len(envelope.Data.Series) != 0 ||
envelope.Data.Breakdown == nil || len(envelope.Data.Breakdown) != 0 ||
envelope.Data.Order != requestlog.UsageBreakdownOrderRequests ||
@@ -149,8 +149,8 @@ func TestUsageAPIDefaultsToFixedUTCAligned24HoursAndReturnsZeroArrays(t *testing
envelope.Data.Health.Scope != "current_process" ||
envelope.Data.Health.DroppedTotal != 2 ||
envelope.Data.Health.WriteFailureTotal != 1 ||
envelope.Data.Health.LastWriteFailureAt == nil ||
envelope.Data.Health.LastWriteFailureAt.Format(time.RFC3339Nano) != "2026-07-27T03:00:00Z" {
envelope.Data.Health.LastWriteFailureAtMS == nil ||
*envelope.Data.Health.LastWriteFailureAtMS != time.Date(2026, time.July, 27, 3, 0, 0, 0, time.UTC).UnixMilli() {
t.Fatalf("default usage envelope = %#v", envelope)
}
var rawEnvelope struct {
@@ -159,7 +159,10 @@ func TestUsageAPIDefaultsToFixedUTCAligned24HoursAndReturnsZeroArrays(t *testing
if err := json.Unmarshal(recorder.Body.Bytes(), &rawEnvelope); err != nil {
t.Fatalf("decode raw response: %v", err)
}
for _, forbidden := range []string{"filters", "request_log"} {
for _, forbidden := range []string{
"filters", "request_log", "timezone", "from", "to", "observed_at",
"estimated_cost" + "_usd",
} {
if _, exists := rawEnvelope.Data[forbidden]; exists {
t.Fatalf("usage response exposes forbidden %q field: %s", forbidden, recorder.Body.String())
}
@@ -169,17 +172,21 @@ func TestUsageAPIDefaultsToFixedUTCAligned24HoursAndReturnsZeroArrays(t *testing
func TestUsageAPISelectsThirtyUTCDaysAndAppliesFilters(t *testing.T) {
now := time.Date(2026, time.July, 27, 12, 0, 0, 0, time.UTC)
reader := &recordingUsageStatReader{report: requestlog.UsageReport{
Summary: requestlog.UsageAggregate{RequestCount: 1, UncachedInputTokens: 2, OutputTokens: 3, Cost: math.Copysign(0, -1)},
Summary: requestlog.UsageAggregate{RequestCount: 1, UncachedInputTokens: 2, OutputTokens: 3},
Series: []requestlog.UsageSeriesPoint{{
BucketStart: time.Date(2026, time.June, 28, 0, 0, 0, 0, time.UTC),
BucketEnd: time.Date(2026, time.June, 29, 0, 0, 0, 0, time.UTC),
BucketStartMS: time.Date(2026, time.June, 28, 0, 0, 0, 0, time.UTC).UnixMilli(),
BucketEndMS: time.Date(2026, time.June, 29, 0, 0, 0, 0, time.UTC).UnixMilli(),
UsageAggregate: requestlog.UsageAggregate{
RequestCount: 1, UncachedInputTokens: 2, OutputTokens: 3, Cost: 1.1234567890123,
RequestCount: 1, UncachedInputTokens: 2, OutputTokens: 3,
EstimatedCostNanoUSD: 1_123_456_789_012,
},
}},
Breakdown: []requestlog.UsageBreakdown{{
GroupID: 9, Model: "upstream-model",
UsageAggregate: requestlog.UsageAggregate{RequestCount: 1, UncachedInputTokens: 2, OutputTokens: 3, Cost: 0.25},
UsageAggregate: requestlog.UsageAggregate{
RequestCount: 1, UncachedInputTokens: 2, OutputTokens: 3,
EstimatedCostNanoUSD: 250_000_000,
},
}},
BreakdownOrder: requestlog.UsageBreakdownOrderCost,
BreakdownGroupCount: 1,
@@ -198,8 +205,8 @@ func TestUsageAPISelectsThirtyUTCDaysAndAppliesFilters(t *testing.T) {
}
query := reader.queries[0]
if query.Granularity != requestlog.UsageGranularityDay ||
query.From.Format(time.RFC3339Nano) != "2026-06-28T00:00:00Z" ||
query.To.Format(time.RFC3339Nano) != "2026-07-28T00:00:00Z" ||
query.FromMS != time.Date(2026, time.June, 28, 0, 0, 0, 0, time.UTC).UnixMilli() ||
query.ToMS != time.Date(2026, time.July, 28, 0, 0, 0, 0, time.UTC).UnixMilli() ||
query.GroupID == nil || *query.GroupID != 9 ||
query.Model != "upstream-model" || query.Limit != 100 ||
query.BreakdownOrder != requestlog.UsageBreakdownOrderCost {
@@ -209,24 +216,23 @@ func TestUsageAPISelectsThirtyUTCDaysAndAppliesFilters(t *testing.T) {
Data struct {
Range string `json:"range"`
Granularity requestlog.UsageGranularity `json:"granularity"`
Timezone string `json:"timezone"`
From string `json:"from"`
To string `json:"to"`
FromMS int64 `json:"from_ms"`
ToMS int64 `json:"to_ms"`
Order requestlog.UsageBreakdownOrder `json:"breakdown_order"`
GroupCount int64 `json:"breakdown_group_count"`
Summary struct {
TotalTokens int64 `json:"total_tokens"`
EstimatedCostUSD json.Number `json:"estimated_cost_usd"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
} `json:"summary"`
Series []struct {
BucketStart string `json:"bucket_start"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostUSD json.Number `json:"estimated_cost_usd"`
BucketStartMS int64 `json:"bucket_start_ms"`
TotalTokens int64 `json:"total_tokens"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
} `json:"series"`
Breakdown []struct {
GroupID uint `json:"group_id"`
Model string `json:"model"`
Cost json.Number `json:"estimated_cost_usd"`
GroupID uint `json:"group_id"`
Model string `json:"model"`
EstimatedCostNanoUSD string `json:"estimated_cost_nano_usd"`
} `json:"breakdown"`
} `json:"data"`
}
@@ -235,17 +241,19 @@ func TestUsageAPISelectsThirtyUTCDaysAndAppliesFilters(t *testing.T) {
}
if envelope.Data.Range != "30d" ||
envelope.Data.Granularity != requestlog.UsageGranularityDay ||
envelope.Data.Timezone != "UTC" ||
envelope.Data.From != "2026-06-28T00:00:00Z" ||
envelope.Data.To != "2026-07-28T00:00:00Z" ||
envelope.Data.FromMS != time.Date(2026, time.June, 28, 0, 0, 0, 0, time.UTC).UnixMilli() ||
envelope.Data.ToMS != time.Date(2026, time.July, 28, 0, 0, 0, 0, time.UTC).UnixMilli() ||
envelope.Data.Order != requestlog.UsageBreakdownOrderCost ||
envelope.Data.GroupCount != 1 ||
envelope.Data.Summary.TotalTokens != 5 ||
envelope.Data.Summary.EstimatedCostUSD.String() != "0" ||
len(envelope.Data.Series) != 1 || envelope.Data.Series[0].BucketStart != "2026-06-28T00:00:00Z" ||
envelope.Data.Series[0].TotalTokens != 5 || envelope.Data.Series[0].EstimatedCostUSD.String() != "1.12345678901" ||
envelope.Data.Summary.EstimatedCostNanoUSD != "0" ||
len(envelope.Data.Series) != 1 ||
envelope.Data.Series[0].BucketStartMS != time.Date(2026, time.June, 28, 0, 0, 0, 0, time.UTC).UnixMilli() ||
envelope.Data.Series[0].TotalTokens != 5 ||
envelope.Data.Series[0].EstimatedCostNanoUSD != "1123456789012" ||
len(envelope.Data.Breakdown) != 1 || envelope.Data.Breakdown[0].GroupID != 9 ||
envelope.Data.Breakdown[0].Model != "upstream-model" || envelope.Data.Breakdown[0].Cost.String() != "0.25" {
envelope.Data.Breakdown[0].Model != "upstream-model" ||
envelope.Data.Breakdown[0].EstimatedCostNanoUSD != "250000000" {
t.Fatalf("usage response = %#v", envelope.Data)
}
}
@@ -304,8 +312,18 @@ func TestUsageAPIRejectsStrictInvalidQueriesWithoutCallingReader(t *testing.T) {
}{
{query: "unknown=1", code: "BAD_REQUEST"},
{query: "range=24h&range=30d", code: "BAD_REQUEST"},
{query: "from=1&to=2", code: "BAD_REQUEST"},
{query: "from_ms=1", code: "VALIDATION_FAILED"},
{query: "from_ms=01&to_ms=2", code: "BAD_REQUEST"},
{query: "from_ms=-1&to_ms=2", code: "BAD_REQUEST"},
{query: "from_ms=1&to_ms=1", code: "VALIDATION_FAILED"},
{query: "range=24h&from_ms=1&to_ms=2", code: "VALIDATION_FAILED"},
{query: "range=1h", code: "VALIDATION_FAILED"},
{query: "group_id=0", code: "VALIDATION_FAILED"},
{query: "group_id=01", code: "BAD_REQUEST"},
{query: "group_id=%2B1", code: "BAD_REQUEST"},
{query: "group_id=%201", code: "BAD_REQUEST"},
{query: "group_id=1&group_id=2", code: "BAD_REQUEST"},
{query: "group_id=9007199254740992", code: "VALIDATION_FAILED"},
{query: "group_id=-1", code: "BAD_REQUEST"},
{query: "model=", code: "VALIDATION_FAILED"},
@@ -330,8 +348,8 @@ func TestUsageAPIRejectsStrictInvalidQueriesWithoutCallingReader(t *testing.T) {
func TestMapUsageRejectsUnsafeOrMismatchedBreakdownMetadata(t *testing.T) {
fixture := newServiceFixture(t)
query := requestlog.UsageQuery{
From: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
To: time.Date(2026, time.July, 28, 0, 0, 0, 0, time.UTC),
FromMS: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC).UnixMilli(),
ToMS: time.Date(2026, time.July, 28, 0, 0, 0, 0, time.UTC).UnixMilli(),
Granularity: requestlog.UsageGranularityHour,
Limit: 100,
BreakdownOrder: requestlog.UsageBreakdownOrderRequests,
@@ -364,7 +382,7 @@ func TestMapUsageRejectsUnsafeOrMismatchedBreakdownMetadata(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := fixture.service.mapUsageResponse(
query.To,
query.ToMS,
query,
test.report,
); err == nil {
@@ -421,16 +439,66 @@ func TestUsageAPIRejectsUnsafeProcessStatsWithoutLeakingCause(t *testing.T) {
}
}
func TestUsageAPIRejectsUnattributedBreakdownWithoutLeakingCause(t *testing.T) {
reader := &recordingUsageStatReader{report: requestlog.UsageReport{
Breakdown: []requestlog.UsageBreakdown{{GroupID: 0, Model: "unattributed"}},
}}
engine, _ := newUsageTestEngine(t, time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC), reader)
func TestUsageAPIReturnsUnattributedAggregateFromSQLite(t *testing.T) {
initControlI18n(t)
now := time.Date(2026, time.July, 27, 12, 0, 0, 0, time.UTC)
fixture := newServiceFixture(t)
if err := fixture.db.Create(&models.UsageStat{
BucketStartMS: now.Add(-time.Hour).UnixMilli(),
AccessKeyID: 0,
GroupID: 0,
Model: "",
RequestCount: 1,
SuccessCount: 1,
}).Error; err != nil {
t.Fatalf("create unattributed UsageStat: %v", err)
}
fixture.service.now = func() time.Time { return now }
fixture.service.usageStats = requestlog.NewService(fixture.db, nil, nil, nil)
engine := gin.New()
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
recorder := performUsageRequest(engine, "test-auth-key", "")
if recorder.Code != http.StatusInternalServerError ||
!strings.Contains(recorder.Body.String(), "INTERNAL_SERVER_ERROR") ||
strings.Contains(strings.ToLower(recorder.Body.String()), "group") {
t.Fatalf("response = %d %s", recorder.Code, recorder.Body.String())
if recorder.Code != http.StatusOK {
t.Fatalf("response = %d %s, want 200", recorder.Code, recorder.Body.String())
}
var envelope struct {
Data struct {
Breakdown []struct {
GroupID uint `json:"group_id"`
Model string `json:"model"`
RequestCount int64 `json:"request_count"`
} `json:"breakdown"`
} `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode response: %v", err)
}
if len(envelope.Data.Breakdown) != 1 ||
envelope.Data.Breakdown[0].GroupID != 0 ||
envelope.Data.Breakdown[0].Model != "" ||
envelope.Data.Breakdown[0].RequestCount != 1 {
t.Fatalf("unattributed breakdown = %#v", envelope.Data.Breakdown)
}
}
func TestUsageAPIAcceptsMaximumSafeCanonicalGroupID(t *testing.T) {
if strconv.IntSize != 64 {
t.Skip("maximum JavaScript safe integer does not fit uint on this architecture")
}
reader := &recordingUsageStatReader{}
engine, _ := newUsageTestEngine(
t,
time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
reader,
)
recorder := performUsageRequest(engine, "test-auth-key", "group_id=9007199254740991")
if recorder.Code != http.StatusOK {
t.Fatalf("response = %d %s, want 200", recorder.Code, recorder.Body.String())
}
if len(reader.queries) != 1 || reader.queries[0].GroupID == nil ||
uint64(*reader.queries[0].GroupID) != uint64(maxSafeInteger) {
t.Fatalf("QueryUsage calls = %#v", reader.queries)
}
}
+67
View File
@@ -0,0 +1,67 @@
package control
import (
"errors"
"fmt"
"strconv"
"time"
"gpt-load/internal/platform/epochms"
)
const maxSafeInteger = int64(9_007_199_254_740_991)
var errUnsafeCanonicalUint = errors.New(
"management unsigned integer is outside the JSON safe integer range",
)
func safeEpochMilliseconds(value time.Time) (int64, error) {
milliseconds, err := epochms.FromTime(value)
if err != nil {
return 0, fmt.Errorf("map management timestamp: %w", err)
}
if milliseconds > maxSafeInteger {
return 0, fmt.Errorf("map management timestamp: unsafe integer")
}
return milliseconds, nil
}
func optionalSafeEpochMilliseconds(value time.Time) (*int64, error) {
if value.IsZero() {
return nil, nil
}
milliseconds, err := safeEpochMilliseconds(value)
if err != nil {
return nil, err
}
return &milliseconds, nil
}
func validateSafeMilliseconds(value int64) error {
if value < 0 || value > maxSafeInteger {
return fmt.Errorf("management timestamp is outside the JSON safe integer range")
}
return nil
}
func parseCanonicalSafeMilliseconds(value string) (int64, error) {
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil || strconv.FormatInt(parsed, 10) != value {
return 0, fmt.Errorf("management timestamp must be canonical base-10")
}
if err := validateSafeMilliseconds(parsed); err != nil {
return 0, err
}
return parsed, nil
}
func parseCanonicalSafeUint(value string) (uint64, error) {
parsed, err := strconv.ParseUint(value, 10, 64)
if err != nil || strconv.FormatUint(parsed, 10) != value {
return 0, fmt.Errorf("management unsigned integer must be canonical base-10")
}
if parsed > uint64(maxSafeInteger) {
return 0, errUnsafeCanonicalUint
}
return parsed, nil
}
+226
View File
@@ -0,0 +1,226 @@
package control
import (
"encoding/json"
"strings"
"testing"
)
func TestAccessKeyMillisWireOmitsLegacyTimestampKeys(t *testing.T) {
encoded, err := json.Marshal(AccessKeyMetadata{
ID: 7,
CreatedAtMS: 1_784_894_400_000,
UpdatedAtMS: 1_784_898_000_000,
})
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
text := string(encoded)
if !strings.Contains(text, `"created_at_ms":1784894400000`) ||
!strings.Contains(text, `"updated_at_ms":1784898000000`) ||
strings.Contains(text, `"created_at"`) ||
strings.Contains(text, `"updated_at"`) {
t.Fatalf("AccessKeyMetadata JSON = %s", text)
}
}
func TestRevealMillisWireOmitsLegacyTimestampKey(t *testing.T) {
assertManagementWireObject(
t,
AccessKeyRevealResult{ID: 7, Key: "secret", RevealedAtMS: 1_784_894_400_000},
[]string{"id", "key", "revealed_at_ms"},
)
}
func TestHealthMillisWireUsesNullableEpochMilliseconds(t *testing.T) {
cooldownUntilMS := int64(1_784_894_460_000)
assertManagementWireObject(
t,
runtimeHealthResponse{
ObservedAtMS: 1_784_894_400_000,
CooldownKeys: []healthProblemKeyResponse{{
CooldownUntilMS: &cooldownUntilMS,
Recovery: healthRecoveryResponse{
Automatic: true,
Mode: "cooldown_expiry",
AtMS: &cooldownUntilMS,
},
}},
RequestLog: requestLogHealthResponse{
LastWriteFailureAtMS: &cooldownUntilMS,
},
},
[]string{
"observed_at_ms",
"cooldown_keys",
"request_log",
},
)
}
func TestInspectAndUpstreamMillisWireUseEpochMilliseconds(t *testing.T) {
cooldownUntilMS := int64(1_784_894_460_000)
assertManagementWireObject(
t,
routeInspectResponse{
ObservedAtMS: 1_784_894_400_000,
Groups: []routeInspectGroupResponse{{
Keys: []routeInspectKeyResponse{{
CooldownUntilMS: &cooldownUntilMS,
}},
}},
},
[]string{"observed_at_ms", "groups"},
)
assertManagementWireObject(
t,
UpstreamKeyResponse{CooldownUntilMS: &cooldownUntilMS},
[]string{"cooldown_until_ms"},
)
}
func TestRequestLogMillisCostWireUsesCursorV2Fields(t *testing.T) {
assertManagementWireObject(
t,
requestLogCursorPayload{
Version: requestLogCursorV2,
CompletedAtMS: 1_784_894_400_000,
RequestID: "00000000-0000-4000-8000-000000000001",
},
[]string{"v", "completed_at_ms", "request_id"},
)
assertManagementWireObject(
t,
requestLogItemResponse{
CompletedAtMS: 1_784_894_400_000,
EstimatedCostNanoUSD: "125000000",
},
[]string{"completed_at_ms", "estimated_cost_nano_usd"},
)
}
func TestUsageMillisCostWireUsesIntegerBucketsAndStringCost(t *testing.T) {
assertManagementWireObject(
t,
usageResponse{
FromMS: 1_784_894_400_000,
ToMS: 1_784_898_000_000,
ObservedAtMS: 1_784_895_000_000,
Summary: usageAggregateResponse{
EstimatedCostNanoUSD: "125000000",
},
Series: []usageSeriesResponse{{
BucketStartMS: 1_784_894_400_000,
BucketEndMS: 1_784_898_000_000,
}},
},
[]string{"from_ms", "to_ms", "observed_at_ms", "summary", "series"},
)
}
func TestModelPriceWireUsesFiveNullableDecimalStrings(t *testing.T) {
zero := "0"
encoded, err := json.Marshal(modelPriceRuleResponse{
Prices: modelPriceValuesResponse{
InputPrice: &zero,
},
UpdatedAtMS: 1_784_894_400_000,
})
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var document struct {
Prices map[string]*string `json:"prices"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
if err := json.Unmarshal(encoded, &document); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
wantPriceKeys := []string{
"input_price_usd_per_million_tokens",
"output_price_usd_per_million_tokens",
"cache_read_price_usd_per_million_tokens",
"cache_write_5m_price_usd_per_million_tokens",
"cache_write_1h_price_usd_per_million_tokens",
}
if len(document.Prices) != len(wantPriceKeys) ||
document.UpdatedAtMS != 1_784_894_400_000 {
t.Fatalf("ModelPrice JSON = %s", encoded)
}
for _, key := range wantPriceKeys {
if _, exists := document.Prices[key]; !exists {
t.Fatalf("ModelPrice JSON missing %q: %s", key, encoded)
}
}
}
func TestIdempotencyOperationMillisWireOmitsLegacyTimestampKey(t *testing.T) {
assertManagementWireObject(
t,
operationExpiredData{
OperationID: "00000000-0000-4000-8000-000000000001",
OperationKind: operationKindAccessKeyCreate,
CompletedAtMS: 1_784_894_400_000,
},
[]string{"operation_id", "operation_kind", "resource_identity", "completed_at_ms"},
)
}
func assertManagementWireObject(t *testing.T, value any, requiredKeys []string) {
t.Helper()
encoded, err := json.Marshal(value)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var document map[string]json.RawMessage
if err := json.Unmarshal(encoded, &document); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
for _, key := range requiredKeys {
if _, exists := document[key]; !exists {
t.Fatalf("management wire missing %q: %s", key, encoded)
}
}
var decoded any
if err := json.Unmarshal(encoded, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
assertNoLegacyManagementWireKeys(t, decoded, encoded)
}
func assertNoLegacyManagementWireKeys(t *testing.T, value any, encoded []byte) {
t.Helper()
legacyKeys := map[string]struct{}{}
for _, key := range []string{
"created_at",
"updated_at",
"completed_at",
"observed_at",
"cooldown_until",
"bucket_start",
"bucket_end",
"revealed_at",
"from",
"to",
"estimated_cost" + "_usd",
} {
legacyKeys[key] = struct{}{}
}
var visit func(any)
visit = func(current any) {
switch typed := current.(type) {
case map[string]any:
for key, child := range typed {
if _, legacy := legacyKeys[key]; legacy {
t.Fatalf("management wire exposes legacy key %q: %s", key, encoded)
}
visit(child)
}
case []any:
for _, child := range typed {
visit(child)
}
}
}
visit(value)
}
+6 -5
View File
@@ -533,15 +533,16 @@ func TestUsageAnthropicServerToolDetailRemainsUnpriced(t *testing.T) {
table, err := pricing.Compile([]pricing.Rule{{
Pattern: "claude-test",
Prices: pricing.Prices{
UncachedInput: pricing.Price{Value: 1, Set: true},
Output: pricing.Price{Value: 1, Set: true},
UncachedInput: pricing.Price{NanoUSDPerMillion: 1_000_000_000, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 1_000_000_000, Set: true},
},
Source: pricing.SourceUser,
}})
if err != nil {
t.Fatal(err)
}
if quote := table.Quote("claude-test", result); quote.State != pricing.CostStateUnpriced || quote.Cost != 0 {
if quote := table.Quote("claude-test", result); quote.State != pricing.CostStateUnpriced ||
quote.EstimatedCostNanoUSD != 0 {
t.Fatalf("Quote() = %+v, want unpriced zero", quote)
}
}
@@ -550,8 +551,8 @@ func TestUsageAnthropicNonStreamServerToolDetailPricing(t *testing.T) {
table, err := pricing.Compile([]pricing.Rule{{
Pattern: "claude-test",
Prices: pricing.Prices{
UncachedInput: pricing.Price{Value: 1, Set: true},
Output: pricing.Price{Value: 1, Set: true},
UncachedInput: pricing.Price{NanoUSDPerMillion: 1_000_000_000, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 1_000_000_000, Set: true},
},
Source: pricing.SourceUser,
}})
+69
View File
@@ -0,0 +1,69 @@
// Package epochms provides checked Unix epoch millisecond primitives.
package epochms
import (
"errors"
"math"
"time"
)
const (
MillisecondsPerHour int64 = 3_600_000
MillisecondsPerDay int64 = 86_400_000
)
var errInvalidEpochMilliseconds = errors.New("epoch milliseconds must be non-negative")
// FromTime converts value to a non-negative Unix epoch millisecond timestamp.
func FromTime(value time.Time) (int64, error) {
milliseconds := value.UTC().UnixMilli()
if milliseconds < 0 {
return 0, errInvalidEpochMilliseconds
}
return milliseconds, nil
}
// ToTime converts a non-negative Unix epoch millisecond timestamp to UTC.
func ToTime(value int64) (time.Time, error) {
if value < 0 {
return time.Time{}, errInvalidEpochMilliseconds
}
return time.Unix(value/1_000, (value%1_000)*int64(time.Millisecond)).UTC(), nil
}
// AlignDown returns the Unix epoch boundary at or before value.
func AlignDown(value, width int64) (int64, error) {
if value < 0 {
return 0, errInvalidEpochMilliseconds
}
if width <= 0 {
return 0, errors.New("epoch millisecond width must be positive")
}
return value - value%width, nil
}
// WindowEndingAt returns count full buckets ending after observedAt's bucket.
func WindowEndingAt(observedAt, width int64, count int) (from, to int64, err error) {
if count <= 0 {
return 0, 0, errors.New("epoch millisecond window count must be positive")
}
start, err := AlignDown(observedAt, width)
if err != nil {
return 0, 0, err
}
if start > math.MaxInt64-width {
return 0, 0, errors.New("epoch millisecond bucket end overflows int64")
}
to = start + width
windowWidth := int64(count)
if width > math.MaxInt64/windowWidth {
return 0, 0, errors.New("epoch millisecond window width overflows int64")
}
windowWidth *= width
if windowWidth > to {
return 0, 0, errors.New("epoch millisecond window begins before the Unix epoch")
}
return to - windowWidth, to, nil
}
+111
View File
@@ -0,0 +1,111 @@
package epochms
import (
"math"
"testing"
"time"
)
func TestFromTimeNormalizesToUTCAndTruncatesToMilliseconds(t *testing.T) {
value := time.Date(2026, 7, 30, 8, 9, 10, 987_654_321, time.FixedZone("UTC+8", 8*60*60))
got, err := FromTime(value)
if err != nil {
t.Fatalf("FromTime() error = %v", err)
}
if want := int64(1_785_370_150_987); got != want {
t.Fatalf("FromTime() = %d, want %d", got, want)
}
}
func TestToTimeRestoresInstantAtMillisecondPrecision(t *testing.T) {
got, err := ToTime(1_785_370_150_987)
if err != nil {
t.Fatalf("ToTime() error = %v", err)
}
want := time.Date(2026, 7, 30, 0, 9, 10, 987_000_000, time.UTC)
if !got.Equal(want) || got.Location() != time.UTC {
t.Fatalf("ToTime() = %s (%s), want %s (UTC)", got, got.Location(), want)
}
}
func TestAlignDownUsesUnixEpochBoundaries(t *testing.T) {
tests := []struct {
name string
value, width int64
want int64
}{
{name: "hour", value: 1_785_373_750_987, width: MillisecondsPerHour, want: 1_785_373_200_000},
{name: "day", value: 1_785_370_150_987, width: MillisecondsPerDay, want: 1_785_369_600_000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := AlignDown(tt.value, tt.width)
if err != nil {
t.Fatalf("AlignDown() error = %v", err)
}
if got != tt.want {
t.Fatalf("AlignDown(%d, %d) = %d, want %d", tt.value, tt.width, got, tt.want)
}
})
}
}
func TestEpochMillisecondsRejectInvalidPersistentTimeAndWindowOverflow(t *testing.T) {
if _, err := FromTime(time.Unix(-1, 0)); err == nil {
t.Fatal("FromTime() accepted a negative instant")
}
if _, err := ToTime(-1); err == nil {
t.Fatal("ToTime() accepted a negative instant")
}
for _, width := range []int64{0, -1} {
if _, err := AlignDown(1, width); err == nil {
t.Fatalf("AlignDown() accepted width %d", width)
}
}
if _, err := AlignDown(-1, MillisecondsPerHour); err == nil {
t.Fatal("AlignDown() accepted a negative instant")
}
if _, _, err := WindowEndingAt(math.MaxInt64, 2, 1); err == nil {
t.Fatal("WindowEndingAt() accepted an overflowing bucket end")
}
}
func TestWindowEndingAtIncludesCurrentBucket(t *testing.T) {
from, to, err := WindowEndingAt(10_801_234, MillisecondsPerHour, 3)
if err != nil {
t.Fatalf("WindowEndingAt() error = %v", err)
}
if from != 3_600_000 || to != 14_400_000 {
t.Fatalf("WindowEndingAt() = [%d, %d), want [3600000, 14400000)", from, to)
}
}
func TestWindowEndingAtRejectsInvalidCount(t *testing.T) {
for _, test := range []struct {
name string
count int
}{
{name: "zero", count: 0},
{name: "negative", count: -1},
} {
t.Run(test.name, func(t *testing.T) {
if _, _, err := WindowEndingAt(0, MillisecondsPerHour, test.count); err == nil {
t.Fatalf("WindowEndingAt() accepted count %d", test.count)
}
})
}
}
func TestWindowEndingAtRejectsWindowWidthOverflow(t *testing.T) {
if _, _, err := WindowEndingAt(0, math.MaxInt64, 2); err == nil {
t.Fatal("WindowEndingAt() accepted a window width that overflows int64")
}
}
func TestWindowEndingAtRejectsWindowBeforeUnixEpoch(t *testing.T) {
if _, _, err := WindowEndingAt(1, MillisecondsPerHour, 2); err == nil {
t.Fatal("WindowEndingAt() accepted a window beginning before the Unix epoch")
}
}
+45 -37
View File
@@ -11,41 +11,41 @@ const (
var builtinUpdatedAt = time.Date(2026, 7, 26, 0, 0, 0, 0, time.UTC)
var builtinRules = []Rule{
{Pattern: "gpt-5.6", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, Output: Price{Value: 30, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.6-sol", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, Output: Price{Value: 30, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.6-terra", Prices: Prices{UncachedInput: Price{Value: 2.5, Set: true}, CacheRead: Price{Value: 0.25, Set: true}, Output: Price{Value: 15, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.6-luna", Prices: Prices{UncachedInput: Price{Value: 1, Set: true}, CacheRead: Price{Value: 0.1, Set: true}, Output: Price{Value: 6, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.5", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, Output: Price{Value: 30, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.5-pro", Prices: Prices{UncachedInput: Price{Value: 30, Set: true}, Output: Price{Value: 180, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-5.4", Prices: Prices{UncachedInput: Price{Value: 2.5, Set: true}, CacheRead: Price{Value: 0.25, Set: true}, Output: Price{Value: 15, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.4-mini", Prices: Prices{UncachedInput: Price{Value: 0.75, Set: true}, CacheRead: Price{Value: 0.075, Set: true}, Output: Price{Value: 4.5, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-5.4-nano", Prices: Prices{UncachedInput: Price{Value: 0.2, Set: true}, CacheRead: Price{Value: 0.02, Set: true}, Output: Price{Value: 1.25, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-5.4-pro", Prices: Prices{UncachedInput: Price{Value: 30, Set: true}, Output: Price{Value: 180, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-4.1", Prices: Prices{UncachedInput: Price{Value: 2, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, Output: Price{Value: 8, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-4.1-mini", Prices: Prices{UncachedInput: Price{Value: 0.4, Set: true}, CacheRead: Price{Value: 0.1, Set: true}, Output: Price{Value: 1.6, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-4.1-nano", Prices: Prices{UncachedInput: Price{Value: 0.1, Set: true}, CacheRead: Price{Value: 0.025, Set: true}, Output: Price{Value: 0.4, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-4o", Prices: Prices{UncachedInput: Price{Value: 2.5, Set: true}, CacheRead: Price{Value: 1.25, Set: true}, Output: Price{Value: 10, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-4o-mini", Prices: Prices{UncachedInput: Price{Value: 0.15, Set: true}, CacheRead: Price{Value: 0.075, Set: true}, Output: Price{Value: 0.6, Set: true}}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-fable-5", Prices: Prices{UncachedInput: Price{Value: 10, Set: true}, CacheRead: Price{Value: 1, Set: true}, CacheWrite5M: Price{Value: 12.5, Set: true}, CacheWrite1H: Price{Value: 20, Set: true}, Output: Price{Value: 50, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-5", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, CacheWrite5M: Price{Value: 6.25, Set: true}, CacheWrite1H: Price{Value: 10, Set: true}, Output: Price{Value: 25, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-8", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, CacheWrite5M: Price{Value: 6.25, Set: true}, CacheWrite1H: Price{Value: 10, Set: true}, Output: Price{Value: 25, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-7", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, CacheWrite5M: Price{Value: 6.25, Set: true}, CacheWrite1H: Price{Value: 10, Set: true}, Output: Price{Value: 25, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-6", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, CacheWrite5M: Price{Value: 6.25, Set: true}, CacheWrite1H: Price{Value: 10, Set: true}, Output: Price{Value: 25, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-5", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, CacheWrite5M: Price{Value: 6.25, Set: true}, CacheWrite1H: Price{Value: 10, Set: true}, Output: Price{Value: 25, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-5-20251101", Prices: Prices{UncachedInput: Price{Value: 5, Set: true}, CacheRead: Price{Value: 0.5, Set: true}, CacheWrite5M: Price{Value: 6.25, Set: true}, CacheWrite1H: Price{Value: 10, Set: true}, Output: Price{Value: 25, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-sonnet-5", Prices: Prices{UncachedInput: Price{Value: 2, Set: true}, CacheRead: Price{Value: 0.2, Set: true}, CacheWrite5M: Price{Value: 2.5, Set: true}, CacheWrite1H: Price{Value: 4, Set: true}, Output: Price{Value: 10, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-sonnet-4-6", Prices: Prices{UncachedInput: Price{Value: 3, Set: true}, CacheRead: Price{Value: 0.3, Set: true}, CacheWrite5M: Price{Value: 3.75, Set: true}, CacheWrite1H: Price{Value: 6, Set: true}, Output: Price{Value: 15, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-sonnet-4-5", Prices: Prices{UncachedInput: Price{Value: 3, Set: true}, CacheRead: Price{Value: 0.3, Set: true}, CacheWrite5M: Price{Value: 3.75, Set: true}, CacheWrite1H: Price{Value: 6, Set: true}, Output: Price{Value: 15, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-sonnet-4-5-20250929", Prices: Prices{UncachedInput: Price{Value: 3, Set: true}, CacheRead: Price{Value: 0.3, Set: true}, CacheWrite5M: Price{Value: 3.75, Set: true}, CacheWrite1H: Price{Value: 6, Set: true}, Output: Price{Value: 15, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-haiku-4-5", Prices: Prices{UncachedInput: Price{Value: 1, Set: true}, CacheRead: Price{Value: 0.1, Set: true}, CacheWrite5M: Price{Value: 1.25, Set: true}, CacheWrite1H: Price{Value: 2, Set: true}, Output: Price{Value: 5, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-haiku-4-5-20251001", Prices: Prices{UncachedInput: Price{Value: 1, Set: true}, CacheRead: Price{Value: 0.1, Set: true}, CacheWrite5M: Price{Value: 1.25, Set: true}, CacheWrite1H: Price{Value: 2, Set: true}, Output: Price{Value: 5, Set: true}}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-3.6-flash", Prices: Prices{UncachedInput: Price{Value: 1.5, Set: true}, CacheRead: Price{Value: 0.15, Set: true}, Output: Price{Value: 7.5, Set: true}}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-3.5-flash", Prices: Prices{UncachedInput: Price{Value: 1.5, Set: true}, CacheRead: Price{Value: 0.15, Set: true}, Output: Price{Value: 9, Set: true}}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-3.5-flash-lite", Prices: Prices{UncachedInput: Price{Value: 0.3, Set: true}, CacheRead: Price{Value: 0.03, Set: true}, Output: Price{Value: 2.5, Set: true}}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-3.1-flash-lite", Prices: Prices{UncachedInput: Price{Value: 0.25, Set: true}, CacheRead: Price{Value: 0.025, Set: true}, Output: Price{Value: 1.5, Set: true}}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-2.5-pro", Prices: Prices{UncachedInput: Price{Value: 1.25, Set: true}, CacheRead: Price{Value: 0.125, Set: true}, Output: Price{Value: 10, Set: true}}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-2.5-flash", Prices: Prices{UncachedInput: Price{Value: 0.3, Set: true}, CacheRead: Price{Value: 0.03, Set: true}, Output: Price{Value: 2.5, Set: true}}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-2.5-flash-lite", Prices: Prices{UncachedInput: Price{Value: 0.1, Set: true}, CacheRead: Price{Value: 0.01, Set: true}, Output: Price{Value: 0.4, Set: true}}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-5.6", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), Output: mustPrice("30")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.6-sol", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), Output: mustPrice("30")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.6-terra", Prices: Prices{UncachedInput: mustPrice("2.5"), CacheRead: mustPrice("0.25"), Output: mustPrice("15")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.6-luna", Prices: Prices{UncachedInput: mustPrice("1"), CacheRead: mustPrice("0.1"), Output: mustPrice("6")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.5", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), Output: mustPrice("30")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.5-pro", Prices: Prices{UncachedInput: mustPrice("30"), Output: mustPrice("180")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-5.4", Prices: Prices{UncachedInput: mustPrice("2.5"), CacheRead: mustPrice("0.25"), Output: mustPrice("15")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-5.4-mini", Prices: Prices{UncachedInput: mustPrice("0.75"), CacheRead: mustPrice("0.075"), Output: mustPrice("4.5")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-5.4-nano", Prices: Prices{UncachedInput: mustPrice("0.2"), CacheRead: mustPrice("0.02"), Output: mustPrice("1.25")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-5.4-pro", Prices: Prices{UncachedInput: mustPrice("30"), Output: mustPrice("180")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt, LongContextPolicy: newBuiltinLongContextPolicy()},
{Pattern: "gpt-4.1", Prices: Prices{UncachedInput: mustPrice("2"), CacheRead: mustPrice("0.5"), Output: mustPrice("8")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-4.1-mini", Prices: Prices{UncachedInput: mustPrice("0.4"), CacheRead: mustPrice("0.1"), Output: mustPrice("1.6")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-4.1-nano", Prices: Prices{UncachedInput: mustPrice("0.1"), CacheRead: mustPrice("0.025"), Output: mustPrice("0.4")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-4o", Prices: Prices{UncachedInput: mustPrice("2.5"), CacheRead: mustPrice("1.25"), Output: mustPrice("10")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gpt-4o-mini", Prices: Prices{UncachedInput: mustPrice("0.15"), CacheRead: mustPrice("0.075"), Output: mustPrice("0.6")}, Source: SourceBuiltin, SourceURL: builtinOpenAIURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-fable-5", Prices: Prices{UncachedInput: mustPrice("10"), CacheRead: mustPrice("1"), CacheWrite5M: mustPrice("12.5"), CacheWrite1H: mustPrice("20"), Output: mustPrice("50")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-5", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), CacheWrite5M: mustPrice("6.25"), CacheWrite1H: mustPrice("10"), Output: mustPrice("25")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-8", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), CacheWrite5M: mustPrice("6.25"), CacheWrite1H: mustPrice("10"), Output: mustPrice("25")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-7", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), CacheWrite5M: mustPrice("6.25"), CacheWrite1H: mustPrice("10"), Output: mustPrice("25")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-6", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), CacheWrite5M: mustPrice("6.25"), CacheWrite1H: mustPrice("10"), Output: mustPrice("25")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-5", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), CacheWrite5M: mustPrice("6.25"), CacheWrite1H: mustPrice("10"), Output: mustPrice("25")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-opus-4-5-20251101", Prices: Prices{UncachedInput: mustPrice("5"), CacheRead: mustPrice("0.5"), CacheWrite5M: mustPrice("6.25"), CacheWrite1H: mustPrice("10"), Output: mustPrice("25")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-sonnet-5", Prices: Prices{UncachedInput: mustPrice("2"), CacheRead: mustPrice("0.2"), CacheWrite5M: mustPrice("2.5"), CacheWrite1H: mustPrice("4"), Output: mustPrice("10")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-sonnet-4-6", Prices: Prices{UncachedInput: mustPrice("3"), CacheRead: mustPrice("0.3"), CacheWrite5M: mustPrice("3.75"), CacheWrite1H: mustPrice("6"), Output: mustPrice("15")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-sonnet-4-5", Prices: Prices{UncachedInput: mustPrice("3"), CacheRead: mustPrice("0.3"), CacheWrite5M: mustPrice("3.75"), CacheWrite1H: mustPrice("6"), Output: mustPrice("15")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-sonnet-4-5-20250929", Prices: Prices{UncachedInput: mustPrice("3"), CacheRead: mustPrice("0.3"), CacheWrite5M: mustPrice("3.75"), CacheWrite1H: mustPrice("6"), Output: mustPrice("15")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-haiku-4-5", Prices: Prices{UncachedInput: mustPrice("1"), CacheRead: mustPrice("0.1"), CacheWrite5M: mustPrice("1.25"), CacheWrite1H: mustPrice("2"), Output: mustPrice("5")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "claude-haiku-4-5-20251001", Prices: Prices{UncachedInput: mustPrice("1"), CacheRead: mustPrice("0.1"), CacheWrite5M: mustPrice("1.25"), CacheWrite1H: mustPrice("2"), Output: mustPrice("5")}, Source: SourceBuiltin, SourceURL: builtinAnthropicURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-3.6-flash", Prices: Prices{UncachedInput: mustPrice("1.5"), CacheRead: mustPrice("0.15"), Output: mustPrice("7.5")}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-3.5-flash", Prices: Prices{UncachedInput: mustPrice("1.5"), CacheRead: mustPrice("0.15"), Output: mustPrice("9")}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-3.5-flash-lite", Prices: Prices{UncachedInput: mustPrice("0.3"), CacheRead: mustPrice("0.03"), Output: mustPrice("2.5")}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-3.1-flash-lite", Prices: Prices{UncachedInput: mustPrice("0.25"), CacheRead: mustPrice("0.025"), Output: mustPrice("1.5")}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-2.5-pro", Prices: Prices{UncachedInput: mustPrice("1.25"), CacheRead: mustPrice("0.125"), Output: mustPrice("10")}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-2.5-flash", Prices: Prices{UncachedInput: mustPrice("0.3"), CacheRead: mustPrice("0.03"), Output: mustPrice("2.5")}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
{Pattern: "gemini-2.5-flash-lite", Prices: Prices{UncachedInput: mustPrice("0.1"), CacheRead: mustPrice("0.01"), Output: mustPrice("0.4")}, Source: SourceBuiltin, SourceURL: builtinGeminiURL, UpdatedAt: builtinUpdatedAt},
}
// BuiltinRules returns a caller-owned copy of the built-in pricing rules.
@@ -57,10 +57,18 @@ func BuiltinRules() []Rule {
return result
}
func mustPrice(value string) Price {
price, err := ParseUSD(value)
if err != nil {
panic("invalid builtin price " + value + ": " + err.Error())
}
return Price{NanoUSDPerMillion: price, Set: true}
}
func newBuiltinLongContextPolicy() *LongContextPolicy {
return &LongContextPolicy{
InputThresholdTokens: 272_000,
InputMultiplier: 2,
OutputMultiplier: 1.5,
InputMultiplier: Multiplier{Numerator: 2, Denominator: 1},
OutputMultiplier: Multiplier{Numerator: 3, Denominator: 2},
}
}
+45 -54
View File
@@ -14,41 +14,41 @@ func TestBuiltinRulesMatchGoldenTable(t *testing.T) {
const anthropicURL = "https://platform.claude.com/docs/en/about-claude/pricing"
const geminiURL = "https://ai.google.dev/gemini-api/docs/pricing"
want := []Rule{
{Pattern: "gpt-5.6", Prices: builtinPrices(5, 0.5, unsetPrice(), unsetPrice(), 30), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.6-sol", Prices: builtinPrices(5, 0.5, unsetPrice(), unsetPrice(), 30), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.6-terra", Prices: builtinPrices(2.5, 0.25, unsetPrice(), unsetPrice(), 15), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.6-luna", Prices: builtinPrices(1, 0.1, unsetPrice(), unsetPrice(), 6), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.5", Prices: builtinPrices(5, 0.5, unsetPrice(), unsetPrice(), 30), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.5-pro", Prices: builtinPrices(30, unsetPrice(), unsetPrice(), unsetPrice(), 180), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-5.4", Prices: builtinPrices(2.5, 0.25, unsetPrice(), unsetPrice(), 15), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.4-mini", Prices: builtinPrices(0.75, 0.075, unsetPrice(), unsetPrice(), 4.5), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-5.4-nano", Prices: builtinPrices(0.2, 0.02, unsetPrice(), unsetPrice(), 1.25), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-5.4-pro", Prices: builtinPrices(30, unsetPrice(), unsetPrice(), unsetPrice(), 180), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-4.1", Prices: builtinPrices(2, 0.5, unsetPrice(), unsetPrice(), 8), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-4.1-mini", Prices: builtinPrices(0.4, 0.1, unsetPrice(), unsetPrice(), 1.6), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-4.1-nano", Prices: builtinPrices(0.1, 0.025, unsetPrice(), unsetPrice(), 0.4), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-4o", Prices: builtinPrices(2.5, 1.25, unsetPrice(), unsetPrice(), 10), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-4o-mini", Prices: builtinPrices(0.15, 0.075, unsetPrice(), unsetPrice(), 0.6), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "claude-fable-5", Prices: builtinPrices(10, 1, 12.5, 20, 50), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-5", Prices: builtinPrices(5, 0.5, 6.25, 10, 25), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-8", Prices: builtinPrices(5, 0.5, 6.25, 10, 25), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-7", Prices: builtinPrices(5, 0.5, 6.25, 10, 25), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-6", Prices: builtinPrices(5, 0.5, 6.25, 10, 25), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-5", Prices: builtinPrices(5, 0.5, 6.25, 10, 25), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-5-20251101", Prices: builtinPrices(5, 0.5, 6.25, 10, 25), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-sonnet-5", Prices: builtinPrices(2, 0.2, 2.5, 4, 10), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-sonnet-4-6", Prices: builtinPrices(3, 0.3, 3.75, 6, 15), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-sonnet-4-5", Prices: builtinPrices(3, 0.3, 3.75, 6, 15), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-sonnet-4-5-20250929", Prices: builtinPrices(3, 0.3, 3.75, 6, 15), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-haiku-4-5", Prices: builtinPrices(1, 0.1, 1.25, 2, 5), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-haiku-4-5-20251001", Prices: builtinPrices(1, 0.1, 1.25, 2, 5), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "gemini-3.6-flash", Prices: builtinPrices(1.5, 0.15, unsetPrice(), unsetPrice(), 7.5), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-3.5-flash", Prices: builtinPrices(1.5, 0.15, unsetPrice(), unsetPrice(), 9), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-3.5-flash-lite", Prices: builtinPrices(0.3, 0.03, unsetPrice(), unsetPrice(), 2.5), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-3.1-flash-lite", Prices: builtinPrices(0.25, 0.025, unsetPrice(), unsetPrice(), 1.5), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-2.5-pro", Prices: builtinPrices(1.25, 0.125, unsetPrice(), unsetPrice(), 10), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-2.5-flash", Prices: builtinPrices(0.3, 0.03, unsetPrice(), unsetPrice(), 2.5), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-2.5-flash-lite", Prices: builtinPrices(0.1, 0.01, unsetPrice(), unsetPrice(), 0.4), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gpt-5.6", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), unsetPrice(), unsetPrice(), nano(30_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.6-sol", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), unsetPrice(), unsetPrice(), nano(30_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.6-terra", Prices: builtinPrices(nano(2_500_000_000), nano(250_000_000), unsetPrice(), unsetPrice(), nano(15_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.6-luna", Prices: builtinPrices(nano(1_000_000_000), nano(100_000_000), unsetPrice(), unsetPrice(), nano(6_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.5", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), unsetPrice(), unsetPrice(), nano(30_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.5-pro", Prices: builtinPrices(nano(30_000_000_000), unsetPrice(), unsetPrice(), unsetPrice(), nano(180_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-5.4", Prices: builtinPrices(nano(2_500_000_000), nano(250_000_000), unsetPrice(), unsetPrice(), nano(15_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-5.4-mini", Prices: builtinPrices(nano(750_000_000), nano(75_000_000), unsetPrice(), unsetPrice(), nano(4_500_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-5.4-nano", Prices: builtinPrices(nano(200_000_000), nano(20_000_000), unsetPrice(), unsetPrice(), nano(1_250_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-5.4-pro", Prices: builtinPrices(nano(30_000_000_000), unsetPrice(), unsetPrice(), unsetPrice(), nano(180_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt, LongContextPolicy: testLongContextPolicy()},
{Pattern: "gpt-4.1", Prices: builtinPrices(nano(2_000_000_000), nano(500_000_000), unsetPrice(), unsetPrice(), nano(8_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-4.1-mini", Prices: builtinPrices(nano(400_000_000), nano(100_000_000), unsetPrice(), unsetPrice(), nano(1_600_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-4.1-nano", Prices: builtinPrices(nano(100_000_000), nano(25_000_000), unsetPrice(), unsetPrice(), nano(400_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-4o", Prices: builtinPrices(nano(2_500_000_000), nano(1_250_000_000), unsetPrice(), unsetPrice(), nano(10_000_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "gpt-4o-mini", Prices: builtinPrices(nano(150_000_000), nano(75_000_000), unsetPrice(), unsetPrice(), nano(600_000_000)), Source: SourceBuiltin, SourceURL: openAIURL, UpdatedAt: updatedAt},
{Pattern: "claude-fable-5", Prices: builtinPrices(nano(10_000_000_000), nano(1_000_000_000), nano(12_500_000_000), nano(20_000_000_000), nano(50_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-5", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), nano(6_250_000_000), nano(10_000_000_000), nano(25_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-8", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), nano(6_250_000_000), nano(10_000_000_000), nano(25_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-7", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), nano(6_250_000_000), nano(10_000_000_000), nano(25_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-6", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), nano(6_250_000_000), nano(10_000_000_000), nano(25_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-5", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), nano(6_250_000_000), nano(10_000_000_000), nano(25_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-opus-4-5-20251101", Prices: builtinPrices(nano(5_000_000_000), nano(500_000_000), nano(6_250_000_000), nano(10_000_000_000), nano(25_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-sonnet-5", Prices: builtinPrices(nano(2_000_000_000), nano(200_000_000), nano(2_500_000_000), nano(4_000_000_000), nano(10_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-sonnet-4-6", Prices: builtinPrices(nano(3_000_000_000), nano(300_000_000), nano(3_750_000_000), nano(6_000_000_000), nano(15_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-sonnet-4-5", Prices: builtinPrices(nano(3_000_000_000), nano(300_000_000), nano(3_750_000_000), nano(6_000_000_000), nano(15_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-sonnet-4-5-20250929", Prices: builtinPrices(nano(3_000_000_000), nano(300_000_000), nano(3_750_000_000), nano(6_000_000_000), nano(15_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-haiku-4-5", Prices: builtinPrices(nano(1_000_000_000), nano(100_000_000), nano(1_250_000_000), nano(2_000_000_000), nano(5_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "claude-haiku-4-5-20251001", Prices: builtinPrices(nano(1_000_000_000), nano(100_000_000), nano(1_250_000_000), nano(2_000_000_000), nano(5_000_000_000)), Source: SourceBuiltin, SourceURL: anthropicURL, UpdatedAt: updatedAt},
{Pattern: "gemini-3.6-flash", Prices: builtinPrices(nano(1_500_000_000), nano(150_000_000), unsetPrice(), unsetPrice(), nano(7_500_000_000)), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-3.5-flash", Prices: builtinPrices(nano(1_500_000_000), nano(150_000_000), unsetPrice(), unsetPrice(), nano(9_000_000_000)), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-3.5-flash-lite", Prices: builtinPrices(nano(300_000_000), nano(30_000_000), unsetPrice(), unsetPrice(), nano(2_500_000_000)), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-3.1-flash-lite", Prices: builtinPrices(nano(250_000_000), nano(25_000_000), unsetPrice(), unsetPrice(), nano(1_500_000_000)), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-2.5-pro", Prices: builtinPrices(nano(1_250_000_000), nano(125_000_000), unsetPrice(), unsetPrice(), nano(10_000_000_000)), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-2.5-flash", Prices: builtinPrices(nano(300_000_000), nano(30_000_000), unsetPrice(), unsetPrice(), nano(2_500_000_000)), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
{Pattern: "gemini-2.5-flash-lite", Prices: builtinPrices(nano(100_000_000), nano(10_000_000), unsetPrice(), unsetPrice(), nano(400_000_000)), Source: SourceBuiltin, SourceURL: geminiURL, UpdatedAt: updatedAt},
}
got := BuiltinRules()
@@ -65,11 +65,11 @@ func TestBuiltinRulesMatchGoldenTable(t *testing.T) {
}
got[0].Pattern = "mutated"
if got[0].LongContextPolicy != nil {
got[0].LongContextPolicy.InputMultiplier = 99
got[0].LongContextPolicy.InputMultiplier = Multiplier{Numerator: 99, Denominator: 1}
}
again := BuiltinRules()
if again[0].Pattern != "gpt-5.6" || again[0].LongContextPolicy == nil ||
again[0].LongContextPolicy.InputMultiplier != 2 {
again[0].LongContextPolicy.InputMultiplier != (Multiplier{Numerator: 2, Denominator: 1}) {
t.Fatalf("BuiltinRules() returned mutable backing data: %+v", again[0])
}
}
@@ -103,13 +103,13 @@ func TestBuiltinRulesDoNotMatchUnsupportedModelFamilies(t *testing.T) {
}
}
func builtinPrices(uncachedInput, cacheRead, cacheWrite5M, cacheWrite1H, output any) Prices {
func builtinPrices(uncachedInput, cacheRead, cacheWrite5M, cacheWrite1H, output Price) Prices {
return Prices{
UncachedInput: builtinPrice(uncachedInput),
CacheRead: builtinPrice(cacheRead),
CacheWrite5M: builtinPrice(cacheWrite5M),
CacheWrite1H: builtinPrice(cacheWrite1H),
Output: builtinPrice(output),
UncachedInput: uncachedInput,
CacheRead: cacheRead,
CacheWrite5M: cacheWrite5M,
CacheWrite1H: cacheWrite1H,
Output: output,
}
}
@@ -117,15 +117,6 @@ func unsetPrice() Price {
return Price{}
}
func builtinPrice(value any) Price {
switch value := value.(type) {
case float64:
return Price{Value: value, Set: true}
case int:
return Price{Value: float64(value), Set: true}
case Price:
return value
default:
panic("unsupported builtin test price")
}
func nano(value NanoUSD) Price {
return fixedPrice(value)
}
+5 -6
View File
@@ -2,7 +2,6 @@ package pricing
import (
"fmt"
"math"
"sort"
"strings"
"unicode"
@@ -66,8 +65,8 @@ func validateRule(rule Rule) error {
rule.Prices.CacheWrite1H,
rule.Prices.Output,
} {
if math.IsNaN(price.Value) || math.IsInf(price.Value, 0) || price.Value < 0 {
return fmt.Errorf("pricing price must be finite and non-negative")
if price.NanoUSDPerMillion < 0 {
return fmt.Errorf("pricing price must be non-negative")
}
priceSet = priceSet || price.Set
}
@@ -87,9 +86,9 @@ func validateRule(rule Rule) error {
if policy.InputThresholdTokens <= 0 {
return fmt.Errorf("long-context input threshold must be positive")
}
if !isFinite(policy.InputMultiplier) || policy.InputMultiplier <= 0 ||
!isFinite(policy.OutputMultiplier) || policy.OutputMultiplier <= 0 {
return fmt.Errorf("long-context multipliers must be finite and positive")
if policy.InputMultiplier.Numerator <= 0 || policy.InputMultiplier.Denominator <= 0 ||
policy.OutputMultiplier.Numerator <= 0 || policy.OutputMultiplier.Denominator <= 0 {
return fmt.Errorf("long-context multipliers must be positive ratios")
}
}
return nil
+28 -32
View File
@@ -1,7 +1,6 @@
package pricing
import (
"math"
"strings"
"sync"
"testing"
@@ -107,7 +106,7 @@ func TestCompileMatchBareUserCatchAllShadowsBuiltins(t *testing.T) {
for _, model := range []string{"gpt-4o", "claude-opus", "unknown-model"} {
rule, ok := table.Match(model)
if !ok || rule.Pattern != "*" || rule.Source != SourceUser ||
rule.Prices.Output.Value != 99 {
rule.Prices.Output.NanoUSDPerMillion != 99 {
t.Errorf("Match(%q) = %+v, %t; want global user override", model, rule, ok)
}
}
@@ -132,11 +131,8 @@ func TestCompileRejectsInvalidRules(t *testing.T) {
{name: "multiple stars", rule: replacePattern(validBuiltin, "gpt-**")},
{name: "invalid source", rule: Rule{Pattern: "gpt-4o", Prices: priced(1), Source: Source("remote")}},
{name: "all prices unset", rule: Rule{Pattern: "gpt-4o", Source: SourceUser}},
{name: "negative price", rule: Rule{Pattern: "gpt-4o", Prices: Prices{Output: Price{Value: -1, Set: true}}, Source: SourceUser}},
{name: "negative unset price", rule: Rule{Pattern: "gpt-4o", Prices: Prices{UncachedInput: Price{Value: -1}, Output: Price{Value: 1, Set: true}}, Source: SourceUser}},
{name: "nan price", rule: Rule{Pattern: "gpt-4o", Prices: Prices{Output: Price{Value: math.NaN(), Set: true}}, Source: SourceUser}},
{name: "positive infinity price", rule: Rule{Pattern: "gpt-4o", Prices: Prices{Output: Price{Value: math.Inf(1), Set: true}}, Source: SourceUser}},
{name: "negative infinity price", rule: Rule{Pattern: "gpt-4o", Prices: Prices{Output: Price{Value: math.Inf(-1), Set: true}}, Source: SourceUser}},
{name: "negative price", rule: Rule{Pattern: "gpt-4o", Prices: Prices{Output: fixedPrice(-1)}, Source: SourceUser}},
{name: "negative unset price", rule: Rule{Pattern: "gpt-4o", Prices: Prices{UncachedInput: Price{NanoUSDPerMillion: -1}, Output: fixedPrice(1)}, Source: SourceUser}},
{name: "builtin missing URL", rule: Rule{Pattern: "gpt-4o", Prices: priced(1), Source: SourceBuiltin, UpdatedAt: updatedAt}},
{name: "builtin missing update time", rule: Rule{Pattern: "gpt-4o", Prices: priced(1), Source: SourceBuiltin, SourceURL: "https://builtin.example/pricing"}},
{name: "user source URL", rule: Rule{Pattern: "gpt-4o", Prices: priced(1), Source: SourceUser, SourceURL: "https://user.example/pricing"}},
@@ -165,8 +161,8 @@ func TestCompileRejectsUserAndPrefixLongContextPolicies(t *testing.T) {
validPolicy := func() *LongContextPolicy {
return &LongContextPolicy{
InputThresholdTokens: 272_000,
InputMultiplier: 2,
OutputMultiplier: 1.5,
InputMultiplier: Multiplier{Numerator: 2, Denominator: 1},
OutputMultiplier: Multiplier{Numerator: 3, Denominator: 2},
}
}
validBuiltin := func(pattern string) Rule {
@@ -225,31 +221,31 @@ func TestCompileRejectsUserAndPrefixLongContextPolicies(t *testing.T) {
name: "zero input multiplier",
rule: func() Rule {
rule := validBuiltin("gpt-5.6")
rule.LongContextPolicy.InputMultiplier = 0
rule.LongContextPolicy.InputMultiplier = Multiplier{}
return rule
}(),
},
{
name: "negative output multiplier",
name: "negative output multiplier numerator",
rule: func() Rule {
rule := validBuiltin("gpt-5.6")
rule.LongContextPolicy.OutputMultiplier = -1
rule.LongContextPolicy.OutputMultiplier = Multiplier{Numerator: -1, Denominator: 1}
return rule
}(),
},
{
name: "nan input multiplier",
name: "negative input multiplier denominator",
rule: func() Rule {
rule := validBuiltin("gpt-5.6")
rule.LongContextPolicy.InputMultiplier = math.NaN()
rule.LongContextPolicy.InputMultiplier = Multiplier{Numerator: 1, Denominator: -1}
return rule
}(),
},
{
name: "infinite output multiplier",
name: "zero output multiplier denominator",
rule: func() Rule {
rule := validBuiltin("gpt-5.6")
rule.LongContextPolicy.OutputMultiplier = math.Inf(1)
rule.LongContextPolicy.OutputMultiplier = Multiplier{Numerator: 1}
return rule
}(),
},
@@ -268,8 +264,8 @@ func TestCompileCopiesLongContextPolicy(t *testing.T) {
policy := &LongContextPolicy{
InputThresholdTokens: 272_000,
InputMultiplier: 2,
OutputMultiplier: 1.5,
InputMultiplier: Multiplier{Numerator: 2, Denominator: 1},
OutputMultiplier: Multiplier{Numerator: 3, Denominator: 2},
}
rules := []Rule{{
Pattern: "gpt-5.6",
@@ -285,26 +281,26 @@ func TestCompileCopiesLongContextPolicy(t *testing.T) {
}
policy.InputThresholdTokens = 1
policy.InputMultiplier = 99
policy.InputMultiplier = Multiplier{Numerator: 99, Denominator: 1}
first, ok := table.Match("gpt-5.6")
if !ok || first.LongContextPolicy == nil {
t.Fatalf("first Match() = %+v, %t; want policy", first, ok)
}
if first.LongContextPolicy.InputThresholdTokens != 272_000 ||
first.LongContextPolicy.InputMultiplier != 2 ||
first.LongContextPolicy.OutputMultiplier != 1.5 {
first.LongContextPolicy.InputMultiplier != (Multiplier{Numerator: 2, Denominator: 1}) ||
first.LongContextPolicy.OutputMultiplier != (Multiplier{Numerator: 3, Denominator: 2}) {
t.Fatalf("first Match() policy = %+v, want original values", first.LongContextPolicy)
}
if first.LongContextPolicy == policy {
t.Fatal("Compile() retained the caller's policy pointer")
}
first.LongContextPolicy.OutputMultiplier = 99
first.LongContextPolicy.OutputMultiplier = Multiplier{Numerator: 99, Denominator: 1}
second, ok := table.Match("gpt-5.6")
if !ok || second.LongContextPolicy == nil {
t.Fatalf("second Match() = %+v, %t; want policy", second, ok)
}
if second.LongContextPolicy.OutputMultiplier != 1.5 {
if second.LongContextPolicy.OutputMultiplier != (Multiplier{Numerator: 3, Denominator: 2}) {
t.Fatalf("second Match() policy = %+v, want immutable copy", second.LongContextPolicy)
}
if second.LongContextPolicy == first.LongContextPolicy {
@@ -321,16 +317,16 @@ func TestCompileCopiesInputAndReturnsRuleValues(t *testing.T) {
t.Fatalf("Compile() error = %v", err)
}
rules[0].Pattern = "other-*"
rules[0].Prices.Output.Value = 99
rules[0].Prices.Output.NanoUSDPerMillion = 99
first, ok := table.Match("gpt-4o")
if !ok || first.Pattern != "gpt-*" || first.Prices.Output.Value != 1 {
if !ok || first.Pattern != "gpt-*" || first.Prices.Output.NanoUSDPerMillion != 1 {
t.Fatalf("first Match() = %+v, %t; want unmodified gpt-* rule", first, ok)
}
first.Pattern = "other-*"
first.Prices.Output.Value = 99
first.Prices.Output.NanoUSDPerMillion = 99
second, ok := table.Match("gpt-4o")
if !ok || second.Pattern != "gpt-*" || second.Prices.Output.Value != 1 {
if !ok || second.Pattern != "gpt-*" || second.Prices.Output.NanoUSDPerMillion != 1 {
t.Fatalf("second Match() = %+v, %t; want unmodified gpt-* rule", second, ok)
}
}
@@ -356,12 +352,12 @@ func TestTableMatchIsConcurrentAndImmutable(t *testing.T) {
defer waitGroup.Done()
for range matchesPerGoroutine {
rule, ok := table.Match("gpt-4o")
if !ok || rule.Pattern != "gpt-*" || rule.Prices.Output.Value != 1 {
if !ok || rule.Pattern != "gpt-*" || rule.Prices.Output.NanoUSDPerMillion != 1 {
errors <- "concurrent Match returned an unexpected rule"
return
}
rule.Pattern = "mutated"
rule.Prices.Output.Value = 99
rule.Prices.Output.NanoUSDPerMillion = 99
}
}()
}
@@ -372,13 +368,13 @@ func TestTableMatchIsConcurrentAndImmutable(t *testing.T) {
}
rule, ok := table.Match("gpt-4o")
if !ok || rule.Pattern != "gpt-*" || rule.Prices.Output.Value != 1 {
if !ok || rule.Pattern != "gpt-*" || rule.Prices.Output.NanoUSDPerMillion != 1 {
t.Fatalf("final Match() = %+v, %t; want unmodified gpt-* rule", rule, ok)
}
}
func priced(value float64) Prices {
return Prices{Output: Price{Value: value, Set: true}}
func priced(value NanoUSD) Prices {
return Prices{Output: fixedPrice(value)}
}
func replacePattern(rule Rule, pattern string) Rule {
+109
View File
@@ -0,0 +1,109 @@
package pricing
import (
"errors"
"math"
"math/big"
"strconv"
"strings"
)
const nanoUSDPerUSD int64 = 1_000_000_000
// NanoUSD is a USD amount represented in billionths of one USD.
type NanoUSD int64
// Multiplier applies an exact positive ratio to a price component.
type Multiplier struct {
Numerator int64
Denominator int64
}
// ParseUSD parses a non-negative decimal USD amount with at most nine decimal places.
func ParseUSD(value string) (NanoUSD, error) {
if value == "" || strings.TrimSpace(value) != value {
return 0, errors.New("USD price must be a non-empty decimal")
}
integer, fraction, hasFraction := strings.Cut(value, ".")
if integer == "" || (hasFraction && fraction == "") || strings.Contains(fraction, ".") || len(fraction) > 9 {
return 0, errors.New("USD price must have at most nine decimal places")
}
if !decimalDigits(integer) || (hasFraction && !decimalDigits(fraction)) {
return 0, errors.New("USD price must be a non-negative decimal")
}
amount := new(big.Int)
if _, ok := amount.SetString(integer+fraction+strings.Repeat("0", 9-len(fraction)), 10); !ok || !amount.IsInt64() {
return 0, errors.New("USD price exceeds int64 nano USD")
}
return NanoUSD(amount.Int64()), nil
}
// FormatUSD formats a NanoUSD amount as a plain decimal USD string.
func FormatUSD(value NanoUSD) string {
amount := big.NewInt(int64(value))
negative := amount.Sign() < 0
if negative {
amount.Abs(amount)
}
whole, fraction := new(big.Int), new(big.Int)
whole.QuoRem(amount, big.NewInt(nanoUSDPerUSD), fraction)
if fraction.Sign() == 0 {
if negative {
return "-" + whole.String()
}
return whole.String()
}
fractionText := strconv.FormatInt(fraction.Int64(), 10)
fractionText = strings.Repeat("0", 9-len(fractionText)) + fractionText
fractionText = strings.TrimRight(fractionText, "0")
prefix := ""
if negative {
prefix = "-"
}
return prefix + whole.String() + "." + fractionText
}
// QuoteComponent returns the rounded nano USD cost of one usage component.
func QuoteComponent(tokens int64, price NanoUSD, multiplier Multiplier) (NanoUSD, bool) {
if tokens < 0 || price < 0 || multiplier.Numerator <= 0 || multiplier.Denominator <= 0 {
return 0, false
}
numerator := big.NewInt(tokens)
numerator.Mul(numerator, big.NewInt(int64(price)))
numerator.Mul(numerator, big.NewInt(multiplier.Numerator))
denominator := big.NewInt(tokensPerMillion)
denominator.Mul(denominator, big.NewInt(multiplier.Denominator))
quotient, remainder := new(big.Int), new(big.Int)
quotient.QuoRem(numerator, denominator, remainder)
remainder.Lsh(remainder, 1)
if remainder.Cmp(denominator) >= 0 {
quotient.Add(quotient, big.NewInt(1))
}
if quotient.Sign() < 0 || !quotient.IsInt64() {
return 0, false
}
return NanoUSD(quotient.Int64()), true
}
// CheckedAddNanoUSD adds non-negative NanoUSD values without overflow.
func CheckedAddNanoUSD(left, right NanoUSD) (NanoUSD, bool) {
if left < 0 || right < 0 || left > NanoUSD(math.MaxInt64)-right {
return 0, false
}
return left + right, true
}
func decimalDigits(value string) bool {
for _, character := range value {
if character < '0' || character > '9' {
return false
}
}
return true
}
+129
View File
@@ -0,0 +1,129 @@
package pricing
import (
"math"
"testing"
)
func TestParseUSDConvertsDecimalUSDPerMillionToNanoUSDExactly(t *testing.T) {
tests := []struct {
value string
want NanoUSD
}{
{value: "0", want: 0},
{value: "0.000000001", want: 1},
{value: "1.25", want: 1_250_000_000},
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
got, err := ParseUSD(tt.value)
if err != nil {
t.Fatalf("ParseUSD(%q) error = %v", tt.value, err)
}
if got != tt.want {
t.Fatalf("ParseUSD(%q) = %d, want %d", tt.value, got, tt.want)
}
})
}
}
func TestParseUSDRejectsInvalidOrUnrepresentablePrices(t *testing.T) {
for _, value := range []string{"-1", "1e3", "0.0000000001", "", " ", "92233720368.54775808"} {
t.Run(value, func(t *testing.T) {
if _, err := ParseUSD(value); err == nil {
t.Fatalf("ParseUSD(%q) accepted an invalid price", value)
}
})
}
}
func TestFormatUSDUsesPlainDecimalWithoutInsignificantZeros(t *testing.T) {
tests := []struct {
value NanoUSD
want string
}{
{value: 0, want: "0"},
{value: 1, want: "0.000000001"},
{value: 1_250_000_000, want: "1.25"},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
if got := FormatUSD(tt.value); got != tt.want {
t.Fatalf("FormatUSD(%d) = %q, want %q", tt.value, got, tt.want)
}
})
}
}
func TestQuoteComponentUsesIntegerHalfUpRounding(t *testing.T) {
tests := []struct {
name string
tokens int64
price NanoUSD
multiplier Multiplier
want NanoUSD
}{
{name: "large intermediate product remains representable after division", tokens: math.MaxInt64, price: 1_000_000, multiplier: Multiplier{Numerator: 1, Denominator: 1}, want: NanoUSD(math.MaxInt64)},
{name: "below half rounds down", tokens: 499_999, price: 1, multiplier: Multiplier{Numerator: 1, Denominator: 1}, want: 0},
{name: "half rounds up", tokens: 500_000, price: 1, multiplier: Multiplier{Numerator: 1, Denominator: 1}, want: 1},
{name: "exact two times multiplier", tokens: 1_000_000, price: 7, multiplier: Multiplier{Numerator: 2, Denominator: 1}, want: 14},
{name: "exact three halves multiplier", tokens: 1_000_000, price: 7, multiplier: Multiplier{Numerator: 3, Denominator: 2}, want: 11},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := QuoteComponent(tt.tokens, tt.price, tt.multiplier)
if !ok || got != tt.want {
t.Fatalf("QuoteComponent(%d, %d, %+v) = %d, %t, want %d, true", tt.tokens, tt.price, tt.multiplier, got, ok, tt.want)
}
})
}
}
func TestQuoteComponentRejectsFinalOverflowAndInvalidInputs(t *testing.T) {
if _, ok := QuoteComponent(math.MaxInt64, NanoUSD(math.MaxInt64), Multiplier{Numerator: 1_000_000, Denominator: 1}); ok {
t.Fatal("QuoteComponent() accepted a result above int64")
}
for _, test := range []struct {
name string
tokens int64
price NanoUSD
multiplier Multiplier
}{
{name: "negative tokens", tokens: -1, price: 1, multiplier: Multiplier{Numerator: 1, Denominator: 1}},
{name: "negative price", tokens: 1, price: -1, multiplier: Multiplier{Numerator: 1, Denominator: 1}},
{name: "zero numerator", tokens: 1, price: 1, multiplier: Multiplier{Numerator: 0, Denominator: 1}},
{name: "zero denominator", tokens: 1, price: 1, multiplier: Multiplier{Numerator: 1, Denominator: 0}},
} {
t.Run(test.name, func(t *testing.T) {
if _, ok := QuoteComponent(test.tokens, test.price, test.multiplier); ok {
t.Fatal("QuoteComponent() accepted invalid input")
}
})
}
}
func TestCheckedAddNanoUSDRejectsNegativeAndOverflowAmounts(t *testing.T) {
tests := []struct {
name string
left, right NanoUSD
want NanoUSD
wantOK bool
}{
{name: "adds valid amounts", left: 7, right: 5, want: 12, wantOK: true},
{name: "negative left", left: -1, right: 5},
{name: "negative right", left: 7, right: -1},
{name: "overflow", left: NanoUSD(math.MaxInt64), right: 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := CheckedAddNanoUSD(tt.left, tt.right)
if got != tt.want || ok != tt.wantOK {
t.Fatalf("CheckedAddNanoUSD(%d, %d) = %d, %t, want %d, %t", tt.left, tt.right, got, ok, tt.want, tt.wantOK)
}
})
}
}
+14 -22
View File
@@ -1,10 +1,6 @@
package pricing
import (
"math"
"gpt-load/internal/usage"
)
import "gpt-load/internal/usage"
const tokensPerMillion = 1_000_000
@@ -29,8 +25,8 @@ func (table *Table) Quote(upstreamModel string, result usage.Result) Quote {
if !ok {
return Quote{State: CostStateUnpriced}
}
inputMultiplier := float64(1)
outputMultiplier := float64(1)
inputMultiplier := Multiplier{Numerator: 1, Denominator: 1}
outputMultiplier := Multiplier{Numerator: 1, Denominator: 1}
if policy := rule.LongContextPolicy; policy != nil &&
inputTokens > policy.InputThresholdTokens {
inputMultiplier = policy.InputMultiplier
@@ -40,7 +36,7 @@ func (table *Table) Quote(upstreamModel string, result usage.Result) Quote {
components := [...]struct {
tokens int64
price Price
multiplier float64
multiplier Multiplier
}{
{tokens: result.Tokens.UncachedInput, price: rule.Prices.UncachedInput, multiplier: inputMultiplier},
{tokens: result.Tokens.CacheRead, price: rule.Prices.CacheRead, multiplier: inputMultiplier},
@@ -50,7 +46,7 @@ func (table *Table) Quote(upstreamModel string, result usage.Result) Quote {
}
totalTokens := int64(0)
cost := float64(0)
cost := NanoUSD(0)
for _, component := range components {
if component.tokens < 0 {
return Quote{State: CostStateUnpriced}
@@ -66,20 +62,20 @@ func (table *Table) Quote(upstreamModel string, result usage.Result) Quote {
if !component.price.Set {
return Quote{State: CostStateUnpriced}
}
effectivePrice := component.price.Value * component.multiplier
if !isFinite(effectivePrice) {
componentCost, ok := QuoteComponent(
component.tokens,
component.price.NanoUSDPerMillion,
component.multiplier,
)
if !ok {
return Quote{State: CostStateUnpriced}
}
componentCost := effectivePrice * float64(component.tokens) / tokensPerMillion
if !isFinite(componentCost) {
return Quote{State: CostStateUnpriced}
}
cost += componentCost
if !isFinite(cost) {
cost, ok = CheckedAddNanoUSD(cost, componentCost)
if !ok {
return Quote{State: CostStateUnpriced}
}
}
return Quote{State: CostStatePriced, Cost: cost}
return Quote{State: CostStatePriced, EstimatedCostNanoUSD: cost}
}
func checkedInputTokens(tokens usage.Tokens) (int64, bool) {
@@ -117,7 +113,3 @@ func hasBlockingDiagnostic(diagnostics usage.Diagnostics) bool {
}
return false
}
func isFinite(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
+86 -103
View File
@@ -8,34 +8,36 @@ import (
"gpt-load/internal/usage"
)
func TestQuoteUsesFiveTokenPricesWithoutRounding(t *testing.T) {
func TestQuoteRoundsFiveTokenComponentsBeforeSumming(t *testing.T) {
t.Parallel()
table := mustCompile(t, Rule{
Pattern: "model",
Prices: Prices{
UncachedInput: Price{Value: 1.11, Set: true},
CacheRead: Price{Value: 2.22, Set: true},
CacheWrite5M: Price{Value: 3.33, Set: true},
CacheWrite1H: Price{Value: 4.44, Set: true},
Output: Price{Value: 5.55, Set: true},
UncachedInput: fixedPrice(1),
CacheRead: fixedPrice(1),
CacheWrite5M: fixedPrice(1),
CacheWrite1H: fixedPrice(1),
Output: fixedPrice(1),
},
Source: SourceUser,
})
quote := table.Quote("model", usage.Result{
Tokens: usage.Tokens{
UncachedInput: 1_000_000,
CacheRead: 1_000_000,
CacheWrite5M: 1_000_000,
CacheWrite1H: 1_000_000,
Output: 1_000_000,
UncachedInput: 500_000,
CacheRead: 500_000,
CacheWrite5M: 500_000,
CacheWrite1H: 500_000,
Output: 500_000,
},
State: usage.StateComplete,
})
if quote.State != CostStatePriced {
t.Fatalf("Quote() state = %q, want %q", quote.State, CostStatePriced)
}
assertCostClose(t, quote.Cost, 16.65)
if quote.EstimatedCostNanoUSD != 5 {
t.Fatalf("Quote() cost = %d, want 5 nano USD", quote.EstimatedCostNanoUSD)
}
}
func TestQuoteAppliesBuiltinLongContextPolicyAtStrictThreshold(t *testing.T) {
@@ -44,11 +46,11 @@ func TestQuoteAppliesBuiltinLongContextPolicyAtStrictThreshold(t *testing.T) {
table := mustCompile(t, Rule{
Pattern: "gpt-5.6",
Prices: Prices{
UncachedInput: Price{Value: 1, Set: true},
CacheRead: Price{Value: 2, Set: true},
CacheWrite5M: Price{Value: 3, Set: true},
CacheWrite1H: Price{Value: 4, Set: true},
Output: Price{Value: 5, Set: true},
UncachedInput: fixedPrice(1_000_000_000),
CacheRead: fixedPrice(2_000_000_000),
CacheWrite5M: fixedPrice(3_000_000_000),
CacheWrite1H: fixedPrice(4_000_000_000),
Output: fixedPrice(5_000_000_000),
},
Source: SourceBuiltin,
SourceURL: "https://builtin.example/pricing",
@@ -58,7 +60,7 @@ func TestQuoteAppliesBuiltinLongContextPolicyAtStrictThreshold(t *testing.T) {
tests := []struct {
name string
tokens usage.Tokens
want float64
want NanoUSD
}{
{
name: "equal threshold does not multiply",
@@ -69,7 +71,7 @@ func TestQuoteAppliesBuiltinLongContextPolicyAtStrictThreshold(t *testing.T) {
CacheWrite1H: 68_000,
Output: 1_000_000,
},
want: 5.68,
want: 5_680_000_000,
},
{
name: "one token over multiplies every input price and output price",
@@ -80,7 +82,7 @@ func TestQuoteAppliesBuiltinLongContextPolicyAtStrictThreshold(t *testing.T) {
CacheWrite1H: 68_001,
Output: 1_000_000,
},
want: 8.860008,
want: 8_860_008_000,
},
}
for _, test := range tests {
@@ -96,7 +98,9 @@ func TestQuoteAppliesBuiltinLongContextPolicyAtStrictThreshold(t *testing.T) {
if quote.State != CostStatePriced {
t.Fatalf("Quote() state = %q, want %q", quote.State, CostStatePriced)
}
assertCostClose(t, quote.Cost, test.want)
if quote.EstimatedCostNanoUSD != test.want {
t.Fatalf("Quote() cost = %d, want %d nano USD", quote.EstimatedCostNanoUSD, test.want)
}
if result.Tokens != before {
t.Fatalf("Quote() mutated tokens: got %+v, want %+v", result.Tokens, before)
}
@@ -104,17 +108,17 @@ func TestQuoteAppliesBuiltinLongContextPolicyAtStrictThreshold(t *testing.T) {
}
}
func TestQuoteLongContextPolicyFailsClosedOnOverflowAndNonFiniteCost(t *testing.T) {
func TestQuoteFailsClosedOnMissingPriceAndOverflow(t *testing.T) {
t.Parallel()
baseRule := Rule{
Pattern: "model",
Prices: Prices{
UncachedInput: Price{Value: 1, Set: true},
CacheRead: Price{Value: 1, Set: true},
CacheWrite5M: Price{Value: 1, Set: true},
CacheWrite1H: Price{Value: 1, Set: true},
Output: Price{Value: 1, Set: true},
UncachedInput: fixedPrice(1),
CacheRead: fixedPrice(1),
CacheWrite5M: fixedPrice(1),
CacheWrite1H: fixedPrice(1),
Output: fixedPrice(1),
},
Source: SourceBuiltin,
SourceURL: "https://builtin.example/pricing",
@@ -123,8 +127,11 @@ func TestQuoteLongContextPolicyFailsClosedOnOverflowAndNonFiniteCost(t *testing.
}
missingPrice := baseRule
missingPrice.Prices.CacheWrite1H = Price{}
nonFiniteCost := baseRule
nonFiniteCost.Prices.UncachedInput = Price{Value: math.MaxFloat64, Set: true}
componentOverflow := baseRule
componentOverflow.Prices.Output = fixedPrice(math.MaxInt64)
sumOverflow := baseRule
sumOverflow.Prices.UncachedInput = fixedPrice(math.MaxInt64)
sumOverflow.Prices.Output = fixedPrice(1)
tests := []struct {
name string
@@ -132,7 +139,7 @@ func TestQuoteLongContextPolicyFailsClosedOnOverflowAndNonFiniteCost(t *testing.
tokens usage.Tokens
}{
{
name: "four-input sum overflow",
name: "four-input token sum overflow",
rule: baseRule,
tokens: usage.Tokens{
UncachedInput: math.MaxInt64,
@@ -140,7 +147,7 @@ func TestQuoteLongContextPolicyFailsClosedOnOverflowAndNonFiniteCost(t *testing.
},
},
{
name: "missing multiplied component price",
name: "missing used component price",
rule: missingPrice,
tokens: usage.Tokens{
UncachedInput: 272_000,
@@ -148,12 +155,23 @@ func TestQuoteLongContextPolicyFailsClosedOnOverflowAndNonFiniteCost(t *testing.
},
},
{
name: "non-finite multiplied component cost",
rule: nonFiniteCost,
name: "component result overflow",
rule: componentOverflow,
tokens: usage.Tokens{Output: 1_000_001},
},
{
name: "final sum overflow",
rule: sumOverflow,
tokens: usage.Tokens{
UncachedInput: 272_001,
UncachedInput: 1_000_000,
Output: 1_000_000,
},
},
{
name: "multiplied result overflow",
rule: componentOverflow,
tokens: usage.Tokens{UncachedInput: 272_001, Output: 1_000_000},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -161,7 +179,7 @@ func TestQuoteLongContextPolicyFailsClosedOnOverflowAndNonFiniteCost(t *testing.
Tokens: test.tokens,
State: usage.StateComplete,
})
if quote.State != CostStateUnpriced || quote.Cost != 0 {
if quote.State != CostStateUnpriced || quote.EstimatedCostNanoUSD != 0 {
t.Fatalf("Quote() = %+v, want unpriced zero", quote)
}
})
@@ -177,8 +195,8 @@ func TestQuoteUserOverridesDoNotInheritBuiltinLongContextPolicy(t *testing.T) {
rules = append(rules, Rule{
Pattern: pattern,
Prices: Prices{
UncachedInput: Price{Value: 1, Set: true},
Output: Price{Value: 1, Set: true},
UncachedInput: fixedPrice(1_000_000_000),
Output: fixedPrice(1_000_000_000),
},
Source: SourceUser,
})
@@ -198,7 +216,9 @@ func TestQuoteUserOverridesDoNotInheritBuiltinLongContextPolicy(t *testing.T) {
if quote.State != CostStatePriced {
t.Fatalf("Quote() state = %q, want %q", quote.State, CostStatePriced)
}
assertCostClose(t, quote.Cost, 1.272001)
if quote.EstimatedCostNanoUSD != 1_272_001_000 {
t.Fatalf("Quote() cost = %d, want 1272001000 nano USD", quote.EstimatedCostNanoUSD)
}
})
}
}
@@ -208,31 +228,29 @@ func TestQuoteDistinguishesUnsetFromExplicitZero(t *testing.T) {
explicitZero := mustCompile(t, Rule{
Pattern: "zero",
Prices: Prices{Output: Price{Value: 0, Set: true}},
Prices: Prices{Output: fixedPrice(0)},
Source: SourceUser,
})
zeroOutput := explicitZero.Quote("zero", usage.Result{
Tokens: usage.Tokens{Output: 1},
State: usage.StateComplete,
})
if zeroOutput.State != CostStatePriced {
t.Fatalf("explicit zero output state = %q, want %q", zeroOutput.State, CostStatePriced)
if zeroOutput.State != CostStatePriced || zeroOutput.EstimatedCostNanoUSD != 0 {
t.Fatalf("explicit zero quote = %+v, want priced zero", zeroOutput)
}
assertCostClose(t, zeroOutput.Cost, 0)
unsetRead := explicitZero.Quote("zero", usage.Result{
Tokens: usage.Tokens{CacheRead: 1},
State: usage.StateComplete,
})
if unsetRead.State != CostStateUnpriced || unsetRead.Cost != 0 {
if unsetRead.State != CostStateUnpriced || unsetRead.EstimatedCostNanoUSD != 0 {
t.Fatalf("unset cache read quote = %+v, want unpriced zero", unsetRead)
}
zeroTokens := explicitZero.Quote("zero", usage.Result{State: usage.StateComplete})
if zeroTokens.State != CostStatePriced {
t.Fatalf("zero-token quote state = %q, want %q", zeroTokens.State, CostStatePriced)
if zeroTokens.State != CostStatePriced || zeroTokens.EstimatedCostNanoUSD != 0 {
t.Fatalf("zero-token quote = %+v, want priced zero", zeroTokens)
}
assertCostClose(t, zeroTokens.Cost, 0)
}
func TestQuoteMapsUsageAndDiagnosticsToCostState(t *testing.T) {
@@ -240,18 +258,20 @@ func TestQuoteMapsUsageAndDiagnosticsToCostState(t *testing.T) {
table := mustCompile(t, Rule{
Pattern: "model",
Prices: Prices{Output: Price{Value: 1, Set: true}},
Prices: Prices{Output: fixedPrice(1_000_000_000)},
Source: SourceUser,
})
if quote := table.Quote("missing", usage.Result{State: usage.StateComplete}); quote.State != CostStateUnpriced || quote.Cost != 0 {
t.Fatalf("unmatched complete quote = %+v, want unpriced zero", quote)
assertUnpriced := func(t *testing.T, quote Quote) {
t.Helper()
if quote.State != CostStateUnpriced || quote.EstimatedCostNanoUSD != 0 {
t.Fatalf("Quote() = %+v, want unpriced zero", quote)
}
}
if quote := table.Quote("model", usage.Result{State: usage.StateNotApplicable}); quote.State != CostStateNotApplicable || quote.Cost != 0 {
assertUnpriced(t, table.Quote("missing", usage.Result{State: usage.StateComplete}))
if quote := table.Quote("model", usage.Result{State: usage.StateNotApplicable}); quote.State != CostStateNotApplicable || quote.EstimatedCostNanoUSD != 0 {
t.Fatalf("not-applicable quote = %+v, want not_applicable zero", quote)
}
if quote := table.Quote("model", usage.Result{State: usage.StateMissing}); quote.State != CostStateUnpriced || quote.Cost != 0 {
t.Fatalf("missing quote = %+v, want unpriced zero", quote)
}
assertUnpriced(t, table.Quote("model", usage.Result{State: usage.StateMissing}))
for _, state := range []usage.State{usage.StateComplete, usage.StatePartial} {
quote := table.Quote("model", usage.Result{Tokens: usage.Tokens{Output: 1}, State: state})
if quote.State != CostStatePriced {
@@ -273,7 +293,7 @@ func TestQuoteMapsUsageAndDiagnosticsToCostState(t *testing.T) {
State: usage.StateComplete,
Diagnostics: diagnostics,
})
if quote.State != CostStateUnpriced || quote.Cost != 0 {
if quote.State != CostStateUnpriced || quote.EstimatedCostNanoUSD != 0 {
t.Errorf("diagnostic %q quote = %+v, want unpriced zero", diagnostic, quote)
}
}
@@ -292,41 +312,7 @@ func TestQuoteMapsUsageAndDiagnosticsToCostState(t *testing.T) {
t.Errorf("diagnostic %q quote state = %q, want %q", diagnostic, quote.State, CostStatePriced)
}
}
if quote := table.Quote("model", usage.Result{State: usage.State("unknown")}); quote.State != CostStateUnpriced || quote.Cost != 0 {
t.Fatalf("unknown usage state quote = %+v, want unpriced zero", quote)
}
}
func TestQuoteRejectsNonFiniteMultiplicationAndAccumulation(t *testing.T) {
t.Parallel()
multiplication := mustCompile(t, Rule{
Pattern: "multiply",
Prices: Prices{Output: Price{Value: math.MaxFloat64, Set: true}},
Source: SourceUser,
})
if quote := multiplication.Quote("multiply", usage.Result{
Tokens: usage.Tokens{Output: 2_000_000},
State: usage.StateComplete,
}); quote.State != CostStateUnpriced || quote.Cost != 0 {
t.Fatalf("non-finite multiplication quote = %+v, want unpriced zero", quote)
}
accumulation := mustCompile(t, Rule{
Pattern: "accumulate",
Prices: Prices{
UncachedInput: Price{Value: math.MaxFloat64 / 2, Set: true},
CacheRead: Price{Value: math.MaxFloat64 / 2, Set: true},
Output: Price{Value: math.MaxFloat64 / 2, Set: true},
},
Source: SourceUser,
})
if quote := accumulation.Quote("accumulate", usage.Result{
Tokens: usage.Tokens{UncachedInput: 1_000_000, CacheRead: 1_000_000, Output: 1_000_000},
State: usage.StateComplete,
}); quote.State != CostStateUnpriced || quote.Cost != 0 {
t.Fatalf("non-finite accumulation quote = %+v, want unpriced zero", quote)
}
assertUnpriced(t, table.Quote("model", usage.Result{State: usage.State("unknown")}))
}
func TestQuoteRejectsNegativeTokensAndTokenTotalOverflow(t *testing.T) {
@@ -335,15 +321,15 @@ func TestQuoteRejectsNegativeTokensAndTokenTotalOverflow(t *testing.T) {
table := mustCompile(t, Rule{
Pattern: "model",
Prices: Prices{
UncachedInput: Price{Value: 0, Set: true},
CacheRead: Price{Value: 0, Set: true},
CacheWrite5M: Price{Value: 0, Set: true},
CacheWrite1H: Price{Value: 0, Set: true},
Output: Price{Value: 0, Set: true},
UncachedInput: fixedPrice(0),
CacheRead: fixedPrice(0),
CacheWrite5M: fixedPrice(0),
CacheWrite1H: fixedPrice(0),
Output: fixedPrice(0),
},
Source: SourceUser,
})
var perFieldOverflow int64 = math.MaxInt64/5 + 1
perFieldOverflow := int64(math.MaxInt64/5 + 1)
tests := []struct {
name string
tokens usage.Tokens
@@ -369,7 +355,7 @@ func TestQuoteRejectsNegativeTokensAndTokenTotalOverflow(t *testing.T) {
Tokens: test.tokens,
State: usage.StateComplete,
})
if quote.State != CostStateUnpriced || quote.Cost != 0 {
if quote.State != CostStateUnpriced || quote.EstimatedCostNanoUSD != 0 {
t.Fatalf("Quote() = %+v, want unpriced zero", quote)
}
})
@@ -385,17 +371,14 @@ func mustCompile(t *testing.T, rules ...Rule) *Table {
return table
}
func assertCostClose(t *testing.T, got, want float64) {
t.Helper()
if math.Abs(got-want) > 1e-12 {
t.Fatalf("cost = %.17g, want %.17g", got, want)
}
func fixedPrice(value NanoUSD) Price {
return Price{NanoUSDPerMillion: value, Set: true}
}
func testLongContextPolicy() *LongContextPolicy {
return &LongContextPolicy{
InputThresholdTokens: 272_000,
InputMultiplier: 2,
OutputMultiplier: 1.5,
InputMultiplier: Multiplier{Numerator: 2, Denominator: 1},
OutputMultiplier: Multiplier{Numerator: 3, Denominator: 2},
}
}
+7 -7
View File
@@ -5,8 +5,8 @@ import "time"
// Price is a USD price per one million tokens. Set distinguishes zero from an
// unavailable price.
type Price struct {
Value float64
Set bool
NanoUSDPerMillion NanoUSD
Set bool
}
// Prices is the provider-neutral price breakdown.
@@ -21,8 +21,8 @@ type Prices struct {
// LongContextPolicy adjusts component prices after an input-token threshold.
type LongContextPolicy struct {
InputThresholdTokens int64
InputMultiplier float64
OutputMultiplier float64
InputMultiplier Multiplier
OutputMultiplier Multiplier
}
// Source identifies where a pricing rule came from.
@@ -52,10 +52,10 @@ const (
CostStateNotApplicable CostState = "not_applicable"
)
// Quote is a calculated request cost in USD.
// Quote is a calculated request cost in nano USD.
type Quote struct {
State CostState
Cost float64
State CostState
EstimatedCostNanoUSD NanoUSD
}
// Table is an immutable compiled set of pricing rules.
+436
View File
@@ -0,0 +1,436 @@
package requestlog
import (
"context"
"errors"
"fmt"
"sort"
"gorm.io/gorm"
"gpt-load/internal/platform/epochms"
"gpt-load/internal/storage/models"
)
const (
homeStatisticsRankingLimit = 5
maxJSONSafeInteger int64 = 9_007_199_254_740_991
)
type HomeStatisticsRange string
const (
HomeStatistics24H HomeStatisticsRange = "24h"
HomeStatistics30D HomeStatisticsRange = "30d"
)
type HomeStatisticsQuery struct {
Range HomeStatisticsRange
ObservedAtMS int64
}
type HomeStatisticsRef struct {
ID uint
Name *string
Deleted bool
}
type HomeModelRanking struct {
Group HomeStatisticsRef
Model string
UsageAggregate
}
type HomeGroupRanking struct {
Group HomeStatisticsRef
UsageAggregate
}
type HomeAccessKeyRanking struct {
AccessKey HomeStatisticsRef
UsageAggregate
}
type HomeStatisticsReport struct {
Range HomeStatisticsRange
ObservedAtMS int64
FromMS int64
ToMS int64
Granularity UsageGranularity
Summary UsageAggregate
Series []UsageSeriesPoint
TopModels []HomeModelRanking
TopGroups []HomeGroupRanking
TopAccessKeys []HomeAccessKeyRanking
}
func (service *Service) QueryHomeStatistics(
ctx context.Context,
input HomeStatisticsQuery,
) (HomeStatisticsReport, error) {
if service == nil || service.db == nil {
return HomeStatisticsReport{}, fmt.Errorf("query home statistics: database is nil")
}
if ctx == nil {
ctx = context.Background()
}
width, count, granularity, err := homeStatisticsWindow(input.Range)
if err != nil {
return HomeStatisticsReport{}, err
}
fromMS, toMS, err := epochms.WindowEndingAt(input.ObservedAtMS, width, count)
if err != nil {
return HomeStatisticsReport{}, fmt.Errorf("query home statistics: invalid observed time: %w", err)
}
usageInput := UsageQuery{
FromMS: fromMS,
ToMS: toMS,
Granularity: granularity,
Limit: homeStatisticsRankingLimit,
BreakdownOrder: UsageBreakdownOrderCost,
}
report := HomeStatisticsReport{
Range: input.Range,
ObservedAtMS: input.ObservedAtMS,
FromMS: fromMS,
ToMS: toMS,
Granularity: granularity,
}
err = service.db.WithContext(ctx).Connection(func(connection *gorm.DB) (operationErr error) {
if err := connection.Exec("BEGIN DEFERRED").Error; err != nil {
return fmt.Errorf("begin home statistics read transaction: %w", err)
}
committed := false
defer func() {
if !committed {
if rollbackErr := rollbackUsageReadTransaction(connection); rollbackErr != nil {
operationErr = errors.Join(operationErr, rollbackErr)
}
}
}()
scope := usageStatScope(connection, usageInput)
if err := validateUsageStatIntegrity(scope); err != nil {
return err
}
summary, err := queryUsageSummary(usageStatScope(connection, usageInput))
if err != nil {
return err
}
if err := validateHomeStatisticsAggregate(summary); err != nil {
return fmt.Errorf("validate home statistics summary: %w", err)
}
sparseSeries, err := queryUsageSeries(
usageStatScope(connection, usageInput),
granularity,
)
if err != nil {
return err
}
for _, point := range sparseSeries {
if err := validateHomeStatisticsAggregate(point.UsageAggregate); err != nil {
return fmt.Errorf("validate home statistics series: %w", err)
}
}
modelRows, err := queryHomeModelRankings(
usageStatScope(connection, usageInput),
)
if err != nil {
return err
}
groupRows, err := queryHomeGroupRankings(
usageStatScope(connection, usageInput),
)
if err != nil {
return err
}
accessRows, err := queryHomeAccessKeyRankings(
usageStatScope(connection, usageInput),
)
if err != nil {
return err
}
groupRefs, err := loadHomeGroupRefs(connection, modelRows, groupRows)
if err != nil {
return err
}
accessRefs, err := loadHomeAccessKeyRefs(connection, accessRows)
if err != nil {
return err
}
report.Summary = summary
report.Series = denseHomeStatisticsSeries(
fromMS,
toMS,
width,
sparseSeries,
)
report.TopModels = mapHomeModelRankings(modelRows, groupRefs)
report.TopGroups = mapHomeGroupRankings(groupRows, groupRefs)
report.TopAccessKeys = mapHomeAccessKeyRankings(accessRows, accessRefs)
if err := connection.Exec("COMMIT").Error; err != nil {
return fmt.Errorf("commit home statistics read transaction: %w", err)
}
committed = true
return nil
})
if err != nil {
return HomeStatisticsReport{}, fmt.Errorf("query home statistics: %w", err)
}
return report, nil
}
func homeStatisticsWindow(
value HomeStatisticsRange,
) (width int64, count int, granularity UsageGranularity, err error) {
switch value {
case HomeStatistics24H:
return epochms.MillisecondsPerHour, 24, UsageGranularityHour, nil
case HomeStatistics30D:
return epochms.MillisecondsPerDay, 30, UsageGranularityDay, nil
default:
return 0, 0, "", fmt.Errorf(
"query home statistics: unsupported range %q",
value,
)
}
}
func denseHomeStatisticsSeries(
fromMS int64,
toMS int64,
width int64,
sparse []UsageSeriesPoint,
) []UsageSeriesPoint {
byStart := make(map[int64]UsageAggregate, len(sparse))
for _, point := range sparse {
byStart[point.BucketStartMS] = point.UsageAggregate
}
result := make([]UsageSeriesPoint, 0, int((toMS-fromMS)/width))
for bucketStartMS := fromMS; bucketStartMS < toMS; bucketStartMS += width {
result = append(result, UsageSeriesPoint{
BucketStartMS: bucketStartMS,
BucketEndMS: bucketStartMS + width,
UsageAggregate: byStart[bucketStartMS],
})
}
return result
}
type homeModelRankingRow struct {
GroupID uint
Model string
UsageAggregate
}
type homeGroupRankingRow struct {
GroupID uint
UsageAggregate
}
type homeAccessKeyRankingRow struct {
AccessKeyID uint
UsageAggregate
}
func queryHomeModelRankings(scope *gorm.DB) ([]homeModelRankingRow, error) {
var rows []homeModelRankingRow
if err := scope.
Select("group_id, model, " + usageAggregateSelect).
Group("group_id, model").
Order("SUM(estimated_cost_nano_usd) DESC").
Order("SUM(request_count) DESC").
Order("group_id ASC").
Order("model ASC").
Limit(homeStatisticsRankingLimit).
Find(&rows).Error; err != nil {
return nil, fmt.Errorf("query home model rankings: %w", err)
}
for _, row := range rows {
if err := validateHomeStatisticsAggregate(row.UsageAggregate); err != nil {
return nil, fmt.Errorf("validate home model ranking: %w", err)
}
}
return rows, nil
}
func queryHomeGroupRankings(scope *gorm.DB) ([]homeGroupRankingRow, error) {
var rows []homeGroupRankingRow
if err := scope.
Select("group_id, " + usageAggregateSelect).
Group("group_id").
Order("SUM(estimated_cost_nano_usd) DESC").
Order("SUM(request_count) DESC").
Order("group_id ASC").
Limit(homeStatisticsRankingLimit).
Find(&rows).Error; err != nil {
return nil, fmt.Errorf("query home group rankings: %w", err)
}
for _, row := range rows {
if err := validateHomeStatisticsAggregate(row.UsageAggregate); err != nil {
return nil, fmt.Errorf("validate home group ranking: %w", err)
}
}
return rows, nil
}
func queryHomeAccessKeyRankings(scope *gorm.DB) ([]homeAccessKeyRankingRow, error) {
var rows []homeAccessKeyRankingRow
if err := scope.
Select("access_key_id, " + usageAggregateSelect).
Group("access_key_id").
Order("SUM(estimated_cost_nano_usd) DESC").
Order("SUM(request_count) DESC").
Order("access_key_id ASC").
Limit(homeStatisticsRankingLimit).
Find(&rows).Error; err != nil {
return nil, fmt.Errorf("query home access key rankings: %w", err)
}
for _, row := range rows {
if err := validateHomeStatisticsAggregate(row.UsageAggregate); err != nil {
return nil, fmt.Errorf("validate home access key ranking: %w", err)
}
}
return rows, nil
}
func validateHomeStatisticsAggregate(value UsageAggregate) error {
if err := validateUsageAggregate(value); err != nil {
return err
}
for _, field := range []struct {
name string
value int64
}{
{name: "request count", value: value.RequestCount},
{name: "success count", value: value.SuccessCount},
{name: "failure count", value: value.FailureCount},
{name: "uncached input tokens", value: value.UncachedInputTokens},
{name: "cache read tokens", value: value.CacheReadTokens},
{name: "cache write 5m tokens", value: value.CacheWrite5MTokens},
{name: "cache write 1h tokens", value: value.CacheWrite1HTokens},
{name: "output tokens", value: value.OutputTokens},
{name: "usage missing count", value: value.UsageMissingCount},
{name: "partial count", value: value.PartialCount},
{name: "unpriced request count", value: value.UnpricedRequestCount},
} {
if field.value > maxJSONSafeInteger {
return fmt.Errorf("%s exceeds JSON safe integer", field.name)
}
}
return nil
}
func loadHomeGroupRefs(
tx *gorm.DB,
modelRows []homeModelRankingRow,
groupRows []homeGroupRankingRow,
) (map[uint]HomeStatisticsRef, error) {
ids := make(map[uint]struct{}, len(modelRows)+len(groupRows))
for _, row := range modelRows {
ids[row.GroupID] = struct{}{}
}
for _, row := range groupRows {
ids[row.GroupID] = struct{}{}
}
return loadHomeRefs[models.Group](tx, ids, "groups")
}
func loadHomeAccessKeyRefs(
tx *gorm.DB,
rows []homeAccessKeyRankingRow,
) (map[uint]HomeStatisticsRef, error) {
ids := make(map[uint]struct{}, len(rows))
for _, row := range rows {
ids[row.AccessKeyID] = struct{}{}
}
return loadHomeRefs[models.AccessKey](tx, ids, "access keys")
}
func loadHomeRefs[T any](
tx *gorm.DB,
idSet map[uint]struct{},
label string,
) (map[uint]HomeStatisticsRef, error) {
ids := make([]uint, 0, len(idSet))
for id := range idSet {
if id != 0 {
ids = append(ids, id)
}
}
sort.Slice(ids, func(left, right int) bool { return ids[left] < ids[right] })
refs := make(map[uint]HomeStatisticsRef, len(idSet))
for id := range idSet {
refs[id] = HomeStatisticsRef{ID: id, Deleted: true}
}
if len(ids) == 0 {
return refs, nil
}
var rows []struct {
ID uint
Name string
}
if err := tx.Session(&gorm.Session{NewDB: true}).Model(new(T)).
Select("id", "name").
Where("id IN ?", ids).
Find(&rows).Error; err != nil {
return nil, fmt.Errorf("query home statistics %s: %w", label, err)
}
for _, row := range rows {
name := row.Name
refs[row.ID] = HomeStatisticsRef{
ID: row.ID,
Name: &name,
Deleted: false,
}
}
return refs, nil
}
func mapHomeModelRankings(
rows []homeModelRankingRow,
refs map[uint]HomeStatisticsRef,
) []HomeModelRanking {
result := make([]HomeModelRanking, 0, len(rows))
for _, row := range rows {
result = append(result, HomeModelRanking{
Group: refs[row.GroupID],
Model: row.Model,
UsageAggregate: row.UsageAggregate,
})
}
return result
}
func mapHomeGroupRankings(
rows []homeGroupRankingRow,
refs map[uint]HomeStatisticsRef,
) []HomeGroupRanking {
result := make([]HomeGroupRanking, 0, len(rows))
for _, row := range rows {
result = append(result, HomeGroupRanking{
Group: refs[row.GroupID],
UsageAggregate: row.UsageAggregate,
})
}
return result
}
func mapHomeAccessKeyRankings(
rows []homeAccessKeyRankingRow,
refs map[uint]HomeStatisticsRef,
) []HomeAccessKeyRanking {
result := make([]HomeAccessKeyRanking, 0, len(rows))
for _, row := range rows {
result = append(result, HomeAccessKeyRanking{
AccessKey: refs[row.AccessKeyID],
UsageAggregate: row.UsageAggregate,
})
}
return result
}
+361
View File
@@ -0,0 +1,361 @@
package requestlog
import (
"context"
"math"
"testing"
"gorm.io/gorm"
"gpt-load/internal/storage/models"
)
func TestQueryHomeStatisticsReturnsDenseEpochWindowsForEmptyData(t *testing.T) {
const observedAtMS int64 = 1_784_896_496_789
tests := []struct {
name string
rangeValue HomeStatisticsRange
fromMS int64
toMS int64
bucketWidth int64
buckets int
}{
{
name: "24 hours",
rangeValue: HomeStatistics24H,
fromMS: 1_784_811_600_000,
toMS: 1_784_898_000_000,
bucketWidth: 3_600_000,
buckets: 24,
},
{
name: "30 days",
rangeValue: HomeStatistics30D,
fromMS: 1_782_345_600_000,
toMS: 1_784_937_600_000,
bucketWidth: 86_400_000,
buckets: 30,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
report, err := newRequestLogTestService(openRequestLogQueryDB(t)).
QueryHomeStatistics(context.Background(), HomeStatisticsQuery{
Range: test.rangeValue,
ObservedAtMS: observedAtMS,
})
if err != nil {
t.Fatalf("QueryHomeStatistics() error = %v", err)
}
if report.Range != test.rangeValue || report.ObservedAtMS != observedAtMS ||
report.FromMS != test.fromMS || report.ToMS != test.toMS ||
len(report.Series) != test.buckets {
t.Fatalf("window/report = %#v", report)
}
if report.Summary != (UsageAggregate{}) ||
len(report.TopModels) != 0 ||
len(report.TopGroups) != 0 ||
len(report.TopAccessKeys) != 0 {
t.Fatalf("empty aggregates/rankings = %#v", report)
}
for index, point := range report.Series {
wantStart := test.fromMS + int64(index)*test.bucketWidth
if point.BucketStartMS != wantStart ||
point.BucketEndMS != wantStart+test.bucketWidth ||
point.UsageAggregate != (UsageAggregate{}) {
t.Fatalf("series[%d] = %#v", index, point)
}
}
})
}
}
func TestQueryHomeStatisticsBuildsTopFiveRankingsAndDeletedRefs(t *testing.T) {
db := openRequestLogQueryDB(t)
groupA := createHomeStatisticsGroup(t, db, "Group A")
groupB := createHomeStatisticsGroup(t, db, "Group B")
accessA := createHomeStatisticsAccessKey(t, db, "Access A", "0001")
accessB := createHomeStatisticsAccessKey(t, db, "Access B", "0002")
const bucketStartMS int64 = 1_784_894_400_000
rows := []models.UsageStat{
homeStatisticsStat(bucketStartMS, accessA.ID, groupA.ID, "z-model", 5, 500),
homeStatisticsStat(bucketStartMS, accessA.ID, groupA.ID, "a-model", 5, 500),
homeStatisticsStat(bucketStartMS, accessB.ID, groupB.ID, "a-model", 5, 500),
homeStatisticsStat(bucketStartMS, 999, 999, "deleted-model", 100, 400),
homeStatisticsStat(bucketStartMS, 0, 0, "", 20, 300),
homeStatisticsStat(bucketStartMS, accessB.ID, groupB.ID, "low-model", 1, 200),
homeStatisticsStat(bucketStartMS, accessB.ID, groupB.ID, "excluded-model", 1, 100),
}
createUsageStats(t, db, rows...)
report, err := newRequestLogTestService(db).QueryHomeStatistics(
context.Background(),
HomeStatisticsQuery{
Range: HomeStatistics24H,
ObservedAtMS: 1_784_896_496_789,
},
)
if err != nil {
t.Fatalf("QueryHomeStatistics() error = %v", err)
}
if report.Summary.RequestCount != 137 ||
report.Summary.SuccessCount != 137 ||
report.Summary.UncachedInputTokens != 1_370 ||
report.Summary.EstimatedCostNanoUSD != 2_500 {
t.Fatalf("summary = %#v", report.Summary)
}
if len(report.TopModels) != 5 ||
len(report.TopGroups) != 4 ||
len(report.TopAccessKeys) != 4 {
t.Fatalf(
"ranking lengths = %d/%d/%d",
len(report.TopModels),
len(report.TopGroups),
len(report.TopAccessKeys),
)
}
wantModels := []struct {
groupID uint
model string
}{
{groupA.ID, "a-model"},
{groupA.ID, "z-model"},
{groupB.ID, "a-model"},
{999, "deleted-model"},
{0, ""},
}
for index, want := range wantModels {
got := report.TopModels[index]
if got.Group.ID != want.groupID || got.Model != want.model {
t.Fatalf("TopModels[%d] = %#v, want group/model %d/%q", index, got, want.groupID, want.model)
}
}
if report.TopModels[0].Group.Name == nil ||
*report.TopModels[0].Group.Name != "Group A" ||
report.TopModels[0].Group.Deleted {
t.Fatalf("current group ref = %#v", report.TopModels[0].Group)
}
for _, index := range []int{3, 4} {
if !report.TopModels[index].Group.Deleted ||
report.TopModels[index].Group.Name != nil {
t.Fatalf("deleted/zero group ref = %#v", report.TopModels[index].Group)
}
}
if report.TopGroups[0].Group.ID != groupA.ID ||
report.TopGroups[0].EstimatedCostNanoUSD != 1_000 ||
report.TopGroups[1].Group.ID != groupB.ID ||
report.TopGroups[1].EstimatedCostNanoUSD != 800 {
t.Fatalf("TopGroups = %#v", report.TopGroups)
}
if report.TopAccessKeys[0].AccessKey.ID != accessA.ID ||
report.TopAccessKeys[0].AccessKey.Name == nil ||
*report.TopAccessKeys[0].AccessKey.Name != "Access A" ||
report.TopAccessKeys[0].AccessKey.Deleted ||
report.TopAccessKeys[1].AccessKey.ID != accessB.ID {
t.Fatalf(
"TopAccessKeys = %#v; first name = %q",
report.TopAccessKeys,
*report.TopAccessKeys[0].AccessKey.Name,
)
}
for _, index := range []int{2, 3} {
if !report.TopAccessKeys[index].AccessKey.Deleted ||
report.TopAccessKeys[index].AccessKey.Name != nil {
t.Fatalf("deleted/zero access ref = %#v", report.TopAccessKeys[index].AccessKey)
}
}
}
func TestQueryHomeStatisticsUsesOneReadSnapshot(t *testing.T) {
db, dsn := openRequestLogFileDB(t)
const bucketStartMS int64 = 1_784_894_400_000
createUsageStats(
t,
db,
homeStatisticsStat(bucketStartMS, 1, 1, "before", 1, 10),
)
writerDB, closeWriter := openUsageQueryWriterDB(t, dsn)
defer closeWriter()
inserted := false
const callbackName = "test:home_statistics_snapshot_insert"
if err := db.Callback().Query().After("gorm:query").Register(callbackName, func(tx *gorm.DB) {
if inserted || tx.Statement.Table != "usage_stats" {
return
}
inserted = true
if err := writerDB.Create(&models.UsageStat{
BucketStartMS: bucketStartMS,
AccessKeyID: 2,
GroupID: 2,
Model: "after-first-read",
RequestCount: 100,
SuccessCount: 100,
EstimatedCostNanoUSD: 1_000,
}).Error; err != nil {
t.Errorf("insert concurrent UsageStat: %v", err)
}
}); err != nil {
t.Fatalf("register query callback: %v", err)
}
t.Cleanup(func() {
if err := db.Callback().Query().Remove(callbackName); err != nil {
t.Errorf("remove query callback: %v", err)
}
})
report, err := newRequestLogTestService(db).QueryHomeStatistics(
context.Background(),
HomeStatisticsQuery{
Range: HomeStatistics24H,
ObservedAtMS: 1_784_896_496_789,
},
)
if err != nil {
t.Fatalf("QueryHomeStatistics() error = %v", err)
}
if !inserted || report.Summary.RequestCount != 1 ||
len(report.TopModels) != 1 || report.TopModels[0].Model != "before" ||
len(report.TopGroups) != 1 || len(report.TopAccessKeys) != 1 {
t.Fatalf("report did not retain one snapshot: inserted=%t report=%#v", inserted, report)
}
}
func TestQueryHomeStatisticsRejectsUnsafeOrCorruptAggregates(t *testing.T) {
const (
bucketStartMS int64 = 1_784_894_400_000
observedAtMS int64 = 1_784_896_496_789
maxSafeInt int64 = 9_007_199_254_740_991
)
tests := []struct {
name string
rows []models.UsageStat
}{
{
name: "unsafe request count",
rows: []models.UsageStat{{
BucketStartMS: bucketStartMS,
AccessKeyID: 1,
GroupID: 1,
Model: "unsafe-count",
RequestCount: maxSafeInt + 1,
SuccessCount: maxSafeInt + 1,
}},
},
{
name: "unsafe token count",
rows: []models.UsageStat{{
BucketStartMS: bucketStartMS,
AccessKeyID: 1,
GroupID: 1,
Model: "unsafe-token",
RequestCount: 1,
SuccessCount: 1,
UncachedInputTokens: maxSafeInt + 1,
}},
},
{
name: "request invariant mismatch",
rows: []models.UsageStat{{
BucketStartMS: bucketStartMS,
AccessKeyID: 1,
GroupID: 1,
Model: "bad-counts",
RequestCount: 3,
SuccessCount: 1,
FailureCount: 1,
}},
},
{
name: "cost overflow",
rows: []models.UsageStat{
homeStatisticsStat(bucketStartMS, 1, 1, "cost-a", 1, math.MaxInt64),
homeStatisticsStat(bucketStartMS, 2, 2, "cost-b", 1, math.MaxInt64),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db := openRequestLogQueryDB(t)
createCorruptUsageStats(t, db, test.rows...)
if _, err := newRequestLogTestService(db).QueryHomeStatistics(
context.Background(),
HomeStatisticsQuery{
Range: HomeStatistics24H,
ObservedAtMS: observedAtMS,
},
); err == nil {
t.Fatal("QueryHomeStatistics() error = nil, want fail-closed rejection")
}
})
}
}
func TestQueryHomeStatisticsRejectsInvalidQuery(t *testing.T) {
service := newRequestLogTestService(openRequestLogQueryDB(t))
for _, query := range []HomeStatisticsQuery{
{Range: HomeStatisticsRange("1h"), ObservedAtMS: 1},
{Range: HomeStatistics24H, ObservedAtMS: -1},
} {
if _, err := service.QueryHomeStatistics(context.Background(), query); err == nil {
t.Fatalf("QueryHomeStatistics(%#v) error = nil", query)
}
}
}
func homeStatisticsStat(
bucketStartMS int64,
accessKeyID uint,
groupID uint,
model string,
requestCount int64,
cost int64,
) models.UsageStat {
return models.UsageStat{
BucketStartMS: bucketStartMS,
AccessKeyID: accessKeyID,
GroupID: groupID,
Model: model,
RequestCount: requestCount,
SuccessCount: requestCount,
UncachedInputTokens: requestCount * 10,
EstimatedCostNanoUSD: cost,
}
}
func createHomeStatisticsGroup(t *testing.T, db *gorm.DB, name string) models.Group {
t.Helper()
row := models.Group{
Name: name,
UpstreamURL: "https://example.invalid",
Protocols: models.JSON(`[]`),
Models: models.JSON(`[]`),
Enabled: true,
}
if err := db.Create(&row).Error; err != nil {
t.Fatalf("create Group %q: %v", name, err)
}
return row
}
func createHomeStatisticsAccessKey(
t *testing.T,
db *gorm.DB,
name string,
suffix string,
) models.AccessKey {
t.Helper()
row := models.AccessKey{
Name: name,
KeyValue: "encrypted-" + suffix,
KeyHash: "hash-" + suffix,
KeySuffix: suffix,
Status: "active",
Filters: models.JSON(`{}`),
}
if err := db.Create(&row).Error; err != nil {
t.Fatalf("create AccessKey %q: %v", name, err)
}
return row
}
+31 -24
View File
@@ -2,9 +2,11 @@ package requestlog
import (
"encoding/json"
"fmt"
"strings"
"unicode/utf8"
"gpt-load/internal/platform/epochms"
"gpt-load/internal/platform/redact"
"gpt-load/internal/pricing"
"gpt-load/internal/storage/models"
@@ -22,7 +24,12 @@ func mapEvent(
redactor *redact.Redactor,
event telemetry.RequestEvent,
prices *pricing.Table,
) models.RequestLog {
) (models.RequestLog, error) {
completedAtMS, err := epochms.FromTime(event.CompletedAt)
if err != nil {
return models.RequestLog{}, fmt.Errorf("map request event completion time: %w", err)
}
attempts := make([]Attempt, 0, len(event.Attempts))
for _, attempt := range event.Attempts {
attempts = append(attempts, Attempt{
@@ -55,29 +62,29 @@ func mapEvent(
}
return models.RequestLog{
ID: event.RequestID,
CreatedAt: event.CompletedAt.UTC(),
AccessKeyID: event.AccessKeyID,
GroupID: event.Usage.GroupID,
Protocol: string(event.Protocol),
ClientModel: redactIdentityValue(redactor, projectModel(event.ClientModel)),
UpstreamModel: redactIdentityValue(redactor, projectModel(event.UpstreamModel)),
Status: string(event.Status),
StatusCode: event.StatusCode,
DurationMs: event.DurationMs,
ErrorCode: event.ErrorCode,
ErrorSummary: sanitizeSummary(redactor, event.ErrorSummary),
AffinityHit: false,
InputTokens: result.Tokens.UncachedInput,
OutputTokens: result.Tokens.Output,
CacheReadTokens: result.Tokens.CacheRead,
CacheWrite5MTokens: result.Tokens.CacheWrite5M,
CacheWrite1HTokens: result.Tokens.CacheWrite1H,
Cost: quote.Cost,
UsageState: string(result.State),
CostState: string(quote.State),
Attempts: models.JSON(encodedAttempts),
}
ID: event.RequestID,
CompletedAtMS: completedAtMS,
AccessKeyID: event.AccessKeyID,
GroupID: event.Usage.GroupID,
Protocol: string(event.Protocol),
ClientModel: redactIdentityValue(redactor, projectModel(event.ClientModel)),
UpstreamModel: redactIdentityValue(redactor, projectModel(event.UpstreamModel)),
Status: string(event.Status),
StatusCode: event.StatusCode,
DurationMs: event.DurationMs,
ErrorCode: event.ErrorCode,
ErrorSummary: sanitizeSummary(redactor, event.ErrorSummary),
AffinityHit: false,
UncachedInputTokens: result.Tokens.UncachedInput,
OutputTokens: result.Tokens.Output,
CacheReadTokens: result.Tokens.CacheRead,
CacheWrite5MTokens: result.Tokens.CacheWrite5M,
CacheWrite1HTokens: result.Tokens.CacheWrite1H,
EstimatedCostNanoUSD: int64(quote.EstimatedCostNanoUSD),
UsageState: string(result.State),
CostState: string(quote.State),
Attempts: models.JSON(encodedAttempts),
}, nil
}
func redactIdentityValue(redactor *redact.Redactor, value string) string {
+72 -33
View File
@@ -46,9 +46,14 @@ func TestMapEventPersistsCompletedAtZeroUsageAndJSONArray(t *testing.T) {
}},
}
row := mapEvent(redact.New(), event, nil)
if row.ID != event.RequestID || !row.CreatedAt.Equal(completedAt.UTC()) || row.CreatedAt.Location() != time.UTC {
t.Fatalf("identity/completed_at = %q/%v, want %q/%v UTC", row.ID, row.CreatedAt, event.RequestID, completedAt.UTC())
row := mustMapEvent(t, redact.New(), event, nil)
if row.ID != event.RequestID || row.CompletedAtMS != 1_784_896_200_000 {
t.Fatalf(
"identity/completed_at_ms = %q/%d, want %q/1784896200000",
row.ID,
row.CompletedAtMS,
event.RequestID,
)
}
if row.AccessKeyID != 17 || row.Protocol != string(protocol.OpenAICompletions) || row.ClientModel != "client-model" ||
row.UpstreamModel != "upstream-model" || row.Status != "error" || row.StatusCode != 429 ||
@@ -59,8 +64,8 @@ func TestMapEventPersistsCompletedAtZeroUsageAndJSONArray(t *testing.T) {
if row.AffinityHit {
t.Fatal("AffinityHit = true, want false in M3")
}
if row.InputTokens != 0 || row.OutputTokens != 0 || row.CacheReadTokens != 0 ||
row.CacheWrite5MTokens != 0 || row.CacheWrite1HTokens != 0 || row.Cost != 0 {
if row.UncachedInputTokens != 0 || row.OutputTokens != 0 || row.CacheReadTokens != 0 ||
row.CacheWrite5MTokens != 0 || row.CacheWrite1HTokens != 0 || row.EstimatedCostNanoUSD != 0 {
t.Fatalf("usage fields are non-zero: %+v", row)
}
@@ -73,12 +78,24 @@ func TestMapEventPersistsCompletedAtZeroUsageAndJSONArray(t *testing.T) {
t.Fatalf("attempts = %+v", attempts)
}
zeroAttempts := mapEvent(redact.New(), telemetry.RequestEvent{}, nil)
zeroEvent := telemetry.RequestEvent{
CompletedAt: time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC),
}
zeroAttempts := mustMapEvent(t, redact.New(), zeroEvent, nil)
if string(zeroAttempts.Attempts) != "[]" {
t.Fatalf("zero attempts JSON = %q, want []", zeroAttempts.Attempts)
}
}
func TestMapEventRejectsPreEpochCompletion(t *testing.T) {
event := testEvent("pre-epoch-completion")
event.CompletedAt = time.Unix(-1, 0)
if _, err := mapEvent(redact.New(), event, nil); err == nil {
t.Fatal("mapEvent() error = nil, want pre-epoch completion rejection")
}
}
func TestMapEventDefensivelyRedactsAndBoundsSummaries(t *testing.T) {
const secret = "sk-this-is-a-secret-value"
unsafeSummary := string([]byte{0xff}) + "\r\n\t " + secret + " " + strings.Repeat("界", 500)
@@ -89,7 +106,8 @@ func TestMapEventDefensivelyRedactsAndBoundsSummaries(t *testing.T) {
}},
}
row := mapEvent(redact.New(), event, nil)
event.CompletedAt = time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
row := mustMapEvent(t, redact.New(), event, nil)
if len(row.ErrorSummary) > maxSummaryBytes || !utf8.ValidString(row.ErrorSummary) {
t.Fatalf("request summary bytes/UTF-8 = %d/%t", len(row.ErrorSummary), utf8.ValidString(row.ErrorSummary))
}
@@ -133,9 +151,10 @@ func TestMapEventBoundsModelsAtUTF8Boundary(t *testing.T) {
projectedPriceTable := compileRequestLogTestPriceTable(
t,
strings.Repeat("b", wantMaxModelBytes-len(truncatedMarker))+truncatedMarker,
pricing.Prices{Output: pricing.Price{Value: 1, Set: true}},
pricing.Prices{Output: pricing.Price{NanoUSDPerMillion: 1_000_000_000, Set: true}},
)
row := mapEvent(redact.New(), event, projectedPriceTable)
event.CompletedAt = time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
row := mustMapEvent(t, redact.New(), event, projectedPriceTable)
assertProjectedModel := func(name, projected, original string, wantTruncated bool) {
t.Helper()
@@ -188,7 +207,7 @@ func TestMapEventBoundsModelsAtUTF8Boundary(t *testing.T) {
rawPriceTable := compileRequestLogTestPriceTable(
t,
rawInvalidModel,
pricing.Prices{Output: pricing.Price{Value: 2, Set: true}},
pricing.Prices{Output: pricing.Price{NanoUSDPerMillion: 2_000_000_000, Set: true}},
)
pricedEvent := telemetry.RequestEvent{
UpstreamModel: rawInvalidModel,
@@ -197,26 +216,28 @@ func TestMapEventBoundsModelsAtUTF8Boundary(t *testing.T) {
Tokens: usage.Tokens{Output: 1_000_000},
}},
}
pricedRow := mapEvent(redact.New(), pricedEvent, rawPriceTable)
pricedEvent.CompletedAt = time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
pricedRow := mustMapEvent(t, redact.New(), pricedEvent, rawPriceTable)
if pricedRow.UpstreamModel == rawInvalidModel || !utf8.ValidString(pricedRow.UpstreamModel) {
t.Fatalf("priced row upstream model was not projected safely: %q", pricedRow.UpstreamModel)
}
if pricedRow.CostState != string(pricing.CostStatePriced) || pricedRow.Cost != 2 {
if pricedRow.CostState != string(pricing.CostStatePriced) ||
pricedRow.EstimatedCostNanoUSD != 2_000_000_000 {
t.Fatalf(
"raw telemetry model quote = %q/%v, want priced/2",
"raw telemetry model quote = %q/%d, want priced/2000000000",
pricedRow.CostState,
pricedRow.Cost,
pricedRow.EstimatedCostNanoUSD,
)
}
}
func TestMapEventPersistsUsageAttributionAndQuote(t *testing.T) {
table := compileRequestLogTestPriceTable(t, "actual-upstream", pricing.Prices{
UncachedInput: pricing.Price{Value: 1, Set: true},
CacheRead: pricing.Price{Value: 2, Set: true},
CacheWrite5M: pricing.Price{Value: 3, Set: true},
CacheWrite1H: pricing.Price{Value: 4, Set: true},
Output: pricing.Price{Value: 5, Set: true},
UncachedInput: pricing.Price{NanoUSDPerMillion: 1_000_000_000, Set: true},
CacheRead: pricing.Price{NanoUSDPerMillion: 2_000_000_000, Set: true},
CacheWrite5M: pricing.Price{NanoUSDPerMillion: 3_000_000_000, Set: true},
CacheWrite1H: pricing.Price{NanoUSDPerMillion: 4_000_000_000, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 5_000_000_000, Set: true},
})
t.Run("complete usage uses final attribution and all five token prices", func(t *testing.T) {
@@ -238,19 +259,25 @@ func TestMapEventPersistsUsageAttributionAndQuote(t *testing.T) {
},
}
row := mapEvent(redact.New(), event, table)
row := mustMapEvent(t, redact.New(), event, table)
if row.GroupID != 23 || row.UpstreamModel != "actual-upstream" {
t.Fatalf("attribution = group:%d model:%q, want group:23 model:actual-upstream", row.GroupID, row.UpstreamModel)
}
if row.InputTokens != 1_000_000 || row.CacheReadTokens != 2_000_000 ||
if row.UncachedInputTokens != 1_000_000 || row.CacheReadTokens != 2_000_000 ||
row.CacheWrite5MTokens != 3_000_000 || row.CacheWrite1HTokens != 4_000_000 ||
row.OutputTokens != 5_000_000 {
t.Fatalf("persisted tokens = %+v", row)
}
if row.UsageState != string(usage.StateComplete) ||
row.CostState != string(pricing.CostStatePriced) || row.Cost != 55 {
t.Fatalf("usage/cost = %q/%q/%v, want complete/priced/55", row.UsageState, row.CostState, row.Cost)
row.CostState != string(pricing.CostStatePriced) ||
row.EstimatedCostNanoUSD != 55_000_000_000 {
t.Fatalf(
"usage/cost = %q/%q/%d, want complete/priced/55000000000",
row.UsageState,
row.CostState,
row.EstimatedCostNanoUSD,
)
}
})
@@ -271,16 +298,22 @@ func TestMapEventPersistsUsageAttributionAndQuote(t *testing.T) {
},
}
row := mapEvent(redact.New(), event, table)
row := mustMapEvent(t, redact.New(), event, table)
if row.InputTokens != 6 || row.CacheReadTokens != 7 ||
if row.UncachedInputTokens != 6 || row.CacheReadTokens != 7 ||
row.CacheWrite5MTokens != 8 || row.CacheWrite1HTokens != 9 ||
row.OutputTokens != 10 {
t.Fatalf("missing tokens were rewritten: %+v", row)
}
if row.UsageState != string(usage.StateMissing) ||
row.CostState != string(pricing.CostStateUnpriced) || row.Cost != 0 {
t.Fatalf("missing usage/cost = %q/%q/%v", row.UsageState, row.CostState, row.Cost)
row.CostState != string(pricing.CostStateUnpriced) ||
row.EstimatedCostNanoUSD != 0 {
t.Fatalf(
"missing usage/cost = %q/%q/%d",
row.UsageState,
row.CostState,
row.EstimatedCostNanoUSD,
)
}
})
@@ -300,15 +333,21 @@ func TestMapEventPersistsUsageAttributionAndQuote(t *testing.T) {
},
}
row := mapEvent(redact.New(), event, table)
row := mustMapEvent(t, redact.New(), event, table)
if row.GroupID != 25 || row.UpstreamModel != "actual-upstream" ||
row.InputTokens != 11 || row.OutputTokens != 12 {
row.UncachedInputTokens != 11 || row.OutputTokens != 12 {
t.Fatalf("not-applicable attribution/tokens = %+v", row)
}
if row.UsageState != string(usage.StateNotApplicable) ||
row.CostState != string(pricing.CostStateNotApplicable) || row.Cost != 0 {
t.Fatalf("not-applicable usage/cost = %q/%q/%v", row.UsageState, row.CostState, row.Cost)
row.CostState != string(pricing.CostStateNotApplicable) ||
row.EstimatedCostNanoUSD != 0 {
t.Fatalf(
"not-applicable usage/cost = %q/%q/%d",
row.UsageState,
row.CostState,
row.EstimatedCostNanoUSD,
)
}
})
}
@@ -337,10 +376,10 @@ func TestMapEventHandlesNilPriceTableFailOpen(t *testing.T) {
},
}
row := mapEvent(redact.New(), event, nil)
row := mustMapEvent(t, redact.New(), event, nil)
if row.UsageState != string(test.usage) || row.CostState != string(test.wantState) ||
row.Cost != 0 || row.InputTokens != 13 || row.CacheReadTokens != 14 ||
row.EstimatedCostNanoUSD != 0 || row.UncachedInputTokens != 13 || row.CacheReadTokens != 14 ||
row.CacheWrite5MTokens != 15 || row.CacheWrite1HTokens != 16 ||
row.OutputTokens != 17 {
t.Fatalf("nil-table mapping = %+v", row)
+53
View File
@@ -0,0 +1,53 @@
package requestlog
import (
"testing"
"time"
"gpt-load/internal/platform/redact"
"gpt-load/internal/pricing"
"gpt-load/internal/telemetry"
"gpt-load/internal/usage"
)
func TestMapEventV3PersistsMillisecondLedgerAndQuotesUpstreamModel(t *testing.T) {
prices := compileRequestLogTestPriceTable(t, "provider-model", pricing.Prices{
Output: pricing.Price{
NanoUSDPerMillion: pricing.NanoUSD(2_000_000_000),
Set: true,
},
})
event := telemetry.RequestEvent{
RequestID: "00000000-0000-4000-8000-000000004001",
CompletedAt: time.Date(2026, time.July, 24, 12, 34, 56, 789_000_000, time.UTC),
AccessKeyID: 41,
ClientModel: "client-alias",
UpstreamModel: "provider-model",
Status: telemetry.RequestStatusSuccess,
StatusCode: 200,
Usage: telemetry.UsageObservation{
GroupID: 73,
Result: usage.Result{
State: usage.StateComplete,
Tokens: usage.Tokens{Output: 1_000_000},
},
},
}
row := mustMapEvent(t, redact.New(), event, prices)
if row.CompletedAtMS != 1_784_896_496_789 {
t.Fatalf("CompletedAtMS = %d, want 1784896496789", row.CompletedAtMS)
}
if row.ClientModel != "client-alias" || row.UpstreamModel != "provider-model" {
t.Fatalf("projected models = %q/%q", row.ClientModel, row.UpstreamModel)
}
if row.EstimatedCostNanoUSD != 2_000_000_000 ||
row.CostState != string(pricing.CostStatePriced) {
t.Fatalf(
"quote = %d/%q, want 2000000000/priced",
row.EstimatedCostNanoUSD,
row.CostState,
)
}
}
+7 -10
View File
@@ -2,7 +2,6 @@ package requestlog
import (
"net/http"
"strconv"
"github.com/sirupsen/logrus"
@@ -24,7 +23,10 @@ func projectProcessLog(
return logrus.InfoLevel, nil, false
}
row := mapEvent(redactor, event, prices)
row, err := mapEvent(redactor, event, prices)
if err != nil {
return logrus.InfoLevel, nil, false
}
groupID, keyID := attributedAttempt(event)
level := logrus.WarnLevel
@@ -59,8 +61,8 @@ func projectProcessLog(
if attemptCount := len(event.Attempts); attemptCount != 1 {
fields["attempt_count"] = attemptCount
}
if row.InputTokens > 0 {
fields["uncached_input_tokens"] = row.InputTokens
if row.UncachedInputTokens > 0 {
fields["uncached_input_tokens"] = row.UncachedInputTokens
}
if row.CacheReadTokens > 0 {
fields["cache_read_tokens"] = row.CacheReadTokens
@@ -86,12 +88,7 @@ func projectProcessLog(
fields["cost_state"] = row.CostState
}
if row.CostState == string(pricing.CostStatePriced) {
fields["estimated_cost_usd"] = strconv.FormatFloat(
row.Cost,
'g',
12,
64,
)
fields["estimated_cost_nano_usd"] = row.EstimatedCostNanoUSD
}
if event.Status != telemetry.RequestStatusSuccess {
if row.ErrorCode != "" {
+49 -50
View File
@@ -50,11 +50,11 @@ func processLogEntries(t *testing.T, output []byte) []map[string]any {
func TestProjectProcessLogUsesSparseFieldsAndAggregatesCacheWrite(t *testing.T) {
table := compileRequestLogTestPriceTable(t, "actual-model", pricing.Prices{
UncachedInput: pricing.Price{Value: 1, Set: true},
CacheRead: pricing.Price{Value: 2, Set: true},
CacheWrite5M: pricing.Price{Value: 3, Set: true},
CacheWrite1H: pricing.Price{Value: 4, Set: true},
Output: pricing.Price{Value: 5, Set: true},
UncachedInput: pricing.Price{NanoUSDPerMillion: 1_000_000_000, Set: true},
CacheRead: pricing.Price{NanoUSDPerMillion: 2_000_000_000, Set: true},
CacheWrite5M: pricing.Price{NanoUSDPerMillion: 3_000_000_000, Set: true},
CacheWrite1H: pricing.Price{NanoUSDPerMillion: 4_000_000_000, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 5_000_000_000, Set: true},
})
event := testEvent("00000000-0000-4000-8000-000000000501")
event.Protocol = protocol.OpenAICompletions
@@ -91,25 +91,25 @@ func TestProjectProcessLogUsesSparseFieldsAndAggregatesCacheWrite(t *testing.T)
t.Fatalf("projection = %t/%s, want true/warning", ok, level)
}
want := logrus.Fields{
"event": "data_plane_request_completed",
"request_id": event.RequestID,
"status": "error",
"status_code": http.StatusBadGateway,
"protocol": string(protocol.OpenAICompletions),
"access_key_id": uint(42),
"client_model": "client-model",
"upstream_model": "actual-model",
"group_id": uint(9),
"key_id": uint(10),
"duration_ms": int64(25),
"attempt_count": 2,
"uncached_input_tokens": int64(1),
"cache_read_tokens": int64(2),
"cache_write_tokens": int64(7),
"output_tokens": int64(5),
"estimated_cost_usd": "5.5e-05",
"error_code": "upstream_error",
"error_summary": "provider failed",
"event": "data_plane_request_completed",
"request_id": event.RequestID,
"status": "error",
"status_code": http.StatusBadGateway,
"protocol": string(protocol.OpenAICompletions),
"access_key_id": uint(42),
"client_model": "client-model",
"upstream_model": "actual-model",
"group_id": uint(9),
"key_id": uint(10),
"duration_ms": int64(25),
"attempt_count": 2,
"uncached_input_tokens": int64(1),
"cache_read_tokens": int64(2),
"cache_write_tokens": int64(7),
"output_tokens": int64(5),
"estimated_cost_nano_usd": int64(55_000),
"error_code": "upstream_error",
"error_summary": "provider failed",
}
assertProcessFields(t, fields, want)
}
@@ -346,10 +346,10 @@ func TestProjectProcessLogOmitsOverflowedCacheWriteAggregate(t *testing.T) {
if value, exists := fields["cost_state"]; exists {
t.Fatalf("cost_state = %#v, want omitted for priced usage", value)
}
if fields["estimated_cost_usd"] != "0" {
if fields["estimated_cost_nano_usd"] != int64(0) {
t.Fatalf(
"estimated_cost_usd = %#v, want 0",
fields["estimated_cost_usd"],
"estimated_cost_nano_usd = %#v, want 0",
fields["estimated_cost_nano_usd"],
)
}
})
@@ -375,8 +375,8 @@ func TestProjectProcessLogOmitsOverflowedCacheWriteAggregate(t *testing.T) {
if fields["cost_state"] != "unpriced" {
t.Fatalf("cost_state = %#v, want unpriced", fields["cost_state"])
}
if value, exists := fields["estimated_cost_usd"]; exists {
t.Fatalf("estimated_cost_usd = %#v, want omitted", value)
if value, exists := fields["estimated_cost_nano_usd"]; exists {
t.Fatalf("estimated_cost_nano_usd = %#v, want omitted", value)
}
})
}
@@ -423,7 +423,7 @@ func TestProjectProcessLogUsesConditionalModelAttemptAndStateFields(t *testing.T
func TestProjectProcessLogDoesNotRoundTinyCostToZero(t *testing.T) {
table := compileRequestLogTestPriceTable(t, "tiny", pricing.Prices{
Output: pricing.Price{Value: 0.001, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 1_000_000, Set: true},
})
event := testEvent("tiny-cost")
event.UpstreamModel = "tiny"
@@ -436,9 +436,8 @@ func TestProjectProcessLogDoesNotRoundTinyCostToZero(t *testing.T) {
if !ok {
t.Fatal("projection skipped")
}
if fields["estimated_cost_usd"] == "" ||
fields["estimated_cost_usd"] == "0" {
t.Fatalf("tiny cost = %#v", fields["estimated_cost_usd"])
if fields["estimated_cost_nano_usd"] != int64(1) {
t.Fatalf("tiny cost = %#v, want 1 nano USD", fields["estimated_cost_nano_usd"])
}
}
@@ -577,11 +576,11 @@ func TestProcessLogFormatterSecretMatrix(t *testing.T) {
func TestProjectProcessLogMatchesDurableProjection(t *testing.T) {
table := compileRequestLogTestPriceTable(t, "consistent", pricing.Prices{
UncachedInput: pricing.Price{Value: 2, Set: true},
CacheRead: pricing.Price{Value: 3, Set: true},
CacheWrite5M: pricing.Price{Value: 4, Set: true},
CacheWrite1H: pricing.Price{Value: 5, Set: true},
Output: pricing.Price{Value: 6, Set: true},
UncachedInput: pricing.Price{NanoUSDPerMillion: 2_000_000_000, Set: true},
CacheRead: pricing.Price{NanoUSDPerMillion: 3_000_000_000, Set: true},
CacheWrite5M: pricing.Price{NanoUSDPerMillion: 4_000_000_000, Set: true},
CacheWrite1H: pricing.Price{NanoUSDPerMillion: 5_000_000_000, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 6_000_000_000, Set: true},
})
event := testEvent("projection-consistency")
event.Protocol = protocol.Anthropic
@@ -601,23 +600,23 @@ func TestProjectProcessLogMatchesDurableProjection(t *testing.T) {
},
}
redactor := redact.New()
row := mapEvent(redactor, event, table)
row := mustMapEvent(t, redactor, event, table)
_, fields, ok := projectProcessLog(redactor, event, table)
if !ok {
t.Fatal("projection skipped")
}
want := map[string]any{
"request_id": row.ID,
"access_key_id": row.AccessKeyID,
"client_model": row.ClientModel,
"upstream_model": row.UpstreamModel,
"duration_ms": row.DurationMs,
"uncached_input_tokens": row.InputTokens,
"cache_read_tokens": row.CacheReadTokens,
"cache_write_tokens": int64(27),
"output_tokens": row.OutputTokens,
"estimated_cost_usd": "0.00027",
"request_id": row.ID,
"access_key_id": row.AccessKeyID,
"client_model": row.ClientModel,
"upstream_model": row.UpstreamModel,
"duration_ms": row.DurationMs,
"uncached_input_tokens": row.UncachedInputTokens,
"cache_read_tokens": row.CacheReadTokens,
"cache_write_tokens": int64(27),
"output_tokens": row.OutputTokens,
"estimated_cost_nano_usd": row.EstimatedCostNanoUSD,
}
for field, wantValue := range want {
if got := fields[field]; !reflect.DeepEqual(got, wantValue) {
@@ -645,7 +644,7 @@ func TestProcessAndDurableProjectionsRedactSensitiveIdentityFieldsConsistently(
}
redactor := redact.New()
row := mapEvent(redactor, event, nil)
row := mustMapEvent(t, redactor, event, nil)
var attempts []Attempt
if err := json.Unmarshal(row.Attempts, &attempts); err != nil {
t.Fatalf("unmarshal durable attempts: %v", err)
+34 -34
View File
@@ -22,14 +22,14 @@ func (service *Service) List(ctx context.Context, input ListQuery) (Page, error)
query := service.db.WithContext(ctx).
Model(&models.RequestLog{}).
Order("created_at DESC").
Order("completed_at_ms DESC").
Order("id DESC").
Limit(limit + 1)
if input.From != nil {
query = query.Where("created_at >= ?", input.From.UTC())
if input.FromMS != nil {
query = query.Where("completed_at_ms >= ?", *input.FromMS)
}
if input.To != nil {
query = query.Where("created_at < ?", input.To.UTC())
if input.ToMS != nil {
query = query.Where("completed_at_ms < ?", *input.ToMS)
}
if input.GroupID != nil {
query = query.Where(`
@@ -55,9 +55,9 @@ func (service *Service) List(ctx context.Context, input ListQuery) (Page, error)
}
if input.Cursor != nil {
query = query.Where(
"created_at < ? OR (created_at = ? AND id < ?)",
input.Cursor.CompletedAt.UTC(),
input.Cursor.CompletedAt.UTC(),
"completed_at_ms < ? OR (completed_at_ms = ? AND id < ?)",
input.Cursor.CompletedAtMS,
input.Cursor.CompletedAtMS,
input.Cursor.RequestID,
)
}
@@ -83,8 +83,8 @@ func (service *Service) List(ctx context.Context, input ListQuery) (Page, error)
if hasNext {
last := records[len(records)-1]
page.NextCursor = &Cursor{
CompletedAt: last.CompletedAt,
RequestID: last.RequestID,
CompletedAtMS: last.CompletedAtMS,
RequestID: last.RequestID,
}
}
return page, nil
@@ -106,28 +106,28 @@ func decodeRequestLogRows(rows []models.RequestLog) ([]Record, error) {
return nil, err
}
records = append(records, Record{
RequestID: row.ID,
CompletedAt: row.CreatedAt.UTC(),
AccessKey: AccessKeyRef{ID: row.AccessKeyID, Deleted: true},
Protocol: protocol.Protocol(row.Protocol),
ClientModel: row.ClientModel,
UpstreamModel: row.UpstreamModel,
Status: telemetry.RequestStatus(row.Status),
StatusCode: row.StatusCode,
DurationMs: row.DurationMs,
ErrorCode: row.ErrorCode,
ErrorSummary: row.ErrorSummary,
AffinityHit: row.AffinityHit,
Attempts: attempts,
GroupID: row.GroupID,
UsageState: usage.State(row.UsageState),
CostState: pricing.CostState(row.CostState),
UncachedInputTokens: row.InputTokens,
CacheReadTokens: row.CacheReadTokens,
CacheWrite5MTokens: row.CacheWrite5MTokens,
CacheWrite1HTokens: row.CacheWrite1HTokens,
OutputTokens: row.OutputTokens,
EstimatedCostUSD: row.Cost,
RequestID: row.ID,
CompletedAtMS: row.CompletedAtMS,
AccessKey: AccessKeyRef{ID: row.AccessKeyID, Deleted: true},
Protocol: protocol.Protocol(row.Protocol),
ClientModel: row.ClientModel,
UpstreamModel: row.UpstreamModel,
Status: telemetry.RequestStatus(row.Status),
StatusCode: row.StatusCode,
DurationMs: row.DurationMs,
ErrorCode: row.ErrorCode,
ErrorSummary: row.ErrorSummary,
AffinityHit: row.AffinityHit,
Attempts: attempts,
GroupID: row.GroupID,
UsageState: usage.State(row.UsageState),
CostState: pricing.CostState(row.CostState),
UncachedInputTokens: row.UncachedInputTokens,
CacheReadTokens: row.CacheReadTokens,
CacheWrite5MTokens: row.CacheWrite5MTokens,
CacheWrite1HTokens: row.CacheWrite1HTokens,
OutputTokens: row.OutputTokens,
EstimatedCostNanoUSD: row.EstimatedCostNanoUSD,
})
}
return records, nil
@@ -137,12 +137,12 @@ func validateRequestLogUsageCost(row models.RequestLog) error {
if err := ValidateUsageCostState(
usage.State(row.UsageState),
pricing.CostState(row.CostState),
row.Cost,
row.EstimatedCostNanoUSD,
); err != nil {
return fmt.Errorf("decode request log usage/cost: %w", err)
}
for _, value := range []int64{
row.InputTokens,
row.UncachedInputTokens,
row.CacheReadTokens,
row.CacheWrite5MTokens,
row.CacheWrite1HTokens,
+63 -62
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"math"
"reflect"
"testing"
"time"
@@ -21,27 +20,26 @@ import (
)
func TestDecodeRequestLogRowsPreservesUsageCostAttribution(t *testing.T) {
completedAt := time.Date(2026, time.July, 27, 1, 2, 3, 4, time.FixedZone("UTC+8", 8*60*60))
rows := []models.RequestLog{{
ID: "00000000-0000-4000-8000-000000000601",
CreatedAt: completedAt,
AccessKeyID: 61,
GroupID: 17,
Protocol: string(protocol.OpenAICompletions),
ClientModel: "client-model",
UpstreamModel: "upstream-model",
Status: string(telemetry.RequestStatusSuccess),
StatusCode: 200,
DurationMs: 25,
InputTokens: 11,
CacheReadTokens: 12,
CacheWrite5MTokens: 13,
CacheWrite1HTokens: 14,
OutputTokens: 15,
Cost: 0.1234567890123,
UsageState: string(usage.StateComplete),
CostState: string(pricing.CostStatePriced),
Attempts: models.JSON(`[]`),
ID: "00000000-0000-4000-8000-000000000601",
CompletedAtMS: 1_785_085_323_000,
AccessKeyID: 61,
GroupID: 17,
Protocol: string(protocol.OpenAICompletions),
ClientModel: "client-model",
UpstreamModel: "upstream-model",
Status: string(telemetry.RequestStatusSuccess),
StatusCode: 200,
DurationMs: 25,
UncachedInputTokens: 11,
CacheReadTokens: 12,
CacheWrite5MTokens: 13,
CacheWrite1HTokens: 14,
OutputTokens: 15,
EstimatedCostNanoUSD: 123_456_789,
UsageState: string(usage.StateComplete),
CostState: string(pricing.CostStatePriced),
Attempts: models.JSON(`[]`),
}}
records, err := decodeRequestLogRows(rows)
@@ -56,20 +54,20 @@ func TestDecodeRequestLogRowsPreservesUsageCostAttribution(t *testing.T) {
got.CostState != pricing.CostStatePriced || got.UncachedInputTokens != 11 ||
got.CacheReadTokens != 12 || got.CacheWrite5MTokens != 13 ||
got.CacheWrite1HTokens != 14 || got.OutputTokens != 15 ||
got.EstimatedCostUSD != 0.1234567890123 ||
got.CompletedAt.Format(time.RFC3339Nano) != "2026-07-26T17:02:03.000000004Z" {
got.EstimatedCostNanoUSD != 123_456_789 ||
got.CompletedAtMS != 1_785_085_323_000 {
t.Fatalf("decoded usage/cost record = %#v", got)
}
}
func TestDecodeRequestLogRowsIgnoresHistoricalKeyMask(t *testing.T) {
rows := []models.RequestLog{{
ID: "00000000-0000-4000-8000-000000000605",
CreatedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
Protocol: string(protocol.OpenAICompletions),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(usage.StateNotApplicable),
CostState: string(pricing.CostStateNotApplicable),
ID: "00000000-0000-4000-8000-000000000605",
CompletedAtMS: 1_785_110_400_000,
Protocol: string(protocol.OpenAICompletions),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(usage.StateNotApplicable),
CostState: string(pricing.CostStateNotApplicable),
Attempts: models.JSON(
`[{"sequence":1,"group_id":7,"group_name":"Primary","key_id":11,"key_mask":"prov****safe","upstream_model":"model","status_code":200,"duration_ms":10,"failure_category":"ok","action":"terminate","will_retry":false,"error_code":"","error_summary":"","committed":true}]`,
),
@@ -95,13 +93,13 @@ func TestDecodeRequestLogRowsIgnoresHistoricalKeyMask(t *testing.T) {
func TestDecodeRequestLogRowsRejectsInvalidUsageCostValues(t *testing.T) {
base := models.RequestLog{
ID: "00000000-0000-4000-8000-000000000602",
CreatedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
Protocol: string(protocol.OpenAICompletions),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(usage.StateComplete),
CostState: string(pricing.CostStatePriced),
Attempts: models.JSON(`[]`),
ID: "00000000-0000-4000-8000-000000000602",
CompletedAtMS: 1_785_110_400_000,
Protocol: string(protocol.OpenAICompletions),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(usage.StateComplete),
CostState: string(pricing.CostStatePriced),
Attempts: models.JSON(`[]`),
}
tests := []struct {
name string
@@ -109,10 +107,8 @@ func TestDecodeRequestLogRowsRejectsInvalidUsageCostValues(t *testing.T) {
}{
{name: "usage state", mutate: func(row *models.RequestLog) { row.UsageState = "invalid" }},
{name: "cost state", mutate: func(row *models.RequestLog) { row.CostState = "invalid" }},
{name: "negative token", mutate: func(row *models.RequestLog) { row.InputTokens = -1 }},
{name: "negative cost", mutate: func(row *models.RequestLog) { row.Cost = -0.01 }},
{name: "infinite cost", mutate: func(row *models.RequestLog) { row.Cost = math.Inf(1) }},
{name: "not a number cost", mutate: func(row *models.RequestLog) { row.Cost = math.NaN() }},
{name: "negative token", mutate: func(row *models.RequestLog) { row.UncachedInputTokens = -1 }},
{name: "negative cost", mutate: func(row *models.RequestLog) { row.EstimatedCostNanoUSD = -1 }},
{
name: "missing usage cannot be priced",
mutate: func(row *models.RequestLog) {
@@ -137,7 +133,7 @@ func TestDecodeRequestLogRowsRejectsInvalidUsageCostValues(t *testing.T) {
name: "unpriced cost must be zero",
mutate: func(row *models.RequestLog) {
row.CostState = string(pricing.CostStateUnpriced)
row.Cost = 0.01
row.EstimatedCostNanoUSD = 1
},
},
{
@@ -145,7 +141,7 @@ func TestDecodeRequestLogRowsRejectsInvalidUsageCostValues(t *testing.T) {
mutate: func(row *models.RequestLog) {
row.UsageState = string(usage.StateNotApplicable)
row.CostState = string(pricing.CostStateNotApplicable)
row.Cost = 0.01
row.EstimatedCostNanoUSD = 1
},
},
}
@@ -165,11 +161,11 @@ func TestDecodeRequestLogRowsAcceptsApprovedUsageCostMatrix(t *testing.T) {
name string
usageState usage.State
costState pricing.CostState
cost float64
cost int64
}{
{name: "complete priced", usageState: usage.StateComplete, costState: pricing.CostStatePriced, cost: 0.01},
{name: "complete priced", usageState: usage.StateComplete, costState: pricing.CostStatePriced, cost: 10_000_000},
{name: "complete unpriced", usageState: usage.StateComplete, costState: pricing.CostStateUnpriced},
{name: "partial priced", usageState: usage.StatePartial, costState: pricing.CostStatePriced, cost: 0.01},
{name: "partial priced", usageState: usage.StatePartial, costState: pricing.CostStatePriced, cost: 10_000_000},
{name: "partial unpriced", usageState: usage.StatePartial, costState: pricing.CostStateUnpriced},
{name: "missing unpriced", usageState: usage.StateMissing, costState: pricing.CostStateUnpriced},
{
@@ -181,21 +177,22 @@ func TestDecodeRequestLogRowsAcceptsApprovedUsageCostMatrix(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
row := models.RequestLog{
ID: "00000000-0000-4000-8000-000000000604",
CreatedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
Protocol: string(protocol.OpenAICompletions),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(test.usageState),
CostState: string(test.costState),
Cost: test.cost,
Attempts: models.JSON(`[]`),
ID: "00000000-0000-4000-8000-000000000604",
CompletedAtMS: 1_785_110_400_000,
Protocol: string(protocol.OpenAICompletions),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(test.usageState),
CostState: string(test.costState),
EstimatedCostNanoUSD: test.cost,
Attempts: models.JSON(`[]`),
}
records, err := decodeRequestLogRows([]models.RequestLog{row})
if err != nil {
t.Fatalf("decodeRequestLogRows() error = %v", err)
}
if len(records) != 1 || records[0].UsageState != test.usageState ||
records[0].CostState != test.costState || records[0].EstimatedCostUSD != test.cost {
records[0].CostState != test.costState ||
records[0].EstimatedCostNanoUSD != test.cost {
t.Fatalf("records = %#v", records)
}
})
@@ -206,7 +203,7 @@ func TestServiceListUsesStableKeysetCursor(t *testing.T) {
db := openRequestLogQueryDB(t)
service := newRequestLogTestService(db)
completedAt := time.Date(2026, time.July, 24, 12, 0, 0, 123, time.UTC)
older := completedAt.Add(-time.Nanosecond)
older := completedAt.Add(-time.Millisecond)
for _, row := range []models.RequestLog{
requestLogQueryRow("00000000-0000-4000-8000-000000000100", older, 41, "older", nil),
requestLogQueryRow("00000000-0000-4000-8000-000000000101", completedAt, 41, "same-time", nil),
@@ -227,7 +224,7 @@ func TestServiceListUsesStableKeysetCursor(t *testing.T) {
t.Fatalf("first page IDs = %v, want %v", got, want)
}
if first.NextCursor == nil ||
!first.NextCursor.CompletedAt.Equal(completedAt) ||
first.NextCursor.CompletedAtMS != 1_784_894_400_000 ||
first.NextCursor.RequestID != "00000000-0000-4000-8000-000000000102" {
t.Fatalf("first NextCursor = %#v", first.NextCursor)
}
@@ -245,7 +242,7 @@ func TestServiceListUsesStableKeysetCursor(t *testing.T) {
}; !reflect.DeepEqual(got, want) {
t.Fatalf("second page IDs = %v, want %v", got, want)
}
if second.Items[0].CompletedAt.Equal(second.Items[1].CompletedAt) {
if second.Items[0].CompletedAtMS == second.Items[1].CompletedAtMS {
t.Fatalf("second page did not advance from tied timestamp: %+v", second.Items)
}
if second.NextCursor != nil {
@@ -280,7 +277,7 @@ func TestServiceListAppliesAllFiltersAndGroupJSON(t *testing.T) {
beforeFrom := requestLogQueryRow(
"00000000-0000-4000-8000-000000000203",
from.Add(-time.Nanosecond),
from.Add(-time.Millisecond),
71,
"client-model",
integerAttempts,
@@ -353,9 +350,11 @@ func TestServiceListAppliesAllFiltersAndGroupJSON(t *testing.T) {
groupID := uint(12)
accessKeyID := uint(71)
fromMS := from.UnixMilli()
toMS := to.UnixMilli()
page, err := service.List(context.Background(), ListQuery{
From: &from,
To: &to,
FromMS: &fromMS,
ToMS: &toMS,
GroupID: &groupID,
ClientModel: "client-model",
AccessKeyID: &accessKeyID,
@@ -408,7 +407,7 @@ func TestServiceListGroupFilterUsesAnyAttemptWhileAttributionUsesFinalGroup(t *t
State: usage.StateNotApplicable,
},
}
row := mapEvent(redact.New(), event, (*pricing.Table)(nil))
row := mustMapEvent(t, redact.New(), event, (*pricing.Table)(nil))
if row.GroupID != 13 {
t.Fatalf("top-level GroupID = %d, want final attribution 13", row.GroupID)
}
@@ -568,7 +567,7 @@ func requestLogQueryRow(
}
return models.RequestLog{
ID: id,
CreatedAt: completedAt,
CompletedAtMS: completedAt.UTC().UnixMilli(),
AccessKeyID: accessKeyID,
Protocol: string(protocol.OpenAICompletions),
ClientModel: clientModel,
@@ -576,6 +575,8 @@ func requestLogQueryRow(
Status: string(telemetry.RequestStatusSuccess),
StatusCode: 200,
DurationMs: 25,
UsageState: string(usage.StateNotApplicable),
CostState: string(pricing.CostStateNotApplicable),
Attempts: models.JSON(encodedAttempts),
}
}
+106 -12
View File
@@ -2,8 +2,10 @@ package requestlog
import (
"context"
"fmt"
"time"
"gpt-load/internal/platform/epochms"
"gpt-load/internal/storage/models"
)
@@ -11,11 +13,14 @@ type RetentionPolicyProvider interface {
RequestLogRetentionDays() int
}
const retentionBatchSize = 1000
const (
retentionBatchSize = 1000
usageStatRetentionDays = 35
)
// Sweep removes request logs strictly older than the configured retention
// boundary. Configuration and database failures are intentionally isolated
// from the data-plane request path.
// Sweep removes request logs and hourly usage aggregates strictly older than
// their respective retention boundaries. Failures are isolated from the data
// plane.
func (service *Service) Sweep(ctx context.Context, now time.Time) {
if ctx == nil {
ctx = context.Background()
@@ -25,17 +30,63 @@ func (service *Service) Sweep(ctx context.Context, now time.Time) {
}
days := service.retentionPolicy.RequestLogRetentionDays()
cutoff := now.UTC().Add(-time.Duration(days) * 24 * time.Hour)
nowMS, err := epochms.FromTime(now)
if err != nil {
service.recordRetentionDeleteFailure(now)
return
}
requestLogCutoffMS, err := retentionCutoffMS(nowMS, days)
if err != nil {
service.recordRetentionDeleteFailure(now)
return
}
usageStatCutoffMS, err := retentionCutoffMS(nowMS, usageStatRetentionDays)
if err != nil {
service.recordRetentionDeleteFailure(now)
return
}
usageStatCutoffMS, err = epochms.AlignDown(
usageStatCutoffMS,
epochms.MillisecondsPerHour,
)
if err != nil {
service.recordRetentionDeleteFailure(now)
return
}
service.deleteExpiredRequestLogs(ctx, requestLogCutoffMS, now)
if ctx.Err() != nil {
return
}
service.deleteExpiredUsageStats(ctx, usageStatCutoffMS, now)
}
func retentionCutoffMS(nowMS int64, days int) (int64, error) {
if days < 0 {
return 0, fmt.Errorf("retention days must be non-negative")
}
retentionMS := int64(days) * epochms.MillisecondsPerDay
if retentionMS > nowMS {
return 0, nil
}
return nowMS - retentionMS, nil
}
func (service *Service) deleteExpiredRequestLogs(
ctx context.Context,
cutoffMS int64,
now time.Time,
) bool {
for {
if ctx.Err() != nil {
return
return false
}
var ids []string
result := service.db.WithContext(ctx).
Model(&models.RequestLog{}).
Where("created_at < ?", cutoff).
Order("created_at ASC").
Where("completed_at_ms < ?", cutoffMS).
Order("completed_at_ms ASC").
Order("id ASC").
Limit(retentionBatchSize).
Pluck("id", &ids)
@@ -43,10 +94,10 @@ func (service *Service) Sweep(ctx context.Context, now time.Time) {
if ctx.Err() == nil {
service.recordRetentionDeleteFailure(now)
}
return
return false
}
if len(ids) == 0 {
return
return true
}
result = service.db.WithContext(ctx).
@@ -56,10 +107,53 @@ func (service *Service) Sweep(ctx context.Context, now time.Time) {
if ctx.Err() == nil {
service.recordRetentionDeleteFailure(now)
}
return
return false
}
if len(ids) < retentionBatchSize {
return
return true
}
}
}
func (service *Service) deleteExpiredUsageStats(
ctx context.Context,
cutoffMS int64,
now time.Time,
) bool {
for {
if ctx.Err() != nil {
return false
}
var ids []uint
result := service.db.WithContext(ctx).
Model(&models.UsageStat{}).
Where("bucket_start_ms < ?", cutoffMS).
Order("bucket_start_ms ASC").
Order("id ASC").
Limit(retentionBatchSize).
Pluck("id", &ids)
if result.Error != nil {
if ctx.Err() == nil {
service.recordRetentionDeleteFailure(now)
}
return false
}
if len(ids) == 0 {
return true
}
result = service.db.WithContext(ctx).
Where("id IN ?", ids).
Delete(&models.UsageStat{})
if result.Error != nil {
if ctx.Err() == nil {
service.recordRetentionDeleteFailure(now)
}
return false
}
if len(ids) < retentionBatchSize {
return true
}
}
}
+146 -3
View File
@@ -19,7 +19,7 @@ import (
"gpt-load/internal/usage"
)
func TestServiceSweepUsesSnapshotRetentionPolicy(t *testing.T) {
func TestRetentionSweepUsesSnapshotRetentionPolicy(t *testing.T) {
now := time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
tests := []struct {
name string
@@ -61,7 +61,7 @@ func TestServiceSweepUsesSnapshotRetentionPolicy(t *testing.T) {
}
}
func TestServiceSweepDeletesStrictlyOlderRowsInBatches(t *testing.T) {
func TestRetentionSweepDeletesStrictlyOlderRowsInBatches(t *testing.T) {
db := openRequestLogQueryDB(t)
service := newRequestLogTestService(db)
now := time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
@@ -99,7 +99,90 @@ func TestServiceSweepDeletesStrictlyOlderRowsInBatches(t *testing.T) {
}
}
func TestServiceSweepStopsOnContextAndDeleteFailure(t *testing.T) {
func TestRetentionSweepUsesFixedThirtyFiveDayIntegerUsageBoundary(t *testing.T) {
db := openRequestLogQueryDB(t)
service := NewService(
db,
redact.New(),
staticRetentionPolicy{days: 1},
newStaticPriceTableProvider(),
)
now := time.Date(2026, time.July, 24, 12, 34, 56, 789_000_000, time.UTC)
const usageCutoffMS int64 = 1_781_870_400_000
for index, bucketStartMS := range []int64{
usageCutoffMS - 3_600_000,
usageCutoffMS,
usageCutoffMS + 3_600_000,
} {
if err := db.Create(&models.UsageStat{
BucketStartMS: bucketStartMS,
AccessKeyID: uint(index),
GroupID: 17,
Model: "retention-model",
RequestCount: 1,
SuccessCount: 1,
}).Error; err != nil {
t.Fatalf("create UsageStat at %d: %v", bucketStartMS, err)
}
}
service.Sweep(context.Background(), now)
var remaining []models.UsageStat
if err := db.Order("bucket_start_ms ASC").Find(&remaining).Error; err != nil {
t.Fatalf("query remaining UsageStats: %v", err)
}
if len(remaining) != 2 ||
remaining[0].BucketStartMS != usageCutoffMS ||
remaining[1].BucketStartMS != usageCutoffMS+3_600_000 {
t.Fatalf("remaining UsageStats = %+v, want cutoff and newer integer buckets", remaining)
}
}
func TestRetentionSweepCleansUsageStatsWhenRequestLogDeleteFails(t *testing.T) {
db := openRequestLogQueryDB(t)
service := newRequestLogTestService(db)
now := time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
service.now = func() time.Time { return now }
discardRetentionWarnings(service)
createRetentionRow(t, db, 1, now.Add(-8*24*time.Hour))
if err := db.Create(&models.UsageStat{
BucketStartMS: now.Add(-36 * 24 * time.Hour).UnixMilli(),
AccessKeyID: 1,
GroupID: 1,
Model: "expired",
RequestCount: 1,
SuccessCount: 1,
}).Error; err != nil {
t.Fatalf("create expired UsageStat: %v", err)
}
if err := db.Exec(`CREATE TRIGGER fail_request_log_retention_delete
BEFORE DELETE ON request_logs
BEGIN
SELECT RAISE(FAIL, 'forced request log retention delete failure');
END`).Error; err != nil {
t.Fatalf("create request log delete trigger: %v", err)
}
service.Sweep(context.Background(), now)
assertRetentionRows(t, db, 1)
var usageCount int64
if err := db.Model(&models.UsageStat{}).Count(&usageCount).Error; err != nil {
t.Fatalf("count remaining UsageStats: %v", err)
}
stats := service.Stats()
if usageCount != 0 || stats.RetentionDeleteFailureTotal != 1 ||
!stats.LastRetentionFailureAt.Equal(now) {
t.Fatalf(
"remaining usage/Stats = %d/%#v, want 0 and one request-log failure",
usageCount,
stats,
)
}
}
func TestRetentionSweepStopsOnContextAndDeleteFailure(t *testing.T) {
now := time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
t.Run("empty result is successful completion", func(t *testing.T) {
@@ -223,6 +306,66 @@ func TestServiceSweepStopsOnContextAndDeleteFailure(t *testing.T) {
t.Fatalf("Stats() = %#v, want one delete failure", stats)
}
})
t.Run("usage stat selection failure is tracked", func(t *testing.T) {
db := openRequestLogQueryDB(t)
service := newRequestLogTestService(db)
service.now = func() time.Time { return now }
discardRetentionWarnings(service)
const callbackName = "test:retention_usage_stat_query_failure"
if err := db.Callback().Query().Before("gorm:query").Register(callbackName, func(tx *gorm.DB) {
if tx.Statement.Table == "usage_stats" {
tx.AddError(errors.New("forced usage stat retention query failure"))
}
}); err != nil {
t.Fatalf("register query callback: %v", err)
}
service.Sweep(context.Background(), now)
stats := service.Stats()
if stats.RetentionDeleteFailureTotal != 1 ||
!stats.LastRetentionFailureAt.Equal(now) {
t.Fatalf("Stats() = %#v, want one usage stat selection failure", stats)
}
})
t.Run("usage stat delete failure is tracked", func(t *testing.T) {
db := openRequestLogQueryDB(t)
service := newRequestLogTestService(db)
service.now = func() time.Time { return now }
discardRetentionWarnings(service)
if err := db.Create(&models.UsageStat{
BucketStartMS: now.Add(-36 * 24 * time.Hour).UnixMilli(),
AccessKeyID: 1,
GroupID: 1,
Model: "expired",
RequestCount: 1,
SuccessCount: 1,
}).Error; err != nil {
t.Fatalf("create expired UsageStat: %v", err)
}
const callbackName = "test:retention_usage_stat_delete_failure"
if err := db.Callback().Delete().Before("gorm:delete").Register(callbackName, func(tx *gorm.DB) {
if tx.Statement.Table == "usage_stats" {
tx.AddError(errors.New("forced usage stat retention delete failure"))
}
}); err != nil {
t.Fatalf("register delete callback: %v", err)
}
service.Sweep(context.Background(), now)
var remaining int64
if err := db.Model(&models.UsageStat{}).Count(&remaining).Error; err != nil {
t.Fatalf("count remaining UsageStats: %v", err)
}
stats := service.Stats()
if remaining != 1 || stats.RetentionDeleteFailureTotal != 1 ||
!stats.LastRetentionFailureAt.Equal(now) {
t.Fatalf("remaining/Stats = %d/%#v, want 1 and one delete failure", remaining, stats)
}
})
}
func TestRetentionReplayBoundaryPreservesThenReaggregatesUsageStat(t *testing.T) {
+8 -6
View File
@@ -478,10 +478,10 @@ func TestServiceWarningsExcludeEventContentAndThrottle(t *testing.T) {
func TestEmitFreezesPriceTableBeforeWorkerFlush(t *testing.T) {
tableA := compileRequestLogTestPriceTable(t, "snapshot-model", pricing.Prices{
Output: pricing.Price{Value: 1, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 1_000_000_000, Set: true},
})
tableB := compileRequestLogTestPriceTable(t, "snapshot-model", pricing.Prices{
Output: pricing.Price{Value: 2, Set: true},
Output: pricing.Price{NanoUSDPerMillion: 2_000_000_000, Set: true},
})
provider := &publishingPriceTableProvider{}
provider.Publish(tableA)
@@ -513,7 +513,7 @@ func TestEmitFreezesPriceTableBeforeWorkerFlush(t *testing.T) {
entries := processLogEntries(t, output.Bytes())
if len(entries) != 1 ||
entries[0]["request_id"] != "snapshot-a" ||
entries[0]["estimated_cost_usd"] != "1" {
entries[0]["estimated_cost_nano_usd"] != float64(1_000_000_000) {
t.Fatalf("snapshot-a completion entries = %#v", entries)
}
@@ -528,9 +528,11 @@ func TestEmitFreezesPriceTableBeforeWorkerFlush(t *testing.T) {
timer.Fire()
rows := receiveValue(t, writes)
if len(rows) != 2 || rows[0].ID != "snapshot-a" || rows[0].Cost != 1 ||
rows[1].ID != "snapshot-b" || rows[1].Cost != 2 {
t.Fatalf("snapshot-priced rows = %+v, want A/1 then B/2", rows)
if len(rows) != 2 || rows[0].ID != "snapshot-a" ||
rows[0].EstimatedCostNanoUSD != 1_000_000_000 ||
rows[1].ID != "snapshot-b" ||
rows[1].EstimatedCostNanoUSD != 2_000_000_000 {
t.Fatalf("snapshot-priced rows = %+v, want A/1000000000 then B/2000000000", rows)
}
if got := provider.loads.Load(); got != 3 {
t.Fatalf("PriceTableProvider.Load calls = %d, want Start once plus one per Emit", got)
+30 -16
View File
@@ -234,22 +234,22 @@ func aggregationRow(
model string,
) models.RequestLog {
return models.RequestLog{
ID: id,
CreatedAt: completedAt,
AccessKeyID: 1,
GroupID: groupID,
Protocol: string(protocol.OpenAICompletions),
ClientModel: "client-model",
UpstreamModel: model,
Status: string(telemetry.RequestStatusSuccess),
StatusCode: 200,
DurationMs: 10,
InputTokens: 1,
OutputTokens: 2,
Cost: 0.25,
UsageState: string(usage.StateComplete),
CostState: string(pricing.CostStatePriced),
Attempts: models.JSON(`[]`),
ID: id,
CompletedAtMS: completedAt.UTC().UnixMilli(),
AccessKeyID: 1,
GroupID: groupID,
Protocol: string(protocol.OpenAICompletions),
ClientModel: model,
UpstreamModel: "upstream-" + model,
Status: string(telemetry.RequestStatusSuccess),
StatusCode: 200,
DurationMs: 10,
UncachedInputTokens: 1,
OutputTokens: 2,
EstimatedCostNanoUSD: 250_000_000,
UsageState: string(usage.StateComplete),
CostState: string(pricing.CostStatePriced),
Attempts: models.JSON(`[]`),
}
}
@@ -286,3 +286,17 @@ func testEvent(id string) telemetry.RequestEvent {
},
}
}
func mustMapEvent(
t testing.TB,
redactor *redact.Redactor,
event telemetry.RequestEvent,
prices *pricing.Table,
) models.RequestLog {
t.Helper()
row, err := mapEvent(redactor, event, prices)
if err != nil {
t.Fatalf("mapEvent() error = %v", err)
}
return row
}
+31 -31
View File
@@ -41,13 +41,13 @@ type Attempt struct {
}
type Cursor struct {
CompletedAt time.Time
RequestID string
CompletedAtMS int64
RequestID string
}
type ListQuery struct {
From *time.Time
To *time.Time
FromMS *int64
ToMS *int64
GroupID *uint
ClientModel string
AccessKeyID *uint
@@ -64,28 +64,28 @@ type AccessKeyRef struct {
}
type Record struct {
RequestID string
CompletedAt time.Time
AccessKey AccessKeyRef
Protocol protocol.Protocol
ClientModel string
UpstreamModel string
Status telemetry.RequestStatus
StatusCode int
DurationMs int64
ErrorCode string
ErrorSummary string
AffinityHit bool
Attempts []Attempt
GroupID uint
UsageState usage.State
CostState pricing.CostState
UncachedInputTokens int64
CacheReadTokens int64
CacheWrite5MTokens int64
CacheWrite1HTokens int64
OutputTokens int64
EstimatedCostUSD float64
RequestID string
CompletedAtMS int64
AccessKey AccessKeyRef
Protocol protocol.Protocol
ClientModel string
UpstreamModel string
Status telemetry.RequestStatus
StatusCode int
DurationMs int64
ErrorCode string
ErrorSummary string
AffinityHit bool
Attempts []Attempt
GroupID uint
UsageState usage.State
CostState pricing.CostState
UncachedInputTokens int64
CacheReadTokens int64
CacheWrite5MTokens int64
CacheWrite1HTokens int64
OutputTokens int64
EstimatedCostNanoUSD int64
}
type Page struct {
@@ -108,8 +108,8 @@ const (
)
type UsageQuery struct {
From time.Time
To time.Time
FromMS int64
ToMS int64
Granularity UsageGranularity
GroupID *uint
Model string
@@ -126,15 +126,15 @@ type UsageAggregate struct {
CacheWrite5MTokens int64
CacheWrite1HTokens int64
OutputTokens int64
Cost float64
EstimatedCostNanoUSD int64
UsageMissingCount int64
PartialCount int64
UnpricedRequestCount int64
}
type UsageSeriesPoint struct {
BucketStart time.Time
BucketEnd time.Time
BucketStartMS int64
BucketEndMS int64
UsageAggregate
}
+3 -24
View File
@@ -1,36 +1,15 @@
package requestlog
import (
"fmt"
"math"
"gpt-load/internal/pricing"
"gpt-load/internal/usage"
"gpt-load/internal/usagecost"
)
func ValidateUsageCostState(
usageState usage.State,
costState pricing.CostState,
cost float64,
estimatedCostNanoUSD int64,
) error {
if cost < 0 || math.IsNaN(cost) || math.IsInf(cost, 0) {
return fmt.Errorf("invalid estimated cost")
}
validCombination := false
switch usageState {
case usage.StateComplete, usage.StatePartial:
validCombination = costState == pricing.CostStatePriced ||
costState == pricing.CostStateUnpriced
case usage.StateMissing:
validCombination = costState == pricing.CostStateUnpriced
case usage.StateNotApplicable:
validCombination = costState == pricing.CostStateNotApplicable
}
if !validCombination {
return fmt.Errorf("invalid usage and cost state combination")
}
if costState != pricing.CostStatePriced && cost != 0 {
return fmt.Errorf("unpriced or not-applicable cost must be zero")
}
return nil
return usagecost.Validate(usageState, costState, estimatedCostNanoUSD)
}
+51 -32
View File
@@ -6,11 +6,12 @@ import (
"database/sql/driver"
"errors"
"fmt"
"math"
"time"
"gorm.io/gorm"
"gpt-load/internal/platform/epochms"
"gpt-load/internal/pricing"
"gpt-load/internal/storage/models"
"gpt-load/internal/usage"
)
@@ -33,8 +34,6 @@ func (service *Service) QueryUsage(ctx context.Context, input UsageQuery) (Usage
return UsageReport{}, err
}
input.From = input.From.UTC()
input.To = input.To.UTC()
limit := input.Limit
if limit <= 0 || limit > maxUsageBreakdownLimit {
limit = defaultUsageBreakdownLimit
@@ -127,10 +126,11 @@ func discardUsageReadConnection(connection *sql.Conn) error {
}
func validateUsageQuery(input UsageQuery) error {
if input.From.IsZero() || input.To.IsZero() || !input.From.Before(input.To) {
if input.FromMS < 0 || input.ToMS <= input.FromMS {
return fmt.Errorf("query usage: invalid time range")
}
if input.To.Sub(input.From) > maxUsageSeriesHours*time.Hour {
if input.ToMS-input.FromMS >
int64(maxUsageSeriesHours)*epochms.MillisecondsPerHour {
return fmt.Errorf("query usage: time range exceeds %d hours", maxUsageSeriesHours)
}
switch input.Granularity {
@@ -151,7 +151,7 @@ func validateUsageQuery(input UsageQuery) error {
func usageStatScope(db *gorm.DB, input UsageQuery) *gorm.DB {
scope := db.Session(&gorm.Session{NewDB: true}).Model(&models.UsageStat{}).
Where("hour_bucket >= ? AND hour_bucket < ?", input.From, input.To)
Where("bucket_start_ms >= ? AND bucket_start_ms < ?", input.FromMS, input.ToMS)
if input.GroupID != nil {
scope = scope.Where("group_id = ?", *input.GroupID)
}
@@ -164,6 +164,10 @@ func usageStatScope(db *gorm.DB, input UsageQuery) *gorm.DB {
func validateUsageStatIntegrity(scope *gorm.DB) error {
var integrity usageStatIntegrity
if err := scope.Select(`
COALESCE(MAX(CASE
WHEN bucket_start_ms < 0
OR bucket_start_ms % 3600000 != 0
THEN 1 ELSE 0 END), 0) AS invalid_bucket,
COALESCE(MAX(CASE
WHEN request_count < 0 OR success_count < 0 OR failure_count < 0
OR usage_missing_count < 0 OR partial_count < 0 OR unpriced_request_count < 0
@@ -174,16 +178,17 @@ func validateUsageStatIntegrity(scope *gorm.DB) error {
END = 1
THEN 1 ELSE 0 END), 0) AS invalid_count,
COALESCE(MAX(CASE
WHEN input_tokens < 0 OR output_tokens < 0 OR cache_read_tokens < 0
WHEN uncached_input_tokens < 0 OR output_tokens < 0 OR cache_read_tokens < 0
OR cache_write_5m_tokens < 0 OR cache_write_1h_tokens < 0
THEN 1 ELSE 0 END), 0) AS invalid_token,
COALESCE(MAX(CASE
WHEN cost < 0 OR cost != cost OR cost > ?
WHEN estimated_cost_nano_usd < 0
THEN 1 ELSE 0 END), 0) AS invalid_cost
`, math.MaxFloat64).Find(&integrity).Error; err != nil {
`).Find(&integrity).Error; err != nil {
return fmt.Errorf("check usage stat integrity: %w", err)
}
if integrity.InvalidCount != 0 || integrity.InvalidToken != 0 || integrity.InvalidCost != 0 {
if integrity.InvalidBucket != 0 || integrity.InvalidCount != 0 ||
integrity.InvalidToken != 0 || integrity.InvalidCost != 0 {
return fmt.Errorf("check usage stat integrity: corrupt row")
}
return nil
@@ -202,9 +207,9 @@ func queryUsageSummary(scope *gorm.DB) (UsageAggregate, error) {
func queryUsageSeries(scope *gorm.DB, granularity UsageGranularity) ([]UsageSeriesPoint, error) {
var source []usageHourPoint
if err := scope.Select("hour_bucket, " + usageAggregateSelect).
Group("hour_bucket").
Order("hour_bucket ASC").
if err := scope.Select("bucket_start_ms, " + usageAggregateSelect).
Group("bucket_start_ms").
Order("bucket_start_ms ASC").
Find(&source).Error; err != nil {
return nil, fmt.Errorf("query usage source series: %w", err)
}
@@ -217,10 +222,9 @@ func queryUsageSeries(scope *gorm.DB, granularity UsageGranularity) ([]UsageSeri
if granularity == UsageGranularityHour {
series := make([]UsageSeriesPoint, 0, len(source))
for _, point := range source {
start := point.HourBucket.UTC()
series = append(series, UsageSeriesPoint{
BucketStart: start,
BucketEnd: start.Add(time.Hour),
BucketStartMS: point.BucketStartMS,
BucketEndMS: point.BucketStartMS + epochms.MillisecondsPerHour,
UsageAggregate: point.UsageAggregate,
})
}
@@ -252,10 +256,10 @@ func queryUsageBreakdown(
case UsageBreakdownOrderRequests:
query = query.
Order("SUM(request_count) DESC").
Order("SUM(cost) DESC")
Order("SUM(estimated_cost_nano_usd) DESC")
case UsageBreakdownOrderCost:
query = query.
Order("SUM(cost) DESC").
Order("SUM(estimated_cost_nano_usd) DESC").
Order("SUM(request_count) DESC")
default:
return nil, false, fmt.Errorf(
@@ -290,21 +294,28 @@ func queryUsageBreakdown(
}
func mergeUsageHoursToDays(source []usageHourPoint) ([]UsageSeriesPoint, error) {
// queryUsageSeries orders source by hour_bucket ASC; adjacent-only daily folding depends on it.
// queryUsageSeries orders source by bucket_start_ms ASC; adjacent-only
// daily folding depends on it.
series := make([]UsageSeriesPoint, 0, len(source))
for _, point := range source {
dayStart := point.HourBucket.UTC().Truncate(24 * time.Hour)
if len(series) == 0 || !series[len(series)-1].BucketStart.Equal(dayStart) {
dayStartMS, err := epochms.AlignDown(
point.BucketStartMS,
epochms.MillisecondsPerDay,
)
if err != nil {
return nil, fmt.Errorf("align usage day: %w", err)
}
if len(series) == 0 || series[len(series)-1].BucketStartMS != dayStartMS {
series = append(series, UsageSeriesPoint{
BucketStart: dayStart,
BucketEnd: dayStart.AddDate(0, 0, 1),
BucketStartMS: dayStartMS,
BucketEndMS: dayStartMS + epochms.MillisecondsPerDay,
UsageAggregate: point.UsageAggregate,
})
continue
}
merged, err := addUsageAggregates(series[len(series)-1].UsageAggregate, point.UsageAggregate)
if err != nil {
return nil, fmt.Errorf("merge usage day %s: %w", dayStart.Format(time.RFC3339), err)
return nil, fmt.Errorf("merge usage day %d: %w", dayStartMS, err)
}
series[len(series)-1].UsageAggregate = merged
}
@@ -337,7 +348,14 @@ func addUsageAggregates(left, right UsageAggregate) (UsageAggregate, error) {
}
*field.target = value
}
result.Cost = left.Cost + right.Cost
cost, ok := pricing.CheckedAddNanoUSD(
pricing.NanoUSD(left.EstimatedCostNanoUSD),
pricing.NanoUSD(right.EstimatedCostNanoUSD),
)
if !ok {
return UsageAggregate{}, fmt.Errorf("estimated cost nano USD overflow or negative")
}
result.EstimatedCostNanoUSD = int64(cost)
if err := validateUsageAggregate(result); err != nil {
return UsageAggregate{}, err
}
@@ -384,7 +402,7 @@ func validateUsageAggregate(aggregate UsageAggregate) error {
return fmt.Errorf("total tokens overflow")
}
}
if aggregate.Cost < 0 || math.IsNaN(aggregate.Cost) || math.IsInf(aggregate.Cost, 0) {
if aggregate.EstimatedCostNanoUSD < 0 {
return fmt.Errorf("invalid cost")
}
return nil
@@ -395,25 +413,26 @@ const usageAggregateSelect = "" +
"COALESCE(SUM(request_count), 0) AS request_count, " +
"COALESCE(SUM(success_count), 0) AS success_count, " +
"COALESCE(SUM(failure_count), 0) AS failure_count, " +
"COALESCE(SUM(input_tokens), 0) AS uncached_input_tokens, " +
"COALESCE(SUM(uncached_input_tokens), 0) AS uncached_input_tokens, " +
"COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, " +
"COALESCE(SUM(cache_write_5m_tokens), 0) AS cache_write5_m_tokens, " +
"COALESCE(SUM(cache_write_1h_tokens), 0) AS cache_write1_h_tokens, " +
"COALESCE(SUM(output_tokens), 0) AS output_tokens, " +
"COALESCE(SUM(cost), 0) AS cost, " +
"COALESCE(SUM(estimated_cost_nano_usd), 0) AS estimated_cost_nano_usd, " +
"COALESCE(SUM(usage_missing_count), 0) AS usage_missing_count, " +
"COALESCE(SUM(partial_count), 0) AS partial_count, " +
"COALESCE(SUM(unpriced_request_count), 0) AS unpriced_request_count"
type usageHourPoint struct {
HourBucket time.Time
BucketStartMS int64
UsageAggregate
}
type usageStatIntegrity struct {
InvalidCount int64
InvalidToken int64
InvalidCost int64
InvalidBucket int64
InvalidCount int64
InvalidToken int64
InvalidCost int64
}
type usageBreakdownRow struct {
+85 -65
View File
@@ -35,8 +35,8 @@ func TestQueryUsageHourAggregatesFiltersAndLeavesSparseBucketsAbsent(t *testing.
groupID := uint(7)
report, err := service.QueryUsage(context.Background(), UsageQuery{
From: start,
To: start.Add(24 * time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(24 * time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
GroupID: &groupID,
Model: "target",
@@ -50,7 +50,8 @@ func TestQueryUsageHourAggregatesFiltersAndLeavesSparseBucketsAbsent(t *testing.
report.Summary.FailureCount != 0 || report.Summary.UncachedInputTokens != 2920 ||
report.Summary.CacheReadTokens != 292 || report.Summary.CacheWrite5MTokens != 20 ||
report.Summary.CacheWrite1HTokens != 49 || report.Summary.OutputTokens != 584 ||
!sameUsageCost(report.Summary.Cost, 29.2) || report.Summary.UsageMissingCount != 12 ||
report.Summary.EstimatedCostNanoUSD != 29_200_000_000 ||
report.Summary.UsageMissingCount != 12 ||
report.Summary.PartialCount != 1 || report.Summary.UnpricedRequestCount != 12 {
t.Fatalf("summary = %#v", report.Summary)
}
@@ -58,11 +59,14 @@ func TestQueryUsageHourAggregatesFiltersAndLeavesSparseBucketsAbsent(t *testing.
t.Fatalf("series length = %d, want 23 actual hour buckets", len(report.Series))
}
for _, point := range report.Series {
if point.BucketStart.Equal(start.Add(7 * time.Hour)) {
if point.BucketStartMS == start.Add(7*time.Hour).UnixMilli() {
t.Fatalf("series synthesized missing hour: %#v", point)
}
if point.BucketEnd.Sub(point.BucketStart) != time.Hour {
t.Fatalf("hour point duration = %s, want 1h", point.BucketEnd.Sub(point.BucketStart))
if point.BucketEndMS-point.BucketStartMS != 3_600_000 {
t.Fatalf(
"hour point duration = %dms, want 3600000",
point.BucketEndMS-point.BucketStartMS,
)
}
}
if len(report.Breakdown) != 1 || report.Breakdown[0].GroupID != 7 ||
@@ -87,8 +91,8 @@ func TestQueryUsageMergesHourlyRowsIntoThirtyUTCDays(t *testing.T) {
createUsageStats(t, db, rows...)
report, err := service.QueryUsage(context.Background(), UsageQuery{
From: start,
To: start.AddDate(0, 0, 30),
FromMS: start.UnixMilli(),
ToMS: start.AddDate(0, 0, 30).UnixMilli(),
Granularity: UsageGranularityDay,
Limit: 100,
BreakdownOrder: UsageBreakdownOrderRequests,
@@ -101,10 +105,12 @@ func TestQueryUsageMergesHourlyRowsIntoThirtyUTCDays(t *testing.T) {
}
for day, point := range report.Series {
wantStart := start.AddDate(0, 0, day)
if !point.BucketStart.Equal(wantStart) || !point.BucketEnd.Equal(wantStart.AddDate(0, 0, 1)) ||
if point.BucketStartMS != wantStart.UnixMilli() ||
point.BucketEndMS != wantStart.AddDate(0, 0, 1).UnixMilli() ||
point.RequestCount != 3 || point.SuccessCount != 3 || point.FailureCount != 0 ||
point.UncachedInputTokens != 30 || point.CacheReadTokens != 3 || point.OutputTokens != 6 ||
!sameUsageCost(point.Cost, 0.3) || point.UsageMissingCount != 1 || point.UnpricedRequestCount != 1 {
point.EstimatedCostNanoUSD != 300_000_000 ||
point.UsageMissingCount != 1 || point.UnpricedRequestCount != 1 {
t.Fatalf("day %d point = %#v", day, point)
}
}
@@ -121,8 +127,8 @@ func TestQueryUsageLimitsBreakdownToStableTopHundred(t *testing.T) {
createUsageStats(t, db, rows...)
report, err := service.QueryUsage(context.Background(), UsageQuery{
From: start,
To: start.Add(time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
Limit: 100,
BreakdownOrder: UsageBreakdownOrderRequests,
@@ -148,11 +154,11 @@ func TestQueryUsageOrdersBreakdownByRequestsAndCost(t *testing.T) {
service := newRequestLogTestService(db)
start := time.Date(2026, time.July, 2, 1, 0, 0, 0, time.UTC)
requestHeavy := usageStat(start, 1, "request-heavy", 20)
requestHeavy.Cost = 1
requestHeavy.EstimatedCostNanoUSD = 1_000_000_000
expensiveLowRequests := usageStat(start, 2, "expensive-low", 5)
expensiveLowRequests.Cost = 10
expensiveLowRequests.EstimatedCostNanoUSD = 10_000_000_000
expensiveHighRequests := usageStat(start, 3, "expensive-high", 7)
expensiveHighRequests.Cost = 10
expensiveHighRequests.EstimatedCostNanoUSD = 10_000_000_000
createUsageStats(t, db, requestHeavy, expensiveLowRequests, expensiveHighRequests)
tests := []struct {
@@ -174,8 +180,8 @@ func TestQueryUsageOrdersBreakdownByRequestsAndCost(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
report, err := service.QueryUsage(context.Background(), UsageQuery{
From: start,
To: start.Add(time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
Limit: 100,
BreakdownOrder: test.order,
@@ -236,8 +242,8 @@ func TestQueryUsageCountsDistinctBreakdownGroupsWithinFilters(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
report, err := service.QueryUsage(context.Background(), UsageQuery{
From: start,
To: start.Add(time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
GroupID: test.groupID,
Model: test.model,
@@ -264,8 +270,8 @@ func TestQueryUsageRejectsInvalidBreakdownOrder(t *testing.T) {
service := newRequestLogTestService(db)
start := time.Date(2026, time.July, 2, 3, 0, 0, 0, time.UTC)
_, err := service.QueryUsage(context.Background(), UsageQuery{
From: start,
To: start.Add(time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
Limit: 100,
BreakdownOrder: UsageBreakdownOrder("unknown"),
@@ -291,11 +297,11 @@ func TestQueryUsageUsesOneReadSnapshot(t *testing.T) {
}
inserted = true
if err := writerDB.Create(&models.UsageStat{
HourBucket: start,
GroupID: 2,
Model: "after-summary",
RequestCount: 1,
SuccessCount: 1,
BucketStartMS: start.UnixMilli(),
GroupID: 2,
Model: "after-summary",
RequestCount: 1,
SuccessCount: 1,
}).Error; err != nil {
t.Errorf("insert concurrent UsageStat: %v", err)
}
@@ -309,8 +315,8 @@ func TestQueryUsageUsesOneReadSnapshot(t *testing.T) {
})
report, err := service.QueryUsage(context.Background(), UsageQuery{
From: start,
To: start.Add(time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
Limit: 100,
BreakdownOrder: UsageBreakdownOrderRequests,
@@ -351,8 +357,8 @@ func TestQueryUsageCancelsAfterBeginWithoutPoisoningDatabaseConnection(t *testin
}()
_, err := service.QueryUsage(ctx, UsageQuery{
From: start,
To: start.Add(time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
Limit: 100,
BreakdownOrder: UsageBreakdownOrderRequests,
@@ -366,11 +372,11 @@ func TestQueryUsageCancelsAfterBeginWithoutPoisoningDatabaseConnection(t *testin
t.Fatalf("query after cancellation = %d/%v, want 1/nil", count, err)
}
if err := db.Create(&models.UsageStat{
HourBucket: start,
GroupID: 2,
Model: "after-cancellation",
RequestCount: 1,
SuccessCount: 1,
BucketStartMS: start.UnixMilli(),
GroupID: 2,
Model: "after-cancellation",
RequestCount: 1,
SuccessCount: 1,
}).Error; err != nil {
t.Fatalf("write after cancellation: %v", err)
}
@@ -387,19 +393,18 @@ func TestQueryUsageRejectsCorruptRowsOutsideTopBreakdown(t *testing.T) {
compensating := usageStat(start, 101, "compensating", 2)
compensating.SuccessCount = 2
compensating.FailureCount = 0
compensating.InputTokens = 1
compensating.Cost = 1
compensating.UncachedInputTokens = 1
compensating.EstimatedCostNanoUSD = 1_000_000_000
corrupt := usageStat(start, 102, "corrupt", 1)
corrupt.SuccessCount = 1
corrupt.FailureCount = 0
corrupt.InputTokens = -1
corrupt.Cost = -1
corrupt.UncachedInputTokens = -1
rows = append(rows, compensating, corrupt)
createUsageStats(t, db, rows...)
createCorruptUsageStats(t, db, rows...)
input := UsageQuery{
From: start,
To: start.Add(time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
Limit: 100,
BreakdownOrder: UsageBreakdownOrderRequests,
@@ -459,7 +464,7 @@ func TestQueryUsageRejectsCorruptAggregates(t *testing.T) {
name: "negative input tokens",
rows: []models.UsageStat{func() models.UsageStat {
row := usageStat(start, 1, "negative-input", 1)
row.InputTokens = -1
row.UncachedInputTokens = -1
return row
}()},
},
@@ -484,7 +489,7 @@ func TestQueryUsageRejectsCorruptAggregates(t *testing.T) {
name: "checked total token overflow",
rows: []models.UsageStat{func() models.UsageStat {
row := usageStat(start, 1, "token-overflow", 1)
row.InputTokens = math.MaxInt64
row.UncachedInputTokens = math.MaxInt64
row.OutputTokens = 1
return row
}()},
@@ -492,27 +497,31 @@ func TestQueryUsageRejectsCorruptAggregates(t *testing.T) {
{
name: "checked count overflow",
rows: []models.UsageStat{
usageStat(start, 1, "overflow-a", math.MaxInt64),
usageStat(start, 2, "overflow-b", 1),
{
BucketStartMS: start.UnixMilli(),
GroupID: 1,
Model: "overflow-a",
RequestCount: math.MaxInt64,
SuccessCount: math.MaxInt64,
},
{
BucketStartMS: start.UnixMilli(),
GroupID: 2,
Model: "overflow-b",
RequestCount: 1,
SuccessCount: 1,
},
},
},
{
name: "negative cost",
rows: []models.UsageStat{func() models.UsageStat {
row := usageStat(start, 1, "negative-cost", 1)
row.Cost = -0.1
return row
}()},
},
{
name: "non-finite summed cost",
name: "checked cost overflow",
rows: []models.UsageStat{func() models.UsageStat {
row := usageStat(start, 1, "huge-cost-a", 1)
row.Cost = math.MaxFloat64
row.EstimatedCostNanoUSD = math.MaxInt64
return row
}(), func() models.UsageStat {
row := usageStat(start, 2, "huge-cost-b", 1)
row.Cost = math.MaxFloat64
row.EstimatedCostNanoUSD = math.MaxInt64
return row
}()},
},
@@ -521,10 +530,10 @@ func TestQueryUsageRejectsCorruptAggregates(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db := openRequestLogQueryDB(t)
createUsageStats(t, db, test.rows...)
createCorruptUsageStats(t, db, test.rows...)
_, err := newRequestLogTestService(db).QueryUsage(context.Background(), UsageQuery{
From: start,
To: start.Add(time.Hour),
FromMS: start.UnixMilli(),
ToMS: start.Add(time.Hour).UnixMilli(),
Granularity: UsageGranularityHour,
Limit: 100,
BreakdownOrder: UsageBreakdownOrderRequests,
@@ -538,17 +547,17 @@ func TestQueryUsageRejectsCorruptAggregates(t *testing.T) {
func usageStat(hour time.Time, groupID uint, model string, requestCount int64) models.UsageStat {
return models.UsageStat{
HourBucket: hour,
BucketStartMS: hour.UTC().UnixMilli(),
GroupID: groupID,
Model: model,
RequestCount: requestCount,
SuccessCount: requestCount,
InputTokens: requestCount * 10,
UncachedInputTokens: requestCount * 10,
OutputTokens: requestCount * 2,
CacheReadTokens: requestCount,
CacheWrite5MTokens: requestCount / 10,
CacheWrite1HTokens: requestCount / 5,
Cost: float64(requestCount) / 10,
EstimatedCostNanoUSD: requestCount * 100_000_000,
UsageMissingCount: requestCount % 2,
PartialCount: 0,
UnpricedRequestCount: requestCount % 2,
@@ -564,8 +573,19 @@ func createUsageStats(t *testing.T, db *gorm.DB, rows ...models.UsageStat) {
}
}
func sameUsageCost(got, want float64) bool {
return math.Abs(got-want) < 1e-12
func createCorruptUsageStats(t *testing.T, db *gorm.DB, rows ...models.UsageStat) {
t.Helper()
if err := db.Exec(`PRAGMA ignore_check_constraints = ON`).Error; err != nil {
t.Fatalf("disable SQLite CHECK constraints: %v", err)
}
for _, row := range rows {
if err := db.Create(&row).Error; err != nil {
t.Fatalf("create corrupt UsageStat %#v: %v", row, err)
}
}
if err := db.Exec(`PRAGMA ignore_check_constraints = OFF`).Error; err != nil {
t.Fatalf("restore SQLite CHECK constraints: %v", err)
}
}
func openUsageQueryWriterDB(t *testing.T, dsn string) (*gorm.DB, func()) {
+108 -69
View File
@@ -6,12 +6,12 @@ import (
"database/sql/driver"
"errors"
"fmt"
"math"
"sort"
"time"
"gorm.io/gorm"
"gpt-load/internal/platform/epochms"
"gpt-load/internal/pricing"
"gpt-load/internal/storage/models"
"gpt-load/internal/telemetry"
@@ -22,32 +22,33 @@ const requestLogTransactionCleanupTimeout = time.Second
const absoluteUsageStatUpsert = `
INSERT INTO usage_stats (
hour_bucket,
bucket_start_ms,
access_key_id,
group_id,
model,
request_count,
success_count,
failure_count,
input_tokens,
uncached_input_tokens,
output_tokens,
cache_read_tokens,
cache_write_5m_tokens,
cache_write_1h_tokens,
cost,
estimated_cost_nano_usd,
usage_missing_count,
partial_count,
unpriced_request_count
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(hour_bucket, group_id, model) DO UPDATE SET
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(bucket_start_ms, access_key_id, group_id, model) DO UPDATE SET
request_count = excluded.request_count,
success_count = excluded.success_count,
failure_count = excluded.failure_count,
input_tokens = excluded.input_tokens,
uncached_input_tokens = excluded.uncached_input_tokens,
output_tokens = excluded.output_tokens,
cache_read_tokens = excluded.cache_read_tokens,
cache_write_5m_tokens = excluded.cache_write_5m_tokens,
cache_write_1h_tokens = excluded.cache_write_1h_tokens,
cost = excluded.cost,
estimated_cost_nano_usd = excluded.estimated_cost_nano_usd,
usage_missing_count = excluded.usage_missing_count,
partial_count = excluded.partial_count,
unpriced_request_count = excluded.unpriced_request_count
@@ -112,21 +113,22 @@ func (writer *gormBatchWriter) WriteBatch(ctx context.Context, rows []models.Req
}
type usageStatKey struct {
HourBucket time.Time
GroupID uint
Model string
BucketStartMS int64
AccessKeyID uint
GroupID uint
Model string
}
type usageStatDelta struct {
RequestCount int64
SuccessCount int64
FailureCount int64
InputTokens int64
UncachedInputTokens int64
OutputTokens int64
CacheReadTokens int64
CacheWrite5MTokens int64
CacheWrite1HTokens int64
Cost float64
EstimatedCostNanoUSD int64
UsageMissingCount int64
PartialCount int64
UnpricedRequestCount int64
@@ -187,7 +189,8 @@ func writeRequestLogBatch(tx *gorm.DB, rows []models.RequestLog) error {
absolute := make([]models.UsageStat, 0, len(keys))
for _, key := range keys {
stat := existingStats[key]
stat.HourBucket = key.HourBucket
stat.BucketStartMS = key.BucketStartMS
stat.AccessKeyID = key.AccessKeyID
stat.GroupID = key.GroupID
stat.Model = key.Model
total, err := checkedUsageStatTotal(stat, deltas[key])
@@ -200,18 +203,19 @@ func writeRequestLogBatch(tx *gorm.DB, rows []models.RequestLog) error {
for _, stat := range absolute {
if err := tx.Exec(
absoluteUsageStatUpsert,
stat.HourBucket,
stat.BucketStartMS,
stat.AccessKeyID,
stat.GroupID,
stat.Model,
stat.RequestCount,
stat.SuccessCount,
stat.FailureCount,
stat.InputTokens,
stat.UncachedInputTokens,
stat.OutputTokens,
stat.CacheReadTokens,
stat.CacheWrite5MTokens,
stat.CacheWrite1HTokens,
stat.Cost,
stat.EstimatedCostNanoUSD,
stat.UsageMissingCount,
stat.PartialCount,
stat.UnpricedRequestCount,
@@ -225,13 +229,18 @@ func writeRequestLogBatch(tx *gorm.DB, rows []models.RequestLog) error {
func buildUsageStatDeltas(rows []models.RequestLog) (map[usageStatKey]usageStatDelta, error) {
deltas := make(map[usageStatKey]usageStatDelta)
for _, row := range rows {
if row.GroupID == 0 || row.UpstreamModel == "" {
continue
bucketStartMS, err := epochms.AlignDown(
row.CompletedAtMS,
epochms.MillisecondsPerHour,
)
if err != nil {
return nil, fmt.Errorf("aggregate request log %q completion time: %w", row.ID, err)
}
key := usageStatKey{
HourBucket: row.CreatedAt.UTC().Truncate(time.Hour),
GroupID: row.GroupID,
Model: row.UpstreamModel,
BucketStartMS: bucketStartMS,
AccessKeyID: row.AccessKeyID,
GroupID: row.GroupID,
Model: row.ClientModel,
}
delta := deltas[key]
if err := delta.addRow(row); err != nil {
@@ -246,12 +255,19 @@ func (delta *usageStatDelta) addRow(row models.RequestLog) error {
if err := checkedInt64Add(&delta.RequestCount, 1, "request_count"); err != nil {
return err
}
if row.Status == string(telemetry.RequestStatusSuccess) {
switch row.Status {
case string(telemetry.RequestStatusSuccess):
if err := checkedInt64Add(&delta.SuccessCount, 1, "success_count"); err != nil {
return err
}
} else if err := checkedInt64Add(&delta.FailureCount, 1, "failure_count"); err != nil {
return err
case string(telemetry.RequestStatusError),
string(telemetry.RequestStatusIncomplete),
string(telemetry.RequestStatusCanceled):
if err := checkedInt64Add(&delta.FailureCount, 1, "failure_count"); err != nil {
return err
}
default:
return fmt.Errorf("aggregate request log %q: invalid status %q", row.ID, row.Status)
}
switch row.UsageState {
@@ -263,15 +279,20 @@ func (delta *usageStatDelta) addRow(row models.RequestLog) error {
if err := checkedInt64Add(&delta.PartialCount, 1, "partial_count"); err != nil {
return err
}
case string(usage.StateComplete), string(usage.StateNotApplicable):
default:
return fmt.Errorf("aggregate request log %q: invalid usage state %q", row.ID, row.UsageState)
}
if row.CostState == string(pricing.CostStateUnpriced) {
if (row.UsageState == string(usage.StateComplete) ||
row.UsageState == string(usage.StatePartial)) &&
row.CostState == string(pricing.CostStateUnpriced) {
if err := checkedInt64Add(&delta.UnpricedRequestCount, 1, "unpriced_request_count"); err != nil {
return err
}
}
if row.UsageState != string(usage.StateComplete) ||
row.CostState != string(pricing.CostStatePriced) {
if row.UsageState != string(usage.StateComplete) &&
row.UsageState != string(usage.StatePartial) {
return nil
}
for _, field := range []struct {
@@ -279,7 +300,7 @@ func (delta *usageStatDelta) addRow(row models.RequestLog) error {
target *int64
value int64
}{
{name: "input_tokens", target: &delta.InputTokens, value: row.InputTokens},
{name: "uncached_input_tokens", target: &delta.UncachedInputTokens, value: row.UncachedInputTokens},
{name: "output_tokens", target: &delta.OutputTokens, value: row.OutputTokens},
{name: "cache_read_tokens", target: &delta.CacheReadTokens, value: row.CacheReadTokens},
{name: "cache_write_5m_tokens", target: &delta.CacheWrite5MTokens, value: row.CacheWrite5MTokens},
@@ -289,11 +310,17 @@ func (delta *usageStatDelta) addRow(row models.RequestLog) error {
return err
}
}
cost, ok := checkedCostAdd(delta.Cost, row.Cost)
if !ok {
return fmt.Errorf("aggregate usage stat cost: checked addition failed")
if row.CostState != string(pricing.CostStatePriced) {
return nil
}
delta.Cost = cost
cost, ok := pricing.CheckedAddNanoUSD(
pricing.NanoUSD(delta.EstimatedCostNanoUSD),
pricing.NanoUSD(row.EstimatedCostNanoUSD),
)
if !ok {
return fmt.Errorf("aggregate usage stat estimated_cost_nano_usd: checked addition failed")
}
delta.EstimatedCostNanoUSD = int64(cost)
return nil
}
@@ -306,27 +333,17 @@ func checkedInt64Add(target *int64, value int64, field string) error {
return nil
}
func checkedCostAdd(left, right float64) (float64, bool) {
if left < 0 || right < 0 ||
math.IsNaN(left) || math.IsNaN(right) ||
math.IsInf(left, 0) || math.IsInf(right, 0) {
return 0, false
}
total := left + right
if total < 0 || math.IsNaN(total) || math.IsInf(total, 0) {
return 0, false
}
return total, true
}
func sortedUsageStatKeys(deltas map[usageStatKey]usageStatDelta) []usageStatKey {
keys := make([]usageStatKey, 0, len(deltas))
for key := range deltas {
keys = append(keys, key)
}
sort.Slice(keys, func(left, right int) bool {
if !keys[left].HourBucket.Equal(keys[right].HourBucket) {
return keys[left].HourBucket.Before(keys[right].HourBucket)
if keys[left].BucketStartMS != keys[right].BucketStartMS {
return keys[left].BucketStartMS < keys[right].BucketStartMS
}
if keys[left].AccessKeyID != keys[right].AccessKeyID {
return keys[left].AccessKeyID < keys[right].AccessKeyID
}
if keys[left].GroupID != keys[right].GroupID {
return keys[left].GroupID < keys[right].GroupID
@@ -344,16 +361,18 @@ func queryExistingUsageStats(
for index, key := range keys {
if index == 0 {
query = query.Where(
"hour_bucket = ? AND group_id = ? AND model = ?",
key.HourBucket,
"bucket_start_ms = ? AND access_key_id = ? AND group_id = ? AND model = ?",
key.BucketStartMS,
key.AccessKeyID,
key.GroupID,
key.Model,
)
continue
}
query = query.Or(
"hour_bucket = ? AND group_id = ? AND model = ?",
key.HourBucket,
"bucket_start_ms = ? AND access_key_id = ? AND group_id = ? AND model = ?",
key.BucketStartMS,
key.AccessKeyID,
key.GroupID,
key.Model,
)
@@ -365,9 +384,10 @@ func queryExistingUsageStats(
existing := make(map[usageStatKey]models.UsageStat, len(rows))
for _, row := range rows {
key := usageStatKey{
HourBucket: row.HourBucket.UTC(),
GroupID: row.GroupID,
Model: row.Model,
BucketStartMS: row.BucketStartMS,
AccessKeyID: row.AccessKeyID,
GroupID: row.GroupID,
Model: row.Model,
}
existing[key] = row
}
@@ -388,7 +408,7 @@ func checkedUsageStatTotal(
{name: "request_count", left: existing.RequestCount, right: delta.RequestCount, set: func(value int64) { total.RequestCount = value }},
{name: "success_count", left: existing.SuccessCount, right: delta.SuccessCount, set: func(value int64) { total.SuccessCount = value }},
{name: "failure_count", left: existing.FailureCount, right: delta.FailureCount, set: func(value int64) { total.FailureCount = value }},
{name: "input_tokens", left: existing.InputTokens, right: delta.InputTokens, set: func(value int64) { total.InputTokens = value }},
{name: "uncached_input_tokens", left: existing.UncachedInputTokens, right: delta.UncachedInputTokens, set: func(value int64) { total.UncachedInputTokens = value }},
{name: "output_tokens", left: existing.OutputTokens, right: delta.OutputTokens, set: func(value int64) { total.OutputTokens = value }},
{name: "cache_read_tokens", left: existing.CacheReadTokens, right: delta.CacheReadTokens, set: func(value int64) { total.CacheReadTokens = value }},
{name: "cache_write_5m_tokens", left: existing.CacheWrite5MTokens, right: delta.CacheWrite5MTokens, set: func(value int64) { total.CacheWrite5MTokens = value }},
@@ -406,13 +426,16 @@ func checkedUsageStatTotal(
}
field.set(value)
}
cost, ok := checkedCostAdd(existing.Cost, delta.Cost)
cost, ok := pricing.CheckedAddNanoUSD(
pricing.NanoUSD(existing.EstimatedCostNanoUSD),
pricing.NanoUSD(delta.EstimatedCostNanoUSD),
)
if !ok {
return models.UsageStat{}, fmt.Errorf(
"calculate absolute usage stat cost: checked addition failed",
"calculate absolute usage stat estimated_cost_nano_usd: checked addition failed",
)
}
total.Cost = cost
total.EstimatedCostNanoUSD = int64(cost)
return total, nil
}
@@ -524,21 +547,37 @@ func (service *Service) drain(ctx context.Context, batch []queuedEvent) {
}
func (service *Service) writeBatch(ctx context.Context, events []queuedEvent) {
rows := make([]models.RequestLog, len(events))
for index, event := range events {
rows[index] = mapEvent(service.redactor, event.Event, event.Prices)
rows := make([]models.RequestLog, 0, len(events))
projectionFailures := 0
for _, event := range events {
row, err := mapEvent(service.redactor, event.Event, event.Prices)
if err != nil {
projectionFailures++
continue
}
rows = append(rows, row)
}
if projectionFailures > 0 {
service.recordPersistFailure("projection_failure", projectionFailures)
}
if len(rows) == 0 {
return
}
if err := service.writer.WriteBatch(ctx, rows); err != nil {
service.writeFailureTotal.Add(1)
service.droppedPersistFailedTotal.Add(uint64(len(events)))
service.statsMu.Lock()
service.lastWriteFailureAt = service.now().UTC()
service.statsMu.Unlock()
service.warn("write_failure", len(events))
service.recordPersistFailure("write_failure", len(rows))
return
}
service.persistedTotal.Add(uint64(len(events)))
service.persistedTotal.Add(uint64(len(rows)))
}
func (service *Service) recordPersistFailure(reason string, count int) {
service.writeFailureTotal.Add(1)
service.droppedPersistFailedTotal.Add(uint64(count))
service.statsMu.Lock()
service.lastWriteFailureAt = service.now().UTC()
service.statsMu.Unlock()
service.warn(reason, count)
}
func (service *Service) dropUnattempted(batch []queuedEvent) {
+187 -98
View File
@@ -116,6 +116,45 @@ func TestServiceDropsFailedBatchAndContinues(t *testing.T) {
}
}
func TestServiceDropsInvalidProjectionWithoutDiscardingValidRows(t *testing.T) {
timers := newManualTimerFactory()
writes := make(chan []models.RequestLog, 1)
service := newService(
batchWriterFunc(func(_ context.Context, rows []models.RequestLog) error {
writes <- append([]models.RequestLog(nil), rows...)
return nil
}),
redact.New(),
timers.New,
newStaticPriceTableProvider(),
)
if err := service.Start(); err != nil {
t.Fatalf("Start() error = %v", err)
}
invalid := testEvent("invalid-completion")
invalid.CompletedAt = time.Unix(-1, 0)
service.Emit(invalid)
service.Emit(testEvent("valid-completion"))
receiveValue(t, timers.created).Fire()
rows := receiveValue(t, writes)
if len(rows) != 1 || rows[0].ID != "valid-completion" {
t.Fatalf("persisted rows = %+v, want only valid-completion", rows)
}
if err := service.Stop(context.Background()); err != nil {
t.Fatalf("Stop() error = %v", err)
}
stats := service.Stats()
if stats.WriteFailureTotal != 1 ||
stats.DroppedPersistFailedTotal != 1 ||
stats.PersistedTotal != 1 ||
stats.DroppedTotal != 1 ||
stats.LastWriteFailureAt.IsZero() {
t.Fatalf("stats after projection failure = %+v", stats)
}
}
func TestServiceStopDrainsAndIsIdempotent(t *testing.T) {
writes := make(chan []models.RequestLog, 2)
service := newService(
@@ -324,43 +363,44 @@ func TestServiceConcurrentStopHonorsOwnDeadlineWithoutDuplicateDrain(t *testing.
}
}
func TestBatchWriterInsertsOnlyNewRequestLogsAndAggregatesUsage(t *testing.T) {
func TestWriteBatchInsertsOnlyNewRequestLogsAndAggregatesUsage(t *testing.T) {
db := openRequestLogQueryDB(t)
hour := time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC)
existing := aggregationRow(aggregationRequestID(1), hour, 7, "aggregate-model")
existing.InputTokens = 99
existing.UncachedInputTokens = 99
if err := db.Create(&existing).Error; err != nil {
t.Fatalf("create existing RequestLog: %v", err)
}
baseline := models.UsageStat{
HourBucket: hour,
GroupID: 7,
Model: "aggregate-model",
RequestCount: 10,
SuccessCount: 10,
InputTokens: 100,
OutputTokens: 200,
Cost: 1,
BucketStartMS: 1_784_894_400_000,
AccessKeyID: 1,
GroupID: 7,
Model: "aggregate-model",
RequestCount: 10,
SuccessCount: 10,
UncachedInputTokens: 100,
OutputTokens: 200,
EstimatedCostNanoUSD: 1_000_000_000,
}
if err := db.Create(&baseline).Error; err != nil {
t.Fatalf("create baseline UsageStat: %v", err)
}
first := aggregationRow(aggregationRequestID(2), hour.Add(time.Minute), 7, "aggregate-model")
first.InputTokens = 3
first.UncachedInputTokens = 3
first.OutputTokens = 5
first.Cost = 0.5
first.EstimatedCostNanoUSD = 500_000_000
duplicate := first
duplicate.GroupID = 999
duplicate.UpstreamModel = "duplicate-must-lose"
duplicate.InputTokens = 1_000
duplicate.UncachedInputTokens = 1_000
replay := existing
replay.GroupID = 999
replay.UpstreamModel = "existing-must-win"
second := aggregationRow(aggregationRequestID(3), hour.Add(2*time.Minute), 7, "aggregate-model")
second.InputTokens = 7
second.UncachedInputTokens = 7
second.OutputTokens = 11
second.Cost = 0.75
second.EstimatedCostNanoUSD = 750_000_000
rows := []models.RequestLog{first, duplicate, replay, second}
var requestLogQueries, usageStatQueries int
@@ -398,26 +438,33 @@ func TestBatchWriterInsertsOnlyNewRequestLogsAndAggregatesUsage(t *testing.T) {
if err := db.First(&persistedFirst, "id = ?", first.ID).Error; err != nil {
t.Fatalf("query first RequestLog: %v", err)
}
if persistedFirst.GroupID != 7 || persistedFirst.UpstreamModel != "aggregate-model" ||
persistedFirst.InputTokens != 3 {
if persistedFirst.GroupID != 7 || persistedFirst.UpstreamModel != "upstream-aggregate-model" ||
persistedFirst.UncachedInputTokens != 3 {
t.Fatalf("first-write-wins row = %+v", persistedFirst)
}
var persistedExisting models.RequestLog
if err := db.First(&persistedExisting, "id = ?", existing.ID).Error; err != nil {
t.Fatalf("query existing RequestLog: %v", err)
}
if persistedExisting.GroupID != 7 || persistedExisting.UpstreamModel != "aggregate-model" ||
persistedExisting.InputTokens != 99 {
if persistedExisting.GroupID != 7 || persistedExisting.UpstreamModel != "upstream-aggregate-model" ||
persistedExisting.UncachedInputTokens != 99 {
t.Fatalf("existing row was updated: %+v", persistedExisting)
}
var stat models.UsageStat
if err := db.Where("hour_bucket = ? AND group_id = ? AND model = ?", hour, 7, "aggregate-model").
if err := db.Where(
"bucket_start_ms = ? AND access_key_id = ? AND group_id = ? AND model = ?",
int64(1_784_894_400_000),
1,
7,
"aggregate-model",
).
Take(&stat).Error; err != nil {
t.Fatalf("query UsageStat: %v", err)
}
if stat.RequestCount != 12 || stat.SuccessCount != 12 || stat.FailureCount != 0 ||
stat.InputTokens != 110 || stat.OutputTokens != 216 || stat.Cost != 2.25 {
stat.UncachedInputTokens != 110 || stat.OutputTokens != 216 ||
stat.EstimatedCostNanoUSD != 2_250_000_000 {
t.Fatalf("UsageStat = %+v, want baseline plus only two new rows", stat)
}
@@ -433,49 +480,51 @@ func TestBatchWriterInsertsOnlyNewRequestLogsAndAggregatesUsage(t *testing.T) {
}
}
func TestBatchWriterAggregatesStatusQualityAndCompletePricedTotals(t *testing.T) {
func TestWriteBatchAggregatesStatusQualityAndCompletePricedTotals(t *testing.T) {
db := openRequestLogQueryDB(t)
hour := time.Date(2026, time.July, 24, 13, 0, 0, 0, time.UTC)
complete := aggregationRow(aggregationRequestID(10), hour, 8, "quality-model")
complete.InputTokens = 1
complete.UncachedInputTokens = 1
complete.OutputTokens = 2
complete.CacheReadTokens = 3
complete.CacheWrite5MTokens = 4
complete.CacheWrite1HTokens = 5
complete.Cost = 1.25
complete.EstimatedCostNanoUSD = 1_250_000_000
completeUnpriced := aggregationRow(aggregationRequestID(15), hour, 8, "quality-model")
completeUnpriced.CostState = string(pricing.CostStateUnpriced)
completeUnpriced.InputTokens = 400
completeUnpriced.UncachedInputTokens = 400
completeUnpriced.OutputTokens = 500
completeUnpriced.Cost = 0
completeUnpriced.EstimatedCostNanoUSD = 0
missing := aggregationRow(aggregationRequestID(11), hour, 8, "quality-model")
missing.Status = string(telemetry.RequestStatusError)
missing.UsageState = string(usage.StateMissing)
missing.CostState = string(pricing.CostStateUnpriced)
missing.InputTokens = 100
missing.Cost = 0
missing.UncachedInputTokens = 100
missing.EstimatedCostNanoUSD = 0
partialPriced := aggregationRow(aggregationRequestID(12), hour, 8, "quality-model")
partialPriced.Status = string(telemetry.RequestStatusIncomplete)
partialPriced.UsageState = string(usage.StatePartial)
partialPriced.InputTokens = 200
partialPriced.UncachedInputTokens = 200
partialPriced.OutputTokens = 300
partialPriced.Cost = 2.5
partialPriced.EstimatedCostNanoUSD = 2_500_000_000
partialUnpriced := aggregationRow(aggregationRequestID(13), hour, 8, "quality-model")
partialUnpriced.Status = string(telemetry.RequestStatusCanceled)
partialUnpriced.UsageState = string(usage.StatePartial)
partialUnpriced.CostState = string(pricing.CostStateUnpriced)
partialUnpriced.Cost = 0
partialUnpriced.EstimatedCostNanoUSD = 0
unknown := aggregationRow(aggregationRequestID(14), hour, 8, "quality-model")
unknown.Status = "future_status"
unknown.UsageState = string(usage.StateNotApplicable)
unknown.CostState = string(pricing.CostStateNotApplicable)
unknown.Cost = 0
notApplicable := aggregationRow(aggregationRequestID(14), hour, 8, "quality-model")
notApplicable.Status = string(telemetry.RequestStatusError)
notApplicable.UsageState = string(usage.StateNotApplicable)
notApplicable.CostState = string(pricing.CostStateNotApplicable)
notApplicable.UncachedInputTokens = 700
notApplicable.OutputTokens = 800
notApplicable.EstimatedCostNanoUSD = 0
if err := (&gormBatchWriter{db: db}).WriteBatch(
context.Background(),
@@ -485,7 +534,7 @@ func TestBatchWriterAggregatesStatusQualityAndCompletePricedTotals(t *testing.T)
missing,
partialPriced,
partialUnpriced,
unknown,
notApplicable,
},
); err != nil {
t.Fatalf("WriteBatch() error = %v", err)
@@ -497,33 +546,33 @@ func TestBatchWriterAggregatesStatusQualityAndCompletePricedTotals(t *testing.T)
}
if stat.RequestCount != 6 || stat.SuccessCount != 2 || stat.FailureCount != 4 ||
stat.UsageMissingCount != 1 || stat.PartialCount != 2 ||
stat.UnpricedRequestCount != 3 {
stat.UnpricedRequestCount != 2 {
t.Fatalf("status/quality counts = %+v", stat)
}
if stat.InputTokens != 1 || stat.OutputTokens != 2 || stat.CacheReadTokens != 3 ||
if stat.UncachedInputTokens != 602 || stat.OutputTokens != 804 || stat.CacheReadTokens != 3 ||
stat.CacheWrite5MTokens != 4 || stat.CacheWrite1HTokens != 5 ||
stat.Cost != 1.25 {
t.Fatalf("complete+priced totals = %+v", stat)
stat.EstimatedCostNanoUSD != 3_750_000_000 {
t.Fatalf("complete/partial totals = %+v", stat)
}
var persistedPartial models.RequestLog
if err := db.First(&persistedPartial, "id = ?", partialPriced.ID).Error; err != nil {
t.Fatalf("query partial RequestLog: %v", err)
}
if persistedPartial.InputTokens != 200 || persistedPartial.OutputTokens != 300 ||
persistedPartial.Cost != 2.5 {
if persistedPartial.UncachedInputTokens != 200 || persistedPartial.OutputTokens != 300 ||
persistedPartial.EstimatedCostNanoUSD != 2_500_000_000 {
t.Fatalf("partial+priced RequestLog lost usage/cost: %+v", persistedPartial)
}
var persistedUnpriced models.RequestLog
if err := db.First(&persistedUnpriced, "id = ?", completeUnpriced.ID).Error; err != nil {
t.Fatalf("query complete+unpriced RequestLog: %v", err)
}
if persistedUnpriced.InputTokens != 400 || persistedUnpriced.OutputTokens != 500 ||
persistedUnpriced.Cost != 0 {
if persistedUnpriced.UncachedInputTokens != 400 || persistedUnpriced.OutputTokens != 500 ||
persistedUnpriced.EstimatedCostNanoUSD != 0 {
t.Fatalf("complete+unpriced RequestLog lost usage or gained cost: %+v", persistedUnpriced)
}
}
func TestBatchWriterSeparatesHourGroupAndUpstreamModel(t *testing.T) {
func TestWriteBatchSeparatesHourAccessGroupAndClientModelWithoutSkippingZeroDimensions(t *testing.T) {
db := openRequestLogQueryDB(t)
location := time.FixedZone("utc-plus-eight", 8*60*60)
localHour := time.Date(2026, time.July, 24, 20, 0, 0, 0, location)
@@ -536,32 +585,54 @@ func TestBatchWriterSeparatesHourGroupAndUpstreamModel(t *testing.T) {
aggregationRow(aggregationRequestID(25), localHour.Add(4*time.Minute), 0, "model-a"),
aggregationRow(aggregationRequestID(26), localHour.Add(5*time.Minute), 9, ""),
}
differentAccess := aggregationRow(
aggregationRequestID(27),
localHour.Add(6*time.Minute),
9,
"model-a",
)
differentAccess.AccessKeyID = 2
rows = append(rows, differentAccess)
if err := (&gormBatchWriter{db: db}).WriteBatch(context.Background(), rows); err != nil {
t.Fatalf("WriteBatch() error = %v", err)
}
var stats []models.UsageStat
if err := db.Order("hour_bucket ASC").Order("group_id ASC").Order("model ASC").Find(&stats).Error; err != nil {
if err := db.Order("bucket_start_ms ASC").
Order("access_key_id ASC").
Order("group_id ASC").
Order("model ASC").
Find(&stats).Error; err != nil {
t.Fatalf("query UsageStats: %v", err)
}
if len(stats) != 4 {
t.Fatalf("UsageStat rows = %d, want four attributable buckets: %+v", len(stats), stats)
if len(stats) != 7 {
t.Fatalf("UsageStat rows = %d, want seven isolated buckets: %+v", len(stats), stats)
}
type bucket struct {
hour time.Time
group uint
model string
count int64
bucketMS int64
access uint
group uint
model string
count int64
}
got := make([]bucket, 0, len(stats))
for _, stat := range stats {
got = append(got, bucket{stat.HourBucket.UTC(), stat.GroupID, stat.Model, stat.RequestCount})
got = append(got, bucket{
stat.BucketStartMS,
stat.AccessKeyID,
stat.GroupID,
stat.Model,
stat.RequestCount,
})
}
want := []bucket{
{time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC), 9, "model-a", 2},
{time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC), 9, "model-b", 1},
{time.Date(2026, time.July, 24, 12, 0, 0, 0, time.UTC), 10, "model-a", 1},
{time.Date(2026, time.July, 24, 13, 0, 0, 0, time.UTC), 9, "model-a", 1},
{1_784_894_400_000, 1, 0, "model-a", 1},
{1_784_894_400_000, 1, 9, "", 1},
{1_784_894_400_000, 1, 9, "model-a", 2},
{1_784_894_400_000, 1, 9, "model-b", 1},
{1_784_894_400_000, 1, 10, "model-a", 1},
{1_784_894_400_000, 2, 9, "model-a", 1},
{1_784_898_000_000, 1, 9, "model-a", 1},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("buckets = %#v, want %#v", got, want)
@@ -575,7 +646,7 @@ func TestBatchWriterSeparatesHourGroupAndUpstreamModel(t *testing.T) {
}
}
func TestBatchWriterRollsBackRequestLogsAndStatsOnFailure(t *testing.T) {
func TestWriteBatchRollsBackRequestLogsAndStatsOnFailure(t *testing.T) {
newRow := func() models.RequestLog {
return aggregationRow(
aggregationRequestID(30),
@@ -637,15 +708,30 @@ func TestBatchWriterRollsBackRequestLogsAndStatsOnFailure(t *testing.T) {
t.Run("existing UsageStat scan conversion", func(t *testing.T) {
db := openRequestLogQueryDB(t)
row := newRow()
hour := row.CreatedAt.UTC().Truncate(time.Hour)
hourMS := row.CompletedAtMS - row.CompletedAtMS%3_600_000
const invalidRequestCount = "not-an-integer"
if err := db.Exec(`PRAGMA ignore_check_constraints = ON`).Error; err != nil {
t.Fatalf("disable SQLite CHECK constraints: %v", err)
}
if err := db.Exec(`
INSERT INTO usage_stats (
hour_bucket, group_id, model, request_count, success_count, cost
) VALUES (?, ?, ?, ?, ?, ?)
`, hour, row.GroupID, row.UpstreamModel, invalidRequestCount, 7, 3.5).Error; err != nil {
bucket_start_ms, access_key_id, group_id, model,
request_count, success_count, estimated_cost_nano_usd
) VALUES (?, ?, ?, ?, ?, ?, ?)
`,
hourMS,
row.AccessKeyID,
row.GroupID,
row.ClientModel,
invalidRequestCount,
7,
3_500_000_000,
).Error; err != nil {
t.Fatalf("insert incompatible UsageStat: %v", err)
}
if err := db.Exec(`PRAGMA ignore_check_constraints = OFF`).Error; err != nil {
t.Fatalf("restore SQLite CHECK constraints: %v", err)
}
err := (&gormBatchWriter{db: db}).WriteBatch(
context.Background(),
@@ -657,23 +743,25 @@ func TestBatchWriterRollsBackRequestLogsAndStatsOnFailure(t *testing.T) {
assertRequestLogAndUsageStatCounts(t, db, 0, 1)
var persisted struct {
RequestCount string `gorm:"column:request_count"`
SuccessCount int64 `gorm:"column:success_count"`
Cost float64 `gorm:"column:cost"`
RequestCount string `gorm:"column:request_count"`
SuccessCount int64 `gorm:"column:success_count"`
EstimatedCostNanoUSD int64 `gorm:"column:estimated_cost_nano_usd"`
}
if err := db.Raw(`
SELECT
CAST(request_count AS TEXT) AS request_count,
success_count,
cost
estimated_cost_nano_usd
FROM usage_stats
WHERE hour_bucket = ? AND group_id = ? AND model = ?
`, hour, row.GroupID, row.UpstreamModel).Scan(&persisted).Error; err != nil {
WHERE bucket_start_ms = ? AND access_key_id = ?
AND group_id = ? AND model = ?
`, hourMS, row.AccessKeyID, row.GroupID, row.ClientModel).
Scan(&persisted).Error; err != nil {
t.Fatalf("read incompatible UsageStat after rollback: %v", err)
}
if persisted.RequestCount != invalidRequestCount ||
persisted.SuccessCount != 7 ||
persisted.Cost != 3.5 {
persisted.EstimatedCostNanoUSD != 3_500_000_000 {
t.Fatalf("existing UsageStat changed after scan failure: %+v", persisted)
}
})
@@ -707,34 +795,34 @@ func TestBatchWriterRollsBackRequestLogsAndStatsOnFailure(t *testing.T) {
})
}
func TestBatchWriterRejectsIntegerAndCostOverflow(t *testing.T) {
func TestWriteBatchRejectsIntegerAndCostOverflow(t *testing.T) {
hour := time.Date(2026, time.July, 24, 15, 0, 0, 0, time.UTC)
t.Run("batch token delta overflow", func(t *testing.T) {
db := openRequestLogQueryDB(t)
first := aggregationRow(aggregationRequestID(40), hour, 12, "overflow-model")
first.InputTokens = math.MaxInt64
first.UncachedInputTokens = math.MaxInt64
second := aggregationRow(aggregationRequestID(41), hour, 12, "overflow-model")
second.InputTokens = 1
second.UncachedInputTokens = 1
assertBatchWriterRejectsRowsWithoutChanges(t, db, nil, []models.RequestLog{first, second})
})
t.Run("batch cost overflow", func(t *testing.T) {
db := openRequestLogQueryDB(t)
first := aggregationRow(aggregationRequestID(42), hour, 12, "overflow-model")
first.Cost = math.MaxFloat64
first.EstimatedCostNanoUSD = math.MaxInt64
second := aggregationRow(aggregationRequestID(43), hour, 12, "overflow-model")
second.Cost = math.MaxFloat64
assertBatchWriterRejectsRowsWithoutChanges(t, db, nil, []models.RequestLog{first, second})
})
t.Run("existing count overflow", func(t *testing.T) {
db := openRequestLogQueryDB(t)
existing := models.UsageStat{
HourBucket: hour,
GroupID: 12,
Model: "overflow-model",
RequestCount: math.MaxInt64,
BucketStartMS: 1_784_905_200_000,
AccessKeyID: 1,
GroupID: 12,
Model: "overflow-model",
RequestCount: math.MaxInt64,
}
row := aggregationRow(aggregationRequestID(44), hour, 12, "overflow-model")
assertBatchWriterRejectsRowsWithoutChanges(t, db, &existing, []models.RequestLog{row})
@@ -743,10 +831,11 @@ func TestBatchWriterRejectsIntegerAndCostOverflow(t *testing.T) {
t.Run("existing token overflow", func(t *testing.T) {
db := openRequestLogQueryDB(t)
existing := models.UsageStat{
HourBucket: hour,
GroupID: 12,
Model: "overflow-model",
InputTokens: math.MaxInt64,
BucketStartMS: 1_784_905_200_000,
AccessKeyID: 1,
GroupID: 12,
Model: "overflow-model",
UncachedInputTokens: math.MaxInt64,
}
row := aggregationRow(aggregationRequestID(45), hour, 12, "overflow-model")
assertBatchWriterRejectsRowsWithoutChanges(t, db, &existing, []models.RequestLog{row})
@@ -755,23 +844,24 @@ func TestBatchWriterRejectsIntegerAndCostOverflow(t *testing.T) {
t.Run("existing cost overflow", func(t *testing.T) {
db := openRequestLogQueryDB(t)
existing := models.UsageStat{
HourBucket: hour,
GroupID: 12,
Model: "overflow-model",
Cost: math.MaxFloat64,
BucketStartMS: 1_784_905_200_000,
AccessKeyID: 1,
GroupID: 12,
Model: "overflow-model",
EstimatedCostNanoUSD: math.MaxInt64,
}
row := aggregationRow(aggregationRequestID(46), hour, 12, "overflow-model")
row.Cost = math.MaxFloat64
assertBatchWriterRejectsRowsWithoutChanges(t, db, &existing, []models.RequestLog{row})
})
t.Run("negative existing integer", func(t *testing.T) {
db := openRequestLogQueryDB(t)
existing := models.UsageStat{
HourBucket: hour,
GroupID: 12,
Model: "overflow-model",
FailureCount: -1,
BucketStartMS: 1_784_905_200_000,
AccessKeyID: 1,
GroupID: 12,
Model: "overflow-model",
FailureCount: -1,
}
row := aggregationRow(aggregationRequestID(47), hour, 12, "overflow-model")
assertBatchWriterRejectsRowsWithoutChanges(t, db, &existing, []models.RequestLog{row})
@@ -780,12 +870,12 @@ func TestBatchWriterRejectsIntegerAndCostOverflow(t *testing.T) {
t.Run("negative row cost", func(t *testing.T) {
db := openRequestLogQueryDB(t)
row := aggregationRow(aggregationRequestID(48), hour, 12, "overflow-model")
row.Cost = -1
row.EstimatedCostNanoUSD = -1
assertBatchWriterRejectsRowsWithoutChanges(t, db, nil, []models.RequestLog{row})
})
}
func TestBatchWriterRollsBackEarlierBucketWhenLaterUpsertFails(t *testing.T) {
func TestWriteBatchRollsBackEarlierBucketWhenLaterUpsertFails(t *testing.T) {
db := openRequestLogQueryDB(t)
hour := time.Date(2026, time.July, 24, 16, 0, 0, 0, time.UTC)
if err := db.Exec(`
@@ -797,7 +887,8 @@ func TestBatchWriterRollsBackEarlierBucketWhenLaterUpsertFails(t *testing.T) {
WHEN COALESCE((
SELECT request_count
FROM usage_stats
WHERE hour_bucket = NEW.hour_bucket
WHERE bucket_start_ms = NEW.bucket_start_ms
AND access_key_id = NEW.access_key_id
AND group_id = NEW.group_id
AND model = 'model-a'
), 0) != 1
@@ -896,9 +987,7 @@ func assertBatchWriterRejectsRowsWithoutChanges(
) {
t.Helper()
if existing != nil {
if err := db.Create(existing).Error; err != nil {
t.Fatalf("create existing UsageStat: %v", err)
}
createCorruptUsageStats(t, db, *existing)
}
var before []models.UsageStat
if err := db.Order("id ASC").Find(&before).Error; err != nil {
+31 -68
View File
@@ -25,7 +25,7 @@ import (
)
// CurrentSchemaVersion identifies the SQLite schema supported by this binary.
const CurrentSchemaVersion uint = 2
const CurrentSchemaVersion uint = 3
const sqliteBusyTimeoutMS = 5000
@@ -159,8 +159,10 @@ func autoMigrate(
return err
}
switch version {
case CurrentSchemaVersion:
case 3:
return db.Transaction(migrateCurrentSchema)
case 2:
return db.Transaction(migrateSchemaV2ToV3)
case 1:
var accessKeyCount int64
if db.Migrator().HasTable(&models.AccessKey{}) {
@@ -180,19 +182,19 @@ func autoMigrate(
if err := migrateAccessKeysV1ToV2(tx, encryptionService); err != nil {
return err
}
if err := migrateCurrentSchema(tx); err != nil {
if err := ensureSchemaV2TablesForV1(tx); err != nil {
return err
}
result := tx.Model(&schemaInfo{}).
Where("version = ?", uint(1)).
Update("version", CurrentSchemaVersion)
Update("version", schemaV2Version)
if result.Error != nil {
return fmt.Errorf("update schema_info: %w", result.Error)
return fmt.Errorf("advance schema_info to version 2: %w", result.Error)
}
if result.RowsAffected != 1 {
return fmt.Errorf("update schema_info: version changed concurrently")
return fmt.Errorf("advance schema_info to version 2: version changed concurrently")
}
return nil
return migrateSchemaV2ToV3(tx)
})
default:
return fmt.Errorf(
@@ -204,7 +206,7 @@ func autoMigrate(
}
func migrateCurrentSchema(db *gorm.DB) error {
if err := db.AutoMigrate(
currentTables := []any{
&models.Group{},
&models.UpstreamKey{},
&models.AccessKey{},
@@ -214,73 +216,34 @@ func migrateCurrentSchema(db *gorm.DB) error {
&models.SystemSetting{},
&models.Job{},
&models.ControlOperation{},
&schemaInfo{},
); err != nil {
return fmt.Errorf("auto-migrate SQLite schema: %w", err)
}
if err := rebuildModelPricesIfNeeded(db); err != nil {
return err
}
return nil
}
func rebuildModelPricesIfNeeded(db *gorm.DB) error {
if !db.Migrator().HasTable(&models.ModelPrice{}) {
return nil
}
type columnInfo struct {
Name string
NotNull int `gorm:"column:notnull"`
}
var columns []columnInfo
if err := db.Raw("PRAGMA table_info('model_prices')").Scan(&columns).Error; err != nil {
return fmt.Errorf("inspect model_prices columns: %w", err)
}
for _, column := range columns {
switch column.Name {
case "input_price", "output_price", "cache_read_price", "cache_write_5m_price", "cache_write_1h_price":
if column.NotNull != 0 {
return rebuildModelPrices(db)
}
existingTables := 0
for _, table := range currentTables {
if db.Migrator().HasTable(table) {
existingTables++
}
}
return nil
}
if existingTables != 0 {
if existingTables != len(currentTables) {
return fmt.Errorf(
"validate SQLite schema version 3: found %d of %d required tables",
existingTables,
len(currentTables),
)
}
return validateSchemaV3(db)
}
func rebuildModelPrices(db *gorm.DB) error {
if err := db.Exec(`CREATE TABLE model_prices__m4_rebuild (
id integer PRIMARY KEY AUTOINCREMENT,
pattern varchar(255) NOT NULL,
input_price real,
output_price real,
cache_read_price real,
cache_write_5m_price real,
cache_write_1h_price real,
source varchar(32) NOT NULL,
created_at datetime,
updated_at datetime,
CONSTRAINT chk_model_price_source CHECK (source = 'user')
)`).Error; err != nil {
return fmt.Errorf("create rebuilt model_prices table: %w", err)
if err := createSchemaV3Tables(db, ""); err != nil {
return err
}
if err := db.Exec(`INSERT INTO model_prices__m4_rebuild (
id, pattern, input_price, output_price, cache_read_price, cache_write_5m_price, cache_write_1h_price, source, created_at, updated_at
)
SELECT id, pattern, input_price, output_price, cache_read_price, cache_write_5m_price, cache_write_1h_price, source, created_at, updated_at
FROM model_prices`).Error; err != nil {
return fmt.Errorf("copy model_prices into rebuilt table: %w", err)
if err := createSchemaV3InfoTable(db, "schema_info"); err != nil {
return err
}
if err := db.Exec("DROP TABLE model_prices").Error; err != nil {
return fmt.Errorf("drop previous model_prices table: %w", err)
if err := createSchemaV3Indexes(db); err != nil {
return err
}
if err := db.Exec("ALTER TABLE model_prices__m4_rebuild RENAME TO model_prices").Error; err != nil {
return fmt.Errorf("rename rebuilt model_prices table: %w", err)
}
if err := db.Exec("CREATE UNIQUE INDEX idx_model_prices_pattern ON model_prices(pattern)").Error; err != nil {
return fmt.Errorf("create rebuilt model_prices Pattern index: %w", err)
}
return nil
return validateSchemaV3(db)
}
func readSchemaVersion(db *gorm.DB) (uint, error) {
+65 -39
View File
@@ -514,7 +514,7 @@ func TestOpenConfiguresParameterizedSQLLogging(t *testing.T) {
}
}
func TestAutoMigrateCreatesNineTablesAndSchemaVersion(t *testing.T) {
func TestAutoMigrateCreatesTenTablesAndSchemaVersion(t *testing.T) {
t.Parallel()
db := openMigratedDatabase(t)
@@ -528,6 +528,7 @@ func TestAutoMigrateCreatesNineTablesAndSchemaVersion(t *testing.T) {
"model_prices",
"system_settings",
"jobs",
"control_operations",
"schema_info",
}
for _, table := range wantTables {
@@ -556,7 +557,7 @@ func TestAutoMigrateCreatesNineTablesAndSchemaVersion(t *testing.T) {
}
}
func TestAutoMigrateCreatesRequestLogV1FieldsAndCompositeIndexes(t *testing.T) {
func TestAutoMigrateCreatesRequestLogV3FieldsAndCompositeIndexes(t *testing.T) {
t.Parallel()
dsn := filepath.Join(t.TempDir(), "fresh-request-log-v1.db")
@@ -610,23 +611,23 @@ func TestAutoMigrateCreatesRequestLogV1FieldsAndCompositeIndexes(t *testing.T) {
name string
desc int
}{
"idx_request_logs_created_id": {
{name: "created_at", desc: 1},
"idx_request_logs_completed_id": {
{name: "completed_at_ms", desc: 1},
{name: "id", desc: 1},
},
"idx_request_logs_access_created_id": {
"idx_request_logs_access_completed_id": {
{name: "access_key_id"},
{name: "created_at", desc: 1},
{name: "completed_at_ms", desc: 1},
{name: "id", desc: 1},
},
"idx_request_logs_status_created_id": {
"idx_request_logs_status_completed_id": {
{name: "status"},
{name: "created_at", desc: 1},
{name: "completed_at_ms", desc: 1},
{name: "id", desc: 1},
},
"idx_request_logs_model_created_id": {
"idx_request_logs_model_completed_id": {
{name: "client_model"},
{name: "created_at", desc: 1},
{name: "completed_at_ms", desc: 1},
{name: "id", desc: 1},
},
}
@@ -664,8 +665,8 @@ func TestAutoMigrateCreatesRequestLogV1FieldsAndCompositeIndexes(t *testing.T) {
}
}
if storage.CurrentSchemaVersion != 2 {
t.Fatalf("CurrentSchemaVersion = %d, want 2", storage.CurrentSchemaVersion)
if storage.CurrentSchemaVersion != 3 {
t.Fatalf("CurrentSchemaVersion = %d, want 3", storage.CurrentSchemaVersion)
}
}
@@ -680,7 +681,7 @@ type m4IndexInfo struct {
Unique int
}
func TestAutoMigrateCreatesM4UsagePricingColumnsAndConstraints(t *testing.T) {
func TestAutoMigrateCreatesSchemaV3UsagePricingColumnsAndConstraints(t *testing.T) {
t.Parallel()
db := openMigratedDatabase(t)
@@ -748,14 +749,14 @@ func TestAutoMigrateCreatesM4UsagePricingColumnsAndConstraints(t *testing.T) {
if err := db.Raw("PRAGMA index_list('usage_stats')").Scan(&usageIndexes).Error; err != nil {
t.Fatalf("inspect usage_stats indexes: %v", err)
}
if !hasUniqueIndex(usageIndexes, "idx_usage_stats_hour_group_model") {
t.Error("usage_stats unique hour/group/model index is missing")
if !hasUniqueIndex(usageIndexes, "idx_usage_stats_bucket_access_group_model") {
t.Error("usage_stats unique bucket/access/group/model index is missing")
}
assertM4IndexKeyColumns(
t,
db,
"idx_usage_stats_hour_group_model",
[]string{"hour_bucket", "group_id", "model"},
"idx_usage_stats_bucket_access_group_model",
[]string{"bucket_start_ms", "access_key_id", "group_id", "model"},
)
var priceColumns []m4ColumnInfo
@@ -763,7 +764,11 @@ func TestAutoMigrateCreatesM4UsagePricingColumnsAndConstraints(t *testing.T) {
t.Fatalf("inspect model_prices columns: %v", err)
}
priceColumnNames := map[string]struct{}{
"input_price": {}, "output_price": {}, "cache_read_price": {}, "cache_write_5m_price": {}, "cache_write_1h_price": {},
"input_price_nano_usd_per_million_tokens": {},
"output_price_nano_usd_per_million_tokens": {},
"cache_read_price_nano_usd_per_million_tokens": {},
"cache_write_5m_price_nano_usd_per_million_tokens": {},
"cache_write_1h_price_nano_usd_per_million_tokens": {},
}
for _, column := range priceColumns {
if _, ok := priceColumnNames[column.Name]; ok && column.NotNull != 0 {
@@ -805,8 +810,8 @@ func TestAutoMigrateCreatesM4UsagePricingColumnsAndConstraints(t *testing.T) {
t.Error("builtin ModelPrice Source was accepted")
}
if storage.CurrentSchemaVersion != 2 {
t.Errorf("CurrentSchemaVersion = %d, want 2", storage.CurrentSchemaVersion)
if storage.CurrentSchemaVersion != 3 {
t.Errorf("CurrentSchemaVersion = %d, want 3", storage.CurrentSchemaVersion)
}
if err := storage.AutoMigrate(db); err != nil {
t.Fatalf("second AutoMigrate() error = %v", err)
@@ -844,7 +849,7 @@ func assertM4IndexKeyColumns(
}
}
func TestAutoMigrateUpgradesCurrentModelPriceDDLWithoutRenamingColumns(t *testing.T) {
func TestAutoMigrateUpgradesV1ModelPriceDDLToSchemaV3FixedPoint(t *testing.T) {
t.Parallel()
db, err := storage.Open(":memory:")
@@ -883,9 +888,14 @@ updated_at datetime
if err := db.Exec(`CREATE INDEX idx_model_prices_pattern ON model_prices(pattern)`).Error; err != nil {
t.Fatalf("create current model_prices Pattern index: %v", err)
}
if err := db.Exec(`INSERT INTO model_prices(pattern, input_price, output_price, cache_read_price, cache_write_5m_price, cache_write_1h_price, source) VALUES
('nonzero', 1.25, 2.5, 3.75, 5, 6.25, 'user'),
('zero', 0, 0, 0, 0, 0, 'user')`).Error; err != nil {
if err := db.Exec(`INSERT INTO model_prices(
pattern, input_price, output_price, cache_read_price,
cache_write_5m_price, cache_write_1h_price, source, created_at, updated_at
) VALUES
('nonzero', 1.25, 2.5, 3.75, 5, 6.25, 'user',
'2026-07-30T00:00:00Z', '2026-07-30T00:00:00Z'),
('zero', 0, 0, 0, 0, 0, 'user',
'2026-07-30T00:00:00Z', '2026-07-30T00:00:00Z')`).Error; err != nil {
t.Fatalf("insert current model_prices rows: %v", err)
}
@@ -910,31 +920,46 @@ func assertModelPriceUpgrade(t *testing.T, db *gorm.DB) {
t.Fatalf("inspect upgraded model_prices columns: %v", err)
}
for _, column := range columns {
if strings.Contains(column.Name, "uncached_input") {
t.Fatalf("model_prices has unexpected renamed price column %q", column.Name)
}
if column.Name == "input_price" || column.Name == "output_price" || column.Name == "cache_read_price" || column.Name == "cache_write_5m_price" || column.Name == "cache_write_1h_price" {
if strings.HasSuffix(column.Name, "_nano_usd_per_million_tokens") {
if column.NotNull != 0 {
t.Errorf("model_prices.%s notnull = %d, want nullable", column.Name, column.NotNull)
}
}
for _, legacy := range []string{
"input_price", "output_price", "cache_read_price",
"cache_write_5m_price", "cache_write_1h_price",
} {
if column.Name == legacy {
t.Fatalf("model_prices retains legacy float column %q", column.Name)
}
}
}
type row struct {
Pattern string
InputPrice float64 `gorm:"column:input_price"`
OutputPrice float64 `gorm:"column:output_price"`
CacheReadPrice float64 `gorm:"column:cache_read_price"`
CacheWrite5MPrice float64 `gorm:"column:cache_write_5m_price"`
CacheWrite1HPrice float64 `gorm:"column:cache_write_1h_price"`
InputPrice int64 `gorm:"column:input_price_nano_usd_per_million_tokens"`
OutputPrice int64 `gorm:"column:output_price_nano_usd_per_million_tokens"`
CacheReadPrice int64 `gorm:"column:cache_read_price_nano_usd_per_million_tokens"`
CacheWrite5MPrice int64 `gorm:"column:cache_write_5m_price_nano_usd_per_million_tokens"`
CacheWrite1HPrice int64 `gorm:"column:cache_write_1h_price_nano_usd_per_million_tokens"`
}
var rows []row
if err := db.Raw("SELECT pattern, input_price, output_price, cache_read_price, cache_write_5m_price, cache_write_1h_price FROM model_prices ORDER BY pattern").Scan(&rows).Error; err != nil {
if err := db.Raw(`SELECT pattern,
input_price_nano_usd_per_million_tokens,
output_price_nano_usd_per_million_tokens,
cache_read_price_nano_usd_per_million_tokens,
cache_write_5m_price_nano_usd_per_million_tokens,
cache_write_1h_price_nano_usd_per_million_tokens
FROM model_prices ORDER BY pattern`).Scan(&rows).Error; err != nil {
t.Fatalf("read upgraded model_prices: %v", err)
}
if len(rows) != 2 {
t.Fatalf("upgraded model_prices rows = %d, want 2", len(rows))
}
if rows[0] != (row{Pattern: "nonzero", InputPrice: 1.25, OutputPrice: 2.5, CacheReadPrice: 3.75, CacheWrite5MPrice: 5, CacheWrite1HPrice: 6.25}) {
if rows[0] != (row{
Pattern: "nonzero", InputPrice: 1_250_000_000,
OutputPrice: 2_500_000_000, CacheReadPrice: 3_750_000_000,
CacheWrite5MPrice: 5_000_000_000, CacheWrite1HPrice: 6_250_000_000,
}) {
t.Errorf("nonzero upgraded row = %+v", rows[0])
}
if rows[1] != (row{Pattern: "zero"}) {
@@ -1095,12 +1120,13 @@ func TestAutoMigrateCreatesCriticalUniqueConstraints(t *testing.T) {
assertDuplicateRejected(t, db.Create(&first).Error, db.Create(&second).Error)
})
t.Run("usage hour group and model", func(t *testing.T) {
t.Run("usage bucket access group and model", func(t *testing.T) {
bucket := time.Date(2026, time.July, 15, 12, 0, 0, 0, time.UTC)
first := models.UsageStat{
HourBucket: bucket,
GroupID: 202,
Model: "model-a",
BucketStartMS: bucket.UnixMilli(),
AccessKeyID: 101,
GroupID: 202,
Model: "model-a",
}
second := first
second.ID = 0
+12 -14
View File
@@ -1,19 +1,17 @@
package models
import "time"
// AccessKey is an encrypted client credential and its persisted access policy.
type AccessKey struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(255);not null"`
KeyValue string `gorm:"type:text;not null"`
KeyHash string `gorm:"type:varchar(128);not null;uniqueIndex"`
KeySuffix string `gorm:"type:char(4);not null;check:chk_access_key_suffix,key_suffix GLOB '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]'"`
Status string `gorm:"type:varchar(32);not null;default:'active';check:chk_access_key_status,status IN ('active','disabled')"`
Filters JSON `gorm:"type:json"`
RPMLimit int64 `gorm:"not null;default:0"`
DailyCostLimit float64 `gorm:"not null;default:0"`
MonthlyCostLimit float64 `gorm:"not null;default:0"`
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(255);not null"`
KeyValue string `gorm:"type:text;not null"`
KeyHash string `gorm:"type:varchar(128);not null;uniqueIndex"`
KeySuffix string `gorm:"type:char(4);not null;check:chk_access_key_suffix,key_suffix GLOB '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]'"`
Status string `gorm:"type:varchar(32);not null;default:'active';check:chk_access_key_status,status IN ('active','disabled')"`
Filters JSON `gorm:"type:json"`
RPMLimit int64 `gorm:"not null;default:0"`
DailyCostLimitNanoUSD int64 `gorm:"column:daily_cost_limit_nano_usd;not null;default:0;check:chk_access_key_daily_cost_limit_nano,daily_cost_limit_nano_usd >= 0"`
MonthlyCostLimitNanoUSD int64 `gorm:"column:monthly_cost_limit_nano_usd;not null;default:0;check:chk_access_key_monthly_cost_limit_nano,monthly_cost_limit_nano_usd >= 0"`
CreatedAtMS int64 `gorm:"column:created_at_ms;not null;autoCreateTime:milli"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;not null;autoUpdateTime:milli"`
}
+15 -17
View File
@@ -1,23 +1,21 @@
package models
import "time"
// ControlOperation durably identifies a create/import mutation and its
// post-commit side-effect progress. It never stores plaintext credentials.
type ControlOperation struct {
CommitSequence uint64 `gorm:"primaryKey;autoIncrement"`
OperationID string `gorm:"type:char(36);not null;uniqueIndex"`
IdempotencyKey string `gorm:"type:char(36);not null;uniqueIndex"`
DigestVersion uint `gorm:"not null;check:chk_control_operation_digest_version,digest_version > 0"`
RequestDigest []byte `gorm:"type:blob;not null;check:chk_control_operation_digest,length(request_digest) = 32"`
OperationKind string `gorm:"type:varchar(32);not null"`
ResourceIdentity string `gorm:"type:varchar(64);not null"`
CanonicalResult []byte `gorm:"type:blob"`
RequiredStages JSON `gorm:"type:json"`
LastCompletedStage string `gorm:"type:varchar(32)"`
FailedStage string `gorm:"type:varchar(32)"`
CompletedAt *time.Time `gorm:"index"`
CompactedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
CommitSequence uint64 `gorm:"primaryKey;autoIncrement"`
OperationID string `gorm:"type:char(36);not null;uniqueIndex"`
IdempotencyKey string `gorm:"type:char(36);not null;uniqueIndex"`
DigestVersion uint `gorm:"not null;check:chk_control_operation_digest_version,digest_version > 0"`
RequestDigest []byte `gorm:"type:blob;not null;check:chk_control_operation_digest,length(request_digest) = 32"`
OperationKind string `gorm:"type:varchar(32);not null"`
ResourceIdentity string `gorm:"type:varchar(64);not null"`
CanonicalResult []byte `gorm:"type:blob"`
RequiredStages JSON `gorm:"type:json"`
LastCompletedStage string `gorm:"type:varchar(32)"`
FailedStage string `gorm:"type:varchar(32)"`
CompletedAtMS *int64 `gorm:"column:completed_at_ms;index"`
CompactedAtMS *int64 `gorm:"column:compacted_at_ms"`
CreatedAtMS int64 `gorm:"column:created_at_ms;not null;autoCreateTime:milli"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;not null;autoUpdateTime:milli"`
}
+4 -9
View File
@@ -1,7 +1,5 @@
package models
import "time"
// UpstreamKeyStatus is the durable operator-controlled state of a credential.
// Runtime cooldown and failure state belongs to state.KeyRegistry.
type UpstreamKeyStatus string
@@ -25,8 +23,8 @@ type Group struct {
Config JSON `gorm:"type:json"`
Enabled bool `gorm:"not null;default:true"`
UpstreamKeys []UpstreamKey `gorm:"foreignKey:GroupID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
CreatedAt time.Time
UpdatedAt time.Time
CreatedAtMS int64 `gorm:"column:created_at_ms;not null;autoCreateTime:milli"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;not null;autoUpdateTime:milli"`
}
// UpstreamKey is an encrypted provider credential that belongs to one group.
@@ -37,9 +35,6 @@ type UpstreamKey struct {
KeyHash string `gorm:"type:varchar(128);not null;uniqueIndex:idx_upstream_keys_group_hash,priority:2"`
Status UpstreamKeyStatus `gorm:"type:varchar(32);not null;default:'active';check:chk_upstream_key_status,status IN ('active','disabled')"`
WeightManual *int
RequestCount int64 `gorm:"not null;default:0"`
TokensTotal int64 `gorm:"not null;default:0"`
CostTotal float64 `gorm:"not null;default:0"`
CreatedAt time.Time
UpdatedAt time.Time
CreatedAtMS int64 `gorm:"column:created_at_ms;not null;autoCreateTime:milli"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;not null;autoUpdateTime:milli"`
}
+10 -12
View File
@@ -1,17 +1,15 @@
package models
import "time"
// ModelPrice stores prices in USD per one million tokens for an upstream model pattern.
type ModelPrice struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Pattern string `gorm:"type:varchar(255);not null;uniqueIndex"`
InputPrice *float64 `gorm:"column:input_price"`
OutputPrice *float64 `gorm:"column:output_price"`
CacheReadPrice *float64 `gorm:"column:cache_read_price"`
CacheWrite5MPrice *float64 `gorm:"column:cache_write_5m_price"`
CacheWrite1HPrice *float64 `gorm:"column:cache_write_1h_price"`
Source string `gorm:"type:varchar(32);not null;check:chk_model_price_source,source = 'user'"`
CreatedAt time.Time
UpdatedAt time.Time
ID uint `gorm:"primaryKey;autoIncrement"`
Pattern string `gorm:"type:varchar(255);not null;uniqueIndex"`
InputPriceNanoUSDPerMillionTokens *int64 `gorm:"column:input_price_nano_usd_per_million_tokens;check:chk_model_price_input_nano,input_price_nano_usd_per_million_tokens IS NULL OR input_price_nano_usd_per_million_tokens >= 0"`
OutputPriceNanoUSDPerMillionTokens *int64 `gorm:"column:output_price_nano_usd_per_million_tokens;check:chk_model_price_output_nano,output_price_nano_usd_per_million_tokens IS NULL OR output_price_nano_usd_per_million_tokens >= 0"`
CacheReadPriceNanoUSDPerMillionTokens *int64 `gorm:"column:cache_read_price_nano_usd_per_million_tokens;check:chk_model_price_cache_read_nano,cache_read_price_nano_usd_per_million_tokens IS NULL OR cache_read_price_nano_usd_per_million_tokens >= 0"`
CacheWrite5MPriceNanoUSDPerMillionTokens *int64 `gorm:"column:cache_write_5m_price_nano_usd_per_million_tokens;check:chk_model_price_cache_write_5m_nano,cache_write_5m_price_nano_usd_per_million_tokens IS NULL OR cache_write_5m_price_nano_usd_per_million_tokens >= 0"`
CacheWrite1HPriceNanoUSDPerMillionTokens *int64 `gorm:"column:cache_write_1h_price_nano_usd_per_million_tokens;check:chk_model_price_cache_write_1h_nano,cache_write_1h_price_nano_usd_per_million_tokens IS NULL OR cache_write_1h_price_nano_usd_per_million_tokens >= 0"`
Source string `gorm:"type:varchar(32);not null;check:chk_model_price_source,source = 'user'"`
CreatedAtMS int64 `gorm:"column:created_at_ms;not null;autoCreateTime:milli"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;not null;autoUpdateTime:milli"`
}
+40 -41
View File
@@ -1,49 +1,48 @@
package models
import "time"
// RequestLog is the durable request-level audit and usage record.
type RequestLog struct {
ID string `gorm:"type:varchar(36);primaryKey;not null;index:idx_request_logs_created_id,priority:2,sort:desc;index:idx_request_logs_access_created_id,priority:3,sort:desc;index:idx_request_logs_status_created_id,priority:3,sort:desc;index:idx_request_logs_model_created_id,priority:3,sort:desc"`
CreatedAt time.Time `gorm:"not null;index:idx_request_logs_created_id,priority:1,sort:desc;index:idx_request_logs_access_created_id,priority:2,sort:desc;index:idx_request_logs_status_created_id,priority:2,sort:desc;index:idx_request_logs_model_created_id,priority:2,sort:desc"`
AccessKeyID uint `gorm:"not null;index:idx_request_logs_access_created_id,priority:1"`
GroupID uint `gorm:"not null;default:0"`
Protocol string `gorm:"type:varchar(32);not null"`
ClientModel string `gorm:"type:varchar(255);not null;index:idx_request_logs_model_created_id,priority:1"`
UpstreamModel string `gorm:"type:varchar(255);not null"`
Status string `gorm:"type:varchar(32);not null;index:idx_request_logs_status_created_id,priority:1"`
StatusCode int `gorm:"not null"`
DurationMs int64 `gorm:"not null"`
ErrorCode string `gorm:"type:varchar(64);not null;default:''"`
ErrorSummary string `gorm:"type:text;not null;default:''"`
AffinityHit bool `gorm:"not null;default:false"`
InputTokens int64 `gorm:"not null;default:0"`
OutputTokens int64 `gorm:"not null;default:0"`
CacheReadTokens int64 `gorm:"not null;default:0"`
CacheWrite5MTokens int64 `gorm:"column:cache_write_5m_tokens;not null;default:0"`
CacheWrite1HTokens int64 `gorm:"column:cache_write_1h_tokens;not null;default:0"`
Cost float64 `gorm:"not null;default:0"`
UsageState string `gorm:"type:varchar(32);not null;default:'not_applicable';check:chk_request_log_usage_state,usage_state IN ('complete','partial','missing','not_applicable')"`
CostState string `gorm:"type:varchar(32);not null;default:'not_applicable';check:chk_request_log_cost_state,cost_state IN ('priced','unpriced','not_applicable')"`
Attempts JSON `gorm:"type:json"`
ID string `gorm:"type:varchar(36);primaryKey;not null;index:idx_request_logs_completed_id,priority:2,sort:desc;index:idx_request_logs_access_completed_id,priority:3,sort:desc;index:idx_request_logs_status_completed_id,priority:3,sort:desc;index:idx_request_logs_model_completed_id,priority:3,sort:desc"`
CompletedAtMS int64 `gorm:"column:completed_at_ms;not null;index:idx_request_logs_completed_id,priority:1,sort:desc;index:idx_request_logs_access_completed_id,priority:2,sort:desc;index:idx_request_logs_status_completed_id,priority:2,sort:desc;index:idx_request_logs_model_completed_id,priority:2,sort:desc"`
AccessKeyID uint `gorm:"not null;index:idx_request_logs_access_completed_id,priority:1"`
GroupID uint `gorm:"not null;default:0"`
Protocol string `gorm:"type:varchar(32);not null"`
ClientModel string `gorm:"type:varchar(255);not null;index:idx_request_logs_model_completed_id,priority:1"`
UpstreamModel string `gorm:"type:varchar(255);not null"`
Status string `gorm:"type:varchar(32);not null;index:idx_request_logs_status_completed_id,priority:1"`
StatusCode int `gorm:"not null"`
DurationMs int64 `gorm:"not null"`
ErrorCode string `gorm:"type:varchar(64);not null;default:''"`
ErrorSummary string `gorm:"type:text;not null;default:''"`
AffinityHit bool `gorm:"not null;default:false"`
UncachedInputTokens int64 `gorm:"column:uncached_input_tokens;not null;default:0"`
OutputTokens int64 `gorm:"not null;default:0"`
CacheReadTokens int64 `gorm:"not null;default:0"`
CacheWrite5MTokens int64 `gorm:"column:cache_write_5m_tokens;not null;default:0"`
CacheWrite1HTokens int64 `gorm:"column:cache_write_1h_tokens;not null;default:0"`
EstimatedCostNanoUSD int64 `gorm:"column:estimated_cost_nano_usd;not null;default:0;check:chk_request_log_cost_nano,estimated_cost_nano_usd >= 0"`
UsageState string `gorm:"type:varchar(32);not null;default:'not_applicable';check:chk_request_log_usage_state,usage_state IN ('complete','partial','missing','not_applicable')"`
CostState string `gorm:"type:varchar(32);not null;default:'not_applicable';check:chk_request_log_cost_state,cost_state IN ('priced','unpriced','not_applicable')"`
Attempts JSON `gorm:"type:json"`
}
// UsageStat is an hourly aggregate by upstream group and upstream model.
// UsageStat is an hourly aggregate by access key, upstream group, and client model.
type UsageStat struct {
ID uint `gorm:"primaryKey;autoIncrement"`
HourBucket time.Time `gorm:"not null;uniqueIndex:idx_usage_stats_hour_group_model,priority:1"`
GroupID uint `gorm:"not null;uniqueIndex:idx_usage_stats_hour_group_model,priority:2"`
Model string `gorm:"type:varchar(255);not null;uniqueIndex:idx_usage_stats_hour_group_model,priority:3"`
RequestCount int64 `gorm:"not null;default:0"`
SuccessCount int64 `gorm:"not null;default:0"`
FailureCount int64 `gorm:"not null;default:0"`
InputTokens int64 `gorm:"not null;default:0"`
OutputTokens int64 `gorm:"not null;default:0"`
CacheReadTokens int64 `gorm:"not null;default:0"`
CacheWrite5MTokens int64 `gorm:"column:cache_write_5m_tokens;not null;default:0"`
CacheWrite1HTokens int64 `gorm:"column:cache_write_1h_tokens;not null;default:0"`
Cost float64 `gorm:"not null;default:0"`
UsageMissingCount int64 `gorm:"not null;default:0"`
PartialCount int64 `gorm:"not null;default:0"`
UnpricedRequestCount int64 `gorm:"not null;default:0"`
ID uint `gorm:"primaryKey;autoIncrement"`
BucketStartMS int64 `gorm:"column:bucket_start_ms;not null;uniqueIndex:idx_usage_stats_bucket_access_group_model,priority:1"`
AccessKeyID uint `gorm:"not null;uniqueIndex:idx_usage_stats_bucket_access_group_model,priority:2"`
GroupID uint `gorm:"not null;uniqueIndex:idx_usage_stats_bucket_access_group_model,priority:3"`
Model string `gorm:"type:varchar(255);not null;uniqueIndex:idx_usage_stats_bucket_access_group_model,priority:4"`
RequestCount int64 `gorm:"not null;default:0"`
SuccessCount int64 `gorm:"not null;default:0"`
FailureCount int64 `gorm:"not null;default:0"`
UncachedInputTokens int64 `gorm:"column:uncached_input_tokens;not null;default:0"`
OutputTokens int64 `gorm:"not null;default:0"`
CacheReadTokens int64 `gorm:"not null;default:0"`
CacheWrite5MTokens int64 `gorm:"column:cache_write_5m_tokens;not null;default:0"`
CacheWrite1HTokens int64 `gorm:"column:cache_write_1h_tokens;not null;default:0"`
EstimatedCostNanoUSD int64 `gorm:"not null;default:0;check:chk_usage_stat_cost_nano,estimated_cost_nano_usd >= 0"`
UsageMissingCount int64 `gorm:"not null;default:0"`
PartialCount int64 `gorm:"not null;default:0"`
UnpricedRequestCount int64 `gorm:"not null;default:0"`
}
+12 -14
View File
@@ -1,27 +1,25 @@
package models
import "time"
// InternalSystemSettingPrefix reserves persistence metadata that must never
// become user-visible runtime configuration.
const InternalSystemSettingPrefix = "_internal."
// SystemSetting stores a dynamically configurable setting as a key-value pair.
type SystemSetting struct {
Key string `gorm:"type:varchar(255);primaryKey;not null"`
Value string `gorm:"type:text;not null"`
UpdatedAt time.Time
Key string `gorm:"type:varchar(255);primaryKey;not null"`
Value string `gorm:"type:text;not null"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;not null;autoUpdateTime:milli"`
}
// Job is one durable background operation and its execution history.
type Job struct {
ID string `gorm:"type:varchar(36);primaryKey;not null"`
Type string `gorm:"type:varchar(64);not null;index"`
Status string `gorm:"type:varchar(32);not null;default:'pending';index"`
Payload JSON `gorm:"type:json"`
Result JSON `gorm:"type:json"`
Error string `gorm:"type:text"`
CreatedAt time.Time `gorm:"not null;index"`
StartedAt *time.Time
FinishedAt *time.Time
ID string `gorm:"type:varchar(36);primaryKey;not null"`
Type string `gorm:"type:varchar(64);not null;index"`
Status string `gorm:"type:varchar(32);not null;default:'pending';index"`
Payload JSON `gorm:"type:json"`
Result JSON `gorm:"type:json"`
Error string `gorm:"type:text"`
CreatedAtMS int64 `gorm:"column:created_at_ms;not null;index;autoCreateTime:milli"`
StartedAtMS *int64 `gorm:"column:started_at_ms"`
FinishedAtMS *int64 `gorm:"column:finished_at_ms"`
}
+8 -7
View File
@@ -14,10 +14,10 @@ import (
"gpt-load/internal/storage/models"
)
func TestAutoMigrateCreatesSchemaV2SecurityTables(t *testing.T) {
func TestAutoMigrateCreatesSchemaV3SecurityTables(t *testing.T) {
db := openMigratedDatabase(t)
if storage.CurrentSchemaVersion != 2 {
t.Fatalf("CurrentSchemaVersion = %d, want 2", storage.CurrentSchemaVersion)
if storage.CurrentSchemaVersion != 3 {
t.Fatalf("CurrentSchemaVersion = %d, want 3", storage.CurrentSchemaVersion)
}
type columnInfo struct {
@@ -91,10 +91,11 @@ func TestAutoMigrateUpgradesV1AccessKeyWithBackups(t *testing.T) {
t.Fatalf("Encrypt() error = %v", err)
}
if err := db.Exec(`INSERT INTO access_keys
(name,key_value,key_hash,status,filters,rpm_limit,daily_cost_limit,monthly_cost_limit)
VALUES (?,?,?,?,?,?,?,?)`,
(name,key_value,key_hash,status,filters,rpm_limit,daily_cost_limit,monthly_cost_limit,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
"legacy", ciphertext, keyService.Hash(plaintext), "active",
`{"groups":[],"protocols":[],"models":[]}`, 0, 0, 0,
"2026-07-30T00:00:00Z", "2026-07-30T00:00:00Z",
).Error; err != nil {
t.Fatalf("insert legacy AccessKey: %v", err)
}
@@ -114,8 +115,8 @@ func TestAutoMigrateUpgradesV1AccessKeyWithBackups(t *testing.T) {
if err := db.Raw("SELECT version FROM schema_info").Scan(&version).Error; err != nil {
t.Fatalf("read schema version: %v", err)
}
if version != 2 {
t.Fatalf("schema version = %d, want 2", version)
if version != 3 {
t.Fatalf("schema version = %d, want 3", version)
}
assertSingleSecureBackup(t, dbPath+".schema-v1-backup-*")
assertSingleSecureBackup(t, keyPath+".schema-v1-backup-*")
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+210
View File
@@ -0,0 +1,210 @@
package storage
import (
"fmt"
"strings"
"gorm.io/gorm"
)
const schemaV3InfoTableSQL = `CREATE TABLE schema_info (
version integer PRIMARY KEY NOT NULL,
CONSTRAINT chk_schema_info_version CHECK (version = 3)
)`
func createSchemaV3InfoTable(db *gorm.DB, table string) error {
if table != "schema_info" && table != "schema_info__v3" {
return fmt.Errorf("create schema v3 info table: unsupported name %q", table)
}
statement := strings.Replace(schemaV3InfoTableSQL, "schema_info", table, 1)
if err := db.Exec(statement).Error; err != nil {
return fmt.Errorf("create schema v3 info table: %w", err)
}
return nil
}
func replaceSchemaV3InfoTable(db *gorm.DB) error {
if err := createSchemaV3InfoTable(db, "schema_info__v3"); err != nil {
return err
}
if err := db.Exec(`INSERT INTO schema_info__v3(version)
SELECT version FROM schema_info`).Error; err != nil {
return fmt.Errorf("copy schema_info to schema v3: %w", err)
}
if err := db.Exec(`DROP TABLE schema_info`).Error; err != nil {
return fmt.Errorf("drop schema v2 schema_info: %w", err)
}
if err := db.Exec(`ALTER TABLE schema_info__v3 RENAME TO schema_info`).Error; err != nil {
return fmt.Errorf("rename schema v3 schema_info: %w", err)
}
return nil
}
func validateSchemaV3(db *gorm.DB) error {
requiredColumns := map[string][]string{
"groups": {"created_at_ms", "updated_at_ms"},
"upstream_keys": {"group_id", "created_at_ms", "updated_at_ms"},
"access_keys": {"daily_cost_limit_nano_usd", "monthly_cost_limit_nano_usd", "created_at_ms", "updated_at_ms"},
"request_logs": {"completed_at_ms", "duration_ms", "estimated_cost_nano_usd", "usage_state", "cost_state"},
"usage_stats": {"bucket_start_ms", "request_count", "success_count", "failure_count", "estimated_cost_nano_usd"},
"model_prices": {"input_price_nano_usd_per_million_tokens", "output_price_nano_usd_per_million_tokens"},
"system_settings": {"updated_at_ms"},
"jobs": {"created_at_ms", "started_at_ms", "finished_at_ms"},
"control_operations": {"created_at_ms", "updated_at_ms", "completed_at_ms", "compacted_at_ms"},
"schema_info": {"version"},
}
for table, columns := range requiredColumns {
for _, column := range columns {
if !db.Migrator().HasColumn(table, column) {
return fmt.Errorf(
"validate SQLite schema version 3: %s.%s is missing",
table,
column,
)
}
}
}
requiredTableFragments := map[string][]string{
"groups": {
"check (created_at_ms >= 0)",
"check (updated_at_ms >= 0)",
},
"upstream_keys": {
"check (created_at_ms >= 0)",
"check (updated_at_ms >= 0)",
"constraint chk_upstream_key_status check (status in ('active','disabled'))",
},
"access_keys": {
"constraint chk_access_key_suffix check (key_suffix glob '[0-9a-f][0-9a-f][0-9a-f][0-9a-f]')",
"constraint chk_access_key_status check (status in ('active','disabled'))",
"constraint chk_access_key_daily_cost_limit_nano check (daily_cost_limit_nano_usd >= 0)",
"constraint chk_access_key_monthly_cost_limit_nano check (monthly_cost_limit_nano_usd >= 0)",
},
"model_prices": {
"constraint chk_model_price_input_nano check",
"constraint chk_model_price_output_nano check",
"constraint chk_model_price_cache_read_nano check",
"constraint chk_model_price_cache_write_5m_nano check",
"constraint chk_model_price_cache_write_1h_nano check",
"constraint chk_model_price_source check (source = 'user')",
},
"request_logs": {
"check (completed_at_ms >= 0)",
"check (duration_ms >= 0)",
"check (uncached_input_tokens >= 0)",
"check (output_tokens >= 0)",
"check (cache_read_tokens >= 0)",
"check (cache_write_5m_tokens >= 0)",
"check (cache_write_1h_tokens >= 0)",
"constraint chk_request_log_cost_nano check (estimated_cost_nano_usd >= 0)",
"constraint chk_request_log_usage_state check (usage_state in ('complete','partial','missing','not_applicable'))",
"constraint chk_request_log_cost_state check (cost_state in ('priced','unpriced','not_applicable'))",
"constraint chk_request_log_usage_cost_state check",
"constraint chk_request_log_nonpriced_cost_zero check",
},
"usage_stats": {
"check (bucket_start_ms >= 0)",
"check (request_count >= 0)",
"check (success_count >= 0)",
"check (failure_count >= 0)",
"check (uncached_input_tokens >= 0)",
"check (output_tokens >= 0)",
"check (cache_read_tokens >= 0)",
"check (cache_write_5m_tokens >= 0)",
"check (cache_write_1h_tokens >= 0)",
"check (usage_missing_count >= 0)",
"check (partial_count >= 0)",
"check (unpriced_request_count >= 0)",
"constraint chk_usage_stat_cost_nano check (estimated_cost_nano_usd >= 0)",
"constraint chk_usage_stat_request_outcome check (request_count = success_count + failure_count)",
},
"system_settings": {
"check (updated_at_ms >= 0)",
},
"jobs": {
"check (created_at_ms >= 0)",
"check (started_at_ms is null or started_at_ms >= 0)",
"check (finished_at_ms is null or finished_at_ms >= 0)",
},
"control_operations": {
"check (completed_at_ms is null or completed_at_ms >= 0)",
"check (compacted_at_ms is null or compacted_at_ms >= 0)",
"check (created_at_ms >= 0)",
"check (updated_at_ms >= 0)",
"constraint chk_control_operation_digest_version check (digest_version > 0)",
"constraint chk_control_operation_digest check (length(request_digest) = 32)",
},
"schema_info": {
"constraint chk_schema_info_version check (version = 3)",
},
}
for table, fragments := range requiredTableFragments {
statement, err := sqliteSchemaSQL(db, "table", table)
if err != nil {
return err
}
normalized := normalizeSQLiteDDL(statement)
for _, fragment := range fragments {
if !strings.Contains(normalized, normalizeSQLiteDDL(fragment)) {
return fmt.Errorf(
"validate SQLite schema version 3: %s is missing constraint %q",
table,
fragment,
)
}
}
}
for _, expected := range schemaV3IndexStatements() {
fields := strings.Fields(expected)
if len(fields) < 4 {
return fmt.Errorf("validate SQLite schema version 3: invalid canonical index DDL")
}
nameIndex := 2
if strings.EqualFold(fields[1], "unique") {
nameIndex = 3
}
name := fields[nameIndex]
actual, err := sqliteSchemaSQL(db, "index", name)
if err != nil {
return err
}
if normalizeSQLiteDDL(actual) != normalizeSQLiteDDL(expected) {
return fmt.Errorf(
"validate SQLite schema version 3: critical index %s differs",
name,
)
}
}
return validateSchemaV3ForeignKeys(db)
}
func sqliteSchemaSQL(db *gorm.DB, objectType, name string) (string, error) {
var statement string
result := db.Raw(
`SELECT sql FROM sqlite_master WHERE type = ? AND name = ?`,
objectType,
name,
).Scan(&statement)
if result.Error != nil {
return "", fmt.Errorf(
"validate SQLite schema version 3: read %s %s: %w",
objectType,
name,
result.Error,
)
}
if result.RowsAffected != 1 || strings.TrimSpace(statement) == "" {
return "", fmt.Errorf(
"validate SQLite schema version 3: %s %s is missing",
objectType,
name,
)
}
return statement, nil
}
func normalizeSQLiteDDL(statement string) string {
return strings.ToLower(strings.Join(strings.Fields(statement), " "))
}
+36
View File
@@ -0,0 +1,36 @@
package usagecost
import (
"fmt"
"gpt-load/internal/pricing"
"gpt-load/internal/usage"
)
// Validate enforces the persisted usage/cost state matrix and amount invariant.
func Validate(
usageState usage.State,
costState pricing.CostState,
estimatedCostNanoUSD int64,
) error {
if estimatedCostNanoUSD < 0 {
return fmt.Errorf("invalid estimated cost")
}
validCombination := false
switch usageState {
case usage.StateComplete, usage.StatePartial:
validCombination = costState == pricing.CostStatePriced ||
costState == pricing.CostStateUnpriced
case usage.StateMissing:
validCombination = costState == pricing.CostStateUnpriced
case usage.StateNotApplicable:
validCombination = costState == pricing.CostStateNotApplicable
}
if !validCombination {
return fmt.Errorf("invalid usage and cost state combination")
}
if costState != pricing.CostStatePriced && estimatedCostNanoUSD != 0 {
return fmt.Errorf("unpriced or not-applicable cost must be zero")
}
return nil
}
+1
View File
@@ -4,6 +4,7 @@
{ "name": "home", "path": "/" },
{ "name": "login", "path": "/login" },
{ "name": "import", "path": "/import" },
{ "name": "groups", "path": "/groups" },
{ "name": "group-detail", "path": "/groups/:id" },
{ "name": "access-keys", "path": "/access-keys" },
{ "name": "monitor", "path": "/monitor" },
+1
View File
@@ -110,6 +110,7 @@ func TestEmbeddedPageRouteManifestContainsCurrentPages(t *testing.T) {
"home": "/",
"login": "/login",
"import": "/import",
"groups": "/groups",
"group-detail": "/groups/:id",
"access-keys": "/access-keys",
"monitor": "/monitor",
+21
View File
@@ -87,6 +87,27 @@ func TestServerServesModelPricesDeepLinkWithoutCatchingUnknownSettingsPaths(t *t
}
}
func TestServerServesGroupsCollectionWithoutCatchingUnknownNestedPaths(t *testing.T) {
engine := testEngine(newServer(fstest.MapFS{
"dist/index.html": &fstest.MapFile{Data: []byte("<!doctype html><title>groups</title>")},
}, "dist"))
groups := httptest.NewRecorder()
engine.ServeHTTP(groups, httptest.NewRequest(http.MethodGet, "/groups", nil))
if groups.Code != http.StatusOK || !strings.Contains(groups.Body.String(), "<title>groups</title>") {
t.Fatalf("GET /groups = %d %q, want embedded index", groups.Code, groups.Body.String())
}
unknown := httptest.NewRecorder()
engine.ServeHTTP(
unknown,
httptest.NewRequest(http.MethodGet, "/groups/unknown/path", nil),
)
if unknown.Code != http.StatusNotFound {
t.Fatalf("GET unknown nested groups path = %d, want 404", unknown.Code)
}
}
func TestServerFallbackReturnsNotFoundForUnknownRequests(t *testing.T) {
server := newServer(fstest.MapFS{
"dist/index.html": &fstest.MapFile{Data: []byte("<!doctype html><title>fallback</title>")},
+4 -1
View File
@@ -4,6 +4,7 @@ import { RouterView } from 'vue-router'
import AppShell from '@/app/AppShell.vue'
import AuthGate from '@/app/AuthGate.vue'
import RouteAnnouncer from '@/app/RouteAnnouncer.vue'
import PublicShell from '@/components/layout/PublicShell.vue'
</script>
<template>
@@ -14,6 +15,8 @@ import RouteAnnouncer from '@/app/RouteAnnouncer.vue'
<component :is="Component" />
</AppShell>
</AuthGate>
<component :is="Component" v-else />
<PublicShell v-else>
<component :is="Component" />
</PublicShell>
</RouterView>
</template>
+75 -18
View File
@@ -46,44 +46,101 @@ export interface ApiClientWithResponse extends ApiClient {
type Envelope = SuccessEnvelope<unknown> | ErrorEnvelope
const apiPathBase = 'https://gpt-load.invalid'
const maxApiPathLength = 8192
const maxPercentEncodingDepth = 2
function isSafeApiPath(path: unknown): path is ApiPath {
if (typeof path !== 'string' || !path.startsWith('/api/')) {
return false
function hasControlCharacter(value: string): boolean {
for (const character of value) {
const codePoint = character.codePointAt(0)
if (codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f)) {
return true
}
}
return false
}
function isSafeApiQuery(query: string): boolean {
try {
decodeURIComponent(path)
decodeURIComponent(query)
} catch {
return false
}
const rawPathname = path.split(/[?#]/, 1)[0]
if (!rawPathname) {
return false
}
for (const segment of rawPathname.split('/')) {
let decodedSegment: string
try {
decodedSegment = decodeURIComponent(segment)
} catch {
let encodedQuery = query
for (let encodingDepth = 0; encodingDepth <= maxPercentEncodingDepth; encodingDepth += 1) {
if (/%(?:0[0-9a-f]|1[0-9a-f]|7f)/i.test(encodedQuery)) {
return false
}
if (encodingDepth === maxPercentEncodingDepth) {
return true
}
const expandedQuery = encodedQuery.replace(/%25/gi, '%')
if (expandedQuery === encodedQuery) {
return true
}
encodedQuery = expandedQuery
}
return false
}
function isSafeApiPathSegment(segment: string): boolean {
let decodedSegment = segment
for (let encodingDepth = 0; encodingDepth <= maxPercentEncodingDepth; encodingDepth += 1) {
if (
decodedSegment.length === 0 ||
decodedSegment === '.' ||
decodedSegment === '..' ||
decodedSegment.includes('/') ||
decodedSegment.includes('\\')
decodedSegment.includes('\\') ||
decodedSegment.includes('?') ||
decodedSegment.includes('#') ||
hasControlCharacter(decodedSegment)
) {
return false
}
if (!decodedSegment.includes('%')) {
return true
}
if (encodingDepth === maxPercentEncodingDepth) {
return false
}
try {
decodedSegment = decodeURIComponent(decodedSegment)
} catch {
return false
}
}
const target = new URL(path, apiPathBase)
return target.origin === apiPathBase && target.pathname.startsWith('/api/')
return false
}
function isSafeApiPath(path: unknown): path is ApiPath {
if (
typeof path !== 'string' ||
path.length > maxApiPathLength ||
!path.startsWith('/api/') ||
path.startsWith('//') ||
path.includes('\\') ||
path.includes('#') ||
hasControlCharacter(path)
) {
return false
}
const queryStart = path.indexOf('?')
const pathname = queryStart === -1 ? path : path.slice(0, queryStart)
const query = queryStart === -1 ? '' : path.slice(queryStart + 1)
if (!isSafeApiQuery(query)) {
return false
}
const segments = pathname.slice('/api/'.length).split('/')
return segments.every(isSafeApiPathSegment)
}
function isRecord(value: unknown): value is Record<string, unknown> {
-1
View File
@@ -1 +0,0 @@
export * from '@/app/resources/access-keys'
-1
View File
@@ -1 +0,0 @@
export * from '@/app/resources/groups'
-1
View File
@@ -1 +0,0 @@
export * from '@/app/resources/health'
-1
View File
@@ -1 +0,0 @@
export * from '@/app/resources/model-prices'
-1
View File
@@ -1 +0,0 @@
export * from '@/app/resources/request-logs'
-1
View File
@@ -1 +0,0 @@
export * from '@/app/resources/route-inspection'
-1
View File
@@ -1 +0,0 @@
export * from '@/app/resources/settings'
Loaded 100 of 184 files, more files were not shown because too many files have changed in this diff. Show more