cdx-manager 0.9.2 → 0.9.3

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/README.md CHANGED
@@ -301,7 +301,7 @@ cdx power all default
301
301
  cdx model provider:ollama default
302
302
  ```
303
303
 
304
- `--model` maps to Codex `--model`, Claude `--model`, and Ollama `ollama run <model>`. `--power` maps to Codex `model_reasoning_effort` and Claude `--effort`. `--permission` maps to provider-native permission flags. `--fast on` clears the stored power setting and uses low effort; setting `--power` again disables fast mode. `--priority` is a 0..100 selector preference used as a tie-breaker after readiness and availability. `--rtk on` injects a launch instruction that encourages assistants to use RTK (`rtk <command>`) for noisy terminal commands when RTK is available, while keeping raw commands for exact output. Logics guidance is auto-enabled when `logics-manager` is available; use `--logics off` to disable that guidance for a session, or `--logics on` to pin it explicitly.
304
+ `--model` maps to Codex `--model`, Claude `--model`, and Ollama `ollama run <model>`. `--power` maps to Codex `model_reasoning_effort` and Claude `--effort`; supported values are `minimal`, `low`, `medium`, `high`, and `xhigh`. `--permission` maps to provider-native permission flags. Codex Fast mode is separate from reasoning effort: `--fast on` opts a Codex session into the Codex Fast service tier, while new sessions, `--fast off`, and default launches force the non-Fast `flex` tier. Use `--power low` when you want low reasoning effort without enabling Codex Fast credits. Existing legacy sessions that stored `fast=on` before this split continue to behave as low effort unless the user explicitly sets `--fast on` again. `--priority` is a 0..100 selector preference used as a tie-breaker after readiness and availability. `--rtk on` injects a launch instruction that encourages assistants to use RTK (`rtk <command>`) for noisy terminal commands when RTK is available, while keeping raw commands for exact output. Logics guidance is auto-enabled when `logics-manager` is available; use `--logics off` to disable that guidance for a session, or `--logics on` to pin it explicitly.
305
305
 
306
306
  ### Launch History
307
307
 
@@ -339,7 +339,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
339
339
  | `cdx config <name> [--json]` | Show persistent launch settings for a session |
340
340
  | `cdx configs [--json]` | Show persistent launch settings for all sessions in one table |
341
341
  | `cdx power\|perm\|fast\|model <name\|all\|provider:PROVIDER\|a,b> <value\|default> [--json]` | Shortcut commands for setting or clearing one launch setting |
342
- | `cdx set <name>\|--sessions all\|a,b\|--provider PROVIDER [--power low\|medium\|high\|xhigh\|max] [--permission review\|default\|auto\|full] [--fast on\|off] [--rtk on\|off] [--logics on\|off] [--model MODEL] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
342
+ | `cdx set <name>\|--sessions all\|a,b\|--provider PROVIDER [--power minimal\|low\|medium\|high\|xhigh] [--permission review\|default\|auto\|full] [--fast on\|off] [--rtk on\|off] [--logics on\|off] [--model MODEL] [--priority 0..100] [--json]` | Persist launch settings for one or more sessions |
343
343
  | `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--rtk\|--logics\|--model\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
344
344
  | `cdx history [name] [--limit N] [--summary] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Show recent launch history or aggregate total launch time per assistant, optionally filtered by period |
345
345
  | `cdx last [--json]` | Launch the most recent existing session from launch history |
@@ -358,8 +358,8 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
358
358
  | `cdx notify <name> --at-reset [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait for a session reset time or schedule an OS wake-up notification when due |
359
359
  | `cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait until the recommended session is usable, or schedule the next known reset notification |
360
360
  | `cdx next [--json] [--refresh]` | Select the best next assistant using the same priority logic as `cdx status` |
