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.
Files changed (80) hide show
  1. app/__init__.py +5 -0
  2. app/cli.py +24 -0
  3. app/core/__init__.py +0 -0
  4. app/core/auth/__init__.py +96 -0
  5. app/core/auth/models.py +49 -0
  6. app/core/auth/refresh.py +144 -0
  7. app/core/balancer/__init__.py +19 -0
  8. app/core/balancer/logic.py +140 -0
  9. app/core/balancer/types.py +9 -0
  10. app/core/clients/__init__.py +0 -0
  11. app/core/clients/http.py +39 -0
  12. app/core/clients/oauth.py +340 -0
  13. app/core/clients/proxy.py +265 -0
  14. app/core/clients/usage.py +143 -0
  15. app/core/config/__init__.py +0 -0
  16. app/core/config/settings.py +69 -0
  17. app/core/crypto.py +37 -0
  18. app/core/errors.py +73 -0
  19. app/core/openai/__init__.py +0 -0
  20. app/core/openai/models.py +122 -0
  21. app/core/openai/parsing.py +55 -0
  22. app/core/openai/requests.py +59 -0
  23. app/core/types.py +4 -0
  24. app/core/usage/__init__.py +185 -0
  25. app/core/usage/logs.py +57 -0
  26. app/core/usage/models.py +35 -0
  27. app/core/usage/pricing.py +172 -0
  28. app/core/usage/types.py +95 -0
  29. app/core/utils/__init__.py +0 -0
  30. app/core/utils/request_id.py +30 -0
  31. app/core/utils/retry.py +16 -0
  32. app/core/utils/sse.py +13 -0
  33. app/core/utils/time.py +19 -0
  34. app/db/__init__.py +0 -0
  35. app/db/models.py +82 -0
  36. app/db/session.py +44 -0
  37. app/dependencies.py +123 -0
  38. app/main.py +124 -0
  39. app/modules/__init__.py +0 -0
  40. app/modules/accounts/__init__.py +0 -0
  41. app/modules/accounts/api.py +81 -0
  42. app/modules/accounts/repository.py +80 -0
  43. app/modules/accounts/schemas.py +66 -0
  44. app/modules/accounts/service.py +211 -0
  45. app/modules/health/__init__.py +0 -0
  46. app/modules/health/api.py +10 -0
  47. app/modules/oauth/__init__.py +0 -0
  48. app/modules/oauth/api.py +57 -0
  49. app/modules/oauth/schemas.py +32 -0
  50. app/modules/oauth/service.py +356 -0
  51. app/modules/oauth/templates/oauth_success.html +122 -0
  52. app/modules/proxy/__init__.py +0 -0
  53. app/modules/proxy/api.py +76 -0
  54. app/modules/proxy/auth_manager.py +51 -0
  55. app/modules/proxy/load_balancer.py +208 -0
  56. app/modules/proxy/schemas.py +85 -0
  57. app/modules/proxy/service.py +707 -0
  58. app/modules/proxy/types.py +37 -0
  59. app/modules/proxy/usage_updater.py +147 -0
  60. app/modules/request_logs/__init__.py +0 -0
  61. app/modules/request_logs/api.py +31 -0
  62. app/modules/request_logs/repository.py +86 -0
  63. app/modules/request_logs/schemas.py +25 -0
  64. app/modules/request_logs/service.py +77 -0
  65. app/modules/shared/__init__.py +0 -0
  66. app/modules/shared/schemas.py +8 -0
  67. app/modules/usage/__init__.py +0 -0
  68. app/modules/usage/api.py +31 -0
  69. app/modules/usage/repository.py +113 -0
  70. app/modules/usage/schemas.py +62 -0
  71. app/modules/usage/service.py +246 -0
  72. app/static/7.css +1336 -0
  73. app/static/index.css +543 -0
  74. app/static/index.html +457 -0
  75. app/static/index.js +1898 -0
  76. codex_lb-0.1.2.dist-info/METADATA +108 -0
  77. codex_lb-0.1.2.dist-info/RECORD +80 -0
  78. codex_lb-0.1.2.dist-info/WHEEL +4 -0
  79. codex_lb-0.1.2.dist-info/entry_points.txt +2 -0
  80. 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
+ )