@wangxt0223/codex-switcher 0.6.0 → 0.6.2
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.
- package/CHANGELOG.md +14 -0
- package/package.json +4 -1
- package/plugins/codex-switcher/README.en.md +4 -0
- package/plugins/codex-switcher/README.md +4 -0
- package/plugins/codex-switcher/scripts/codex-sw +13 -1
- package/plugins/codex-switcher/scripts/codex-switcher +260 -10
- package/plugins/codex-switcher/scripts/profile-metrics.py +25 -5
- package/plugins/codex-switcher/scripts/test-switcher.sh +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.2 - 2026-04-12
|
|
4
|
+
|
|
5
|
+
- Added usage-API proxy auto-detection (manual proxy > env proxy > macOS system proxy).
|
|
6
|
+
- Added `proxy` source display: `(manual)`, `(auto:env)`, `(auto:system)`, or `off`.
|
|
7
|
+
- Kept proxy scope limited to usage API calls used by `list`/`proxy test`.
|
|
8
|
+
- Added smoke-test coverage for env-proxy auto-detection and isolated proxy behavior.
|
|
9
|
+
|
|
10
|
+
## 0.6.1 - 2026-04-12
|
|
11
|
+
|
|
12
|
+
- Fixed symlink invocation path resolution for `codex-sw` / `codex-switcher`, so global npm installs can always find bundled scripts.
|
|
13
|
+
- Fixed `list` row field reuse bug that could leak previous row values into later rows.
|
|
14
|
+
- Improved usage API request compatibility by adding `ChatGPT-Account-Id` and browser-like headers, while keeping local sessions fallback.
|
|
15
|
+
- Added smoke-test coverage for symlink launch path behavior.
|
|
16
|
+
|
|
3
17
|
## 0.6.0 - 2026-04-12
|
|
4
18
|
|
|
5
19
|
- Refactored core model from profile-based switching to `env + account` with built-in `default=~/.codex`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wangxt0223/codex-switcher",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Env + account switcher for Codex CLI and Codex App with shared env data and per-account auth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wangxt",
|
|
@@ -43,5 +43,8 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public",
|
|
45
45
|
"registry": "https://registry.npmjs.org/"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@wangxt0223/codex-switcher": "^0.6.0"
|
|
46
49
|
}
|
|
47
50
|
}
|
|
@@ -34,6 +34,8 @@ codex-switcher account use <account> [--env <env>] [--target cli|app|both] [--sy
|
|
|
34
34
|
codex-switcher account logout [account] [--env <env>] [--target cli|app|both]
|
|
35
35
|
codex-switcher account current [cli|app]
|
|
36
36
|
|
|
37
|
+
codex-switcher proxy [<host:port>|off|test]
|
|
38
|
+
|
|
37
39
|
codex-switcher list
|
|
38
40
|
codex-switcher status
|
|
39
41
|
codex-switcher current [cli|app]
|
|
@@ -77,6 +79,8 @@ Usage data strategy:
|
|
|
77
79
|
- API first (`chatgpt.com/backend-api/wham/usage`)
|
|
78
80
|
- Auto fallback to local `sessions/*.jsonl` on API failure
|
|
79
81
|
- `LAST ACTIVITY` appends source marker `(api)` or `(local)`
|
|
82
|
+
- You can configure a dedicated proxy for this usage API via `codex-switcher proxy 127.0.0.1:7899` (only affects `list`)
|
|
83
|
+
- If no manual proxy is configured, env/system proxy settings are auto-detected for usage API requests
|
|
80
84
|
|
|
81
85
|
## Compatibility Commands
|
|
82
86
|
|
|
@@ -34,6 +34,8 @@ codex-switcher account use <account> [--env <env>] [--target cli|app|both] [--sy
|
|
|
34
34
|
codex-switcher account logout [account] [--env <env>] [--target cli|app|both]
|
|
35
35
|
codex-switcher account current [cli|app]
|
|
36
36
|
|
|
37
|
+
codex-switcher proxy [<host:port>|off|test]
|
|
38
|
+
|
|
37
39
|
codex-switcher list
|
|
38
40
|
codex-switcher status
|
|
39
41
|
codex-switcher current [cli|app]
|
|
@@ -77,6 +79,8 @@ codex-switcher account use corp --env project-a
|
|
|
77
79
|
- 默认优先 API(`chatgpt.com/backend-api/wham/usage`)
|
|
78
80
|
- API 失败自动回退本地 `sessions/*.jsonl`
|
|
79
81
|
- `LAST ACTIVITY` 列会追加 `(api)` 或 `(local)` 标记来源
|
|
82
|
+
- 可通过 `codex-switcher proxy 127.0.0.1:7899` 为该用量 API 单独配置代理(仅影响 `list`)
|
|
83
|
+
- 未手动设置时会自动检测环境变量/系统代理并用于用量 API 请求
|
|
80
84
|
|
|
81
85
|
## 兼容命令
|
|
82
86
|
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
resolve_script_dir() {
|
|
5
|
+
local src="${BASH_SOURCE[0]}"
|
|
6
|
+
local dir
|
|
7
|
+
while [[ -h "$src" ]]; do
|
|
8
|
+
dir="$(cd -P "$(dirname "$src")" && pwd)"
|
|
9
|
+
src="$(readlink "$src")"
|
|
10
|
+
[[ "$src" == /* ]] || src="$dir/$src"
|
|
11
|
+
done
|
|
12
|
+
cd -P "$(dirname "$src")" && pwd
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(resolve_script_dir)"
|
|
4
16
|
export CODEX_SWITCHER_INVOKED_AS="codex-sw"
|
|
5
17
|
exec "$SCRIPT_DIR/codex-switcher" "$@"
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
resolve_script_dir() {
|
|
5
|
+
local src="${BASH_SOURCE[0]}"
|
|
6
|
+
local dir
|
|
7
|
+
while [[ -h "$src" ]]; do
|
|
8
|
+
dir="$(cd -P "$(dirname "$src")" && pwd)"
|
|
9
|
+
src="$(readlink "$src")"
|
|
10
|
+
[[ "$src" == /* ]] || src="$dir/$src"
|
|
11
|
+
done
|
|
12
|
+
cd -P "$(dirname "$src")" && pwd
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(resolve_script_dir)"
|
|
16
|
+
|
|
4
17
|
STATE_DIR="${CODEX_SWITCHER_STATE_DIR:-$HOME/.codex-switcher}"
|
|
5
18
|
ENVS_DIR="${CODEX_SWITCHER_ENVS_DIR:-$HOME/.codex-envs}"
|
|
6
19
|
ACCOUNTS_DIR="${CODEX_SWITCHER_ACCOUNTS_DIR:-$STATE_DIR/env-accounts}"
|
|
@@ -15,6 +28,9 @@ LOCK_DIR="$STATE_DIR/.lock"
|
|
|
15
28
|
LOCK_WAIT_SECONDS="${CODEX_SWITCHER_LOCK_WAIT_SECONDS:-10}"
|
|
16
29
|
|
|
17
30
|
SCRIPT_NAME="${CODEX_SWITCHER_INVOKED_AS:-$(basename "$0")}"
|
|
31
|
+
USAGE_API_ENDPOINT="https://chatgpt.com/backend-api/wham/usage"
|
|
32
|
+
USAGE_PROXY_FILE="$STATE_DIR/usage_proxy"
|
|
33
|
+
DISABLE_SYSTEM_PROXY_DETECT="${CODEX_SWITCHER_DISABLE_SYSTEM_PROXY_DETECT:-false}"
|
|
18
34
|
|
|
19
35
|
usage() {
|
|
20
36
|
cat <<'USAGE'
|
|
@@ -34,6 +50,8 @@ Usage:
|
|
|
34
50
|
codex-sw account logout [account] [--env <env>] [--target cli|app|both]
|
|
35
51
|
codex-sw account current [cli|app]
|
|
36
52
|
|
|
53
|
+
codex-sw proxy [<host:port>|off|test]
|
|
54
|
+
|
|
37
55
|
codex-sw list
|
|
38
56
|
codex-sw status
|
|
39
57
|
codex-sw current [cli|app]
|
|
@@ -66,6 +84,8 @@ Compatibility:
|
|
|
66
84
|
Notes:
|
|
67
85
|
- Built-in env "default" always maps to ~/.codex (or CODEX_SWITCHER_DEFAULT_HOME).
|
|
68
86
|
- Same-env account switch only swaps auth.json and does not sync shared data.
|
|
87
|
+
- `proxy` only affects usage API calls used by `list`.
|
|
88
|
+
- If manual proxy is not set, usage API proxy is auto-detected from env/system proxy settings.
|
|
69
89
|
- `list` prints: EMAIL / PLAN / 5H USAGE / WEEKLY USAGE / LAST ACTIVITY (+ env/account context).
|
|
70
90
|
- Usage metrics are API-first and automatically fallback to local sessions; source is shown as (api|local).
|
|
71
91
|
USAGE
|
|
@@ -85,9 +105,8 @@ now_utc() {
|
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
switcher_version() {
|
|
88
|
-
local
|
|
89
|
-
|
|
90
|
-
package_json="$(cd "$script_dir/../../.." && pwd)/package.json"
|
|
108
|
+
local package_json version
|
|
109
|
+
package_json="$(cd "$SCRIPT_DIR/../../.." && pwd)/package.json"
|
|
91
110
|
|
|
92
111
|
if [[ -f "$package_json" ]]; then
|
|
93
112
|
version="$(sed -nE 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$package_json" | head -n 1)"
|
|
@@ -185,6 +204,116 @@ current_account_file() {
|
|
|
185
204
|
echo "$STATE_DIR/current_${target}_account"
|
|
186
205
|
}
|
|
187
206
|
|
|
207
|
+
normalize_usage_proxy_value() {
|
|
208
|
+
local raw="${1:-}"
|
|
209
|
+
local value
|
|
210
|
+
value="$(echo "$raw" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
211
|
+
[[ -n "$value" ]] || return 1
|
|
212
|
+
[[ "$value" != *[[:space:]]* ]] || return 1
|
|
213
|
+
case "$value" in
|
|
214
|
+
http://*|https://*|socks5://*)
|
|
215
|
+
echo "$value"
|
|
216
|
+
;;
|
|
217
|
+
*)
|
|
218
|
+
echo "http://$value"
|
|
219
|
+
;;
|
|
220
|
+
esac
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
read_usage_proxy() {
|
|
224
|
+
local value
|
|
225
|
+
[[ -f "$USAGE_PROXY_FILE" ]] || return 1
|
|
226
|
+
value="$(head -n 1 "$USAGE_PROXY_FILE" 2>/dev/null || true)"
|
|
227
|
+
normalize_usage_proxy_value "$value"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
detect_usage_proxy_from_env() {
|
|
231
|
+
local key raw normalized
|
|
232
|
+
for key in CODEX_SWITCHER_USAGE_PROXY HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy; do
|
|
233
|
+
raw="${!key:-}"
|
|
234
|
+
[[ -n "$raw" ]] || continue
|
|
235
|
+
normalized="$(normalize_usage_proxy_value "$raw" || true)"
|
|
236
|
+
[[ -n "$normalized" ]] || continue
|
|
237
|
+
echo "$normalized"
|
|
238
|
+
return 0
|
|
239
|
+
done
|
|
240
|
+
return 1
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
detect_usage_proxy_from_system_macos() {
|
|
244
|
+
[[ "$DISABLE_SYSTEM_PROXY_DETECT" != "true" ]] || return 1
|
|
245
|
+
[[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]] || return 1
|
|
246
|
+
command -v scutil >/dev/null 2>&1 || return 1
|
|
247
|
+
|
|
248
|
+
local dump host port enabled normalized
|
|
249
|
+
dump="$(scutil --proxy 2>/dev/null || true)"
|
|
250
|
+
[[ -n "$dump" ]] || return 1
|
|
251
|
+
|
|
252
|
+
host="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPSProxy[[:space:]]*:[[:space:]]*(.+)$/\1/p' | head -n 1)"
|
|
253
|
+
port="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPSPort[[:space:]]*:[[:space:]]*([0-9]+)$/\1/p' | head -n 1)"
|
|
254
|
+
enabled="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPSEnable[[:space:]]*:[[:space:]]*([01])$/\1/p' | head -n 1)"
|
|
255
|
+
if [[ "$enabled" == "1" && -n "$host" && -n "$port" ]]; then
|
|
256
|
+
normalized="$(normalize_usage_proxy_value "http://$host:$port" || true)"
|
|
257
|
+
[[ -n "$normalized" ]] && echo "$normalized" && return 0
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
host="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPProxy[[:space:]]*:[[:space:]]*(.+)$/\1/p' | head -n 1)"
|
|
261
|
+
port="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPPort[[:space:]]*:[[:space:]]*([0-9]+)$/\1/p' | head -n 1)"
|
|
262
|
+
enabled="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPEnable[[:space:]]*:[[:space:]]*([01])$/\1/p' | head -n 1)"
|
|
263
|
+
if [[ "$enabled" == "1" && -n "$host" && -n "$port" ]]; then
|
|
264
|
+
normalized="$(normalize_usage_proxy_value "http://$host:$port" || true)"
|
|
265
|
+
[[ -n "$normalized" ]] && echo "$normalized" && return 0
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
host="$(echo "$dump" | sed -nE 's/^[[:space:]]*SOCKSProxy[[:space:]]*:[[:space:]]*(.+)$/\1/p' | head -n 1)"
|
|
269
|
+
port="$(echo "$dump" | sed -nE 's/^[[:space:]]*SOCKSPort[[:space:]]*:[[:space:]]*([0-9]+)$/\1/p' | head -n 1)"
|
|
270
|
+
enabled="$(echo "$dump" | sed -nE 's/^[[:space:]]*SOCKSEnable[[:space:]]*:[[:space:]]*([01])$/\1/p' | head -n 1)"
|
|
271
|
+
if [[ "$enabled" == "1" && -n "$host" && -n "$port" ]]; then
|
|
272
|
+
normalized="$(normalize_usage_proxy_value "socks5://$host:$port" || true)"
|
|
273
|
+
[[ -n "$normalized" ]] && echo "$normalized" && return 0
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
return 1
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
resolve_usage_proxy_with_source() {
|
|
280
|
+
local value
|
|
281
|
+
|
|
282
|
+
value="$(read_usage_proxy || true)"
|
|
283
|
+
if [[ -n "$value" ]]; then
|
|
284
|
+
printf 'manual\t%s\n' "$value"
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
value="$(detect_usage_proxy_from_env || true)"
|
|
289
|
+
if [[ -n "$value" ]]; then
|
|
290
|
+
printf 'auto-env\t%s\n' "$value"
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
value="$(detect_usage_proxy_from_system_macos || true)"
|
|
295
|
+
if [[ -n "$value" ]]; then
|
|
296
|
+
printf 'auto-system\t%s\n' "$value"
|
|
297
|
+
return 0
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
return 1
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
set_usage_proxy() {
|
|
304
|
+
local value="$1"
|
|
305
|
+
local file tmp
|
|
306
|
+
file="$USAGE_PROXY_FILE"
|
|
307
|
+
tmp="${file}.tmp"
|
|
308
|
+
printf '%s\n' "$value" > "$tmp"
|
|
309
|
+
chmod 600 "$tmp" 2>/dev/null || true
|
|
310
|
+
mv "$tmp" "$file"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
clear_usage_proxy() {
|
|
314
|
+
rm -f -- "$USAGE_PROXY_FILE"
|
|
315
|
+
}
|
|
316
|
+
|
|
188
317
|
set_current_env() {
|
|
189
318
|
local target="$1"
|
|
190
319
|
local env="$2"
|
|
@@ -567,19 +696,22 @@ app_stop_managed() {
|
|
|
567
696
|
}
|
|
568
697
|
|
|
569
698
|
metrics_script_path() {
|
|
570
|
-
|
|
571
|
-
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
572
|
-
echo "$script_dir/profile-metrics.py"
|
|
699
|
+
echo "$SCRIPT_DIR/profile-metrics.py"
|
|
573
700
|
}
|
|
574
701
|
|
|
575
702
|
collect_profile_metrics_tsv() {
|
|
576
703
|
local account="$1"
|
|
577
704
|
local auth_file="$2"
|
|
578
705
|
local data_path="$3"
|
|
706
|
+
local usage_proxy="${4:-}"
|
|
579
707
|
local script_path
|
|
580
708
|
script_path="$(metrics_script_path)"
|
|
581
709
|
[[ -f "$script_path" ]] || return 1
|
|
582
|
-
|
|
710
|
+
if [[ -n "$usage_proxy" ]]; then
|
|
711
|
+
python3 "$script_path" --account-name "$account" --auth-file "$auth_file" --data-path "$data_path" --usage-proxy "$usage_proxy" 2>/dev/null
|
|
712
|
+
else
|
|
713
|
+
python3 "$script_path" --account-name "$account" --auth-file "$auth_file" --data-path "$data_path" 2>/dev/null
|
|
714
|
+
fi
|
|
583
715
|
}
|
|
584
716
|
|
|
585
717
|
cmd_env_list() {
|
|
@@ -914,15 +1046,104 @@ cmd_account_current() {
|
|
|
914
1046
|
esac
|
|
915
1047
|
}
|
|
916
1048
|
|
|
1049
|
+
cmd_proxy_show() {
|
|
1050
|
+
local info source value
|
|
1051
|
+
info="$(resolve_usage_proxy_with_source || true)"
|
|
1052
|
+
if [[ -z "$info" ]]; then
|
|
1053
|
+
echo "usage_api_proxy: off"
|
|
1054
|
+
return 0
|
|
1055
|
+
fi
|
|
1056
|
+
IFS=$'\t' read -r source value <<< "$info"
|
|
1057
|
+
case "$source" in
|
|
1058
|
+
manual)
|
|
1059
|
+
echo "usage_api_proxy: $value (manual)"
|
|
1060
|
+
;;
|
|
1061
|
+
auto-env)
|
|
1062
|
+
echo "usage_api_proxy: $value (auto:env)"
|
|
1063
|
+
;;
|
|
1064
|
+
auto-system)
|
|
1065
|
+
echo "usage_api_proxy: $value (auto:system)"
|
|
1066
|
+
;;
|
|
1067
|
+
*)
|
|
1068
|
+
echo "usage_api_proxy: $value"
|
|
1069
|
+
;;
|
|
1070
|
+
esac
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
cmd_proxy_set() {
|
|
1074
|
+
local raw="$1"
|
|
1075
|
+
local normalized
|
|
1076
|
+
normalized="$(normalize_usage_proxy_value "$raw")" || err "invalid proxy '$raw' (expected host:port or scheme://host:port)"
|
|
1077
|
+
set_usage_proxy "$normalized"
|
|
1078
|
+
log_event INFO "usage_proxy_set value=$normalized"
|
|
1079
|
+
echo "Set usage API proxy: $normalized"
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
cmd_proxy_off() {
|
|
1083
|
+
clear_usage_proxy
|
|
1084
|
+
log_event INFO "usage_proxy_off"
|
|
1085
|
+
echo "Manual usage API proxy disabled"
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
cmd_proxy_test() {
|
|
1089
|
+
local proxy env_name account auth_file token body_file http_code body_preview info source
|
|
1090
|
+
info="$(resolve_usage_proxy_with_source || true)"
|
|
1091
|
+
[[ -n "$info" ]] || err "usage API proxy is off and no auto proxy detected. run: $SCRIPT_NAME proxy <host:port>"
|
|
1092
|
+
IFS=$'\t' read -r source proxy <<< "$info"
|
|
1093
|
+
|
|
1094
|
+
env_name="$(read_current_env cli || true)"
|
|
1095
|
+
account="$(read_current_account cli || true)"
|
|
1096
|
+
auth_file="$(account_auth_path "$env_name" "$account")"
|
|
1097
|
+
[[ -f "$auth_file" ]] || auth_file="$(env_home_path "$env_name")/auth.json"
|
|
1098
|
+
[[ -f "$auth_file" ]] || err "auth.json not found for current CLI env/account: $env_name/$account"
|
|
1099
|
+
|
|
1100
|
+
token="$(python3 - "$auth_file" <<'PY'
|
|
1101
|
+
import json, sys
|
|
1102
|
+
path = sys.argv[1]
|
|
1103
|
+
try:
|
|
1104
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
1105
|
+
data = json.load(f)
|
|
1106
|
+
token = ((data.get("tokens") or {}).get("access_token") or "").strip()
|
|
1107
|
+
print(token)
|
|
1108
|
+
except Exception:
|
|
1109
|
+
print("")
|
|
1110
|
+
PY
|
|
1111
|
+
)"
|
|
1112
|
+
[[ -n "$token" ]] || err "access_token missing in $auth_file"
|
|
1113
|
+
|
|
1114
|
+
body_file="$(mktemp /tmp/codex-sw-proxy-test.XXXXXX)"
|
|
1115
|
+
http_code="$(HTTPS_PROXY="$proxy" HTTP_PROXY="$proxy" curl -sS -o "$body_file" -w '%{http_code}' \
|
|
1116
|
+
-H "Authorization: Bearer $token" \
|
|
1117
|
+
-H "Accept: application/json" \
|
|
1118
|
+
-H "User-Agent: Mozilla/5.0" \
|
|
1119
|
+
"$USAGE_API_ENDPOINT" || true)"
|
|
1120
|
+
body_preview="$(head -c 160 "$body_file" 2>/dev/null || true)"
|
|
1121
|
+
rm -f -- "$body_file"
|
|
1122
|
+
|
|
1123
|
+
if [[ "$http_code" == "200" ]]; then
|
|
1124
|
+
echo "usage_api_proxy_test: ok (http=200, source=$source, proxy=$proxy, env/account=$env_name/$account)"
|
|
1125
|
+
return 0
|
|
1126
|
+
fi
|
|
1127
|
+
|
|
1128
|
+
echo "usage_api_proxy_test: failed (http=${http_code:-000}, source=$source, proxy=$proxy, env/account=$env_name/$account)" >&2
|
|
1129
|
+
[[ -n "$body_preview" ]] && echo "response_preview: $body_preview" >&2
|
|
1130
|
+
return 1
|
|
1131
|
+
}
|
|
1132
|
+
|
|
917
1133
|
cmd_list() {
|
|
918
1134
|
local cli_env app_env cli_account app_account
|
|
919
1135
|
local env account marks auth_file home metrics
|
|
920
1136
|
local email plan usage_5h usage_weekly last_activity source
|
|
1137
|
+
local usage_proxy="" usage_info="" usage_proxy_source=""
|
|
921
1138
|
|
|
922
1139
|
cli_env="$(read_current_env cli || true)"
|
|
923
1140
|
app_env="$(read_current_env app || true)"
|
|
924
1141
|
cli_account="$(read_current_account cli || true)"
|
|
925
1142
|
app_account="$(read_current_account app || true)"
|
|
1143
|
+
usage_info="$(resolve_usage_proxy_with_source || true)"
|
|
1144
|
+
if [[ -n "$usage_info" ]]; then
|
|
1145
|
+
IFS=$'\t' read -r usage_proxy_source usage_proxy <<< "$usage_info"
|
|
1146
|
+
fi
|
|
926
1147
|
|
|
927
1148
|
printf '%-12s %-30s %-44s %-10s %-16s %-18s %s\n' "ENV" "ACCOUNT" "EMAIL" "PLAN" "5H USAGE" "WEEKLY USAGE" "LAST ACTIVITY"
|
|
928
1149
|
while IFS= read -r env; do
|
|
@@ -931,13 +1152,19 @@ cmd_list() {
|
|
|
931
1152
|
local found=0
|
|
932
1153
|
while IFS= read -r account; do
|
|
933
1154
|
found=1
|
|
1155
|
+
email=""
|
|
1156
|
+
plan=""
|
|
1157
|
+
usage_5h=""
|
|
1158
|
+
usage_weekly=""
|
|
1159
|
+
last_activity=""
|
|
1160
|
+
source=""
|
|
934
1161
|
marks=""
|
|
935
1162
|
[[ "$env" == "$cli_env" && "$account" == "$cli_account" ]] && marks="${marks} [cli-current]"
|
|
936
1163
|
[[ "$env" == "$app_env" && "$account" == "$app_account" ]] && marks="${marks} [app-current]"
|
|
937
1164
|
auth_file="$(account_auth_path "$env" "$account")"
|
|
938
1165
|
[[ -f "$auth_file" ]] || auth_file="$home/auth.json"
|
|
939
1166
|
|
|
940
|
-
metrics="$(collect_profile_metrics_tsv "$account" "$auth_file" "$home" || true)"
|
|
1167
|
+
metrics="$(collect_profile_metrics_tsv "$account" "$auth_file" "$home" "$usage_proxy" || true)"
|
|
941
1168
|
if [[ -n "$metrics" ]]; then
|
|
942
1169
|
IFS=$'\t' read -r email plan usage_5h usage_weekly last_activity source <<< "$metrics"
|
|
943
1170
|
fi
|
|
@@ -954,7 +1181,13 @@ cmd_list() {
|
|
|
954
1181
|
done < <(list_accounts_for_env "$env")
|
|
955
1182
|
|
|
956
1183
|
if [[ "$found" -eq 0 ]]; then
|
|
957
|
-
|
|
1184
|
+
email=""
|
|
1185
|
+
plan=""
|
|
1186
|
+
usage_5h=""
|
|
1187
|
+
usage_weekly=""
|
|
1188
|
+
last_activity=""
|
|
1189
|
+
source=""
|
|
1190
|
+
metrics="$(collect_profile_metrics_tsv "-" "$home/auth.json" "$home" "$usage_proxy" || true)"
|
|
958
1191
|
if [[ -n "$metrics" ]]; then
|
|
959
1192
|
IFS=$'\t' read -r email plan usage_5h usage_weekly last_activity source <<< "$metrics"
|
|
960
1193
|
fi
|
|
@@ -1242,7 +1475,7 @@ cmd_init() {
|
|
|
1242
1475
|
;;
|
|
1243
1476
|
esac
|
|
1244
1477
|
|
|
1245
|
-
script_abs="$
|
|
1478
|
+
script_abs="$SCRIPT_DIR/codex-switcher"
|
|
1246
1479
|
target_link="$HOME/.local/bin/codex-sw"
|
|
1247
1480
|
block_start="# >>> codex-sw init >>>"
|
|
1248
1481
|
block_end="# <<< codex-sw init <<<"
|
|
@@ -1700,6 +1933,23 @@ main() {
|
|
|
1700
1933
|
;;
|
|
1701
1934
|
esac
|
|
1702
1935
|
;;
|
|
1936
|
+
proxy)
|
|
1937
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME proxy [<host:port>|off|test]"
|
|
1938
|
+
case "${1:-}" in
|
|
1939
|
+
"")
|
|
1940
|
+
cmd_proxy_show
|
|
1941
|
+
;;
|
|
1942
|
+
off)
|
|
1943
|
+
with_lock cmd_proxy_off
|
|
1944
|
+
;;
|
|
1945
|
+
test)
|
|
1946
|
+
cmd_proxy_test
|
|
1947
|
+
;;
|
|
1948
|
+
*)
|
|
1949
|
+
with_lock cmd_proxy_set "$1"
|
|
1950
|
+
;;
|
|
1951
|
+
esac
|
|
1952
|
+
;;
|
|
1703
1953
|
add)
|
|
1704
1954
|
[[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME add <account>"
|
|
1705
1955
|
with_lock cmd_add "$1"
|
|
@@ -11,6 +11,10 @@ from typing import Any, Dict, Optional
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage"
|
|
14
|
+
DEFAULT_USER_AGENT = (
|
|
15
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
16
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
|
17
|
+
)
|
|
14
18
|
DASH = "-"
|
|
15
19
|
KNOWN_PLANS = {"free", "plus", "pro", "team", "business", "enterprise", "edu"}
|
|
16
20
|
|
|
@@ -20,6 +24,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
20
24
|
parser.add_argument("--account-name", required=True)
|
|
21
25
|
parser.add_argument("--auth-file", required=True)
|
|
22
26
|
parser.add_argument("--data-path", required=True)
|
|
27
|
+
parser.add_argument("--usage-proxy", default="")
|
|
23
28
|
parser.add_argument("--timeout-seconds", type=int, default=4)
|
|
24
29
|
return parser.parse_args()
|
|
25
30
|
|
|
@@ -165,7 +170,7 @@ def extract_windows_from_usage_blob(blob: Dict[str, Any]) -> Dict[int, Dict[str,
|
|
|
165
170
|
return windows
|
|
166
171
|
|
|
167
172
|
|
|
168
|
-
def request_usage(access_token: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
173
|
+
def request_usage(access_token: str, account_id: str, usage_proxy: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
169
174
|
if not access_token:
|
|
170
175
|
return None
|
|
171
176
|
cmd = [
|
|
@@ -177,15 +182,27 @@ def request_usage(access_token: str, timeout_seconds: int) -> Optional[Dict[str,
|
|
|
177
182
|
str(max(2, timeout_seconds + 2)),
|
|
178
183
|
"-H",
|
|
179
184
|
f"Authorization: Bearer {access_token}",
|
|
180
|
-
|
|
185
|
+
"-H",
|
|
186
|
+
"Accept: application/json",
|
|
187
|
+
"-H",
|
|
188
|
+
f"User-Agent: {DEFAULT_USER_AGENT}",
|
|
181
189
|
]
|
|
190
|
+
if account_id:
|
|
191
|
+
cmd.extend(["-H", f"ChatGPT-Account-Id: {account_id}"])
|
|
192
|
+
cmd.append(USAGE_ENDPOINT)
|
|
182
193
|
try:
|
|
194
|
+
env = os.environ.copy()
|
|
195
|
+
proxy = (usage_proxy or "").strip()
|
|
196
|
+
if proxy:
|
|
197
|
+
env["HTTPS_PROXY"] = proxy
|
|
198
|
+
env["HTTP_PROXY"] = proxy
|
|
183
199
|
result = subprocess.run(
|
|
184
200
|
cmd,
|
|
185
201
|
check=False,
|
|
186
202
|
stdout=subprocess.PIPE,
|
|
187
203
|
stderr=subprocess.PIPE,
|
|
188
204
|
text=True,
|
|
205
|
+
env=env,
|
|
189
206
|
)
|
|
190
207
|
except Exception:
|
|
191
208
|
return None
|
|
@@ -276,8 +293,8 @@ def collect_local_metrics(data_path: str) -> Dict[str, Any]:
|
|
|
276
293
|
return {"source": "local"}
|
|
277
294
|
|
|
278
295
|
|
|
279
|
-
def collect_api_metrics(access_token: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
280
|
-
payload = request_usage(access_token, timeout_seconds)
|
|
296
|
+
def collect_api_metrics(access_token: str, account_id: str, usage_proxy: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
297
|
+
payload = request_usage(access_token, account_id, usage_proxy, timeout_seconds)
|
|
281
298
|
if not payload:
|
|
282
299
|
return None
|
|
283
300
|
|
|
@@ -318,6 +335,9 @@ def main() -> int:
|
|
|
318
335
|
access_token = tokens.get("access_token")
|
|
319
336
|
if not isinstance(access_token, str):
|
|
320
337
|
access_token = ""
|
|
338
|
+
account_id = tokens.get("account_id")
|
|
339
|
+
if not isinstance(account_id, str):
|
|
340
|
+
account_id = ""
|
|
321
341
|
id_token = tokens.get("id_token")
|
|
322
342
|
if not isinstance(id_token, str):
|
|
323
343
|
id_token = ""
|
|
@@ -331,7 +351,7 @@ def main() -> int:
|
|
|
331
351
|
if plan_from_claims == "unknown":
|
|
332
352
|
plan_from_claims = normalize_plan(auth_data.get("chatgpt_plan_type") or auth_data.get("plan_type"))
|
|
333
353
|
|
|
334
|
-
api_metrics = collect_api_metrics(access_token, args.timeout_seconds)
|
|
354
|
+
api_metrics = collect_api_metrics(access_token, account_id, args.usage_proxy, args.timeout_seconds)
|
|
335
355
|
metrics = api_metrics if api_metrics is not None else collect_local_metrics(args.data_path)
|
|
336
356
|
|
|
337
357
|
windows = metrics.get("windows")
|
|
@@ -3,6 +3,7 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
5
|
SW="$ROOT/scripts/codex-sw"
|
|
6
|
+
SW_LINK=""
|
|
6
7
|
|
|
7
8
|
bash -n "$SW"
|
|
8
9
|
|
|
@@ -12,6 +13,8 @@ ENVS="$TMPBASE/envs"
|
|
|
12
13
|
DEFAULT_HOME="$TMPBASE/default-home"
|
|
13
14
|
BIN="$TMPBASE/bin"
|
|
14
15
|
mkdir -p "$BIN" "$DEFAULT_HOME"
|
|
16
|
+
SW_LINK="$BIN/codex-sw-link"
|
|
17
|
+
ln -s "$SW" "$SW_LINK"
|
|
15
18
|
|
|
16
19
|
cleanup() {
|
|
17
20
|
pkill -f "$BIN/fake-codex-app" >/dev/null 2>&1 || true
|
|
@@ -63,6 +66,7 @@ cat > "$BIN/curl" <<'CURL'
|
|
|
63
66
|
#!/usr/bin/env bash
|
|
64
67
|
set -euo pipefail
|
|
65
68
|
mode="${CODEX_SWITCHER_TEST_CURL_MODE:-success}"
|
|
69
|
+
echo "proxy=${HTTPS_PROXY:-}" >> "${CODEX_SWITCHER_TEST_CURL_LOG:?}"
|
|
66
70
|
if [[ "$mode" == "success" ]]; then
|
|
67
71
|
cat <<'JSON'
|
|
68
72
|
{"rate_limit":{"plan_type":"plus","primary_window":{"used_percent":40,"limit_window_seconds":18000,"reset_at":"2099-01-01T06:30:00Z"},"secondary_window":{"used_percent":20,"limit_window_seconds":604800,"reset_at":"2099-01-03T08:00:00Z"}},"last_activity_at":"2099-01-01T04:30:00Z"}
|
|
@@ -81,16 +85,26 @@ export CODEX_SWITCHER_ACCOUNTS_DIR="$STATE/env-accounts"
|
|
|
81
85
|
export CODEX_SWITCHER_APP_BIN="$BIN/fake-codex-app"
|
|
82
86
|
export CODEX_SWITCHER_LOCK_WAIT_SECONDS=2
|
|
83
87
|
export CODEX_SWITCHER_DEFAULT_HOME="$DEFAULT_HOME"
|
|
88
|
+
export CODEX_SWITCHER_DISABLE_SYSTEM_PROXY_DETECT=true
|
|
84
89
|
export CODEX_SWITCHER_TEST_NPM_LOG="$TMPBASE/npm-args.log"
|
|
85
90
|
export CODEX_SWITCHER_TEST_CODEX_LOG="$TMPBASE/codex-args.log"
|
|
91
|
+
export CODEX_SWITCHER_TEST_CURL_LOG="$TMPBASE/curl-args.log"
|
|
86
92
|
export CODEX_SWITCHER_TEST_CURL_MODE="success"
|
|
93
|
+
unset HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy
|
|
87
94
|
: > "$CODEX_SWITCHER_TEST_CODEX_LOG"
|
|
95
|
+
: > "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
88
96
|
|
|
89
97
|
echo '{"memo":"persist"}' > "$DEFAULT_HOME/shared.json"
|
|
90
98
|
|
|
91
99
|
check_out="$("$SW" check)"
|
|
92
100
|
echo "$check_out" | grep -Eq '^version: [0-9]+\.[0-9]+\.[0-9]+$'
|
|
93
101
|
echo "$check_out" | grep -q "check: ok"
|
|
102
|
+
link_check_out="$("$SW_LINK" check)"
|
|
103
|
+
echo "$link_check_out" | grep -Eq '^version: [0-9]+\.[0-9]+\.[0-9]+$'
|
|
104
|
+
echo "$link_check_out" | grep -q "check: ok"
|
|
105
|
+
[[ "$("$SW" proxy)" == "usage_api_proxy: off" ]]
|
|
106
|
+
"$SW" proxy 127.0.0.1:7899
|
|
107
|
+
[[ "$("$SW" proxy)" == "usage_api_proxy: http://127.0.0.1:7899 (manual)" ]]
|
|
94
108
|
init_out="$("$SW" init --dry-run)"
|
|
95
109
|
echo "$init_out" | grep -q "\[dry-run\]"
|
|
96
110
|
"$SW" upgrade
|
|
@@ -170,16 +184,33 @@ grep -q "(personal)personal@example.com" /tmp/codex_sw_list_api
|
|
|
170
184
|
grep -q "(api)" /tmp/codex_sw_list_api
|
|
171
185
|
grep -q "60% (" /tmp/codex_sw_list_api
|
|
172
186
|
grep -q "80% (" /tmp/codex_sw_list_api
|
|
187
|
+
grep -q "proxy=http://127.0.0.1:7899" "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
188
|
+
|
|
189
|
+
"$SW_LINK" list >/tmp/codex_sw_list_symlink
|
|
190
|
+
grep -q "(personal)personal@example.com" /tmp/codex_sw_list_symlink
|
|
191
|
+
grep -q "(api)" /tmp/codex_sw_list_symlink
|
|
173
192
|
|
|
174
193
|
mkdir -p "$ENVS/project/home/sessions/2026/04/12"
|
|
175
194
|
cat > "$ENVS/project/home/sessions/2026/04/12/rollout-test.jsonl" <<'JSONL'
|
|
176
195
|
{"timestamp":"2026-04-12T09:00:00Z","type":"event_msg","payload":{"type":"token_count","rate_limits":{"plan_type":"business","primary":{"used_percent":25,"window_minutes":300,"resets_at":1776004200},"secondary":{"used_percent":70,"window_minutes":10080,"resets_at":1776519000}}}}
|
|
177
196
|
JSONL
|
|
178
197
|
export CODEX_SWITCHER_TEST_CURL_MODE="fail"
|
|
198
|
+
"$SW" proxy off
|
|
199
|
+
[[ "$("$SW" proxy)" == "usage_api_proxy: off" ]]
|
|
200
|
+
: > "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
179
201
|
"$SW" list >/tmp/codex_sw_list_local
|
|
180
202
|
grep -q "(local)" /tmp/codex_sw_list_local
|
|
181
203
|
grep -q "75% (" /tmp/codex_sw_list_local
|
|
182
204
|
grep -q "30% (" /tmp/codex_sw_list_local
|
|
205
|
+
grep -q "^proxy=$" "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
206
|
+
|
|
207
|
+
export CODEX_SWITCHER_TEST_CURL_MODE="success"
|
|
208
|
+
export HTTPS_PROXY="http://10.10.10.10:18080"
|
|
209
|
+
[[ "$("$SW" proxy)" == "usage_api_proxy: http://10.10.10.10:18080 (auto:env)" ]]
|
|
210
|
+
: > "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
211
|
+
"$SW" list >/tmp/codex_sw_list_auto_env
|
|
212
|
+
grep -q "proxy=http://10.10.10.10:18080" "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
213
|
+
unset HTTPS_PROXY
|
|
183
214
|
|
|
184
215
|
"$SW" app status >/tmp/codex_sw_app_status_1
|
|
185
216
|
[[ "$?" -eq 0 ]]
|