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.
- {codex_lb-0.1.2 → codex_lb-0.1.4}/.github/release-please-config.json +1 -2
- codex_lb-0.1.4/.github/release-please-manifest.json +3 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/.github/workflows/release-please.yml +1 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/.github/workflows/release.yml +60 -42
- {codex_lb-0.1.2 → codex_lb-0.1.4}/CHANGELOG.md +14 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/PKG-INFO +3 -3
- {codex_lb-0.1.2 → codex_lb-0.1.4}/README.md +2 -2
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/db/session.py +27 -9
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/dependencies.py +11 -10
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/service.py +36 -15
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/repository.py +17 -3
- {codex_lb-0.1.2 → codex_lb-0.1.4}/pyproject.toml +1 -1
- codex_lb-0.1.2/.github/release-please-manifest.json +0 -3
- {codex_lb-0.1.2 → codex_lb-0.1.4}/.dockerignore +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/.env.example +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/.github/workflows/ci.yml +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/.gitignore +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/.pre-commit-config.yaml +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/AGENTS.md +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/Dockerfile +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/LICENSE +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/cli.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/auth/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/auth/models.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/auth/refresh.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/balancer/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/balancer/logic.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/balancer/types.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/http.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/oauth.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/proxy.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/clients/usage.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/config/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/config/settings.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/crypto.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/errors.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/openai/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/openai/models.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/openai/parsing.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/openai/requests.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/types.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/logs.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/models.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/pricing.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/usage/types.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/request_id.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/retry.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/sse.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/core/utils/time.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/db/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/db/models.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/main.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/repository.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/schemas.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/accounts/service.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/health/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/health/api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/schemas.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/service.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/oauth/templates/oauth_success.html +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/auth_manager.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/load_balancer.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/schemas.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/types.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/proxy/usage_updater.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/schemas.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/request_logs/service.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/shared/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/shared/schemas.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/repository.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/schemas.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/modules/usage/service.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/static/7.css +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/static/index.css +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/static/index.html +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/app/static/index.js +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/docker-compose.yml +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/docs/screenshots/accounts.jpeg +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/docs/screenshots/dashboard.jpeg +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/__init__.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/conftest.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_accounts_api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_accounts_api_extended.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_codex_usage_api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_db_models.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_health_and_errors.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_load_balancer_integration.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_oauth_flow.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_proxy_api_extended.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_proxy_compact.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_proxy_responses.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_repositories.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_request_logs_api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_request_logs_filters.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_usage_api.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/integration/test_usage_summary.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_auth.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_auth_refresh.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_load_balancer.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_oauth_client.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_pricing.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_proxy_utils.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_retry.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_sse.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_usage.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/tests/unit/test_usage_client.py +0 -0
- {codex_lb-0.1.2 → codex_lb-0.1.4}/uv.lock +0 -0
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
name: Release
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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:
|
|
162
|
-
id:
|
|
177
|
+
- name: Resolve previous tag
|
|
178
|
+
id: prev_tag
|
|
163
179
|
shell: bash
|
|
164
180
|
run: |
|
|
165
181
|
set -euo pipefail
|
|
166
|
-
TAG_NAME="${
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-

|
|
83
|
+

|
|
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
|
-

|
|
30
|
+

|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|