ai-switch-cli 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-switch-cli
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: One-command provider and model switching for Claude Code and Codex, with session-history checks
5
5
  Author: ZidongS
6
6
  License: MIT
@@ -78,13 +78,26 @@ Select `glm` or `deepseek`, enter the API key, and activate the generated config
78
78
  ai-switch use glm
79
79
  ```
80
80
 
81
- `use` lists the models the profile offers and remembers your choice:
81
+ `use` activates the profile and does **not** ask anything: the profile's whole model list is published, so you
82
+ pick the model inside the agent with `/model` (Codex) or its Opus/Sonnet/Haiku categories (Claude Code). The
83
+ default for new sessions is the one you last used, or the profile default. Add `--choose` if you want to be
84
+ asked, or `-m/--model` to set the default directly.
82
85
 
83
86
  ```text
84
- This profile provides 2 models:
87
+ Active profile: glm
88
+ Default model: glm-5.3 (new sessions)
89
+ Published models: 3 - switch any time with /model inside codex (Claude Code uses its
90
+ Opus/Sonnet/Haiku mappings), or change the default with -m/--model.
91
+ ```
92
+
93
+ With `--choose` you get the picker instead:
94
+
95
+ ```text
96
+ This profile provides 3 models:
85
97
  1) glm-5.3 GLM-5.3 flagship (1M context) [default]
86
- 2) glm-5.3-flash GLM-5.3 flash (fast, 1M context)
87
- Select model [1-2, Enter=glm-5.3, q=cancel]:
98
+ 2) glm-5.3-flash GLM-5.3 Flash (fast, text+image, 1M context)
99
+ 3) glm-5-turbo GLM-5 Turbo (agent-optimized, 200K context)
100
+ Select model [1-3, Enter=glm-5.3, q=cancel]:
88
101
  ```
89
102
 
90
103
  For scripts and servers, choose without prompting (name, unique prefix, or index):
@@ -93,8 +106,11 @@ For scripts and servers, choose without prompting (name, unique prefix, or index
93
106
  ai-switch use glm --model glm-5.3-flash
94
107
  ai-switch use glm -m 2 -y
95
108
  ai-switch use glm --dry-run # show what would change
109
+ ai-switch use glm -m glm-5.3 --pin # publish only this model (see below)
96
110
  ```
97
111
 
112
+ Selecting a model sets the **default for new sessions** in both agents, and the profile's other models stay in the catalogue so you can still switch inside the agent. That means an in-app `/model` choice is a per-session override — and Codex writes its own choice back to `~/.codex/config.toml` when it exits, so the two agents can drift apart without anything else noticing; `ai-switch doctor` reports exactly that. If you want an activation to be exclusive instead, add `--pin`: the published catalogue then holds only the selected model, so Codex's own picker has nothing else to switch to (the next `ai-switch use` without `--pin` restores the full menu).
113
+
98
114
  Edit `~/.codex/config.toml` and `~/.claude/settings.json` for another provider, then save that configuration as a second profile:
99
115
 
100
116
  ```bash
@@ -64,13 +64,26 @@ Select `glm` or `deepseek`, enter the API key, and activate the generated config
64
64
  ai-switch use glm
65
65
  ```
66
66
 
67
- `use` lists the models the profile offers and remembers your choice:
67
+ `use` activates the profile and does **not** ask anything: the profile's whole model list is published, so you
68
+ pick the model inside the agent with `/model` (Codex) or its Opus/Sonnet/Haiku categories (Claude Code). The
69
+ default for new sessions is the one you last used, or the profile default. Add `--choose` if you want to be
70
+ asked, or `-m/--model` to set the default directly.
68
71
 
69
72
  ```text
70
- This profile provides 2 models:
73
+ Active profile: glm
74
+ Default model: glm-5.3 (new sessions)
75
+ Published models: 3 - switch any time with /model inside codex (Claude Code uses its
76
+ Opus/Sonnet/Haiku mappings), or change the default with -m/--model.
77
+ ```
78
+
79
+ With `--choose` you get the picker instead:
80
+
81
+ ```text
82
+ This profile provides 3 models:
71
83
  1) glm-5.3 GLM-5.3 flagship (1M context) [default]
72
- 2) glm-5.3-flash GLM-5.3 flash (fast, 1M context)
73
- Select model [1-2, Enter=glm-5.3, q=cancel]:
84
+ 2) glm-5.3-flash GLM-5.3 Flash (fast, text+image, 1M context)
85
+ 3) glm-5-turbo GLM-5 Turbo (agent-optimized, 200K context)
86
+ Select model [1-3, Enter=glm-5.3, q=cancel]:
74
87
  ```
75
88
 
76
89
  For scripts and servers, choose without prompting (name, unique prefix, or index):
