Lazy loaded image
从 0 到 1 打通 LLM Access Gateway:鉴权、非流式与 SSE 流式链路验证
字数 2611阅读时长 7 分钟
2026-4-5
2026-4-5
type
Post
status
Published
date
Apr 5, 2026
slug
gatewayfirst
summary
tags
gateway
category
icon
password

摘要

本文只回答一个问题:
当前这个 LLM Access Gateway,是否已经具备了一个可本地复现、可直接验收的最小接入闭环?
这里的“最小闭环”指四条链路同时成立:
  1. 缺失 API Key 会被拒绝
  1. 无效 API Key 会被拒绝
  1. 鉴权通过后的非流式请求能够返回 OpenAI-compatible JSON
  1. 鉴权通过后的流式请求能够返回 text/event-stream,逐步输出 chunk,并以 data: [DONE] 结束
如果这四条路径不能稳定成立,那么后续 quota、routing、observability、deployment 都没有讨论价值。
本文最终证明的是:
  • 入口鉴权边界已经成立
  • 非流式与 SSE 流式返回已经成立
  • 项目已经从“仓库骨架”进入“可本地验收的 MVP”

一、阶段目标

这个系统是否已经能稳定接住请求、拒绝非法请求,并对合法请求给出正确的 JSON / SSE 响应。

二、最小链路

本文只看最小请求路径:
notion image
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

notion image

这一步证明了什么

  • 鉴权 middleware 已经在 chat handler 之前生效
  • 请求不会在缺失 key 的情况下落到下游聊天逻辑
  • 网关的“拒绝入口”已经成立

2)invalid api key → 401

notion image

这一步证明了什么

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

3)valid key, non-stream → 200

notion image

这一步证明了什么

  • 鉴权成功路径已经成立
  • 请求已经能够穿过 handler、service 和 provider 层
  • 网关至少能够完成一条 OpenAI-compatible 的非流式返回
最小成功链路成立了。

4)stream=truetext/event-stream + [DONE]

notion image

这一步证明了什么

  • 网关已经能正确返回 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
这一段链路成立,至少依赖三个点同时满足:
  1. 设置 Content-Type: text/event-stream
  1. 逐 chunk 执行 fmt.Fprintf(w, "data: %s\n\n", payload)Flush()
  1. 在流结束时输出 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 跑通了,并且能用一组清晰、可复现、可交叉验证的证据证明它。
这还远远不够。
但对于一个治理型网关项目来说,这正是值得记录的第一个阶段性里程碑

十一、下一步计划

在这篇文章之后,项目的重点不应该是继续“加一个能展示的功能”,而是沿着治理型网关的方向继续推进。
下一步更有价值的方向是:
  1. 多租户 API Key、RPM / TPM、usage 记录
  1. provider routing、health check、fallback
  1. logs / metrics / traces
  1. 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=truetext/event-stream + [DONE]

验证命令

预期结果

这一步证明了什么

  • 网关已经能正确返回 Content-Type: text/event-stream
  • SSE chunk 会被逐步写出,而不是等全部完成后一次性返回
  • 流式链路存在明确结束标志 [DONE]
这是整个阶段里最重要的验证点。
因为如果没有它,这个项目最多只能算“带鉴权的模型调用包装层”;
SSE 打通之后,它才开始具备“模型接入网关”的雏形。
回到首页