codex-lb 0.2.0__tar.gz → 0.3.1__tar.gz

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 (155) hide show
  1. codex_lb-0.3.1/.all-contributorsrc +52 -0
  2. codex_lb-0.3.1/.github/release-please-manifest.json +3 -0
  3. {codex_lb-0.2.0 → codex_lb-0.3.1}/.gitignore +2 -1
  4. codex_lb-0.3.1/CHANGELOG.md +110 -0
  5. {codex_lb-0.2.0 → codex_lb-0.3.1}/PKG-INFO +33 -7
  6. codex_lb-0.3.1/README.md +81 -0
  7. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/auth/__init__.py +10 -0
  8. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/balancer/logic.py +33 -6
  9. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/config/settings.py +2 -0
  10. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/usage/__init__.py +2 -0
  11. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/usage/logs.py +12 -2
  12. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/usage/quota.py +10 -4
  13. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/usage/types.py +3 -2
  14. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/db/migrations/__init__.py +14 -3
  15. codex_lb-0.3.1/app/db/migrations/versions/add_accounts_chatgpt_account_id.py +29 -0
  16. codex_lb-0.3.1/app/db/migrations/versions/add_accounts_reset_at.py +29 -0
  17. codex_lb-0.3.1/app/db/migrations/versions/add_dashboard_settings.py +31 -0
  18. codex_lb-0.3.1/app/db/migrations/versions/add_request_logs_reasoning_effort.py +21 -0
  19. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/db/models.py +33 -0
  20. codex_lb-0.3.1/app/db/session.py +136 -0
  21. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/dependencies.py +27 -1
  22. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/main.py +11 -2
  23. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/accounts/auth_manager.py +44 -3
  24. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/accounts/repository.py +14 -6
  25. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/accounts/service.py +4 -2
  26. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/oauth/service.py +4 -3
  27. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/proxy/load_balancer.py +74 -5
  28. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/proxy/service.py +155 -31
  29. codex_lb-0.3.1/app/modules/proxy/sticky_repository.py +56 -0
  30. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/request_logs/repository.py +6 -3
  31. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/request_logs/schemas.py +2 -0
  32. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/request_logs/service.py +8 -1
  33. codex_lb-0.3.1/app/modules/settings/__init__.py +1 -0
  34. codex_lb-0.3.1/app/modules/settings/api.py +37 -0
  35. codex_lb-0.3.1/app/modules/settings/repository.py +40 -0
  36. codex_lb-0.3.1/app/modules/settings/schemas.py +13 -0
  37. codex_lb-0.3.1/app/modules/settings/service.py +33 -0
  38. codex_lb-0.3.1/app/modules/shared/schemas.py +22 -0
  39. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/usage/schemas.py +1 -0
  40. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/usage/service.py +17 -1
  41. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/usage/updater.py +36 -7
  42. codex_lb-0.3.1/app/static/index.css +1248 -0
  43. codex_lb-0.3.1/app/static/index.html +541 -0
  44. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/static/index.js +327 -49
  45. {codex_lb-0.2.0 → codex_lb-0.3.1}/docker-compose.yml +2 -0
  46. codex_lb-0.3.1/docs/screenshots/accounts.jpg +0 -0
  47. codex_lb-0.3.1/docs/screenshots/dashboard.jpg +0 -0
  48. {codex_lb-0.2.0 → codex_lb-0.3.1}/pyproject.toml +4 -4
  49. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/conftest.py +3 -2
  50. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_accounts_api.py +18 -10
  51. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_accounts_api_extended.py +7 -4
  52. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_db_models.py +1 -1
  53. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_migrations.py +2 -2
  54. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_oauth_flow.py +8 -3
  55. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_proxy_api_extended.py +19 -15
  56. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_proxy_compact.py +14 -6
  57. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_proxy_responses.py +9 -3
  58. codex_lb-0.3.1/tests/integration/test_proxy_sticky_sessions.py +298 -0
  59. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_repositories.py +24 -0
  60. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_request_logs_api.py +1 -0
  61. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_request_logs_filters.py +3 -0
  62. codex_lb-0.3.1/tests/integration/test_settings_api.py +32 -0
  63. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_usage_api.py +27 -0
  64. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_auth.py +3 -0
  65. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_auth_manager.py +2 -0
  66. codex_lb-0.3.1/tests/unit/test_load_balancer.py +305 -0
  67. codex_lb-0.3.1/tests/unit/test_usage_updater.py +124 -0
  68. {codex_lb-0.2.0 → codex_lb-0.3.1}/uv.lock +138 -122
  69. codex_lb-0.2.0/.github/release-please-manifest.json +0 -3
  70. codex_lb-0.2.0/CHANGELOG.md +0 -72
  71. codex_lb-0.2.0/README.md +0 -55
  72. codex_lb-0.2.0/app/db/session.py +0 -76
  73. codex_lb-0.2.0/app/modules/shared/schemas.py +0 -8
  74. codex_lb-0.2.0/app/static/7.css +0 -1336
  75. codex_lb-0.2.0/app/static/index.css +0 -543
  76. codex_lb-0.2.0/app/static/index.html +0 -457
  77. codex_lb-0.2.0/docs/screenshots/accounts.jpeg +0 -0
  78. codex_lb-0.2.0/docs/screenshots/dashboard.jpeg +0 -0
  79. codex_lb-0.2.0/tests/unit/test_load_balancer.py +0 -129
  80. {codex_lb-0.2.0 → codex_lb-0.3.1}/.dockerignore +0 -0
  81. {codex_lb-0.2.0 → codex_lb-0.3.1}/.env.example +0 -0
  82. {codex_lb-0.2.0 → codex_lb-0.3.1}/.github/release-please-config.json +0 -0
  83. {codex_lb-0.2.0 → codex_lb-0.3.1}/.github/workflows/ci.yml +0 -0
  84. {codex_lb-0.2.0 → codex_lb-0.3.1}/.github/workflows/release-please.yml +0 -0
  85. {codex_lb-0.2.0 → codex_lb-0.3.1}/.github/workflows/release.yml +0 -0
  86. {codex_lb-0.2.0 → codex_lb-0.3.1}/.pre-commit-config.yaml +0 -0
  87. {codex_lb-0.2.0 → codex_lb-0.3.1}/AGENTS.md +0 -0
  88. {codex_lb-0.2.0 → codex_lb-0.3.1}/Dockerfile +0 -0
  89. {codex_lb-0.2.0 → codex_lb-0.3.1}/LICENSE +0 -0
  90. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/__init__.py +0 -0
  91. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/cli.py +0 -0
  92. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/__init__.py +0 -0
  93. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/auth/models.py +0 -0
  94. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/auth/refresh.py +0 -0
  95. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/balancer/__init__.py +0 -0
  96. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/balancer/types.py +0 -0
  97. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/clients/__init__.py +0 -0
  98. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/clients/http.py +0 -0
  99. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/clients/oauth.py +0 -0
  100. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/clients/proxy.py +0 -0
  101. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/clients/usage.py +0 -0
  102. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/config/__init__.py +0 -0
  103. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/crypto.py +0 -0
  104. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/errors.py +0 -0
  105. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/openai/__init__.py +0 -0
  106. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/openai/models.py +0 -0
  107. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/openai/parsing.py +0 -0
  108. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/openai/requests.py +0 -0
  109. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/plan_types.py +0 -0
  110. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/types.py +0 -0
  111. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/usage/models.py +0 -0
  112. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/usage/pricing.py +0 -0
  113. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/utils/__init__.py +0 -0
  114. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/utils/request_id.py +0 -0
  115. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/utils/retry.py +0 -0
  116. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/utils/sse.py +0 -0
  117. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/core/utils/time.py +0 -0
  118. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/db/__init__.py +0 -0
  119. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/db/migrations/versions/__init__.py +0 -0
  120. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/db/migrations/versions/normalize_account_plan_types.py +0 -0
  121. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/__init__.py +0 -0
  122. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/accounts/__init__.py +0 -0
  123. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/accounts/api.py +0 -0
  124. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/accounts/schemas.py +0 -0
  125. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/health/__init__.py +0 -0
  126. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/health/api.py +0 -0
  127. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/health/schemas.py +0 -0
  128. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/oauth/__init__.py +0 -0
  129. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/oauth/api.py +0 -0
  130. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/oauth/schemas.py +0 -0
  131. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/oauth/templates/oauth_success.html +0 -0
  132. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/proxy/__init__.py +0 -0
  133. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/proxy/api.py +0 -0
  134. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/proxy/helpers.py +0 -0
  135. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/proxy/schemas.py +0 -0
  136. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/proxy/types.py +0 -0
  137. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/request_logs/__init__.py +0 -0
  138. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/request_logs/api.py +0 -0
  139. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/shared/__init__.py +0 -0
  140. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/usage/__init__.py +0 -0
  141. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/usage/api.py +0 -0
  142. {codex_lb-0.2.0 → codex_lb-0.3.1}/app/modules/usage/repository.py +0 -0
  143. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/__init__.py +0 -0
  144. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_codex_usage_api.py +0 -0
  145. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_health_and_errors.py +0 -0
  146. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_load_balancer_integration.py +0 -0
  147. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/integration/test_usage_summary.py +0 -0
  148. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_auth_refresh.py +0 -0
  149. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_oauth_client.py +0 -0
  150. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_pricing.py +0 -0
  151. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_proxy_utils.py +0 -0
  152. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_retry.py +0 -0
  153. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_sse.py +0 -0
  154. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_usage.py +0 -0
  155. {codex_lb-0.2.0 → codex_lb-0.3.1}/tests/unit/test_usage_client.py +0 -0
