codex-lb 0.1.2__tar.gz → 0.1.4__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 (123) hide show
  1. {codex_lb-0.1.2 → codex_lb-0.1.4}/.github/release-please-config.json +1 -2
  2. codex_lb-0.1.4/.github/release-please-manifest.json +3 -0
  3. {codex_lb-0.1.2 → codex_lb-0.1.4}/.github/workflows/release-please.yml +1 -0
  4. {codex_lb-0.1.2 → codex_lb-0.1.4}/.github/workflows/release.yml +60 -42
  5. {codex_lb-0.1.2 → codex_lb-0.1.4}/CHANGELOG.md +14 -0
  6. {codex_lb-0.1.2 → codex_lb-0.1.4}/PKG-INFO +3 -3
  7. {codex_lb-0.1.2 → codex_lb-0.1.4}/README.md +2 -2
  8. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/db/session.py +27 -9
  9. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/dependencies.py +11 -10
  10. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/service.py +36 -15
  11. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/repository.py +17 -3
  12. {codex_lb-0.1.2 → codex_lb-0.1.4}/pyproject.toml +1 -1
  13. codex_lb-0.1.2/.github/release-please-manifest.json +0 -3
  14. {codex_lb-0.1.2 → codex_lb-0.1.4}/.dockerignore +0 -0
  15. {codex_lb-0.1.2 → codex_lb-0.1.4}/.env.example +0 -0
  16. {codex_lb-0.1.2 → codex_lb-0.1.4}/.github/workflows/ci.yml +0 -0
  17. {codex_lb-0.1.2 → codex_lb-0.1.4}/.gitignore +0 -0
  18. {codex_lb-0.1.2 → codex_lb-0.1.4}/.pre-commit-config.yaml +0 -0
  19. {codex_lb-0.1.2 → codex_lb-0.1.4}/AGENTS.md +0 -0
  20. {codex_lb-0.1.2 → codex_lb-0.1.4}/Dockerfile +0 -0
  21. {codex_lb-0.1.2 → codex_lb-0.1.4}/LICENSE +0 -0
  22. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/__init__.py +0 -0
  23. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/cli.py +0 -0
  24. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/__init__.py +0 -0
  25. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/auth/__init__.py +0 -0
  26. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/auth/models.py +0 -0
  27. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/auth/refresh.py +0 -0
  28. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/balancer/__init__.py +0 -0
  29. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/balancer/logic.py +0 -0
  30. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/balancer/types.py +0 -0
  31. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/__init__.py +0 -0
  32. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/http.py +0 -0
  33. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/oauth.py +0 -0
  34. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/proxy.py +0 -0
  35. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/usage.py +0 -0
  36. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/config/__init__.py +0 -0
  37. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/config/settings.py +0 -0
  38. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/crypto.py +0 -0
  39. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/errors.py +0 -0
  40. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/openai/__init__.py +0 -0
  41. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/openai/models.py +0 -0
  42. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/openai/parsing.py +0 -0
  43. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/openai/requests.py +0 -0
  44. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/types.py +0 -0
  45. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/__init__.py +0 -0
  46. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/logs.py +0 -0
  47. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/models.py +0 -0
  48. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/pricing.py +0 -0
  49. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/types.py +0 -0
  50. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/__init__.py +0 -0
  51. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/request_id.py +0 -0
  52. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/retry.py +0 -0
  53. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/sse.py +0 -0
  54. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/time.py +0 -0
  55. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/db/__init__.py +0 -0
  56. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/db/models.py +0 -0
  57. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/main.py +0 -0
  58. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/__init__.py +0 -0
  59. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/__init__.py +0 -0
  60. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/api.py +0 -0
  61. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/repository.py +0 -0
  62. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/schemas.py +0 -0
  63. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/service.py +0 -0
  64. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/health/__init__.py +0 -0
  65. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/health/api.py +0 -0
  66. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/__init__.py +0 -0
  67. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/api.py +0 -0
  68. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/schemas.py +0 -0
  69. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/service.py +0 -0
  70. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/templates/oauth_success.html +0 -0
  71. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/__init__.py +0 -0
  72. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/api.py +0 -0
  73. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/auth_manager.py +0 -0
  74. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/load_balancer.py +0 -0
  75. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/schemas.py +0 -0
  76. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/types.py +0 -0
  77. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/usage_updater.py +0 -0
  78. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/__init__.py +0 -0
  79. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/api.py +0 -0
  80. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/schemas.py +0 -0
  81. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/service.py +0 -0
  82. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/shared/__init__.py +0 -0
  83. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/shared/schemas.py +0 -0
  84. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/__init__.py +0 -0
  85. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/api.py +0 -0
  86. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/repository.py +0 -0
  87. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/schemas.py +0 -0
  88. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/service.py +0 -0
  89. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/static/7.css +0 -0
  90. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/static/index.css +0 -0
  91. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/static/index.html +0 -0
  92. {codex_lb-0.1.2 → codex_lb-0.1.4}/app/static/index.js +0 -0
  93. {codex_lb-0.1.2 → codex_lb-0.1.4}/docker-compose.yml +0 -0
  94. {codex_lb-0.1.2 → codex_lb-0.1.4}/docs/screenshots/accounts.jpeg +0 -0
  95. {codex_lb-0.1.2 → codex_lb-0.1.4}/docs/screenshots/dashboard.jpeg +0 -0
  96. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/__init__.py +0 -0
  97. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/conftest.py +0 -0
  98. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_accounts_api.py +0 -0
  99. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_accounts_api_extended.py +0 -0
  100. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_codex_usage_api.py +0 -0
  101. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_db_models.py +0 -0
  102. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_health_and_errors.py +0 -0
  103. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_load_balancer_integration.py +0 -0
  104. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_oauth_flow.py +0 -0
  105. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_proxy_api_extended.py +0 -0
  106. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_proxy_compact.py +0 -0
  107. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_proxy_responses.py +0 -0
  108. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_repositories.py +0 -0
  109. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_request_logs_api.py +0 -0
  110. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_request_logs_filters.py +0 -0
  111. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_usage_api.py +0 -0
  112. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_usage_summary.py +0 -0
  113. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_auth.py +0 -0
  114. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_auth_refresh.py +0 -0
  115. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_load_balancer.py +0 -0
  116. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_oauth_client.py +0 -0
  117. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_pricing.py +0 -0
  118. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_proxy_utils.py +0 -0
  119. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_retry.py +0 -0
  120. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_sse.py +0 -0
  121. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_usage.py +0 -0
  122. {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_usage_client.py +0 -0
  123. {codex_lb-0.1.2 → codex_lb-0.1.4}/uv.lock +0 -0
@@ -6,8 +6,7 @@
6
6
  "changelog-path": "CHANGELOG.md",
7
7
  "extra-files": [
8
8
  "app/__init__.py"
9
- ],
10
- "skip-github-release": true
9
+ ]
11
10
  }
