codex-lb 0.3.0__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.
Files changed (160) hide show
  1. codex_lb-0.4.0/.all-contributorsrc +71 -0
  2. codex_lb-0.4.0/.github/release-please-manifest.json +3 -0
  3. {codex_lb-0.3.0 → codex_lb-0.4.0}/.github/workflows/ci.yml +20 -0
  4. {codex_lb-0.3.0 → codex_lb-0.4.0}/AGENTS.md +15 -0
  5. {codex_lb-0.3.0 → codex_lb-0.4.0}/CHANGELOG.md +28 -0
  6. codex_lb-0.4.0/PKG-INFO +172 -0
  7. codex_lb-0.4.0/README.md +119 -0
  8. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/clients/proxy.py +33 -3
  9. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/config/settings.py +1 -0
  10. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/openai/requests.py +21 -3
  11. codex_lb-0.4.0/app/core/openai/v1_requests.py +148 -0
  12. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/models.py +3 -3
  13. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/main.py +1 -0
  14. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/accounts/repository.py +4 -1
  15. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/proxy/api.py +36 -0
  16. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/proxy/service.py +29 -0
  17. codex_lb-0.4.0/app/modules/request_logs/api.py +85 -0
  18. codex_lb-0.4.0/app/modules/request_logs/repository.py +215 -0
  19. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/request_logs/schemas.py +11 -2
  20. codex_lb-0.4.0/app/modules/request_logs/service.py +163 -0
  21. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/usage/updater.py +58 -26
  22. codex_lb-0.4.0/app/static/index.css +1625 -0
  23. codex_lb-0.4.0/app/static/index.html +716 -0
  24. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/static/index.js +409 -42
  25. codex_lb-0.4.0/docs/screenshots/accounts.jpg +0 -0
  26. codex_lb-0.4.0/docs/screenshots/dashboard.jpg +0 -0
  27. {codex_lb-0.3.0 → codex_lb-0.4.0}/pyproject.toml +1 -1
  28. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_proxy_responses.py +100 -0
  29. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_repositories.py +2 -2
  30. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_request_logs_filters.py +127 -0
  31. codex_lb-0.4.0/tests/test_request_logs_options_api.py +111 -0
  32. codex_lb-0.4.0/tests/unit/test_openai_requests.py +105 -0
  33. codex_lb-0.4.0/tests/unit/test_proxy_errors.py +96 -0
  34. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_proxy_utils.py +27 -0
  35. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_usage_client.py +3 -1
  36. codex_lb-0.4.0/tests/unit/test_usage_updater.py +343 -0
  37. {codex_lb-0.3.0 → codex_lb-0.4.0}/uv.lock +924 -924
  38. codex_lb-0.3.0/.github/release-please-manifest.json +0 -3
  39. codex_lb-0.3.0/PKG-INFO +0 -108
  40. codex_lb-0.3.0/README.md +0 -55
  41. codex_lb-0.3.0/app/modules/request_logs/api.py +0 -31
  42. codex_lb-0.3.0/app/modules/request_logs/repository.py +0 -103
  43. codex_lb-0.3.0/app/modules/request_logs/service.py +0 -86
  44. codex_lb-0.3.0/app/static/7.css +0 -1409
  45. codex_lb-0.3.0/app/static/index.css +0 -572
  46. codex_lb-0.3.0/app/static/index.html +0 -504
  47. codex_lb-0.3.0/docs/screenshots/accounts.jpeg +0 -0
  48. codex_lb-0.3.0/docs/screenshots/dashboard.jpeg +0 -0
  49. codex_lb-0.3.0/tests/unit/test_usage_updater.py +0 -124
  50. {codex_lb-0.3.0 → codex_lb-0.4.0}/.dockerignore +0 -0
  51. {codex_lb-0.3.0 → codex_lb-0.4.0}/.env.example +0 -0
  52. {codex_lb-0.3.0 → codex_lb-0.4.0}/.github/release-please-config.json +0 -0
  53. {codex_lb-0.3.0 → codex_lb-0.4.0}/.github/workflows/release-please.yml +0 -0
  54. {codex_lb-0.3.0 → codex_lb-0.4.0}/.github/workflows/release.yml +0 -0
  55. {codex_lb-0.3.0 → codex_lb-0.4.0}/.gitignore +0 -0
  56. {codex_lb-0.3.0 → codex_lb-0.4.0}/.pre-commit-config.yaml +0 -0
  57. {codex_lb-0.3.0 → codex_lb-0.4.0}/Dockerfile +0 -0
  58. {codex_lb-0.3.0 → codex_lb-0.4.0}/LICENSE +0 -0
  59. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/__init__.py +0 -0
  60. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/cli.py +0 -0
  61. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/__init__.py +0 -0
  62. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/auth/__init__.py +0 -0
  63. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/auth/models.py +0 -0
  64. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/auth/refresh.py +0 -0
  65. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/balancer/__init__.py +0 -0
  66. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/balancer/logic.py +0 -0
  67. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/balancer/types.py +0 -0
  68. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/clients/__init__.py +0 -0
  69. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/clients/http.py +0 -0
  70. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/clients/oauth.py +0 -0
  71. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/clients/usage.py +0 -0
  72. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/config/__init__.py +0 -0
  73. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/crypto.py +0 -0
  74. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/errors.py +0 -0
  75. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/openai/__init__.py +0 -0
  76. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/openai/models.py +0 -0
  77. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/openai/parsing.py +0 -0
  78. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/plan_types.py +0 -0
  79. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/types.py +0 -0
  80. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/usage/__init__.py +0 -0
  81. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/usage/logs.py +0 -0
  82. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/usage/models.py +0 -0
  83. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/usage/pricing.py +0 -0
  84. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/usage/quota.py +0 -0
  85. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/usage/types.py +0 -0
  86. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/utils/__init__.py +0 -0
  87. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/utils/request_id.py +0 -0
  88. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/utils/retry.py +0 -0
  89. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/utils/sse.py +0 -0
  90. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/core/utils/time.py +0 -0
  91. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/__init__.py +0 -0
  92. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/migrations/__init__.py +0 -0
  93. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/migrations/versions/__init__.py +0 -0
  94. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/migrations/versions/add_accounts_chatgpt_account_id.py +0 -0
  95. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/migrations/versions/add_accounts_reset_at.py +0 -0
  96. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/migrations/versions/add_dashboard_settings.py +0 -0
  97. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/migrations/versions/add_request_logs_reasoning_effort.py +0 -0
  98. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/migrations/versions/normalize_account_plan_types.py +0 -0
  99. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/db/session.py +0 -0
  100. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/dependencies.py +0 -0
  101. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/__init__.py +0 -0
  102. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/accounts/__init__.py +0 -0
  103. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/accounts/api.py +0 -0
  104. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/accounts/auth_manager.py +0 -0
  105. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/accounts/schemas.py +0 -0
  106. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/accounts/service.py +0 -0
  107. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/health/__init__.py +0 -0
  108. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/health/api.py +0 -0
  109. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/health/schemas.py +0 -0
  110. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/oauth/__init__.py +0 -0
  111. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/oauth/api.py +0 -0
  112. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/oauth/schemas.py +0 -0
  113. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/oauth/service.py +0 -0
  114. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/oauth/templates/oauth_success.html +0 -0
  115. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/proxy/__init__.py +0 -0
  116. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/proxy/helpers.py +0 -0
  117. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/proxy/load_balancer.py +0 -0
  118. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/proxy/schemas.py +0 -0
  119. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/proxy/sticky_repository.py +0 -0
  120. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/proxy/types.py +0 -0
  121. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/request_logs/__init__.py +0 -0
  122. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/settings/__init__.py +0 -0
  123. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/settings/api.py +0 -0
  124. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/settings/repository.py +0 -0
  125. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/settings/schemas.py +0 -0
  126. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/settings/service.py +0 -0
  127. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/shared/__init__.py +0 -0
  128. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/shared/schemas.py +0 -0
  129. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/usage/__init__.py +0 -0
  130. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/usage/api.py +0 -0
  131. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/usage/repository.py +0 -0
  132. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/usage/schemas.py +0 -0
  133. {codex_lb-0.3.0 → codex_lb-0.4.0}/app/modules/usage/service.py +0 -0
  134. {codex_lb-0.3.0 → codex_lb-0.4.0}/docker-compose.yml +0 -0
  135. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/__init__.py +0 -0
  136. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/conftest.py +0 -0
  137. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_accounts_api.py +0 -0
  138. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_accounts_api_extended.py +0 -0
  139. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_codex_usage_api.py +0 -0
  140. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_db_models.py +0 -0
  141. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_health_and_errors.py +0 -0
  142. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_load_balancer_integration.py +0 -0
  143. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_migrations.py +0 -0
  144. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_oauth_flow.py +0 -0
  145. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_proxy_api_extended.py +0 -0
  146. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_proxy_compact.py +0 -0
  147. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_proxy_sticky_sessions.py +0 -0
  148. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_request_logs_api.py +0 -0
  149. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_settings_api.py +0 -0
  150. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_usage_api.py +0 -0
  151. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/integration/test_usage_summary.py +0 -0
  152. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_auth.py +0 -0
  153. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_auth_manager.py +0 -0
  154. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_auth_refresh.py +0 -0
  155. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_load_balancer.py +0 -0
  156. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_oauth_client.py +0 -0
  157. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_pricing.py +0 -0
  158. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_retry.py +0 -0
  159. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_sse.py +0 -0
  160. {codex_lb-0.3.0 → codex_lb-0.4.0}/tests/unit/test_usage.py +0 -0
