Skip to content
Back to skills

Llm Router Ops

BSecurity

Operate a self-hosted OpenAI-compatible LLM router — client-wiring per client, context-window ad gotcha, env-vs-persisted-config gotcha, why an unauth health probe should 401. Use wiring a client, adding a backend, diagnosing misbehavior.

  • 3 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 19, 2026
ai-agentspythonrustgobashapidatabasebackend

Works with

  • cli
  • api

Security analysis

B84/100
  • mediumUses curl or wget to download content
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study

Pro shows the line behind each finding and how to fix it

Scanned September 23, 2026

npx -y skills add dryvist/claude-code-plugins --skill llm-router-ops --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Llm Router Ops?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Llm Router Ops
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/dryvist-llm-router-ops/badge)](https://www.skillsdirectory.com/skills/dryvist-llm-router-ops)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: llm-router-ops
description: Operate a self-hosted OpenAI-compatible LLM router — client-wiring per client, context-window ad gotcha, env-vs-persisted-config gotcha, why an unauth health probe should 401. Use wiring a client, adding a backend, diagnosing misbehavior.
---

# Operating an OpenAI-compatible LLM router

A self-hosted LLM router (e.g. LiteLLM) gives every consumer — human tool or
agent — one shared base URL and one shared bearer key, and load-balances or
routes to whichever backend actually serves a given model alias. This is the
generic client-wiring and operational-gotcha pattern; your own base URL,
model aliases, and key-storage location stay in your own inventory.

## Minimal client wiring

Every client type needs the same three things: base URL, bearer key, model
alias. The alias is what decouples a client from knowing which backend is
really serving it.

```bash
curl -s https://<router-host>/v1/chat/completions \
  -H "Authorization: Bearer $ROUTER_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "<alias>", "messages": [{"role": "user", "content": "hello"}]}'
```

```python
from openai import OpenAI
client = OpenAI(base_url="https://<router-host>/v1", api_key=os.environ["ROUTER_MASTER_KEY"])
```

Any tool with an "OpenAI-Compatible" or "OpenAI API" provider type (chat UIs,
low-code agent builders, Ansible-style role group vars) takes the same two
values — base URL and key — with no other client-specific config needed.

## Adding a backend model

1. Add the model block to the router config: alias name, backend model id,
   backend base URL.
2. **Set the context window explicitly** if the router doesn't already know
   the backend's real native context. An unrecognized backend id otherwise
   resolves to a `null`/unset advertised context, which starves any consumer
   that trusts the router's advertised limit to decide how much to send.
3. Restart/reload the router process so the new config takes effect.
4. Verify: `GET /v1/models` (with the bearer key) lists the new alias, and
   its advertised context window is the real one, not null.

## Gotcha: env var vs. the tool's own persisted config

Many chat-UI-style front ends persist their provider config in their own
database and will silently let that override the environment on a redeploy
— so a correct env var stops mattering after the UI's admin panel has ever
been touched. If the front end has a "persistent config" or equivalent
toggle, disable it so the **environment is authoritative** on every restart,
instead of the tool re-applying whatever was last saved in its UI.

## Gotcha: an unconditional import your dependency tree no longer pulls in

If the router's own error-classification path unconditionally imports an
optional dependency (a DB client, a metrics library) to decide whether a
failure is an outage vs. a plain auth rejection, and a version bump of the
router's package drops that dependency as a *transitive* pull, every
rejected or missing key starts returning a generic 500 instead of a 401 —
masking real auth failures as opaque server errors. If backend-classifier
logic in your router depends on an `import` succeeding for an `isinstance`
check, pin that package explicitly even though you never call it directly.

## Gotcha: anonymous probes should 401, not 200-with-an-error-body

If the router requires the bearer key on every route including
`/v1/models`, an unauthenticated health check will always see `401` — that
is correct and not a misconfiguration. Don't "fix" a monitoring probe by
loosening auth on the models endpoint; fix the probe to send the key.

## Verification

```bash
curl -s https://<router-host>/v1/models \
  -H "Authorization: Bearer $ROUTER_MASTER_KEY" | jq '.data[].id'
```

Every alias you expect is present, and each carries a non-null advertised
context window.

## Related

- **openbao-secrets** (openbao) — the pattern for sourcing the router's
  master key at runtime instead of storing it statically.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…