Create, duplicate, rename and delete presets from the panel
Each provider's preset list ends in "New ... preset...", and the preset editor gains Duplicate, Rename and Delete. New asks for a name and what to start from: a copy of one of that provider's presets, or a blank template. Delete is greyed out, with the reason, on the preset in use, and warns when it would leave a provider with no preset at all. CLI: - preset rename <name> <new>: the new name is hard-linked in, state.json is repointed, and only then does the old name go. - preset new <name> --provider <p> [--blank]: with a provider and no source it copies that provider's own default instead of the OpenRouter-only `default`; --blank starts from the scaffold. - valid_preset_name on every preset subcommand: no slash, no leading dot. `preset show ../../etc/passwd` used to print the file. - preset rm warns when it removes a provider's last preset. - preflight refuses a preset with every tier empty. Otherwise a blank preset would switch cleanly and Claude Code would ask the gateway for its default Anthropic models, billed at full price on OpenRouter. The panel's blocked card offers "Edit preset..." for it. The key helper now reads the active preset in a single open (new cm-json auth-of) and re-reads state.json once on a miss. Renaming the active preset could otherwise catch a running session between reading the old name and opening the file: 1 failure in 51 key fetches in a race test before, 0 in 118 across 60 renames after. It could also briefly hand out the openrouter key for a preset that uses another. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ Subcommands:
|
||||
scan-sessions <projects> [max-age-days] [ignored] broken transcripts, as JSON
|
||||
ignore-session <ignored> <projects> add|remove|clear|list [id]
|
||||
cache-models <cache> <provider> <1|0> [baseUrl] store a TSV catalogue (stdin)
|
||||
preset-rename <presets-dir> <old> <new> <state> rename, repointing state.json
|
||||
auth-of <preset> "mode<TAB>keyRef"; fails if missing
|
||||
"""
|
||||
|
||||
import glob
|
||||
@@ -250,6 +252,53 @@ def cmd_scaffold(argv):
|
||||
print(json.dumps(base, indent=2))
|
||||
|
||||
|
||||
def cmd_auth_of(argv):
|
||||
"""auth-of <preset> - 'mode<TAB>keyRef' for the key helper, defaults filled in.
|
||||
|
||||
Exits non-zero when the file is missing, which load() would otherwise
|
||||
hide as an empty preset - and an empty preset reads as vault/openrouter,
|
||||
so a preset renamed mid-read would quietly hand out the wrong key.
|
||||
"""
|
||||
try:
|
||||
with open(argv[0], encoding="utf-8") as fh:
|
||||
text = fh.read().strip()
|
||||
except OSError:
|
||||
sys.exit(1)
|
||||
p = json.loads(text) if text else {}
|
||||
auth = p.get("auth") or {}
|
||||
print("%s\t%s" % (auth.get("mode") or "vault", auth.get("keyRef") or "openrouter"))
|
||||
|
||||
|
||||
def cmd_preset_rename(argv):
|
||||
"""preset-rename <presets-dir> <old> <new> <state.json>
|
||||
|
||||
Every running session's key helper resolves the active preset through
|
||||
state.json on each key fetch, so at no instant may state.json name a file
|
||||
that is not there. The new name is linked in first, state.json repointed,
|
||||
and only then does the old name go. Prints {"renamed", "active"}.
|
||||
"""
|
||||
d, old, new, state_path = argv[:4]
|
||||
src = os.path.join(d, old + ".json")
|
||||
dst = os.path.join(d, new + ".json")
|
||||
if not os.path.isfile(src):
|
||||
raise SystemExit("preset '%s' not found" % old)
|
||||
if os.path.exists(dst):
|
||||
raise SystemExit("preset '%s' already exists" % new)
|
||||
try:
|
||||
os.link(src, dst)
|
||||
except OSError:
|
||||
__import__("shutil").copy2(src, dst) # filesystems without hard links
|
||||
|
||||
state = load(state_path, {})
|
||||
active = isinstance(state, dict) and state.get("preset") == old
|
||||
if active:
|
||||
state["preset"] = new
|
||||
save(state_path, state)
|
||||
|
||||
os.unlink(src)
|
||||
print(json.dumps({"renamed": True, "active": active}))
|
||||
|
||||
|
||||
def cmd_get(argv):
|
||||
data = load(argv[0])
|
||||
cur = data
|
||||
@@ -1176,6 +1225,8 @@ COMMANDS = {
|
||||
"set-all": cmd_set_all,
|
||||
"set-auth": cmd_set_auth,
|
||||
"scaffold": cmd_scaffold,
|
||||
"preset-rename": cmd_preset_rename,
|
||||
"auth-of": cmd_auth_of,
|
||||
"get": cmd_get,
|
||||
"presets": cmd_presets,
|
||||
"managed": cmd_managed,
|
||||
|
||||
Reference in New Issue
Block a user