type
Post
status
Published
date
Apr 5, 2026
slug
gatewayfirst
summary
tags
gateway
category
icon
password
摘要
本文只回答一个问题:
当前这个LLM Access Gateway,是否已经具备了一个可本地复现、可直接验收的最小接入闭环?
这里的“最小闭环”指四条链路同时成立:
- 缺失 API Key 会被拒绝
- 无效 API Key 会被拒绝
- 鉴权通过后的非流式请求能够返回 OpenAI-compatible JSON
- 鉴权通过后的流式请求能够返回
text/event-stream,逐步输出 chunk,并以data: [DONE]结束
如果这四条路径不能稳定成立,那么后续 quota、routing、observability、deployment 都没有讨论价值。
本文最终证明的是:
- 入口鉴权边界已经成立
- 非流式与 SSE 流式返回已经成立
- 项目已经从“仓库骨架”进入“可本地验收的 MVP”
一、阶段目标
这个系统是否已经能稳定接住请求、拒绝非法请求,并对合法请求给出正确的 JSON / SSE 响应。
二、最小链路
本文只看最小请求路径:

flowchart LR
A["Client / curl"] --> B["HTTP Router"]
B --> C["API Key Auth"]
C --> D["Chat Handler"]
D --> E["Chat Service"]
E --> F["Mock Provider"]
F --> D
D --> A
这里最需要单独验证的是 SSE。
因为普通非流式返回本质上是“一次处理后一次写回”,而 SSE 是“建立响应后持续写出 chunk,并显式结束流”。
这意味着,
stream=true 不是普通 HTTP 返回的附属行为,而是一条独立的协议链路。四、测试矩阵
阶段 1 的核心测试如下:
用例 | 场景 | 请求条件 | 预期状态码 | 预期响应特征 | 证明点 |
TC-01 | missing key | 无 Authorization | 401 | {"error":"missing api key"} | 鉴权失败分支可用 |
TC-02 | invalid key | Authorization: Bearer invalid-key | 401 | {"error":"invalid api key"} | 无效 key 被正确拒绝 |
TC-03 | valid key, non-stream | Authorization: Bearer lag-local-dev-key | 200 | JSON,包含 "object":"chat.completion" | 鉴权成功 + 非流式链路可用 |
TC-04 | valid key, stream | 有效 key, "stream": true | 200 | Content-Type: text/event-stream,多条 data:,最终 [DONE] | 鉴权成功 + SSE 流式链路可用 |
它覆盖了两类路径:
失败路径
- 缺失 key
- 无效 key
成功路径
- 普通 JSON 返回
- SSE 流式返回
也正是这四条路径,把“网关只是启动了”和“网关已经形成最小接入闭环”区分开来。
五、四组验证结果
1)missing api key → 401

这一步证明了什么
- 鉴权 middleware 已经在 chat handler 之前生效
- 请求不会在缺失 key 的情况下落到下游聊天逻辑
- 网关的“拒绝入口”已经成立
2)invalid api key → 401

这一步证明了什么
- Bearer 解析已经成立
- API Key 查找已经成立
- “找不到 key” 被正确映射到了
401,而不是错误地落成500
这是一个很关键的验证点,因为它证明的不是“某个 key 可用”,而是:
错误语义被正确建模。
3)valid key, non-stream → 200

这一步证明了什么
- 鉴权成功路径已经成立
- 请求已经能够穿过 handler、service 和 provider 层
- 网关至少能够完成一条 OpenAI-compatible 的非流式返回
最小成功链路成立了。
4)stream=true → text/event-stream + [DONE]