@@ -0,0 +1,52 @@
1
+ {
2
+ "projectName": "codex-lb",
3
+ "projectOwner": "Soju06",
4
+ "repoType": "github",
5
+ "repoHost": "https://github.com",
6
+ "files": [
7
+ "README.md"
8
+ ],
9
+ "imageSize": 100,
10
+ "commit": true,
11
+ "commitConvention": "angular",
12
+ "contributors": [
13
+ {
14
+ "login": "Soju06",
15
+ "name": "Soju06",
16
+ "avatar_url": "https://avatars.githubusercontent.com/u/34199905?v=4",
17
+ "profile": "https://github.com/Soju06",
18
+ "contributions": [
19
+ "code",
20
+ "test",
21
+ "maintenance",
22
+ "infra"
23
+ ]
24
+ },
25
+ {
26
+ "login": "JKamsker",
27
+ "name": "Jonas Kamsker",
28
+ "avatar_url": "https://avatars.githubusercontent.com/u/11245306?v=4",
29
+ "profile": "http://jonas.kamsker.at/",
30
+ "contributions": [
31
+ "code",
32
+ "bug",
33
+ "maintenance"
34
+ ]
35
+ },
36
+ {
37
+ "login": "Quack6765",
38
+ "name": "Quack",
39
+ "avatar_url": "https://avatars.githubusercontent.com/u/5446230?v=4",
40
+ "profile": "https://github.com/Quack6765",
41
+ "contributions": [
42
+ "code",
43
+ "bug",
44
+ "maintenance",
45
+ "design"
46
+ ]
47
+ }
48
+ ],
49
+ "contributorsPerLine": 7,
50
+ "linkToUsage": false,
51
+ "commitType": "docs"
52
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.1"
3
+ }
@@ -34,4 +34,5 @@ node_modules/
34
34
  # Local
