Kyde Gateway Reference
Routing rules, configuration, CLI, and the ledger format. For installing and operating the stack see the deployment guide; for using the dashboard see the user manual.
- Multi-provider routing
- MCP routing
- Custom and local upstreams
- CLI
- Agent identity
- Policy enforcement (DLP prevention)
- Architecture
- Ledger entry fields
- Extending toward hardware roots
Multi-provider routing
Section titled “Multi-provider routing”One proxy instance handles all providers simultaneously. Routing is path-based — no configuration, environment variables, or headers needed. The proxy detects the upstream from the request path.
Routing rules
Section titled “Routing rules”| Path pattern | Upstream | Final URL |
|---|---|---|
/v1/chat/completions | OpenAI (default) | https://api.openai.com/v1/chat/completions |
/v1/messages | Anthropic (auto-detected) | https://api.anthropic.com/v1/messages |
/openai/v1/chat/completions | OpenAI | https://api.openai.com/v1/chat/completions |
/anthropic/v1/messages | Anthropic | https://api.anthropic.com/v1/messages |
/claude/v1/messages | Anthropic (alias) | https://api.anthropic.com/v1/messages |
/gemini/chat/completions | Gemini | https://generativelanguage.googleapis.com/v1beta/openai/chat/completions |
/copilot/chat/completions | Copilot | https://api.githubcopilot.com/chat/completions |
The provider prefix (/openai/, /anthropic/, etc.) can appear before or after /v1/:
/v1/openai/chat/completionsand/openai/v1/chat/completionsboth work.
For unprefixed paths, the endpoint name determines the provider:
messages→ Anthropic (this is Anthropic’s native endpoint)- Everything else → OpenAI (default)
Auth headers are forwarded as-is, so each request must include the correct key for its target upstream.
Concurrency and threads
Section titled “Concurrency and threads”No explicit thread configuration is required for normal use. FastAPI/Uvicorn handles simultaneous requests via async I/O, which is enough for concurrent provider traffic.
For higher throughput, run multiple workers (the Docker Compose prod posture already runs 4):
uvicorn server:app --host 0.0.0.0 --port 8080 --workers 4Example: one proxy, three providers
Section titled “Example: one proxy, three providers”# OpenAI — default for /v1/chat/completionscurl -sS http://localhost:8080/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'
# Gemini — explicit prefixcurl -sS http://localhost:8080/gemini/chat/completions \ -H "Authorization: Bearer $GEMINI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gemini-2.0-flash","messages":[{"role":"user","content":"hello"}]}'
# Claude/Anthropic — auto-detected from /v1/messages endpointcurl -sS http://localhost:8080/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ -d '{"model":"claude-sonnet-4-20250514","max_tokens":128,"messages":[{"role":"user","content":"hello"}]}'
# Or with explicit prefixcurl -sS http://localhost:8080/anthropic/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ -d '{"model":"claude-sonnet-4-20250514","max_tokens":128,"messages":[{"role":"user","content":"hello"}]}'MCP routing
Section titled “MCP routing”The proxy also routes Model Context Protocol traffic — JSON-RPC over
Streamable HTTP — alongside the LLM upstreams. MCP routes are declared before
the catch-all proxy route so /mcp/... isn’t swallowed as a chat-completions
path.
| Path | Methods | Behavior |
|---|---|---|
/mcp/{server_name} | GET, POST, DELETE | Resolve {server_name} against the per-tenant registry, walk the JSON-RPC envelope through DLP, forward to the upstream, and record a signed ledger row. |
/mcp (aggregator) | GET, POST, DELETE | Serve the union of every backend’s tools; route tools/call by {server}__{tool} namespace to the right backend. tools/list is served from a per-tenant in-memory catalog (5-minute TTL). |
- The agent’s
Authorizationheader is forwarded unchanged — the gateway is transparent on upstream auth and never caches or refreshes credentials. - DLP on the MCP path is observe-only by default (findings land in
dlp_alertswithsource_type='mcp'); per-tool allow/deny policy is handled bymcp_policy.py. DELETEterminates the upstream MCP session.
Custom and local upstreams
Section titled “Custom and local upstreams”The upstream registry can be extended or overridden via a config.yaml file in
the project root. New upstream names become valid path prefixes automatically —
no code changes required.
Config file location
Section titled “Config file location”| Source | Path |
|---|---|
| Default | config.yaml next to pyproject.toml |
| Override | Set KYDE_CONFIG=/absolute/path/to/config.yaml |
In the Docker Compose stack, the repo’s config.yaml is mounted into both the
gateway and the dashboard API at /app/config.yaml.
Format
Section titled “Format”upstreams: <name>: base: <url> # required — scheme + host (+ optional port), no trailing slash api_prefix: <path> # optional — prepended between base and the request pathOverride a built-in upstream
Section titled “Override a built-in upstream”Redirect the openai upstream to a local proxy:
upstreams: openai: base: http://my-openai-proxy.internal api_prefix: /v1Add a local LLM
Section titled “Add a local LLM”upstreams: ollama: base: http://localhost:11434 api_prefix: /v1
vllm: base: http://localhost:8000 api_prefix: /v1
lmstudio: base: http://localhost:1234 api_prefix: /v1Once defined, each name is a valid path prefix:
| Request path | Forwards to |
|---|---|
/ollama/v1/chat/completions | http://localhost:11434/v1/chat/completions |
/vllm/v1/chat/completions | http://localhost:8000/v1/chat/completions |
/lmstudio/v1/chat/completions | http://localhost:1234/v1/chat/completions |
The same prefix-stripping and /v1/ deduplication rules that apply to built-in
providers apply to custom ones too.
Point an agent at a custom upstream
Section titled “Point an agent at a custom upstream”import openai
client = openai.OpenAI( base_url="http://localhost:8000/ollama/v1", api_key="ollama", # placeholder — forwarded as-is)response = client.chat.completions.create( model="llama3.2", messages=[{"role": "user", "content": "hello"}],)Startup output
Section titled “Startup output”On startup, load_upstreams() prints a line for each entry loaded from the
config file so you can confirm the registry was applied:
✓ config: upstream 'ollama' → http://localhost:11434 ✓ config: upstream 'openai' → http://my-openai-proxy.internalEntries that are missing the required base field are skipped with a warning.
If config.yaml does not exist, the built-in defaults are used silently.
kyde keygen [--type local|tpm] # generate keys (default: local Ed25519)kyde key # show all keys (local & TPM), TPM status, active keykyde ledger list # show recent entrieskyde ledger verify # verify full chain integritykyde ledger show <id> # detailed view of one entryIn the Docker Compose stack, prefix with docker compose exec kyde-gateway ….
Key generation
Section titled “Key generation”kyde keygen # Ed25519 (default, backward compatible)kyde keygen --type local # Ed25519 software keykyde keygen --type tpm # ECDSA P-256 hardware key (requires TPM + tpm2-pytss)kyde keygen --type local --force # Overwrite existing key (caution!)Key protection: keygen refuses to overwrite existing keys unless --force is passed. This prevents accidental key loss.
Key information
Section titled “Key information”The kyde key command shows:
- TPM Status: Whether TPM is accessible (if not installed, shows ✗)
- Local Software Key: Ed25519 key status and whether it’s active
- TPM Key: ECDSA P-256 key status and whether it’s active (precedence if TPM accessible)
- Active Public Key: The fingerprint and PEM of the currently used key
Agent identity
Section titled “Agent identity”Agents are identified by the X-Agent-ID header:
client = openai.OpenAI( base_url="http://localhost:4000/v1", default_headers={"X-Agent-ID": "my-research-agent-v1"},)Without it, the proxy derives a consistent pseudonymous ID from the API key hash.
Policy enforcement (DLP prevention)
Section titled “Policy enforcement (DLP prevention)”Beyond logging, the proxy can block requests inline before they reach the
upstream. Inline DLP prevention runs on the request hot path and returns a
403 when a payload matches an enforced policy:
- Regex prevention — a pattern match survives the allowlist, clears
DLP_REGEX_THRESHOLD, and the pattern is explicitly opted into prevention (per-pattern, via adlp_prevention_patternsrow). - BERT prevention — the classifier score clears
DLP_BERT_THRESHOLDfor a non-allowlisted label.
Design points:
- Fail-open — if the scanner is unreachable, the request is forwarded and a high-severity incident is raised rather than taking the gateway down.
- Delta-only — since LLM clients re-send the whole conversation each turn, only messages appended since the last forwarded entry are scanned, so a match in earlier history doesn’t permanently block a session.
- Block responses never echo matched values — only pattern ids/names/severities.
The post-hoc scanner (dlp.py) still detects and alerts after forwarding;
inline prevention (dlp_prevention.py) is the enforcement counterpart.
Architecture
Section titled “Architecture”src/kyde/├── __init__.py Package marker├── proxy.py Entry point + CLI dispatch├── commands.py CLI: keygen, serve, ledger inspection├── server.py FastAPI proxy, request interception, streaming, route order├── config.py Upstream registry loader (merges defaults + config.yaml)├── ledger.py Append-only Postgres ledger (JSONB), hash chaining├── signing.py Ed25519 keypair management, sign/verify├── dashboard.py FastAPI audit dashboard││ # MCP routing├── mcp_proxy.py Per-server MCP JSON-RPC proxy (/mcp/{server_name})├── mcp_aggregator.py Bare /mcp aggregator — union tool catalog + namespaced routing├── mcp_registry.py Per-tenant MCP server registry├── mcp_policy.py Per-tool allow/deny policy enforcement├── mcp_ledger.py Signed ledger rows for MCP calls││ # DLP├── dlp.py Post-hoc DLP scanner → alerts (observe)├── dlp_prevention.py Inline request-side blocking → 403 (enforce)├── dlp_policies.py DLP policies + allowlist└── dlp_json_walk.py Walk JSON-RPC params/results through DLP(Plus supporting modules: auth.py, crypto.py, settings.py, audit_log.py,
notifications.py, topology.py, migrations/, and others.)
Ledger entry fields
Section titled “Ledger entry fields”| Field | Description |
|---|---|
agent_id | Who acted |
action_type | chat or tool_call |
why | Last N messages before the action (causal context) |
input_hash | SHA-256 of full request |
output_hash | SHA-256 of full response |
tool_calls | Extracted tool name + args |
prompt_tokens | Input/up token count from upstream |
completion_tokens | Output/down token count from upstream |
prev_hash | Hash of previous entry (chain) |
entry_hash | Hash of this entry’s signed fields |
signature | Ed25519 signature over all above fields |
Extending toward hardware roots
Section titled “Extending toward hardware roots”The signing key in ~/.agent-ledger/signing.key can be replaced with a PKCS#11
interface to a hardware security module (HSM) or TPM. The core/signing.py
module is the only component that needs to change — everything else stays identical.
# Future: swap software key for HSM-backed keyprivate_key = load_pkcs11_key(slot=0, pin=os.environ["HSM_PIN"])This is the extensibility point that takes the proxy from software-rooted to hardware-rooted trust without touching the ledger or proxy logic.