Follow-up to the previous pass, each with a test that fails without it. - Cancelling a run also sets resume_blocked, so filtering on that flag alone dropped the steps and tokens of cancelled work while still counting the run and its duration. Only a run a later run resumed from is excluded now: the resume claim leaves output_dir intact, cancelling clears it, which tells the two apart. - A fixed getTimezoneOffset() was applied to every historical message, so winter records read an hour out when the panel is opened on summer time, moving messages near midnight onto the wrong day. The endpoint now takes an IANA timezone and converts each timestamp with its own offset, keeping the fixed offset as the fallback. - A fork with no new turn was skipped before its thread was registered, so it did not appear in the chat total until its first message. The thread is now counted before the cloned rows are suppressed. - Routed and aliased responses record the model that actually answered in responseDetails.responseModelId, while contextUsage.modelId stays the requested checkpoint. Most used models now prefers the former. - Renamed runs showed the model label instead of the name the user chose. The name leads and the model moves beside the dataset.
57 lines
1.9 KiB
Python
57 lines
1.9 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""
|
|
Usage numbers for the Profile settings tab.
|
|
|
|
Read-only aggregation over the local studio.db (see
|
|
``storage.profile_stats_db``). Nothing is uploaded.
|
|
"""
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
|
|
from auth.authentication import get_current_subject
|
|
from loggers import get_logger
|
|
from storage.profile_stats_db import (
|
|
MAX_DAILY_DAYS,
|
|
MAX_TZ_OFFSET_MINUTES,
|
|
compute_profile_stats,
|
|
)
|
|
from utils.utils import log_and_http_error
|
|
|
|
router = APIRouter()
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
@router.get("/stats")
|
|
async def get_profile_stats(
|
|
days: int = Query(MAX_DAILY_DAYS, ge = 1, le = MAX_DAILY_DAYS),
|
|
tz_offset_minutes: int = Query(0, ge = -MAX_TZ_OFFSET_MINUTES, le = MAX_TZ_OFFSET_MINUTES),
|
|
tz: str = Query("", max_length = 64),
|
|
current_subject: str = Depends(get_current_subject),
|
|
) -> dict[str, Any]:
|
|
"""Usage stats for the signed-in user's local history.
|
|
|
|
Days and hours are bucketed in the caller's timezone so a remote browser
|
|
does not read the server's calendar. ``tz`` is an IANA name, which carries
|
|
each date's own daylight-saving offset; ``tz_offset_minutes`` is the
|
|
``Date.getTimezoneOffset()`` fallback for hosts with no tzdata.
|
|
"""
|
|
try:
|
|
# A cold pass parses every message's metadata JSON: ~90 ms at 10k
|
|
# messages, ~1.2 s at 260k. Off the event loop so it cannot stall token
|
|
# streaming when Settings is opened mid-generation.
|
|
return await asyncio.to_thread(
|
|
compute_profile_stats,
|
|
days = days,
|
|
tz_offset_minutes = tz_offset_minutes,
|
|
tz_name = tz,
|
|
)
|
|
except Exception as exc:
|
|
raise log_and_http_error(
|
|
exc, 500, "Failed to compute profile statistics", log = logger
|
|
) from exc
|