12
11
  }
13
12
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.4"
3
+ }
@@ -4,6 +4,7 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - main
7
+ workflow_dispatch:
7
8
 
8
9
  concurrency:
9
10
  group: ${{ github.workflow }}-${{ github.ref }}
@@ -1,15 +1,21 @@
1
1
  name: Release
2
2
 
3
3
  on:
4
- push:
5
- tags:
6
- - "v*.*.*"
4
+ release:
5
+ types: [published]
7
6
  workflow_dispatch:
7
+ inputs:
8
+ tag:
9
+ description: "Release tag (e.g. v0.1.2)"
10
+ required: true
8
11
 
9
12
  concurrency:
10
13
  group: ${{ github.workflow }}-${{ github.ref }}
11
14
  cancel-in-progress: true
12
15
 
16
+ env:
17
+ RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
18
+
13
19
  jobs:
14
20
  build:
15
21
  name: Build (sdist + wheel)
@@ -20,6 +26,7 @@ jobs:
20
26
  uses: actions/checkout@v4
21
27
  with:
22
28
  fetch-depth: 0
29
+ ref: ${{ env.RELEASE_TAG }}
23
30
 
24
31
  - name: Set up uv
25
32
  uses: astral-sh/setup-uv@v5
@@ -40,8 +47,10 @@ jobs:
40
47
  except Exception: # pragma: no cover