35
35
  .local/
36
36
 
37
- app/static/components
37
+ app/static/components
38
+ .codex-lb/
@@ -0,0 +1,110 @@
1
+ # Changelog
2
+
3
+ ## [0.3.1](https://github.com/Soju06/codex-lb/compare/v0.3.0...v0.3.1) (2026-01-22)
4
+
5
+
6
+ ### Documentation
7
+
8
+ * add Quack6765 as a contributor for design ([7a5ec08](https://github.com/Soju06/codex-lb/commit/7a5ec084b9a8d32c844127739f826a5f83bf1440))
9
+ * update .all-contributorsrc ([14ea9da](https://github.com/Soju06/codex-lb/commit/14ea9da361a978a56c4d1f7facefe789193c7b91))
10
+ * update README.md ([f283d60](https://github.com/Soju06/codex-lb/commit/f283d60ae359585cd128a965ca6fba2a14249a11))
11
+
12
+ ## [0.3.0](https://github.com/Soju06/codex-lb/compare/v0.2.0...v0.3.0) (2026-01-21)
13
+
14
+
15
+ ### Features
16
+
17
+ * add cached input tokens handling and update related metrics in … ([5bf6609](https://github.com/Soju06/codex-lb/commit/5bf66095b8000ffc8fbdf8d989f60171604f69d3))
18
+ * add cached input tokens handling and update related metrics in logs and usage schemas ([c965036](https://github.com/Soju06/codex-lb/commit/c9650367c1a2d14e63e3440788b7cd44b08ebd9a))
19
+ * add formatting for cached input tokens metadata in metrics display ([53feaa6](https://github.com/Soju06/codex-lb/commit/53feaa62f7c5c282508f37c3fd42d9af655c2fa9))
20
+ * add secondary usage tracking and selection logic for accounts in load balancer ([d66cf69](https://github.com/Soju06/codex-lb/commit/d66cf69b2834b42fefbbfa646d82477f9832fdda))
21
+ * add ty type checking and refactors ([41fa811](https://github.com/Soju06/codex-lb/commit/41fa8112ba9b900ffa5dbee3a39d94267e2caa75))
22
+ * **app:** add migrations and reasoning effort support ([9eae590](https://github.com/Soju06/codex-lb/commit/9eae5903a08363291e397f983a531ddf325658d7))
23
+ * implement dashboard settings for sticky threads and reset preferences ([cd04812](https://github.com/Soju06/codex-lb/commit/cd0481247f0ceffdd92173ea84773960e52a7253))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * **app:** tune sqlite pragmas and usage UI ([a44a4fd](https://github.com/Soju06/codex-lb/commit/a44a4fd6fe5771282a12ee62a34c9be819254322))
29
+ * **app:** update effort display format in history ([0796740](https://github.com/Soju06/codex-lb/commit/0796740ab570cf476b2285a615559a9a6318082f))
30
+ * **app:** update effort display format to include parentheses ([6fbae96](https://github.com/Soju06/codex-lb/commit/6fbae960f393ff92cae0feb614ca0e811a855851))
31
+ * **dashboard:** fallback primary remaining to summary ([02b3d39](https://github.com/Soju06/codex-lb/commit/02b3d39c2b734271af7c420fc52b7e87350177e1))
32
+ * **db:** avoid leaked async connection in migration ([9aa1d03](https://github.com/Soju06/codex-lb/commit/9aa1d0395481a96a21db2d0add18ee1753f183b2))
33
+ * **db:** use returning for dml checks ([4ec7c7a](https://github.com/Soju06/codex-lb/commit/4ec7c7a6615e6e5852b0865e09184544f09ebedc))
34
+ * **ui:** style and label settings checkboxes ([722cad8](https://github.com/Soju06/codex-lb/commit/722cad851706e2784815dad4069902cc95b3f662))
35
+
36
+
37
+ ### Documentation
38
+
39
+ * expand 0.2.0 changelog ([32148dc](https://github.com/Soju06/codex-lb/commit/32148dc2d195cec0dd85f61fc0a13d8cbef24e24))
40
+
41
+ ## [0.2.0](https://github.com/Soju06/codex-lb/compare/v0.1.5...v0.2.0) (2026-01-19)
42
+
43
+
44
+ ### Features
45
+
46
+ * add ty type checking and pre-commit hook
47
+ * add health response schema and typed context cleanup
48
+
49
+
50
+ ### Bug Fixes
51
+
52
+ * normalize stored plan types (pro/team/business/enterprise/edu) so accounts no longer show as unknown
53
+ * prevent rate-limit status when usage is below 100% by using cooldown/backoff and primary-window quota checks
54
+ * surface per-account quota reset times by applying primary/secondary reset windows with fallbacks
55
+
56
+
57
+ ### Refactor
58
+
59
+ * move auth/usage helpers into module boundaries and extract proxy helpers
60
+ * tighten typing across services and tests
61
+
62
+ ## [0.1.5](https://github.com/Soju06/codex-lb/compare/v0.1.4...v0.1.5) (2026-01-14)
63
+
64
+
65
+ ### Bug Fixes
66
+
67
+ * align rate-limit backoff and reset handling ([4d59650](https://github.com/Soju06/codex-lb/commit/4d596508e5ad13e68aa6e64f9cb32324bd38f07b))
68
+
69
+ ## [0.1.4](https://github.com/Soju06/codex-lb/compare/v0.1.3...v0.1.4) (2026-01-13)
70
+
71
+
72
+ ### Bug Fixes
73
+
74
+ * **db:** harden session cleanup on cancellation ([dee3916](https://github.com/Soju06/codex-lb/commit/dee3916efa83dedec1d5ad43e1e14950b8c6e4a7))
75
+
76
+ ## [0.1.3](https://github.com/Soju06/codex-lb/compare/v0.1.2...v0.1.3) (2026-01-12)
77
+
78
+
79
+ ### Documentation
80
+
81
+ * use absolute image URLs for PyPI ([5fa65a5](https://github.com/Soju06/codex-lb/commit/5fa65a572980f356738f49be3adf2c62fdc38466))
82
+
83
+ ## [0.1.2](https://github.com/Soju06/codex-lb/compare/v0.1.1...v0.1.2) (2026-01-12)
84
+
85
+
86
+ ### Bug Fixes
87
+
88
+ * sync package __version__ ([3dd97e6](https://github.com/Soju06/codex-lb/commit/3dd97e6397a8ea9d3528c166d1e729936f98f737))
89
+
90
+ ## [0.1.1](https://github.com/Soju06/codex-lb/compare/v0.1.0...v0.1.1) (2026-01-12)
91
+
92
+
93
+ ### Bug Fixes
94
+
95
+ * address lint warnings ([7c3cc06](https://github.com/Soju06/codex-lb/commit/7c3cc06c9a6a9a9a8895c1dd5fcc57b3c0eebdb3))
96
+ * reactivate accounts when secondary quota clears ([58a4263](https://github.com/Soju06/codex-lb/commit/58a42630d644559f96f045a96c25d0126810542e))
97
+ * skip project install in docker build ([64e9156](https://github.com/Soju06/codex-lb/commit/64e9156075c256ef48c0587ea1abb7cc092b97a5))
98
+
99
+
100
+ ### Documentation
101
+
102
+ * add dashboard hero and accounts view ([3522654](https://github.com/Soju06/codex-lb/commit/3522654fe5d09adbe32895d4b24e8b00faac9dfe))
103
+
104
+ ## [0.1.0](https://github.com/Soju06/codex-lb/releases/tag/v0.1.0) (2026-01-07)
105
+
106
+
107
+ ### Bug Fixes
108
+
109
+ * address lint warnings ([7c3cc06](https://github.com/Soju06/codex-lb/commit/7c3cc06c9a6a9a8895c1dd5fcc57b3c0eebdb3))
110
+ * skip project install in docker build ([64e9156](https://github.com/Soju06/codex-lb/commit/64e9156075c256ef48c0587ea1abb7cc092b97a5))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-lb
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Codex load balancer and proxy for ChatGPT accounts with usage dashboard
5
5
  Author-email: Soju06 <qlskssk@gmail.com>
6
6
  Maintainer-email: Soju06 <qlskssk@gmail.com>
@@ -39,7 +39,7 @@ Classifier: Topic :: Software Development :: Libraries
39
39
  Classifier: Topic :: System :: Networking
40
40
  Requires-Python: >=3.13
41
41
  Requires-Dist: aiohttp-retry>=2.9.1
42
- Requires-Dist: aiohttp>=3.13.2
42
+ Requires-Dist: aiohttp>=3.13.3
43
43
  Requires-Dist: aiosqlite>=0.22.1
44
44
  Requires-Dist: cryptography>=46.0.3
45
45
  Requires-Dist: fastapi[standard]>=0.128.0
@@ -55,9 +55,13 @@ Description-Content-Type: text/markdown
55
55
 
56
56
  Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
57
57
 
58
- <p align="center">
59
- <img src="https://raw.githubusercontent.com/Soju06/codex-lb/main/docs/screenshots/dashboard.jpeg" alt="Codex Load Balancer dashboard" width="100%">
60
- </p>
58
+ ### Main Dashboard View
59
+
60
+ ![main dashboard view](docs/screenshots/dashboard.jpg)
61
+
62
+ ### Accounts View
63
+
64
+ ![Accounts list and details](docs/screenshots/accounts.jpg)
61
65
 
62
66
  ## Quick Start
63
67
 
@@ -78,9 +82,7 @@ uvx codex-lb
78
82
 
79
83
  Open [localhost:2455](http://localhost:2455) → Add account → Done.
80
84
 
81
- ## Accounts view
82
85
 
83
- ![Accounts list and details](https://raw.githubusercontent.com/Soju06/codex-lb/main/docs/screenshots/accounts.jpeg)
84
86
 
85
87
  ## Codex CLI & Extension Setup
86
88
 
@@ -102,7 +104,31 @@ requires_openai_auth = true # Required: enables model selection in Codex IDE ex
102
104
  ## Data
103
105
 
104
106
  All data stored in `~/.codex-lb/`:
107
+
105
108
  - `store.db` – accounts, usage logs
106
109
  - `encryption.key` – encrypts tokens (auto-generated)
107
110
 
108
111
  Backup this directory to preserve your accounts.
112
+
113
+ ## Contributors ✨
114
+
115
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
116
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
117
+ <!-- prettier-ignore-start -->
118
+ <!-- markdownlint-disable -->
119
+ <table>
120
+ <tbody>
121
+ <tr>
122
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/Soju06"><img src="https://avatars.githubusercontent.com/u/34199905?v=4?s=100" width="100px;" alt="Soju06"/><br /><sub><b>Soju06</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=Soju06" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=Soju06" title="Tests">⚠️</a> <a href="#maintenance-Soju06" title="Maintenance">🚧</a> <a href="#infra-Soju06" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
123
+ <td align="center" valign="top" width="14.28%"><a href="http://jonas.kamsker.at/"><img src="https://avatars.githubusercontent.com/u/11245306?v=4?s=100" width="100px;" alt="Jonas Kamsker"/><br /><sub><b>Jonas Kamsker</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=JKamsker" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3AJKamsker" title="Bug reports">🐛</a> <a href="#maintenance-JKamsker" title="Maintenance">🚧</a></td>
124
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/Quack6765"><img src="https://avatars.githubusercontent.com/u/5446230?v=4?s=100" width="100px;" alt="Quack"/><br /><sub><b>Quack</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=Quack6765" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3AQuack6765" title="Bug reports">🐛</a> <a href="#maintenance-Quack6765" title="Maintenance">🚧</a> <a href="#design-Quack6765" title="Design">🎨</a></td>
125
+ </tr>
126
+ </tbody>
127
+ </table>
128
+
129
+ <!-- markdownlint-restore -->
130
+ <!-- prettier-ignore-end -->
131
+
132
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
133
+
134
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
@@ -0,0 +1,81 @@
1
+ # codex-lb
2
+
3
+ Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
4
+
5
+ ### Main Dashboard View
6
+
7
+ ![main dashboard view](docs/screenshots/dashboard.jpg)
8
+
9
+ ### Accounts View
10
+
11
+ ![Accounts list and details](docs/screenshots/accounts.jpg)
12
+
13
+ ## Quick Start
14
+
15
+ ### Docker
16
+
17
+ ```bash
18
+ docker run -d --name codex-lb \
19
+ -p 2455:2455 -p 1455:1455 \
20
+ -v ~/.codex-lb:/var/lib/codex-lb \
21
+ ghcr.io/soju06/codex-lb:latest
22
+ ```
23
+
24
+ ### uvx
25
+
26
+ ```bash
27
+ uvx codex-lb
28
+ ```
29
+
30
+ Open [localhost:2455](http://localhost:2455) → Add account → Done.
31
+
32
+
33
+
34
+ ## Codex CLI & Extension Setup
35
+
36
+ Add to `~/.codex/config.toml`:
37
+
38
+ ```toml
39
+ model = "gpt-5.2-codex"
40
+ model_reasoning_effort = "xhigh"
41
+ model_provider = "codex-lb"
42
+
43
+ [model_providers.codex-lb]
44
+ name = "OpenAI" # MUST be "OpenAI" - enables /compact endpoint
45
+ base_url = "http://127.0.0.1:2455/backend-api/codex"
46
+ wire_api = "responses"
47
+ chatgpt_base_url = "http://127.0.0.1:2455"
48
+ requires_openai_auth = true # Required: enables model selection in Codex IDE extension
49
+ ```
50
+
51
+ ## Data
52
+
53
+ All data stored in `~/.codex-lb/`:
54
+
55
+ - `store.db` – accounts, usage logs
56
+ - `encryption.key` – encrypts tokens (auto-generated)
57
+
58
+ Backup this directory to preserve your accounts.
59
+
60
+ ## Contributors ✨
61
+
62
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
63
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
64
+ <!-- prettier-ignore-start -->
65
+ <!-- markdownlint-disable -->
66
+ <table>
67
+ <tbody>
68
+ <tr>
69
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/Soju06"><img src="https://avatars.githubusercontent.com/u/34199905?v=4?s=100" width="100px;" alt="Soju06"/><br /><sub><b>Soju06</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=Soju06" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=Soju06" title="Tests">⚠️</a> <a href="#maintenance-Soju06" title="Maintenance">🚧</a> <a href="#infra-Soju06" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
70
+ <td align="center" valign="top" width="14.28%"><a href="http://jonas.kamsker.at/"><img src="https://avatars.githubusercontent.com/u/11245306?v=4?s=100" width="100px;" alt="Jonas Kamsker"/><br /><sub><b>Jonas Kamsker</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=JKamsker" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3AJKamsker" title="Bug reports">🐛</a> <a href="#maintenance-JKamsker" title="Maintenance">🚧</a></td>
71
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/Quack6765"><img src="https://avatars.githubusercontent.com/u/5446230?v=4?s=100" width="100px;" alt="Quack"/><br /><sub><b>Quack</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=Quack6765" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3AQuack6765" title="Bug reports">🐛</a> <a href="#maintenance-Quack6765" title="Maintenance">🚧</a> <a href="#design-Quack6765" title="Design">🎨</a></td>
72
+ </tr>
73
+ </tbody>
74
+ </table>
75
+
76
+ <!-- markdownlint-restore -->
77
+ <!-- prettier-ignore-end -->
78
+
79
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
80
+
81
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
@@ -90,7 +90,17 @@ def claims_from_auth(auth: AuthFile) -> AccountClaims:
90
90
  )
91
91
 
92
92
 
93
+ def generate_unique_account_id(account_id: str | None, email: str | None) -> str:
94
+ if account_id and email and email != DEFAULT_EMAIL:
95
+ email_hash = hashlib.sha256(email.encode()).hexdigest()[:8]
96
+ return f"{account_id}_{email_hash}"
97
+ if account_id:
98
+ return account_id
99
+ return fallback_account_id(email)
100
+
101
+
93
102
  def fallback_account_id(email: str | None) -> str:
103
+ """Generate a fallback account ID when no OpenAI account ID is available."""
94
104
  if email and email != DEFAULT_EMAIL:
95
105
  digest = hashlib.sha256(email.encode()).hexdigest()[:12]
96
106
  return f"email_{digest}"
@@ -16,6 +16,9 @@ PERMANENT_FAILURE_CODES = {
16
16
  "account_deleted": "Account has been deleted",
17
17
  }
18
18
 
19
+ SECONDS_PER_DAY = 60 * 60 * 24
20
+ UNKNOWN_RESET_BUCKET_DAYS = 10_000
21
+
19
22
 
20
23
  @dataclass
21
24
  class AccountState:
@@ -24,6 +27,8 @@ class AccountState:
24
27
  used_percent: float | None = None
25
28
  reset_at: float | None = None
26
29
  cooldown_until: float | None = None
30
+ secondary_used_percent: float | None = None
31
+ secondary_reset_at: int | None = None
27
32
  last_error_at: float | None = None
28
33
  last_selected_at: float | None = None
29
34
  error_count: int = 0
@@ -36,7 +41,12 @@ class SelectionResult:
36
41
  error_message: str | None
37
42
 
38
43
 
39
- def select_account(states: Iterable[AccountState], now: float | None = None) -> SelectionResult:
44
+ def select_account(
45
+ states: Iterable[AccountState],
46
+ now: float | None = None,
47
+ *,
48
+ prefer_earlier_reset: bool = False,
49
+ ) -> SelectionResult:
40
50
  current = now or time.time()
41
51
  available: list[AccountState] = []
42
52
  all_states = list(states)
@@ -95,18 +105,35 @@ def select_account(states: Iterable[AccountState], now: float | None = None) ->
95
105
  return SelectionResult(None, f"Rate limit exceeded. Try again in {wait_seconds:.0f}s")
96
106
  return SelectionResult(None, "No available accounts")
97
107
 
98
- def _sort_key(state: AccountState) -> tuple[float, float, str]:
99
- used = state.used_percent if state.used_percent is not None else 0.0
108
+ def _usage_sort_key(state: AccountState) -> tuple[float, float, float, str]:
109
+ primary_used = state.used_percent if state.used_percent is not None else 0.0
110
+ secondary_used = state.secondary_used_percent if state.secondary_used_percent is not None else primary_used
100
111
  last_selected = state.last_selected_at or 0.0
101
- return used, last_selected, state.account_id
102
-
103
- selected = min(available, key=_sort_key)
112
+ return secondary_used, primary_used, last_selected, state.account_id
113
+
114
+ def _reset_first_sort_key(state: AccountState) -> tuple[int, float, float, float, str]:
115
+ reset_bucket_days = UNKNOWN_RESET_BUCKET_DAYS
116
+ if state.secondary_reset_at is not None:
117
+ reset_bucket_days = max(
118
+ 0,
119
+ int((state.secondary_reset_at - current) // SECONDS_PER_DAY),
120
+ )
121
+ secondary_used, primary_used, last_selected, account_id = _usage_sort_key(state)
122
+ return reset_bucket_days, secondary_used, primary_used, last_selected, account_id
123
+
124
+ selected = min(available, key=_reset_first_sort_key if prefer_earlier_reset else _usage_sort_key)
104
125
  return SelectionResult(selected, None)
105
126
 
106
127
 
107
128
  def handle_rate_limit(state: AccountState, error: UpstreamError) -> None:
129
+ state.status = AccountStatus.RATE_LIMITED
108
130
  state.error_count += 1
109
131
  state.last_error_at = time.time()
132
+
133
+ reset_at = _extract_reset_at(error)
134
+ if reset_at is not None:
135
+ state.reset_at = reset_at
136
+
110
137
  message = error.get("message")
111
138
  delay = parse_retry_after(message) if message else None
112
139
  if delay is None:
@@ -40,6 +40,8 @@ class Settings(BaseSettings):
40
40
  usage_refresh_interval_seconds: int = 60
41
41
  encryption_key_file: Path = DEFAULT_ENCRYPTION_KEY_FILE
42
42
  database_migrations_fail_fast: bool = True
43
+ log_proxy_request_shape: bool = False
44
+ log_proxy_request_shape_raw_cache_key: bool = False
43
45
 
44
46
  @field_validator("database_url")
45
47
  @classmethod
@@ -17,12 +17,14 @@ from app.db.models import Account
17
17
  PLAN_CAPACITY_CREDITS_PRIMARY = {
18
18
  "plus": 225.0,
19
19
  "business": 225.0,
20
+ "team": 225.0,
20
21
  "pro": 1500.0,
21
22
  }
22
23
 
23
24
  PLAN_CAPACITY_CREDITS_SECONDARY = {
24
25
  "plus": 7560.0,
25
26
  "business": 7560.0,
27
+ "team": 7560.0,
26
28
  "pro": 50400.0,
27
29
  }
28
30
 
@@ -13,6 +13,17 @@ class RequestLogLike(Protocol):
13
13
  reasoning_tokens: int | None
14
14
 
15
15
 
16
+ def cached_input_tokens_from_log(log: RequestLogLike) -> int | None:
17
+ cached_tokens = log.cached_input_tokens
18
+ if cached_tokens is None:
19
+ return None
20
+ cached_tokens = max(0, int(cached_tokens))
21
+ input_tokens = log.input_tokens
22
+ if input_tokens is not None:
23
+ cached_tokens = min(cached_tokens, int(input_tokens))
24
+ return cached_tokens
25
+
26
+
16
27
  def usage_tokens_from_log(log: RequestLogLike) -> UsageTokens | None:
17
28
  input_tokens = log.input_tokens
18
29
  if input_tokens is None:
@@ -20,8 +31,7 @@ def usage_tokens_from_log(log: RequestLogLike) -> UsageTokens | None:
20
31
  output_tokens = log.output_tokens if log.output_tokens is not None else log.reasoning_tokens
21
32
  if output_tokens is None:
22
33
  return None
23
- cached_tokens = log.cached_input_tokens or 0
24
- cached_tokens = max(0, min(cached_tokens, input_tokens))
34
+ cached_tokens = cached_input_tokens_from_log(log) or 0
25
35
  return UsageTokens(
26
36
  input_tokens=float(input_tokens),
27
37
  output_tokens=float(output_tokens),
@@ -30,8 +30,11 @@ def apply_usage_quota(
30
30
  reset_at = secondary_reset
31
31
  return status, used_percent, reset_at
32
32
  if status == AccountStatus.QUOTA_EXCEEDED:
33
- status = AccountStatus.ACTIVE
34
- reset_at = None
33
+ if runtime_reset and runtime_reset > time.time():
34
+ reset_at = runtime_reset
35
+ else:
36
+ status = AccountStatus.ACTIVE
37
+ reset_at = None
35
38
  elif status == AccountStatus.QUOTA_EXCEEDED and secondary_reset is not None:
36
39
  reset_at = secondary_reset
37
40
 
@@ -45,8 +48,11 @@ def apply_usage_quota(
45
48
  reset_at = _fallback_primary_reset(primary_window_minutes) or reset_at
46
49
  return status, used_percent, reset_at
47
50
  if status == AccountStatus.RATE_LIMITED:
48
- status = AccountStatus.ACTIVE
49
- reset_at = None
51
+ if runtime_reset and runtime_reset > time.time():
52
+ reset_at = runtime_reset
53
+ else:
54
+ status = AccountStatus.ACTIVE
55
+ reset_at = None
50
56
 
51
57
  return status, used_percent, reset_at
52
58
 
@@ -67,8 +67,9 @@ class UsageCostSummary:
67
67
  class UsageMetricsSummary:
68
68
  requests_7d: int | None
69
69
  tokens_secondary_window: int | None
70
- error_rate_7d: float | None
71
- top_error: str | None
70
+ cached_tokens_secondary_window: int | None = None
71
+ error_rate_7d: float | None = None
72
+ top_error: str | None = None
72
73
 
73
74
 
74
75
  @dataclass(frozen=True)
@@ -8,7 +8,13 @@ from typing import Awaitable, Callable, Final
8
8
  from sqlalchemy import text
9
9
  from sqlalchemy.ext.asyncio import AsyncSession
10
10
 
11
- from app.db.migrations.versions import normalize_account_plan_types
11
+ from app.db.migrations.versions import (
12
+ add_accounts_chatgpt_account_id,
13
+ add_accounts_reset_at,
14
+ add_dashboard_settings,
15
+ add_request_logs_reasoning_effort,
16
+ normalize_account_plan_types,
17
+ )
12
18
 
13
19
  _CREATE_MIGRATIONS_TABLE = """
14
20
  CREATE TABLE IF NOT EXISTS schema_migrations (
@@ -21,6 +27,7 @@ _INSERT_MIGRATION = """
21
27
  INSERT INTO schema_migrations (name, applied_at)
22
28
  VALUES (:name, :applied_at)
23
29
  ON CONFLICT(name) DO NOTHING
30
+ RETURNING name
24
31
  """
25
32
 
26
33
 
@@ -32,6 +39,10 @@ class Migration:
32
39
 
33
40
  MIGRATIONS: Final[tuple[Migration, ...]] = (
34
41
  Migration("001_normalize_account_plan_types", normalize_account_plan_types.run),
42
+ Migration("002_add_request_logs_reasoning_effort", add_request_logs_reasoning_effort.run),
43
+ Migration("003_add_accounts_reset_at", add_accounts_reset_at.run),
44
+ Migration("004_add_accounts_chatgpt_account_id", add_accounts_chatgpt_account_id.run),
45
+ Migration("005_add_dashboard_settings", add_dashboard_settings.run),
35
46
  )
36
47
 
37
48
 
@@ -54,8 +65,8 @@ async def _apply_migration(session: AsyncSession, migration: Migration) -> bool:
54
65
  "applied_at": _utcnow_iso(),
55
66
  },
56
67
  )
57
- rowcount = getattr(result, "rowcount", 0) or 0
58
- if not rowcount:
68
+ inserted = result.scalar_one_or_none()
69
+ if inserted is None:
59
70
  return False
60
71
  await migration.run(session)
61
72
  return True
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import text
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+
7
+ async def run(session: AsyncSession) -> None:
8
+ bind = session.get_bind()
9
+ dialect = getattr(getattr(bind, "dialect", None), "name", None)
10
+ if dialect == "sqlite":
11
+ await _sqlite_add_column_if_missing(session, "accounts", "chatgpt_account_id", "VARCHAR")
12
+ elif dialect == "postgresql":
13
+ await session.execute(
14
+ text("ALTER TABLE accounts ADD COLUMN IF NOT EXISTS chatgpt_account_id VARCHAR"),
15
+ )
16
+
17
+
18
+ async def _sqlite_add_column_if_missing(
19
+ session: AsyncSession,
20
+ table: str,
21
+ column: str,
22
+ column_type: str,
23
+ ) -> None:
24
+ result = await session.execute(text(f"PRAGMA table_info({table})"))
25
+ rows = result.fetchall()
26
+ existing = {row[1] for row in rows if len(row) > 1}
27
+ if column in existing:
28
+ return
29
+ await session.execute(text(f"ALTER TABLE {table} ADD COLUMN {column} {column_type}"))
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import text
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+
7
+ async def run(session: AsyncSession) -> None:
8
+ bind = session.get_bind()
9
+ dialect = getattr(getattr(bind, "dialect", None), "name", None)
10
+ if dialect == "sqlite":
11
+ await _sqlite_add_column_if_missing(session, "accounts", "reset_at", "INTEGER")
12
+ elif dialect == "postgresql":
13
+ await session.execute(
14
+ text("ALTER TABLE accounts ADD COLUMN IF NOT EXISTS reset_at INTEGER"),
15
+ )
16
+
17
+
18
+ async def _sqlite_add_column_if_missing(
19
+ session: AsyncSession,
20
+ table: str,
21
+ column: str,
22
+ column_type: str,
23
+ ) -> None:
24
+ result = await session.execute(text(f"PRAGMA table_info({table})"))
25
+ rows = result.fetchall()
26
+ existing = {row[1] for row in rows if len(row) > 1}
27
+ if column in existing:
28
+ return
29
+ await session.execute(text(f"ALTER TABLE {table} ADD COLUMN {column} {column_type}"))
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import inspect
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from sqlalchemy.orm import Session
6
+
7
+ from app.db.models import DashboardSettings
8
+
9
+
10
+ def _settings_table_exists(session: Session) -> bool:
11
+ inspector = inspect(session.connection())
12
+ return inspector.has_table("dashboard_settings")
13
+
14
+
15
+ async def run(session: AsyncSession) -> None:
16
+ exists = await session.run_sync(_settings_table_exists)
17
+ if not exists:
18
+ return
19
+
20
+ row = await session.get(DashboardSettings, 1)
21
+ if row is not None:
22
+ return
23
+
24
+ session.add(
25
+ DashboardSettings(
26
+ id=1,
27
+ sticky_threads_enabled=False,
28
+ prefer_earlier_reset_accounts=False,
29
+ )
30
+ )
31
+ await session.flush()