@@ -79,8 +92,11 @@ For scripts and servers, choose without prompting (name, unique prefix, or index
79
92
  ai-switch use glm --model glm-5.3-flash
80
93
  ai-switch use glm -m 2 -y
81
94
  ai-switch use glm --dry-run # show what would change
95
+ ai-switch use glm -m glm-5.3 --pin # publish only this model (see below)
82
96
  ```
83
97
 
98
+ Selecting a model sets the **default for new sessions** in both agents, and the profile's other models stay in the catalogue so you can still switch inside the agent. That means an in-app `/model` choice is a per-session override — and Codex writes its own choice back to `~/.codex/config.toml` when it exits, so the two agents can drift apart without anything else noticing; `ai-switch doctor` reports exactly that. If you want an activation to be exclusive instead, add `--pin`: the published catalogue then holds only the selected model, so Codex's own picker has nothing else to switch to (the next `ai-switch use` without `--pin` restores the full menu).
99
+
84
100
  Edit `~/.codex/config.toml` and `~/.claude/settings.json` for another provider, then save that configuration as a second profile:
85
101
 
86
102
  ```bash
@@ -23,7 +23,7 @@ Conversation history is never part of a profile
23
23
  import argparse, hashlib, json, os, re, shutil, sqlite3, sys, tempfile, time
24
24
  from pathlib import Path
25
25
 
26
- VERSION = "0.3.0"
26
+ VERSION = "0.3.2"
27
27
 
28
28
 
29
29
  def _env_path(name, default):
@@ -476,7 +476,7 @@ def resolve_model(catalog, token):
476
476
  raise ValueError(f"unknown model '{token}'; this profile offers: {available}")
477
477
 
478
478
 
479
- def pick_model(catalog, requested, fallback, assume_yes=False):
479
+ def pick_model(catalog, requested, fallback, assume_yes=False, force=False):
480
480
  """Choose the model to activate (no prompting when requested/non-interactive)."""
481
481
  if not catalog:
482
482
  return requested
@@ -486,7 +486,7 @@ def pick_model(catalog, requested, fallback, assume_yes=False):
486
486
  return resolve_model(catalog, requested)
487
487
  if fallback not in slugs:
488
488
  fallback = catalog["default"] if catalog["default"] in slugs else slugs[0]
489
- if assume_yes or not sys.stdin.isatty() or len(entries) == 1:
489
+ if assume_yes or (not force and (not sys.stdin.isatty() or len(entries) == 1)):
490
490
  return fallback
491
491
  print(f"This profile provides {len(entries)} models:")
492
492
  for index, entry in enumerate(entries, 1):
@@ -517,16 +517,17 @@ def catalog_target(text, catalog_path):
517
517
  return catalog_path
518
518
 
519
519
 
520
- def build_codex_catalog(d, catalog):
520
+ def build_codex_catalog(d, catalog, include_profile_entries=True):
521
521
  """Union of the profile's catalog file and every selectable model entry."""
522
522
  entries = {}
523
- try:
524
- raw = json.loads((d / "codex-models.json").read_text())
525
- for entry in (raw.get("models") if isinstance(raw, dict) else raw) or []:
526
- if isinstance(entry, dict) and entry.get("slug"):
527
- entries[entry["slug"]] = entry
528
- except (OSError, ValueError, AttributeError):
529
- pass
523
+ if include_profile_entries:
524
+ try:
525
+ raw = json.loads((d / "codex-models.json").read_text())
526
+ for entry in (raw.get("models") if isinstance(raw, dict) else raw) or []:
527
+ if isinstance(entry, dict) and entry.get("slug"):
528
+ entries[entry["slug"]] = entry
529
+ except (OSError, ValueError, AttributeError):
530
+ pass
530
531
  for entry in catalog["models"]:
531
532
  if isinstance(entry.get("codex"), dict):
532
533
  entries[entry["slug"]] = entry["codex"]
@@ -537,7 +538,7 @@ def build_codex_catalog(d, catalog):
537
538
  return {"models": ordered}
538
539
 
539
540
 
540
- def compute_changes(d, slug):
541
+ def compute_changes(d, slug, pin=False):
541
542
  """Return {path: text-or-None}: None means "remove this stale file"."""
542
543
  changes, notes = {}, []
543
544
  catalog = load_catalog(d)
@@ -556,9 +557,14 @@ def compute_changes(d, slug):
556
557
  catalog = dict(catalog, models=catalog["models"] + [entry])
557
558
  if text is not None:
558
559
  # Every profile publishes its models, so the picker inside Codex lists
559
- # them too.
560
+ # them too. --pin publishes only the activated one, which removes the
561
+ # other entries from Codex's own picker.
560
562
  if catalog:
561
- codex_catalog = build_codex_catalog(d, catalog)
563
+ if pin and entry:
564
+ codex_catalog = build_codex_catalog(d, dict(catalog, models=[entry]),
565
+ include_profile_entries=False)
566
+ else:
567
+ codex_catalog = build_codex_catalog(d, catalog)
562
568
  if codex_catalog is not None:
563
569
  catalog_path = catalog_target(text, CODEX_MODELS)
564
570
  changes[catalog_path] = json.dumps(codex_catalog, indent=2) + "\n"
@@ -694,23 +700,34 @@ def cmd_use(args):
694
700
  if not any((d / profile_file).exists() for profile_file in PROFILE_FILES):
695
701
  raise ValueError(f"profile '{name}' has no Codex or Claude configuration to activate")
696
702
  catalog = load_catalog(d)
697
- fallback = args.model or last_model(name) or (catalog or {}).get("default")
698
- if catalog and args.model and catalog.get("open_ended"):
703
+ fallback = last_model(name) or (catalog or {}).get("default")
704
+ slug = args.model or fallback
705
+ if args.model and catalog and catalog.get("open_ended"):
699
706
  try:
700
707
  slug = resolve_model(catalog, args.model)
701
708
  except ValueError:
702
709
  slug = args.model
703
710
  print(f"Note: '{args.model}' is not in the profile's model list; using it as-is.", file=sys.stderr)
704
- else:
705
- slug = pick_model(catalog, args.model, fallback, assume_yes=args.yes)
711
+ elif args.model and catalog:
712
+ slug = resolve_model(catalog, args.model)
713
+ elif catalog and args.choose:
714
+ # Picking a model here only sets the default for new sessions; every model
715
+ # of the profile is published, so it can also be changed inside the agent.
716
+ slug = pick_model(catalog, None, fallback, force=True)
706
717
  if slug and catalog is None:
707
718
  print(f"Note: profile '{name}' has no model list, using '{slug}' as-is.", file=sys.stderr)
708
- changes, notes = compute_changes(d, slug)
719
+ changes, notes = compute_changes(d, slug, pin=args.pin)
709
720
  for note in notes:
710
721
  print(f"Note: {note}", file=sys.stderr)
711
722
  backup = apply_changes(changes, name, slug, dry_run=args.dry_run)
712
723
  if not args.dry_run:
713
- print(f"Active profile: {name}" + (f"\nActive model: {slug}" if slug else ""))
724
+ print(f"Active profile: {name}" + (f"\nDefault model: {slug} (new sessions)" if slug else ""))
725
+ if args.pin and slug:
726
+ print(f"Pinned: only '{slug}' is published, so Codex cannot switch to another model.")
727
+ elif catalog:
728
+ print(f"Published models: {len(catalog['models'])} - switch any time with /model inside "
729
+ "codex (Claude Code uses its Opus/Sonnet/Haiku mappings), or change the default "
730
+ "with -m/--model.")
714
731
  print(f"Backup: {backup}" if backup else "Backup: nothing to back up (no files existed yet)")
715
732
  print("Restart claude/codex so they reload their configuration.")
716
733
  if not args.no_check:
@@ -1273,9 +1290,24 @@ def check_report():
1273
1290
  f"{len(transcripts)} transcript(s) under {display_path(projects)}")
