This commit is contained in:
OldFangDong 2026-11-06 04:57:56 +00:00 • committed by GitHub
commit a40b725416
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 464 additions and 26 deletions

View file

@ -5222,20 +5222,36 @@
}
},
{
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"in": "query",
"name": "timezone",
"required": false,
"schema": {
"anyOf": [
{
"maximum": 840,
"minimum": -840,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"title": "Timezone"
}
},
{
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"in": "query",
"name": "include_current_utc_day",
"required": false,
"schema": {
"default": true,
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"title": "Include Current Utc Day",
"type": "boolean"
}
}
],
"responses": {
@ -5395,20 +5411,36 @@
}
},
{
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"in": "query",
"name": "timezone",
"required": false,
"schema": {
"anyOf": [
{
"maximum": 840,
"minimum": -840,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"title": "Timezone"
}
},
{
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"in": "query",
"name": "include_current_utc_day",
"required": false,
"schema": {
"default": true,
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"title": "Include Current Utc Day",
"type": "boolean"
}
}
],
"responses": {
@ -5577,20 +5609,36 @@
}
},
{
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"in": "query",
"name": "timezone",
"required": false,
"schema": {
"anyOf": [
{
"maximum": 840,
"minimum": -840,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"title": "Timezone"
}
},
{
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"in": "query",
"name": "include_current_utc_day",
"required": false,
"schema": {
"default": true,
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"title": "Include Current Utc Day",
"type": "boolean"
}
}
],
"responses": {
@ -5749,20 +5797,36 @@
}
},
{
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"in": "query",
"name": "timezone",
"required": false,
"schema": {
"anyOf": [
{
"maximum": 840,
"minimum": -840,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"title": "Timezone"
}
},
{
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"in": "query",
"name": "include_current_utc_day",
"required": false,
"schema": {
"default": true,
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"title": "Include Current Utc Day",
"type": "boolean"
}
}
],
"responses": {
@ -5922,20 +5986,36 @@
}
},
{
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"in": "query",
"name": "timezone",
"required": false,
"schema": {
"anyOf": [
{
"maximum": 840,
"minimum": -840,
"type": "integer"
},
{
"type": "null"
}
],
"description": "Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention.",
"title": "Timezone"
}
},
{
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"in": "query",
"name": "include_current_utc_day",
"required": false,
"schema": {
"default": true,
"description": "When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended.",
"title": "Include Current Utc Day",
"type": "boolean"
}
}
],
"responses": {

View file

@ -41,6 +41,7 @@ from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
from litellm.proxy.utils import PrismaClient, get_prisma_client_or_throw
from litellm.repositories.daily_activity_buckets import api_key_bucket
from litellm.repositories.daily_activity_repository import DailyActivityRepository
from litellm.repositories.daily_activity_sql import adjust_dates_for_timezone
from litellm.router import Router
from litellm.types.proxy.management_endpoints.common_daily_activity import (
CacheLeakageKeysResponse,
@ -83,11 +84,22 @@ def get_daily_activity_llm_router() -> Router | None:
def _date_range_error(query: EntityQuery, *, user_aggregated: bool) -> InvalidDateRange | None:
if user_aggregated:
date_range: Final = parse_canonical_date_range(query.start_date, query.end_date)
return date_range if isinstance(date_range, InvalidDateRange) else None
if query.timezone_offset_minutes is not None and not (-840 <= query.timezone_offset_minutes <= 840):
return InvalidDateRange(reason="timezone must be between -840 and 840 minutes")
range_error: Final[str | None] = aggregated_date_range_error(query.start_date, query.end_date)
date_range: Final = parse_canonical_date_range(query.start_date, query.end_date)
if isinstance(date_range, InvalidDateRange):
return date_range
if user_aggregated:
return None
adjusted_start, adjusted_end = adjust_dates_for_timezone(
date_range.start.isoformat(),
date_range.end.isoformat(),
query.timezone_offset_minutes,
query.include_current_utc_day,
)
range_error: Final = aggregated_date_range_error(adjusted_start, adjusted_end)
return None if range_error is None else InvalidDateRange(reason=range_error)

View file

@ -228,11 +228,13 @@ def _user_query(
),
timezone: int | None = Query(
default=None,
ge=-840,
le=840,
description="Timezone offset in minutes from UTC (e.g., 480 for PST). "
"Matches JavaScript's Date.getTimezoneOffset() convention.",
),
include_current_utc_day: bool = Query(
default=False,
default=True,
description="When the range ends on the caller's current local day, extend it to "
"today's UTC bucket so spend written after the caller's local midnight (in UTC "
"terms) is included. Requires the timezone parameter. Historical ranges are "
@ -258,7 +260,20 @@ def _team_query(
model: str | None = None,
api_key: str | None = None,
exclude_team_ids: str | None = None,
timezone: int | None = None,
timezone: int | None = Query(
default=None,
ge=-840,
le=840,
description="Timezone offset in minutes from UTC (e.g., 480 for PST). "
"Matches JavaScript's Date.getTimezoneOffset() convention.",
),
include_current_utc_day: bool = Query(
default=True,
description="When the range ends on the caller's current local day, extend it to "
"today's UTC bucket so spend written after the caller's local midnight (in UTC "
"terms) is included. Requires the timezone parameter. Historical ranges are "
"never extended.",
),
) -> EntityQuery:
return EntityQuery(
entity_ids=_query_ids(team_ids),
@ -268,7 +283,7 @@ def _team_query(
end_date=end_date,
model=model,
timezone_offset_minutes=timezone,
include_current_utc_day=False,
include_current_utc_day=include_current_utc_day,
)
@ -278,7 +293,20 @@ def _tag_query(
model: str | None = None,
api_key: str | None = None,
tags: str | None = None,
timezone: int | None = None,
timezone: int | None = Query(
default=None,
ge=-840,
le=840,
description="Timezone offset in minutes from UTC (e.g., 480 for PST). "
"Matches JavaScript's Date.getTimezoneOffset() convention.",
),
include_current_utc_day: bool = Query(
default=True,
description="When the range ends on the caller's current local day, extend it to "
"today's UTC bucket so spend written after the caller's local midnight (in UTC "
"terms) is included. Requires the timezone parameter. Historical ranges are "
"never extended.",
),
) -> EntityQuery:
return EntityQuery(
entity_ids=_query_ids(tags),
@ -288,7 +316,7 @@ def _tag_query(
end_date=end_date,
model=model,
timezone_offset_minutes=timezone,
include_current_utc_day=False,
include_current_utc_day=include_current_utc_day,
)
@ -299,7 +327,20 @@ def _organization_query(
model: str | None = None,
api_key: str | None = None,
exclude_organization_ids: str | None = None,
timezone: int | None = None,
timezone: int | None = Query(
default=None,
ge=-840,
le=840,
description="Timezone offset in minutes from UTC (e.g., 480 for PST). "
"Matches JavaScript's Date.getTimezoneOffset() convention.",
),
include_current_utc_day: bool = Query(
default=True,
description="When the range ends on the caller's current local day, extend it to "
"today's UTC bucket so spend written after the caller's local midnight (in UTC "
"terms) is included. Requires the timezone parameter. Historical ranges are "
"never extended.",
),
) -> EntityQuery:
return EntityQuery(
entity_ids=_query_ids(organization_ids),
@ -309,7 +350,7 @@ def _organization_query(
end_date=end_date,
model=model,
timezone_offset_minutes=timezone,
include_current_utc_day=False,
include_current_utc_day=include_current_utc_day,
)
@ -320,7 +361,20 @@ def _customer_query(
model: str | None = None,
api_key: str | None = None,
exclude_end_user_ids: str | None = None,
timezone: int | None = None,
timezone: int | None = Query(
default=None,
ge=-840,
le=840,
description="Timezone offset in minutes from UTC (e.g., 480 for PST). "
"Matches JavaScript's Date.getTimezoneOffset() convention.",
),
include_current_utc_day: bool = Query(
default=True,
description="When the range ends on the caller's current local day, extend it to "
"today's UTC bucket so spend written after the caller's local midnight (in UTC "
"terms) is included. Requires the timezone parameter. Historical ranges are "
"never extended.",
),
) -> EntityQuery:
return EntityQuery(
entity_ids=_query_ids(end_user_ids),
@ -330,7 +384,7 @@ def _customer_query(
end_date=end_date,
model=model,
timezone_offset_minutes=timezone,
include_current_utc_day=False,
include_current_utc_day=include_current_utc_day,
)
@ -341,7 +395,20 @@ def _agent_query(
model: str | None = None,
api_key: str | None = None,
exclude_agent_ids: str | None = None,
timezone: int | None = None,
timezone: int | None = Query(
default=None,
ge=-840,
le=840,
description="Timezone offset in minutes from UTC (e.g., 480 for PST). "
"Matches JavaScript's Date.getTimezoneOffset() convention.",
),
include_current_utc_day: bool = Query(
default=True,
description="When the range ends on the caller's current local day, extend it to "
"today's UTC bucket so spend written after the caller's local midnight (in UTC "
"terms) is included. Requires the timezone parameter. Historical ranges are "
"never extended.",
),
) -> EntityQuery:
return EntityQuery(
entity_ids=_query_ids(agent_ids),
@ -351,7 +418,7 @@ def _agent_query(
end_date=end_date,
model=model,
timezone_offset_minutes=timezone,
include_current_utc_day=False,
include_current_utc_day=include_current_utc_day,
)

View file

@ -22,6 +22,7 @@ from litellm.models.gateway_activity import GatewayRouteActivity, GatewayStatusC
from litellm.proxy._types import CommonProxyErrors, LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.deps.repositories import GatewayActivityRepositoryDep
from litellm.repositories.daily_activity_sql import adjust_dates_for_timezone
from litellm.types.proxy.gateway_requests import (
GatewayRequestActivityResponse,
GatewayRequestBreakdownEntry,
@ -87,6 +88,21 @@ async def get_gateway_daily_activity(
activity: GatewayActivityRepositoryDep,
start_date: str | None = Query(default=None, description="Start date in YYYY-MM-DD format"),
end_date: str | None = Query(default=None, description="End date in YYYY-MM-DD format"),
timezone_offset_minutes: Annotated[
int | None,
Query(
alias="timezone",
ge=-840,
le=840,
description="Timezone offset in minutes from UTC, matching JavaScript's Date.getTimezoneOffset().",
),
] = None,
include_current_utc_day: Annotated[
bool,
Query(
description="Include the current UTC bucket when the range reaches the caller's local today. Requires timezone."
),
] = True,
) -> GatewayRequestActivityResponse:
"""
Successful and failed gateway requests, counted at the ASGI edge.
@ -109,7 +125,14 @@ async def get_gateway_daily_activity(
default_start, default_end = _default_range()
selected_start: Final = start_date or default_start
selected_end: Final = end_date or default_end
activity_rows: Final = await activity.get_activity(selected_start, selected_end)
query_start, query_end = adjust_dates_for_timezone(
selected_start,
selected_end,
timezone_offset_minutes,
include_current_utc_day,
utc_now=datetime.now(timezone.utc),
)
activity_rows: Final = await activity.get_activity(query_start, query_end)
rows: Final = tuple(row for row in activity_rows if isinstance(row, GatewayRouteActivity))
status_code_rows: Final = tuple(row for row in activity_rows if isinstance(row, GatewayStatusCodeActivity))
verbose_proxy_logger.debug("/gateway/daily/activity - aggregated %d rows", len(rows))

View file

@ -47,6 +47,10 @@ PRISMA_TO_PG_TABLE: Final[Mapping[DailyActivityTable, str]] = MappingProxyType(
)
_MIN_TIMEZONE_OFFSET_MINUTES: Final = -840 # UTC+14 (e.g. Line Islands)
_MAX_TIMEZONE_OFFSET_MINUTES: Final = 840 # UTC-14
def adjust_dates_for_timezone(
start_date: str,
end_date: str,
@ -54,7 +58,11 @@ def adjust_dates_for_timezone(
include_current_utc_day: bool = False,
utc_now: datetime | None = None,
) -> tuple[str, str]:
if not include_current_utc_day or timezone_offset_minutes is None:
if (
not include_current_utc_day
or timezone_offset_minutes is None
or not (_MIN_TIMEZONE_OFFSET_MINUTES <= timezone_offset_minutes <= _MAX_TIMEZONE_OFFSET_MINUTES)
):
return start_date, end_date
now: Final = utc_now if utc_now is not None else datetime.now(timezone.utc)
caller_local_today: Final = (now - timedelta(minutes=timezone_offset_minutes)).date().isoformat()

View file

@ -1557,6 +1557,32 @@ def test_user_aggregate_keeps_current_day_query_semantics(
assert scope.include_current_utc_day is True
def test_aggregate_routes_default_include_current_utc_day_to_true(
daily_activity_client: tuple[TestClient, _FakeRepository],
) -> None:
client, repository = daily_activity_client
for route in ("/user/daily/activity/aggregated", "/team/daily/activity/aggregated"):
response: Final = client.get(
route,
params={**_DATE_PARAMS, "timezone": 480},
)
assert response.status_code == 200, response.text
scope: Final = repository.aggregated.call_args.args[0]
assert scope.include_current_utc_day is True
def test_aggregate_routes_reject_invalid_timezone(
daily_activity_client: tuple[TestClient, _FakeRepository],
) -> None:
client, _ = daily_activity_client
for route in ("/user/daily/activity/aggregated", "/team/daily/activity/aggregated"):
response: Final = client.get(
route,
params={**_DATE_PARAMS, "timezone": 20000000},
)
assert response.status_code in (400, 422)
@pytest.mark.parametrize(
("start_date", "end_date", "message"),
(

View file

@ -71,6 +71,34 @@ def _prisma_returning(rows: Sequence[Mapping[str, str | int | None]] | None) ->
return client
@pytest.fixture
def gateway_daily_activity_client() -> TestClient:
buckets: Final = (("2025-12-31", 3), ("2026-01-01", 7))
async def query_raw(_query: str, start_date: str, end_date: str) -> tuple[Mapping[str, str | int | None], ...]:
return tuple(
{
"kind": "route",
"date": date,
"category": "llm",
"route": "/chat/completions",
"successful_requests": successful,
"failed_requests": 0,
"status_code": None,
}
for date, successful in buckets
if start_date <= date <= end_date
)
prisma: Final = MagicMock()
prisma.db.query_raw = AsyncMock(side_effect=query_raw)
app: Final = FastAPI()
app.include_router(router)
app.dependency_overrides[user_api_key_auth] = _admin
app.dependency_overrides[get_gateway_activity_repository] = lambda: GatewayActivityRepository(prisma)
return TestClient(app)
class TestDefaultRange:
def test_spans_the_documented_lookback(self):
start, end = _default_range()
@ -307,6 +335,58 @@ class TestGatewayDailyActivityRoute:
assert response.status_code == 200
assert prisma.db.query_raw.call_args_list[0].args[1:] == _FROZEN_RANGE
@pytest.mark.parametrize(
("utc_hour", "timezone_offset_minutes", "include_current_utc_day", "expected_successful_requests"),
(
pytest.param(12, None, True, 3, id="historical-without-timezone"),
pytest.param(12, 0, True, 3, id="historical-utc"),
pytest.param(12, -330, True, 3, id="historical-east"),
pytest.param(12, 300, True, 3, id="historical-west"),
pytest.param(1, 300, True, 10, id="live-west-year-rollover"),
pytest.param(1, 300, None, 10, id="live-west-default"),
pytest.param(1, 300, False, 3, id="live-west-opt-out"),
pytest.param(1, None, True, 3, id="live-without-timezone"),
pytest.param(1, 0, True, 3, id="historical-utc-after-midnight"),
pytest.param(1, 840, True, 10, id="maximum-offset"),
pytest.param(12, -840, True, 3, id="minimum-offset"),
),
)
def test_timezone_queries_only_include_the_live_utc_bucket_for_the_callers_current_day(
self,
gateway_daily_activity_client: TestClient,
utc_hour: int,
timezone_offset_minutes: int | None,
include_current_utc_day: bool | None,
expected_successful_requests: int,
) -> None:
params: Final = {
"start_date": "2025-12-31",
"end_date": "2025-12-31",
**(
{"include_current_utc_day": str(include_current_utc_day).lower()}
if include_current_utc_day is not None
else {}
),
**({"timezone": str(timezone_offset_minutes)} if timezone_offset_minutes is not None else {}),
}
with patch("litellm.proxy.management_endpoints.gateway_request_endpoints.datetime") as clock:
clock.now.return_value = datetime(2026, 1, 1, utc_hour, tzinfo=timezone.utc)
response: Final = gateway_daily_activity_client.get("/gateway/daily/activity", params=params)
assert response.status_code == 200, response.text
assert response.json()["total_successful_requests"] == expected_successful_requests
@pytest.mark.parametrize("timezone_offset_minutes", (-841, 841))
def test_timezone_outside_supported_offsets_is_rejected(
self, gateway_daily_activity_client: TestClient, timezone_offset_minutes: int
) -> None:
response: Final = gateway_daily_activity_client.get(
"/gateway/daily/activity",
params={"start_date": "2025-12-31", "end_date": "2025-12-31", "timezone": timezone_offset_minutes},
)
assert response.status_code == 422, response.text
def test_serialized_response_carries_the_documented_shape(self):
prisma: Final = _prisma_returning(
[

View file

@ -263,6 +263,12 @@ def test_timezone_adjustment_live_end_handles_offset_and_opt_in_cases() -> None:
assert adjust_dates_for_timezone(
"2026-07-06", "2026-08-09", 420, include_current_utc_day=True, utc_now=pt_evening
) == ("2026-07-06", "2026-08-09")
assert adjust_dates_for_timezone(
"2026-07-06", "2026-08-05", 20000000, include_current_utc_day=True, utc_now=pt_evening
) == ("2026-07-06", "2026-08-05")
assert adjust_dates_for_timezone(
"2026-07-06", "2026-08-05", -900, include_current_utc_day=True, utc_now=pt_evening
) == ("2026-07-06", "2026-08-05")
@pytest.mark.parametrize("offset_minutes", [None, 0, -330, 480])

View file

@ -41,7 +41,12 @@ export function useHomeUsage(): HomeUsage {
const request = useMemo<DailyActivityRequest | null>(
() =>
accessToken && (isAdmin || userId)
? { accessToken, ...range, entityIds: isAdmin ? null : [userId as string] }
? {
accessToken,
...range,
entityIds: isAdmin ? null : [userId as string],
includeCurrentUtcDay: true,
}
: null,
[accessToken, isAdmin, range, userId],
);

View file

@ -256,6 +256,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations, teamsLoadin
startTime,
endTime,
entityIds: effectiveUserId ? [effectiveUserId] : null,
includeCurrentUtcDay: true,
}
: null,
[accessToken, startTime, endTime, effectiveUserId],

View file

@ -103,6 +103,7 @@ const UsagePanel: React.FC<Props> = ({ accessToken, userId }) => {
startTime: start,
endTime: end,
entityIds: userId ? [userId] : null,
includeCurrentUtcDay: true,
};
return dailyActivityAggregatedCall("user", request);
},

View file

@ -6,6 +6,7 @@ import {
dailyActivityExportCall,
dailyActivityKeySearchCall,
dailyActivityModelTopKeysCall,
gatewayDailyActivityCall,
} from "./networking";
import type { DailyActivityEntity, DailyActivityRequest } from "./UsagePage/dailyActivityApi";
@ -26,8 +27,8 @@ const captureFetch = () => {
const requestedUrl = (mockFetch: ReturnType<typeof captureFetch>, callIndex = 0): URL =>
new URL(String(mockFetch.mock.calls[callIndex][0]), "http://example.com");
const start = new Date("2025-01-05T00:00:00Z");
const end = new Date("2025-01-31T00:00:00Z");
const start = new Date(2025, 0, 5);
const end = new Date(2025, 0, 31);
const req = (overrides: Partial<DailyActivityRequest> = {}): DailyActivityRequest => ({
accessToken: "sk-key",
@ -38,6 +39,8 @@ const req = (overrides: Partial<DailyActivityRequest> = {}): DailyActivityReques
afterEach(() => {
global.fetch = originalFetch;
vi.useRealTimers();
vi.unstubAllEnvs();
});
describe("dailyActivityAggregatedCall", () => {
@ -115,13 +118,20 @@ describe("dailyActivityAggregatedCall", () => {
expect(requestedUrl(mockFetch).searchParams.has("api_key")).toBe(false);
});
it("sends include_current_utc_day only when set", async () => {
const mockFetch = captureFetch();
it.each<[DailyActivityEntity]>([["user"], ["team"], ["tag"], ["organization"], ["customer"], ["agent"]])(
"preserves true, false, and omitted live-day settings for %s",
async (entity) => {
const mockFetch = captureFetch();
await dailyActivityAggregatedCall("user", req({ includeCurrentUtcDay: true }));
for (const setting of [true, false, undefined]) {
await dailyActivityAggregatedCall(entity, req({ includeCurrentUtcDay: setting }));
expect(requestedUrl(mockFetch).searchParams.get("include_current_utc_day")).toBe("true");
});
expect(
requestedUrl(mockFetch, mockFetch.mock.calls.length - 1).searchParams.get("include_current_utc_day"),
).toBe(setting === undefined ? null : String(setting));
}
},
);
it("sends api_key_limit only when provided", async () => {
const mockFetch = captureFetch();
@ -134,6 +144,29 @@ describe("dailyActivityAggregatedCall", () => {
});
});
describe("gatewayDailyActivityCall", () => {
it.each(["America/Los_Angeles", "Asia/Kolkata"])(
"sends local date boundaries and the caller's live-day timezone in %s",
async (timezone) => {
vi.stubEnv("TZ", timezone);
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date("2026-08-06T02:00:00Z"));
const mockFetch = captureFetch();
const rangeStart = new Date(2026, 11, 31);
const rangeEnd = new Date(2027, 0, 1);
await gatewayDailyActivityCall("sk-key", rangeStart, rangeEnd);
const url = requestedUrl(mockFetch);
expect(url.pathname).toBe("/gateway/daily/activity");
expect(url.searchParams.get("start_date")).toBe("2026-12-31");
expect(url.searchParams.get("end_date")).toBe("2027-01-01");
expect(url.searchParams.get("timezone")).toBe(String(new Date().getTimezoneOffset()));
expect(url.searchParams.get("include_current_utc_day")).toBe("true");
},
);
});
describe("dailyActivityKeySearchCall", () => {
it("GETs the search route with the search term", async () => {
const mockFetch = captureFetch();

View file

@ -1384,7 +1384,7 @@ const dailyActivityQuery = (
? { [excludeParam]: excludeEntityIds && excludeEntityIds.length > 0 ? excludeEntityIds.join(",") : undefined }
: {}),
timezone: new Date().getTimezoneOffset().toString(),
include_current_utc_day: entity === "user" && req.includeCurrentUtcDay ? "true" : undefined,
include_current_utc_day: req.includeCurrentUtcDay?.toString(),
...extra,
};
};
@ -2270,6 +2270,8 @@ export const gatewayDailyActivityCall = async (accessToken: string, startTime: D
query: {
start_date: formatDate(startTime),
end_date: formatDate(endTime),
timezone: new Date().getTimezoneOffset().toString(),
include_current_utc_day: "true",
},
});
} catch (error) {

View file

@ -53965,7 +53965,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_agent_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -54004,7 +54007,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_agent_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -54044,7 +54050,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_agent_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -54083,7 +54092,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_agent_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -54122,7 +54134,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_agent_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -59652,7 +59667,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -59691,7 +59709,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -59731,7 +59752,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -59770,7 +59794,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -59809,7 +59836,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -60703,7 +60733,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -60742,7 +60775,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -60782,7 +60818,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -60821,7 +60860,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -60860,7 +60902,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_end_user_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -62474,6 +62519,10 @@ export interface operations {
start_date?: string | null;
/** @description End date in YYYY-MM-DD format */
end_date?: string | null;
/** @description Timezone offset in minutes from UTC, matching JavaScript's Date.getTimezoneOffset(). */
timezone?: number | null;
/** @description Include the current UTC bucket when the range reaches the caller's local today. Requires timezone. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -70063,7 +70112,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_organization_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -70102,7 +70154,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_organization_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -70142,7 +70197,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_organization_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -70181,7 +70239,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_organization_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -70220,7 +70281,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_organization_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -76334,7 +76398,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
tags?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -76372,7 +76439,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
tags?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -76411,7 +76481,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
tags?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -76449,7 +76522,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
tags?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -76487,7 +76563,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
tags?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -77025,7 +77104,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_team_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -77064,7 +77146,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_team_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -77104,7 +77189,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_team_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -77143,7 +77231,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_team_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;
@ -77182,7 +77273,10 @@ export interface operations {
model?: string | null;
api_key?: string | null;
exclude_team_ids?: string | null;
/** @description Timezone offset in minutes from UTC (e.g., 480 for PST). Matches JavaScript's Date.getTimezoneOffset() convention. */
timezone?: number | null;
/** @description When the range ends on the caller's current local day, extend it to today's UTC bucket so spend written after the caller's local midnight (in UTC terms) is included. Requires the timezone parameter. Historical ranges are never extended. */
include_current_utc_day?: boolean;
};
header?: never;
path?: never;