Kong AI Gateway — spendguard 插件(Go + Lua)
2026 年的 Kong AI Gateway 自带
ai-proxy/ai-prompt-guard/ai-rate-limiting-advanced这一套插件家族。SpendGuard 以spendguard的身份并列接入 —— 这是一个运行在access+body_filter阶段的 Kong 插件,在任何提供商收到请求之前就把上游 LLM API 流量拦下来做闸口。 Go 插件(plugins/kong/spendguard-go/)是官方支持的生产路径; 另有一个实验性 Lua 移植版放在plugins/kong/spendguard-lua/, 给那些没法在 worker 旁边跑go-plugin-server子进程的 Kong OSS 部署用。
为什么要做成 Kong 插件
Section titled “为什么要做成 Kong 插件”Kong DataPlane 的 worker 和 SpendGuard sidecar pod 处在不同的网络命名空间, 所以标准 adapter 那套 UDS 契约(SO_PEERCRED 认证、pod 内通信)根本够不着。 Kong 插件改走 HTTPS+mTLS 配套监听器,跑的还是同一条决策通道:
POST /v1/tokenize—— 给请求体做 token 计数POST /v1/decision—— 预留裁决(ALLOW/DENY/DEGRADE)POST /v1/trace—— 在 body 收尾时发出LLM_CALL_POST.SUCCESS/RUN_ABORTED这条 trace 事件
配套服务就是跑在 “HTTP companion” 模式下的 SpendGuard sidecar —— 同一个二进制、同一条审计链,只是传输方式不同。
生产环境(Helm)
Section titled “生产环境(Helm)”helm install spendguard ./charts/spendguard \ --set kongPlugin.enabled=true \ --set kongPlugin.tenantId=00000000-0000-4000-8000-000000000001 \ --set kongPlugin.svid.secretName=spendguard-kong-svid这个 chart 给 Kong 路径渲染出四个资源:
- Deployment
release-name-spendguard-kong-companion—— sidecar 二进制,带SPENDGUARD_SIDECAR_HTTP_COMPANION_PORT=8443启动 - Service
release-name-spendguard-kong-companion—— ClusterIP, 暴露 8443 端口(mTLS)+ 9090 端口(探针) - ServiceAccount —— 给 cert-manager 绑定用的 workload identity
- NetworkPolicy —— ingress 限定在带
app.kubernetes.io/name=kong标签的 pod(也就是 Kong DataPlane 标签)
插件本身以 SpendGuard Go 插件二进制(或 Lua rockspec)的形式分发。
按
examples/kong-gateway-composite/go-build.sh
这份参考脚本把它打进你的 Kong DataPlane 镜像。
examples/kong-gateway-composite/
下的参考清单带了两个 KongPlugin 资源(Go + Lua)。挑一个 apply,
然后给目标 Service 打 annotation:
kubectl apply -f examples/kong-gateway-composite/kong-plugin-crd.yamlkubectl annotate svc my-llm-upstream \ konghq.com/plugins=spendguard-go --overwriteDB-less 模式(KONG_DATABASE=off)下,运维人员把插件内联进声明式的
kong.yml —— 完整示例见
examples/kong-gateway-composite/kong-conf.yaml。
插件 schema 在 Go 和 Lua 两个发行版里字段完全对齐。两者吃的是同一份
KongPlugin CRD。
| 键 | 必填 | 默认值 | 用途 |
|-----|----------|---------|---------|
| sidecar_url | 是 | — | SpendGuard 配套服务的 HTTPS URL,必须以 https:// 开头。 |
| sidecar_ca_pem / sidecar_ca_file | 是(二选一) | — | 给配套服务的 workload 证书签名的 CA bundle。 |
| client_cert_pem / client_cert_file | 是(二选一) | — | 插件 workload 证书(SVID URI SAN 里编码了租户)。 |
| client_key_pem / client_key_file | 是(二选一) | — | 对应的私钥。 |
| tenant_id | 是 | — | 租户 UUID;必须跟 SVID URI SAN 里的租户一致。 |
| fail_open | 是 | false | 默认 fail-closed。设成 true 时,sidecar 出错会降级放行并打一条 kong.log.warn。 |
| timeout_ms | 是 | 500 | 到配套服务的单次请求 HTTP 超时(50–30000ms)。 |
| budget_id | 否 | — | 多预算租户可选的显式预算绑定。 |
| prompt_class | 是 | general | 契约求值器用的 prompt class。 |
PRIORITY = 950,高于 Kong 的 ai-proxy(770)。SpendGuard 在
ai-proxy 之前触发,所以预留发生在上游鉴权和提供商路由之前。
约束的理由见
docs/specs/coverage/D09_kong_ai_gateway/review-standards.md
§6.3。
Kong 的 access 阶段需要拿到完整请求体,SpendGuard 才能给它做 token
计数。在你要做闸口的路由上设 request_buffering: true;否则
kong.request.get_raw_body() 返回 nil,插件会以 SPENDGUARD_EMPTY_BODY
fail closed。
按
docs/specs/coverage/D09_kong_ai_gateway/design.md
§3.3,插件跑在两个阶段:
access(预留)
Section titled “access(预留)”kong.request.get_raw_body()—— 取出缓冲好的上游请求体- 通过 JSON 键 + 路径判断提供商的报文形态(OpenAI
/v1/chat/completions还是 Anthropic/v1/messages) POST /v1/tokenize——input_tokensPOST /v1/decision—— 裁决- 分支:
- ALLOW → 把
reservation_id塞进kong.ctx.shared,放行 - DENY →
kong.response.exit(429, { code: "SPENDGUARD_DENY" }) - DEGRADE →
fail_open=false→kong.response.exit(503), 否则打日志 + 放行
- ALLOW → 把
body_filter(提交)
Section titled “body_filter(提交)”- 累积
ngx.arg[1]的 chunk,直到ngx.arg[2]为 true - 解析提供商用量(OpenAI
usage.prompt_tokens/ Anthropicusage.input_tokens) POST /v1/trace,带ACCEPTED+ token 计数(上游 5xx / 解析失败时 带REJECTED)- 置位
kong.ctx.shared[spendguard_committed] = "1",让重复的 filter 调用变成 no-op
SpendGuard 的 Kong 插件只做 INV-5 流尾提交。 闸口跑在 access 阶段,
对账跑在 body 收尾时 —— 流式过程中不会逐 token 触发上限。
具体来说:
- 不做流式 SSE 的预算强制。 Kong 的
response_buffering: true会在body_filter阶段触发之前把整个上游响应缓冲下来;SpendGuard 在 body 收尾时一次性读这份缓冲好的响应体。按 design §3.5,流中途取消是 v1 的非目标。 - 不做 Bedrock SigV4 改写。 v1 覆盖 OpenAI 形态和 Anthropic 形态的
payload。Bedrock 路由复用 Kong
ai-proxy的 route_type 字段; SpendGuard 不改上游鉴权的 header。 - 不集成 Konnect SaaS plane。 分发只走
KongPluginCRD 或kong.conf。SpendGuard 不往 Kong 托管的插件 registry 推。
Lua 兜底
Section titled “Lua 兜底”Lua 移植版是给那些没法在 worker 旁边跑 go-plugin-server 子进程的
Kong OSS 3.0–3.5 部署用的。它覆盖同样的 access + body_filter
生命周期,文档里标注为实验性:
luarocks install spendguard然后用同一份 KongPlugin CRD、配 plugin: spendguard 来绑定(Lua
移植版和 Go 插件都注册在同一个 spendguard 插件名下)。Lua 移植版
拿不到 conformance-test 的保证 —— 对齐情况表见
plugins/kong/spendguard-lua/README.md。
附带的 docker-compose demo 端到端跑通完整的 ALLOW + DENY + STREAM 矩阵,对的是真实的 Kong gateway + SpendGuard 配套服务 + 计数桩提供商:
make demo-up DEMO_MODE=kong_gateway_real这个模式起 postgres + sidecar + kong-companion + kong-gateway + counting-stub,然后通过 Kong 发三个调用:
- ALLOW —— 预算内的小请求体 → HTTP 200,计数桩计数器
+1, sidecar 里有预留行。 - DENY ——
X-Spendguard-Estimate-Override: 2000000000header 把种子数据里设的 1B 硬上限冲爆 → HTTP 429,计数桩计数器不变 (证明闸口在上游调用之前就触发了)。 - STREAM —— 预算内的
stream=true请求体 → HTTP 200,计数桩+1,有流尾提交行。
干净跑通时的成功行:
[demo] kong_gateway_real ALL 3 steps PASS (ALLOW + DENY + STREAM)完整细节和 verify-SQL 闸口见
deploy/demo/kong_gateway/README.md。
SPENDGUARD_EMPTY_BODY—— 在路由上设request_buffering: true。access阶段要靠缓冲好的请求体来做 token 计数。SPENDGUARD_UNRECOGNISED_REQUEST—— 请求体看着既不像 OpenAI/v1/chat/completions也不像 Anthropic/v1/messages的形态。v1 只 覆盖这两家提供商;Bedrock / Mistral 先过一遍ai-proxy,让 SpendGuard 看到归一化之后的请求体。SPENDGUARD_FAIL_CLOSED: plugin misconfigured——sidecar_url/client_cert_*/tenant_id缺失或格式不对。查启动日志;配错了插件 会拒绝加载。SPENDGUARD_TOKENIZE_UNREACHABLE—— 插件够不着配套服务。检查 NetworkPolicy 渲染得对不对(Kong DataPlane 的 pod 必须带app.kubernetes.io/name=kong)。429 SPENDGUARD_DENY但实际并没超额 —— 契约求值器发出了硬上限 STOP。检查种子预算 / bundle 配置;X-Spendguard-Estimate-Override这个 demo header 只用于 demo,生产二进制里没编进去。
- Quickstart —— 5 分钟把整套 SpendGuard 跑起来
- 契约 YAML 参考 —— 编写 allow/stop 规则
- Drop-in:LiteLLM proxy 模式 —— egress 路径的替代方案
- 其他 adapter 集成:Pydantic-AI · LangChain · OpenAI Agents SDK · Microsoft AGT