claude-multiacc 2.0.64 → 2.0.66

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 (233) hide show
  1. package/bin/claude-accounts +15 -201
  2. package/bin/codex +25 -7
  3. package/bin/codex-accounts +16 -3
  4. package/docs/AUTORESUME.md +10 -1
  5. package/docs/CODEX.md +39 -0
  6. package/lib/__pycache__/audit.cpython-314.pyc +0 -0
  7. package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
  8. package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
  9. package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
  10. package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
  11. package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
  12. package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
  13. package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
  14. package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
  15. package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
  16. package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
  17. package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
  18. package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
  19. package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
  20. package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
  21. package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
  22. package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
  23. package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
  24. package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
  25. package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
  26. package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
  27. package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
  28. package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
  29. package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
  30. package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
  31. package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
  32. package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
  33. package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
  34. package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
  35. package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
  36. package/lib/__pycache__/report.cpython-314.pyc +0 -0
  37. package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
  38. package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
  39. package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
  40. package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
  41. package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
  42. package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
  43. package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
  44. package/lib/autoresume.py +28 -8
  45. package/lib/claude_oauth_lock.py +172 -0
  46. package/lib/claude_oauth_refresh.py +225 -0
  47. package/lib/codex_speed.py +351 -0
  48. package/lib/codex_speed.sh +39 -0
  49. package/lib/common.sh +13 -3
  50. package/lib/model_match.py +4 -1
  51. package/lib/session_settings.py +25 -6
  52. package/lib/shim_path.py +6 -0
  53. package/package.json +4 -3
  54. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
  55. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
  56. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
  57. package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
  58. package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
  59. package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
  60. package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
  61. package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
  62. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
  63. package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
  64. package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
  65. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
  66. package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
  67. package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
  68. package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
  69. package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
  70. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
  95. package/tests/__pycache__/test_claude_oauth_lock.cpython-314.pyc +0 -0
  96. package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
  97. package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
  98. package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
  99. package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
  100. package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
  101. package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
  102. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
  103. package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
  108. package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
  109. package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
  110. package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
  111. package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
  112. package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
  113. package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
  114. package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
  115. package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
  116. package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
  117. package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
  118. package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
  119. package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
  120. package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
  121. package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
  122. package/tests/claude_oauth_fixture.py +100 -0
  123. package/tests/run-tests.sh +23 -7
  124. package/tests/test_claude_oauth_lock.py +138 -0
  125. package/tests/test_claude_oauth_refresh.py +106 -0
  126. package/tests/test_claude_oauth_refresh_failures.py +112 -0
  127. package/tests/test_codex_speed.py +236 -0
  128. package/tests/test_mutate_lock.py +107 -0
  129. package/tests/test_shim_path.py +44 -4
  130. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  131. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  132. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  133. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  134. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  135. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  136. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  137. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  138. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  139. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  140. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  141. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  142. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  143. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  144. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  145. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  146. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  147. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  148. package/lib/__pycache__/codex_mcp_oauth.cpython-312.pyc +0 -0
  149. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  150. package/lib/__pycache__/codex_resume_cwd.cpython-312.pyc +0 -0
  151. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  152. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  153. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  154. package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
  155. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  156. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  157. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  158. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  159. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  160. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  161. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  162. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  163. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  164. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  165. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  166. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  167. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  168. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  169. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  170. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  171. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  172. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  173. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  174. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  175. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  176. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  177. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  178. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  179. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  180. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  181. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  182. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  183. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  184. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  185. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  186. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  187. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  188. package/tests/__pycache__/test_autoresume_cwd.cpython-312.pyc +0 -0
  189. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-312.pyc +0 -0
  190. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-312.pyc +0 -0
  191. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-312.pyc +0 -0
  192. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  193. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  194. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  195. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  196. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  197. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  198. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  199. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  200. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  201. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  202. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  203. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  204. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  205. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  206. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  207. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  208. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  209. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  210. package/tests/__pycache__/test_codex_exec_stdin.cpython-312.pyc +0 -0
  211. package/tests/__pycache__/test_codex_mcp_oauth.cpython-312.pyc +0 -0
  212. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  213. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  214. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  215. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  216. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  217. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  218. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  219. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  220. package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
  221. package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
  222. package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
  223. package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
  224. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  225. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  226. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  227. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  228. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  229. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  230. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  231. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  232. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  233. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