1274
1291
 
1275
1292
  state = load_state()
1276
- add("ok", "Active profile", f"{active_profile() or '(none)'}"
1277
- + (f", model {state.get('models', {}).get(active_profile())}" if isinstance(state.get("models"), dict)
1278
- and state.get("models", {}).get(active_profile()) else ""))
1293
+ active = active_profile()
1294
+ recorded = (state.get("models") or {}).get(active) if isinstance(state.get("models"), dict) else None
1295
+ add("ok", "Active profile", f"{active or '(none)'}" + (f", model {recorded}" if recorded else ""))
1296
+ if recorded:
1297
+ # A running agent rewrites its own config on exit, so the live files can
1298
+ # silently diverge from what 'ai-switch use' activated.
1299
+ if model and model != recorded:
1300
+ add("warn", "Codex is not using the model ai-switch activated",
1301
+ f"config.toml has model = {model}, ai-switch activated {recorded}",
1302
+ "A running codex session rewrites config.toml when it exits, so a /model choice made "
1303
+ f"inside codex wins. Close codex and re-run 'ai-switch use {active} -m {recorded}', or use "
1304
+ "--pin so the in-app picker cannot switch away.")
1305
+ claude_model = str(settings.get("model") or "")
1306
+ stem = lambda text: re.split(r"[\[/]", text)[0]
1307
+ if claude_model and stem(claude_model) != stem(str(recorded)):
1308
+ add("warn", "Claude Code is not using the model ai-switch activated",
1309
+ f"settings.json has model = {claude_model}, ai-switch activated {recorded}",
1310
+ f"Re-run 'ai-switch use {active} -m {recorded}' to bring both agents back in sync.")
1279
1311
  profiles = [p.name for p in sorted(PROFILES.iterdir()) if p.is_dir()] if PROFILES.exists() else []
