codex-lb 0.1.2__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.
- app/__init__.py +5 -0
- app/cli.py +24 -0
- app/core/__init__.py +0 -0
- app/core/auth/__init__.py +96 -0
- app/core/auth/models.py +49 -0
- app/core/auth/refresh.py +144 -0
- app/core/balancer/__init__.py +19 -0
- app/core/balancer/logic.py +140 -0
- app/core/balancer/types.py +9 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/http.py +39 -0
- app/core/clients/oauth.py +340 -0
- app/core/clients/proxy.py +265 -0
- app/core/clients/usage.py +143 -0
- app/core/config/__init__.py +0 -0
- app/core/config/settings.py +69 -0
- app/core/crypto.py +37 -0
- app/core/errors.py +73 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/models.py +122 -0
- app/core/openai/parsing.py +55 -0
- app/core/openai/requests.py +59 -0
- app/core/types.py +4 -0
- app/core/usage/__init__.py +185 -0
- app/core/usage/logs.py +57 -0
- app/core/usage/models.py +35 -0
- app/core/usage/pricing.py +172 -0
- app/core/usage/types.py +95 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/request_id.py +30 -0
- app/core/utils/retry.py +16 -0
- app/core/utils/sse.py +13 -0
- app/core/utils/time.py +19 -0
- app/db/__init__.py +0 -0
- app/db/models.py +82 -0
- app/db/session.py +44 -0
- app/dependencies.py +123 -0
- app/main.py +124 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +81 -0
- app/modules/accounts/repository.py +80 -0
- app/modules/accounts/schemas.py +66 -0
- app/modules/accounts/service.py +211 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +10 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +57 -0
- app/modules/oauth/schemas.py +32 -0
- app/modules/oauth/service.py +356 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/api.py +76 -0
- app/modules/proxy/auth_manager.py +51 -0
- app/modules/proxy/load_balancer.py +208 -0
- app/modules/proxy/schemas.py +85 -0
- app/modules/proxy/service.py +707 -0
- app/modules/proxy/types.py +37 -0
- app/modules/proxy/usage_updater.py +147 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +31 -0
- app/modules/request_logs/repository.py +86 -0
- app/modules/request_logs/schemas.py +25 -0
- app/modules/request_logs/service.py +77 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +8 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/api.py +31 -0
- app/modules/usage/repository.py +113 -0
- app/modules/usage/schemas.py +62 -0
- app/modules/usage/service.py +246 -0
- app/static/7.css +1336 -0
- app/static/index.css +543 -0
- app/static/index.html +457 -0
- app/static/index.js +1898 -0
- codex_lb-0.1.2.dist-info/METADATA +108 -0
- codex_lb-0.1.2.dist-info/RECORD +80 -0
- codex_lb-0.1.2.dist-info/WHEEL +4 -0
- codex_lb-0.1.2.dist-info/entry_points.txt +2 -0
- codex_lb-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
from app.core import usage as usage_core
|
|
6
|
+
from app.core.usage.logs import cost_from_log, total_tokens_from_log, usage_tokens_from_log
|
|
7
|
+
from app.core.usage.pricing import CostItem, calculate_costs
|
|
8
|
+
from app.core.usage.types import (
|
|
9
|
+
UsageCostSummary,
|
|
10
|
+
UsageMetricsSummary,
|
|
11
|
+
UsageSummaryPayload,
|
|
12
|
+
UsageWindowRow,
|
|
13
|
+
UsageWindowSnapshot,
|
|
14
|
+
)
|
|
15
|
+
from app.core.utils.time import from_epoch_seconds, utcnow
|
|
16
|
+
from app.db.models import Account, RequestLog
|
|
17
|
+
from app.modules.accounts.repository import AccountsRepository
|
|
18
|
+
from app.modules.proxy.usage_updater import UsageUpdater
|
|
19
|
+
from app.modules.request_logs.repository import RequestLogsRepository
|
|
20
|
+
from app.modules.usage.repository import UsageRepository
|
|
21
|
+
from app.modules.usage.schemas import (
|
|
22
|
+
UsageCost,
|
|
23
|
+
UsageCostByModel,
|
|
24
|
+
UsageHistoryItem,
|
|
25
|
+
UsageHistoryResponse,
|
|
26
|
+
UsageMetrics,
|
|
27
|
+
UsageSummaryResponse,
|
|
28
|
+
UsageWindow,
|
|
29
|
+
UsageWindowResponse,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UsageService:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
usage_repo: UsageRepository,
|
|
37
|
+
logs_repo: RequestLogsRepository,
|
|
38
|
+
accounts_repo: AccountsRepository,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._usage_repo = usage_repo
|
|
41
|
+
self._logs_repo = logs_repo
|
|
42
|
+
self._accounts_repo = accounts_repo
|
|
43
|
+
self._usage_updater = UsageUpdater(usage_repo, accounts_repo)
|
|
44
|
+
|
|
45
|
+
async def get_usage_summary(self) -> UsageSummaryResponse:
|
|
46
|
+
await self._refresh_usage()
|
|
47
|
+
now = utcnow()
|
|
48
|
+
accounts = await self._accounts_repo.list_accounts()
|
|
49
|
+
account_map = {account.id: account for account in accounts}
|
|
50
|
+
|
|
51
|
+
primary_rows = await self._latest_usage_rows("primary")
|
|
52
|
+
secondary_rows = await self._latest_usage_rows("secondary")
|
|
53
|
+
|
|
54
|
+
primary_window = usage_core.summarize_usage_window(primary_rows, account_map, "primary")
|
|
55
|
+
secondary_window = usage_core.summarize_usage_window(secondary_rows, account_map, "secondary")
|
|
56
|
+
|
|
57
|
+
secondary_minutes = await self._usage_repo.latest_window_minutes("secondary")
|
|
58
|
+
if secondary_minutes is None:
|
|
59
|
+
secondary_minutes = usage_core.default_window_minutes("secondary")
|
|
60
|
+
logs_secondary: list[RequestLog]
|
|
61
|
+
if secondary_minutes:
|
|
62
|
+
logs_secondary = await self._logs_repo.list_since(now - timedelta(minutes=secondary_minutes))
|
|
63
|
+
else:
|
|
64
|
+
logs_secondary = []
|
|
65
|
+
cost_items = [item for item in (_log_to_cost_item(log) for log in logs_secondary) if item]
|
|
66
|
+
cost = calculate_costs(cost_items)
|
|
67
|
+
|
|
68
|
+
metrics = _usage_metrics(logs_secondary)
|
|
69
|
+
payload = usage_core.parse_usage_summary(primary_window, secondary_window, cost, metrics)
|
|
70
|
+
return _summary_payload_to_response(payload)
|
|
71
|
+
|
|
72
|
+
async def get_usage_history(self, hours: int) -> UsageHistoryResponse:
|
|
73
|
+
await self._refresh_usage()
|
|
74
|
+
now = utcnow()
|
|
75
|
+
since = now - timedelta(hours=hours)
|
|
76
|
+
accounts = await self._accounts_repo.list_accounts()
|
|
77
|
+
account_map = {account.id: account for account in accounts}
|
|
78
|
+
usage_rows = [row.to_window_row() for row in await self._usage_repo.aggregate_since(since, window="primary")]
|
|
79
|
+
logs = await self._logs_repo.list_since(since)
|
|
80
|
+
|
|
81
|
+
accounts_history = _build_account_history(usage_rows, logs, account_map, "primary")
|
|
82
|
+
return UsageHistoryResponse(window_hours=hours, accounts=accounts_history)
|
|
83
|
+
|
|
84
|
+
async def get_usage_window(self, window: str) -> UsageWindowResponse:
|
|
85
|
+
await self._refresh_usage()
|
|
86
|
+
now = utcnow()
|
|
87
|
+
window_key = (window or "").lower()
|
|
88
|
+
if window_key not in {"primary", "secondary"}:
|
|
89
|
+
raise ValueError("window must be 'primary' or 'secondary'")
|
|
90
|
+
accounts = await self._accounts_repo.list_accounts()
|
|
91
|
+
account_map = {account.id: account for account in accounts}
|
|
92
|
+
usage_rows = await self._latest_usage_rows(window_key)
|
|
93
|
+
window_minutes = await self._usage_repo.latest_window_minutes(window_key)
|
|
94
|
+
if window_minutes is None:
|
|
95
|
+
window_minutes = usage_core.default_window_minutes(window_key)
|
|
96
|
+
logs: list[RequestLog]
|
|
97
|
+
if window_minutes:
|
|
98
|
+
logs = await self._logs_repo.list_since(now - timedelta(minutes=window_minutes))
|
|
99
|
+
else:
|
|
100
|
+
logs = []
|
|
101
|
+
|
|
102
|
+
accounts_history = _build_account_history(usage_rows, logs, account_map, window_key)
|
|
103
|
+
return UsageWindowResponse(
|
|
104
|
+
window_key=window_key,
|
|
105
|
+
window_minutes=window_minutes,
|
|
106
|
+
accounts=accounts_history,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def _refresh_usage(self) -> None:
|
|
110
|
+
accounts = await self._accounts_repo.list_accounts()
|
|
111
|
+
latest_usage = await self._usage_repo.latest_by_account(window="primary")
|
|
112
|
+
await self._usage_updater.refresh_accounts(accounts, latest_usage)
|
|
113
|
+
|
|
114
|
+
async def _latest_usage_rows(self, window: str) -> list[UsageWindowRow]:
|
|
115
|
+
latest = await self._usage_repo.latest_by_account(window=window)
|
|
116
|
+
return [
|
|
117
|
+
UsageWindowRow(
|
|
118
|
+
account_id=entry.account_id,
|
|
119
|
+
used_percent=entry.used_percent,
|
|
120
|
+
reset_at=entry.reset_at,
|
|
121
|
+
window_minutes=entry.window_minutes,
|
|
122
|
+
)
|
|
123
|
+
for entry in latest.values()
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_account_history(
|
|
128
|
+
usage_rows: list[UsageWindowRow],
|
|
129
|
+
logs: list[RequestLog],
|
|
130
|
+
account_map: dict[str, Account],
|
|
131
|
+
window: str,
|
|
132
|
+
) -> list[UsageHistoryItem]:
|
|
133
|
+
usage_by_account = {row.account_id: row for row in usage_rows}
|
|
134
|
+
counts: dict[str, int] = {}
|
|
135
|
+
costs: dict[str, float] = {}
|
|
136
|
+
|
|
137
|
+
for log in logs:
|
|
138
|
+
account_id = log.account_id
|
|
139
|
+
counts[account_id] = counts.get(account_id, 0) + 1
|
|
140
|
+
cost = cost_from_log(log)
|
|
141
|
+
if cost is None:
|
|
142
|
+
continue
|
|
143
|
+
costs[account_id] = costs.get(account_id, 0.0) + cost
|
|
144
|
+
|
|
145
|
+
results: list[UsageHistoryItem] = []
|
|
146
|
+
for account_id, account in account_map.items():
|
|
147
|
+
usage = usage_by_account.get(account_id)
|
|
148
|
+
used_percent = usage.used_percent if usage else None
|
|
149
|
+
used_percent_value = float(used_percent) if used_percent is not None else 0.0
|
|
150
|
+
remaining_percent = usage_core.remaining_percent_from_used(used_percent_value) or 0.0
|
|
151
|
+
capacity = usage_core.capacity_for_plan(account.plan_type, window)
|
|
152
|
+
remaining_credits = usage_core.remaining_credits_from_percent(used_percent_value, capacity) or 0.0
|
|
153
|
+
results.append(
|
|
154
|
+
UsageHistoryItem(
|
|
155
|
+
account_id=account_id,
|
|
156
|
+
email=account.email,
|
|
157
|
+
remaining_percent_avg=remaining_percent,
|
|
158
|
+
capacity_credits=float(capacity or 0.0),
|
|
159
|
+
remaining_credits=float(remaining_credits),
|
|
160
|
+
request_count=counts.get(account_id, 0),
|
|
161
|
+
cost_usd=round(costs.get(account_id, 0.0), 6),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return results
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _log_to_cost_item(log: RequestLog) -> CostItem | None:
|
|
168
|
+
model = log.model
|
|
169
|
+
usage = usage_tokens_from_log(log)
|
|
170
|
+
if not model or not usage:
|
|
171
|
+
return None
|
|
172
|
+
return CostItem(model=model, usage=usage)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _usage_metrics(logs_secondary: list[RequestLog]) -> UsageMetricsSummary:
|
|
176
|
+
total_requests = len(logs_secondary)
|
|
177
|
+
error_logs = [log for log in logs_secondary if log.status != "success"]
|
|
178
|
+
error_rate: float | None = None
|
|
179
|
+
if total_requests > 0:
|
|
180
|
+
error_rate = len(error_logs) / total_requests
|
|
181
|
+
top_error = _top_error_code(error_logs)
|
|
182
|
+
tokens_secondary = _sum_tokens(logs_secondary)
|
|
183
|
+
return UsageMetricsSummary(
|
|
184
|
+
requests_7d=total_requests,
|
|
185
|
+
tokens_secondary_window=tokens_secondary,
|
|
186
|
+
error_rate_7d=error_rate,
|
|
187
|
+
top_error=top_error,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _sum_tokens(logs: list[RequestLog]) -> int:
|
|
192
|
+
total = 0
|
|
193
|
+
for log in logs:
|
|
194
|
+
total += total_tokens_from_log(log) or 0
|
|
195
|
+
return total
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _top_error_code(logs: list[RequestLog]) -> str | None:
|
|
199
|
+
counts: dict[str, int] = {}
|
|
200
|
+
for log in logs:
|
|
201
|
+
code = log.error_code
|
|
202
|
+
if not code:
|
|
203
|
+
continue
|
|
204
|
+
counts[code] = counts.get(code, 0) + 1
|
|
205
|
+
if not counts:
|
|
206
|
+
return None
|
|
207
|
+
return max(counts.items(), key=lambda item: item[1])[0]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _summary_payload_to_response(payload: UsageSummaryPayload) -> UsageSummaryResponse:
|
|
211
|
+
return UsageSummaryResponse(
|
|
212
|
+
primary_window=_window_snapshot_to_model(payload.primary_window),
|
|
213
|
+
secondary_window=_window_snapshot_to_model(payload.secondary_window) if payload.secondary_window else None,
|
|
214
|
+
cost=_cost_summary_to_model(payload.cost),
|
|
215
|
+
metrics=_metrics_summary_to_model(payload.metrics) if payload.metrics else None,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _window_snapshot_to_model(snapshot: UsageWindowSnapshot) -> UsageWindow:
|
|
220
|
+
capacity_credits = float(snapshot.capacity_credits)
|
|
221
|
+
remaining_credits = usage_core.remaining_credits_from_used(snapshot.used_credits, capacity_credits) or 0.0
|
|
222
|
+
remaining_percent = max(0.0, 100.0 - float(snapshot.used_percent)) if capacity_credits > 0 else 0.0
|
|
223
|
+
return UsageWindow(
|
|
224
|
+
remaining_percent=remaining_percent,
|
|
225
|
+
capacity_credits=capacity_credits,
|
|
226
|
+
remaining_credits=remaining_credits,
|
|
227
|
+
reset_at=from_epoch_seconds(snapshot.reset_at),
|
|
228
|
+
window_minutes=snapshot.window_minutes,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _cost_summary_to_model(cost: UsageCostSummary) -> UsageCost:
|
|
233
|
+
return UsageCost(
|
|
234
|
+
currency=cost.currency,
|
|
235
|
+
total_usd_7d=cost.total_usd_7d,
|
|
236
|
+
by_model=[UsageCostByModel(model=item.model, usd=item.usd) for item in cost.by_model],
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _metrics_summary_to_model(metrics: UsageMetricsSummary) -> UsageMetrics:
|
|
241
|
+
return UsageMetrics(
|
|
242
|
+
requests_7d=metrics.requests_7d,
|
|
243
|
+
tokens_secondary_window=metrics.tokens_secondary_window,
|
|
244
|
+
error_rate_7d=metrics.error_rate_7d,
|
|
245
|
+
top_error=metrics.top_error,
|
|
246
|
+
)
|