200 lines
5.2 KiB
Go
200 lines
5.2 KiB
Go
package requestlog
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"gpt-load/internal/pricing"
|
|
"gpt-load/internal/protocol"
|
|
"gpt-load/internal/storage/models"
|
|
"gpt-load/internal/telemetry"
|
|
"gpt-load/internal/usage"
|
|
)
|
|
|
|
const defaultListLimit = 50
|
|
|
|
func (service *Service) List(ctx context.Context, input ListQuery) (Page, error) {
|
|
limit := input.Limit
|
|
if limit <= 0 {
|
|
limit = defaultListLimit
|
|
}
|
|
|
|
query := service.db.WithContext(ctx).
|
|
Model(&models.RequestLog{}).
|
|
Order("created_at DESC").
|
|
Order("id DESC").
|
|
Limit(limit + 1)
|
|
if input.From != nil {
|
|
query = query.Where("created_at >= ?", input.From.UTC())
|
|
}
|
|
if input.To != nil {
|
|
query = query.Where("created_at < ?", input.To.UTC())
|
|
}
|
|
if input.GroupID != nil {
|
|
query = query.Where(`
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM json_each(COALESCE(request_logs.attempts, '[]')) AS attempt
|
|
WHERE json_type(attempt.value, '$.group_id') = 'integer'
|
|
AND json_extract(attempt.value, '$.group_id') = ?
|
|
)
|
|
`, *input.GroupID)
|
|
}
|
|
if input.ClientModel != "" {
|
|
query = query.Where("client_model = ?", input.ClientModel)
|
|
}
|
|
if input.AccessKeyID != nil {
|
|
query = query.Where("access_key_id = ?", *input.AccessKeyID)
|
|
}
|
|
if input.Status != "" {
|
|
query = query.Where("status = ?", input.Status)
|
|
}
|
|
if input.RequestID != "" {
|
|
query = query.Where("id = ?", input.RequestID)
|
|
}
|
|
if input.Cursor != nil {
|
|
query = query.Where(
|
|
"created_at < ? OR (created_at = ? AND id < ?)",
|
|
input.Cursor.CompletedAt.UTC(),
|
|
input.Cursor.CompletedAt.UTC(),
|
|
input.Cursor.RequestID,
|
|
)
|
|
}
|
|
|
|
var rows []models.RequestLog
|
|
if err := query.Find(&rows).Error; err != nil {
|
|
return Page{}, fmt.Errorf("query request logs: %w", err)
|
|
}
|
|
|
|
hasNext := len(rows) > limit
|
|
if hasNext {
|
|
rows = rows[:limit]
|
|
}
|
|
records, err := decodeRequestLogRows(rows)
|
|
if err != nil {
|
|
return Page{}, err
|
|
}
|
|
if err := service.loadAccessKeyRefs(ctx, records); err != nil {
|
|
return Page{}, err
|
|
}
|
|
|
|
page := Page{Items: records}
|
|
if hasNext {
|
|
last := records[len(records)-1]
|
|
page.NextCursor = &Cursor{
|
|
CompletedAt: last.CompletedAt,
|
|
RequestID: last.RequestID,
|
|
}
|
|
}
|
|
return page, nil
|
|
}
|
|
|
|
func decodeRequestLogRows(rows []models.RequestLog) ([]Record, error) {
|
|
records := make([]Record, 0, len(rows))
|
|
for _, row := range rows {
|
|
attempts := make([]Attempt, 0)
|
|
if len(row.Attempts) > 0 && string(row.Attempts) != "null" {
|
|
if err := json.Unmarshal(row.Attempts, &attempts); err != nil {
|
|
return nil, fmt.Errorf("decode request log attempts: %w", err)
|
|
}
|
|
if attempts == nil {
|
|
attempts = make([]Attempt, 0)
|
|
}
|
|
}
|
|
if err := validateRequestLogUsageCost(row); err != nil {
|
|
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,
|
|
})
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func validateRequestLogUsageCost(row models.RequestLog) error {
|
|
if err := ValidateUsageCostState(
|
|
usage.State(row.UsageState),
|
|
pricing.CostState(row.CostState),
|
|
row.Cost,
|
|
); err != nil {
|
|
return fmt.Errorf("decode request log usage/cost: %w", err)
|
|
}
|
|
for _, value := range []int64{
|
|
row.InputTokens,
|
|
row.CacheReadTokens,
|
|
row.CacheWrite5MTokens,
|
|
row.CacheWrite1HTokens,
|
|
row.OutputTokens,
|
|
} {
|
|
if value < 0 {
|
|
return fmt.Errorf("decode request log usage tokens: negative value")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (service *Service) loadAccessKeyRefs(ctx context.Context, records []Record) error {
|
|
if len(records) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ids := make([]uint, 0, len(records))
|
|
seen := make(map[uint]struct{}, len(records))
|
|
for _, record := range records {
|
|
if _, ok := seen[record.AccessKey.ID]; ok {
|
|
continue
|
|
}
|
|
seen[record.AccessKey.ID] = struct{}{}
|
|
ids = append(ids, record.AccessKey.ID)
|
|
}
|
|
|
|
var accessKeys []struct {
|
|
ID uint
|
|
Name string
|
|
}
|
|
if err := service.db.WithContext(ctx).
|
|
Model(&models.AccessKey{}).
|
|
Select("id", "name").
|
|
Where("id IN ?", ids).
|
|
Find(&accessKeys).Error; err != nil {
|
|
return fmt.Errorf("query request log access keys: %w", err)
|
|
}
|
|
refs := make(map[uint]AccessKeyRef, len(accessKeys))
|
|
for _, accessKey := range accessKeys {
|
|
name := accessKey.Name
|
|
refs[accessKey.ID] = AccessKeyRef{
|
|
ID: accessKey.ID,
|
|
Name: &name,
|
|
Deleted: false,
|
|
}
|
|
}
|
|
for index := range records {
|
|
if ref, ok := refs[records[index].AccessKey.ID]; ok {
|
|
records[index].AccessKey = ref
|
|
}
|
|
}
|
|
return nil
|
|
}
|