这一步证明了什么
- 网关已经能正确返回
Content-Type: text/event-stream
- SSE chunk 会被逐步写出,而不是等全部完成后一次性返回
- 流式链路存在明确结束标志
[DONE]
这是整个阶段里最重要的验证点。
因为如果没有它,这个项目最多只能算“带鉴权的模型调用包装层”;
而 SSE 打通之后,它才开始具备“模型接入网关”的雏形。
六、关键代码路径
1. 路由与鉴权边界
入口在:
internal/api/router.go:17-31
这里定义了
POST /v1/chat/completions,并在 chat handler 之前执行 requireAPIKey(...)。也就是说,缺失 key、无效 key、禁用 key 都会在入口层被直接拒绝。
2. Handler 的流式 / 非流式分流
HTTP 层处理位于:
internal/api/handlers/chat.go:26-62
这里根据
req.Stream 分叉:false:普通 JSON 返回
true:进入streamCompletion(...)
也正因为 handler 层显式分流,本文才能把“普通返回成立”和“SSE 返回成立”分开证明。
3. SSE 写出逻辑
流式实现位于:
internal/api/handlers/chat.go:64-108
这一段链路成立,至少依赖三个点同时满足:
- 设置
Content-Type: text/event-stream
- 逐 chunk 执行
fmt.Fprintf(w, "data: %s\n\n", payload)并Flush()
- 在流结束时输出
data: [DONE]
缺少其中任何一个,都不能算真正打通 SSE。
4. Service 层抽象
服务层位于:
internal/service/chat/service.go:80-135
这里负责把 provider 输出统一为 service 层响应:
- 非流式:
CompletionResponse
- 流式:
CompletionChunk
- 增量内容:
choices[].delta
也就是说,这一阶段真正被证明的,不只是 curl 拿到了结果,而是:
入口鉴权边界、HTTP 协议写出逻辑、service 响应抽象,这三层最小链路已经同时成立。
七、这一阶段能说明什么
1)它不是“只有仓库结构的空项目”
因为它已经能:
- 启动本地依赖
- 接受真实 HTTP 请求
- 返回真实 JSON / SSE 响应
2)它不是“只能成功、不能失败”的演示代码
因为它已经对最基本的失败路径给出了稳定响应语义:
- missing key →
401
- invalid key →
401
失败路径能不能稳定成立,决定了这个项目更像是“demo”,还是“系统雏形”。
3)它已经具备模型接入层最基础的流式代理能力
这不是“会返回一段文本”这么简单,而是:
- 会建立 SSE 响应
- 会逐 chunk 输出
- 会显式结束流
这一步对于模型接入层是本质性的,不是锦上添花。
九、待完成
限制条件 | 当前情况 | 待完成 |
上游 provider | 仍然是 mock provider | 真实上游延迟、错误语义、usage 口径、多 provider 兼容问题 |
路由范围 | 只围绕 POST /v1/chat/completions | 更完整的 provider 管理面与治理逻辑 |
性能验证 | 未做压测 | TTFT、P95、P99、吞吐等性能结论 |
交付验证 | 仅本地可复现 | 生产级部署、运维与稳定性交付能力 |
把这些边界压缩成一句话,就是:
这一阶段只证明它已经能接住请求并正确返回,还没有证明它已经能稳定治理、观测并交付请求。
十、结语
我已经把一个面向模型接入的最小系统闭环,从 0 到 1 跑通了,并且能用一组清晰、可复现、可交叉验证的证据证明它。
这还远远不够。
但对于一个治理型网关项目来说,这正是值得记录的第一个阶段性里程碑。
十一、下一步计划
在这篇文章之后,项目的重点不应该是继续“加一个能展示的功能”,而是沿着治理型网关的方向继续推进。
下一步更有价值的方向是:
- 多租户 API Key、RPM / TPM、usage 记录
- provider routing、health check、fallback
- logs / metrics / traces
- Docker / Kubernetes / 压测 / 故障演练
也就是说:
- 阶段 1 证明“它能接住请求”
- 后续阶段才会证明“它能治理请求、观测请求并稳定交付请求”
验证环境与方法
本文的验证方式,不依赖前端页面,也不依赖第三方测试平台,而是直接使用本地环境 +
curl 做最小验收。本地开发路径见项目文档:
docs/local-development.md
启动命令
其中:
go run ./cmd/devinit会初始化本地开发租户和 API Key
- 本地有效 key 为
lag-local-dev-key
- 网关启动后监听
http://127.0.0.1:8080
本文只使用这条最小本地路径,不引入额外复杂度。
1)missing api key → 401
验证命令
预期结果
这一步证明了什么
- 鉴权 middleware 已经在 chat handler 之前生效
- 请求不会在缺失 key 的情况下落到下游聊天逻辑
- 网关的“拒绝入口”已经成立
2)invalid api key → 401
验证命令
预期结果
这一步证明了什么
- Bearer 解析已经成立
- API Key 查找已经成立
- “找不到 key” 被正确映射到了
401,而不是错误地落成500
这是一个很关键的验证点,因为它证明的不是“某个 key 可用”,而是:
错误语义被正确建模。
3)valid key, non-stream → 200
验证命令
预期结果
这一步证明了什么
- 鉴权成功路径已经成立
- 请求已经能够穿过 handler、service 和 provider 层
- 网关至少能够完成一条 OpenAI-compatible 的非流式返回
这里要特别强调一点:
这并不证明它已经接通了真实 provider,也不证明它已经具备复杂路由能力。它只证明了一件事:
最小成功链路成立了。
4)stream=true → text/event-stream + [DONE]
验证命令
预期结果
这一步证明了什么
- 网关已经能正确返回
Content-Type: text/event-stream
- SSE chunk 会被逐步写出,而不是等全部完成后一次性返回
- 流式链路存在明确结束标志
[DONE]
这是整个阶段里最重要的验证点。
因为如果没有它,这个项目最多只能算“带鉴权的模型调用包装层”;
而 SSE 打通之后,它才开始具备“模型接入网关”的雏形。
分享