41
48
  import tomli as tomllib
42
49
 
43
- tag_ref = os.environ.get("GITHUB_REF", "")
44
- tag = tag_ref.split("/")[-1]
50
+ tag = os.environ.get("RELEASE_TAG", "")
51
+ if not tag:
52
+ print("Missing RELEASE_TAG")
53
+ sys.exit(1)
45
54
  m = re.fullmatch(r"v(\d+\.\d+\.\d+)", tag)
46
55
  if not m:
47
56
  print(f"Invalid tag format: {tag!r}")
@@ -105,10 +114,15 @@ jobs:
105
114
  steps:
106
115
  - name: Checkout repository
107
116
  uses: actions/checkout@v4
117
+ with:
118
+ ref: ${{ env.RELEASE_TAG }}
108
119
 
109
120
  - name: Set up Docker Buildx
110
121
  uses: docker/setup-buildx-action@v3
111
122
 
123
+ - name: Set up QEMU
124
+ uses: docker/setup-qemu-action@v3
125
+
112
126
  - name: Log in to GHCR
113
127
  uses: docker/login-action@v3
114
128
  with:
@@ -133,6 +147,7 @@ jobs:
133
147
  context: .
134
148
  file: Dockerfile
135
149
  push: true
150
+ platforms: linux/amd64,linux/arm64
136
151
  tags: ${{ steps.meta.outputs.tags }}
137
152
  labels: ${{ steps.meta.outputs.labels }}
138
153
  cache-from: type=gha
@@ -151,6 +166,7 @@ jobs:
151
166
  uses: actions/checkout@v4
152
167
  with:
153
168
  fetch-depth: 0
169
+ ref: ${{ env.RELEASE_TAG }}
154
170
 
155
171
  - name: Download dist artifacts
156
172
  uses: actions/download-artifact@v4
@@ -158,62 +174,64 @@ jobs:
158
174
  name: dist
159
175
  path: dist
160
176
 
161
- - name: Generate release notes
162
- id: notes
177
+ - name: Resolve previous tag
178
+ id: prev_tag
163
179
  shell: bash
164
180
  run: |
165
181
  set -euo pipefail
166
- TAG_NAME="${GITHUB_REF#refs/tags/}"
167
- TAG_VERSION="${TAG_NAME#v}"
168
- RELEASE_DATE="$(date -u +%Y-%m-%d)"
169
-
182
+ TAG_NAME="${RELEASE_TAG}"
170
183
  PREV_TAG=""
171
184
  if git describe --tags --abbrev=0 "${TAG_NAME}^" >/dev/null 2>&1; then
172
185
  PREV_TAG="$(git describe --tags --abbrev=0 "${TAG_NAME}^")"
173
186
  fi
174
-
175
187
  if [ -n "${PREV_TAG}" ]; then
176
188
  RANGE="${PREV_TAG}..${TAG_NAME}"
177
189
  else
178
190
  RANGE="${TAG_NAME}"
179
191
  fi
192
+ echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
193
+ echo "prev_tag=${PREV_TAG}" >> "$GITHUB_OUTPUT"
194
+ echo "range=${RANGE}" >> "$GITHUB_OUTPUT"
180
195
 