@@ -1708,7 +1708,7 @@ token_identity_receipt() { # Unknown results must never overwrite a peer's prove
1708
1708
  limits_distribute_now() {
1709
1709
  local lock="$ACC_ROOT/tmp/limits-push.lock"
1710
1710
  mkdir -p "$ACC_ROOT/tmp" 2>/dev/null || return 0
1711
- if ! mkdir "$lock" 2>/dev/null; then
1711
+ if ! atomic_mkdir "$lock" 2>/dev/null; then
1712
1712
  # A DETACHED push that is killed (logout, reboot, pkill) never runs its EXIT trap,
1713
1713
  # and mkdir can never take a lock dir nobody will remove: one stranded lock
1714
1714
  # silently stopped ALL telemetry distribution on the live pool from 2026-09-03
@@ -1721,7 +1721,7 @@ limits_distribute_now() {
1721
1721
  # exactly as before.
1722
1722
  [ $(( $(epoch_now) - $(file_mtime "$lock") )) -gt 600 ] || return 0
1723
1723
  rm -rf "$lock" 2>/dev/null
1724
- mkdir "$lock" 2>/dev/null || return 0
1724
+ atomic_mkdir "$lock" 2>/dev/null || return 0
1725
1725
  log_to sync.log "stale limits-push lock broken (older than 600s); distributing"
1726
1726
  fi
1727
1727
  trap 'rmdir "$lock" 2>/dev/null || true' EXIT
@@ -1792,7 +1792,7 @@ cmd_limits() {
1792
1792
  done
1793
1793
  local lock="$ACC_ROOT/.locks/limits"
1794
1794
  mkdir -p "$ACC_ROOT/.locks"
1795
- if ! mkdir "$lock" 2>/dev/null; then
1795
+ if ! atomic_mkdir "$lock" 2>/dev/null; then
1796
1796
  local age=$(( $(epoch_now) - $(file_mtime "$lock") ))
1797
1797
  if [ "$age" -lt 120 ]; then
1798
1798
  # A --json caller still gets the document (built from the state on disk) —
@@ -1802,7 +1802,7 @@ cmd_limits() {
1802
1802
  return 0
1803
1803
  fi
1804
1804
  rm -rf "$lock"
1805
- if ! mkdir "$lock" 2>/dev/null; then
1805
+ if ! atomic_mkdir "$lock" 2>/dev/null; then
1806
1806
  if [ "$json" = "1" ]; then emit_report_json limits; return $?; fi
1807
1807
  return 0
1808
1808
  fi
@@ -1846,12 +1846,6 @@ TOKEN_URL = os.environ.get('CLAUDE_MULTIACC_TOKEN_URL',
1846
1846
  'https://console.anthropic.com/v1/oauth/token')
1847
1847
  CLIENT_ID = os.environ.get('CLAUDE_MULTIACC_CLIENT_ID',
1848
1848
  '9d1c250a-e61b-44d9-88ed-5944d1962f5e')
1849
- # Only refresh a token that has been expired for a while: a LIVE session refreshes
1850
- # its own credential within moments of expiry, so a long-expired one proves no
1851
- # other writer is active (refresh tokens rotate; two racing refreshers strand one).
1852
- REFRESH_MIN_EXPIRED = 300
1853
- REFRESH_FAIL_BACKOFF = 600 # transient (network/5xx/429): retry in 10 min
1854
- REFRESH_DENIED_BACKOFF = 21600 # 4xx = grant likely revoked: 6h; re-login needed anyway
1855
1849
  # A usage fetch the server says will never succeed (x-should-retry: false — e.g. the
1856
1850
  # 403 "does not meet scope requirement user:profile" that a setup token ALWAYS gets)
1857
1851
  # is not a hiccup to retry every pass. Retrying it is what manufactured the 429s that
@@ -2008,39 +2002,12 @@ def park_dead_grant(aid, d, slug, detail):
2008
2002
  say(f'{aid}: {detail} — re-login needed (claude-accounts relogin {aid})')
2009
2003
 
2010
2004
 
2011
- # The client's own write lock: proper-lockfile's DIRECTORY <dir>/.storage-write.lock, stale
2012
- # after 15 s (lib/mcp_oauth.py ClientLock). Claude Code takes it around every write of its
2013
- # credential document, file or Keychain item alike — re-read, one change, the whole
2014
- # document back — and its write FAILS after about 7.5 s of retries on a held lock. So the
2015
- # refresher never holds it across the token POST: a lock held when a refresh would start
2016
- # skips that refresh (nothing has rotated yet; the next pass retries), and the
2017
- # check-and-set and the write take it after the POST, waiting longer, because a rotated
2018
- # grant must land: past the wait (or with no helper) it is written as before, without it.
2019
- try:
2020
- from mcp_oauth import CLIENT_LOCK, CLIENT_LOCK_STALE_S, ClientLock # noqa: E402
2021
- except Exception: # noqa: BLE001 — fail open: refresh exactly as before, unlocked
2022
- ClientLock = None
2023
- CLIENT_LOCK_PROBE_S = 1.0 # a client's write takes milliseconds: held this long, it is busy
2005
+ # Legacy Keychain migration needs only the client's short storage-write lock.
2006
+ # Refresh request locking and credential CAS live in claude_oauth_refresh.py.
2007
+ from mcp_oauth import ClientLock # noqa: E402
2024
2008
  CLIENT_LOCK_WAIT_S = 10.0
2025
2009
 
2026
2010
 
2027
- def client_lock_busy(d):
2028
- """A live writer holds <d>'s client lock, and still does CLIENT_LOCK_PROBE_S later."""
2029
- if ClientLock is None:
2030
- return False
2031
- path = os.path.join(d, CLIENT_LOCK)
2032
- deadline = time.monotonic() + CLIENT_LOCK_PROBE_S
2033
- while True:
2034
- try:
2035
- if time.time() - os.stat(path).st_mtime > CLIENT_LOCK_STALE_S:
2036
- return False # its holder is gone; the write takes it over
2037
- except OSError:
2038
- return False # no lock: nobody is writing
2039
- if time.monotonic() >= deadline:
2040
- return True
2041
- time.sleep(0.05)
2042
-
2043
-
2044
2011
  class FileStore:
2045
2012
  """The OAuth credential as `.credentials.json`: atomic 0600 writes. The client keeps
2046
2013
  its MCP servers' OAuth state (mcpOAuth, mcpOAuthClientConfig) in this same file, so
@@ -2141,166 +2108,13 @@ def oauth_store(d):
2141
2108
 
2142
2109
 
2143
2110
  def refresh_oauth(aid, d, store):
2144
- """Refresh a long-expired OAuth access token via the refresh-token grant and
2145
- persist the ROTATED credential atomically (0600 file, or the Keychain item it
2146
- came from). Returns the new bearer, or None (fail open: the stored credential
2147
- is never touched on failure). Failures back off via <dir>/.oauth-refresh.json —
2148
- a side file, NOT limits.json, because telemetry state must only ever reflect
2149
- real usage fetches."""
2150
- spath = os.path.join(d, '.oauth-refresh.json')
2151
- try:
2152
- doc = store.read()
2153
- o = doc.get('claudeAiOauth') if isinstance(doc, dict) else None
2154
- # Present-but-null/non-object claudeAiOauth (interrupted or reset credential
2155
- # write) must degrade THIS account only, like every other malformed input; and a
2156
- # document with no login in it at all (MCP state alone) has nothing to refresh.
2157
- if not isinstance(o, dict):
2158
- return None
2159
- except Exception:
2160
- return None
2161
- # NB: no "must have an accessToken" gate. A credential whose access token was
2162
- # cleared but whose REFRESH token is alive is exactly the shape a grant is supposed
2163
- # to recover from; requiring the dead half to be present meant such an account could
2164
- # never come back, and (with a setup token beside it) went dark for telemetry
2165
- # forever. The expiresAt gate below is what protects a live session's credential.
2166
- if not o.get('refreshToken'):
2167
- park_dead_grant(aid, d, 'no-refresh-token',
2168
- 'credential has no refresh token and its access token expired')
2169
- return None
2170
- if o.get('expiresAt', 0) / 1000.0 > now - REFRESH_MIN_EXPIRED:
2171
- return None # not expired long enough to prove no live session owns it
2172
- if o.get('refreshTokenExpiresAt', 0) / 1000.0 <= now:
2173
- # Nothing can revive this account: park it so the shim stops selecting it
2174
- # (every run under it would fail with "OAuth session expired").
2175
- park_dead_grant(aid, d, 'refresh-token-expired',
2176
- 'the refresh token itself expired; only a re-login can fix it')
2177
- return None
2178
- if not force:
2179
- try:
2180
- if json.load(open(spath)).get('retry_after', 0) > now:
2181
- return None # earlier refresh failure still backing off
2182
- except Exception:
2183
- pass
2184
-
2185
- def back_off(wait, why, denials=0):
2186
- try:
2187
- with open(spath + '.tmp', 'w') as f:
2188
- json.dump({'retry_after': int(now + wait), 'error': why,
2189
- 'denials': denials,
2190
- 'at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}, f)
2191
- os.replace(spath + '.tmp', spath)
2192
- except Exception:
2193
- pass
2194
- say(f'{aid}: oauth refresh failed ({why}); backing off {wait}s; limits left as-is')
2111
+ # Import failures skip this account through the caller's guard; never rotate
2112
+ # without the native request lock. The helper owns the full refresh transaction.
2113
+ from claude_oauth_refresh import OAuthRefresher
2114
+ return OAuthRefresher(now=now, force=force, token_url=TOKEN_URL, client_id=CLIENT_ID,
2115
+ say=say, park_dead_grant=park_dead_grant,
2116
+ clear_expired=clear_expired).refresh(aid, d, store)
2195
2117
 
2196
- if client_lock_busy(d):
2197
- # The client is writing this credential right now (it may be its own refresh of
2198
- # the same grant): no request is sent, so nothing rotates, and no backoff is noted.
2199
- say(f'{aid}: oauth refresh skipped — the client holds its credential lock '
2200
- f'({CLIENT_LOCK}); the next pass retries')
2201
- return None
2202
- started_with = o['refreshToken']
2203
- body = json.dumps({'grant_type': 'refresh_token',
2204
- 'refresh_token': o['refreshToken'],
2205
- 'client_id': CLIENT_ID}).encode()
2206
- req = urllib.request.Request(TOKEN_URL, data=body, headers={
2207
- 'Content-Type': 'application/json',
2208
- 'User-Agent': 'claude-multiacc/1.0',
2209
- })
2210
- try:
2211
- data = json.loads(urllib.request.urlopen(req, timeout=30).read().decode())
2212
- except urllib.error.HTTPError as e:
2213
- if e.code in (400, 401, 403):
2214
- # A 4xx from the TOKEN endpoint is only proof of a dead grant when the
2215
- # server says so (OAuth's invalid_grant). Everything else 4xx — a bad
2216
- # client_id, an endpoint change, a WAF page, a provider incident — would
2217
- # hit EVERY account at once, so it must never park the whole pool on the
2218
- # first try: back off, and only park after this account has been refused
2219
- # repeatedly.
2220
- body = ''
2221
- try:
2222
- body = e.read().decode('utf-8', 'replace')[:400]
2223
- except Exception:
2224
- pass
2225
- denials = 1
2226
- try:
2227
- denials = int(json.load(open(spath)).get('denials', 0)) + 1
2228
- except Exception:
2229
- pass
2230
- if 'invalid_grant' in body:
2231
- park_dead_grant(aid, d, f'refresh-denied-http-{e.code}',
2232
- 'the refresh grant was refused as invalid_grant (revoked or rotated away)')
2233
- elif denials >= 3:
2234
- park_dead_grant(aid, d, f'refresh-denied-http-{e.code}',
2235
- f'the refresh grant was refused {denials} times in a row')
2236
- back_off(REFRESH_DENIED_BACKOFF,
2237
- f'HTTP {e.code} — refresh token may be revoked; re-login needed',
2238
- denials=denials)
2239
- else:
2240
- back_off(REFRESH_FAIL_BACKOFF, f'HTTP {e.code}')
2241
- return None
2242
- except Exception as e:
2243
- back_off(REFRESH_FAIL_BACKOFF, str(e)[:200])
2244
- return None
2245
- tok = data.get('access_token') if isinstance(data, dict) else None
2246
- if not tok:
2247
- back_off(REFRESH_DENIED_BACKOFF, 'no access_token in response')
2248
- return None
2249
- o['accessToken'] = tok
2250
- # The grant ROTATES the refresh token: persist it (and both expiries) or the
2251
- # account is stranded — hence atomic write, and a loud message if it fails.
2252
- if data.get('refresh_token'):
2253
- o['refreshToken'] = data['refresh_token']
2254
- if data.get('expires_in'):
2255
- o['expiresAt'] = int((now + float(data['expires_in'])) * 1000)
2256
- else:
2257
- # No expires_in in the response: assume a conservative 1h. Leaving the old
2258
- # (past) expiresAt would make every later pass re-run the grant in a loop.
2259
- o['expiresAt'] = int((now + 3600) * 1000)
2260
- if data.get('refresh_token_expires_in'):
2261
- o['refreshTokenExpiresAt'] = int((now + float(data['refresh_token_expires_in'])) * 1000)
2262
- # CHECK-AND-SET. The grant rotates, and a live claude session refreshes the same
2263
- # file. REFRESH_MIN_EXPIRED makes that unlikely, not impossible — and losing the
2264
- # race by overwriting means the session's newer credential is destroyed. If the
2265
- # on-disk refresh token is no longer the one this grant was issued against, the
2266
- # other writer won: keep its result, discard ours. Both under the client's own lock,
2267
- # so no client write lands between the check and the write.
2268
- lock = ClientLock(d, CLIENT_LOCK_WAIT_S) if ClientLock is not None else None
2269
- try:
2270
- held = lock is not None and lock.acquire()
2271
- except Exception: # noqa: BLE001 — the rotated grant lands either way
2272
- held = False
2273
- if lock is not None and not held and os.path.isdir(lock.path):
2274
- say(f'{aid}: the client kept its credential lock ({CLIENT_LOCK}) past '
2275
- f'{int(CLIENT_LOCK_WAIT_S)}s — writing the rotated login without it')
2276
- try:
2277
- try:
2278
- disk = store.read().get('claudeAiOauth', {})
2279
- if isinstance(disk, dict) and disk.get('refreshToken') != started_with:
2280
- say(f'{aid}: credential was refreshed by something else mid-flight — '
2281
- f'keeping the newer one in the {store.kind}')
2282
- return None
2283
- except Exception:
2284
- pass
2285
- try:
2286
- # Only the login is ours: both stores write it INTO the document as it is now,
2287
- # so the MCP state the client saved meanwhile (mcpOAuth) is never rolled back.
2288
- store.write({'claudeAiOauth': o})
2289
- except Exception as e:
2290
- say(f'{aid}: token refreshed but credentials NOT persisted ({e}) — re-login may be needed')
2291
- return None
2292
- finally:
2293
- if held:
2294
- lock.release()
2295
- try:
2296
- os.remove(spath)
2297
- except OSError:
2298
- pass
2299
- # The grant answered: whatever parked this account before, it authenticates now.
2300
- if clear_expired(d):
2301
- say(f'{aid}: dead-auth marker cleared (refresh grant works again)')
2302
- say(f'{aid}: oauth access token refreshed via refresh-token grant')
2303
- return tok
2304
2118
 
2305
2119
  for acct in manifest.get('accounts', []):
2306
2120
  aid = acct['id']
@@ -2400,8 +2214,8 @@ for acct in manifest.get('accounts', []):
2400
2214
  bearer, source = tok, 'oauth'
2401
2215
  # A setup token is a LAST RESORT and only for an account that has no OAuth
2402
2216
  # credential at all. Where one exists but could not be used this pass — its
2403
- # access token expired moments ago (refresh_oauth waits REFRESH_MIN_EXPIRED to
2404
- # prove no live session owns it), a refresh backoff, or a Keychain this session
2217
+ # access token expired moments ago (refresh_oauth gives the native
2218
+ # client a five-minute grace period), a refresh backoff, or a Keychain this session
2405
2219
  # cannot open — spending the token is guaranteed to earn a 403 it can never not
2406
2220
  # earn, and that 403 used to park the whole ACCOUNT for six hours. Telemetry then
2407
2221
  # froze for an account whose OAuth would have worked on the very next pass, and
package/bin/codex CHANGED
@@ -156,6 +156,20 @@ cli_positional_at() { # argv -> prints the index of its first positional argumen
156
156
  return 1
157
157
  }
158
158
 
159
+ # Account/model capabilities are resolved only after the serving account is known.
160
+ # The helper is optional so a damaged install retains the shim's fail-open contract.
161
+ codex_native_argv() {
162
+ CODEX_SPEED_ARGV=("$@")
163
+ if [ -f "$SELF_DIR/../lib/codex_speed.sh" ]; then
164
+ . "$SELF_DIR/../lib/codex_speed.sh"
165
+ codex_speed_prepare "$@" || return $?
166
+ fi
167
+ }
168
+ codex_native_exec() {
169
+ codex_native_argv "$@" || exit $?
170
+ exec "$REAL" ${CODEX_SPEED_ARGV[@]+"${CODEX_SPEED_ARGV[@]}"}
171
+ }
172
+
159
173
  # Whether argv is `codex [global options] mcp add|remove …`.
160
174
  mcp_mutation_argv() {
161
175
  local i j
@@ -252,7 +266,7 @@ if [ -n "${CODEX_HOME:-}" ] \
252
266
  mcp_oauth_ensure "$CODEX_HOME"
253
267
  fi
254
268
  mcp_nested_mirror "$@" || true
255
- exec "$REAL" "$@"
269
+ codex_native_exec "$@"
256
270
  fi
257
271
 
258
272
  now="$(date +%s)"
@@ -1074,6 +1088,9 @@ ar_argv_ok() { # the argv codex is about to get
1074
1088
  model:*) case "$a" in ''|-*) return 1 ;; esac ;;
1075
1089
  effort:model_reasoning_effort=?*)
1076
1090
  case "${a#model_reasoning_effort=}" in *[!$AR_LOWER]*) return 1 ;; esac ;;
1091
+ effort:service_tier=*)
1092
+ case "${a#service_tier=}" in default|fast|priority|ultrafast|'"default"'|'"fast"'|'"priority"'|'"ultrafast"'|"'default'"|"'fast'"|"'priority'"|"'ultrafast'") ;; *) return 1 ;; esac ;;
1093
+ effort:features.fast_mode=true) ;;
1077
1094
  *) return 1 ;;
1078
1095
  esac
1079
1096
  want=""
@@ -1272,7 +1289,7 @@ if [ -n "${CODEX_ACCOUNT:-}" ] && [ "$AR_PROBE" != "1" ]; then
1272
1289
  mcp_ensure "$d"
1273
1290
  mcp_oauth_ensure "$d"
1274
1291
  mcp_mirror_run "$d" "$@" || true
1275
- exec "$REAL" "$@"
1292
+ codex_native_exec "$@"
1276
1293
  fi
1277
1294
  sel_log "pin-invalid account=$CODEX_ACCOUNT (no such dir; random fallback)"
1278
1295
  fi
@@ -1343,7 +1360,7 @@ if [ "${#valid[@]}" -eq 0 ]; then
1343
1360
  [ -t 2 ] && printf 'codex-multiacc: no pool account is usable (%s) — see: codex-accounts expired, then: codex-accounts relogin\n' \
1344
1361
  "${ids# }" >&2
1345
1362
  fi
1346
- exec "$REAL" "$@"
1363
+ codex_native_exec "$@"
1347
1364
  fi
1348
1365
 
1349
1366
  # ---- rotation ----------------------------------------------------------------
@@ -1657,14 +1674,14 @@ if [ "$wants_retry" = "0" ]; then
1657
1674
  fi
1658
1675
  ar_scrub_env
1659
1676
  [ "$AR_NO_DAEMON" != 1 ] || set -- --no-daemon "$@"
1660
- exec "$REAL" "$@"
1677
+ codex_native_exec "$@"
1661
1678
  fi
1662
1679
 
1663
1680
  # Retry path: buffer stdio so a retried call never double-emits partial output.
1664
1681
  mkdir -p "$ACC_ROOT/tmp" 2>/dev/null || true
1665
1682
  tmpd="$(mktemp -d "$ACC_ROOT/tmp/shim.XXXXXX" 2>/dev/null)" || {
1666
1683
  export CODEX_HOME="$pick"
1667
- exec "$REAL" "$@"
1684
+ codex_native_exec "$@"
1668
1685
  }
1669
1686
  trap 'rm -rf "$tmpd"' EXIT
1670
1687
 
@@ -1673,7 +1690,7 @@ trap 'rm -rf "$tmpd"' EXIT
1673
1690
  # nonzero — a hard failure. Verify now, fall back to plain exec if we cannot.
1674
1691
  if ! : > "$tmpd/out" 2>/dev/null || ! : > "$tmpd/err" 2>/dev/null; then
1675
1692
  export CODEX_HOME="$pick"
1676
- exec "$REAL" "$@"
1693
+ codex_native_exec "$@"
1677
1694
  fi
1678
1695
 
1679
1696
  stdin_file=""
@@ -1708,8 +1725,9 @@ cur="$pick"
1708
1725
  rc=0
1709
1726
  while :; do
1710
1727
  if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
1728
+ CODEX_HOME="$cur" codex_native_argv "$@" || exit $?
1711
1729
  CODEX_HOME="$cur" CODEX_MULTIACC_SELECTED_HOME="$cur" \
1712
- codex_retry_run "$REAL" "$@" <&3 > "$tmpd/out" 2> "$tmpd/err"
1730
+ codex_retry_run "$REAL" ${CODEX_SPEED_ARGV[@]+"${CODEX_SPEED_ARGV[@]}"} <&3 > "$tmpd/out" 2> "$tmpd/err"
1713
1731
  rc=$?
1714
1732
  exec 3<&-
1715
1733
  if [ "$rc" -ne 0 ] && [ "$attempt" -eq 1 ] \
@@ -106,6 +106,10 @@ USAGE
106
106
  every install and self-update runs it, and each limits pass refreshes its own pool's
107
107
  copy once it is an hour old. --if-stale leaves a younger copy alone; --quiet prints
108
108
  nothing. show prints the merged catalog.
109
+ codex-accounts speed --account-dir DIR --model MODEL --speed normal|fast|ultrafast [--json]
110
+ resolve speed from this account's native model catalog. Ultrafast verifies
111
+ account access through native model/list without inference; unknown or
112
+ unavailable access falls back to Fast. Never changes account configuration.
109
113
  codex-accounts post-sync (server side) seed dirs, fix perms, quick verify
110
114
  codex-accounts mcp add <name> [--provider claude|codex|both] [--scope user]
111
115
  [-e KEY=VAL]... [-H 'Header: v']... [--transport stdio|http]
@@ -144,6 +148,8 @@ ENV
144
148
  'none' = local-only, nothing is pushed anywhere
145
149
  CODEX_MULTIACC_SYNC_ROOT / _SYNC_REPO remote pool root / addon repo for it
146
150
  CODEX_ACCOUNT pin the shim to one account
151
+ CODEX_SPEED normal|fast|ultrafast for this launch; unavailable Ultrafast uses Fast
152
+ CODEX_SPEED_MODEL model hint for app-server launches that choose their model over RPC
147
153
  CODEX_SHIM_RETRY=0 disable the `codex exec` auto-retry
148
154
  CODEX_MULTIACC_DISABLE=1 bypass the shim entirely
149
155
  CODEX_MULTIACC_AUTO_RESET=0 disable automatic usage-reset redemption
@@ -156,6 +162,12 @@ EOF
156
162
 
157
163
  require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run claude-multiacc install first"; }
158
164
 
165
+ cmd_speed() {
166
+ local native
167
+ native="$(find_real_codex "$_self" 2>/dev/null)" || native=""
168
+ "$PYBIN" "$LIB_DIR/codex_speed.py" --native-cli "$native" "$@"
169
+ }
170
+
159
171
  cmd_configure() {
160
172
  require_manifest
161
173
  local apply=0 check=0 arg rc
@@ -257,11 +269,11 @@ PYEOF
257
269
  # later contender legitimately re-took.
258
270
  login_lock_acquire() { # $1 = lock dir; nonzero when someone else holds it
259
271
  local L="$1"
260
- if ! mkdir "$L" 2>/dev/null; then
272
+ if ! atomic_mkdir "$L" 2>/dev/null; then
261
273
  [ $(( $(epoch_now) - $(file_mtime "$L") )) -gt 1800 ] || return 1
262
274
  mv "$L" "$L.stale.$$" 2>/dev/null || return 1
263
275
  rm -rf "$L.stale.$$" 2>/dev/null
264
- mkdir "$L" 2>/dev/null || return 1
276
+ atomic_mkdir "$L" 2>/dev/null || return 1
265
277
  fi
266
278
  : > "$L/owner.$$" 2>/dev/null || true
267
279
  return 0
@@ -2477,7 +2489,7 @@ PYEOF
2477
2489
  # distribution for as long as the arg loops rejected the flag. An UNKNOWN verb must still
2478
2490
  # FAIL, or the probe stops meaning what it says — so this list must hold exactly the verbs
2479
2491
  # the dispatcher below implements, and a test pins that both ways.
2480
- _KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp model-catalog"
2492
+ _KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp model-catalog speed"
2481
2493
  case " $_KNOWN_VERBS " in
2482
2494
  *" ${1:-help} "*)
2483
2495
  # ...but never past a literal `--`: from there on the words belong to an MCP
@@ -2510,6 +2522,7 @@ case "${1:-help}" in
2510
2522
  init-pool) shift; cmd_init_pool "$@" ;;
2511
2523
  mcp) shift; cmd_mcp "$@" ;;
2512
2524
  model-catalog) shift; cmd_model_catalog "$@" ;;
2525
+ speed) shift; cmd_speed "$@" ;;
2513
2526
  help|--help|-h) usage ;;
2514
2527
  *) usage; exit 1 ;;
2515
2528
  esac
@@ -225,7 +225,16 @@ The prompt is submitted automatically:
225
225
  - **To replace the whole text**, set `CLAUDE_MULTIACC_AUTORESUME_PROMPT` or
226
226
  `CODEX_MULTIACC_AUTORESUME_PROMPT`.
227
227
 
228
- ### Model and effort
228
+ ### Model, effort, and Codex speed
229
+
230
+ Codex relaunches also carry the most recently saved `service_tier` from the
231
+ thread's own settings or turn context. Normal, Fast, and Ultrafast remain the same
232
+ across account rotation; Ultrafast becomes Fast when the selected destination
233
+ account/model does not advertise it. Claude receives no speed flag. The shim
234
+ updates the named thread's tier through native `thread/resume` before launching
235
+ the CLI, because native resume restores a saved tier ahead of `-c` configuration.
236
+ See [Session speed](CODEX.md#session-speed) for capability discovery and the
237
+ `CODEX_SPEED` launch override.
229
238
 
230
239
  The watcher reads the model and effort from the session's own history when it decides to
231
240
  relaunch, so a `/model` or `/effort` switch made after the launch is honoured:
package/docs/CODEX.md CHANGED
@@ -92,6 +92,45 @@ Codex-specific notes:
92
92
  - **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
93
93
  addon's no-API-keys rule.
94
94
 
95
+ ## Session speed
96
+
97
+ Set `CODEX_SPEED=normal`, `fast`, or `ultrafast` for a Codex launch. Normal explicitly
98
+ uses the standard tier, even when an account's configuration defaults to Fast.
99
+ The setting also works with `CODEX_ACCOUNT` and `CODEX_HOME` pins. Explicit native
100
+ `-c service_tier=...` overrides take precedence. Claude has no corresponding option.
101
+
102
+ ```bash
103
+ CODEX_SPEED=fast codex
104
+ CODEX_SPEED=ultrafast codex exec -m gpt-6.1-sol 'Review the changed code'
105
+ codex-accounts speed --account-dir ~/.codex-accounts/acct-18 --model gpt-6.1-sol --speed ultrafast --json
106
+ ```
107
+
108
+ The shim reads **the selected account's** native model catalog, never an e-mail or
109
+ a plan-name guess. Ultrafast falls back to Fast when that account/model does not
110
+ advertise it. Every Ultrafast launch verifies the native account's `model/list`
111
+ with no inference, so even a copied cache cannot grant another account access;
112
+ an inconclusive read falls back to Fast.
113
+ Catalog probes use a temporary SQLite index, preserving the account's normal
114
+ session-index work. The inspection command above uses the same native check for
115
+ Ultrafast.
116
+
117
+ Quota retries resolve the speed again for the next account. Automatic quota
118
+ resumes carry the speed recorded in the session history. For those resumes and
119
+ explicit speed requests on a named session UUID, the shim applies the resolved
120
+ tier through native `thread/resume` before launching the resumed CLI: Codex
121
+ currently restores its saved tier ahead of process configuration. That
122
+ preparation sends no turn and preserves the conversation. A failed preparation
123
+ stops the launch instead of silently selecting another speed.
124
+
125
+ App-server callers may supply `CODEX_SPEED_MODEL` when their model is chosen over
126
+ RPC. Read the resolved `config.service_tier` with native `config/read` and pass it
127
+ as `serviceTier` on `thread/start`, `thread/resume`, and `turn/start`; otherwise
128
+ saved thread settings can override the process configuration.
129
+
130
+ The [official speed documentation](https://learn.chatgpt.com/docs/agent-configuration/speed)
131
+ describes Fast's higher usage and Ultrafast access for Pro $500 and eligible
132
+ Enterprise/Edu plans. Actual account/model capabilities remain authoritative.
133
+
95
134
  ## Spendable Codex credits
96
135
 
97
136
  The existing authenticated usage GET also supplies the account's spendable Codex