diff --git a/backend/internal/handler/admin/usage_handler.go b/backend/internal/handler/admin/usage_handler.go index a7e0156d18..ad2bbecd22 100644 --- a/backend/internal/handler/admin/usage_handler.go +++ b/backend/internal/handler/admin/usage_handler.go @@ -110,6 +110,7 @@ func (h *UsageHandler) List(c *gin.Context) { } model := c.Query("model") + requestID := strings.TrimSpace(c.Query("request_id")) billingMode := strings.TrimSpace(c.Query("billing_mode")) var requestType *int16 @@ -176,6 +177,7 @@ func (h *UsageHandler) List(c *gin.Context) { APIKeyID: apiKeyID, AccountID: accountID, GroupID: groupID, + RequestID: requestID, Model: model, ModelFilterSource: usagestats.ModelSourceRequested, RequestType: requestType, diff --git a/backend/internal/handler/admin/usage_handler_request_type_test.go b/backend/internal/handler/admin/usage_handler_request_type_test.go index bb4d43b4b2..7ea657749c 100644 --- a/backend/internal/handler/admin/usage_handler_request_type_test.go +++ b/backend/internal/handler/admin/usage_handler_request_type_test.go @@ -107,6 +107,18 @@ func TestAdminUsageListExactTotalTrue(t *testing.T) { require.True(t, repo.listFilters.ExactTotal) } +func TestAdminUsageListRequestIDFilter(t *testing.T) { + repo := &adminUsageRepoCapture{} + router := newAdminUsageRequestTypeTestRouter(repo) + + req := httptest.NewRequest(http.MethodGet, "/admin/usage?request_id=req-0123", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "req-0123", repo.listFilters.RequestID) +} + func TestAdminUsageListInvalidExactTotal(t *testing.T) { repo := &adminUsageRepoCapture{} router := newAdminUsageRequestTypeTestRouter(repo) diff --git a/backend/internal/pkg/usagestats/usage_log_types.go b/backend/internal/pkg/usagestats/usage_log_types.go index 29af4d2d50..da9e748b19 100644 --- a/backend/internal/pkg/usagestats/usage_log_types.go +++ b/backend/internal/pkg/usagestats/usage_log_types.go @@ -270,6 +270,7 @@ type UsageLogFilters struct { APIKeyID int64 AccountID int64 GroupID int64 + RequestID string Model string // ModelFilterSource controls how Model is matched. Empty preserves raw usage_logs.model semantics. ModelFilterSource string diff --git a/backend/internal/repository/usage_log_repo_query.go b/backend/internal/repository/usage_log_repo_query.go index 88b7eeaf35..084b79ad97 100644 --- a/backend/internal/repository/usage_log_repo_query.go +++ b/backend/internal/repository/usage_log_repo_query.go @@ -116,6 +116,10 @@ func (r *usageLogRepository) ListWithFilters(ctx context.Context, params paginat conditions = append(conditions, fmt.Sprintf("group_id = $%d", len(args)+1)) args = append(args, filters.GroupID) } + if requestID := strings.TrimSpace(filters.RequestID); requestID != "" { + conditions = append(conditions, fmt.Sprintf("request_id = $%d", len(args)+1)) + args = append(args, requestID) + } conditions, args = appendUsageLogModelWhereCondition(conditions, args, filters.Model, filters.ModelFilterSource) conditions, args = appendRequestTypeOrStreamWhereCondition(conditions, args, filters.RequestType, filters.Stream) if filters.BillingType != nil { diff --git a/backend/internal/repository/usage_log_repo_request_type_test.go b/backend/internal/repository/usage_log_repo_request_type_test.go index aff472b0cb..9c59b38e54 100644 --- a/backend/internal/repository/usage_log_repo_request_type_test.go +++ b/backend/internal/repository/usage_log_repo_request_type_test.go @@ -374,6 +374,23 @@ func TestUsageLogRepositoryListWithFiltersRequestTypePriority(t *testing.T) { require.NoError(t, mock.ExpectationsWereMet()) } +func TestUsageLogRepositoryListWithFiltersRequestID(t *testing.T) { + db, mock := newSQLMock(t) + repo := &usageLogRepository{sql: db} + + filters := usagestats.UsageLogFilters{RequestID: " req-0123 "} + + mock.ExpectQuery("SELECT .* FROM usage_logs WHERE request_id = \\$1 ORDER BY id DESC LIMIT \\$2 OFFSET \\$3"). + WithArgs("req-0123", 21, 0). + WillReturnRows(sqlmock.NewRows([]string{"id"})) + + logs, page, err := repo.ListWithFilters(context.Background(), pagination.PaginationParams{Page: 1, PageSize: 20}, filters) + require.NoError(t, err) + require.Empty(t, logs) + require.NotNil(t, page) + require.NoError(t, mock.ExpectationsWereMet()) +} + func TestUsageLogRepositoryListWithFiltersRequestedModelSource(t *testing.T) { db, mock := newSQLMock(t) repo := &usageLogRepository{sql: db}