@@ -0,0 +1,71 @@
1
+ {
2
+ "projectName": "codex-lb",
3
+ "projectOwner": "Soju06",
4
+ "repoType": "github",
5
+ "repoHost": "https://github.com",
6
+ "files": [
7
+ "README.md"
8
+ ],
9
+ "imageSize": 100,
10
+ "commit": true,
11
+ "commitConvention": "angular",
12
+ "contributors": [
13
+ {
14
+ "login": "Soju06",
15
+ "name": "Soju06",
16
+ "avatar_url": "https://avatars.githubusercontent.com/u/34199905?v=4",
17
+ "profile": "https://github.com/Soju06",
18
+ "contributions": [
19
+ "code",
20
+ "test",
21
+ "maintenance",
22
+ "infra"
23
+ ]
24
+ },
25
+ {
26
+ "login": "JKamsker",
27
+ "name": "Jonas Kamsker",
28
+ "avatar_url": "https://avatars.githubusercontent.com/u/11245306?v=4",
29
+ "profile": "http://jonas.kamsker.at/",
30
+ "contributions": [
31
+ "code",
32
+ "bug",
33
+ "maintenance"
34
+ ]
35
+ },
36
+ {
37
+ "login": "Quack6765",
38
+ "name": "Quack",
39
+ "avatar_url": "https://avatars.githubusercontent.com/u/5446230?v=4",
40
+ "profile": "https://github.com/Quack6765",
41
+ "contributions": [
42
+ "code",
43
+ "bug",
44
+ "maintenance",
45
+ "design"
46
+ ]
47
+ },
48
+ {
49
+ "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
+ ]
66
+ }
67
+ ],
68
+ "contributorsPerLine": 7,
69
+ "linkToUsage": false,
70
+ "commitType": "docs"
71
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -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,33 @@
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
+
22
+ ## [0.3.1](https://github.com/Soju06/codex-lb/compare/v0.3.0...v0.3.1) (2026-01-22)
23
+
24
+
25
+ ### Documentation
26
+
27
+ * add Quack6765 as a contributor for design ([7a5ec08](https://github.com/Soju06/codex-lb/commit/7a5ec084b9a8d32c844127739f826a5f83bf1440))
28
+ * update .all-contributorsrc ([14ea9da](https://github.com/Soju06/codex-lb/commit/14ea9da361a978a56c4d1f7facefe789193c7b91))
29
+ * update README.md ([f283d60](https://github.com/Soju06/codex-lb/commit/f283d60ae359585cd128a965ca6fba2a14249a11))
30
+
3
31
  ## [0.3.0](https://github.com/Soju06/codex-lb/compare/v0.2.0...v0.3.0) (2026-01-21)
4
32
 
5
33
 
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-lb
3
+ Version: 0.4.0
4
+ Summary: Codex load balancer and proxy for ChatGPT accounts with usage dashboard
5
+ Author-email: Soju06 <qlskssk@gmail.com>
6
+ Maintainer-email: Soju06 <qlskssk@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 Soju06
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Keywords: chatgpt,codex,dashboard,fastapi,load-balancer,proxy,rate-limit,usage
30
+ Classifier: Development Status :: 3 - Alpha
31
+ Classifier: Environment :: Console
32
+ Classifier: Framework :: FastAPI
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.13
37
+ Classifier: Topic :: Internet :: Proxy Servers
38
+ Classifier: Topic :: Software Development :: Libraries
39
+ Classifier: Topic :: System :: Networking
40
+ Requires-Python: >=3.13
41
+ Requires-Dist: aiohttp-retry>=2.9.1
42
+ Requires-Dist: aiohttp>=3.13.3
43
+ Requires-Dist: aiosqlite>=0.22.1
44
+ Requires-Dist: cryptography>=46.0.3
45
+ Requires-Dist: fastapi[standard]>=0.128.0
46
+ Requires-Dist: greenlet>=3.3.0
47
+ Requires-Dist: pydantic-settings>=2.12.0
48
+ Requires-Dist: pydantic>=2.12.5
49
+ Requires-Dist: python-dotenv>=1.2.1
50
+ Requires-Dist: python-multipart>=0.0.21
51
+ Requires-Dist: sqlalchemy>=2.0.45
52
+ Description-Content-Type: text/markdown
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
+
64
+ # codex-lb
65
+
66
+ Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
67
+
68
+ ## Screenshots
69
+
70
+ ### Main Dashboard View
71
+
72
+ ![main dashboard view](docs/screenshots/dashboard.jpg)
73
+
74
+ ### Accounts View
75
+
76
+ ![Accounts list and details](docs/screenshots/accounts.jpg)
77
+
78
+ ## Quick Start
79
+
80
+ ### Docker
81
+
82
+ ```bash
83
+ docker run -d --name codex-lb \
84
+ -p 2455:2455 -p 1455:1455 \
85
+ -v ~/.codex-lb:/var/lib/codex-lb \
86
+ ghcr.io/soju06/codex-lb:latest
87
+ ```
88
+
89
+ ### uvx
90
+
91
+ ```bash
92
+ uvx codex-lb
93
+ ```
94
+
95
+ Open [localhost:2455](http://localhost:2455) → Add account → Done.
96
+
97
+ ## Codex CLI & Extension Setup
98
+
99
+ Add to `~/.codex/config.toml`:
100
+
101
+ ```toml
102
+ model = "gpt-5.2-codex"
103
+ model_reasoning_effort = "xhigh"
104
+ model_provider = "codex-lb"
105
+
106
+ [model_providers.codex-lb]
107
+ name = "OpenAI" # MUST be "OpenAI" - enables /compact endpoint
108
+ base_url = "http://127.0.0.1:2455/backend-api/codex"
109
+ wire_api = "responses"
110
+ chatgpt_base_url = "http://127.0.0.1:2455"
111
+ requires_openai_auth = true # Required: enables model selection in Codex IDE extension
112
+ ```
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
+
140
+ ## Data
141
+
142
+ All data stored in `~/.codex-lb/`:
143
+
144
+ - `store.db` – accounts, usage logs
145
+ - `encryption.key` – encrypts tokens (auto-generated)
146
+
147
+ Backup this directory to preserve your accounts.
148
+
149
+ ## Contributors ✨
150
+
151
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
152
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
153
+ <!-- prettier-ignore-start -->
154
+ <!-- markdownlint-disable -->
155
+ <table>
156
+ <tbody>
157
+ <tr>
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>
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>
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>
163
+ </tr>
164
+ </tbody>
165
+ </table>
166
+
167
+ <!-- markdownlint-restore -->
168
+ <!-- prettier-ignore-end -->
169
+
170
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
171
+
172
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
@@ -0,0 +1,119 @@
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
+
11
+ # codex-lb
12
+
13
+ Load balancer for ChatGPT accounts. Pool multiple accounts, track usage, view everything in a dashboard.
14
+
15
+ ## Screenshots
16
+
17
+ ### Main Dashboard View
18
+
19
+ ![main dashboard view](docs/screenshots/dashboard.jpg)
20
+
21
+ ### Accounts View
22
+
23
+ ![Accounts list and details](docs/screenshots/accounts.jpg)
24
+
25
+ ## Quick Start
26
+
27
+ ### Docker
28
+
29
+ ```bash
30
+ docker run -d --name codex-lb \
31
+ -p 2455:2455 -p 1455:1455 \
32
+ -v ~/.codex-lb:/var/lib/codex-lb \
33
+ ghcr.io/soju06/codex-lb:latest
34
+ ```
35
+
36
+ ### uvx
37
+
38
+ ```bash
39
+ uvx codex-lb
40
+ ```
41
+
42
+ Open [localhost:2455](http://localhost:2455) → Add account → Done.
43
+
44
+ ## Codex CLI & Extension Setup
45
+
46
+ Add to `~/.codex/config.toml`:
47
+
48
+ ```toml
49
+ model = "gpt-5.2-codex"
50
+ model_reasoning_effort = "xhigh"
51
+ model_provider = "codex-lb"
52
+
53
+ [model_providers.codex-lb]
54
+ name = "OpenAI" # MUST be "OpenAI" - enables /compact endpoint
55
+ base_url = "http://127.0.0.1:2455/backend-api/codex"
56
+ wire_api = "responses"
57
+ chatgpt_base_url = "http://127.0.0.1:2455"
58
+ requires_openai_auth = true # Required: enables model selection in Codex IDE extension
59
+ ```
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
+
87
+ ## Data
88
+
89
+ All data stored in `~/.codex-lb/`:
90
+
91
+ - `store.db` – accounts, usage logs
92
+ - `encryption.key` – encrypts tokens (auto-generated)
93
+
94
+ Backup this directory to preserve your accounts.
95
+
96
+ ## Contributors ✨
97
+
98
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
99
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
100
+ <!-- prettier-ignore-start -->
101
+ <!-- markdownlint-disable -->
102
+ <table>
103
+ <tbody>
104
+ <tr>
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>
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>
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>
110
+ </tr>
111
+ </tbody>
112
+ </table>
113
+
114
+ <!-- markdownlint-restore -->
115
+ <!-- prettier-ignore-end -->
116
+
117
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
118
+
119
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
@@ -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: aiohttp.ClientResponse) -> ResponseFailedEvent:
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: aiohttp.ClientResponse) -> OpenAIErrorEnvelope:
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
- return self.model_dump(mode="json", exclude_none=True)
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
- return self.model_dump(mode="json", exclude_none=True)
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