181
- COMMIT_COUNT="$(git rev-list --count "${RANGE}")"
182
-
196
+ - name: Generate release notes (GitHub)
197
+ id: notes
198
+ uses: actions/github-script@v7
199
+ env:
200
+ TAG_NAME: ${{ steps.prev_tag.outputs.tag_name }}
201
+ PREV_TAG: ${{ steps.prev_tag.outputs.prev_tag }}
202
+ with:
203
+ script: |
204
+ const fs = require("fs");
205
+ const tag = process.env.TAG_NAME;
206
+ const prev = process.env.PREV_TAG || undefined;
207
+ if (!tag) {
208
+ core.setFailed("Missing TAG_NAME");
209
+ return;
210
+ }
211
+ const { data } = await github.rest.repos.generateReleaseNotes({
212
+ owner: context.repo.owner,
213
+ repo: context.repo.repo,
214
+ tag_name: tag,
215
+ previous_tag_name: prev,
216
+ });
217
+ fs.writeFileSync("release_notes.md", `${data.body.trimEnd()}\\n`);
218
+ core.setOutput("tag_name", tag);
219
+
220
+ - name: Append install and contributors
221
+ shell: bash
222
+ env:
223
+ TAG_NAME: ${{ steps.prev_tag.outputs.tag_name }}
224
+ run: |
225
+ set -euo pipefail
226
+ TAG_VERSION="${TAG_NAME#v}"
183
227
  {
184
- echo "## What's New in ${TAG_NAME}"
185
- echo
186
- echo "- Release date: ${RELEASE_DATE}"
187
- echo "- Commits: ${COMMIT_COUNT}"
188
- echo
189
- if [ -n "${PREV_TAG}" ]; then
190
- echo "### Changes"
191
- git log --pretty=format:"- %s" "${RANGE}"
192
- else
193
- echo "### Changes"
194
- git log --pretty=format:"- %s" "${RANGE}"
195
- fi
196
228
  echo
197
- echo
198
- echo "### Install / Run"
229
+ echo "## Install / Run"
199
230
  echo '```bash'
200
231
  echo 'uvx codex-lb'
201
- echo 'pip install codex-lb'
202
232
  echo "docker pull ghcr.io/${GITHUB_REPOSITORY}:${TAG_VERSION}"
203
233
  echo '```'
204
- echo
205
- echo "### Artifacts"
206
- if ls dist >/dev/null 2>&1; then
207
- ls -1 dist | sed 's/^/- dist\\//'
208
- else
209
- echo "- dist/ (not available)"
210
- fi
211
- echo
212
- echo "### Contributors"
213
- git shortlog -s "${RANGE}" | awk '{$1=$1}1' | sed 's/^/- /'
214
- } > release_notes.md
215
-
216
- echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
234
+ } >> release_notes.md
217
235
 
218
236
  - name: Create GitHub Release
