codex-lb 0.3.1__tar.gz → 0.4.0__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.3.1 → codex_lb-0.4.0}/.all-contributorsrc +19 -0
- codex_lb-0.4.0/.github/release-please-manifest.json +3 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/.github/workflows/ci.yml +20 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/AGENTS.md +15 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/CHANGELOG.md +19 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/PKG-INFO +41 -3
- {codex_lb-0.3.1 → codex_lb-0.4.0}/README.md +40 -2
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/clients/proxy.py +33 -3
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/config/settings.py +1 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/openai/requests.py +21 -3
- codex_lb-0.4.0/app/core/openai/v1_requests.py +148 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/models.py +3 -3
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/main.py +1 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/accounts/repository.py +4 -1
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/proxy/api.py +36 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/proxy/service.py +29 -0
- codex_lb-0.4.0/app/modules/request_logs/api.py +85 -0
- codex_lb-0.4.0/app/modules/request_logs/repository.py +215 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/request_logs/schemas.py +11 -2
- codex_lb-0.4.0/app/modules/request_logs/service.py +163 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/usage/updater.py +58 -26
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/static/index.css +378 -1
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/static/index.html +183 -8
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/static/index.js +308 -13
- {codex_lb-0.3.1 → codex_lb-0.4.0}/pyproject.toml +1 -1
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_proxy_responses.py +100 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_repositories.py +2 -2
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_request_logs_filters.py +127 -0
- codex_lb-0.4.0/tests/test_request_logs_options_api.py +111 -0
- codex_lb-0.4.0/tests/unit/test_openai_requests.py +105 -0
- codex_lb-0.4.0/tests/unit/test_proxy_errors.py +96 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_proxy_utils.py +27 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_usage_client.py +3 -1
- codex_lb-0.4.0/tests/unit/test_usage_updater.py +343 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/uv.lock +924 -924
- codex_lb-0.3.1/.github/release-please-manifest.json +0 -3
- codex_lb-0.3.1/app/modules/request_logs/api.py +0 -31
- codex_lb-0.3.1/app/modules/request_logs/repository.py +0 -103
- codex_lb-0.3.1/app/modules/request_logs/service.py +0 -86
- codex_lb-0.3.1/tests/unit/test_usage_updater.py +0 -124
- {codex_lb-0.3.1 → codex_lb-0.4.0}/.dockerignore +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/.env.example +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/.github/release-please-config.json +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/.github/workflows/release-please.yml +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/.github/workflows/release.yml +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/.gitignore +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/.pre-commit-config.yaml +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/Dockerfile +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/LICENSE +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/cli.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/auth/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/auth/models.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/auth/refresh.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/balancer/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/balancer/logic.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/balancer/types.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/clients/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/clients/http.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/clients/oauth.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/clients/usage.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/config/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/crypto.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/errors.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/openai/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/openai/models.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/openai/parsing.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/plan_types.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/types.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/usage/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/usage/logs.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/usage/models.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/usage/pricing.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/usage/quota.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/usage/types.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/utils/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/utils/request_id.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/utils/retry.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/utils/sse.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/core/utils/time.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/migrations/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/migrations/versions/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/migrations/versions/add_accounts_chatgpt_account_id.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/migrations/versions/add_accounts_reset_at.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/migrations/versions/add_dashboard_settings.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/migrations/versions/add_request_logs_reasoning_effort.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/migrations/versions/normalize_account_plan_types.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/db/session.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/dependencies.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/accounts/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/accounts/api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/accounts/auth_manager.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/accounts/schemas.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/accounts/service.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/health/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/health/api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/health/schemas.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/oauth/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/oauth/api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/oauth/schemas.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/oauth/service.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/oauth/templates/oauth_success.html +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/proxy/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/proxy/helpers.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/proxy/load_balancer.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/proxy/schemas.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/proxy/sticky_repository.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/proxy/types.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/request_logs/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/settings/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/settings/api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/settings/repository.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/settings/schemas.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/settings/service.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/shared/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/shared/schemas.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/usage/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/usage/api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/usage/repository.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/usage/schemas.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/app/modules/usage/service.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/docker-compose.yml +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/docs/screenshots/accounts.jpg +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/docs/screenshots/dashboard.jpg +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/__init__.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/conftest.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_accounts_api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_accounts_api_extended.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_codex_usage_api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_db_models.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_health_and_errors.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_load_balancer_integration.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_migrations.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_oauth_flow.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_proxy_api_extended.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_proxy_compact.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_proxy_sticky_sessions.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_request_logs_api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_settings_api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_usage_api.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/integration/test_usage_summary.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_auth.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_auth_manager.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_auth_refresh.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_load_balancer.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_oauth_client.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_pricing.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_retry.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_sse.py +0 -0
- {codex_lb-0.3.1 → codex_lb-0.4.0}/tests/unit/test_usage.py +0 -0
|
@@ -44,6 +44,25 @@
|
|
|
44
44
|
"maintenance",
|
|
45
45
|
"design"
|
|
46
46
|
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"login": "hhsw2015",
|
|
50
|
+
"name": "Jill Kok, San Mou",
|
|
51
|
+
"avatar_url": "https://avatars.githubusercontent.com/u/103614420?v=4",
|
|
52
|
+
"profile": "https://github.com/hhsw2015",
|
|
53
|
+
"contributions": [
|
|
54
|
+
"code",
|
|
55
|
+
"test"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"login": "pcy06",
|
|
60
|
+
"name": "PARK CHANYOUNG",
|
|
61
|
+
"avatar_url": "https://avatars.githubusercontent.com/u/44970486?v=4",
|
|
62
|
+
"profile": "https://github.com/pcy06",
|
|
63
|
+
"contributions": [
|
|
64
|
+
"doc"
|
|
65
|
+
]
|
|
47
66
|
}
|
|
48
67
|
],
|
|
49
68
|
"contributorsPerLine": 7,
|
|
@@ -30,6 +30,26 @@ jobs:
|
|
|
30
30
|
- name: Ruff format (check)
|
|
31
31
|
run: uvx ruff format --check .
|
|
32
32
|
|
|
33
|
+
typecheck:
|
|
34
|
+
name: Type check (ty)
|
|
35
|
+
runs-on: ubuntu-24.04
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- name: Checkout repository
|
|
39
|
+
uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- name: Set up uv
|
|
42
|
+
uses: astral-sh/setup-uv@v5
|
|
43
|
+
with:
|
|
44
|
+
python-version: "3.13"
|
|
45
|
+
enable-cache: true
|
|
46
|
+
|
|
47
|
+
- name: Install dependencies
|
|
48
|
+
run: uv sync --dev --frozen
|
|
49
|
+
|
|
50
|
+
- name: Ty check
|
|
51
|
+
run: uv run ty check
|
|
52
|
+
|
|
33
53
|
test:
|
|
34
54
|
name: Tests (pytest)
|
|
35
55
|
runs-on: ubuntu-24.04
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
3
|
## Environment
|
|
4
|
+
|
|
4
5
|
- Python: .venv/bin/python (uv, CPython 3.13.3)
|
|
5
6
|
|
|
6
7
|
## Code Conventions (Typing & Data Contracts)
|
|
8
|
+
|
|
7
9
|
- Prefer strict typing end-to-end. Avoid `dict`, `Mapping[str, object]`, and `object` in app/service/repository layers when the shape is known.
|
|
8
10
|
- Use explicit dataclasses or Pydantic models for internal payloads; convert to response schemas at the edge.
|
|
9
11
|
- ORM models should be passed through services instead of generic containers; avoid `getattr`/`[]` access on ORM results.
|
|
10
12
|
- Expose time values in dashboard APIs as ISO 8601 strings (`datetime` in schemas), not epoch numbers.
|
|
11
13
|
- If a test depends on a contract change (field name/type), update the test to match the new typed schema.
|
|
12
14
|
|
|
15
|
+
## Code Conventions (Anti-Patterns to Avoid)
|
|
16
|
+
|
|
17
|
+
- **No Speculative Fallbacks**: Do not use multiple keys for the same configuration (e.g., `os.getenv("A") or os.getenv("B")`). Pick one canonical name and stick to it.
|
|
18
|
+
- **Single Source of Truth**: Do not create redundant fields in data models (JSON/DB) that represent the same state. Calculate derived values dynamically.
|
|
19
|
+
- **Fail Fast**: Do not clutter code with excessive `None` checks or fallback defaults for critical configurations. Raise explicit errors for missing or invalid configuration.
|
|
20
|
+
- **Refactor over Duplicate**: Do not duplicate logic to avoid touching existing code. Refactor the existing code to support the new requirement.
|
|
21
|
+
|
|
13
22
|
## Code Conventions (Structure & Responsibilities)
|
|
23
|
+
|
|
14
24
|
- Keep domain boundaries clear: `core/` for reusable logic, `modules/*` for API-facing features, `db/` for persistence, `static/` for dashboard assets.
|
|
15
25
|
- Follow module layout conventions in `app/modules/<feature>/`: `api.py` (routes), `service.py` (business logic), `repository.py` (DB access), `schemas.py` (Pydantic I/O models).
|
|
16
26
|
- Prefer small, focused files; split when a file grows beyond a single responsibility or mixes layers.
|
|
@@ -20,6 +30,7 @@
|
|
|
20
30
|
- Validate inputs early and fail fast with clear errors; never silently coerce invalid types.
|
|
21
31
|
|
|
22
32
|
## Code Conventions (Testing / TC)
|
|
33
|
+
|
|
23
34
|
- Add or update tests whenever contracts change (field names/types, response formats, default values).
|
|
24
35
|
- Keep unit tests under `tests/unit` and integration tests under `tests/integration` using existing markers.
|
|
25
36
|
- Tests should assert public behavior (API responses, service outputs) rather than internal implementation details.
|
|
@@ -27,6 +38,7 @@
|
|
|
27
38
|
- Prefer deterministic inputs (fixed timestamps, explicit payloads) to avoid flaky tests.
|
|
28
39
|
|
|
29
40
|
## Code Conventions (DI & Context)
|
|
41
|
+
|
|
30
42
|
- Use FastAPI `Depends` providers in `app/dependencies.py` to construct per-request contexts (`*Context` dataclasses).
|
|
31
43
|
- Contexts should hold only the session, repositories, and service for a single module; avoid cross-module service coupling.
|
|
32
44
|
- Repositories must be constructed with the request-scoped `AsyncSession` from `get_session`; no global sessions.
|
|
@@ -35,6 +47,7 @@
|
|
|
35
47
|
- When adding a new module, define `api.py` endpoints that depend on a module-specific context provider.
|
|
36
48
|
|
|
37
49
|
## Git Workflow & Contribution
|
|
50
|
+
|
|
38
51
|
1. **Important**: Create branches, commits, or PRs **only upon explicit user request**. Implicit actions are not allowed.
|
|
39
52
|
2. **Branch Naming**: Use prefixes like `feature/`, `fix/`, `chore/` (e.g., `feature/add-login`).
|
|
40
53
|
3. **Commit Messages**: Follow [Conventional Commits](https://www.conventionalcommits.org/).
|
|
@@ -42,6 +55,7 @@
|
|
|
42
55
|
- Types: `feat`, `fix`, `docs`, `refactor`, `chore`, `test`
|
|
43
56
|
- Example: `feat(api): add auth endpoint`
|
|
44
57
|
4. **Workflow**:
|
|
58
|
+
|
|
45
59
|
```bash
|
|
46
60
|
git checkout -b feature/add-login
|
|
47
61
|
git commit -m "feat(api): add auth endpoint"
|
|
@@ -49,4 +63,5 @@
|
|
|
49
63
|
git push -u origin feature/add-login
|
|
50
64
|
gh pr create --title "feat(api): add auth" --body "..."
|
|
51
65
|
```
|
|
66
|
+
|
|
52
67
|
5. **Best Practices**: Commit often in small units. Do not commit directly to `main`. Always check `git diff` before pushing.
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/Soju06/codex-lb/compare/v0.3.1...v0.4.0) (2026-01-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **proxy:** add v1 responses compatibility for OpenCode ([#28](https://github.com/Soju06/codex-lb/issues/28)) ([04d58d2](https://github.com/Soju06/codex-lb/commit/04d58d2430e4ba88f28e9e811f08b628e9a4674c))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **dashboard:** remove rounding in avgPerHour calculation ([#29](https://github.com/Soju06/codex-lb/issues/29)) ([b432939](https://github.com/Soju06/codex-lb/commit/b432939d6ea832d917658dfdbcb935f88f9e08a6)), closes [#26](https://github.com/Soju06/codex-lb/issues/26)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* add hhsw2015 as a contributor for code, and test ([#31](https://github.com/Soju06/codex-lb/issues/31)) ([a1f0e79](https://github.com/Soju06/codex-lb/commit/a1f0e796e45862e520953f60716d2b5eaab3a0d9))
|
|
19
|
+
* add opencode setup guide ([#32](https://github.com/Soju06/codex-lb/issues/32)) ([9330619](https://github.com/Soju06/codex-lb/commit/93306198902e558e6bce89719d7cd6b1e797ddc5))
|
|
20
|
+
* add pcy06 as a contributor for doc ([#34](https://github.com/Soju06/codex-lb/issues/34)) ([506b7b1](https://github.com/Soju06/codex-lb/commit/506b7b160b11b558533fafb39793870ceefd9131))
|
|
21
|
+
|
|
3
22
|
## [0.3.1](https://github.com/Soju06/codex-lb/compare/v0.3.0...v0.3.1) (2026-01-22)
|
|
4
23
|
|
|
5
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-lb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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>
|
|
@@ -51,10 +51,22 @@ Requires-Dist: python-multipart>=0.0.21
|
|
|
51
51
|
Requires-Dist: sqlalchemy>=2.0.45
|
|
52
52
|
Description-Content-Type: text/markdown
|
|
53
53
|
|
|
54
|
+
<!--
|
|
55
|
+
About
|
|
56
|
+
Codex/ChatGPT account load balancer & proxy with usage tracking, dashboard, and OpenCode-compatible endpoints
|
|
57
|
+
|
|
58
|
+
Topics
|
|
59
|
+
python oauth sqlalchemy dashboard load-balancer openai rate-limit api-proxy codex fastapi usage-tracking chatgpt opencode
|
|
60
|
+
|
|
61
|
+
Resources
|
|
62
|
+
-->
|
|
63
|
+
|
|
54
64
|
# codex-lb
|
|
55
65
|
|
|
56
66
|
Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
|
|
57
67
|
|
|
68
|
+
## Screenshots
|
|
69
|
+
|
|
58
70
|
### Main Dashboard View
|
|
59
71
|
|
|
60
72
|

|
|
@@ -82,8 +94,6 @@ uvx codex-lb
|
|
|
82
94
|
|
|
83
95
|
Open [localhost:2455](http://localhost:2455) → Add account → Done.
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
87
97
|
## Codex CLI & Extension Setup
|
|
88
98
|
|
|
89
99
|
Add to `~/.codex/config.toml`:
|
|
@@ -101,6 +111,32 @@ chatgpt_base_url = "http://127.0.0.1:2455"
|
|
|
101
111
|
requires_openai_auth = true # Required: enables model selection in Codex IDE extension
|
|
102
112
|
```
|
|
103
113
|
|
|
114
|
+
## OpenCode Setup
|
|
115
|
+
|
|
116
|
+
Run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
opencode auth login
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Then select `OpenAI` -> `Manually enter API Key` and enter any value.
|
|
123
|
+
|
|
124
|
+
Add the following to `~/.config/opencode/opencode.json`:
|
|
125
|
+
|
|
126
|
+
```jsonc
|
|
127
|
+
{
|
|
128
|
+
...
|
|
129
|
+
"provider": {
|
|
130
|
+
"openai": {
|
|
131
|
+
"options": {
|
|
132
|
+
"baseURL": "http://127.0.0.1:2455/v1"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
...
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
104
140
|
## Data
|
|
105
141
|
|
|
106
142
|
All data stored in `~/.codex-lb/`:
|
|
@@ -122,6 +158,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
122
158
|
<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
159
|
<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
160
|
<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>
|
|
161
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hhsw2015"><img src="https://avatars.githubusercontent.com/u/103614420?v=4?s=100" width="100px;" alt="Jill Kok, San Mou"/><br /><sub><b>Jill Kok, San Mou</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Tests">⚠️</a></td>
|
|
162
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pcy06"><img src="https://avatars.githubusercontent.com/u/44970486?v=4?s=100" width="100px;" alt="PARK CHANYOUNG"/><br /><sub><b>PARK CHANYOUNG</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=pcy06" title="Documentation">📖</a></td>
|
|
125
163
|
</tr>
|
|
126
164
|
</tbody>
|
|
127
165
|
</table>
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
About
|
|
3
|
+
Codex/ChatGPT account load balancer & proxy with usage tracking, dashboard, and OpenCode-compatible endpoints
|
|
4
|
+
|
|
5
|
+
Topics
|
|
6
|
+
python oauth sqlalchemy dashboard load-balancer openai rate-limit api-proxy codex fastapi usage-tracking chatgpt opencode
|
|
7
|
+
|
|
8
|
+
Resources
|
|
9
|
+
-->
|
|
10
|
+
|
|
1
11
|
# codex-lb
|
|
2
12
|
|
|
3
13
|
Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
|
|
4
14
|
|
|
15
|
+
## Screenshots
|
|
16
|
+
|
|
5
17
|
### Main Dashboard View
|
|
6
18
|
|
|
7
19
|

|
|
@@ -29,8 +41,6 @@ uvx codex-lb
|
|
|
29
41
|
|
|
30
42
|
Open [localhost:2455](http://localhost:2455) → Add account → Done.
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
44
|
## Codex CLI & Extension Setup
|
|
35
45
|
|
|
36
46
|
Add to `~/.codex/config.toml`:
|
|
@@ -48,6 +58,32 @@ chatgpt_base_url = "http://127.0.0.1:2455"
|
|
|
48
58
|
requires_openai_auth = true # Required: enables model selection in Codex IDE extension
|
|
49
59
|
```
|
|
50
60
|
|
|
61
|
+
## OpenCode Setup
|
|
62
|
+
|
|
63
|
+
Run:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
opencode auth login
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then select `OpenAI` -> `Manually enter API Key` and enter any value.
|
|
70
|
+
|
|
71
|
+
Add the following to `~/.config/opencode/opencode.json`:
|
|
72
|
+
|
|
73
|
+
```jsonc
|
|
74
|
+
{
|
|
75
|
+
...
|
|
76
|
+
"provider": {
|
|
77
|
+
"openai": {
|
|
78
|
+
"options": {
|
|
79
|
+
"baseURL": "http://127.0.0.1:2455/v1"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
...
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
51
87
|
## Data
|
|
52
88
|
|
|
53
89
|
All data stored in `~/.codex-lb/`:
|
|
@@ -69,6 +105,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
69
105
|
<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
106
|
<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
107
|
<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>
|
|
108
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hhsw2015"><img src="https://avatars.githubusercontent.com/u/103614420?v=4?s=100" width="100px;" alt="Jill Kok, San Mou"/><br /><sub><b>Jill Kok, San Mou</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Tests">⚠️</a></td>
|
|
109
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pcy06"><img src="https://avatars.githubusercontent.com/u/44970486?v=4?s=100" width="100px;" alt="PARK CHANYOUNG"/><br /><sub><b>PARK CHANYOUNG</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=pcy06" title="Documentation">📖</a></td>
|
|
72
110
|
</tr>
|
|
73
111
|
</tbody>
|
|
74
112
|
</table>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
from typing import AsyncIterator, Mapping
|
|
4
|
+
from typing import AsyncIterator, Mapping, Protocol, TypeAlias
|
|
5
5
|
|
|
6
6
|
import aiohttp
|
|
7
7
|
|
|
@@ -28,6 +28,18 @@ class StreamIdleTimeoutError(Exception):
|
|
|
28
28
|
pass
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
class ErrorResponseProtocol(Protocol):
|
|
32
|
+
status: int
|
|
33
|
+
reason: str | None
|
|
34
|
+
|
|
35
|
+
async def json(self, *, content_type: str | None = None) -> object: ...
|
|
36
|
+
|
|
37
|
+
async def text(self, *, encoding: str | None = None, errors: str = "strict") -> str: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ErrorResponse: TypeAlias = aiohttp.ClientResponse | ErrorResponseProtocol
|
|
41
|
+
|
|
42
|
+
|
|
31
43
|
class ProxyResponseError(Exception):
|
|
32
44
|
def __init__(self, status_code: int, payload: OpenAIErrorEnvelope) -> None:
|
|
33
45
|
super().__init__(f"Proxy response error ({status_code})")
|
|
@@ -88,8 +100,10 @@ async def _iter_sse_lines(
|
|
|
88
100
|
yield line
|
|
89
101
|
|
|
90
102
|
|
|
91
|
-
async def _error_event_from_response(resp:
|
|
103
|
+
async def _error_event_from_response(resp: ErrorResponse) -> ResponseFailedEvent:
|
|
92
104
|
fallback_message = f"Upstream error: HTTP {resp.status}"
|
|
105
|
+
if resp.reason:
|
|
106
|
+
fallback_message += f" {resp.reason}"
|
|
93
107
|
try:
|
|
94
108
|
data = await resp.json(content_type=None)
|
|
95
109
|
except Exception:
|
|
@@ -112,11 +126,16 @@ async def _error_event_from_response(resp: aiohttp.ClientResponse) -> ResponseFa
|
|
|
112
126
|
if key in payload:
|
|
113
127
|
event["response"]["error"][key] = payload[key]
|
|
114
128
|
return event
|
|
129
|
+
message = _extract_upstream_message(data)
|
|
130
|
+
if message:
|
|
131
|
+
return response_failed_event("upstream_error", message, response_id=get_request_id())
|
|
115
132
|
return response_failed_event("upstream_error", fallback_message, response_id=get_request_id())
|
|
116
133
|
|
|
117
134
|
|
|
118
|
-
async def _error_payload_from_response(resp:
|
|
135
|
+
async def _error_payload_from_response(resp: ErrorResponse) -> OpenAIErrorEnvelope:
|
|
119
136
|
fallback_message = f"Upstream error: HTTP {resp.status}"
|
|
137
|
+
if resp.reason:
|
|
138
|
+
fallback_message += f" {resp.reason}"
|
|
120
139
|
try:
|
|
121
140
|
data = await resp.json(content_type=None)
|
|
122
141
|
except Exception:
|
|
@@ -128,9 +147,20 @@ async def _error_payload_from_response(resp: aiohttp.ClientResponse) -> OpenAIEr
|
|
|
128
147
|
error = parse_error_payload(data)
|
|
129
148
|
if error:
|
|
130
149
|
return {"error": error.model_dump(exclude_none=True)}
|
|
150
|
+
message = _extract_upstream_message(data)
|
|
151
|
+
if message:
|
|
152
|
+
return openai_error("upstream_error", message)
|
|
131
153
|
return openai_error("upstream_error", fallback_message)
|
|
132
154
|
|
|
133
155
|
|
|
156
|
+
def _extract_upstream_message(data: dict) -> str | None:
|
|
157
|
+
for key in ("message", "detail", "error"):
|
|
158
|
+
value = data.get(key)
|
|
159
|
+
if isinstance(value, str) and value.strip():
|
|
160
|
+
return value
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
134
164
|
async def stream_responses(
|
|
135
165
|
payload: ResponsesRequest,
|
|
136
166
|
headers: Mapping[str, str],
|
|
@@ -42,6 +42,7 @@ class Settings(BaseSettings):
|
|
|
42
42
|
database_migrations_fail_fast: bool = True
|
|
43
43
|
log_proxy_request_shape: bool = False
|
|
44
44
|
log_proxy_request_shape_raw_cache_key: bool = False
|
|
45
|
+
log_proxy_request_payload: bool = False
|
|
45
46
|
|
|
46
47
|
@field_validator("database_url")
|
|
47
48
|
@classmethod
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
4
4
|
|
|
5
5
|
from app.core.types import JsonObject, JsonValue
|
|
6
6
|
|
|
@@ -44,8 +44,16 @@ class ResponsesRequest(BaseModel):
|
|
|
44
44
|
prompt_cache_key: str | None = None
|
|
45
45
|
text: ResponsesTextControls | None = None
|
|
46
46
|
|
|
47
|
+
@field_validator("store")
|
|
48
|
+
@classmethod
|
|
49
|
+
def _ensure_store_false(cls, value: bool | None) -> bool | None:
|
|
50
|
+
if value is True:
|
|
51
|
+
raise ValueError("store must be false")
|
|
52
|
+
return value
|
|
53
|
+
|
|
47
54
|
def to_payload(self) -> JsonObject:
|
|
48
|
-
|
|
55
|
+
payload = self.model_dump(mode="json", exclude_none=True)
|
|
56
|
+
return _strip_unsupported_fields(payload)
|
|
49
57
|
|
|
50
58
|
|
|
51
59
|
class ResponsesCompactRequest(BaseModel):
|
|
@@ -56,4 +64,14 @@ class ResponsesCompactRequest(BaseModel):
|
|
|
56
64
|
input: list[JsonValue]
|
|
57
65
|
|
|
58
66
|
def to_payload(self) -> JsonObject:
|
|
59
|
-
|
|
67
|
+
payload = self.model_dump(mode="json", exclude_none=True)
|
|
68
|
+
return _strip_unsupported_fields(payload)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_UNSUPPORTED_UPSTREAM_FIELDS = {"max_output_tokens"}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _strip_unsupported_fields(payload: dict[str, JsonValue]) -> dict[str, JsonValue]:
|
|
75
|
+
for key in _UNSUPPORTED_UPSTREAM_FIELDS:
|
|
76
|
+
payload.pop(key, None)
|
|
77
|
+
return payload
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
6
|
+
|
|
7
|
+
from app.core.openai.requests import (
|
|
8
|
+
ResponsesCompactRequest,
|
|
9
|
+
ResponsesReasoning,
|
|
10
|
+
ResponsesRequest,
|
|
11
|
+
ResponsesTextControls,
|
|
12
|
+
)
|
|
13
|
+
from app.core.types import JsonValue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class V1ResponsesRequest(BaseModel):
|
|
17
|
+
model_config = ConfigDict(extra="allow")
|
|
18
|
+
|
|
19
|
+
model: str = Field(min_length=1)
|
|
20
|
+
messages: list[JsonValue] | None = None
|
|
21
|
+
input: list[JsonValue] | None = None
|
|
22
|
+
instructions: str | None = None
|
|
23
|
+
tools: list[JsonValue] = Field(default_factory=list)
|
|
24
|
+
tool_choice: str | None = None
|
|
25
|
+
parallel_tool_calls: bool | None = None
|
|
26
|
+
reasoning: ResponsesReasoning | None = None
|
|
27
|
+
store: bool | None = None
|
|
28
|
+
stream: bool | None = None
|
|
29
|
+
include: list[str] = Field(default_factory=list)
|
|
30
|
+
prompt_cache_key: str | None = None
|
|
31
|
+
text: ResponsesTextControls | None = None
|
|
32
|
+
|
|
33
|
+
@field_validator("store")
|
|
34
|
+
@classmethod
|
|
35
|
+
def _ensure_store_false(cls, value: bool | None) -> bool | None:
|
|
36
|
+
if value is True:
|
|
37
|
+
raise ValueError("store must be false")
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
@model_validator(mode="after")
|
|
41
|
+
def _validate_input(self) -> "V1ResponsesRequest":
|
|
42
|
+
if self.messages is None and self.input is None:
|
|
43
|
+
raise ValueError("Provide either 'input' or 'messages'.")
|
|
44
|
+
if self.messages is not None and self.input not in (None, []):
|
|
45
|
+
raise ValueError("Provide either 'input' or 'messages', not both.")
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def to_responses_request(self) -> ResponsesRequest:
|
|
49
|
+
data = self.model_dump(mode="json", exclude_none=True)
|
|
50
|
+
messages = data.pop("messages", None)
|
|
51
|
+
instructions = data.get("instructions")
|
|
52
|
+
instruction_text = instructions if isinstance(instructions, str) else ""
|
|
53
|
+
input_value = data.get("input")
|
|
54
|
+
input_items: list[JsonValue] = input_value if isinstance(input_value, list) else []
|
|
55
|
+
|
|
56
|
+
if messages is not None:
|
|
57
|
+
instruction_text, input_items = _coerce_messages(instruction_text, messages)
|
|
58
|
+
|
|
59
|
+
data["instructions"] = instruction_text
|
|
60
|
+
data["input"] = input_items
|
|
61
|
+
return ResponsesRequest.model_validate(data)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class V1ResponsesCompactRequest(BaseModel):
|
|
65
|
+
model_config = ConfigDict(extra="allow")
|
|
66
|
+
|
|
67
|
+
model: str = Field(min_length=1)
|
|
68
|
+
messages: list[JsonValue] | None = None
|
|
69
|
+
input: list[JsonValue] | None = None
|
|
70
|
+
instructions: str | None = None
|
|
71
|
+
|
|
72
|
+
@model_validator(mode="after")
|
|
73
|
+
def _validate_input(self) -> "V1ResponsesCompactRequest":
|
|
74
|
+
if self.messages is None and self.input is None:
|
|
75
|
+
raise ValueError("Provide either 'input' or 'messages'.")
|
|
76
|
+
if self.messages is not None and self.input not in (None, []):
|
|
77
|
+
raise ValueError("Provide either 'input' or 'messages', not both.")
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def to_compact_request(self) -> ResponsesCompactRequest:
|
|
81
|
+
data = self.model_dump(mode="json", exclude_none=True)
|
|
82
|
+
messages = data.pop("messages", None)
|
|
83
|
+
instructions = data.get("instructions")
|
|
84
|
+
instruction_text = instructions if isinstance(instructions, str) else ""
|
|
85
|
+
input_value = data.get("input")
|
|
86
|
+
input_items: list[JsonValue] = input_value if isinstance(input_value, list) else []
|
|
87
|
+
|
|
88
|
+
if messages is not None:
|
|
89
|
+
instruction_text, input_items = _coerce_messages(instruction_text, messages)
|
|
90
|
+
|
|
91
|
+
data["instructions"] = instruction_text
|
|
92
|
+
data["input"] = input_items
|
|
93
|
+
return ResponsesCompactRequest.model_validate(data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _coerce_messages(existing_instructions: str, messages: list[JsonValue]) -> tuple[str, list[JsonValue]]:
|
|
97
|
+
instruction_parts: list[str] = []
|
|
98
|
+
input_messages: list[JsonValue] = []
|
|
99
|
+
for message in messages:
|
|
100
|
+
if not isinstance(message, dict):
|
|
101
|
+
raise ValueError("Each message must be an object.")
|
|
102
|
+
message_dict = cast(dict[str, JsonValue], message)
|
|
103
|
+
role_value = message_dict.get("role")
|
|
104
|
+
role = role_value if isinstance(role_value, str) else None
|
|
105
|
+
if role in ("system", "developer"):
|
|
106
|
+
content_text = _content_to_text(message_dict.get("content"))
|
|
107
|
+
if content_text:
|
|
108
|
+
instruction_parts.append(content_text)
|
|
109
|
+
continue
|
|
110
|
+
input_messages.append(cast(JsonValue, message_dict))
|
|
111
|
+
merged = _merge_instructions(existing_instructions, instruction_parts)
|
|
112
|
+
return merged, input_messages
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _merge_instructions(existing: str, extra_parts: list[str]) -> str:
|
|
116
|
+
if not extra_parts:
|
|
117
|
+
return existing
|
|
118
|
+
extra = "\n".join([part for part in extra_parts if part])
|
|
119
|
+
if not extra:
|
|
120
|
+
return existing
|
|
121
|
+
if existing:
|
|
122
|
+
return f"{existing}\n{extra}"
|
|
123
|
+
return extra
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _content_to_text(content: object) -> str | None:
|
|
127
|
+
if content is None:
|
|
128
|
+
return None
|
|
129
|
+
if isinstance(content, str):
|
|
130
|
+
return content
|
|
131
|
+
if isinstance(content, list):
|
|
132
|
+
parts: list[str] = []
|
|
133
|
+
for part in content:
|
|
134
|
+
if isinstance(part, str):
|
|
135
|
+
parts.append(part)
|
|
136
|
+
elif isinstance(part, dict):
|
|
137
|
+
part_dict = cast(dict[str, JsonValue], part)
|
|
138
|
+
text = part_dict.get("text")
|
|
139
|
+
if isinstance(text, str):
|
|
140
|
+
parts.append(text)
|
|
141
|
+
return "\n".join([part for part in parts if part])
|
|
142
|
+
if isinstance(content, dict):
|
|
143
|
+
content_dict = cast(dict[str, JsonValue], content)
|
|
144
|
+
text = content_dict.get("text")
|
|
145
|
+
if isinstance(text, str):
|
|
146
|
+
return text
|
|
147
|
+
return None
|
|
148
|
+
return None
|
|
@@ -48,7 +48,7 @@ class UsageHistory(Base):
|
|
|
48
48
|
__tablename__ = "usage_history"
|
|
49
49
|
|
|
50
50
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
51
|
-
account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.id"), nullable=False)
|
|
51
|
+
account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False)
|
|
52
52
|
recorded_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
|
53
53
|
window: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
54
54
|
used_percent: Mapped[float] = mapped_column(Float, nullable=False)
|
|
@@ -65,7 +65,7 @@ class RequestLog(Base):
|
|
|
65
65
|
__tablename__ = "request_logs"
|
|
66
66
|
|
|
67
67
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
68
|
-
account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.id"), nullable=False)
|
|
68
|
+
account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False)
|
|
69
69
|
request_id: Mapped[str] = mapped_column(String, nullable=False)
|
|
70
70
|
requested_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
|
71
71
|
model: Mapped[str] = mapped_column(String, nullable=False)
|
|
@@ -84,7 +84,7 @@ class StickySession(Base):
|
|
|
84
84
|
__tablename__ = "sticky_sessions"
|
|
85
85
|
|
|
86
86
|
key: Mapped[str] = mapped_column(String, primary_key=True)
|
|
87
|
-
account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.id"), nullable=False)
|
|
87
|
+
account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False)
|
|
88
88
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
|
|
89
89
|
updated_at: Mapped[datetime] = mapped_column(
|
|
90
90
|
DateTime,
|
|
@@ -102,6 +102,7 @@ def create_app() -> FastAPI:
|
|
|
102
102
|
return await http_exception_handler(request, exc)
|
|
103
103
|
|
|
104
104
|
app.include_router(proxy_api.router)
|
|
105
|
+
app.include_router(proxy_api.v1_router)
|
|
105
106
|
app.include_router(proxy_api.usage_router)
|
|
106
107
|
app.include_router(accounts_api.router)
|
|
107
108
|
app.include_router(usage_api.router)
|
|
@@ -5,7 +5,7 @@ from datetime import datetime
|
|
|
5
5
|
from sqlalchemy import delete, select, update
|
|
6
6
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
7
7
|
|
|
8
|
-
from app.db.models import Account, AccountStatus
|
|
8
|
+
from app.db.models import Account, AccountStatus, RequestLog, StickySession, UsageHistory
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class AccountsRepository:
|
|
@@ -54,6 +54,9 @@ class AccountsRepository:
|
|
|
54
54
|
return result.scalar_one_or_none() is not None
|
|
55
55
|
|
|
56
56
|
async def delete(self, account_id: str) -> bool:
|
|
57
|
+
await self._session.execute(delete(UsageHistory).where(UsageHistory.account_id == account_id))
|
|
58
|
+
await self._session.execute(delete(RequestLog).where(RequestLog.account_id == account_id))
|
|
59
|
+
await self._session.execute(delete(StickySession).where(StickySession.account_id == account_id))
|
|
57
60
|
result = await self._session.execute(delete(Account).where(Account.id == account_id).returning(Account.id))
|
|
58
61
|
await self._session.commit()
|
|
59
62
|
return result.scalar_one_or_none() is not None
|