1280
1312
  add("ok" if profiles else "note", "Profiles", ", ".join(profiles) or "none yet")
1281
1313
 
@@ -1365,8 +1397,12 @@ files inside a profile are switched. Restart claude/codex after switching."""
1365
1397
 
1366
1398
  p = sub.add_parser("use", help="activate a profile (optionally choosing a model)")
1367
1399
  p.add_argument("name")
1368
- p.add_argument("-m", "--model", help="model to activate (name, unique prefix, or 1-based index)")
1369
- p.add_argument("-y", "--yes", action="store_true", help="never prompt; use the default model")
1400
+ p.add_argument("-m", "--model", help="set the default model for new sessions (name, unique prefix, or index)")
1401
+ p.add_argument("--choose", action="store_true",
1402
+ help="ask which model the new sessions should start with (the others stay selectable in /model)")
1403
+ p.add_argument("--pin", action="store_true",
1404
+ help="publish only this model, so Codex's own picker cannot switch to another one")
1405
+ p.add_argument("-y", "--yes", action="store_true", help="accepted for compatibility; use never prompts by default")
1370
1406
  p.add_argument("-n", "--dry-run", action="store_true", help="show what would change")
1371
1407
  p.add_argument("--no-check", action="store_true", help="skip the session-history health check")
1372
1408
  p.set_defaults(fn=cmd_use)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-switch-cli
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: One-command provider and model switching for Claude Code and Codex, with session-history checks
5
5
  Author: ZidongS
6
6
  License: MIT
@@ -78,13 +78,26 @@ Select `glm` or `deepseek`, enter the API key, and activate the generated config
78
78
  ai-switch use glm
79
79
  ```
80
80
 
81
- `use` lists the models the profile offers and remembers your choice:
81
+ `use` activates the profile and does **not** ask anything: the profile's whole model list is published, so you
82
+ pick the model inside the agent with `/model` (Codex) or its Opus/Sonnet/Haiku categories (Claude Code). The
83
+ default for new sessions is the one you last used, or the profile default. Add `--choose` if you want to be
84
+ asked, or `-m/--model` to set the default directly.
82
85
 
83
86
  ```text
84
- This profile provides 2 models:
87
+ Active profile: glm
88
+ Default model: glm-5.3 (new sessions)
89
+ Published models: 3 - switch any time with /model inside codex (Claude Code uses its
90
+ Opus/Sonnet/Haiku mappings), or change the default with -m/--model.
91
+ ```
92
+
93
+ With `--choose` you get the picker instead:
94
+
95
+ ```text
96
+ This profile provides 3 models:
85
97
  1) glm-5.3 GLM-5.3 flagship (1M context) [default]
86
- 2) glm-5.3-flash GLM-5.3 flash (fast, 1M context)
87
- Select model [1-2, Enter=glm-5.3, q=cancel]:
98
+ 2) glm-5.3-flash GLM-5.3 Flash (fast, text+image, 1M context)
99
+ 3) glm-5-turbo GLM-5 Turbo (agent-optimized, 200K context)
100
+ Select model [1-3, Enter=glm-5.3, q=cancel]:
88
101
  ```
89
102
 
90
103
  For scripts and servers, choose without prompting (name, unique prefix, or index):
@@ -93,8 +106,11 @@ For scripts and servers, choose without prompting (name, unique prefix, or index
93
106
  ai-switch use glm --model glm-5.3-flash
94
107
  ai-switch use glm -m 2 -y
95
108
  ai-switch use glm --dry-run # show what would change
109
+ ai-switch use glm -m glm-5.3 --pin # publish only this model (see below)
96
110
  ```
97
111
 
112
+ Selecting a model sets the **default for new sessions** in both agents, and the profile's other models stay in the catalogue so you can still switch inside the agent. That means an in-app `/model` choice is a per-session override — and Codex writes its own choice back to `~/.codex/config.toml` when it exits, so the two agents can drift apart without anything else noticing; `ai-switch doctor` reports exactly that. If you want an activation to be exclusive instead, add `--pin`: the published catalogue then holds only the selected model, so Codex's own picker has nothing else to switch to (the next `ai-switch use` without `--pin` restores the full menu).
113
+
98
114
  Edit `~/.codex/config.toml` and `~/.claude/settings.json` for another provider, then save that configuration as a second profile:
99
115
 
100
116
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-switch-cli"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "One-command provider and model switching for Claude Code and Codex, with session-history checks"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes