codex-workspaces 0.3.2__tar.gz → 0.3.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_workspaces-0.3.2 → codex_workspaces-0.3.4}/CHANGELOG.md +27 -2
- {codex_workspaces-0.3.2/src/codex_workspaces.egg-info → codex_workspaces-0.3.4}/PKG-INFO +10 -2
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/README.MD +9 -1
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/README.zh-CN.md +9 -1
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/docs/RELEASE.zh-CN.md +2 -2
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/docs/TESTING.zh-CN.md +1 -1
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/pyproject.toml +1 -1
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/__init__.py +1 -1
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/cli.py +14 -7
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/core.py +179 -20
- codex_workspaces-0.3.4/src/codex_workspaces/private_api/auth.py +48 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/private_api/client.py +86 -1
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/private_api/errors.py +12 -4
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/private_api/models.py +1 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/store.py +2 -2
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4/src/codex_workspaces.egg-info}/PKG-INFO +10 -2
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/tests/test_cli.py +171 -6
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/tests/test_core.py +203 -11
- codex_workspaces-0.3.2/src/codex_workspaces/private_api/auth.py +0 -45
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/MANIFEST.in +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/docs/DESIGN.zh-CN.md +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/setup.cfg +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/__main__.py +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/auth_inspector.py +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/config.py +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/errors.py +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/platforms.py +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/private_api/__init__.py +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces/stats.py +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces.egg-info/SOURCES.txt +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces.egg-info/dependency_links.txt +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces.egg-info/entry_points.txt +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces.egg-info/requires.txt +0 -0
- {codex_workspaces-0.3.2 → codex_workspaces-0.3.4}/src/codex_workspaces.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,32 @@ All notable changes to `codex-workspaces` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows a simple changelog format while it is still pre-release.
|
|
6
6
|
|
|
7
|
-
## 0.3.
|
|
7
|
+
## 0.3.4 - 2026-07-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `accounts current --id` and `accounts current --json` for shell-friendly account composition.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Default experimental private API quota configuration now points at the ChatGPT WHAM usage endpoint while remaining disabled by default.
|
|
16
|
+
- Updated WHAM quota parsing for `rate_limit.primary_window`, reset times, window duration, allowed/limit status, and credits metadata.
|
|
17
|
+
- Tightened quota auth extraction to use ChatGPT `auth.json` fields (`tokens.access_token`, `tokens.refresh_token`, and `tokens.account_id`) without recursively guessing token fields.
|
|
18
|
+
- Updated README and testing docs to document the current `base_url`, `quota_endpoint`, and `account_endpoint` configuration fields.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Fixed `quota`, `quota --json`, `accounts quota`, and `accounts list -a` so disabled or failing private API quota calls return friendly errors instead of Python tracebacks.
|
|
23
|
+
- Fixed `accounts list -a` so one account's realtime quota failure does not interrupt the whole account list.
|
|
24
|
+
- Fixed `accounts current` command composition by documenting `accounts info "$(codex-workspaces accounts current --id)"`.
|
|
25
|
+
- Fixed WHAM quota requests to include `OpenAI-Account-Id`.
|
|
26
|
+
- Fixed unauthorized and expired-access-token quota failures to return actionable login/refresh hints without leaking credentials.
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
|
|
30
|
+
- Avoids using `id_token` or recursively discovered token-like fields as private API bearer tokens.
|
|
31
|
+
|
|
32
|
+
## 0.3.3 - 2026-07-04
|
|
8
33
|
|
|
9
34
|
### Added
|
|
10
35
|
|
|
@@ -23,7 +48,7 @@ This project follows a simple changelog format while it is still pre-release.
|
|
|
23
48
|
|
|
24
49
|
- Improved account list/info metadata enrichment while keeping auth parsing optional and non-blocking.
|
|
25
50
|
- Improved local stats presentation while keeping all statistics local and read-only.
|
|
26
|
-
- Updated README, design, testing, release, and changelog docs for the 0.3.
|
|
51
|
+
- Updated README, design, testing, release, and changelog docs for the 0.3.3 workflow.
|
|
27
52
|
|
|
28
53
|
### Security
|
|
29
54
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-workspaces
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Cross-platform Codex workspace switcher with a preserved macOS shell workflow.
|
|
5
5
|
Author: blockchain-project-lives
|
|
6
6
|
Project-URL: Homepage, https://github.com/blockchain-project-lives/codex-workspaces
|
|
@@ -223,9 +223,14 @@ Enable explicitly:
|
|
|
223
223
|
```bash
|
|
224
224
|
codex-workspaces config set experimental_private_api.enabled true
|
|
225
225
|
codex-workspaces config set experimental_private_api.quota_enabled true
|
|
226
|
-
codex-workspaces config set experimental_private_api.refresh_enabled true
|
|
227
226
|
```
|
|
228
227
|
|
|
228
|
+
The current endpoint fields are:
|
|
229
|
+
|
|
230
|
+
- `experimental_private_api.base_url` defaults to `https://chatgpt.com`
|
|
231
|
+
- `experimental_private_api.quota_endpoint` defaults to `/backend-api/wham/usage`
|
|
232
|
+
- `experimental_private_api.account_endpoint` defaults to an empty string
|
|
233
|
+
|
|
229
234
|
Query current account quota:
|
|
230
235
|
|
|
231
236
|
```bash
|
|
@@ -254,6 +259,8 @@ codex-workspaces accounts refresh acct_work
|
|
|
254
259
|
codex-workspaces accounts refresh --all --json
|
|
255
260
|
```
|
|
256
261
|
|
|
262
|
+
Refresh remains disabled by default. The current `account_endpoint` is not the Codex responses API; do not point it at the responses endpoint unless the provider logic is updated to support that POST-based flow.
|
|
263
|
+
|
|
257
264
|
Realtime quota uses explicit experimental configuration, timeout, serial account iteration, and a local TTL cache under `~/.codex-workspaces/cache/quota/`. The cache stores quota summaries and auth hashes only; it does not store tokens, cookies, authorization headers, or raw `auth.json`.
|
|
258
265
|
|
|
259
266
|
`stats` and `quota` are different: `stats` is local historical usage from SQLite, while `quota` is a realtime remote lookup. Quota failures do not affect local workspace/account switching.
|
|
@@ -274,6 +281,7 @@ Manage account metadata and snapshots:
|
|
|
274
281
|
```bash
|
|
275
282
|
codex-workspaces accounts note acct_research "lab account"
|
|
276
283
|
codex-workspaces accounts info acct_research
|
|
284
|
+
codex-workspaces accounts info "$(codex-workspaces accounts current --id)"
|
|
277
285
|
codex-workspaces accounts rename acct_research acct_lab
|
|
278
286
|
codex-workspaces accounts delete acct_lab --force
|
|
279
287
|
```
|
|
@@ -195,9 +195,14 @@ Enable explicitly:
|
|
|
195
195
|
```bash
|
|
196
196
|
codex-workspaces config set experimental_private_api.enabled true
|
|
197
197
|
codex-workspaces config set experimental_private_api.quota_enabled true
|
|
198
|
-
codex-workspaces config set experimental_private_api.refresh_enabled true
|
|
199
198
|
```
|
|
200
199
|
|
|
200
|
+
The current endpoint fields are:
|
|
201
|
+
|
|
202
|
+
- `experimental_private_api.base_url` defaults to `https://chatgpt.com`
|
|
203
|
+
- `experimental_private_api.quota_endpoint` defaults to `/backend-api/wham/usage`
|
|
204
|
+
- `experimental_private_api.account_endpoint` defaults to an empty string
|
|
205
|
+
|
|
201
206
|
Query current account quota:
|
|
202
207
|
|
|
203
208
|
```bash
|
|
@@ -226,6 +231,8 @@ codex-workspaces accounts refresh acct_work
|
|
|
226
231
|
codex-workspaces accounts refresh --all --json
|
|
227
232
|
```
|
|
228
233
|
|
|
234
|
+
Refresh remains disabled by default. The current `account_endpoint` is not the Codex responses API; do not point it at the responses endpoint unless the provider logic is updated to support that POST-based flow.
|
|
235
|
+
|
|
229
236
|
Realtime quota uses explicit experimental configuration, timeout, serial account iteration, and a local TTL cache under `~/.codex-workspaces/cache/quota/`. The cache stores quota summaries and auth hashes only; it does not store tokens, cookies, authorization headers, or raw `auth.json`.
|
|
230
237
|
|
|
231
238
|
`stats` and `quota` are different: `stats` is local historical usage from SQLite, while `quota` is a realtime remote lookup. Quota failures do not affect local workspace/account switching.
|
|
@@ -246,6 +253,7 @@ Manage account metadata and snapshots:
|
|
|
246
253
|
```bash
|
|
247
254
|
codex-workspaces accounts note acct_research "lab account"
|
|
248
255
|
codex-workspaces accounts info acct_research
|
|
256
|
+
codex-workspaces accounts info "$(codex-workspaces accounts current --id)"
|
|
249
257
|
codex-workspaces accounts rename acct_research acct_lab
|
|
250
258
|
codex-workspaces accounts delete acct_lab --force
|
|
251
259
|
```
|
|
@@ -195,9 +195,14 @@ codex-workspaces stats work --days 14
|
|
|
195
195
|
```bash
|
|
196
196
|
codex-workspaces config set experimental_private_api.enabled true
|
|
197
197
|
codex-workspaces config set experimental_private_api.quota_enabled true
|
|
198
|
-
codex-workspaces config set experimental_private_api.refresh_enabled true
|
|
199
198
|
```
|
|
200
199
|
|
|
200
|
+
当前 endpoint 配置字段是:
|
|
201
|
+
|
|
202
|
+
- `experimental_private_api.base_url` 默认 `https://chatgpt.com`
|
|
203
|
+
- `experimental_private_api.quota_endpoint` 默认 `/backend-api/wham/usage`
|
|
204
|
+
- `experimental_private_api.account_endpoint` 默认空字符串
|
|
205
|
+
|
|
201
206
|
查询当前账号额度:
|
|
202
207
|
|
|
203
208
|
```bash
|
|
@@ -226,6 +231,8 @@ codex-workspaces accounts refresh acct_work
|
|
|
226
231
|
codex-workspaces accounts refresh --all --json
|
|
227
232
|
```
|
|
228
233
|
|
|
234
|
+
refresh 当前仍默认关闭。当前 `account_endpoint` 不是 Codex responses API;除非 provider 实现了 POST responses 流程,否则不要把它指向 responses endpoint。
|
|
235
|
+
|
|
229
236
|
实时额度能力使用显式实验配置、请求超时、串行账号遍历和本地 TTL 缓存,缓存目录为 `~/.codex-workspaces/cache/quota/`。缓存只保存额度摘要和 auth hash,不保存 token、cookie、authorization header 或原始 `auth.json`。
|
|
230
237
|
|
|
231
238
|
`stats` 和 `quota` 不同:`stats` 是本地 SQLite 历史统计,`quota` 是实时远端查询。额度查询失败不会影响本地 workspace/account 切换。
|
|
@@ -246,6 +253,7 @@ codex-workspaces accounts restore-default
|
|
|
246
253
|
```bash
|
|
247
254
|
codex-workspaces accounts note acct_research "实验室账号"
|
|
248
255
|
codex-workspaces accounts info acct_research
|
|
256
|
+
codex-workspaces accounts info "$(codex-workspaces accounts current --id)"
|
|
249
257
|
codex-workspaces accounts rename acct_research acct_lab
|
|
250
258
|
codex-workspaces accounts delete acct_lab --force
|
|
251
259
|
```
|
|
@@ -88,9 +88,9 @@ permissions:
|
|
|
88
88
|
1. 更新版本号和 `CHANGELOG.md`。
|
|
89
89
|
2. 本地执行测试和构建检查。
|
|
90
90
|
3. 合并到 `main`。
|
|
91
|
-
4. 创建 Git tag,例如 `v0.3.
|
|
91
|
+
4. 创建 Git tag,例如 `v0.3.4`,触发 `Publish to TestPyPI`。
|
|
92
92
|
5. 确认 TestPyPI 上传和安装正常。
|
|
93
|
-
6. 从同一个提交创建并推送正式发布分支,例如 `release/v0.3.
|
|
93
|
+
6. 从同一个提交创建并推送正式发布分支,例如 `release/v0.3.4`,触发 `Publish to PyPI`。
|
|
94
94
|
7. 如果 `pypi` Environment 配置了 Required reviewers,在 GitHub Actions 里批准部署。
|
|
95
95
|
8. 在 PyPI 页面确认 wheel、sdist 和 README 渲染正常。
|
|
96
96
|
|
|
@@ -130,7 +130,7 @@ CODEX_WORKSPACES_LINK="$tmp_home/.codex" \
|
|
|
130
130
|
CODEX_WORKSPACES_ROOT="$tmp_home/.codex-workspaces" \
|
|
131
131
|
codex-workspaces config set experimental_private_api.quota_enabled true
|
|
132
132
|
|
|
133
|
-
#
|
|
133
|
+
# 默认内置 chatgpt.com WHAM quota endpoint;开启后会真实请求实验性 private API。
|
|
134
134
|
# CODEX_WORKSPACES_LINK="$tmp_home/.codex" \
|
|
135
135
|
# CODEX_WORKSPACES_ROOT="$tmp_home/.codex-workspaces" \
|
|
136
136
|
# codex-workspaces quota --json
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codex-workspaces"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "Cross-platform Codex workspace switcher with a preserved macOS shell workflow."
|
|
9
9
|
readme = "README.MD"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -50,8 +50,7 @@ def run(argv: Sequence[str], manager: WorkspaceManager) -> int:
|
|
|
50
50
|
no_cache = True
|
|
51
51
|
else:
|
|
52
52
|
manager.fail(f"未知参数: {arg}", f"Unknown option: {arg}")
|
|
53
|
-
manager.show_quota(json_output=json_output, no_cache=no_cache)
|
|
54
|
-
return 0
|
|
53
|
+
return manager.show_quota(json_output=json_output, no_cache=no_cache)
|
|
55
54
|
if command == "accounts":
|
|
56
55
|
return run_accounts(args, manager)
|
|
57
56
|
if command == "stats":
|
|
@@ -262,9 +261,18 @@ def run_accounts(args: Sequence[str], manager: WorkspaceManager) -> int:
|
|
|
262
261
|
manager.accounts_list(all_with_quota=all_with_quota, no_cache=no_cache, json_output=json_output, verbose=verbose)
|
|
263
262
|
return 0
|
|
264
263
|
if command in {"current", "whoami"}:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
id_only = False
|
|
265
|
+
json_output = False
|
|
266
|
+
for arg in rest:
|
|
267
|
+
if arg == "--id":
|
|
268
|
+
id_only = True
|
|
269
|
+
elif arg == "--json":
|
|
270
|
+
json_output = True
|
|
271
|
+
else:
|
|
272
|
+
manager.fail(f"未知参数: {arg}", f"Unknown option: {arg}")
|
|
273
|
+
if id_only and json_output:
|
|
274
|
+
manager.fail("--id 和 --json 不能同时使用", "--id and --json cannot be used together")
|
|
275
|
+
manager.accounts_current(id_only=id_only, json_output=json_output)
|
|
268
276
|
return 0
|
|
269
277
|
if command == "info":
|
|
270
278
|
if len(rest) != 1:
|
|
@@ -319,8 +327,7 @@ def run_accounts(args: Sequence[str], manager: WorkspaceManager) -> int:
|
|
|
319
327
|
account = arg
|
|
320
328
|
else:
|
|
321
329
|
manager.fail(f"未知参数: {arg}", f"Unknown option: {arg}")
|
|
322
|
-
manager.accounts_quota(account or "", json_output=json_output, no_cache=no_cache)
|
|
323
|
-
return 0
|
|
330
|
+
return manager.accounts_quota(account or "", json_output=json_output, no_cache=no_cache)
|
|
324
331
|
if command == "export":
|
|
325
332
|
if not rest:
|
|
326
333
|
manager.fail(
|
|
@@ -22,7 +22,16 @@ from .errors import CodexWorkspacesError
|
|
|
22
22
|
from .platforms import SystemPlatform
|
|
23
23
|
from .private_api import AccountRemoteInfo, AuthMaterial, ConfiguredHttpPrivateApiProvider, PrivateApiProvider, QuotaInfo
|
|
24
24
|
from .private_api.auth import extract_auth_material
|
|
25
|
-
from .private_api.errors import
|
|
25
|
+
from .private_api.errors import (
|
|
26
|
+
PrivateApiAuthError,
|
|
27
|
+
PrivateApiDisabledError,
|
|
28
|
+
PrivateApiError,
|
|
29
|
+
PrivateApiForbiddenError,
|
|
30
|
+
PrivateApiNetworkError,
|
|
31
|
+
PrivateApiRateLimitedError,
|
|
32
|
+
PrivateApiUnsupportedResponseError,
|
|
33
|
+
redact_sensitive_text,
|
|
34
|
+
)
|
|
26
35
|
from .store import AccountMeta, WorkspaceMeta, WorkspaceStore, auth_hash, chmod_best_effort, copy_auth, iso_now, read_json, write_json_atomic
|
|
27
36
|
from .stats import ModelUsage, StatsBundle, StatsError, WorkspaceStats, combine_workspace_stats, compute_workspace_stats
|
|
28
37
|
|
|
@@ -419,6 +428,11 @@ class WorkspaceManager:
|
|
|
419
428
|
defaults = self.default_private_api_config()
|
|
420
429
|
for key, value in defaults.items():
|
|
421
430
|
private.setdefault(key, value)
|
|
431
|
+
if private.get("provider") == "codex":
|
|
432
|
+
if not private.get("base_url"):
|
|
433
|
+
private["base_url"] = defaults["base_url"]
|
|
434
|
+
if not private.get("quota_endpoint"):
|
|
435
|
+
private["quota_endpoint"] = defaults["quota_endpoint"]
|
|
422
436
|
return data
|
|
423
437
|
|
|
424
438
|
def write_tool_config(self, data: dict) -> None:
|
|
@@ -430,8 +444,8 @@ class WorkspaceManager:
|
|
|
430
444
|
"quota_enabled": False,
|
|
431
445
|
"refresh_enabled": False,
|
|
432
446
|
"provider": "codex",
|
|
433
|
-
"base_url": "",
|
|
434
|
-
"quota_endpoint": "",
|
|
447
|
+
"base_url": "https://chatgpt.com",
|
|
448
|
+
"quota_endpoint": "/backend-api/wham/usage",
|
|
435
449
|
"account_endpoint": "",
|
|
436
450
|
"timeout_seconds": 10,
|
|
437
451
|
"rate_limit_per_minute": 20,
|
|
@@ -528,6 +542,116 @@ class WorkspaceManager:
|
|
|
528
542
|
user_agent=f"codex-workspaces/{self.tool_version()} experimental-private-api",
|
|
529
543
|
)
|
|
530
544
|
|
|
545
|
+
def ensure_quota_endpoint_configured(self, settings: dict) -> None:
|
|
546
|
+
if self.private_api_provider is None and not settings.get("quota_endpoint"):
|
|
547
|
+
raise PrivateApiUnsupportedResponseError("quota endpoint is not configured")
|
|
548
|
+
|
|
549
|
+
def private_api_error_payload(self, exc: Exception) -> dict[str, object]:
|
|
550
|
+
if isinstance(exc, PrivateApiDisabledError):
|
|
551
|
+
error_type = "disabled"
|
|
552
|
+
elif isinstance(exc, PrivateApiAuthError):
|
|
553
|
+
error_type = "auth_error"
|
|
554
|
+
elif isinstance(exc, PrivateApiRateLimitedError):
|
|
555
|
+
error_type = "rate_limited"
|
|
556
|
+
elif isinstance(exc, PrivateApiForbiddenError):
|
|
557
|
+
error_type = "forbidden"
|
|
558
|
+
elif isinstance(exc, PrivateApiUnsupportedResponseError):
|
|
559
|
+
error_type = "unsupported_response"
|
|
560
|
+
elif isinstance(exc, PrivateApiNetworkError):
|
|
561
|
+
error_type = "network_error"
|
|
562
|
+
elif isinstance(exc, CodexWorkspacesError) and (
|
|
563
|
+
"experimental private API features are disabled" in str(exc) or "private API 功能未开启" in str(exc)
|
|
564
|
+
):
|
|
565
|
+
error_type = "disabled"
|
|
566
|
+
else:
|
|
567
|
+
error_type = "internal_error"
|
|
568
|
+
return {"type": error_type, "message": redact_sensitive_text(str(exc))}
|
|
569
|
+
|
|
570
|
+
def quota_list_status_from_error(self, exc: Exception) -> str:
|
|
571
|
+
payload = self.private_api_error_payload(exc)
|
|
572
|
+
error_type = str(payload["type"])
|
|
573
|
+
message = str(payload["message"])
|
|
574
|
+
if error_type == "unsupported_response" and "quota endpoint is not configured" in message:
|
|
575
|
+
return "not-configured"
|
|
576
|
+
return {
|
|
577
|
+
"auth_error": "auth-error",
|
|
578
|
+
"rate_limited": "rate-limited",
|
|
579
|
+
"network_error": "network-error",
|
|
580
|
+
"unsupported_response": "error",
|
|
581
|
+
"internal_error": "error",
|
|
582
|
+
}.get(error_type, error_type.replace("_", "-"))
|
|
583
|
+
|
|
584
|
+
def format_private_api_error(self, exc: Exception) -> str:
|
|
585
|
+
payload = self.private_api_error_payload(exc)
|
|
586
|
+
error_type = str(payload["type"])
|
|
587
|
+
message = str(payload["message"])
|
|
588
|
+
if error_type == "disabled":
|
|
589
|
+
return "\n".join(
|
|
590
|
+
[
|
|
591
|
+
"ERROR: experimental private API features are disabled.",
|
|
592
|
+
"",
|
|
593
|
+
"Enable explicitly:",
|
|
594
|
+
" codex-workspaces config set experimental_private_api.enabled true",
|
|
595
|
+
" codex-workspaces config set experimental_private_api.quota_enabled true",
|
|
596
|
+
]
|
|
597
|
+
)
|
|
598
|
+
if error_type == "unsupported_response" and "quota endpoint is not configured" in message:
|
|
599
|
+
return "\n".join(
|
|
600
|
+
[
|
|
601
|
+
"ERROR: realtime quota is not configured.",
|
|
602
|
+
"",
|
|
603
|
+
message,
|
|
604
|
+
"",
|
|
605
|
+
"Hint:",
|
|
606
|
+
" Configure experimental_private_api.quota_endpoint, or disable quota:",
|
|
607
|
+
" codex-workspaces config set experimental_private_api.quota_enabled false",
|
|
608
|
+
]
|
|
609
|
+
)
|
|
610
|
+
titles = {
|
|
611
|
+
"auth_error": "ERROR: realtime quota authentication failed.",
|
|
612
|
+
"forbidden": "ERROR: realtime quota access was forbidden.",
|
|
613
|
+
"rate_limited": "ERROR: realtime quota is rate limited.",
|
|
614
|
+
"network_error": "ERROR: realtime quota request failed.",
|
|
615
|
+
"unsupported_response": "ERROR: realtime quota response is unsupported.",
|
|
616
|
+
}
|
|
617
|
+
return "\n".join(
|
|
618
|
+
[
|
|
619
|
+
titles.get(error_type, "ERROR: realtime quota failed."),
|
|
620
|
+
"",
|
|
621
|
+
message,
|
|
622
|
+
"",
|
|
623
|
+
"Hint:",
|
|
624
|
+
" Check experimental_private_api settings, your account auth, or try again later.",
|
|
625
|
+
]
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
def render_private_api_error(
|
|
629
|
+
self,
|
|
630
|
+
exc: Exception,
|
|
631
|
+
*,
|
|
632
|
+
json_output: bool,
|
|
633
|
+
account_id: Optional[str],
|
|
634
|
+
workspace: Optional[str],
|
|
635
|
+
) -> int:
|
|
636
|
+
payload = self.private_api_error_payload(exc)
|
|
637
|
+
if json_output:
|
|
638
|
+
self.info(
|
|
639
|
+
json.dumps(
|
|
640
|
+
{
|
|
641
|
+
"status": "error",
|
|
642
|
+
"account": account_id,
|
|
643
|
+
"workspace": workspace,
|
|
644
|
+
"error": payload,
|
|
645
|
+
},
|
|
646
|
+
ensure_ascii=False,
|
|
647
|
+
indent=2,
|
|
648
|
+
sort_keys=True,
|
|
649
|
+
)
|
|
650
|
+
)
|
|
651
|
+
else:
|
|
652
|
+
self.info(self.format_private_api_error(exc))
|
|
653
|
+
return 2
|
|
654
|
+
|
|
531
655
|
def show_stats(
|
|
532
656
|
self,
|
|
533
657
|
name: Optional[str] = None,
|
|
@@ -1539,12 +1663,29 @@ class WorkspaceManager:
|
|
|
1539
1663
|
f"{(quota.plan or account.plan or '-'): <7} {self.percent_text(quota.used_percent):<8} {reset_at:<17} {status:<10} {quota.source}"
|
|
1540
1664
|
)
|
|
1541
1665
|
|
|
1542
|
-
def accounts_current(self) -> None:
|
|
1666
|
+
def accounts_current(self, *, id_only: bool = False, json_output: bool = False) -> None:
|
|
1543
1667
|
current = self.current_target()
|
|
1544
1668
|
if current.kind != "target" or current.path is None:
|
|
1545
1669
|
self.fail("当前工作区不存在。", "Current workspace does not exist.")
|
|
1546
1670
|
name = self.current_name(current.path) or "current"
|
|
1547
1671
|
meta = self.store.ensure_workspace_meta(name, current.path)
|
|
1672
|
+
if id_only:
|
|
1673
|
+
self.info(meta.active_account_id or "")
|
|
1674
|
+
return
|
|
1675
|
+
if json_output:
|
|
1676
|
+
self.info(
|
|
1677
|
+
json.dumps(
|
|
1678
|
+
{
|
|
1679
|
+
"workspace": name,
|
|
1680
|
+
"active_account_id": meta.active_account_id,
|
|
1681
|
+
"default_account_id": meta.default_account_id,
|
|
1682
|
+
},
|
|
1683
|
+
ensure_ascii=False,
|
|
1684
|
+
indent=2,
|
|
1685
|
+
sort_keys=True,
|
|
1686
|
+
)
|
|
1687
|
+
)
|
|
1688
|
+
return
|
|
1548
1689
|
self.info(f"{name}: active={meta.active_account_id or '-'} default={meta.default_account_id or '-'}")
|
|
1549
1690
|
|
|
1550
1691
|
def accounts_info(self, account: str) -> None:
|
|
@@ -1658,6 +1799,7 @@ class WorkspaceManager:
|
|
|
1658
1799
|
|
|
1659
1800
|
def get_account_quota(self, account_id: str, *, no_cache: bool = False) -> QuotaInfo:
|
|
1660
1801
|
settings = self.ensure_private_api_enabled("quota")
|
|
1802
|
+
self.ensure_quota_endpoint_configured(settings)
|
|
1661
1803
|
auth_material = self.account_auth_material(account_id)
|
|
1662
1804
|
ttl = int(settings.get("cache_ttl_seconds") or 300)
|
|
1663
1805
|
if not no_cache:
|
|
@@ -1692,22 +1834,39 @@ class WorkspaceManager:
|
|
|
1692
1834
|
return QuotaInfo(status="disabled", error=str(exc), source="disabled")
|
|
1693
1835
|
return QuotaInfo(status="no-auth", error=str(exc), source="local")
|
|
1694
1836
|
except PrivateApiError as exc:
|
|
1695
|
-
return QuotaInfo(status=
|
|
1837
|
+
return QuotaInfo(status=self.quota_list_status_from_error(exc), error=str(exc) if verbose else None, source="private-api")
|
|
1838
|
+
except Exception as exc:
|
|
1839
|
+
return QuotaInfo(
|
|
1840
|
+
status=self.quota_list_status_from_error(exc),
|
|
1841
|
+
error=redact_sensitive_text(str(exc)) if verbose else None,
|
|
1842
|
+
source="private-api",
|
|
1843
|
+
)
|
|
1696
1844
|
|
|
1697
|
-
def show_quota(self, *, json_output: bool = False, no_cache: bool = False) ->
|
|
1698
|
-
workspace
|
|
1699
|
-
account_id =
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1845
|
+
def show_quota(self, *, json_output: bool = False, no_cache: bool = False) -> int:
|
|
1846
|
+
workspace = None
|
|
1847
|
+
account_id = None
|
|
1848
|
+
try:
|
|
1849
|
+
workspace, _, meta = self.current_workspace_account()
|
|
1850
|
+
account_id = meta.active_account_id or meta.default_account_id
|
|
1851
|
+
assert account_id is not None
|
|
1852
|
+
quota = self.get_account_quota(account_id, no_cache=no_cache)
|
|
1853
|
+
account_meta = self.store.read_account_meta(account_id) if self.store.account_meta_path(account_id).is_file() else None
|
|
1854
|
+
self.render_quota(workspace, account_id, quota, account_meta, json_output=json_output)
|
|
1855
|
+
return 0
|
|
1856
|
+
except Exception as exc:
|
|
1857
|
+
return self.render_private_api_error(exc, json_output=json_output, account_id=account_id, workspace=workspace)
|
|
1704
1858
|
|
|
1705
|
-
def accounts_quota(self, account: str, *, json_output: bool = False, no_cache: bool = False) ->
|
|
1706
|
-
account_id =
|
|
1707
|
-
|
|
1708
|
-
self.
|
|
1709
|
-
|
|
1710
|
-
|
|
1859
|
+
def accounts_quota(self, account: str, *, json_output: bool = False, no_cache: bool = False) -> int:
|
|
1860
|
+
account_id = None
|
|
1861
|
+
try:
|
|
1862
|
+
account_id = self.account_id_from_input(account)
|
|
1863
|
+
if not self.store.account_meta_path(account_id).is_file():
|
|
1864
|
+
self.fail(f"账号不存在: {account_id}", f"Account not found: {account_id}")
|
|
1865
|
+
quota = self.get_account_quota(account_id, no_cache=no_cache)
|
|
1866
|
+
self.render_quota(None, account_id, quota, self.store.read_account_meta(account_id), json_output=json_output)
|
|
1867
|
+
return 0
|
|
1868
|
+
except Exception as exc:
|
|
1869
|
+
return self.render_private_api_error(exc, json_output=json_output, account_id=account_id, workspace=None)
|
|
1711
1870
|
|
|
1712
1871
|
def render_quota(self, workspace: Optional[str], account_id: str, quota: QuotaInfo, account_meta: Optional[AccountMeta], *, json_output: bool) -> None:
|
|
1713
1872
|
payload = {
|
|
@@ -2806,7 +2965,7 @@ def usage(lang: str) -> str:
|
|
|
2806
2965
|
迁移旧 ~/.codex-<工作区名> 目录,并可导入旧 ~/.codex-accounts。
|
|
2807
2966
|
|
|
2808
2967
|
codex-workspaces accounts list
|
|
2809
|
-
codex-workspaces accounts current
|
|
2968
|
+
codex-workspaces accounts current [--id|--json]
|
|
2810
2969
|
codex-workspaces accounts info <账号>
|
|
2811
2970
|
codex-workspaces accounts init <账号>
|
|
2812
2971
|
codex-workspaces accounts save <账号>
|
|
@@ -2910,7 +3069,7 @@ Usage:
|
|
|
2910
3069
|
Migrate legacy ~/.codex-<workspace> directories and optionally import old ~/.codex-accounts.
|
|
2911
3070
|
|
|
2912
3071
|
codex-workspaces accounts list
|
|
2913
|
-
codex-workspaces accounts current
|
|
3072
|
+
codex-workspaces accounts current [--id|--json]
|
|
2914
3073
|
codex-workspaces accounts info <account>
|
|
2915
3074
|
codex-workspaces accounts init <account>
|
|
2916
3075
|
codex-workspaces accounts save <account>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..store import auth_hash
|
|
8
|
+
from .errors import PrivateApiAuthError
|
|
9
|
+
from .models import AuthMaterial
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_auth_material(account_id: str, auth_path: Path) -> AuthMaterial:
|
|
13
|
+
raw_hash = auth_hash(auth_path)
|
|
14
|
+
try:
|
|
15
|
+
data = json.loads(auth_path.read_text(encoding="utf-8"))
|
|
16
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
17
|
+
raise PrivateApiAuthError("auth.json is not readable JSON") from exc
|
|
18
|
+
if not isinstance(data, dict):
|
|
19
|
+
raise PrivateApiAuthError("auth.json is not a JSON object")
|
|
20
|
+
|
|
21
|
+
auth_mode = string_value(data.get("auth_mode"))
|
|
22
|
+
if auth_mode != "chatgpt":
|
|
23
|
+
raise PrivateApiAuthError("unsupported auth_mode; run codex login or an official codex command to refresh auth")
|
|
24
|
+
|
|
25
|
+
tokens = data.get("tokens")
|
|
26
|
+
if not isinstance(tokens, dict):
|
|
27
|
+
raise PrivateApiAuthError("missing tokens in auth.json; run codex login or an official codex command to refresh auth")
|
|
28
|
+
|
|
29
|
+
access_token = string_value(tokens.get("access_token"))
|
|
30
|
+
refresh_token = string_value(tokens.get("refresh_token"))
|
|
31
|
+
openai_account_id = string_value(tokens.get("account_id")) or string_value(data.get("account_id"))
|
|
32
|
+
if not access_token:
|
|
33
|
+
raise PrivateApiAuthError("missing tokens.access_token; run codex login or an official codex command to refresh auth")
|
|
34
|
+
|
|
35
|
+
return AuthMaterial(
|
|
36
|
+
account_id=account_id,
|
|
37
|
+
auth_path=auth_path,
|
|
38
|
+
access_token=access_token,
|
|
39
|
+
refresh_token=refresh_token,
|
|
40
|
+
raw_auth_hash=raw_hash,
|
|
41
|
+
openai_account_id=openai_account_id,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def string_value(value: Any) -> str | None:
|
|
46
|
+
if isinstance(value, str) and value.strip():
|
|
47
|
+
return value.strip()
|
|
48
|
+
return None
|