gpumon-ingress — LLM proxy
Why this exists / when to use what
gpumon-ingressis the mandatory entry point for every LLM call made by any agent, script, or service in the federation. It speaks the OpenAI chat-completions wire format on the front, picks a pool frompools.local.yaml/pools2.yaml, routes to NIM first then to LAN GPU nodes on 429 / circuit-breaker open, and ledgers every call viagpumon-writer. Callers never talk to LiteLLM, NIM, or vLLM directly — they talk to ingress, send thex-gpumon-*headers, and ingress does the rest (key rotation, RPM budgeting, cooldown bookkeeping, attribution).Use this surface when you want to make an LLM call. If you already made one (e.g. you called Anthropic directly) and just want to ledger the outcome, talk to the writer instead — see
writer.md.
Endpoints
| Method | Path | Body | Notes |
|---|---|---|---|
POST |
/v1/chat/completions |
OpenAI chat-completions request | Streams when stream: true; ledgers every call. |
POST |
/v1/embeddings |
OpenAI embeddings request | Routes to embedding pool. |
GET |
/v1/models |
— | Lists pools + upstream models. |
GET |
/health |
— | Liveness + pool counts. |
Addresses
| Audience | URL |
|---|---|
| In-swarm callers (overlay DNS) | http://gpumon-ingress:4001 |
| LAN callers (any machine on the .1.0/24) | http://192.168.1.211:4010 |
mepstudio local dev compose |
http://localhost:4010 |
The same image runs in three places (swarm on node-eleven, mepstudio docker-compose, mepstudio host-mode). Pick the address closest to the caller. Auth is none on LAN — egress is firewalled at the UDM.
Required headers
Every request MUST include these. Missing headers degrade attribution to
unknown and your project will not appear in the dashboard.
| Header | Required | Purpose |
|---|---|---|
x-gpumon-client-id |
yes | Logical caller name (fraud-enricher, edgar-summarizer, claude-mem, etc.). Persisted to llm_requests.caller_service. |
x-gpumon-project |
yes | Project bucket for billing / quota / dashboards. Persisted to llm_requests.project_id. |
x-gpumon-pool |
yes | Logical pool name; ingress routes accordingly. See pool table below. |
x-gpumon-workflow-stage |
recommended | Free-form stage tag (cleanup, triples, verify, ocr-page-23). Persisted to llm_requests.workflow_stage. |
x-gpumon-doc-id |
when relevant | Document or row id this call serves; lets the dashboard cluster calls by source doc. |
x-gpumon-session-id |
recommended | Stable session identifier; lets the dashboard group N calls into one workload row. Pairs with /workloads (see writer.md). |
x-gpumon-workload-id |
optional | If you pre-registered a workload via POST /workloads, send the wl_xxx id here. |
Header names are legacy in a couple of places (ingress forwards them
upstream as x-gpumon-service and packs workload_id into
x-gpumon-doc-id) because LiteLLM's allow-list is strict. Callers should
keep using the names in the table above — ingress handles the renaming.
Pools
The pool name is a logical routing key. Ingress maps it to one or more LiteLLM model IDs (or NIM passthrough sidecars) and picks one per request using a weighted-random policy with per-deployment cooldowns on 429 / auth failures.
Live source of truth: services/ingress/pools.local.yaml (and v2 schema
pools2.yaml). Re-verify by hitting GET /v1/models on ingress.
Canonical pools
| Pool | Purpose | Max completion |
|---|---|---|
pool-load-balance |
Canonical NIM-first 128k+ context, LAN fallback. Use this for new callers. | 8192 |
pool-summary-short |
1-sentence summaries | 64 |
pool-summary-medium |
Paragraph summaries | 256 |
pool-summary-long |
Multi-paragraph summaries | 1024 |
pool-citations |
Legal citation extraction | 512 |
pool-bluebook |
Bluebook citation formatting | 256 |
pool-title |
Document title generation | 32 |
pool-md-formatter |
Markdown cleanup | 4096 |
pool-claude-mem |
claude-mem observation extraction | 4096 |
All of the above are aliases of pool-load-balance with a smaller cap —
no source changes needed for legacy callers.
Vision / OCR pools (NIM passthrough, cannot be aliased)
| Pool | Purpose |
|---|---|
pool-ocr |
Vision OCR — direct LiteLLM pool-ocr passthrough |
pool-vision |
Vision analysis (non-OCR) — direct passthrough |
pool-vision-local |
LAN-only fallback for vision when NIM is degraded |
These bypass LiteLLM's chat-completions adapter because LiteLLM's
nvidia_nim provider strips image_url content from vision messages.
Fraud-specific pools
| Pool | Purpose |
|---|---|
pool-fraud-gemma |
Gemma backend |
pool-fraud-qwen-large |
Qwen 80B / 120B class |
pool-fraud-qwen-14b |
Qwen 14B class (cheaper) |
pool-fraud-triples |
Triple extraction |
pool-fraud-cleanup |
Document cleanup |
pool-fraud-verify |
Quorum verification |
Discovery
ssh rooot@node-eleven 'curl -s http://localhost:4000/v1/models | jq -r .data[].id'
Or via ingress directly:
curl http://192.168.1.211:4010/v1/models | jq -r '.data[].id'
Concurrency & 429 semantics
- NIM sustained per-key cap is ~5-6 RPM (not the documented 40). Ingress has 14 keys = ~70 RPM aggregate for vision.
- Per-backend circuit breaker OPENs after ~5 fails in 30s; ingress then routes to the LAN fallback tier (spark-2:11234 + nvidia-2:8080 + nvidia-4:8080).
- Cooldowns are tracked per
(model, api_key_alias)and persisted to the writer (/cooldown/:key). Callers do not see 429s under normal load; they see successful responses from the fallback tier. - Honor
Retry-Afteron the rare 429 that escapes (fallback tier also exhausted). Implement exponential backoff with jitter.
Bun fetch example (20 lines)
const r = await fetch("http://192.168.1.211:4010/v1/chat/completions", {
method: "POST",
headers: {
"content-type": "application/json",
"x-gpumon-client-id": "fraud-enricher",
"x-gpumon-project": "fraud-heuristics",
"x-gpumon-pool": "pool-fraud-triples",
"x-gpumon-workflow-stage": "extract-triples",
"x-gpumon-doc-id": "row-12345",
"x-gpumon-session-id": crypto.randomUUID(),
},
body: JSON.stringify({
model: "pool-fraud-triples",
messages: [{ role: "user", content: "Extract triples from: ..." }],
max_tokens: 2048,
stream: false,
}),
});
if (!r.ok) throw new Error(`ingress ${r.status}: ${await r.text()}`);
const out = await r.json();
console.log(out.choices[0].message.content);
curl example
curl http://192.168.1.211:4010/v1/chat/completions \
-H 'content-type: application/json' \
-H 'x-gpumon-client-id: edgar-summarizer' \
-H 'x-gpumon-project: edgar-cik-cli' \
-H 'x-gpumon-pool: pool-summary-medium' \
-H 'x-gpumon-workflow-stage: 10k-summary' \
-H 'x-gpumon-doc-id: 0000320193-2024-10K' \
-d '{
"model": "pool-summary-medium",
"messages": [{"role":"user","content":"Summarize this 10-K in one paragraph: ..."}],
"max_tokens": 256
}'
Cross-references
- Header bookkeeping is closed by the writer — see
writer.md. - Long-running workloads should register first — see
writer.md. - For batch workloads (1000+ calls), queue the unit of work, not the
LLM call — see
broker.mdandclient-pattern.md.