Without agent_tls the control plane served plaintext h2c on every interface, and any machine reaching it could register as any node. The server now refuses to start that way unless agent_insecure: true, and then binds loopback when agent_listen names only a port. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
282 lines
11 KiB
YAML
282 lines
11 KiB
YAML
# llmgw router configuration (phase 1).
|
|
#
|
|
# The endpoint list is static here. From phase 3 it comes from the control
|
|
# plane instead and this section disappears; everything else stays.
|
|
#
|
|
# Any field can be overridden from the environment with an LLMGW_ prefix and
|
|
# __ for nesting: LLMGW_ROUTER__RETRIES=3
|
|
|
|
listen: ":8080" # client-facing OpenAI API
|
|
admin_listen: "127.0.0.1:9090" # dashboard, management API, pprof — keep it local
|
|
# Metrics only, on their own port, so Prometheus can reach them without pprof
|
|
# being reachable too. Empty means off; metrics stay on admin_listen either way.
|
|
# metrics_listen: "0.0.0.0:9091"
|
|
# Where node agents connect. Separate from the client port so the fleet-facing
|
|
# surface can be firewalled independently.
|
|
agent_listen: ":7443"
|
|
|
|
# Mutual TLS on the agent port (plan §5.6). With it, a node's identity is taken
|
|
# from its certificate's common name and an agent claiming a different node id
|
|
# is refused. Without it the port would be plaintext HTTP/2 and anyone who can
|
|
# reach it could claim to be any node, so the control plane refuses to start
|
|
# unless `agent_insecure: true` is set — and then listens on loopback unless
|
|
# agent_listen names an address. That is for one machine or a test.
|
|
#
|
|
# These certificates come from llmgw's own authority, not a public one: public
|
|
# authorities stopped issuing the clientAuth usage, so no Let's Encrypt
|
|
# certificate can authenticate an agent.
|
|
#
|
|
# llmgw ctl cert init-ca --dir /etc/llmgw/pki
|
|
# llmgw ctl cert issue-server llmgw-ctl --host 10.0.0.140 --dir /etc/llmgw/pki
|
|
#
|
|
# Every address an agent might dial has to appear in --host, bare IP addresses
|
|
# included: hostname verification reads subject alternative names and ignores
|
|
# the common name entirely.
|
|
#
|
|
# This covers the fleet-facing port only. The client-facing API above is public
|
|
# and belongs behind a publicly trusted certificate.
|
|
#
|
|
# agent_tls:
|
|
# ca: /etc/llmgw/ca.pem
|
|
# cert: /etc/llmgw/server.pem
|
|
# key: /etc/llmgw/server-key.pem
|
|
|
|
# The public page at "/". Someone who reaches this address without a key needs a
|
|
# route to getting one, and the gateway cannot guess who to send them to.
|
|
#
|
|
# landing:
|
|
# contact: "platform@example.com" # or an https:// URL, or "#platform on Slack"
|
|
# notice: "Internal use only."
|
|
|
|
# API keys on the client-facing port (plan §7.9). Off by default, because a
|
|
# gateway on a trusted network with no keys issued still has to serve — but
|
|
# nothing else protects that port, so turning it on is what makes it safe to
|
|
# expose.
|
|
#
|
|
# llmgw keys init --store /var/lib/llmgw/llmgw.db
|
|
# llmgw keys create --store /var/lib/llmgw/llmgw.db --project default "laptop"
|
|
#
|
|
# The key is printed once. Only its hash is stored, so there is nothing to
|
|
# recover it from.
|
|
#
|
|
# auth:
|
|
# enabled: true
|
|
# store: /var/lib/llmgw/llmgw.db
|
|
# # How long a resolved key is trusted before being read again. This is the
|
|
# # price of keeping the database off the request path: it bounds how long a
|
|
# # revoked key keeps working.
|
|
# cache_ttl: 30s
|
|
# # How long a rejected key is remembered as rejected, so a client looping
|
|
# # with a bad key cannot become a database read per request.
|
|
# negative_ttl: 5s
|
|
#
|
|
# # Serve the signup form. What it creates is a pending account that can do
|
|
# # nothing until `llmgw users approve` places it in a project, so this is not
|
|
# # a way in — it only moves key creation from you to the person who needs one.
|
|
# # Off, there is no form and you issue keys with `llmgw keys create --claim`.
|
|
# signup_open: true
|
|
#
|
|
# # What each model costs, in micro-units per thousand tokens. A unit is
|
|
# # whatever you decide: self-hosted models have no per-token price, so it can
|
|
# # be GPU-seconds, a notional rate, or 1000 everywhere — which turns budgets
|
|
# # into plain token quotas.
|
|
# #
|
|
# # A model priced at zero is exempt from budget checks, which is what lets a
|
|
# # key with nothing left still be served a cheap fallback.
|
|
# default_price: { prompt_per_1k: 0, completion_per_1k: 0 }
|
|
# pricing:
|
|
# llama-3.3-70b: { prompt_per_1k: 1000, completion_per_1k: 3000 }
|
|
# qwen2.5-32b: { prompt_per_1k: 400, completion_per_1k: 1200 }
|
|
#
|
|
# # Usage rows are written in batches off the request path. Beyond buffer_size
|
|
# # they are dropped rather than blocking a request: losing some accounting is
|
|
# # bad, turning a stalled database into an outage is worse.
|
|
# flush_interval: 200ms
|
|
# batch_size: 500
|
|
# buffer_size: 8192
|
|
#
|
|
# # How long the record of each request is kept (what `ctl request` reads).
|
|
# # Charts, totals and budgets come from five-minute sums that are kept, so
|
|
# # this only bounds the table that grows by a row a request. 0 keeps all.
|
|
# usage_retention: 2160h # 90 days
|
|
|
|
router:
|
|
scrape_interval: 2s
|
|
scrape_timeout: 1s
|
|
# How long an endpoint may go unscraped before it is scored down. It is never
|
|
# dropped for staleness alone: if the whole pool went stale there would be
|
|
# nowhere left to send anything.
|
|
stale_after: 10s
|
|
|
|
# Additional endpoints to try after a failure, and only while nothing has
|
|
# reached the client yet.
|
|
retries: 2
|
|
# How long a failing endpoint leaves the candidate pool. A long fallback list
|
|
# with no cooldown turns one endpoint's rate limit into a cluster-wide retry
|
|
# storm.
|
|
cooldown: 15s
|
|
# Consecutive failures that open the breaker. Set to 1 for pure cooldown
|
|
# behaviour.
|
|
failure_threshold: 3
|
|
|
|
default_profile: chat # chat | batch | long-context | debug
|
|
|
|
# Approximate prefix routing (plan §7.5 tier 1): send a request to the replica
|
|
# that has probably already prefilled its prefix.
|
|
#
|
|
# It earns its keep when the working set does not fit in one replica's cache.
|
|
# With a handful of prompts and a large cache, scattering is free — every
|
|
# replica ends up holding everything — and this only adds constraint. Measured
|
|
# with sim-engines whose caches were too small for the working set: cache hits
|
|
# went from 22.5% to 81.7%.
|
|
#
|
|
# Watch llmgw_prefix_decisions_total to tune it. A high
|
|
# load_balance_override count means the guard is doing its job and affinity is
|
|
# not getting a chance; a high cache_miss_min_load means match_ratio or
|
|
# min_match is too strict for your traffic.
|
|
prefix:
|
|
enabled: false
|
|
# The shortest match worth acting on, in bytes. Below this the overlap is
|
|
# coincidence — two English prompts share "The".
|
|
min_match: 64
|
|
# The share of the prompt that must match. A short shared header on a very
|
|
# long prompt saves almost no prefill.
|
|
match_ratio: 0.3
|
|
# The imbalance guard, and both must trip before affinity is abandoned.
|
|
# Absolute alone fires on an idle cluster where one replica has two requests
|
|
# and another none; relative alone fires on a busy one where 200 against 180
|
|
# is a rounding error. Without the guard the cache hit rate rises while p99
|
|
# collapses.
|
|
balance_abs: 8
|
|
balance_rel: 1.5
|
|
# An engine evicts on its own schedule, so an old record may describe a
|
|
# cache that is gone.
|
|
ttl: 10m
|
|
max_nodes: 100000
|
|
session_header: x-session-id # affinity key; falls back to the `user` field
|
|
# Ask engines for usage even when the client did not, so token accounting
|
|
# works regardless of client behaviour. The extra chunk is dropped on the way
|
|
# back to a client that did not ask for it.
|
|
inject_usage: true
|
|
max_body_bytes: 33554432
|
|
|
|
timeouts:
|
|
dial: 5s
|
|
response_header: 60s
|
|
first_token: 120s
|
|
idle: 60s # between streamed tokens; catches a wedged engine
|
|
total: 30m
|
|
|
|
# Flow control: decides *when* a request may proceed, before scheduling
|
|
# decides where it goes. Without it a batch workload starves interactive
|
|
# traffic, because an engine queue is FIFO and knows nothing about tenants.
|
|
admission:
|
|
enabled: true
|
|
# With auth on, the key decides the band and this header is ignored. With
|
|
# auth off the header is trusted, so any client can promote itself into the
|
|
# top band — do not expose the port in that configuration.
|
|
band_header: x-llmgw-band
|
|
default_band: gold
|
|
# A band waits once pool saturation passes its ceiling, so the cluster
|
|
# degrades from the bottom up. A ceiling of 1.0 means "flow up to the
|
|
# healthy limit"; use a value above 1 for a band that must never wait.
|
|
bands:
|
|
- { name: platinum, priority: 400, saturation_ceiling: 1.00 }
|
|
- { name: gold, priority: 300, saturation_ceiling: 0.85 }
|
|
- { name: silver, priority: 200, saturation_ceiling: 0.60 }
|
|
- { name: bronze, priority: 100, saturation_ceiling: 0.30 }
|
|
# Drop a request that has waited this long: the client has usually given up,
|
|
# and sending the GPU work nobody is waiting for helps no one.
|
|
queue_ttl: 30s
|
|
# Reject immediately once a band's queue is this deep, rather than letting
|
|
# it accumulate silently.
|
|
queue_capacity_per_band: 2000
|
|
interval: 10ms
|
|
|
|
# What the scorers normalize against. Measure these with `ctl bench` rather
|
|
# than guessing: target_waiting in particular governs how the whole system
|
|
# behaves under load. Too low starves the GPU, too high brings back queueing
|
|
# delay.
|
|
scoring:
|
|
target_waiting: 4
|
|
target_inflight: 8
|
|
kv_ceiling: 0.9
|
|
session_load_factor: 1.25
|
|
|
|
# How aggressively the control plane reconciles.
|
|
# The engines every node installs, and at which version. Unset keeps the
|
|
# versions this release was checked against (llama.cpp b10991). Each agent
|
|
# picks the build for its own hardware and installs it itself; changing a
|
|
# version here reaches every node on its next connect.
|
|
# engines:
|
|
# llamacpp: b10991
|
|
|
|
control:
|
|
interval: 5s
|
|
base_backoff: 10s
|
|
max_backoff: 10m
|
|
# Node expiry is staged deliberately: a network blip must not tear down
|
|
# replicas that are still serving, and rebuilding a multi-node replica costs
|
|
# tens of minutes of model loading.
|
|
unreachable_after: 15s
|
|
lost_after: 60s
|
|
port_range: [20000, 20999]
|
|
mgmt_interface: eth0
|
|
|
|
# Each model needs a source — a weights path or a HuggingFace id — and at least
|
|
# one profile if the control plane is to place it. Without profiles the model is
|
|
# catalogued but never started, which is the router-only mode below.
|
|
models:
|
|
- name: llama-3.3-70b
|
|
source: meta-llama/Llama-3.3-70B-Instruct
|
|
aliases: [llama]
|
|
fallbacks: [qwen2.5-32b]
|
|
routing_profile: chat
|
|
scaling: { min: 1, max: 4 }
|
|
profiles:
|
|
# Two nodes of four H100s each. A multi-node profile requires a fabric,
|
|
# and a replica never spans two of them.
|
|
- name: tp8
|
|
engine: vllm
|
|
gpu_class: h100-80g
|
|
tp: 8
|
|
nodes: 2
|
|
gpus_per_node: 4
|
|
priority: 100
|
|
weight: 8
|
|
startup_timeout: 30m
|
|
# Where a whole node is free, one node is cheaper to schedule and faster
|
|
# to start than two.
|
|
- name: tp4-a100
|
|
engine: vllm
|
|
gpu_class: a100-40g
|
|
tp: 4
|
|
priority: 50
|
|
weight: 3
|
|
startup_timeout: 20m
|
|
|
|
- name: qwen2.5-32b
|
|
source: Qwen/Qwen2.5-32B-Instruct
|
|
aliases: [qwen]
|
|
routing_profile: batch
|
|
scaling: { min: 1, max: 2 }
|
|
profiles:
|
|
- name: tp2
|
|
engine: sglang
|
|
gpu_class: h100-80g
|
|
tp: 2
|
|
priority: 100
|
|
weight: 2
|
|
startup_timeout: 15m
|
|
|
|
# A static replica list, used only when no model declares a profile. That is the
|
|
# router-only mode for testing against sim-engines; with the control plane
|
|
# running, agents provide the replicas instead.
|
|
#
|
|
# endpoints:
|
|
# - id: sim-1
|
|
# url: http://127.0.0.1:8001
|
|
# models: [llama-3.3-70b]
|
|
# pool: local
|
|
# weight: 1
|