codex-lb 0.3.1__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. app/core/clients/proxy.py +33 -3
  2. app/core/config/settings.py +9 -8
  3. app/core/handlers/__init__.py +3 -0
  4. app/core/handlers/exceptions.py +39 -0
  5. app/core/middleware/__init__.py +9 -0
  6. app/core/middleware/api_errors.py +33 -0
  7. app/core/middleware/request_decompression.py +101 -0
  8. app/core/middleware/request_id.py +27 -0
  9. app/core/openai/chat_requests.py +172 -0
  10. app/core/openai/chat_responses.py +534 -0
  11. app/core/openai/message_coercion.py +60 -0
  12. app/core/openai/models_catalog.py +72 -0
  13. app/core/openai/requests.py +23 -5
  14. app/core/openai/v1_requests.py +92 -0
  15. app/db/models.py +3 -3
  16. app/db/session.py +25 -8
  17. app/dependencies.py +43 -16
  18. app/main.py +13 -67
  19. app/modules/accounts/repository.py +25 -10
  20. app/modules/proxy/api.py +94 -0
  21. app/modules/proxy/load_balancer.py +75 -58
  22. app/modules/proxy/repo_bundle.py +23 -0
  23. app/modules/proxy/service.py +127 -102
  24. app/modules/request_logs/api.py +61 -7
  25. app/modules/request_logs/repository.py +131 -16
  26. app/modules/request_logs/schemas.py +11 -2
  27. app/modules/request_logs/service.py +97 -20
  28. app/modules/usage/service.py +65 -4
  29. app/modules/usage/updater.py +58 -26
  30. app/static/index.css +378 -1
  31. app/static/index.html +183 -8
  32. app/static/index.js +308 -13
  33. {codex_lb-0.3.1.dist-info → codex_lb-0.5.0.dist-info}/METADATA +42 -3
  34. {codex_lb-0.3.1.dist-info → codex_lb-0.5.0.dist-info}/RECORD +37 -25
  35. {codex_lb-0.3.1.dist-info → codex_lb-0.5.0.dist-info}/WHEEL +0 -0
  36. {codex_lb-0.3.1.dist-info → codex_lb-0.5.0.dist-info}/entry_points.txt +0 -0
  37. {codex_lb-0.3.1.dist-info → codex_lb-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,27 +5,81 @@ from datetime import datetime
5
5
  from fastapi import APIRouter, Depends, Query
6
6
 
7
7
  from app.dependencies import RequestLogsContext, get_request_logs_context
8
- from app.modules.request_logs.schemas import RequestLogsResponse
8
+ from app.modules.request_logs.schemas import (
9
+ RequestLogFilterOptionsResponse,
10
+ RequestLogModelOption,
11
+ RequestLogsResponse,
12
+ )
13
+ from app.modules.request_logs.service import RequestLogModelOption as ServiceRequestLogModelOption
9
14
 
10
15
  router = APIRouter(prefix="/api/request-logs", tags=["dashboard"])
11
16
 
17
+ _MODEL_OPTION_DELIMITER = ":::"
18
+
19
+
20
+ def _parse_model_option(value: str) -> ServiceRequestLogModelOption | None:
21
+ raw = (value or "").strip()
22
+ if not raw:
23
+ return None
24
+ if _MODEL_OPTION_DELIMITER not in raw:
25
+ return ServiceRequestLogModelOption(model=raw, reasoning_effort=None)
26
+ model, effort = raw.split(_MODEL_OPTION_DELIMITER, 1)
27
+ model = model.strip()
28
+ effort = effort.strip()
29
+ if not model:
30
+ return None
31
+ return ServiceRequestLogModelOption(model=model, reasoning_effort=effort or None)
32
+
12
33
 
13
34
  @router.get("", response_model=RequestLogsResponse)
14
35
  async def list_request_logs(
15
- limit: int = Query(50, ge=1, le=200),
16
- account_id: str | None = Query(default=None, alias="accountId"),
17
- status: str | None = Query(default=None),
18
- model: str | None = Query(default=None),
36
+ limit: int = Query(50, ge=1, le=1000),
37
+ offset: int = Query(0, ge=0),
38
+ search: str | None = Query(default=None),
39
+ account_id: list[str] | None = Query(default=None, alias="accountId"),
40
+ status: list[str] | None = Query(default=None),
41
+ model: list[str] | None = Query(default=None),
42
+ reasoning_effort: list[str] | None = Query(default=None, alias="reasoningEffort"),
43
+ model_option: list[str] | None = Query(default=None, alias="modelOption"),
19
44
  since: datetime | None = Query(default=None),
20
45
  until: datetime | None = Query(default=None),
21
46
  context: RequestLogsContext = Depends(get_request_logs_context),
22
47
  ) -> RequestLogsResponse:
48
+ parsed_options: list[ServiceRequestLogModelOption] | None = None
49
+ if model_option:
50
+ parsed = [_parse_model_option(value) for value in model_option]
51
+ parsed_options = [value for value in parsed if value is not None] or None
23
52
  logs = await context.service.list_recent(
24
53
  limit=limit,
54
+ offset=offset,
55
+ search=search,
25
56
  since=since,
26
57
  until=until,
27
- account_id=account_id,
28
- model=model,
58
+ account_ids=account_id,
59
+ model_options=parsed_options,
60
+ models=model,
61
+ reasoning_efforts=reasoning_effort,
29
62
  status=status,
30
63
  )
31
64
  return RequestLogsResponse(requests=logs)
65
+
66
+
67
+ @router.get("/options", response_model=RequestLogFilterOptionsResponse)
68
+ async def list_request_log_filter_options(
69
+ status: list[str] | None = Query(default=None),
70
+ since: datetime | None = Query(default=None),
71
+ until: datetime | None = Query(default=None),
72
+ context: RequestLogsContext = Depends(get_request_logs_context),
73
+ ) -> RequestLogFilterOptionsResponse:
74
+ options = await context.service.list_filter_options(
75
+ status=status,
76
+ since=since,
77
+ until=until,
78
+ )
79
+ return RequestLogFilterOptionsResponse(
80
+ account_ids=options.account_ids,
81
+ model_options=[
82
+ RequestLogModelOption(model=option.model, reasoning_effort=option.reasoning_effort)
83
+ for option in options.model_options
84
+ ],
85
+ )
@@ -3,12 +3,13 @@ from __future__ import annotations
3
3
  from datetime import datetime
4
4
 
5
5
  import anyio
6
- from sqlalchemy import and_, select
6
+ from sqlalchemy import String, and_, cast, or_, select
7
+ from sqlalchemy import exc as sa_exc
7
8
  from sqlalchemy.ext.asyncio import AsyncSession
8
9
 
9
10
  from app.core.utils.request_id import ensure_request_id
10
11
  from app.core.utils.time import utcnow
11
- from app.db.models import RequestLog
12
+ from app.db.models import Account, RequestLog
12
13
 
13
14
 
14
15
  class RequestLogsRepository:
@@ -56,6 +57,8 @@ class RequestLogsRepository:
56
57
  await self._session.commit()
57
58
  await self._session.refresh(log)
58
59
  return log
60
+ except sa_exc.ResourceClosedError:
61
+ return log
59
62
  except BaseException:
60
63
  await _safe_rollback(self._session)
61
64
  raise
@@ -63,35 +66,147 @@ class RequestLogsRepository:
63
66
  async def list_recent(
64
67
  self,
65
68
  limit: int = 50,
69
+ offset: int = 0,
70
+ search: str | None = None,
66
71
  since: datetime | None = None,
67
72
  until: datetime | None = None,
68
- account_id: str | None = None,
69
- model: str | None = None,
70
- status: str | None = None,
71
- error_codes: list[str] | None = None,
73
+ account_ids: list[str] | None = None,
74
+ model_options: list[tuple[str, str | None]] | None = None,
75
+ models: list[str] | None = None,
76
+ reasoning_efforts: list[str] | None = None,
77
+ include_success: bool = True,
78
+ include_error_other: bool = True,
79
+ error_codes_in: list[str] | None = None,
80
+ error_codes_excluding: list[str] | None = None,
72
81
  ) -> list[RequestLog]:
73
82
  conditions = []
74
83
  if since is not None:
75
84
  conditions.append(RequestLog.requested_at >= since)
76
85
  if until is not None:
77
86
  conditions.append(RequestLog.requested_at <= until)
78
- if account_id is not None:
79
- conditions.append(RequestLog.account_id == account_id)
80
- if model is not None:
81
- conditions.append(RequestLog.model == model)
82
- if status is not None:
83
- conditions.append(RequestLog.status == status)
84
- if error_codes:
85
- conditions.append(RequestLog.error_code.in_(error_codes))
86
-
87
- stmt = select(RequestLog).order_by(RequestLog.requested_at.desc())
87
+ if account_ids:
88
+ conditions.append(RequestLog.account_id.in_(account_ids))
89
+
90
+ if model_options:
91
+ pair_conditions = []
92
+ for model, effort in model_options:
93
+ base = (model or "").strip()
94
+ if not base:
95
+ continue
96
+ if effort is None:
97
+ pair_conditions.append(and_(RequestLog.model == base, RequestLog.reasoning_effort.is_(None)))
98
+ else:
99
+ pair_conditions.append(and_(RequestLog.model == base, RequestLog.reasoning_effort == effort))
100
+ if pair_conditions:
101
+ conditions.append(or_(*pair_conditions))
102
+ else:
103
+ if models:
104
+ conditions.append(RequestLog.model.in_(models))
105
+ if reasoning_efforts:
106
+ conditions.append(RequestLog.reasoning_effort.in_(reasoning_efforts))
107
+
108
+ status_conditions = []
109
+ if include_success:
110
+ status_conditions.append(RequestLog.status == "success")
111
+ if error_codes_in:
112
+ status_conditions.append(and_(RequestLog.status == "error", RequestLog.error_code.in_(error_codes_in)))
113
+ if include_error_other:
114
+ error_clause = [RequestLog.status == "error"]
115
+ if error_codes_excluding:
116
+ error_clause.append(
117
+ or_(
118
+ RequestLog.error_code.is_(None),
119
+ ~RequestLog.error_code.in_(error_codes_excluding),
120
+ )
121
+ )
122
+ status_conditions.append(and_(*error_clause))
123
+ if status_conditions:
124
+ conditions.append(or_(*status_conditions))
125
+ if search:
126
+ search_pattern = f"%{search}%"
127
+ conditions.append(
128
+ or_(
129
+ RequestLog.account_id.ilike(search_pattern),
130
+ Account.email.ilike(search_pattern),
131
+ RequestLog.request_id.ilike(search_pattern),
132
+ RequestLog.model.ilike(search_pattern),
133
+ RequestLog.reasoning_effort.ilike(search_pattern),
134
+ RequestLog.status.ilike(search_pattern),
135
+ RequestLog.error_code.ilike(search_pattern),
136
+ RequestLog.error_message.ilike(search_pattern),
137
+ cast(RequestLog.requested_at, String).ilike(search_pattern),
138
+ cast(RequestLog.input_tokens, String).ilike(search_pattern),
139
+ cast(RequestLog.output_tokens, String).ilike(search_pattern),
140
+ cast(RequestLog.cached_input_tokens, String).ilike(search_pattern),
141
+ cast(RequestLog.reasoning_tokens, String).ilike(search_pattern),
142
+ cast(RequestLog.latency_ms, String).ilike(search_pattern),
143
+ )
144
+ )
145
+
146
+ stmt = (
147
+ select(RequestLog)
148
+ .outerjoin(Account, Account.id == RequestLog.account_id)
149
+ .order_by(RequestLog.requested_at.desc())
150
+ )
88
151
  if conditions:
89
152
  stmt = stmt.where(and_(*conditions))
153
+ if offset:
154
+ stmt = stmt.offset(offset)
90
155
  if limit:
91
156
  stmt = stmt.limit(limit)
92
157
  result = await self._session.execute(stmt)
93
158
  return list(result.scalars().all())
94
159
 
160
+ async def list_filter_options(
161
+ self,
162
+ since: datetime | None = None,
163
+ until: datetime | None = None,
164
+ include_success: bool = True,
165
+ include_error_other: bool = True,
166
+ error_codes_in: list[str] | None = None,
167
+ error_codes_excluding: list[str] | None = None,
168
+ ) -> tuple[list[str], list[tuple[str, str | None]]]:
169
+ conditions = []
170
+ if since is not None:
171
+ conditions.append(RequestLog.requested_at >= since)
172
+ if until is not None:
173
+ conditions.append(RequestLog.requested_at <= until)
174
+ status_conditions = []
175
+ if include_success:
176
+ status_conditions.append(RequestLog.status == "success")
177
+ if error_codes_in:
178
+ status_conditions.append(and_(RequestLog.status == "error", RequestLog.error_code.in_(error_codes_in)))
179
+ if include_error_other:
180
+ error_clause = [RequestLog.status == "error"]
181
+ if error_codes_excluding:
182
+ error_clause.append(
183
+ or_(
184
+ RequestLog.error_code.is_(None),
185
+ ~RequestLog.error_code.in_(error_codes_excluding),
186
+ )
187
+ )
188
+ status_conditions.append(and_(*error_clause))
189
+ if status_conditions:
190
+ conditions.append(or_(*status_conditions))
191
+
192
+ account_stmt = select(RequestLog.account_id).distinct().order_by(RequestLog.account_id.asc())
193
+ model_stmt = (
194
+ select(RequestLog.model, RequestLog.reasoning_effort)
195
+ .distinct()
196
+ .order_by(RequestLog.model.asc(), RequestLog.reasoning_effort.asc())
197
+ )
198
+ if conditions:
199
+ clause = and_(*conditions)
200
+ account_stmt = account_stmt.where(clause)
201
+ model_stmt = model_stmt.where(clause)
202
+
203
+ account_rows = await self._session.execute(account_stmt)
204
+ model_rows = await self._session.execute(model_stmt)
205
+
206
+ account_ids = [row[0] for row in account_rows.all() if row[0]]
207
+ model_options = [(row[0], row[1]) for row in model_rows.all() if row[0]]
208
+ return account_ids, model_options
209
+
95
210
 
96
211
  async def _safe_rollback(session: AsyncSession) -> None:
97
212
  if not session.in_transaction():
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import List
5
4
 
6
5
  from pydantic import Field
7
6
 
@@ -24,4 +23,14 @@ class RequestLogEntry(DashboardModel):
24
23
 
25
24
 
26
25
  class RequestLogsResponse(DashboardModel):
27
- requests: List[RequestLogEntry] = Field(default_factory=list)
26
+ requests: list[RequestLogEntry] = Field(default_factory=list)
27
+
28
+
29
+ class RequestLogModelOption(DashboardModel):
30
+ model: str
31
+ reasoning_effort: str | None = None
32
+
33
+
34
+ class RequestLogFilterOptionsResponse(DashboardModel):
35
+ account_ids: list[str] = Field(default_factory=list)
36
+ model_options: list[RequestLogModelOption] = Field(default_factory=list)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
3
4
  from datetime import datetime
4
5
  from typing import cast
5
6
 
@@ -17,6 +18,26 @@ RATE_LIMIT_CODES = {"rate_limit_exceeded", "usage_limit_reached"}
17
18
  QUOTA_CODES = {"insufficient_quota", "usage_not_included", "quota_exceeded"}
18
19
 
19
20
 
21
+ @dataclass(frozen=True, slots=True)
22
+ class RequestLogModelOption:
23
+ model: str
24
+ reasoning_effort: str | None
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class RequestLogStatusFilter:
29
+ include_success: bool
30
+ include_error_other: bool
31
+ error_codes_in: list[str] | None
32
+ error_codes_excluding: list[str] | None
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class RequestLogFilterOptions:
37
+ account_ids: list[str]
38
+ model_options: list[RequestLogModelOption]
39
+
40
+
20
41
  class RequestLogsService:
21
42
  def __init__(self, repo: RequestLogsRepository) -> None:
22
43
  self._repo = repo
@@ -24,38 +45,94 @@ class RequestLogsService:
24
45
  async def list_recent(
25
46
  self,
26
47
  limit: int = 50,
48
+ offset: int = 0,
49
+ search: str | None = None,
27
50
  since: datetime | None = None,
28
51
  until: datetime | None = None,
29
- account_id: str | None = None,
30
- model: str | None = None,
31
- status: str | None = None,
52
+ account_ids: list[str] | None = None,
53
+ model_options: list[RequestLogModelOption] | None = None,
54
+ models: list[str] | None = None,
55
+ reasoning_efforts: list[str] | None = None,
56
+ status: list[str] | None = None,
32
57
  ) -> list[RequestLogEntry]:
33
- status_filter, error_codes = _map_status_filter(status)
58
+ status_filter = _map_status_filter(status)
34
59
  logs = await self._repo.list_recent(
35
60
  limit=limit,
61
+ offset=offset,
62
+ search=search,
36
63
  since=since,
37
64
  until=until,
38
- account_id=account_id,
39
- model=model,
40
- status=status_filter,
41
- error_codes=error_codes,
65
+ account_ids=account_ids,
66
+ model_options=(
67
+ [(option.model, option.reasoning_effort) for option in model_options] if model_options else None
68
+ ),
69
+ models=models,
70
+ reasoning_efforts=reasoning_efforts,
71
+ include_success=status_filter.include_success,
72
+ include_error_other=status_filter.include_error_other,
73
+ error_codes_in=status_filter.error_codes_in,
74
+ error_codes_excluding=status_filter.error_codes_excluding,
42
75
  )
43
76
  return [_to_entry(log) for log in logs]
44
77
 
78
+ async def list_filter_options(
79
+ self,
80
+ since: datetime | None = None,
81
+ until: datetime | None = None,
82
+ status: list[str] | None = None,
83
+ ) -> RequestLogFilterOptions:
84
+ status_filter = _map_status_filter(status)
85
+ account_ids, model_options = await self._repo.list_filter_options(
86
+ since=since,
87
+ until=until,
88
+ include_success=status_filter.include_success,
89
+ include_error_other=status_filter.include_error_other,
90
+ error_codes_in=status_filter.error_codes_in,
91
+ error_codes_excluding=status_filter.error_codes_excluding,
92
+ )
93
+ return RequestLogFilterOptions(
94
+ account_ids=account_ids,
95
+ model_options=[
96
+ RequestLogModelOption(model=model, reasoning_effort=reasoning_effort)
97
+ for model, reasoning_effort in model_options
98
+ ],
99
+ )
100
+
45
101
 
46
- def _map_status_filter(status: str | None) -> tuple[str | None, list[str] | None]:
102
+ def _map_status_filter(status: list[str] | None) -> RequestLogStatusFilter:
47
103
  if not status:
48
- return None, None
49
- normalized = status.lower()
50
- if normalized == "ok":
51
- return "success", None
52
- if normalized == "rate_limit":
53
- return "error", sorted(RATE_LIMIT_CODES)
54
- if normalized == "quota":
55
- return "error", sorted(QUOTA_CODES)
56
- if normalized == "error":
57
- return "error", None
58
- return status, None
104
+ return RequestLogStatusFilter(
105
+ include_success=True,
106
+ include_error_other=True,
107
+ error_codes_in=None,
108
+ error_codes_excluding=None,
109
+ )
110
+ normalized = {value.lower() for value in status if value}
111
+ if not normalized or "all" in normalized:
112
+ return RequestLogStatusFilter(
113
+ include_success=True,
114
+ include_error_other=True,
115
+ error_codes_in=None,
116
+ error_codes_excluding=None,
117
+ )
118
+
119
+ include_success = "ok" in normalized
120
+ include_rate_limit = "rate_limit" in normalized
121
+ include_quota = "quota" in normalized
122
+ include_error_other = "error" in normalized
123
+
124
+ error_codes_in: set[str] = set()
125
+ if include_rate_limit:
126
+ error_codes_in |= RATE_LIMIT_CODES
127
+ if include_quota:
128
+ error_codes_in |= QUOTA_CODES
129
+
130
+ return RequestLogStatusFilter(
131
+ include_success=include_success,
132
+ include_error_other=include_error_other,
133
+ error_codes_in=sorted(error_codes_in) if error_codes_in else None,
134
+ error_codes_excluding=sorted(RATE_LIMIT_CODES | QUOTA_CODES) if include_error_other else None,
135
+ )
59
136
 
60
137
 
61
138
  def _log_status(log: RequestLog) -> str:
@@ -1,7 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
4
+ import weakref
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
3
7
  from datetime import timedelta
4
- from typing import cast
8
+ from typing import AsyncContextManager, ClassVar, cast
5
9
 
6
10
  from app.core import usage as usage_core
7
11
  from app.core.usage.logs import (
@@ -37,17 +41,30 @@ from app.modules.usage.schemas import (
37
41
  from app.modules.usage.updater import UsageUpdater
38
42
 
39
43
 
44
+ @dataclass(slots=True)
45
+ class _RefreshState:
46
+ lock: asyncio.Lock
47
+ task: asyncio.Task[None] | None = None
48
+
49
+
40
50
  class UsageService:
51
+ _refresh_states: ClassVar[weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, _RefreshState]] = (
52
+ weakref.WeakKeyDictionary()
53
+ )
54
+
41
55
  def __init__(
42
56
  self,
43
57
  usage_repo: UsageRepository,
44
58
  logs_repo: RequestLogsRepository,
45
59
  accounts_repo: AccountsRepository,
60
+ refresh_repo_factory: Callable[[], AsyncContextManager[tuple[UsageRepository, AccountsRepository]]]
61
+ | None = None,
46
62
  ) -> None:
47
63
  self._usage_repo = usage_repo
48
64
  self._logs_repo = logs_repo
49
65
  self._accounts_repo = accounts_repo
50
66
  self._usage_updater = UsageUpdater(usage_repo, accounts_repo)
67
+ self._refresh_repo_factory = refresh_repo_factory
51
68
 
52
69
  async def get_usage_summary(self) -> UsageSummaryResponse:
53
70
  await self._refresh_usage()
@@ -114,9 +131,53 @@ class UsageService:
114
131
  )
115
132
 
116
133
  async def _refresh_usage(self) -> None:
117
- accounts = await self._accounts_repo.list_accounts()
118
- latest_usage = await self._usage_repo.latest_by_account(window="primary")
119
- await self._usage_updater.refresh_accounts(accounts, latest_usage)
134
+ state = self._refresh_state()
135
+ task = state.task
136
+ if task and not task.done():
137
+ await asyncio.shield(task)
138
+ return
139
+
140
+ created = False
141
+ async with state.lock:
142
+ task = state.task
143
+ if not task or task.done():
144
+ task = asyncio.create_task(self._refresh_usage_once())
145
+ state.task = task
146
+ created = True
147
+
148
+ if task is None:
149
+ return
150
+
151
+ try:
152
+ if created:
153
+ await asyncio.shield(task)
154
+ else:
155
+ await asyncio.shield(task)
156
+ finally:
157
+ if task.done() and state.task is task:
158
+ state.task = None
159
+
160
+ async def _refresh_usage_once(self) -> None:
161
+ if self._refresh_repo_factory is None:
162
+ accounts = await self._accounts_repo.list_accounts()
163
+ latest_usage = await self._usage_repo.latest_by_account(window="primary")
164
+ await self._usage_updater.refresh_accounts(accounts, latest_usage)
165
+ return
166
+
167
+ async with self._refresh_repo_factory() as (usage_repo, accounts_repo):
168
+ latest_usage = await usage_repo.latest_by_account(window="primary")
169
+ accounts = await accounts_repo.list_accounts()
170
+ updater = UsageUpdater(usage_repo, accounts_repo)
171
+ await updater.refresh_accounts(accounts, latest_usage)
172
+
173
+ @classmethod
174
+ def _refresh_state(cls) -> _RefreshState:
175
+ loop = asyncio.get_running_loop()
176
+ state = cls._refresh_states.get(loop)
177
+ if state is None:
178
+ state = _RefreshState(lock=asyncio.Lock())
179
+ cls._refresh_states[loop] = state
180
+ return state
120
181
 
121
182
  async def _latest_usage_rows(self, window: str) -> list[UsageWindowRow]:
122
183
  latest = await self._usage_repo.latest_by_account(window=window)