219
237
  uses: softprops/action-gh-release@v2
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.4](https://github.com/Soju06/codex-lb/compare/v0.1.3...v0.1.4) (2026-01-13)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **db:** harden session cleanup on cancellation ([dee3916](https://github.com/Soju06/codex-lb/commit/dee3916efa83dedec1d5ad43e1e14950b8c6e4a7))
9
+
10
+ ## [0.1.3](https://github.com/Soju06/codex-lb/compare/v0.1.2...v0.1.3) (2026-01-12)
11
+
12
+
13
+ ### Documentation
14
+
15
+ * use absolute image URLs for PyPI ([5fa65a5](https://github.com/Soju06/codex-lb/commit/5fa65a572980f356738f49be3adf2c62fdc38466))
16
+
3
17
  ## [0.1.2](https://github.com/Soju06/codex-lb/compare/v0.1.1...v0.1.2) (2026-01-12)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-lb
3
- Version: 0.1.2
3
+ Version: 0.1.4
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>
@@ -56,7 +56,7 @@ Description-Content-Type: text/markdown
56
56
  Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
57
57
 
58
58
  <p align="center">
59
- <img src="docs/screenshots/dashboard.jpeg" alt="Codex Load Balancer dashboard" width="100%">
59
+ <img src="https://raw.githubusercontent.com/Soju06/codex-lb/main/docs/screenshots/dashboard.jpeg" alt="Codex Load Balancer dashboard" width="100%">
60
60
  </p>
61
61
 
62
62
  ## Quick Start
@@ -80,7 +80,7 @@ Open [localhost:2455](http://localhost:2455) → Add account → Done.
80
80
 
81
81
  ## Accounts view
82
82
 
83
- ![Accounts list and details](docs/screenshots/accounts.jpeg)
83
+ ![Accounts list and details](https://raw.githubusercontent.com/Soju06/codex-lb/main/docs/screenshots/accounts.jpeg)
84
84
 
85
85
  ## Codex CLI & Extension Setup
86
86
 
@@ -3,7 +3,7 @@
3
3
  Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
4
4
 
5
5
  <p align="center">
6
- <img src="docs/screenshots/dashboard.jpeg" alt="Codex Load Balancer dashboard" width="100%">
6
+ <img src="https://raw.githubusercontent.com/Soju06/codex-lb/main/docs/screenshots/dashboard.jpeg" alt="Codex Load Balancer dashboard" width="100%">
7
7
  </p>
8
8
 
9
9
  ## Quick Start
@@ -27,7 +27,7 @@ Open [localhost:2455](http://localhost:2455) → Add account → Done.
27
27
 
28
28
  ## Accounts view
29
29
 
30
- ![Accounts list and details](docs/screenshots/accounts.jpeg)
30
+ ![Accounts list and details](https://raw.githubusercontent.com/Soju06/codex-lb/main/docs/screenshots/accounts.jpeg)
31
31
 
32
32
  ## Codex CLI & Extension Setup
33
33
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  from pathlib import Path
4
5
  from typing import AsyncIterator
5
6
 
@@ -23,16 +24,33 @@ def _ensure_sqlite_dir(url: str) -> None:
23
24
  Path(path).expanduser().parent.mkdir(parents=True, exist_ok=True)
24
25
 
25
26
 
27
+ async def _safe_rollback(session: AsyncSession) -> None:
28
+ if not session.in_transaction():
29
+ return
30
+ try:
31
+ await asyncio.shield(session.rollback())
32
+ except Exception:
33
+ return
34
+
35
+
36
+ async def _safe_close(session: AsyncSession) -> None:
37
+ try:
38
+ await asyncio.shield(session.close())
39
+ except Exception:
40
+ return
41
+
42
+
26
43
  async def get_session() -> AsyncIterator[AsyncSession]:
27
- async with SessionLocal() as session:
28
- try:
29
- yield session
30
- except Exception:
31
- await session.rollback()
32
- raise
33
- finally:
34
- if session.in_transaction():
35
- await session.rollback()
44
+ session = SessionLocal()
45
+ try:
46
+ yield session
47
+ except BaseException:
48
+ await _safe_rollback(session)
49
+ raise
50
+ finally:
51
+ if session.in_transaction():
52
+ await _safe_rollback(session)
53
+ await _safe_close(session)
36
54
 
37
55
 
38
56
  async def init_db() -> None:
@@ -7,7 +7,7 @@ from dataclasses import dataclass
7
7
  from fastapi import Depends
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
 
10
- from app.db.session import SessionLocal, get_session
10
+ from app.db.session import SessionLocal, _safe_close, _safe_rollback, get_session
11
11
  from app.modules.accounts.repository import AccountsRepository
12
12
  from app.modules.accounts.service import AccountsService
13
13
  from app.modules.oauth.service import OauthService
@@ -87,15 +87,16 @@ def get_usage_context(
87
87
 
88
88
  @asynccontextmanager
89
89
  async def _accounts_repo_context() -> AsyncIterator[AccountsRepository]:
90
- async with SessionLocal() as session:
91
- try:
92
- yield AccountsRepository(session)
93
- except Exception:
94
- await session.rollback()
95
- raise
96
- finally:
97
- if session.in_transaction():
98
- await session.rollback()
90
+ session = SessionLocal()
91
+ try:
92
+ yield AccountsRepository(session)
93
+ except BaseException:
94
+ await _safe_rollback(session)
95
+ raise
96
+ finally:
97
+ if session.in_transaction():
98
+ await _safe_rollback(session)
99
+ await _safe_close(session)
99
100
 
100
101
 
101
102
  def get_oauth_context(
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import time
4
5
  from datetime import timedelta
5
6
  from typing import AsyncIterator, Iterable, Mapping
@@ -36,6 +37,8 @@ from app.modules.proxy.usage_updater import UsageUpdater
36
37
  from app.modules.request_logs.repository import RequestLogsRepository
37
38
  from app.modules.usage.repository import UsageRepository
38
39
 
40
+ logger = logging.getLogger(__name__)
41
+
39
42
 
40
43
  class ProxyService:
41
44
  def __init__(
@@ -191,6 +194,7 @@ class ProxyService:
191
194
  yield format_sse_event(event)
192
195
  return
193
196
 
197
+ account_id_value = account.id
194
198
  try:
195
199
  account = await self._ensure_fresh(account)
196
200
  async for line in self._stream_once(
@@ -243,7 +247,15 @@ class ProxyService:
243
247
  await self._load_balancer.mark_permanent_failure(account, exc.code)
244
248
  continue
245
249
  except Exception:
246
- await self._load_balancer.record_error(account)
250
+ try:
251
+ await self._load_balancer.record_error(account)
252
+ except Exception:
253
+ logger.warning(
254
+ "Failed to record proxy error account_id=%s request_id=%s",
255
+ account_id_value,
256
+ request_id,
257
+ exc_info=True,
258
+ )
247
259
  if attempt == max_attempts - 1:
248
260
  event = response_failed_event(
249
261
  "upstream_error",
@@ -267,8 +279,9 @@ class ProxyService:
267
279
  request_id: str,
268
280
  allow_retry: bool,
269
281
  ) -> AsyncIterator[str]:
282
+ account_id_value = account.id
270
283
  access_token = self._encryptor.decrypt(account.access_token_encrypted)
271
- account_id = _header_account_id(account.id)
284
+ account_id = _header_account_id(account_id_value)
272
285
  model = payload.model
273
286
  start = time.monotonic()
274
287
  status = "success"
@@ -341,19 +354,27 @@ class ProxyService:
341
354
  reasoning_tokens = (
342
355
  usage.output_tokens_details.reasoning_tokens if usage and usage.output_tokens_details else None
343
356
  )
344
- await self._logs_repo.add_log(
345
- account_id=account.id,
346
- request_id=request_id,
347
- model=model,
348
- input_tokens=input_tokens,
349
- output_tokens=output_tokens,
350
- cached_input_tokens=cached_input_tokens,
351
- reasoning_tokens=reasoning_tokens,
352
- latency_ms=latency_ms,
353
- status=status,
354
- error_code=error_code,
355
- error_message=error_message,
356
- )
357
+ try:
358
+ await self._logs_repo.add_log(
359
+ account_id=account_id_value,
360
+ request_id=request_id,
361
+ model=model,
362
+ input_tokens=input_tokens,
363
+ output_tokens=output_tokens,
364
+ cached_input_tokens=cached_input_tokens,
365
+ reasoning_tokens=reasoning_tokens,
366
+ latency_ms=latency_ms,
367
+ status=status,
368
+ error_code=error_code,
369
+ error_message=error_message,
370
+ )
371
+ except Exception:
372
+ logger.warning(
373
+ "Failed to persist request log account_id=%s request_id=%s",
374
+ account_id_value,
375
+ request_id,
376
+ exc_info=True,
377
+ )
357
378
 
358
379
  async def _refresh_usage(self, accounts: list[Account]) -> None:
359
380
  latest_usage = await self._usage_repo.latest_by_account(window="primary")
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  from datetime import datetime
4
5
 
5
6
  from sqlalchemy import and_, select
@@ -49,9 +50,13 @@ class RequestLogsRepository:
49
50
  requested_at=requested_at or utcnow(),
50
51
  )
51
52
  self._session.add(log)
52
- await self._session.commit()
53
- await self._session.refresh(log)
54
- return log
53
+ try:
54
+ await self._session.commit()
55
+ await self._session.refresh(log)
56
+ return log
57
+ except BaseException:
58
+ await _safe_rollback(self._session)
59
+ raise
55
60
 
56
61
  async def list_recent(
57
62
  self,
@@ -84,3 +89,12 @@ class RequestLogsRepository:
84
89
  stmt = stmt.limit(limit)
85
90
  result = await self._session.execute(stmt)
86
91
  return list(result.scalars().all())
92
+
93
+
94
+ async def _safe_rollback(session: AsyncSession) -> None:
95
+ if not session.in_transaction():
96
+ return
97
+ try:
98
+ await asyncio.shield(session.rollback())
99
+ except Exception:
100
+ return
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codex-lb"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "Codex load balancer and proxy for ChatGPT accounts with usage dashboard"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.1.2"
3
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes