@dst-justin/relay 2.6.0 → 2.8.0

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 (3) hide show
  1. package/README.md +8 -0
  2. package/package.json +2 -2
  3. package/relay +90 -212
package/README.md CHANGED
@@ -288,6 +288,14 @@ Sessions live in `~/.claude/projects/` and are shared across all accounts — af
288
288
 
289
289
  ## Changelog
290
290
 
291
+ ### v2.8.0 — 2026-09-06
292
+ - **Removed** `relay provider use <name>` and `relay provider off` — the global "always-on" provider switch (which mutated `${CLAUDE_DIR}/settings.json`) is gone entirely. All provider usage now goes through `relay run <name>`, which never touches global state.
293
+ - Add Codex CLI as an alternate `relay run` exec target: `relay provider add <name> --codex` stores `agent=codex` (requires `--model`; `--discover-models`/`--subagent-model` are ignored with a warning), and `relay run <name> --codex`/`--claude` override the stored agent for a single invocation.
294
+ - Codex runs pass the LiteLLM `base_url`/`model` via `-c` TOML overrides; the auth token is handed to the `codex` process only through a child-process env var, never argv.
295
+
296
+ ### v2.7.0 — 2026-09-04
297
+ - `relay provider add` now turns on gateway model discovery by default (previously required `--discover-models`); opt out with `--no-discover-models`.
298
+
291
299
  ### v2.6.0 — 2026-09-04
292
300
  - Add `relay run -c`/`--continue`/`-r`/`--resume` — replays the last account/provider `relay run` used in the current directory, so a crashed or Ctrl-C'd one-off provider session can resume without retyping the provider name.
293
301
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dst-justin/relay",
3
- "version": "2.6.0",
4
- "description": "Multi-account switcher for Claude Code — instant credential swap across macOS, Linux, and Windows",
3
+ "version": "2.8.0",
4
+ "description": "Multi-account switcher for Claude Code — instant credential swap across macOS, Linux, and Windows, with one-off LiteLLM provider runs targeting Claude Code or Codex CLI",
5
5
  "bin": {
6
6
  "relay": "./relay.js"
7
7
  },
package/relay CHANGED
@@ -15,12 +15,11 @@ CURRENT_FILE="${RELAY_DIR}/current"
15
15
  ORDER_FILE="${RELAY_DIR}/order"
16
16
  UPDATE_CACHE="${RELAY_DIR}/.update_cache"
17
17
  PROVIDERS_STORE="${RELAY_DIR}/providers"
18
- ACTIVE_PROVIDER_FILE="${RELAY_DIR}/active_provider"
19
- SETTINGS_ENV_SNAPSHOT="${RELAY_DIR}/settings_env_snapshot.json"
20
18
  CLAUDE_DIR="${HOME}/.claude"
21
19
  CLAUDE_JSON="${HOME}/.claude.json"
22
20
  CLAUDE_SETTINGS="${CLAUDE_DIR}/settings.json"
23
21
  REAL_CLAUDE=$(command -v claude 2>/dev/null || echo "")
22
+ REAL_CODEX=$(command -v codex 2>/dev/null || echo "")
24
23
  # macOS: /usr/bin/python3 uses the system TLS stack (correct certs);
25
24
  # /usr/local/bin/python3 (Homebrew/standalone) often lacks bundled certs → SSL failures
26
25
  if [[ "$(uname)" == "Darwin" ]] && [[ -x "/usr/bin/python3" ]]; then
@@ -106,7 +105,6 @@ account_exists() { [[ -f "$(account_creds "$1")" ]]; }
106
105
 
107
106
  provider_file() { echo "${PROVIDERS_STORE}/$1.json"; }
108
107
  provider_exists() { [[ -f "$(provider_file "$1")" ]]; }
109
- active_provider_name() { [[ -f "${ACTIVE_PROVIDER_FILE}" ]] && cat "${ACTIVE_PROVIDER_FILE}" || echo ""; }
110
108
 
111
109
  # list providers alphabetically, one per line (bash 3.2 compatible)
