SKILL.md

  1---
  2name: updating-llm-client-model-lists
  3description: >-
  4  Synchronizes model configurations across Zed and Crush from the Plexus proxy
  5  and Hyper. Use when the user asks to update model lists, sync models, refresh
  6  available models, or mentions Plexus or Hyper model availability.
  7user-invocable: true
  8license: LicenseRef-MutuaL-1.2
  9metadata:
 10  author: Amolith <amolith@secluded.site>
 11---
 12
 13Updates LLM client configs from two sources:
 14
 151. **Plexus** (self-hosted at `harp-willow.snowy-egret.ts.net:4000`) β€” feeds
 16   both Zed (`language_models.openai_compatible.plexus`) and Crush
 17   (`providers.plexus.models`).
 182. **Charm Hyper** (`hyper.charm.land`) and **Hyper Staging**
 19   (`hyper.charmcli.dev`) β€” feed Zed only
 20   (`language_models.openai_compatible.hyper` and
 21   `language_models.openai_compatible.hyper-staging` respectively). Crush manages
 22   its own Hyper models; do not touch them.
 23
 24## Scripts
 25
 26Two reusable, typed Python scripts live in `scripts/` alongside this skill. They
 27fetch and normalise model metadata into a uniform TSV or JSON format. Run them
 28with `--json` when piping into a config-apply helper, or `--tsv` (default) for
 29review.
 30
 31| Script | Source | Auth | Notes |
 32|--------|--------|------|-------|
 33| `scripts/plexus.py` | Plexus management + metadata APIs | 1Password (`op read`) | Unsets `OP_SERVICE_ACCOUNT_TOKEN` internally, retrieves admin key, does two-step alias β†’ metadata lookup. Excludes `nomic-embed-text-v1.5`, `diff-apply`, `fix-json`, and any alias without a `metadata` block. Uses `floor(context_length * 0.2)` for `max_completion_tokens`. |
 34| `scripts/hyper.py` | Hyper `/v1/models` | None | Pass `--staging` to target Hyper Staging. Uses `max_output_tokens` verbatim (no 20% rule). |
 35
 36Both scripts share `scripts/models.py` (the `Model` dataclass, display-name
 37cleaner, and TSV/JSON emitters). All scripts are executable and use
 38`#!/usr/bin/env python3`.
 39
 40### Running the scripts
 41
 42```bash
 43SKILL_DIR="$HOME/.agents/skills/updating-llm-client-model-lists/scripts"
 44
 45# Plexus β€” TSV for review
 46"$SKILL_DIR/plexus.py" --tsv | column -t -s$'\t'
 47
 48# Plexus β€” JSON for config apply
 49"$SKILL_DIR/plexus.py" --json
 50
 51# Hyper β€” TSV
 52"$SKILL_DIR/hyper.py" --tsv | column -t -s$'\t'
 53
 54# Hyper Staging β€” JSON
 55"$SKILL_DIR/hyper.py" --staging --json
 56```
 57
 58The TSV/JSON output is the single source of truth for model IDs, display names,
 59context windows, and capability flags. Review the display names in the table,
 60and if they need modification, make the modification while editing the configs.
 61
 62## jq
 63
 64The `jq` on PATH inside Crush is a shell function, not the real binary. Always
 65invoke the real one via `mise exec jq -- jq …`.
 66
 67## JSONC
 68
 69Zed's `settings.json` is JSONC (allows trailing commas and comments), so stdlib
 70`jq` cannot parse it. Use Python with the `json5` module instead:
 71
 72```python
 73import json, json5, pathlib
 74src = pathlib.Path("/home/amolith/.config/zed/settings.json")
 75data = json5.loads(src.read_text())
 76# …mutate data…
 77out = json.dumps(data, indent=4, ensure_ascii=False).replace("    ", "\t")
 78src.write_text(out)
 79```
 80
 81## Network reachability
 82
 831. Ping `harp-willow.snowy-egret.ts.net` once.
 842. If unreachable and Tailscale DNS is disabled, enable it:
 85   ```bash
 86   tailscale set --accept-dns=true
 87   ```
 883. Test ping again. If still unreachable, STOP and inform the user.
 89
 90Remember to disable after finishing the whole process if it was previously
 91disabled:
 92
 93```bash
 94tailscale set --accept-dns=false
 95```
 96
 97## max_completion_tokens
 98
 99Different sources use different rules:
