118 lines
5.2 KiB
Markdown
118 lines
5.2 KiB
Markdown
|
|
# LLM tiers — tracking the served model without hardcoding it
|
||
|
|
|
||
|
|
The homelab's served model changes. `glm-4.6-reap` became `deepseek-v4-flash`;
|
||
|
|
something else will replace it. Every place mcpctl writes a model id down is a
|
||
|
|
place that silently rots when that happens — a row left requesting a suspended
|
||
|
|
model gets `HTTP 400 model not found` on the next call, and the failure surfaces
|
||
|
|
somewhere unhelpful (a gate that quietly stops ranking prompts, an agent that
|
||
|
|
500s).
|
||
|
|
|
||
|
|
mcpctl's answer is that **it never names a model, and prefers not to name a
|
||
|
|
row**. Consumers ask for a *role*; the registry says who currently fills it.
|
||
|
|
|
||
|
|
## The three layers
|
||
|
|
|
||
|
|
```
|
||
|
|
LiteLLM (llm.ad.itaz.eu) deepseek-v4-flash, -fast, -low, -think, -max
|
||
|
|
▲ served-model ids — change on every model swap
|
||
|
|
│ Pulumi owns this mapping
|
||
|
|
mcpd Llm rows vllm-current (tier: fast)
|
||
|
|
▲ vllm-think (tier: heavy)
|
||
|
|
│ stable identities; `model` follows the deployment
|
||
|
|
mcpctl consumers "give me the fast one"
|
||
|
|
no model id, no row name
|
||
|
|
```
|
||
|
|
|
||
|
|
**Layer 1 → 2 is Pulumi's job.** `deployments/mcpctl/llm-target.ts` in the
|
||
|
|
`kubernetes-deployment` repo declares both rows via the `@mcpctl/pulumi`
|
||
|
|
provider and feeds them the active served-model name. Swap the model, run a
|
||
|
|
targeted `pulumi up`, and the rows follow. See
|
||
|
|
[pulumi-provider-llm-autopoint](../src/pulumi/README.md).
|
||
|
|
|
||
|
|
**Layer 2 → 3 is `src/mcplocal/src/server-llm.ts`.** `tier` is already a
|
||
|
|
first-class field on the `Llm` resource and is set by the same Pulumi resource
|
||
|
|
that sets `model`, so the two cannot drift.
|
||
|
|
|
||
|
|
## Resolution rules
|
||
|
|
|
||
|
|
`resolveServerLlmByTier(client, tier)` returns the row filling a tier, or
|
||
|
|
`null`. A row is eligible when:
|
||
|
|
|
||
|
|
- its `tier` matches exactly;
|
||
|
|
- its `status` is `active` — selecting a row with no live backend just moves
|
||
|
|
the failure to the first inference call;
|
||
|
|
- its `kind` is **not** `virtual`. Virtual rows are backed by some user's
|
||
|
|
`mcplocal` over the SSE control channel, i.e. by a machine and personal
|
||
|
|
credentials nobody chose deliberately. An explicit pin may still name one;
|
||
|
|
automatic resolution must not route a project's traffic through a laptop.
|
||
|
|
|
||
|
|
When a tier holds several rows — `heavy` holds both `vllm-think` and the cloud
|
||
|
|
`anthropic-fallback` — an ordered **well-known name** list breaks the tie:
|
||
|
|
|
||
|
|
| tier | preference order |
|
||
|
|
|------|------------------|
|
||
|
|
| `fast` | `vllm-fast`, `vllm-current` |
|
||
|
|
| `heavy` | `vllm-think`, `vllm-thinking` |
|
||
|
|
|
||
|
|
Names that match nothing are inert. That is deliberate: the list carries both
|
||
|
|
the current names and the symmetric names the rows might be renamed to, so a
|
||
|
|
rename stays a Pulumi-only change and neither deploy order breaks the other.
|
||
|
|
Rows absent from the list are still eligible — they just sort last, then by
|
||
|
|
name, so the choice is stable across calls regardless of mcpd's list order.
|
||
|
|
|
||
|
|
Override the order without a release:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
MCPCTL_LLM_PREFER_FAST=spare-row,vllm-current
|
||
|
|
MCPCTL_LLM_PREFER_HEAVY=vllm-think
|
||
|
|
```
|
||
|
|
|
||
|
|
Resolution never throws. mcpd being unreachable is a normal degraded state for
|
||
|
|
`mcplocal`, and every caller has a fallback path; returning `null` keeps that
|
||
|
|
fallback intact instead of failing session setup.
|
||
|
|
|
||
|
|
## Who uses it
|
||
|
|
|
||
|
|
**Gate prompt-selection** (`project-mcp-endpoint.ts`). Order:
|
||
|
|
|
||
|
|
1. `MCPCTL_GATE_SELECTION_LLM` — a global pin, so selection can sit on a fast
|
||
|
|
no-think Llm while chat keeps a thinking model;
|
||
|
|
2. the project's own `llmProvider` (`none` disables);
|
||
|
|
3. whichever row currently fills the `fast` tier.
|
||
|
|
|
||
|
|
Step 3 is what makes an unpinned project work. Without it, a project with no
|
||
|
|
`llmProvider` fell through to the local personal-token provider — and on a
|
||
|
|
machine whose personal key is stale, that means every gated session silently
|
||
|
|
ran on priority-ordered prompts instead of LLM ranking:
|
||
|
|
|
||
|
|
```
|
||
|
|
[gate] LLM prompt-selection failed: Anthropic HTTP 404: model: claude-opus-4-20250514
|
||
|
|
— falling back to priority-ordered prompts
|
||
|
|
```
|
||
|
|
|
||
|
|
The gate prints a `⚠ Smart prompt-selection unavailable` banner in the
|
||
|
|
`begin_session` response whenever it degrades. Absence of that banner on a
|
||
|
|
project with no pin is what the smoke test asserts.
|
||
|
|
|
||
|
|
Explicit pins still win everywhere — this only changes what happens when
|
||
|
|
nothing is pinned.
|
||
|
|
|
||
|
|
## Why not rename `vllm-current` to `vllm-fast`?
|
||
|
|
|
||
|
|
`name` is immutable in mcpd (agents and projects reference it), so a rename is
|
||
|
|
a data migration plus a Pulumi resource replace, not an edit. Once resolution
|
||
|
|
is tier-based nothing in mcpctl reads the names, so the migration buys nothing.
|
||
|
|
`vllm-fast` would also be actively confusing: the fast row points at
|
||
|
|
`deepseek-v4-flash` (the base route, which carries no forced `extra_body`),
|
||
|
|
while LiteLLM separately serves a `deepseek-v4-fast` route that pins
|
||
|
|
`thinking: false`. Two different things, one name apart.
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
- `src/mcplocal/tests/server-llm.test.ts` — selection rules, tiebreak, env
|
||
|
|
override, degraded paths.
|
||
|
|
- `src/mcplocal/tests/smoke/llm-tier.smoke.test.ts` — against the live stack:
|
||
|
|
both tiers resolve, the resolved fast row answers real inference (the drift
|
||
|
|
check — a row pointing at a suspended model fails here), and an unpinned
|
||
|
|
gated project gets LLM-ranked selection.
|