Kong AI Gateway — spendguard plugin (Go + Lua)
Kong AI Gateway ships the
ai-proxy/ai-prompt-guard/ai-rate-limiting-advancedplugin family in 2026. SpendGuard slots in alongside asspendguard— a Kong plugin that runs inaccess
body_filterand gates upstream LLM API traffic before any provider receives it. The Go plugin (plugins/kong/spendguard-go/) is the supported production path; an experimental Lua port lives atplugins/kong/spendguard-lua/for Kong OSS deployments that cannot host ago-plugin-serversubprocess.
Why a Kong plugin
Section titled “Why a Kong plugin”Kong DataPlane workers live in a separate network namespace from the SpendGuard sidecar pod, so the standard adapter UDS contract (SO_PEERCRED-authenticated, in-pod) is unreachable. The Kong plugin speaks the same decision lane over an HTTPS+mTLS companion listener instead:
POST /v1/tokenize— token-count the request bodyPOST /v1/decision— reserve verdict (ALLOW/DENY/DEGRADE)POST /v1/trace— emit theLLM_CALL_POST.SUCCESS/RUN_ABORTEDtrace event at end-of-body
The companion service ships as a SpendGuard sidecar in “HTTP companion” mode — same binary, same audit chain, different transport.
Install
Section titled “Install”Production (Helm)
Section titled “Production (Helm)”helm install spendguard ./charts/spendguard \ --set kongPlugin.enabled=true \ --set kongPlugin.tenantId=00000000-0000-4000-8000-000000000001 \ --set kongPlugin.svid.secretName=spendguard-kong-svidThe chart renders four resources for the Kong path:
- Deployment
release-name-spendguard-kong-companion— sidecar binary running withSPENDGUARD_SIDECAR_HTTP_COMPANION_PORT=8443 - Service
release-name-spendguard-kong-companion— ClusterIP exposing port 8443 (mTLS) + 9090 (probes) - ServiceAccount — workload identity for cert-manager binding
- NetworkPolicy — ingress restricted to pods carrying
app.kubernetes.io/name=kong(the Kong DataPlane label)
The plugin itself ships as the SpendGuard Go plugin binary (or the
Lua rockspec). Bake it into your Kong DataPlane image via the
reference recipe in
examples/kong-gateway-composite/go-build.sh.
Bind the plugin
Section titled “Bind the plugin”The reference manifests at
examples/kong-gateway-composite/
ship two KongPlugin resources (Go + Lua). Apply one, then annotate
the target Service:
kubectl apply -f examples/kong-gateway-composite/kong-plugin-crd.yamlkubectl annotate svc my-llm-upstream \ konghq.com/plugins=spendguard-go --overwriteFor DB-less mode (KONG_DATABASE=off) operators inline the plugin
into their declarative kong.yml — see
examples/kong-gateway-composite/kong-conf.yaml
for the worked example.
Configuration
Section titled “Configuration”The plugin schema is field-equivalent across the Go and Lua
distributions. Both accept the same KongPlugin CRD.
| Key | Required | Default | Purpose |
|-----|----------|---------|---------|
| sidecar_url | YES | — | HTTPS URL of the SpendGuard companion. Must start with https://. |
| sidecar_ca_pem / sidecar_ca_file | YES (one of) | — | CA bundle that signs the companion’s workload cert. |
| client_cert_pem / client_cert_file | YES (one of) | — | Plugin workload cert (SVID URI SAN encodes the tenant). |
| client_key_pem / client_key_file | YES (one of) | — | Matching private key. |
| tenant_id | YES | — | Tenant UUID; must match the SVID URI SAN tenant. |
| fail_open | YES | false | Fail-closed by default. When true, sidecar errors degrade-to-allow with a kong.log.warn. |
| timeout_ms | YES | 500 | Per-request HTTP timeout to the companion (50–30000ms). |
| budget_id | NO | — | Optional explicit budget binding for multi-budget tenants. |
| prompt_class | YES | general | Contract-evaluator prompt class. |
Plugin priority
Section titled “Plugin priority”PRIORITY = 950, above Kong’s ai-proxy (770). SpendGuard fires
before ai-proxy so the reserve happens upstream of upstream
auth and provider routing. See
docs/specs/coverage/D09_kong_ai_gateway/review-standards.md
§6.3 for the constraint rationale.
Request buffering
Section titled “Request buffering”Kong’s access phase needs the full request body before SpendGuard
can token-count it. Set request_buffering: true on the routes you
gate; otherwise kong.request.get_raw_body() returns nil and the
plugin fails closed with SPENDGUARD_EMPTY_BODY.
Lifecycle
Section titled “Lifecycle”Per docs/specs/coverage/D09_kong_ai_gateway/design.md
§3.3, the plugin runs in two phases:
access (reserve)
Section titled “access (reserve)”kong.request.get_raw_body()— pull the buffered upstream body- Detect provider shape (OpenAI
/v1/chat/completionsvs Anthropic/v1/messages) by JSON keys + path POST /v1/tokenize—input_tokensPOST /v1/decision— verdict- Branch:
- ALLOW → stash
reservation_idinkong.ctx.sharedand fall through - DENY →
kong.response.exit(429, { code: "SPENDGUARD_DENY" }) - DEGRADE →
fail_open=false→kong.response.exit(503), otherwise log + fall through
- ALLOW → stash
body_filter (commit)
Section titled “body_filter (commit)”- Accumulate
ngx.arg[1]chunks untilngx.arg[2]is true - Parse provider usage (OpenAI
usage.prompt_tokens/ Anthropicusage.input_tokens) POST /v1/tracewithACCEPTED+ token counts (orREJECTEDon upstream 5xx / parse failure)- Flag
kong.ctx.shared[spendguard_committed] = "1"so a duplicate filter invocation is a no-op
Limitations
Section titled “Limitations”SpendGuard’s Kong plugin is INV-5 end-of-stream commit only. The
gate runs at the access phase and the reconciliation runs at
end-of-body — there is no token-by-token cap fired mid-flight.
Specifically:
- No streaming SSE budget enforcement. Kong’s
response_buffering: truebuffers the full upstream response before thebody_filterphase fires; SpendGuard reads the buffered body once at end-of-body. Mid-stream cancellation is a v1 non-goal per design §3.5. - No Bedrock SigV4 mutation. v1 covers OpenAI-shaped and
Anthropic-shaped payloads. Bedrock routing reuses Kong’s
ai-proxyroute_type field; SpendGuard does not mutate upstream-auth headers. - No Konnect SaaS plane integration. Distribution is via
KongPluginCRD orkong.confonly. SpendGuard does not ship to Kong’s hosted plugin registry.
Lua fallback
Section titled “Lua fallback”The Lua port exists for Kong OSS 3.0–3.5 deployments that cannot
host a go-plugin-server subprocess alongside the worker. It covers
the same access + body_filter lifecycle and is documented as
experimental:
luarocks install spendguardThen bind via the same KongPlugin CRD with plugin: spendguard
(the Lua port and Go plugin both register under the same spendguard
plugin name). The Lua port does NOT get the conformance-test
guarantee — see
plugins/kong/spendguard-lua/README.md
for the parity table.
A bundled docker-compose demo proves the full ALLOW + DENY + STREAM matrix end-to-end against a real Kong gateway + SpendGuard companion
- counting-stub provider:
make demo-up DEMO_MODE=kong_gateway_realThe mode boots postgres + sidecar + kong-companion + kong-gateway + counting-stub, then issues three calls through Kong:
- ALLOW — small body within budget → HTTP 200, counting-stub
counter
+1, sidecar reservation row exists. - DENY —
X-Spendguard-Estimate-Override: 2000000000header blows past the seeded 1B hard-cap → HTTP 429, counting-stub counter UNCHANGED (proves the gate fires before the upstream call). - STREAM —
stream=truebody within budget → HTTP 200, counting-stub+1, end-of-body commit row.
Success line on a clean run:
[demo] kong_gateway_real ALL 3 steps PASS (ALLOW + DENY + STREAM)Full details and the verify-SQL gates are in
deploy/demo/kong_gateway/README.md.
Troubleshooting
Section titled “Troubleshooting”SPENDGUARD_EMPTY_BODY— Setrequest_buffering: trueon the route. Theaccessphase needs the buffered body to token-count.SPENDGUARD_UNRECOGNISED_REQUEST— The body did not look like OpenAI/v1/chat/completionsor Anthropic/v1/messagesshape. v1 covers only those two providers; route Bedrock / Mistral throughai-proxyfirst so SpendGuard sees the normalised body.SPENDGUARD_FAIL_CLOSED: plugin misconfigured— Missing or malformedsidecar_url/client_cert_*/tenant_id. Check the startup log; the plugin refuses to load on misconfiguration.SPENDGUARD_TOKENIZE_UNREACHABLE— The plugin could not reach the companion. Check the NetworkPolicy renders correctly (the Kong DataPlane pods must carryapp.kubernetes.io/name=kong).429 SPENDGUARD_DENYwithout a real overage — The contract evaluator emitted a hard-cap STOP. Check the seeded budget / bundle config; theX-Spendguard-Estimate-Overridedemo header is demo-only and not compiled into production binaries.
Related
Section titled “Related”- Quickstart — full SpendGuard stack up in 5 minutes
- Contract YAML reference — author allow/stop rules
- Drop-in: LiteLLM proxy mode — egress-path alternative
- Other adapter integrations: Pydantic-AI · LangChain · OpenAI Agents SDK · Microsoft AGT