112
110
  list_provider_names() {
@@ -127,6 +125,14 @@ _provider_discover() {
127
125
  "${PY}" -c 'import json,sys; print("1" if json.load(open(sys.argv[1])).get("discover_models") else "")' "$(provider_file "$1")"
128
126
  }
129
127
 
128
+ # escape a value for embedding as a TOML basic string in a `codex -c key="value"` arg
129
+ _toml_str() {
130
+ local s="$1"
131
+ s="${s//\\/\\\\}"
132
+ s="${s//\"/\\\"}"
133
+ printf '"%s"' "${s}"
134
+ }
135
+
130
136
  # list accounts in canonical add-order, one per line (bash 3.2 compatible).
131
137
  # Self-healing: drops names whose credential file is gone, appends any
132
138
  # credential file not yet tracked (alphabetically), and rewrites ORDER_FILE.
@@ -293,144 +299,11 @@ require_claude() {
293
299
  exit 1
294
300
  }
295
301
 
296
- # Fully replace the 4 managed keys with exactly the given KEY=VALUE pairs.
297
- # Snapshots the pre-relay original values/absences the first time this is
298
- # called since the last full restore (SETTINGS_ENV_SNAPSHOT doesn't exist).
299
- # Assumes the caller already holds the credential lock.
300
- _settings_env_activate_locked() {
301
- RELAY_SETTINGS_PATH="${CLAUDE_SETTINGS}" \
302
- RELAY_SNAPSHOT_PATH="${SETTINGS_ENV_SNAPSHOT}" \
303
- RELAY_ENV_PAIRS="$(printf '%s\n' "$@")" \
304
- "${PY}" <<'EOF'
305
- import json, os, sys, tempfile
306
-
307
- MANAGED = ["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_MODEL", "CLAUDE_CODE_SUBAGENT_MODEL", "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"]
308
-
309
- def load_json(p, default):
310
- if not os.path.exists(p):
311
- return default
312
- raw = open(p).read().strip()
313
- return json.loads(raw) if raw else default
314
-
315
- def atomic_write(p, data, mode=0o600):
316
- d = os.path.dirname(p) or "."
317
- fd, tmp = tempfile.mkstemp(dir=d, prefix=".relay-tmp-")
318
- try:
319
- with os.fdopen(fd, "w") as f:
320
- json.dump(data, f, indent=2)
321
- f.write("\n")
322
- os.chmod(tmp, mode)
323
- os.replace(tmp, p)
324
- except Exception:
325
- try:
326
- os.unlink(tmp)
327
- except OSError:
328
- pass
329
- raise
330
-
331
- path = os.environ["RELAY_SETTINGS_PATH"]
332
- snapshot_path = os.environ["RELAY_SNAPSHOT_PATH"]
333
- pairs = [p for p in os.environ.get("RELAY_ENV_PAIRS", "").split("\n") if p]
334
-
335
- try:
336
- data = load_json(path, {})
337
- except json.JSONDecodeError as e:
338
- print(f"settings.json is not valid JSON: {e}", file=sys.stderr)
339
- sys.exit(1)
340
- if not isinstance(data, dict):
341
- print("settings.json root is not a JSON object", file=sys.stderr)
342
- sys.exit(1)
343
-
344
- env = data.get("env")
345
- if not isinstance(env, dict):
346
- env = {}
347
-
348
- if not os.path.exists(snapshot_path):
349
- snapshot = {k: env.get(k) for k in MANAGED}
350
- atomic_write(snapshot_path, snapshot, 0o600)
351
-
352
- for k in MANAGED:
353
- env.pop(k, None)
354
- for pair in pairs:
355
- k, _, v = pair.partition("=")
356
- env[k] = v
357
- data["env"] = env
358
- atomic_write(path, data, 0o600)
359
- EOF
360
- }
361
- _settings_env_activate() { with_credential_lock _settings_env_activate_locked "$@"; }
362
-
363
- # Restore the snapshotted values/absences for the 4 managed keys (or clear
364
- # them if no snapshot exists, as a defensive fallback), then delete the
365
- # snapshot. Assumes the caller already holds the credential lock.
366
- _settings_env_restore_locked() {
367
- "${PY}" - "${CLAUDE_SETTINGS}" "${SETTINGS_ENV_SNAPSHOT}" <<'EOF'
368
- import json, os, sys, tempfile
369
-
370
- MANAGED = ["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_MODEL", "CLAUDE_CODE_SUBAGENT_MODEL", "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"]
371
-
372
- def load_json(p, default):
373
- if not os.path.exists(p):
374
- return default
375
- raw = open(p).read().strip()
376
- return json.loads(raw) if raw else default
377
-
378
- def atomic_write(p, data, mode=0o600):
379
- d = os.path.dirname(p) or "."
380
- fd, tmp = tempfile.mkstemp(dir=d, prefix=".relay-tmp-")
381
- try:
382
- with os.fdopen(fd, "w") as f:
383
- json.dump(data, f, indent=2)
384
- f.write("\n")
385
- os.chmod(tmp, mode)
386
- os.replace(tmp, p)
387
- except Exception:
388
- try:
389
- os.unlink(tmp)
390
- except OSError:
391
- pass
392
- raise
393
-
394
- path, snapshot_path = sys.argv[1], sys.argv[2]
395
-
396
- try:
397
- data = load_json(path, {})
398
- except json.JSONDecodeError as e:
399
- print(f"settings.json is not valid JSON: {e}", file=sys.stderr)
400
- sys.exit(1)
401
- if not isinstance(data, dict):
402
- print("settings.json root is not a JSON object", file=sys.stderr)
403
- sys.exit(1)
404
-
405
- env = data.get("env")
406
- if not isinstance(env, dict):
407
- env = {}
408
-
409
- changed = False
410
- if os.path.exists(snapshot_path):
411
- snapshot = load_json(snapshot_path, {})
412
- for k in MANAGED:
413
- v = snapshot.get(k)
414
- if v is None:
415
- changed = env.pop(k, None) is not None or changed
416
- else:
417
- changed = env.get(k) != v or changed
418
- env[k] = v
419
- else:
420
- for k in MANAGED:
421
- if k in env:
422
- del env[k]
423
- changed = True
424
-
425
- if changed:
426
- data["env"] = env
427
- atomic_write(path, data, 0o600)
428
-
429
- if os.path.exists(snapshot_path):
430
- os.unlink(snapshot_path)
431
- EOF
302
+ require_codex() {
303
+ [[ -n "${REAL_CODEX}" ]] && return 0
304
+ err "codex not found install it with: npm i -g @openai/codex"
305
+ exit 1
432
306
  }
433
- _settings_env_restore() { with_credential_lock _settings_env_restore_locked; }
434
307
 
435
308
  # ══════════════════════════════════════════════════════════════════
436
309
  # Python core: parallel usage fetch + table rendering
@@ -687,11 +560,6 @@ _do_switch_locked() {
687
560
  local name="$1"
688
561
  local current; current=$(current_name)
689
562
 
690
- if [[ -n "$(active_provider_name)" ]]; then
691
- _settings_env_restore_locked
692
- rm -f "${ACTIVE_PROVIDER_FILE}"
693
- fi
694
-
695
563
  if [[ "${current}" == "${name}" ]]; then
696
564
  ok "Already on account '${B}${name}${R}'"
697
565
  return 0
@@ -731,16 +599,8 @@ _sync_current_creds() {
731
599
  [[ -n "${live}" ]] && printf '%s' "${live}" > "$(account_creds "${cur}")"
732
600
  }
733
601
 
734
- _provider_banner() {
735
- local active; active=$(active_provider_name)
736
- [[ -z "${active}" ]] && return 0
737
- local base_url; base_url=$(_provider_field "${active}" base_url)
738
- printf "\n ${YL}⚡ litellm:${R} ${B}%s${R} ${D}(%s)${R}\n" "${active}" "${base_url}"
739
- }
740
-
741
602
  cmd_quick() {
742
603
  _check_update_bg
743
- _provider_banner
744
604
  _sync_current_creds
745
605
  render_table quick "${CREDS_STORE}" "${META_STORE}" "$(current_name)" "$@"
746
606
  _show_update_notice
@@ -786,7 +646,6 @@ cmd_list() {
786
646
  }
787
647
 
788
648
  _cmd_status_once() {
789
- _provider_banner
790
649
  _sync_current_creds
791
650
  local current; current=$(current_name)
792
651
  hdr "Current Status"
@@ -966,7 +825,7 @@ PYEOF
966
825
 
967
826
  cmd_provider_add() {
968
827
  local name="${1:-}"
969
- [[ -z "${name}" ]] && { err "usage: relay provider add <name> --base-url <url> --token <token> [--model <model>] [--subagent-model <model>] [--discover-models]"; exit 1; }
828
+ [[ -z "${name}" ]] && { err "usage: relay provider add <name> --base-url <url> --token <token> [--model <model>] [--subagent-model <model>] [--no-discover-models] [--codex]"; exit 1; }
970
829
  shift
971
830
  case "${name}" in
972
831
  *[!a-zA-Z0-9_-]*) err "name must contain only letters, numbers, underscores, or hyphens"; exit 1 ;;
@@ -981,7 +840,10 @@ cmd_provider_add() {
981
840
  return 0
982
841
  fi
983
842
 
984
- local base_url="" token="" model="" subagent_model="" discover=0
843
+ # Discovery defaults on: without a pinned --model there's nothing else to
844
+ # route with, and with one it's still useful for `relay provider list` /
845
+ # future switches, so opt-out (--no-discover-models) rather than opt-in.
846
+ local base_url="" token="" model="" subagent_model="" discover=1 agent="claude"
985
847
  while [[ $# -gt 0 ]]; do
986
848
  case "$1" in
987
849
  --base-url) base_url="${2:-}"; shift 2 ;;
@@ -989,6 +851,8 @@ cmd_provider_add() {
989
851
  --model) model="${2:-}"; shift 2 ;;
990
852
  --subagent-model) subagent_model="${2:-}"; shift 2 ;;
991
853
  --discover-models) discover=1; shift ;;
854
+ --no-discover-models) discover=0; shift ;;
855
+ --codex) agent="codex"; shift ;;
992
856
  *) err "unknown option: $1"; exit 1 ;;
993
857
  esac
994
858
  done
@@ -1000,12 +864,30 @@ cmd_provider_add() {
1000
864
  *) err "--base-url must start with http:// or https://"; exit 1 ;;
1001
865
  esac
1002
866
 
867
+ # Codex CLI has no gateway-model-discovery equivalent — it always needs an
868
+ # explicit model, and CLAUDE_CODE_SUBAGENT_MODEL is a Claude-Code-only concept.
869
+ if [[ "${agent}" == "codex" ]]; then
870
+ if [[ -z "${model}" ]]; then
871
+ err "--codex requires --model <model> — Codex CLI has no model-discovery fallback"
872
+ exit 1
873
+ fi
874
+ if [[ "${discover}" == "1" ]]; then
875
+ warn "--discover-models has no effect with --codex (no gateway model discovery in Codex) — ignoring"
876
+ discover=0
877
+ fi
878
+ if [[ -n "${subagent_model}" ]]; then
879
+ warn "--subagent-model has no effect with --codex (Claude-Code-only) — ignoring"
880
+ subagent_model=""
881
+ fi
882
+ fi
883
+
1003
884
  RELAY_PROVIDER_PATH="$(provider_file "${name}")" \
1004
885
  RELAY_PROVIDER_BASE_URL="${base_url}" \
1005
886
  RELAY_PROVIDER_TOKEN="${token}" \
1006
887
  RELAY_PROVIDER_MODEL="${model}" \
1007
888
  RELAY_PROVIDER_SUBAGENT_MODEL="${subagent_model}" \
1008
889
  RELAY_PROVIDER_DISCOVER="${discover}" \
890
+ RELAY_PROVIDER_AGENT="${agent}" \
1009
891
  "${PY}" -c '
1010
892
  import json, os
1011
893
  path = os.environ["RELAY_PROVIDER_PATH"]
@@ -1014,6 +896,7 @@ token = os.environ["RELAY_PROVIDER_TOKEN"]
1014
896
  model = os.environ.get("RELAY_PROVIDER_MODEL", "")
1015
897
  subagent_model = os.environ.get("RELAY_PROVIDER_SUBAGENT_MODEL", "")
1016
898
  discover = os.environ.get("RELAY_PROVIDER_DISCOVER", "")
899
+ agent = os.environ.get("RELAY_PROVIDER_AGENT", "")
1017
900
  d = {"base_url": base_url, "auth_token": token}
1018
901
  if model:
1019
902
  d["model"] = model
@@ -1021,6 +904,8 @@ if subagent_model:
1021
904
  d["subagent_model"] = subagent_model
1022
905
  if discover == "1":
1023
906
  d["discover_models"] = True
907
+ if agent == "codex":
908
+ d["agent"] = "codex"
1024
909
  with open(path, "w") as f:
1025
910
  json.dump(d, f)
1026
911
  os.chmod(path, 0o600)
@@ -1036,59 +921,21 @@ cmd_provider_list() {
1036
921
  warn "No providers configured — add one with: relay provider add <name> --base-url <url> --token <token>"
1037
922
  return 0
1038
923
  fi
1039
- local active; active=$(active_provider_name)
1040
- local name base_url model subagent_model info
924
+ local name base_url model subagent_model agent info
1041
925
  while IFS= read -r name; do
1042
926
  [[ -z "${name}" ]] && continue
1043
927
  base_url=$(_provider_field "${name}" base_url)
1044
928
  model=$(_provider_field "${name}" model)
1045
929
  subagent_model=$(_provider_field "${name}" subagent_model)
930
+ agent=$(_provider_field "${name}" agent)
1046
931
  info="${base_url}"
1047
932
  [[ -n "${model}" ]] && info="${info} model=${model}"
1048
933
  [[ -n "${subagent_model}" ]] && info="${info} subagent_model=${subagent_model}"
1049
- if [[ "${name}" == "${active}" ]]; then
1050
- printf " ${GR}${B}✓ %-16s${R} ${D}%s${R}\n" "${name}" "${info}"
1051
- else
1052
- printf " %-16s ${D}%s${R}\n" "${name}" "${info}"
1053
- fi
934
+ [[ "${agent}" == "codex" ]] && info="${info} agent=codex"
935
+ printf " %-16s ${D}%s${R}\n" "${name}" "${info}"
1054
936
  done <<< "${names}"
1055
937
  }
1056
938
 
1057
- cmd_provider_use() {
1058
- local name="${1:-}"
1059
- [[ -z "${name}" ]] && { err "usage: relay provider use <name>"; exit 1; }
1060
- provider_exists "${name}" || { err "Provider '${name}' not found"; exit 1; }
1061
-
1062
- local base_url token model subagent_model discover
1063
- base_url=$(_provider_field "${name}" base_url)
1064
- token=$(_provider_field "${name}" auth_token)
1065
- model=$(_provider_field "${name}" model)
1066
- subagent_model=$(_provider_field "${name}" subagent_model)
1067
- discover=$(_provider_discover "${name}")
1068
-
1069
- local pairs=("ANTHROPIC_BASE_URL=${base_url}" "ANTHROPIC_AUTH_TOKEN=${token}")
1070
- [[ -n "${model}" ]] && pairs+=("ANTHROPIC_MODEL=${model}")
1071
- [[ -n "${subagent_model}" ]] && pairs+=("CLAUDE_CODE_SUBAGENT_MODEL=${subagent_model}")
1072
- [[ -n "${discover}" ]] && pairs+=("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1")
1073
-
1074
- _settings_env_activate "${pairs[@]}" || { err "Failed to update ${CLAUDE_SETTINGS}"; exit 1; }
1075
- printf '%s' "${name}" > "${ACTIVE_PROVIDER_FILE}"
1076
-
1077
- printf "\n ${GR}${B}⚡ provider active → %s${R} ${D}%s${R}\n" "${name}" "${base_url}"
1078
- printf " ${D}Active sessions pick up the switch on next message. New session: ${CY}claude -c${R}\n\n"
1079
- }
1080
-
1081
- cmd_provider_off() {
1082
- local active; active=$(active_provider_name)
1083
- if [[ -z "${active}" ]]; then
1084
- warn "No provider is currently active"
1085
- return 0
1086
- fi
1087
- _settings_env_restore || { err "Failed to update ${CLAUDE_SETTINGS}"; exit 1; }
1088
- rm -f "${ACTIVE_PROVIDER_FILE}"
1089
- ok "Provider mode off — subscription account resumes"
1090
- }
1091
-
1092
939
  cmd_provider_remove() {
1093
940
  local name="${1:-}"
1094
941
  [[ -z "${name}" ]] && { err "usage: relay provider remove <name>"; exit 1; }
@@ -1096,9 +943,6 @@ cmd_provider_remove() {
1096
943
  printf "\n ${YL}Delete provider '${B}${name}${R}${YL}'? (y/N) ${R}"
1097
944
  read -r c
1098
945
  [[ "${c}" = "y" || "${c}" = "Y" ]] || { log "cancelled"; return 0; }
1099
- if [[ "$(active_provider_name)" == "${name}" ]]; then
1100
- cmd_provider_off
1101
- fi
1102
946
  rm -f "$(provider_file "${name}")"
1103
947
  ok "Deleted provider '${name}'"
1104
948
  }
@@ -1123,9 +967,7 @@ _run_last_name_for_cwd() {
1123
967
 
1124
968
  cmd_run() {
1125
969
  local name="${1:-}"
1126
- [[ -z "${name}" ]] && { err "usage: relay run <name> [-- <claude args...>]"; exit 1; }
1127
-
1128
- require_claude
970
+ [[ -z "${name}" ]] && { err "usage: relay run <name> [--codex|--claude] [-- <args...>]"; exit 1; }
1129
971
 
1130
972
  case "${name}" in
1131
973
  -c|--continue|-r|--resume)
@@ -1136,15 +978,30 @@ cmd_run() {
1136
978
  ;;
1137
979
  *)
1138
980
  shift
1139
- [[ "${1:-}" == "--" ]] && shift
1140
981
  ;;
1141
982
  esac
1142
983
 
984
+ local agent_override=""
985
+ case "${1:-}" in
986
+ --codex) agent_override="codex"; shift ;;
987
+ --claude) agent_override="claude"; shift ;;
988
+ esac
989
+ [[ "${1:-}" == "--" ]] && shift
990
+
1143
991
  if account_exists "${name}"; then
992
+ if [[ "${agent_override}" == "codex" ]]; then
993
+ err "'${name}' is an account (Claude OAuth credential) — --codex is not valid for accounts"
994
+ exit 1
995
+ fi
996
+ require_claude
1144
997
  _run_record_last "${name}"
1145
998
  do_switch "${name}"
1146
999
  exec "${REAL_CLAUDE}" "$@"
1147
1000
  elif provider_exists "${name}"; then
1001
+ local agent="${agent_override}"
1002
+ [[ -z "${agent}" ]] && agent=$(_provider_field "${name}" agent)
1003
+ [[ -z "${agent}" ]] && agent="claude"
1004
+
1148
1005
  _run_record_last "${name}"
1149
1006
  local base_url token model subagent_model discover settings_file
1150
1007
  base_url=$(_provider_field "${name}" base_url)
@@ -1153,6 +1010,28 @@ cmd_run() {
1153
1010
  subagent_model=$(_provider_field "${name}" subagent_model)
1154
1011
  discover=$(_provider_discover "${name}")
1155
1012
 
1013
+ if [[ "${agent}" == "codex" ]]; then
1014
+ require_codex
1015
+ [[ -z "${model}" ]] && { err "Provider '${name}' has no --model set — Codex requires an explicit model (no discovery fallback)"; exit 1; }
1016
+
1017
+ local base_url_toml model_toml
1018
+ base_url_toml=$(_toml_str "${base_url}")
1019
+ model_toml=$(_toml_str "${model}")
1020
+
1021
+ local codex_args=(
1022
+ -c model_provider="relay"
1023
+ -c model_providers.relay.name="relay"
1024
+ -c "model_providers.relay.base_url=${base_url_toml}"
1025
+ -c model_providers.relay.env_key="RELAY_CODEX_API_KEY"
1026
+ -c model_providers.relay.wire_api="responses"
1027
+ -c model_providers.relay.stream_idle_timeout_ms=600000
1028
+ -c "model=${model_toml}"
1029
+ )
1030
+ RELAY_CODEX_API_KEY="${token}" exec "${REAL_CODEX}" "${codex_args[@]}" "$@"
1031
+ fi
1032
+
1033
+ require_claude
1034
+
1156
1035
  # Opportunistic cleanup: a prior `relay run <provider>` can't clean up
1157
1036
  # after itself (exec replaces the process, so no trap ever fires) —
1158
1037
  # sweep anything old enough that the session which created it has
@@ -2802,8 +2681,9 @@ cmd_help() {
2802
2681
  printf " ${B}One-off runs${R}\n"
2803
2682
  printf " %-42s %s\n" " relay run <name>" "one-off session on an account or provider, then exec claude"
2804
2683
  printf " %-42s %s\n" " relay run <name> -- <args>" "forward extra args to claude itself, e.g. -- --resume"
2684
+ printf " %-42s %s\n" " relay run <name> --codex" "force Codex CLI for this run, regardless of the provider's stored agent"
2685
+ printf " %-42s %s\n" " relay run <name> --claude" "force Claude Code for this run (override a provider's stored --codex)"
2805
2686
  printf " %-42s %s\n" " relay run -c|--resume" "replay last account/provider used by 'relay run' in this dir"
2806
- printf " %-42s %s\n" " (leaves global switch/provider state untouched — safe for concurrent sessions)"
2807
2687
  echo ""
2808
2688
 
2809
2689
  printf " ${B}LiteLLM providers${R}\n"
@@ -2813,10 +2693,10 @@ cmd_help() {
2813
2693
  printf " %-42s %s\n" " --model <model>" "optional: pin ANTHROPIC_MODEL to this alias"
2814
2694
  printf " %-42s %s\n" " --subagent-model <model>" "optional: pin CLAUDE_CODE_SUBAGENT_MODEL"
2815
2695
  printf " %-42s %s\n" " " "(overrides every subagent's own model: frontmatter)"
2816
- printf " %-42s %s\n" " --discover-models" "optional: enable gateway model discovery instead of pinning"
2696
+ printf " %-42s %s\n" " --no-discover-models" "opt out: gateway model discovery is on by default"
2697
+ printf " %-42s %s\n" " --codex" "target OpenAI's Codex CLI instead of Claude Code for this provider"
2698
+ printf " %-42s %s\n" " " "(requires --model; --discover-models / --subagent-model don't apply)"
2817
2699
  printf " %-42s %s\n" " relay provider list" "list configured providers + their settings"
2818
- printf " %-42s %s\n" " relay provider use <name>" "route all future sessions through this provider"
2819
- printf " %-42s %s\n" " relay provider off" "stop routing through a provider, resume subscription account"
2820
2700
  printf " %-42s %s\n" " relay provider remove <name>" "delete a provider (prompts for confirmation)"
2821
2701
  echo ""
2822
2702
 
@@ -2897,10 +2777,8 @@ case "${CMD}" in
2897
2777
  case "${sub}" in
2898
2778
  add) cmd_provider_add "$@" ;;
2899
2779
  list|ls) cmd_provider_list ;;
2900
- use) cmd_provider_use "$@" ;;
2901
- off) cmd_provider_off ;;
2902
2780
  remove|rm) cmd_provider_remove "$@" ;;
2903
- *) err "usage: relay provider <add|list|use|off|remove> ..."; exit 1 ;;
2781
+ *) err "usage: relay provider <add|list|remove> ..."; exit 1 ;;
2904
2782
  esac ;;
2905
2783
  run) cmd_run "$@" ;;
2906
2784
  sessions|sess) cmd_sessions ;;