361
- | `cdx select --provider PROVIDER [--min-reasoning-effort low\|medium\|high] [--min-power low\|medium\|high] [--require-ready] [--refresh] --json` | Select a suitable session for headless automation |
362
- | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low\|medium\|high] [--power low\|medium\|high] [--permission MODE] [--timeout-seconds N] --json` | Run one headless task and return a stable JSON result |
361
+ | `cdx select --provider PROVIDER [--min-reasoning-effort minimal\|low\|medium\|high\|xhigh] [--min-power minimal\|low\|medium\|high\|xhigh] [--require-ready] [--refresh] --json` | Select a suitable session for headless automation |
362
+ | `cdx run [session] --cwd PATH (--prompt-file PATH\|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal\|low\|medium\|high\|xhigh] [--power minimal\|low\|medium\|high\|xhigh] [--permission MODE] [--timeout-seconds N] --json` | Run one headless task and return a stable JSON result |
363
363
  | `cdx stats [name] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Aggregate launch counts, duration, and known headless token usage by session |
364
364
  | `cdx status [--json] [--refresh]` | Show token usage table for all sessions; JSON returns a versioned payload with structured warnings |
365
365
  | `cdx status --small [--refresh]` / `cdx status -s [--refresh]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
@@ -597,6 +597,7 @@ Session names are URL-encoded when used as directory or file names. CLI command
597
597
  ## Troubleshooting
598
598
 
599
599
  - **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first.
600
+ - **One of two Codex accounts keeps asking for login** — run `cdx doctor --json` and inspect each Codex session's `codex_auth_file` and `codex_live_auth` checks. Codex sessions use isolated `CODEX_HOME` directories, but newly created sessions seed from the current global `~/.codex/auth.json` when one exists. For two separate Codex accounts, create or repair each session by running `cdx login <name>` for that session; `cdx login` does not log out first, so use `cdx logout <name>` explicitly only when you want to clear that isolated profile.
600
601
  - **`cdx` says no compatible Python 3 interpreter was found** — install Python 3 and make `py -3`, `python`, or `python3` available on PATH.
601
602
  - **`cdx add` succeeds but the session does not appear** — check that `CDX_HOME` is consistent between calls; a mismatch creates two separate registries.
602
603
  - **Status shows `n/a` for all fields** — the Codex app-server rate-limit probe may be unavailable, the session may not be authenticated, and no legacy transcript/history status has been captured yet.
@@ -0,0 +1,35 @@
1
+ # CDX Manager 0.9.3
2
+
3
+ ## Highlights
4
+
5
+ - Codex Fast now stays opt-in and no longer silently lowers normal Codex session reasoning effort.
6
+ - Codex authentication diagnostics now detect when local credentials belong to a different account than the live CLI session.
7
+ - `cdx run` usage parsing now covers the Codex 0.141 JSONL `turn.completed` usage shape, including `reasoning_output_tokens`.
8
+
9
+ ## Changes
10
+
11
+ ### Codex Fast launch behavior
12
+
13
+ `cdx set <session> --fast on` now marks Fast as a service-tier choice instead of treating it as a reason to lower power to `low`. Normal Codex sessions continue to launch with their configured reasoning effort and an explicit flex service tier when Fast is off.
14
+
15
+ ### Codex auth isolation diagnostics
16
+
17
+ `cdx doctor` now reads the email from local Codex tokens and compares it with the live `codex login status` account when both are available. If a managed session has local tokens for one account but the CLI is authenticated as another, the report surfaces a repairable `codex_auth_mismatch` issue instead of reporting a vague live-auth failure.
18
+
19
+ The auth probe also avoids trusting local credentials alone for launch and run readiness, which keeps multi-account sessions from accidentally using the wrong global Codex login after a CLI update or account switch.
20
+
21
+ ### Codex 0.141 run usage parsing
22
+
23
+ Codex CLI 0.141 emits headless usage in JSONL `turn.completed` events. `cdx run` now maps the current `reasoning_output_tokens` field into the stable `reasoning_tokens` output and keeps computing total tokens from input plus output when the provider omits `total_tokens`.
24
+
25
+ ## Validation
26
+
27
+ - `cdx doctor --json`
28
+ - `cdx status --json --refresh`
29
+ - `cdx run --provider codex --cwd /Users/alexandreagostini/Documents/cdx-manager --prompt 'Reply exactly: CDX_CODEX_OK' --timeout-seconds 90 --json`
30
+ - `cdx run --provider claude --cwd /Users/alexandreagostini/Documents/cdx-manager --prompt 'Reply exactly: CDX_CLAUDE_OK' --timeout-seconds 90 --json`
31
+ - `npm run lint`
32
+ - `npm test`
33
+ - `logics-manager lint --require-status`
34
+ - `logics-manager health`
35
+ - `git diff --check`
@@ -72,6 +72,10 @@
72
72
  "v0.9.1": {
73
73
  "github_tarball_sha256": "8cbab620c823aeaf56e72c1a4d20e251a1c7142bfeeb3d516321d59c2c0a621d",
74
74
  "github_zip_sha256": "253909ede6dca61937835bd4ee29145ddacaa08fbdfba1b39810da73ae2ccc84"
75
+ },
76
+ "v0.9.2": {
77
+ "github_tarball_sha256": "3e3ae4e4efc63a97dc6623ae8ddff36f04f4b7a0b531878a28c041298223d0b8",
78
+ "github_zip_sha256": "09acd2866770f4dc7f9aaba6aff8308766bb211dabddd7f28bdfde9ace427e78"
75
79
  }
76
80
  }
77
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.9.2"
7
+ version = "0.9.3"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
package/src/cli.py CHANGED
@@ -64,7 +64,7 @@ from .status_view import (
64
64
  )
65
65
  from .update_check import check_for_update, check_logics_manager_for_update
66
66
 
67
- VERSION = "0.9.2"
67
+ VERSION = "0.9.3"
68
68
 
69
69
 
70
70
  # ---------------------------------------------------------------------------
@@ -82,8 +82,8 @@ def _print_help(use_color=False):
82
82
  f" {_style('cdx status --small|-s [--refresh]', '36', use_color)}",
83
83
  f" {_style('cdx status <name> [--json] [--refresh]', '36', use_color)}",
84
84
  f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
85
- f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json', '36', use_color)}",
86
- f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
85
+ f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
86
+ f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json', '36', use_color)}",
87
87
  f" {_style('cdx runs [--limit N] --json', '36', use_color)}",
88
88
  f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
89
89
  f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
@@ -91,7 +91,7 @@ def _print_help(use_color=False):
91
91
  f" {_style('cdx config <name> [--json]', '36', use_color)}",
92
92
  f" {_style('cdx configs [--json]', '36', use_color)}",
93
93
  f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
94
- f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
94
+ f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
95
95
  f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]', '36', use_color)}",
96
96
  f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
97
97
  f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
@@ -58,7 +58,7 @@ EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--
58
58
  IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
59
59
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
60
60
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
61
- SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
61
+ SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
62
62
  UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
63
63
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
64
64
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
@@ -66,9 +66,9 @@ CONFIGS_USAGE = "Usage: cdx configs [--json]"
66
66
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
67
67
  STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
68
68
  LAST_USAGE = "Usage: cdx last [--json]"
69
- SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
69
+ SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
70
70
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
71
- RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
71
+ RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
72
72
  RUNS_USAGE = "Usage: cdx runs [--limit N] --json"
73
73
  RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
74
74
  RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
@@ -1683,8 +1683,8 @@ def _resolve_bulk_launch_targets(parsed, service):
1683
1683
 
1684
1684
 
1685
1685
  def _reasoning_rank(value):
1686
- order = {"low": 0, "medium": 1, "high": 2, "xhigh": 2, "max": 2}
1687
- return order.get(str(value or "low").lower(), 0)
1686
+ order = {"minimal": 0, "low": 1, "medium": 2, "high": 3, "xhigh": 4}
1687
+ return order.get(str(value or "low").lower(), -1)
1688
1688
 
1689
1689
 
1690
1690
  def _session_reasoning_effort(session):
@@ -1693,7 +1693,7 @@ def _session_reasoning_effort(session):
1693
1693
  launch.get("reasoning_effort")
1694
1694
  or launch.get("reasoningEffort")
1695
1695
  or launch.get("power")
1696
- or ("low" if launch.get("fast") is True else None)
1696
+ or ("low" if launch.get("fast") is True and launch.get("fastMode") != "service_tier" else None)
1697
1697
  or "low"
1698
1698
  )
1699
1699
 
@@ -2290,6 +2290,7 @@ def handle_doctor(rest, ctx):
2290
2290
  ctx["service"],
2291
2291
  ctx["service"]["base_dir"],
2292
2292
  env=ctx.get("env"),
2293
+ spawn_sync=ctx.get("spawn_sync"),
2293
2294
  )
2294
2295
  if json_flag:
2295
2296
  _write_json(ctx, _json_success("doctor", "Collected health report", report=report))
@@ -2672,11 +2673,6 @@ def handle_login(rest, ctx):
2672
2673
  session = ctx["service"]["get_session"](args[0])
2673
2674
  if not session:
2674
2675
  raise CdxError(f"Unknown session: {args[0]}")
2675
- if session["provider"] == PROVIDER_CODEX:
2676
- _run_interactive_provider_command(
2677
- session, "logout", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
2678
- signal_emitter=ctx.get("signal_emitter")
2679
- )
2680
2676
  _run_interactive_provider_command(
2681
2677
  session, "login", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
2682
2678
  signal_emitter=ctx.get("signal_emitter")
@@ -2687,6 +2683,7 @@ def handle_login(rest, ctx):
2687
2683
  spawn_sync=ctx.get("spawn_sync"),
2688
2684
  env_override=ctx.get("env"),
2689
2685
  behavior="probe-only",
2686
+ trust_local_credentials=False,
2690
2687
  )
2691
2688
  if not auth_probe.get("authenticated") and session["provider"] == PROVIDER_CLAUDE:
2692
2689
  _bootstrap_claude_setup_token(session, ctx)
@@ -2696,6 +2693,7 @@ def handle_login(rest, ctx):
2696
2693
  spawn_sync=ctx.get("spawn_sync"),
2697
2694
  env_override=ctx.get("env"),
2698
2695
  behavior="probe-only",
2696
+ trust_local_credentials=False,
2699
2697
  )
2700
2698
  if not auth_probe.get("authenticated"):
2701
2699
  raise CdxError(
@@ -2859,6 +2857,7 @@ def handle_launch(command, ctx, initial_prompt=None):
2859
2857
  stdin_is_tty=ctx["stdin_is_tty"],
2860
2858
  behavior="launch",
2861
2859
  signal_emitter=ctx.get("signal_emitter"),
2860
+ trust_local_credentials=False,
2862
2861
  )
2863
2862
  message = f"Launching {session['provider']} session {session['name']}"
2864
2863
  if not json_flag:
package/src/health.py CHANGED
@@ -6,6 +6,8 @@ import tempfile
6
6
  from urllib.parse import quote, unquote
7
7
 
8
8
  from .cli_render import _pad_table, _style
9
+ from .config import PROVIDER_CODEX
10
+ from .provider_runtime import codex_auth_diagnostic
9
11
 
10
12
 
11
13
  def _encode(name):
@@ -30,7 +32,7 @@ def _issue(status, code, message, detail=None, repairable=False):
30
32
  }
31
33
 
32
34
 
33
- def collect_health_report(service, base_dir, env=None):
35
+ def collect_health_report(service, base_dir, env=None, spawn_sync=None):
34
36
  env = env or os.environ
35
37
  issues = []
36
38
 
@@ -79,11 +81,48 @@ def collect_health_report(service, base_dir, env=None):
79
81
  state_path = _state_file_path(base_dir, name)
80
82
  if not os.path.isfile(state_path):
81
83
  issues.append(_issue("FAIL", "missing_state", f"session {name} state file is missing", state_path, True))
84
+ if session.get("provider") == PROVIDER_CODEX:
85
+ issues.extend(_codex_auth_issues(session, spawn_sync=spawn_sync, env=env))
82
86
 
83
87
  issues.extend(_collect_profile_issues(base_dir, session_names))
84
88
  return {"base_dir": base_dir, "issues": issues, "summary": summarize_health(issues)}
85
89
 
86
90
 
91
+ def _codex_auth_issues(session, spawn_sync=None, env=None):
92
+ name = session["name"]
93
+ diag = codex_auth_diagnostic(session, spawn_sync=spawn_sync, env_override=env)
94
+ identity = diag.get("account_email") or "unknown account"
95
+ issues = [
96
+ _issue(
97
+ "OK" if diag.get("auth_json_exists") else "WARN",
98
+ "codex_auth_file",
99
+ f"session {name} Codex auth file {'found' if diag.get('auth_json_exists') else 'missing'} ({identity})",
100
+ {
101
+ "session": name,
102
+ "auth_home": diag.get("auth_home"),
103
+ "auth_json_exists": diag.get("auth_json_exists"),
104
+ "local_tokens_present": diag.get("local_tokens_present"),
105
+ "account_email": diag.get("account_email"),
106
+ },
107
+ )
108
+ ]
109
+ live_status = diag.get("live_status")
110
+ live_ok = live_status == "authenticated"
111
+ live_warn = live_status in ("logged_out", "error")
112
+ issues.append(_issue(
113
+ "OK" if live_ok else "WARN" if live_warn else "WARN",
114
+ "codex_live_auth",
115
+ f"session {name} live Codex auth status: {live_status}",
116
+ {
117
+ "session": name,
118
+ "auth_home": diag.get("auth_home"),
119
+ "live_status": live_status,
120
+ "live_error": diag.get("live_error"),
121
+ },
122
+ ))
123
+ return issues
124
+
125
+
87
126
  def _check_cdx_home(base_dir):
88
127
  try:
89
128
  os.makedirs(base_dir, exist_ok=True)
@@ -7,6 +7,7 @@ import shutil
7
7
  import subprocess
8
8
  import sys
9
9
  import uuid
10
+ import base64
10
11
  from datetime import datetime, timezone
11
12
 
12
13
  from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
@@ -14,7 +15,7 @@ from .errors import CdxError
14
15
 
15
16
 
16
17
  LOG_ROTATE_BYTES = 10 * 1024 * 1024 # 10 MB
17
- REASONING_EFFORT_VALUES = {"low", "medium", "high"}
18
+ REASONING_EFFORT_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
18
19
  RTK_PROMPT = (
19
20
  "When running noisy shell commands, prefer RTK wrappers (`rtk <command>`) if `rtk` is available. "
20
21
  "Use raw commands when exact, unfiltered output is required."
@@ -148,6 +149,64 @@ def _has_local_codex_auth(auth_home):
148
149
  )
149
150
 
150
151
 
152
+ def _decode_jwt_claims(token):
153
+ if not token or "." not in str(token):
154
+ return {}
155
+ parts = str(token).split(".")
156
+ if len(parts) < 2:
157
+ return {}
158
+ padding = "=" * (-len(parts[1]) % 4)
159
+ try:
160
+ decoded = base64.urlsafe_b64decode(parts[1] + padding)
161
+ return json.loads(decoded.decode("utf-8"))
162
+ except (ValueError, json.JSONDecodeError, UnicodeDecodeError):
163
+ return {}
164
+
165
+
166
+ def _read_codex_account_email(auth_home):
167
+ try:
168
+ with open(os.path.join(auth_home, "auth.json"), "r", encoding="utf-8") as handle:
169
+ auth = json.load(handle)
170
+ except (FileNotFoundError, OSError, json.JSONDecodeError):
171
+ return None
172
+ tokens = auth.get("tokens") if isinstance(auth, dict) else {}
173
+ if not isinstance(tokens, dict):
174
+ return None
175
+ for token_name in ("id_token", "access_token"):
176
+ claims = _decode_jwt_claims(tokens.get(token_name))
177
+ email = claims.get("email")
178
+ if not email and token_name == "access_token":
179
+ profile = claims.get("https://api.openai.com/profile") or {}
180
+ email = profile.get("email") if isinstance(profile, dict) else None
181
+ if email:
182
+ return str(email).strip().lower()
183
+ return None
184
+
185
+
186
+ def codex_auth_diagnostic(session, spawn_sync=None, env_override=None):
187
+ auth_home = _get_auth_home(session)
188
+ auth_path = os.path.join(auth_home, "auth.json")
189
+ result = {
190
+ "auth_home": auth_home,
191
+ "auth_json_exists": os.path.isfile(auth_path),
192
+ "local_tokens_present": _has_local_codex_auth(auth_home),
193
+ "account_email": _read_codex_account_email(auth_home),
194
+ "live_status": "unknown",
195
+ "live_error": None,
196
+ }
197
+ try:
198
+ result["live_status"] = "authenticated" if _probe_provider_auth(
199
+ session,
200
+ spawn_sync=spawn_sync,
201
+ env_override=env_override,
202
+ trust_local_credentials=False,
203
+ ) else "logged_out"
204
+ except CdxError as error:
205
+ result["live_status"] = "error"
206
+ result["live_error"] = str(error)
207
+ return result
208
+
209
+
151
210
  def _read_claude_account_email(auth_home):
152
211
  config_path = os.path.join(auth_home, ".claude.json")
153
212
  try:
@@ -196,11 +255,25 @@ def _launch_power(session):
196
255
  power = launch.get("reasoning_effort") or launch.get("reasoningEffort") or launch.get("power")
197
256
  if power:
198
257
  return power
199
- if launch.get("fast") is True:
258
+ if _legacy_fast_low_effort(launch):
200
259
  return "low"
201
260
  return None
202
261
 
203
262
 
263
+ def _legacy_fast_low_effort(launch):
264
+ return launch.get("fast") is True and launch.get("fastMode") != "service_tier"
265
+
266
+
267
+ def _codex_fast_enabled(launch):
268
+ return launch.get("fast") is True and launch.get("fastMode") == "service_tier"
269
+
270
+
271
+ def _codex_fast_config_args(launch):
272
+ if _codex_fast_enabled(launch):
273
+ return ["-c", 'service_tier="fast"', "-c", "features.fast_mode=true"]
274
+ return ["-c", 'service_tier="flex"', "-c", "features.fast_mode=false"]
275
+
276
+
204
277
  def _normalize_reasoning_effort(reasoning_effort=None, power=None, usage="Unsupported reasoning effort."):
205
278
  effort = str(reasoning_effort).strip().lower() if reasoning_effort is not None else None
206
279
  alias = str(power).strip().lower() if power is not None else None
@@ -269,6 +342,7 @@ def _launch_config_args(session):
269
342
  return args
270
343
  if power:
271
344
  args += ["-c", f'model_reasoning_effort="{power}"']
345
+ args += _codex_fast_config_args(launch)
272
346
  if permission:
273
347
  args += LAUNCH_PERMISSION_ARGS[PROVIDER_CODEX].get(permission, [])
274
348
  return args
@@ -482,6 +556,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
482
556
  args += ["--model", model]
483
557
  if power:
484
558
  args += ["-c", f'model_reasoning_effort="{power}"']
559
+ args += _codex_fast_config_args(launch)
485
560
  if permission:
486
561
  args += HEADLESS_CODEX_PERMISSION_ARGS.get(permission, [])
487
562
  if initial_prompt:
@@ -43,7 +43,7 @@ def run_payload_reasoning_effort(parsed, session):
43
43
  or launch.get("reasoning_effort")
44
44
  or launch.get("reasoningEffort")
45
45
  or launch.get("power")
46
- or ("low" if launch.get("fast") is True else None)
46
+ or ("low" if launch.get("fast") is True and launch.get("fastMode") != "service_tier" else None)
47
47
  )
48
48
 
49
49
 
package/src/run_usage.py CHANGED
@@ -91,6 +91,7 @@ def _usage_from_dict(value):
91
91
  output_tokens = _int_value(usage.get("output_tokens"), usage.get("completion_tokens"))
92
92
  reasoning_tokens = _int_value(
93
93
  usage.get("reasoning_tokens"),
94
+ usage.get("reasoning_output_tokens"),
94
95
  _nested_int(usage, "output_tokens_details", "reasoning_tokens"),
95
96
  _nested_int(usage, "completion_tokens_details", "reasoning_tokens"),
96
97
  )
@@ -66,8 +66,8 @@ RESERVED_SESSION_NAMES = {
66
66
  STATUS_CACHE_TTL_SECONDS = 60
67
67
  CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
68
68
  MAX_STATUS_WORKERS = 8
69
- LAUNCH_POWER_VALUES = {"low", "medium", "high", "xhigh", "max"}
70
- LAUNCH_REASONING_EFFORT_VALUES = {"low", "medium", "high"}
69
+ LAUNCH_POWER_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
70
+ LAUNCH_REASONING_EFFORT_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
71
71
  LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
72
72
  MAX_LAUNCH_MODEL_LENGTH = 128
73
73
  MIN_LAUNCH_PRIORITY = 0
@@ -107,7 +107,7 @@ def _seed_codex_auth_from_global(auth_home, env=None):
107
107
  return True
108
108
 
109
109
 
110
- def _normalize_launch_settings(settings):
110
+ def _normalize_launch_settings(settings, mark_fast_service_tier=True):
111
111
  normalized = {}
112
112
  if not settings:
113
113
  return normalized
@@ -138,6 +138,12 @@ def _normalize_launch_settings(settings):
138
138
  normalized["fast"] = False
139
139
  else:
140
140
  raise CdxError(f"Unsupported fast value: {settings['fast']}")
141
+ if normalized["fast"] is True and mark_fast_service_tier:
142
+ normalized["fastMode"] = "service_tier"
143
+ else:
144
+ normalized.pop("fastMode", None)
145
+ if settings.get("fastMode") == "service_tier" and normalized["fast"] is True:
146
+ normalized["fastMode"] = "service_tier"
141
147
  if "rtk" in settings and settings["rtk"] is not None:
142
148
  value = settings["rtk"]
143
149
  if isinstance(value, bool):
@@ -827,17 +833,15 @@ def create_session_service(options=None):
827
833
  updates = _normalize_launch_settings(settings)
828
834
  if not updates:
829
835
  raise CdxError("At least one launch setting is required.")
830
- current = _normalize_launch_settings(session.get("launch") or {})
836
+ current = _normalize_launch_settings(session.get("launch") or {}, mark_fast_service_tier=False)
831
837
  launch = {**current, **updates}
832
838
  explicit_power = "power" in updates or "reasoning_effort" in updates
833
- if explicit_power and "fast" not in updates:
839
+ if explicit_power and "fast" not in updates and launch.get("fastMode") != "service_tier":
834
840
  launch["fast"] = False
835
- if "fast" in updates and not explicit_power:
836
- if updates["fast"] is True:
837
- launch.pop("power", None)
838
- launch.pop("reasoning_effort", None)
839
- launch.pop("reasoningEffort", None)
840
- elif not any(key in launch for key in ("power", "reasoning_effort", "reasoningEffort")):
841
+ launch.pop("fastMode", None)
842
+ if updates.get("fast") is False:
843
+ launch.pop("fastMode", None)
844
+ if not any(key in launch for key in ("power", "reasoning_effort", "reasoningEffort")):
841
845
  launch["power"] = DEFAULT_LAUNCH_SETTINGS["power"]
842
846
  now = _local_now_iso()
843
847
  return store["update_session"](name, lambda s: {