---
name: updating-llm-client-model-lists
description: >-
  Synchronizes model configurations across Zed and Crush from the Plexus proxy
  and Hyper. Use when the user asks to update model lists, sync models, refresh
  available models, or mentions Plexus or Hyper model availability.
user-invocable: true
license: LicenseRef-MutuaL-1.2
metadata:
  author: Amolith <amolith@secluded.site>
---

Updates LLM client configs from two sources:

1. **Plexus** (self-hosted at `harp-willow.snowy-egret.ts.net:4000`) — feeds
   both Zed (`language_models.openai_compatible.plexus`) and Crush
   (`providers.plexus.models`).
2. **Charm Hyper** (`hyper.charm.land`) and **Hyper Staging**
   (`hyper.charmcli.dev`) — feed Zed only
   (`language_models.openai_compatible.hyper` and
   `language_models.openai_compatible.hyper-staging` respectively). Crush manages
   its own Hyper models; do not touch them.

## Scripts

Two reusable, typed Python scripts live in `scripts/` alongside this skill. They
fetch and normalise model metadata into a uniform TSV or JSON format. Run them
with `--json` when piping into a config-apply helper, or `--tsv` (default) for
review.

| Script | Source | Auth | Notes |
|--------|--------|------|-------|
| `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`. |
| `scripts/hyper.py` | Hyper `/v1/models` | None | Pass `--staging` to target Hyper Staging. Uses `max_output_tokens` verbatim (no 20% rule). |

Both scripts share `scripts/models.py` (the `Model` dataclass, display-name
cleaner, and TSV/JSON emitters). All scripts are executable and use
`#!/usr/bin/env python3`.

### Running the scripts

```bash
SKILL_DIR="$HOME/.agents/skills/updating-llm-client-model-lists/scripts"

# Plexus — TSV for review
"$SKILL_DIR/plexus.py" --tsv | column -t -s$'\t'

# Plexus — JSON for config apply
"$SKILL_DIR/plexus.py" --json

# Hyper — TSV
"$SKILL_DIR/hyper.py" --tsv | column -t -s$'\t'

# Hyper Staging — JSON
"$SKILL_DIR/hyper.py" --staging --json
```

The TSV/JSON output is the single source of truth for model IDs, display names,
context windows, and capability flags. Review the display names in the table,
and if they need modification, make the modification while editing the configs.

## jq

The `jq` on PATH inside Crush is a shell function, not the real binary. Always
invoke the real one via `mise exec jq -- jq …`.

## JSONC

Zed's `settings.json` is JSONC (allows trailing commas and comments), so stdlib
`jq` cannot parse it. Use Python with the `json5` module instead:

```python
import json, json5, pathlib
src = pathlib.Path("/home/amolith/.config/zed/settings.json")
data = json5.loads(src.read_text())
# …mutate data…
out = json.dumps(data, indent=4, ensure_ascii=False).replace("    ", "\t")
src.write_text(out)
```

## Network reachability

1. Ping `harp-willow.snowy-egret.ts.net` once.
2. If unreachable and Tailscale DNS is disabled, enable it:
   ```bash
   tailscale set --accept-dns=true
   ```
3. Test ping again. If still unreachable, STOP and inform the user.

Remember to disable after finishing the whole process if it was previously
disabled:

```bash
tailscale set --accept-dns=false
```

## max_completion_tokens

Different sources use different rules:

- **Plexus**: use `floor(context_length * 0.2)`. Do NOT use the
  `top_provider.max_completion_tokens` field — some models report their entire
  context window, which is not a sensible default. `scripts/plexus.py` applies
  this automatically.
- **Hyper / Hyper Staging**: use the `max_output_tokens` field verbatim.
  `scripts/hyper.py` uses this directly (no 20% rule).

## Rules

### Exclusions (Plexus)

- Always exclude `nomic-embed-text-v1.5` (embedding model, not supported by any
  client)
- Exclude `diff-apply` and `fix-json` from all client model lists
- Exclude models with no `context_length` field (e.g. voxtral-small,
  mistral-ocr) unless the user provides values manually
- Only aliases with a `metadata` block are fetched; aliases without metadata
  (like `diff-apply`, `fix-json`, `memri`, `smol`) are implicitly excluded.

### Capabilities

- Assume every Plexus-listed model supports tools, regardless of whether
  Plexus reports `tools` in `supported_parameters`.
- For Hyper and Hyper Staging models, every entry supports tools and
  interleaved reasoning; set `tools: true`, `parallel_tool_calls: true`, and
  `interleaved_reasoning: true` on every Hyper entry.

### Display names

`scripts/models.py:clean_name()` handles normalisation for both sources:
- Strip redundant vendor prefixes (`DeepSeek: DeepSeek V4 Pro` → `DeepSeek V4
  Pro`)
