Two client-side fixes against the covert-gpt LiteLLM gateway, both traced from opencode throwing intermittent ContextWindowExceededError 400s. opencode (mcp-config/sync.sh): spec_for() mapped LiteLLM's /model/info onto opencode's schema 1:1, but the two mean different things by "output". LiteLLM's max_output_tokens is the whole window (vLLM caps TOTAL context, not output separately, which is correct for LiteLLM); opencode's limit.output is a RESERVE it subtracts from context to pick a compaction point. Copying one to the other told opencode to reserve the entire 262144 window, so it targeted an input of context-output and the server counted one token more than opencode did -- landing on context+1 and a hard 400, every time (measured: 196609 in + 65536 out = 262145 vs a 262144 window; the retries shrinking max_tokens by 65 never helped because the input grew to compensate). Capping the reserve alone would NOT have fixed it -- opencode would still sit exactly on the boundary, just at a different input size. So: cap the reserve (OUTPUT_RESERVE_CAP) and hold a margin back from context (CONTEXT_SAFETY_MARGIN). Generated at sync time, so it fixes every gateway model at once, including third-party ones. hermes (hermes/gateway.yaml, hermes/install.sh): Hermes reached the gateway by pretending it WAS OpenRouter -- the LiteLLM key in OPENROUTER_API_KEY, the gateway in OPENROUTER_BASE_URL. Requests routed fine, but fetch_openrouter_models hardcodes openrouter.ai/api/v1/models and ignores OPENROUTER_BASE_URL, so the picker listed OpenRouter's ~342-model catalog -- models this gateway cannot serve -- while the gateway's own models, including cg-1's, never appeared at all. Replaced with a named custom provider. install.sh now merges gateway.yaml into ~/.hermes/config.yaml, so a fresh box gets a Hermes that can actually talk to the gateway instead of one pointed at nothing. The merge is idempotent and narrow: only the covert-gpt entry is touched, model.default/provider are seeded only when unset, and a box still on the masquerade is migrated off it (fingerprint: provider openrouter + base_url pointing at this gateway; a genuine OpenRouter user never has that, and is left alone). No secrets: gateway.yaml names the key via key_env, and mcp-config/sync.sh is what exports it. Hermes needs no context safety margin -- it omits max_tokens entirely, so vLLM bounds output by whatever context is left. Also folds in the in-flight hermes bootstrap wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mcp-config — shared opencode config for laptops
Single source of truth for what opencode sees: the covert-gpt model provider
(LiteLLM at llm.covert-gpt.com) and the MCP servers, which run
client-side. Users install once; their config then refreshes automatically
on each new shell.
MCP runs on the CLIENT, never on the gateway
The LiteLLM gateway hosts no MCP servers — deliberately. Every API worth exposing (Gitea, Proxmox, UniFi, NetBird, k8s) is user-scoped, so hosting it centrally means one shared token per server: every user acts as the same identity and attribution is gone.
LiteLLM can do per-request BYOK (env values written ${X-Header} are filled
from the caller's headers — verified working for proxmox on v1.87.0), but only
for servers that tolerate a credential-less spawn. LiteLLM builds its
tool→server mapping from a headerless background listing, and gitea-mcp
hard-exits without a token, so it never registers and its tool calls 404. The
gateway could therefore host only some servers — and a split brain (some MCPs
server-side, some client-side) is worse than either. So all MCP servers are
defined here, each reading the user's own credentials from their own
environment. The config is identical for everyone; only the secrets differ.
Credentials are referenced as {env:VAR} and are never written into the
config. Each user exports them in ~/.zshrc.local:
| MCP server | env vars |
|---|---|
gitea |
GITEA_KEY |
proxmox |
PROXMOX_MCP_TOKEN_ID, PROXMOX_MCP_TOKEN_SECRET |
Requires npx — the MCP servers are npm packages that opencode spawns as
npx -y <pkg>@<version>. bootstrap.sh installs Node for this (node/).
Without it every server dies with "command not found", which opencode surfaces
only as a dead server — easy to misread as a broken config.
This repo lives in Gitea at public/dotfiles. (It is mirrored from the
ai-k8s-iac repo under mcp-config/; edit it there or here and keep them in
sync — the laptops pull from Gitea's main.)
How it works
-
opencode.gateway.json— the shared base config:- the
covert-gptmodel provider (LiteLLM's OpenAI-compatible endpoint athttps://llm.covert-gpt.com/v1, models map left empty — it is filled by discovery at sync time), and - the MCP servers, each
type: local, spawned by opencode asnpx -y <pkg>@<pinned-version>on the user's own machine.
Identical for every user: every credential (the LiteLLM key, the Gitea PAT, the Proxmox token) is injected via
{env:VAR}and never written into the file. Versions are pinned —npxruns whatever the registry serves, and that process gets your tokens. - the
-
sync.sh— sourced from each user's shell rc. On a new shell (throttled ~1h) it pulls the base config from Gitea, queries the gateway's/v1/modelswith the user's key to auto-discover models (opencode can't list models from a custom provider itself, so the map is materialized client-side; if discovery fails the previous map is carried over), deploys the repo's skills (see below), deploys the config to~/.config/opencode/opencode.gateway.json, and exportsLITELLM_KEY(from~/.config/mcp-sync/key) plusOPENCODE_CONFIG(pointing at the deployed file — opencode does NOT auto-load extra files in its config dir, so without this export the file is ignored). Silent + non-fatal: a Gitea hiccup never breaks the shell, last-good config stays in place. -
../skills/— the shared skill folders, deployed to~/.config/mcp-sync/skillsand wired in throughskills.pathsin the config above. Skills are plain files, so — unlike models and MCP servers — there is nothing on the gateway to discover and the repo is the source of truth; the tree is fetched whole from the repo tarball. They deploy to mcp-sync's own directory rather than~/.config/opencode/skillsso the atomic replace can never delete a skill the user wrote by hand. Seeskills/README.md. -
install.sh— one-time bootstrap (curl-pipe). Fetchessync.sh, stores the user's key (0600), and adds the rc hook.
opencode merges the OPENCODE_CONFIG file on top of the user's global
~/.config/opencode/opencode.json (and under any project opencode.json),
so the synced config coexists with each user's own settings and any
local-only MCP servers (e.g. a client-side kubernetes pointed at their
own kubeconfig). Sync never touches those.
User setup (one time)
Each user needs a LiteLLM virtual key (for models) plus their own
credentials for each MCP server. Run bootstrap.sh (it installs Node, which
provides the npx the MCP servers need), then:
curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/mcp-config/install.sh | sh
# (prompts for the sk-... key; or pass it: ... | LITELLM_KEY=sk-... sh)
Then export your own MCP credentials in ~/.zshrc.local:
export GITEA_KEY=... # your Gitea PAT (repo + issue scopes)
export PROXMOX_MCP_TOKEN_ID=... # e.g. alice@pve!mcp
export PROXMOX_MCP_TOKEN_SECRET=... # the token UUID
Open a new terminal, run opencode, and the tools are there — acting as you.
Adding an MCP server (admin)
Add it to mcp.<name> in opencode.gateway.json as type: local, with a
pinned package version and {env:VAR} refs for every secret, then push to
main. Users pick it up within the sync TTL (or immediately on a new shell).
Two hard requirements:
- It must be an npm package (opencode spawns it with
npx). - Its credentials must be per-user, via
{env:VAR}. Never commit a token, and never introduce a shared one — that is the whole reason MCP is not on the gateway.
Models are different: they live on the gateway
(charts/inference/values.yaml → model_list) and are auto-discovered by the
/v1/models query in sync.sh. No client change needed.
Adding a skill (admin)
Add a folder under skills/ and push to main — see skills/README.md.
Verify with opencode debug skill.
"I added a model on the gateway and opencode doesn't see it"
Almost always a stale local config, not a gateway or key problem. The sync runs
only at shell start and is throttled to MCP_SYNC_TTL (1h), so a new model
can take up to an hour and a new terminal to appear.
Pull it immediately:
mcp-sync # force a refresh now, ignoring the TTL; prints the model list
If the model still isn't listed, it isn't reaching your key — check the gateway, not the client:
curl -fsS -H "Authorization: Bearer $LITELLM_KEY" \
https://llm.covert-gpt.com/v1/models | jq -r '.data[].id'
Whatever that prints is exactly what sync.sh will deploy. A model present here
but missing in opencode is a sync problem; a model missing here is a LiteLLM
model_list or key-scoping problem.
Notes / caveats
- Local-server precedence: opencode merges with later-config-wins on key conflicts. If a user names a local server the same as a gateway one, theirs wins. Use distinct names.
- Key rotation: re-run
install.sh(or edit~/.config/mcp-sync/key). - Other clients: this ships an opencode config. Claude Code / open-webui would need their own equivalent file + a parallel sync entry; not done yet.
- Throttle:
mcp-syncforces one refresh;MCP_SYNC_TTL=0in the environment makes every new shell pull. - python3 is required for model discovery. Without it
sync.shdeploys the base config verbatim, so the provider comes up with an EMPTY model list. It warns when this happens;bootstrap.shinstalls python3.