100
101- **Plexus**: use `floor(context_length * 0.2)`. Do NOT use the
102  `top_provider.max_completion_tokens` field β€” some models report their entire
103  context window, which is not a sensible default. `scripts/plexus.py` applies
104  this automatically.
105- **Hyper / Hyper Staging**: use the `max_output_tokens` field verbatim.
106  `scripts/hyper.py` uses this directly (no 20% rule).
107
108## Rules
109
110### Exclusions (Plexus)
111
112- Always exclude `nomic-embed-text-v1.5` (embedding model, not supported by any
113  client)
114- Exclude `diff-apply` and `fix-json` from all client model lists
115- Exclude models with no `context_length` field (e.g. voxtral-small,
116  mistral-ocr) unless the user provides values manually
117- Only aliases with a `metadata` block are fetched; aliases without metadata
118  (like `diff-apply`, `fix-json`, `memri`, `smol`) are implicitly excluded.
119
120### Capabilities
121
122- Assume every Plexus-listed model supports tools, regardless of whether
123  Plexus reports `tools` in `supported_parameters`.
124- For Hyper and Hyper Staging models, every entry supports tools and
125  interleaved reasoning; set `tools: true`, `parallel_tool_calls: true`, and
126  `interleaved_reasoning: true` on every Hyper entry.
127
128### Display names
129
130`scripts/models.py:clean_name()` handles normalisation for both sources:
131- Strip redundant vendor prefixes (`DeepSeek: DeepSeek V4 Pro` β†’ `DeepSeek V4
132  Pro`)
133- Remove parentheses (`(fast)` β†’ `fast`)
134- Replace hyphens with spaces
135- Split brand-version compounds (`Qwen3.6` β†’ `Qwen 3.6`)
136- Normalise capitalisation for known brands and acronyms
137  (DeepSeek, GLM, GPT, MiMo, MiniMax, Qwen, OSS, FP8, INT4, etc.)
138
139When a Plexus alias ID ends in `-h` (indicating a hosted variant), the
140display name from Plexus already reflects the correct form; no special
141suffix handling is needed beyond `clean_name()`.
142
143## Update Zed config
144
145File: `~/.config/zed/settings.json` (JSONC β€” edit via Python `json5`, see
146above).
147
148### Plexus
149
150Update `language_models.openai_compatible.plexus.available_models[]`. Each
151entry:
152
153```json
154{
155  "name": "<id>",
156  "display_name": "<name>",
157  "max_tokens": <context_length>,
158  "max_completion_tokens": <max_completion_tokens>,
159  "capabilities": {
160    "chat_completions": true,
161    "prompt_cache_key": false,
162    "tools": true,
163    "parallel_tool_calls": true,
164    "images": <supports_image>
165  }
166}
167```
168
169### Hyper
170
171Update `language_models.openai_compatible.hyper.available_models[]`. Each
172entry:
173
174```json
175{
176  "name": "<id>",
177  "display_name": "<name>",
178  "max_tokens": <context_window>,
179  "max_completion_tokens": <max_output_tokens>,
180  "capabilities": {
181    "chat_completions": false,
182    "prompt_cache_key": false,
183    "tools": true,
184    "parallel_tool_calls": true,
185    "interleaved_reasoning": true,
186    "images": <supports_attachments>
187  }
188}
189```
190
191### Hyper Staging
192
193Update `language_models.openai_compatible.hyper-staging.available_models[]`
194with the same schema as Hyper. The `api_url` is
195`https://hyper.charmcli.dev/v1`.
196
197Example apply helper for all three Zed providers:
198
199```python
200import json, json5, pathlib, subprocess
201
202SKILL_DIR = "$HOME/.agents/skills/updating-llm-client-model-lists/scripts"
203src = pathlib.Path.home() / ".config/zed/settings.json"
204data = json5.loads(src.read_text())
205om = data["language_models"]["openai_compatible"]
206
207# Plexus
208plexus = json.loads(subprocess.check_output([SKILL_DIR + "/plexus.py", "--json"]))
209om["plexus"]["available_models"] = [{
210    "name": m["id"], "display_name": m["name"],
211    "max_tokens": m["context_length"],
212    "max_completion_tokens": m["max_completion_tokens"],
213    "capabilities": {"chat_completions": True, "prompt_cache_key": False,
214        "tools": True, "parallel_tool_calls": True, "images": m["supports_image"]},
215} for m in plexus]
216
217# Hyper
218def hyper_entries(staging=False):
219    cmd = [SKILL_DIR + "/hyper.py", "--json"]
220    if staging: cmd.append("--staging")
221    models = json.loads(subprocess.check_output(cmd))
222    return [{
223        "name": m["id"], "display_name": m["name"],
224        "max_tokens": m["context_length"],
225        "max_completion_tokens": m["max_completion_tokens"],
226        "capabilities": {"chat_completions": False, "prompt_cache_key": False,
227            "tools": True, "parallel_tool_calls": True,
228            "interleaved_reasoning": True, "images": m["supports_image"]},
229    } for m in models]
230
231om["hyper"]["available_models"] = hyper_entries()
232om["hyper-staging"] = {
233    "api_url": "https://hyper.charmcli.dev/v1",
234    "available_models": hyper_entries(staging=True),
235}
236
237out = json.dumps(data, indent=4, ensure_ascii=False).replace("    ", "\t")
238src.write_text(out)
239```
240
241## Update Crush config
242
243File: `~/.local/share/chezmoi/dot_config/crush/crush.json`
244
245Update `providers.plexus.models[]`. Each entry:
246
247```json
248{
249  "id": "<id>",
250  "name": "<name>",
251  "context_window": <context_length>,
252  "default_max_tokens": <max_completion_tokens>,
253  "can_reason": <supports_reasoning>,
254  "supports_attachments": <supports_image>
255}
256```
257
258This file is strict JSON β€” safe to edit with `mise exec jq` directly:
259
260```bash
261"$SKILL_DIR/plexus.py" --json > /tmp/plexus_models.json
262mise exec jq -- jq --tab --slurpfile new /tmp/plexus_models.json \
263  '.providers.plexus.models = [$new[0][] | {
264    "id": .id, "name": .name,
265    "context_window": .context_length,
266    "default_max_tokens": .max_completion_tokens,
267    "can_reason": .supports_reasoning,
268    "supports_attachments": .supports_image
269  }]' \
270  ~/.local/share/chezmoi/dot_config/crush/crush.json > /tmp/crush_new.json
271mv /tmp/crush_new.json ~/.local/share/chezmoi/dot_config/crush/crush.json
272```
273
274After editing, run `chezmoi apply ~/.config/crush/crush.json`. STOP if chezmoi
275has any output other than success. Do not touch `providers.hyper` β€” Crush
276manages its own Hyper models.