summaryrefslogtreecommitdiff
path: root/link/pi.sh
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-09-25 11:30:38 +0300
committerIgor Soarez <igor@soarez.org>2026-09-25 11:30:38 +0300
commit57e49ddde1dffb17043cd4b065c2907fbe5a863d (patch)
tree5376c1a312d403aa453906b6a6a8e4edb9fdb675 /link/pi.sh
parent96c42e431c8252f02556aba9d9f7373dcdf92e41 (diff)
pi: providers can come from ~/.config/pi/models.d, like settings already do
models.json was a plain symlink to the tracked file, which left a machine whose models are served from somewhere other than a vendor's own API with nowhere to say so. pi reads providers from exactly one path -- join(getAgentDir(), "models.json") -- and a `providers` block in a settings fragment is inert, because the settings schema has no such key. So on such a machine pi cannot be used at all, and the endpoint is not something this repo may name. link/pi.sh now generates models.json the way it already generates settings.json: the repo's file, then every ~/.config/pi/models.d/*.json. No three-way merge here -- pi never writes this file, keeping runtime state in the sibling models-store.json -- so a hand-edit of ~/.pi/agent/models.json no longer survives a relink. Put it in a fragment. It reports which provider came from which fragment, added or replaced, so a local file shadowing a provider the repo ships cannot do it quietly. Proposed as a change request from the machine that needs it; verified here against pi 0.84.2 before applying.
Diffstat (limited to 'link/pi.sh')
-rwxr-xr-xlink/pi.sh88
1 files changed, 88 insertions, 0 deletions
diff --git a/link/pi.sh b/link/pi.sh
index 2f0a05c..22ecd96 100755
--- a/link/pi.sh
+++ b/link/pi.sh
@@ -229,3 +229,91 @@ case "${pi_status%%:*}" in
locked) yellow "pi settings: pi holds its settings lock; left as is, the next link catches up" ;;
error) red "pi settings: ${pi_status#*:}" ;;
esac
+
+# pi's models.json, generated too. It used to be a plain symlink to the tracked
+# file, which left a machine whose models come from somewhere other than a
+# vendor's own API with nowhere to say so: pi reads providers from exactly one
+# path (join(getAgentDir(), "models.json")), and a `providers` block in a
+# settings fragment is inert, since the settings schema has no such key.
+#
+# So the same shape as the settings above: the repo's file, then every
+# ~/.config/pi/models.d/*.json another layer or this machine puts there. Unlike
+# the settings this is a plain generated file with no three-way merge -- pi
+# never writes it, keeping its runtime state in the sibling models-store.json --
+# so what lands is whatever the repo and the fragments say. A hand-edit of
+# ~/.pi/agent/models.json does not survive a relink; put it in a fragment.
+pi_models_out="$(DRY_RUN="$is_dry_run" python3 - \
+ "$config_dir/pi/models.json" \
+ "$xdg_config_dir/pi/models.d" \
+ "$pi_dir/models.json" <<'PY'
+import glob, json, os, sys
+
+base_path, frag_dir, target = sys.argv[1:4]
+
+
+def load(path):
+ with open(path) as f:
+ return json.load(f)
+
+
+# The repo's providers, then each fragment's. A fragment may add a provider or
+# replace one of the same name; any other top-level key takes the last file's
+# value. Which provider came from where is reported: a fragment quietly
+# shadowing a provider the repo ships would otherwise be invisible.
+desired = {}
+notes = []
+for path in [base_path] + sorted(glob.glob(os.path.join(frag_dir, "*.json"))):
+ try:
+ layer = load(path)
+ except (OSError, ValueError) as e:
+ print("error:cannot read %s: %s" % (path, e))
+ sys.exit(0)
+ for key, value in layer.items():
+ if key == "providers":
+ providers = desired.setdefault("providers", {})
+ if path != base_path:
+ for name in value:
+ notes.append("%s %s by %s" % (
+ name,
+ "replaced" if name in providers else "added",
+ os.path.basename(path),
+ ))
+ providers.update(value)
+ else:
+ desired[key] = value
+
+# A symlink is the pre-generation state: replace it, even with no fragments, so
+# the live file is never the tracked one.
+was_link = os.path.islink(target)
+try:
+ current = None if was_link else load(target)
+except (OSError, ValueError):
+ current = None
+
+for note in notes:
+ print("note:" + note)
+
+if current == desired:
+ print("unchanged:")
+elif os.environ.get("DRY_RUN"):
+ print("would-write:" + ("replacing the link" if was_link else "merged changes"))
+else:
+ tmp = target + ".dotfiles-tmp"
+ with open(tmp, "w") as f:
+ json.dump(desired, f, indent=2)
+ f.write("\n")
+ os.replace(tmp, target)
+ print("wrote:" + ("replaced the link" if was_link else "merged changes"))
+PY
+)"
+
+while IFS= read -r line
+do
+ case "${line%%:*}" in
+ note) yellow "pi models: ${line#*:}" ;;
+ wrote) green "pi models: ${line#*:}" ;;
+ unchanged) green "pi models: unchanged" ;;
+ would-write) yellow "pi models will be generated: ${line#*:}" ;;
+ error) red "pi models: ${line#*:}" ;;
+ esac
+done <<< "$pi_models_out"