- Remove parentheses (`(fast)` → `fast`)
- Replace hyphens with spaces
- Split brand-version compounds (`Qwen3.6` → `Qwen 3.6`)
- Normalise capitalisation for known brands and acronyms
  (DeepSeek, GLM, GPT, MiMo, MiniMax, Qwen, OSS, FP8, INT4, etc.)

When a Plexus alias ID ends in `-h` (indicating a hosted variant), the
display name from Plexus already reflects the correct form; no special
suffix handling is needed beyond `clean_name()`.

## Update Zed config

File: `~/.config/zed/settings.json` (JSONC — edit via Python `json5`, see
above).

### Plexus

Update `language_models.openai_compatible.plexus.available_models[]`. Each
entry:

```json
{
  "name": "<id>",
  "display_name": "<name>",
  "max_tokens": <context_length>,
  "max_completion_tokens": <max_completion_tokens>,
  "capabilities": {
    "chat_completions": true,
    "prompt_cache_key": false,
    "tools": true,
    "parallel_tool_calls": true,
    "images": <supports_image>
  }
}
```

### Hyper

Update `language_models.openai_compatible.hyper.available_models[]`. Each
entry:

```json
{
  "name": "<id>",
  "display_name": "<name>",
  "max_tokens": <context_window>,
  "max_completion_tokens": <max_output_tokens>,
  "capabilities": {
    "chat_completions": false,
    "prompt_cache_key": false,
    "tools": true,
    "parallel_tool_calls": true,
    "interleaved_reasoning": true,
    "images": <supports_attachments>
  }
}
```

### Hyper Staging

Update `language_models.openai_compatible.hyper-staging.available_models[]`
with the same schema as Hyper. The `api_url` is
`https://hyper.charmcli.dev/v1`.

Example apply helper for all three Zed providers:

```python
import json, json5, pathlib, subprocess

SKILL_DIR = "$HOME/.agents/skills/updating-llm-client-model-lists/scripts"
src = pathlib.Path.home() / ".config/zed/settings.json"
data = json5.loads(src.read_text())
om = data["language_models"]["openai_compatible"]

# Plexus
plexus = json.loads(subprocess.check_output([SKILL_DIR + "/plexus.py", "--json"]))
om["plexus"]["available_models"] = [{
    "name": m["id"], "display_name": m["name"],
    "max_tokens": m["context_length"],
    "max_completion_tokens": m["max_completion_tokens"],
    "capabilities": {"chat_completions": True, "prompt_cache_key": False,
        "tools": True, "parallel_tool_calls": True, "images": m["supports_image"]},
} for m in plexus]

# Hyper
def hyper_entries(staging=False):
    cmd = [SKILL_DIR + "/hyper.py", "--json"]
    if staging: cmd.append("--staging")
    models = json.loads(subprocess.check_output(cmd))
    return [{
        "name": m["id"], "display_name": m["name"],
        "max_tokens": m["context_length"],
        "max_completion_tokens": m["max_completion_tokens"],
        "capabilities": {"chat_completions": False, "prompt_cache_key": False,
            "tools": True, "parallel_tool_calls": True,
            "interleaved_reasoning": True, "images": m["supports_image"]},
    } for m in models]

om["hyper"]["available_models"] = hyper_entries()
om["hyper-staging"] = {
    "api_url": "https://hyper.charmcli.dev/v1",
    "available_models": hyper_entries(staging=True),
}

out = json.dumps(data, indent=4, ensure_ascii=False).replace("    ", "\t")
src.write_text(out)
```

## Update Crush config

File: `~/.local/share/chezmoi/dot_config/crush/crush.json`

Update `providers.plexus.models[]`. Each entry:

```json
{
  "id": "<id>",
  "name": "<name>",
  "context_window": <context_length>,
  "default_max_tokens": <max_completion_tokens>,
  "can_reason": <supports_reasoning>,
  "supports_attachments": <supports_image>
}
```

This file is strict JSON — safe to edit with `mise exec jq` directly:

```bash
"$SKILL_DIR/plexus.py" --json > /tmp/plexus_models.json
mise exec jq -- jq --tab --slurpfile new /tmp/plexus_models.json \
  '.providers.plexus.models = [$new[0][] | {
    "id": .id, "name": .name,
    "context_window": .context_length,
    "default_max_tokens": .max_completion_tokens,
    "can_reason": .supports_reasoning,
    "supports_attachments": .supports_image
  }]' \
  ~/.local/share/chezmoi/dot_config/crush/crush.json > /tmp/crush_new.json
mv /tmp/crush_new.json ~/.local/share/chezmoi/dot_config/crush/crush.json
```

After editing, run `chezmoi apply ~/.config/crush/crush.json`. STOP if chezmoi
has any output other than success. Do not touch `providers.hyper` — Crush
manages its own Hyper models.
