codex-lb 0.1.5__py3-none-any.whl → 0.2.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.
- app/__init__.py +1 -1
- app/core/auth/__init__.py +2 -1
- app/core/balancer/logic.py +12 -2
- app/core/clients/proxy.py +2 -4
- app/core/config/settings.py +2 -1
- app/core/plan_types.py +64 -0
- app/core/types.py +4 -2
- app/core/usage/__init__.py +3 -2
- app/core/usage/quota.py +58 -0
- app/core/utils/sse.py +6 -2
- app/db/migrations/__init__.py +80 -0
- app/db/migrations/versions/__init__.py +1 -0
- app/db/migrations/versions/normalize_account_plan_types.py +17 -0
- app/db/session.py +14 -0
- app/dependencies.py +0 -8
- app/main.py +4 -4
- app/modules/{proxy → accounts}/auth_manager.py +33 -4
- app/modules/accounts/repository.py +3 -3
- app/modules/accounts/service.py +10 -7
- app/modules/health/api.py +5 -3
- app/modules/health/schemas.py +9 -0
- app/modules/oauth/service.py +5 -1
- app/modules/proxy/helpers.py +285 -0
- app/modules/proxy/load_balancer.py +12 -36
- app/modules/proxy/service.py +37 -307
- app/modules/request_logs/service.py +5 -3
- app/modules/usage/service.py +7 -6
- app/modules/{proxy/usage_updater.py → usage/updater.py} +1 -1
- app/static/index.js +23 -7
- {codex_lb-0.1.5.dist-info → codex_lb-0.2.0.dist-info}/METADATA +1 -1
- {codex_lb-0.1.5.dist-info → codex_lb-0.2.0.dist-info}/RECORD +34 -27
- {codex_lb-0.1.5.dist-info → codex_lb-0.2.0.dist-info}/WHEEL +0 -0
- {codex_lb-0.1.5.dist-info → codex_lb-0.2.0.dist-info}/entry_points.txt +0 -0
- {codex_lb-0.1.5.dist-info → codex_lb-0.2.0.dist-info}/licenses/LICENSE +0 -0
app/modules/proxy/service.py
CHANGED
|
@@ -3,9 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import time
|
|
5
5
|
from datetime import timedelta
|
|
6
|
-
from typing import AsyncIterator,
|
|
7
|
-
|
|
8
|
-
from pydantic import ValidationError
|
|
6
|
+
from typing import AsyncIterator, Mapping
|
|
9
7
|
|
|
10
8
|
from app.core import usage as usage_core
|
|
11
9
|
from app.core.auth.refresh import RefreshError
|
|
@@ -15,27 +13,37 @@ from app.core.clients.proxy import ProxyResponseError, filter_inbound_headers
|
|
|
15
13
|
from app.core.clients.proxy import compact_responses as core_compact_responses
|
|
16
14
|
from app.core.clients.proxy import stream_responses as core_stream_responses
|
|
17
15
|
from app.core.crypto import TokenEncryptor
|
|
18
|
-
from app.core.errors import
|
|
19
|
-
from app.core.openai.models import
|
|
16
|
+
from app.core.errors import openai_error, response_failed_event
|
|
17
|
+
from app.core.openai.models import OpenAIResponsePayload
|
|
20
18
|
from app.core.openai.parsing import parse_sse_event
|
|
21
19
|
from app.core.openai.requests import ResponsesCompactRequest, ResponsesRequest
|
|
22
|
-
from app.core.usage.types import UsageWindowRow
|
|
20
|
+
from app.core.usage.types import UsageWindowRow
|
|
23
21
|
from app.core.utils.request_id import ensure_request_id
|
|
24
22
|
from app.core.utils.sse import format_sse_event
|
|
25
23
|
from app.core.utils.time import utcnow
|
|
26
|
-
from app.db.models import Account,
|
|
24
|
+
from app.db.models import Account, UsageHistory
|
|
25
|
+
from app.modules.accounts.auth_manager import AuthManager
|
|
27
26
|
from app.modules.accounts.repository import AccountsRepository
|
|
28
|
-
from app.modules.proxy.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
from app.modules.proxy.helpers import (
|
|
28
|
+
_apply_error_metadata,
|
|
29
|
+
_credits_headers,
|
|
30
|
+
_credits_snapshot,
|
|
31
|
+
_header_account_id,
|
|
32
|
+
_normalize_error_code,
|
|
33
|
+
_parse_openai_error,
|
|
34
|
+
_plan_type_for_accounts,
|
|
35
|
+
_rate_limit_details,
|
|
36
|
+
_rate_limit_headers,
|
|
37
|
+
_select_accounts_for_limits,
|
|
38
|
+
_summarize_window,
|
|
39
|
+
_upstream_error_from_openai,
|
|
40
|
+
_window_snapshot,
|
|
35
41
|
)
|
|
36
|
-
from app.modules.proxy.
|
|
42
|
+
from app.modules.proxy.load_balancer import LoadBalancer
|
|
43
|
+
from app.modules.proxy.types import RateLimitStatusPayloadData
|
|
37
44
|
from app.modules.request_logs.repository import RequestLogsRepository
|
|
38
45
|
from app.modules.usage.repository import UsageRepository
|
|
46
|
+
from app.modules.usage.updater import UsageUpdater
|
|
39
47
|
|
|
40
48
|
logger = logging.getLogger(__name__)
|
|
41
49
|
|
|
@@ -304,7 +312,11 @@ class ProxyService:
|
|
|
304
312
|
return
|
|
305
313
|
event = parse_sse_event(first)
|
|
306
314
|
if event and event.type in ("response.failed", "error"):
|
|
307
|
-
|
|
315
|
+
if event.type == "response.failed":
|
|
316
|
+
response = event.response
|
|
317
|
+
error = response.error if response else None
|
|
318
|
+
else:
|
|
319
|
+
error = event.error
|
|
308
320
|
code = _normalize_error_code(
|
|
309
321
|
error.code if error else None,
|
|
310
322
|
error.type if error else None,
|
|
@@ -326,7 +338,11 @@ class ProxyService:
|
|
|
326
338
|
event_type = event.type
|
|
327
339
|
if event_type in ("response.failed", "error"):
|
|
328
340
|
status = "error"
|
|
329
|
-
|
|
341
|
+
if event_type == "response.failed":
|
|
342
|
+
response = event.response
|
|
343
|
+
error = response.error if response else None
|
|
344
|
+
else:
|
|
345
|
+
error = event.error
|
|
330
346
|
error_code = _normalize_error_code(
|
|
331
347
|
error.code if error else None,
|
|
332
348
|
error.type if error else None,
|
|
@@ -420,9 +436,12 @@ class ProxyService:
|
|
|
420
436
|
await self._handle_stream_error(account, _upstream_error_from_openai(error), code)
|
|
421
437
|
|
|
422
438
|
async def _handle_stream_error(self, account: Account, error: UpstreamError, code: str) -> None:
|
|
423
|
-
if code
|
|
439
|
+
if code == "rate_limit_exceeded":
|
|
424
440
|
await self._load_balancer.mark_rate_limit(account, error)
|
|
425
441
|
return
|
|
442
|
+
if code == "usage_limit_reached":
|
|
443
|
+
await self._load_balancer.mark_quota_exceeded(account, error)
|
|
444
|
+
return
|
|
426
445
|
if code in {"insufficient_quota", "usage_not_included", "quota_exceeded"}:
|
|
427
446
|
await self._load_balancer.mark_quota_exceeded(account, error)
|
|
428
447
|
return
|
|
@@ -432,297 +451,8 @@ class ProxyService:
|
|
|
432
451
|
await self._load_balancer.record_error(account)
|
|
433
452
|
|
|
434
453
|
|
|
435
|
-
def _header_account_id(account_id: str | None) -> str | None:
|
|
436
|
-
if not account_id:
|
|
437
|
-
return None
|
|
438
|
-
if account_id.startswith(("email_", "local_")):
|
|
439
|
-
return None
|
|
440
|
-
return account_id
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
KNOWN_PLAN_TYPES = {
|
|
444
|
-
"guest",
|
|
445
|
-
"free",
|
|
446
|
-
"go",
|
|
447
|
-
"plus",
|
|
448
|
-
"pro",
|
|
449
|
-
"free_workspace",
|
|
450
|
-
"team",
|
|
451
|
-
"business",
|
|
452
|
-
"education",
|
|
453
|
-
"quorum",
|
|
454
|
-
"k12",
|
|
455
|
-
"enterprise",
|
|
456
|
-
"edu",
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
PLAN_TYPE_PRIORITY = (
|
|
460
|
-
"enterprise",
|
|
461
|
-
"business",
|
|
462
|
-
"team",
|
|
463
|
-
"pro",
|
|
464
|
-
"plus",
|
|
465
|
-
"education",
|
|
466
|
-
"edu",
|
|
467
|
-
"free_workspace",
|
|
468
|
-
"free",
|
|
469
|
-
"go",
|
|
470
|
-
"guest",
|
|
471
|
-
"quorum",
|
|
472
|
-
"k12",
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
def _select_accounts_for_limits(accounts: Iterable[Account]) -> list[Account]:
|
|
477
|
-
return [account for account in accounts if account.status not in (AccountStatus.DEACTIVATED, AccountStatus.PAUSED)]
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
def _summarize_window(
|
|
481
|
-
rows: list[UsageWindowRow],
|
|
482
|
-
account_map: dict[str, Account],
|
|
483
|
-
window: str,
|
|
484
|
-
) -> UsageWindowSummary | None:
|
|
485
|
-
if not rows:
|
|
486
|
-
return None
|
|
487
|
-
return usage_core.summarize_usage_window(rows, account_map, window)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
def _window_snapshot(
|
|
491
|
-
summary: UsageWindowSummary | None,
|
|
492
|
-
rows: list[UsageWindowRow],
|
|
493
|
-
window: str,
|
|
494
|
-
now_epoch: int,
|
|
495
|
-
) -> RateLimitWindowSnapshotData | None:
|
|
496
|
-
if summary is None:
|
|
497
|
-
return None
|
|
498
|
-
|
|
499
|
-
used_percent = _normalize_used_percent(summary.used_percent, rows)
|
|
500
|
-
if used_percent is None:
|
|
501
|
-
return None
|
|
502
|
-
|
|
503
|
-
reset_at = summary.reset_at
|
|
504
|
-
if reset_at is None:
|
|
505
|
-
return None
|
|
506
|
-
|
|
507
|
-
window_minutes = summary.window_minutes or usage_core.default_window_minutes(window)
|
|
508
|
-
if not window_minutes:
|
|
509
|
-
return None
|
|
510
|
-
|
|
511
|
-
limit_window_seconds = int(window_minutes * 60)
|
|
512
|
-
reset_after_seconds = max(0, int(reset_at) - now_epoch)
|
|
513
|
-
|
|
514
|
-
return RateLimitWindowSnapshotData(
|
|
515
|
-
used_percent=_percent_to_int(used_percent),
|
|
516
|
-
limit_window_seconds=limit_window_seconds,
|
|
517
|
-
reset_after_seconds=reset_after_seconds,
|
|
518
|
-
reset_at=int(reset_at),
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def _normalize_used_percent(
|
|
523
|
-
value: float | None,
|
|
524
|
-
rows: Iterable[UsageWindowRow],
|
|
525
|
-
) -> float | None:
|
|
526
|
-
if value is not None:
|
|
527
|
-
return value
|
|
528
|
-
values = [row.used_percent for row in rows if row.used_percent is not None]
|
|
529
|
-
if not values:
|
|
530
|
-
return None
|
|
531
|
-
return sum(values) / len(values)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
def _percent_to_int(value: float) -> int:
|
|
535
|
-
bounded = max(0.0, min(100.0, value))
|
|
536
|
-
return int(bounded)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def _rate_limit_details(
|
|
540
|
-
primary: RateLimitWindowSnapshotData | None,
|
|
541
|
-
secondary: RateLimitWindowSnapshotData | None,
|
|
542
|
-
) -> RateLimitStatusDetailsData | None:
|
|
543
|
-
if not primary and not secondary:
|
|
544
|
-
return None
|
|
545
|
-
used_percents = [window.used_percent for window in (primary, secondary) if window]
|
|
546
|
-
limit_reached = any(used >= 100 for used in used_percents)
|
|
547
|
-
return RateLimitStatusDetailsData(
|
|
548
|
-
allowed=not limit_reached,
|
|
549
|
-
limit_reached=limit_reached,
|
|
550
|
-
primary_window=primary,
|
|
551
|
-
secondary_window=secondary,
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
def _aggregate_credits(entries: Iterable[UsageHistory]) -> tuple[bool, bool, float] | None:
|
|
556
|
-
has_data = False
|
|
557
|
-
has_credits = False
|
|
558
|
-
unlimited = False
|
|
559
|
-
balance_total = 0.0
|
|
560
|
-
|
|
561
|
-
for entry in entries:
|
|
562
|
-
credits_has = entry.credits_has
|
|
563
|
-
credits_unlimited = entry.credits_unlimited
|
|
564
|
-
credits_balance = entry.credits_balance
|
|
565
|
-
if credits_has is None and credits_unlimited is None and credits_balance is None:
|
|
566
|
-
continue
|
|
567
|
-
has_data = True
|
|
568
|
-
if credits_has is True:
|
|
569
|
-
has_credits = True
|
|
570
|
-
if credits_unlimited is True:
|
|
571
|
-
unlimited = True
|
|
572
|
-
if credits_balance is not None and not credits_unlimited:
|
|
573
|
-
try:
|
|
574
|
-
balance_total += float(credits_balance)
|
|
575
|
-
except (TypeError, ValueError):
|
|
576
|
-
continue
|
|
577
|
-
|
|
578
|
-
if not has_data:
|
|
579
|
-
return None
|
|
580
|
-
if unlimited:
|
|
581
|
-
has_credits = True
|
|
582
|
-
return has_credits, unlimited, balance_total
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def _credits_snapshot(entries: Iterable[UsageHistory]) -> CreditStatusDetailsData | None:
|
|
586
|
-
aggregate = _aggregate_credits(entries)
|
|
587
|
-
if aggregate is None:
|
|
588
|
-
return None
|
|
589
|
-
has_credits, unlimited, balance_total = aggregate
|
|
590
|
-
balance_value = str(round(balance_total, 2))
|
|
591
|
-
return CreditStatusDetailsData(
|
|
592
|
-
has_credits=has_credits,
|
|
593
|
-
unlimited=unlimited,
|
|
594
|
-
balance=balance_value,
|
|
595
|
-
approx_local_messages=None,
|
|
596
|
-
approx_cloud_messages=None,
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
def _plan_type_for_accounts(accounts: Iterable[Account]) -> str:
|
|
601
|
-
normalized = [_normalize_plan_type(account.plan_type) for account in accounts]
|
|
602
|
-
filtered = [plan for plan in normalized if plan is not None]
|
|
603
|
-
if not filtered:
|
|
604
|
-
return "guest"
|
|
605
|
-
unique = set(filtered)
|
|
606
|
-
if len(unique) == 1:
|
|
607
|
-
return filtered[0]
|
|
608
|
-
for plan in PLAN_TYPE_PRIORITY:
|
|
609
|
-
if plan in unique:
|
|
610
|
-
return plan
|
|
611
|
-
return "guest"
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def _normalize_plan_type(value: str | None) -> str | None:
|
|
615
|
-
if not value:
|
|
616
|
-
return None
|
|
617
|
-
normalized = value.strip().lower()
|
|
618
|
-
if normalized not in KNOWN_PLAN_TYPES:
|
|
619
|
-
return None
|
|
620
|
-
return normalized
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
def _rate_limit_headers(
|
|
624
|
-
window_label: str,
|
|
625
|
-
summary: UsageWindowSummary,
|
|
626
|
-
) -> dict[str, str]:
|
|
627
|
-
used_percent = summary.used_percent
|
|
628
|
-
window_minutes = summary.window_minutes
|
|
629
|
-
if used_percent is None or window_minutes is None:
|
|
630
|
-
return {}
|
|
631
|
-
headers = {
|
|
632
|
-
f"x-codex-{window_label}-used-percent": str(float(used_percent)),
|
|
633
|
-
f"x-codex-{window_label}-window-minutes": str(int(window_minutes)),
|
|
634
|
-
}
|
|
635
|
-
reset_at = summary.reset_at
|
|
636
|
-
if reset_at is not None:
|
|
637
|
-
headers[f"x-codex-{window_label}-reset-at"] = str(int(reset_at))
|
|
638
|
-
return headers
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
def _credits_headers(entries: Iterable[UsageHistory]) -> dict[str, str]:
|
|
642
|
-
aggregate = _aggregate_credits(entries)
|
|
643
|
-
if aggregate is None:
|
|
644
|
-
return {}
|
|
645
|
-
has_credits, unlimited, balance_total = aggregate
|
|
646
|
-
balance_value = f"{balance_total:.2f}"
|
|
647
|
-
return {
|
|
648
|
-
"x-codex-credits-has-credits": "true" if has_credits else "false",
|
|
649
|
-
"x-codex-credits-unlimited": "true" if unlimited else "false",
|
|
650
|
-
"x-codex-credits-balance": balance_value,
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
def _normalize_error_code(code: str | None, error_type: str | None) -> str:
|
|
655
|
-
value = code or error_type
|
|
656
|
-
if not value:
|
|
657
|
-
return "upstream_error"
|
|
658
|
-
return value.lower()
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
def _parse_openai_error(payload: OpenAIErrorEnvelope) -> OpenAIError | None:
|
|
662
|
-
error = payload.get("error")
|
|
663
|
-
if not error:
|
|
664
|
-
return None
|
|
665
|
-
try:
|
|
666
|
-
return OpenAIError.model_validate(error)
|
|
667
|
-
except ValidationError:
|
|
668
|
-
if not isinstance(error, dict):
|
|
669
|
-
return None
|
|
670
|
-
return OpenAIError(
|
|
671
|
-
message=_coerce_str(error.get("message")),
|
|
672
|
-
type=_coerce_str(error.get("type")),
|
|
673
|
-
code=_coerce_str(error.get("code")),
|
|
674
|
-
param=_coerce_str(error.get("param")),
|
|
675
|
-
plan_type=_coerce_str(error.get("plan_type")),
|
|
676
|
-
resets_at=_coerce_number(error.get("resets_at")),
|
|
677
|
-
resets_in_seconds=_coerce_number(error.get("resets_in_seconds")),
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def _coerce_str(value: object) -> str | None:
|
|
682
|
-
return value if isinstance(value, str) else None
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
def _coerce_number(value: object) -> int | float | None:
|
|
686
|
-
if isinstance(value, (int, float)):
|
|
687
|
-
return value
|
|
688
|
-
if isinstance(value, str):
|
|
689
|
-
try:
|
|
690
|
-
return float(value.strip())
|
|
691
|
-
except ValueError:
|
|
692
|
-
return None
|
|
693
|
-
return None
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
def _apply_error_metadata(target: OpenAIErrorDetail, error: OpenAIError | None) -> None:
|
|
697
|
-
if not error:
|
|
698
|
-
return
|
|
699
|
-
if error.plan_type is not None:
|
|
700
|
-
target["plan_type"] = error.plan_type
|
|
701
|
-
if error.resets_at is not None:
|
|
702
|
-
target["resets_at"] = error.resets_at
|
|
703
|
-
if error.resets_in_seconds is not None:
|
|
704
|
-
target["resets_in_seconds"] = error.resets_in_seconds
|
|
705
|
-
|
|
706
|
-
|
|
707
454
|
class _RetryableStreamError(Exception):
|
|
708
455
|
def __init__(self, code: str, error: UpstreamError) -> None:
|
|
709
456
|
super().__init__(code)
|
|
710
457
|
self.code = code
|
|
711
458
|
self.error = error
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
def _upstream_error_from_openai(error: OpenAIError | None) -> UpstreamError:
|
|
715
|
-
if not error:
|
|
716
|
-
return {}
|
|
717
|
-
data = error.model_dump(exclude_none=True)
|
|
718
|
-
payload: UpstreamError = {}
|
|
719
|
-
message = data.get("message")
|
|
720
|
-
if isinstance(message, str):
|
|
721
|
-
payload["message"] = message
|
|
722
|
-
resets_at = data.get("resets_at")
|
|
723
|
-
if isinstance(resets_at, (int, float)):
|
|
724
|
-
payload["resets_at"] = resets_at
|
|
725
|
-
resets_in_seconds = data.get("resets_in_seconds")
|
|
726
|
-
if isinstance(resets_in_seconds, (int, float)):
|
|
727
|
-
payload["resets_in_seconds"] = resets_in_seconds
|
|
728
|
-
return payload
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
+
from typing import cast
|
|
4
5
|
|
|
5
|
-
from app.core.usage.logs import cost_from_log, total_tokens_from_log
|
|
6
|
+
from app.core.usage.logs import RequestLogLike, cost_from_log, total_tokens_from_log
|
|
6
7
|
from app.db.models import RequestLog
|
|
7
8
|
from app.modules.request_logs.repository import RequestLogsRepository
|
|
8
9
|
from app.modules.request_logs.schemas import RequestLogEntry
|
|
@@ -63,6 +64,7 @@ def _log_status(log: RequestLog) -> str:
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
def _to_entry(log: RequestLog) -> RequestLogEntry:
|
|
67
|
+
log_like = cast(RequestLogLike, log)
|
|
66
68
|
return RequestLogEntry(
|
|
67
69
|
requested_at=log.requested_at,
|
|
68
70
|
account_id=log.account_id,
|
|
@@ -71,7 +73,7 @@ def _to_entry(log: RequestLog) -> RequestLogEntry:
|
|
|
71
73
|
status=_log_status(log),
|
|
72
74
|
error_code=log.error_code,
|
|
73
75
|
error_message=log.error_message,
|
|
74
|
-
tokens=total_tokens_from_log(
|
|
75
|
-
cost_usd=cost_from_log(
|
|
76
|
+
tokens=total_tokens_from_log(log_like),
|
|
77
|
+
cost_usd=cost_from_log(log_like, precision=6),
|
|
76
78
|
latency_ms=log.latency_ms,
|
|
77
79
|
)
|
app/modules/usage/service.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import timedelta
|
|
4
|
+
from typing import cast
|
|
4
5
|
|
|
5
6
|
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.logs import RequestLogLike, cost_from_log, total_tokens_from_log, usage_tokens_from_log
|
|
7
8
|
from app.core.usage.pricing import CostItem, calculate_costs
|
|
8
9
|
from app.core.usage.types import (
|
|
9
10
|
UsageCostSummary,
|
|
@@ -15,7 +16,6 @@ from app.core.usage.types import (
|
|
|
15
16
|
from app.core.utils.time import from_epoch_seconds, utcnow
|
|
16
17
|
from app.db.models import Account, RequestLog
|
|
17
18
|
from app.modules.accounts.repository import AccountsRepository
|
|
18
|
-
from app.modules.proxy.usage_updater import UsageUpdater
|
|
19
19
|
from app.modules.request_logs.repository import RequestLogsRepository
|
|
20
20
|
from app.modules.usage.repository import UsageRepository
|
|
21
21
|
from app.modules.usage.schemas import (
|
|
@@ -28,6 +28,7 @@ from app.modules.usage.schemas import (
|
|
|
28
28
|
UsageWindow,
|
|
29
29
|
UsageWindowResponse,
|
|
30
30
|
)
|
|
31
|
+
from app.modules.usage.updater import UsageUpdater
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class UsageService:
|
|
@@ -137,7 +138,7 @@ def _build_account_history(
|
|
|
137
138
|
for log in logs:
|
|
138
139
|
account_id = log.account_id
|
|
139
140
|
counts[account_id] = counts.get(account_id, 0) + 1
|
|
140
|
-
cost = cost_from_log(log)
|
|
141
|
+
cost = cost_from_log(cast(RequestLogLike, log))
|
|
141
142
|
if cost is None:
|
|
142
143
|
continue
|
|
143
144
|
costs[account_id] = costs.get(account_id, 0.0) + cost
|
|
@@ -166,7 +167,7 @@ def _build_account_history(
|
|
|
166
167
|
|
|
167
168
|
def _log_to_cost_item(log: RequestLog) -> CostItem | None:
|
|
168
169
|
model = log.model
|
|
169
|
-
usage = usage_tokens_from_log(log)
|
|
170
|
+
usage = usage_tokens_from_log(cast(RequestLogLike, log))
|
|
170
171
|
if not model or not usage:
|
|
171
172
|
return None
|
|
172
173
|
return CostItem(model=model, usage=usage)
|
|
@@ -191,7 +192,7 @@ def _usage_metrics(logs_secondary: list[RequestLog]) -> UsageMetricsSummary:
|
|
|
191
192
|
def _sum_tokens(logs: list[RequestLog]) -> int:
|
|
192
193
|
total = 0
|
|
193
194
|
for log in logs:
|
|
194
|
-
total += total_tokens_from_log(log) or 0
|
|
195
|
+
total += total_tokens_from_log(cast(RequestLogLike, log)) or 0
|
|
195
196
|
return total
|
|
196
197
|
|
|
197
198
|
|
|
@@ -232,7 +233,7 @@ def _window_snapshot_to_model(snapshot: UsageWindowSnapshot) -> UsageWindow:
|
|
|
232
233
|
def _cost_summary_to_model(cost: UsageCostSummary) -> UsageCost:
|
|
233
234
|
return UsageCost(
|
|
234
235
|
currency=cost.currency,
|
|
235
|
-
|
|
236
|
+
totalUsd7d=cost.total_usd_7d,
|
|
236
237
|
by_model=[UsageCostByModel(model=item.model, usd=item.usd) for item in cost.by_model],
|
|
237
238
|
)
|
|
238
239
|
|
|
@@ -12,8 +12,8 @@ from app.core.usage.models import UsagePayload
|
|
|
12
12
|
from app.core.utils.request_id import get_request_id
|
|
13
13
|
from app.core.utils.time import utcnow
|
|
14
14
|
from app.db.models import Account, AccountStatus, UsageHistory
|
|
15
|
+
from app.modules.accounts.auth_manager import AuthManager
|
|
15
16
|
from app.modules.accounts.repository import AccountsRepository
|
|
16
|
-
from app.modules.proxy.auth_manager import AuthManager
|
|
17
17
|
from app.modules.usage.repository import UsageRepository
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
app/static/index.js
CHANGED
|
@@ -74,11 +74,15 @@
|
|
|
74
74
|
error: "deactivated",
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
const KNOWN_PLAN_TYPES = new Set([
|
|
78
|
+
"free",
|
|
79
|
+
"plus",
|
|
80
|
+
"pro",
|
|
81
|
+
"team",
|
|
82
|
+
"business",
|
|
83
|
+
"enterprise",
|
|
84
|
+
"edu",
|
|
85
|
+
]);
|
|
82
86
|
|
|
83
87
|
const ROUTING_LABELS = {
|
|
84
88
|
usage_weighted: "usage weighted",
|
|
@@ -92,7 +96,7 @@
|
|
|
92
96
|
timeout: "timeout",
|
|
93
97
|
upstream: "upstream",
|
|
94
98
|
rate_limit_exceeded: "rate limit",
|
|
95
|
-
usage_limit_reached: "
|
|
99
|
+
usage_limit_reached: "quota",
|
|
96
100
|
insufficient_quota: "quota",
|
|
97
101
|
usage_not_included: "quota",
|
|
98
102
|
quota_exceeded: "quota",
|
|
@@ -444,7 +448,19 @@
|
|
|
444
448
|
REQUEST_STATUS_LABELS[status] || "Unknown";
|
|
445
449
|
const requestStatusClass = (status) =>
|
|
446
450
|
REQUEST_STATUS_CLASSES[status] || "deactivated";
|
|
447
|
-
const
|
|
451
|
+
const normalizePlanType = (plan) => {
|
|
452
|
+
if (plan === null || plan === undefined) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
const value = String(plan).trim().toLowerCase();
|
|
456
|
+
return KNOWN_PLAN_TYPES.has(value) ? value : null;
|
|
457
|
+
};
|
|
458
|
+
const titleCase = (value) =>
|
|
459
|
+
value ? value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() : "";
|
|
460
|
+
const planLabel = (plan) => {
|
|
461
|
+
const normalized = normalizePlanType(plan);
|
|
462
|
+
return normalized ? titleCase(normalized) : "Unknown";
|
|
463
|
+
};
|
|
448
464
|
const routingLabel = (strategy) => ROUTING_LABELS[strategy] || "unknown";
|
|
449
465
|
const errorLabel = (code) => ERROR_LABELS[code] || "--";
|
|
450
466
|
const progressClass = (status) => PROGRESS_CLASS_BY_STATUS[status] || "";
|
|
@@ -1,80 +1,87 @@
|
|
|
1
|
-
app/__init__.py,sha256=
|
|
1
|
+
app/__init__.py,sha256=uqZSnn_VEL8TIUxsYqdf4zA2ByJYfjA06fArVAzrHFo,89
|
|
2
2
|
app/cli.py,sha256=gkIAkYOT9SbQjUDnVmwhVKZeKjL3YJCMrOjFINwBx54,544
|
|
3
|
-
app/dependencies.py,sha256=
|
|
4
|
-
app/main.py,sha256=
|
|
3
|
+
app/dependencies.py,sha256=UfxnxyoYih6TOfGGnptJnBSojg7BF31JjA5XyvcXS7o,3576
|
|
4
|
+
app/main.py,sha256=qr55Cm336acBUDnBrWZ64gtOI4xImo_8FeuVfDtyIP4,4349
|
|
5
5
|
app/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
app/core/crypto.py,sha256=zUz2GVqXigzXB5zX2Diq2cR41Kl37bBqxJiZmWGiIcY,1145
|
|
7
7
|
app/core/errors.py,sha256=go4Q5vv6_Rt8ZS230Mp436yCViEKu_xICylGF0gvGJg,1805
|
|
8
|
-
app/core/
|
|
9
|
-
app/core/
|
|
8
|
+
app/core/plan_types.py,sha256=6if0Zj2sTrtvpijkiJ0AtfkY0EfwARjKQetcD7Q8DM8,1548
|
|
9
|
+
app/core/types.py,sha256=cVWOt-cYNjleEBvPYQVOO-tru38mZYsrBrlYRUfXPxg,208
|
|
10
|
+
app/core/auth/__init__.py,sha256=r5YdeosInCIftl6whdxsWIYick2wp0o716wN5HLI4y4,2841
|
|
10
11
|
app/core/auth/models.py,sha256=iyygu0uHK7fu8txyIyCkXmlgqYPbqEZu3IFysD1t-gk,1557
|
|
11
12
|
app/core/auth/refresh.py,sha256=jhYxT2mQFX4CCvuBQfYNcI53iOtWa0vSkTuZAfbpd9k,5105
|
|
12
13
|
app/core/balancer/__init__.py,sha256=bGy7gITRPObt6P9NdG3pg8t3qWoSW0XWoADqfNYrhdg,405
|
|
13
|
-
app/core/balancer/logic.py,sha256=
|
|
14
|
+
app/core/balancer/logic.py,sha256=W6Mmg_KUszoYx_DynrzmT4k0OHwbPb5tr-ng1a--GO4,5623
|
|
14
15
|
app/core/balancer/types.py,sha256=gDgjlTy-NH3YhHYl2-YYpIabnckN9Q8-4cRy6S1u0K4,191
|
|
15
16
|
app/core/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
17
|
app/core/clients/http.py,sha256=yfFwsaIZNbSmNtyV02WnMKn00JdoNNSlSIx5xB3QY4o,987
|
|
17
18
|
app/core/clients/oauth.py,sha256=XgAzQVAMudBUCQ9nGKUC9N7zagSiawBdhIVmxf9HHwQ,11832
|
|
18
|
-
app/core/clients/proxy.py,sha256=
|
|
19
|
+
app/core/clients/proxy.py,sha256=ZWs0TBYcOgDz_jZ3tucDsRiqQ_9TGxU1oPGiI1mu-bE,9378
|
|
19
20
|
app/core/clients/usage.py,sha256=kG7TXqmy8IX9m4wJx5fOGDB2hqunivOU6o2xfoXCGy4,4800
|
|
20
21
|
app/core/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
app/core/config/settings.py,sha256
|
|
22
|
+
app/core/config/settings.py,sha256=LZukDTD4ILfq2CjyF89qxk9s4Lu-qqzotL4e61z0FXo,2550
|
|
22
23
|
app/core/openai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
24
|
app/core/openai/models.py,sha256=SzVPqmp9IDbJTW6gLGHeLxrpAmFehJPSC4fVsqzEmQA,3344
|
|
24
25
|
app/core/openai/parsing.py,sha256=VuE1OyPAv1umrSbkzqa6dcjPYfk00isOVB3O0xUPLBw,1537
|
|
25
26
|
app/core/openai/requests.py,sha256=Nnd4ZtxUtXP-jJ7LGKpPf_JDBIFo-X9k0qMw9SyANTc,1675
|
|
26
|
-
app/core/usage/__init__.py,sha256=
|
|
27
|
+
app/core/usage/__init__.py,sha256=q8rz-XwZhIZ-hsmPmXB9JveqyP46tusLUR39c1cC75E,5687
|
|
27
28
|
app/core/usage/logs.py,sha256=nQP6208cmsawCre0s7ekadyZiXofNqz18_3xBeuku-Q,1812
|
|
28
29
|
app/core/usage/models.py,sha256=FtBQx4Rb7jpwcqxmGXxg7RTVV17LK1QOSWaJIkaaNoQ,878
|
|
29
30
|
app/core/usage/pricing.py,sha256=6p8rJ26Gk61mz2t_h9sa0T7NiPDUTiNpzoDewMzT6E0,5464
|
|
31
|
+
app/core/usage/quota.py,sha256=Xwz6V_qjT8AcD8vHUv0Qxbrzve3pE92PRB_kAGU_oRI,1977
|
|
30
32
|
app/core/usage/types.py,sha256=CbFF6JYSLvALa2P0qYuj9J9b_w53Fgjg0JOu4RzMWbI,2104
|
|
31
33
|
app/core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
34
|
app/core/utils/request_id.py,sha256=gxafI-Se8dRQTir-HNGelMzC9S2gwYZntTkVzEqzp_I,705
|
|
33
35
|
app/core/utils/retry.py,sha256=UmBap1Wh-CBT7r4fHzVb_PI9-LR9-HjUtDzRnhRjP2U,822
|
|
34
|
-
app/core/utils/sse.py,sha256=
|
|
36
|
+
app/core/utils/sse.py,sha256=DJMOU4vW5Ir_4WeL5t5t7i33aRMnqVPU0eQvGn4sBv8,537
|
|
35
37
|
app/core/utils/time.py,sha256=B6FfSe43Eq_puE6eourly1X3gajyihK2VOAwJ8M3wyI,497
|
|
36
38
|
app/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
39
|
app/db/models.py,sha256=JCknQCuzjHfgyuSzjHqBmeIE-0XisIAQGEhEWqmzabs,3841
|
|
38
|
-
app/db/session.py,sha256=
|
|
40
|
+
app/db/session.py,sha256=1e8ATHwgnPRuZvULd0DxFTS6oVoRfLoCCY3UerePOsU,2068
|
|
41
|
+
app/db/migrations/__init__.py,sha256=LIvASH5r6U75B8-RmS2K1mGCmQ09PV2WqCvtyk9ACDQ,2152
|
|
42
|
+
app/db/migrations/versions/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
43
|
+
app/db/migrations/versions/normalize_account_plan_types.py,sha256=WpPhkJ2gtpgw5mbErj2He9ahjeqNV3Z7c9RmmMjMmtM,575
|
|
39
44
|
app/modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
45
|
app/modules/accounts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
46
|
app/modules/accounts/api.py,sha256=rSkYrg3_p_JE1kHZ4xXa5lBFTdT40v1oNAmlTDGOguY,2807
|
|
42
|
-
app/modules/accounts/
|
|
47
|
+
app/modules/accounts/auth_manager.py,sha256=Sj_nuL663x0Rv1QZiGNJDDz7MvhMZF65ksXWGNzK53Q,2996
|
|
48
|
+
app/modules/accounts/repository.py,sha256=R9-9OzIDe2aDwLdhTHUCWXdOavN4nVxAeNMOv1l_ROU,3010
|
|
43
49
|
app/modules/accounts/schemas.py,sha256=gtlbPg5uxM3t_V5JxCL6eP-UaU6TSE0UoX2yIpxM_a0,1659
|
|
44
|
-
app/modules/accounts/service.py,sha256=
|
|
50
|
+
app/modules/accounts/service.py,sha256=Wh-npKQjMREFnt29RQkwWEjf6Dg_2PQYdj0iwGTVvSg,8874
|
|
45
51
|
app/modules/health/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
-
app/modules/health/api.py,sha256=
|
|
52
|
+
app/modules/health/api.py,sha256=CSs09qeEy0DjM9twmmQGg-kNt2J4XNFBX72CwLuK-Gs,297
|
|
53
|
+
app/modules/health/schemas.py,sha256=nnF32SQbR-5rLSaRtwwiC3ZOhjSnRHPgr2cagwW2rqo,177
|
|
47
54
|
app/modules/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
55
|
app/modules/oauth/api.py,sha256=Wu_DEXgN3hP9Si6MMkl6-0v_Blwjahqwtun1iP7DjVk,1877
|
|
49
56
|
app/modules/oauth/schemas.py,sha256=sdDKP7u9bO87lcZXjK7uSokurHPS22nN2p_jkw9iEBc,777
|
|
50
|
-
app/modules/oauth/service.py,sha256=
|
|
57
|
+
app/modules/oauth/service.py,sha256=NWpz_GP6_VuXdStZ4qYjiotqxn4FXZkSRlrguPznTPY,12841
|
|
51
58
|
app/modules/oauth/templates/oauth_success.html,sha256=YNSGUIozcZEJQjpFtM2sgF4n8jqfbmx8LRwdXTraym4,3799
|
|
52
59
|
app/modules/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
60
|
app/modules/proxy/api.py,sha256=BR_qNlNZg2Ft5GYQ-7AzlLlJFf6RVJmlzJzFr_KHUIc,2921
|
|
54
|
-
app/modules/proxy/
|
|
55
|
-
app/modules/proxy/load_balancer.py,sha256=
|
|
61
|
+
app/modules/proxy/helpers.py,sha256=-XVTNTpkDvJ3KfsuGHztfUVBGBFm2H-AaNTNH2dNe6o,8739
|
|
62
|
+
app/modules/proxy/load_balancer.py,sha256=ti3SS3A-nXYFSaJSo9L4-1QrYpBt4UbB3gZfBpJQ97U,6898
|
|
56
63
|
app/modules/proxy/schemas.py,sha256=55pXtUCl2R_93kAPOJJ7Ji4Jn3qVu10vq2KSCCkNdp4,2748
|
|
57
|
-
app/modules/proxy/service.py,sha256=
|
|
64
|
+
app/modules/proxy/service.py,sha256=U0mAC90HDbovOMd1CmyaZ0AvAYNRsxq8jDjR4nbdd64,18932
|
|
58
65
|
app/modules/proxy/types.py,sha256=iqEyoO8vGr8N5oEzUSvVWCai7UZbJAU62IvO7JNS9qs,927
|
|
59
|
-
app/modules/proxy/usage_updater.py,sha256=t5wpn3SmPnTXceiQFF0tw-9V5oPPhP4C0anwbI7T5v0,5588
|
|
60
66
|
app/modules/request_logs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
67
|
app/modules/request_logs/api.py,sha256=6nV2Uv_hnK7WI3gNpKrgTx4MyUQIXk1QxKp40nij0Xo,1037
|
|
62
68
|
app/modules/request_logs/repository.py,sha256=SnwDwD84wgslmdIgYFGD8ReUYmIyhxT6MOjFAolLBC8,3333
|
|
63
69
|
app/modules/request_logs/schemas.py,sha256=GSCi4TEWMmQ-THsQl2irRrA_msU8jzsqKSBDFn7hiJU,592
|
|
64
|
-
app/modules/request_logs/service.py,sha256=
|
|
70
|
+
app/modules/request_logs/service.py,sha256=1G1-z6ytP1d51aLQxSE4nrtCC0y5Ys1aX5XtmzccbJw,2477
|
|
65
71
|
app/modules/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
72
|
app/modules/shared/schemas.py,sha256=SSoWTiSeiSxD06MDn9NbZWhx12JMQtvjJMIk1Kvxwio,240
|
|
67
73
|
app/modules/usage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
74
|
app/modules/usage/api.py,sha256=jpPc2VSjmiP6DjRZvotHCh_trLINOngSyTdS7MY9fgg,1108
|
|
69
75
|
app/modules/usage/repository.py,sha256=DtJI4kgajW7YUJ0JKJjdNCPBXT_fdBwqDoepi9aznyA,4791
|
|
70
76
|
app/modules/usage/schemas.py,sha256=kh0D2IbIFkl5WTo8XPs-7AC4C8jRpyX0pF6KYQH9ifU,1579
|
|
71
|
-
app/modules/usage/service.py,sha256=
|
|
77
|
+
app/modules/usage/service.py,sha256=n0kdL_AMT271jWim-BK2jiI7I1H1D9d8uzbHtw13xSg,9853
|
|
78
|
+
app/modules/usage/updater.py,sha256=TevRdwO0vA3HF-E-UkDMPs_3Bv-8qu_8VZX1Jd0AcfE,5591
|
|
72
79
|
app/static/7.css,sha256=9EHW2Ouff2fRXgcQbYuCuglxTFgQZGNLLTXKTS6S5aI,80687
|
|
73
80
|
app/static/index.css,sha256=ct1FfBg0PY7c6KxSS6A7JM_WDwdeJqhQa4pXTgyJ19Q,8786
|
|
74
81
|
app/static/index.html,sha256=FmWQ0l40rTBsHIR9i0ttGTzG17ePASsO2VDv6Q48CNw,24477
|
|
75
|
-
app/static/index.js,sha256=
|
|
76
|
-
codex_lb-0.
|
|
77
|
-
codex_lb-0.
|
|
78
|
-
codex_lb-0.
|
|
79
|
-
codex_lb-0.
|
|
80
|
-
codex_lb-0.
|
|
82
|
+
app/static/index.js,sha256=a45sCr7nXJE9muacaxRNTao7l_j8rpHKuEck_qvgSaI,52644
|
|
83
|
+
codex_lb-0.2.0.dist-info/METADATA,sha256=3tQgTWLJl4waxyKXczt119GY9JNS3ICqtvJ1vnNMrfk,3828
|
|
84
|
+
codex_lb-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
85
|
+
codex_lb-0.2.0.dist-info/entry_points.txt,sha256=SEa5T6Uz2Fhy574No6Y0XyGmYi3PXLrhu2xStJTqyI8,42
|
|
86
|
+
codex_lb-0.2.0.dist-info/licenses/LICENSE,sha256=cHPibxiL0TXwrUX_kNY6ym544EX1UCzKhxdaca5cFuk,1062
|
|
87
|
+
codex_lb-0.2.0.dist-info/RECORD,,
|