# Mini LLM Gateway

> 周期建议：10–14 天。只连接两个本地 Mock Provider；不得使用真实模型账号、Provider Key 或 Key 池。

## 项目背景

一个小型 AI 应用同时面对“不同 Provider 协议不一致、模型名称经常变化、上游超时/限流、调用成本难核对”等问题。如果每个业务都直接连接模型，鉴权、重试、回退和用量统计会散落在各处；如果网关设计不严谨，又可能把不同调用方的数据混在一起，重复输出流，或在已经向客户端发送内容后切换 Provider 造成拼接答案。

团队需要一个最小、本地、OpenAI-compatible 的 LLM Gateway 教学样板。它只验证契约、路由、故障边界、流式语义、每 Key 限流和用量留痕，不建设真实商业平台。

## 项目目标

交付一个本地可运行的 HTTP 服务，实现以下固定公开接口：

- `GET /health`
- `GET /v1/models`
- `POST /v1/chat/completions`
- `GET /v1/usage`

系统通过 Gateway Key 鉴权，把模型别名路由到两个可控 Mock Provider，支持非流式和 SSE，在严格规则下回退，并确保每个入站 Chat Completion 请求最终只产生一条用量记录。

## 用户故事

- 作为应用开发者，我希望使用稳定的 OpenAI-compatible 接口和模型别名，从而不关心 Mock Provider 差异。
- 作为调用方负责人，我希望自己的 Key 有独立限流和独立用量视图，从而避免相互干扰。
- 作为平台开发者，我希望超时和上游错误按明确规则回退，从而避免重复或拼接输出。
- 作为审计者，我希望每个请求有唯一记录且不泄漏 Key、Prompt 正文或响应正文，从而核对可靠性。

## 功能要求

### 必须完成

1. **固定 HTTP 契约**
   - `GET /health` 返回服务状态和两个 Mock Provider 的可用摘要，不泄漏配置或密钥。
   - `GET /v1/models` 返回当前 Gateway Key 可用的模型别名列表，采用 OpenAI 风格的 `object: "list"` 与 `data` 结构。
   - `POST /v1/chat/completions` 至少支持 `model`、`messages`、`stream`；非法 JSON、空消息、未知别名和超限字段返回稳定错误结构。
   - 非流式成功响应至少包含 `id`、`object`、`created`、`model`、`choices` 和 `usage`。
   - `GET /v1/usage` 只返回当前 Gateway Key 的用量记录，支持最小分页或条数限制。
2. **Gateway Key 鉴权与隔离**
   - 使用 `Authorization: Bearer <gateway-key>`；缺失/无效 Key 返回 `401`，无权限模型返回 `403`。
   - 持久化和配置中只保存 Key 的单向哈希；日志、错误和用量记录最多保存不可逆摘要/短指纹，不得保存或回显明文。
   - 至少准备两个本地测试 Key，分别绑定不同 Key 标识；只能通过安全的本地初始化方式获得测试明文。
3. **两个 Mock Provider 与模型别名**
   - 实现两个行为可配置、结果可区分的 Mock Provider Adapter。
   - 外部请求只使用稳定别名；路由配置把别名映射到主 Provider、备用 Provider 和 Provider 模型名。
   - 响应和用量可显示别名及实际 Provider 标识，但不得把内部凭证或敏感配置暴露给调用方。
4. **非流式路由与受控回退**
   - 主 Provider 在超时、`429` 或 `5xx` 时可以尝试一次备用 Provider。
   - 主 Provider 返回 `400`、`401`、`403` 时禁止回退，应返回映射后的明确错误。
   - 总超时、单 Provider 超时、最大回退次数集中配置；不能无界重试。
5. **SSE 流式语义**
   - `stream: true` 时使用 `text/event-stream`，每个正常内容事件格式为 `data: <JSON>\n\n`。
   - 每个已建立并完成的流必须且只能出现一次 `data: [DONE]\n\n`。
   - 只有在任何内容 chunk 发给客户端之前，且错误为超时、`429` 或 `5xx` 时才允许切换备用 Provider。
   - 一旦首个内容 chunk 已发送，后续上游失败不得切换 Provider；应发送一个结构化终止错误事件并以唯一 `[DONE]` 结束，审计状态标记为失败/部分输出。
   - `400`、`401`、`403` 在首 chunk 前也不得触发回退。
6. **每 Key 独立限流**
   - 实现固定窗口、滑动窗口或令牌桶之一；限流计数以 Gateway Key 标识隔离。
   - 网关自身限流返回 `429` 和可理解的重试信息，不调用任何 Provider。
   - Key A 达到限制不能消耗或阻塞 Key B 的额度。
7. **每请求唯一最终用量记录**
   - 每次进入 `POST /v1/chat/completions` 的请求都分配唯一 `request_id`，包括鉴权、校验、网关限流、Provider 和流式失败。
   - 最终恰好写入一条用量记录，不得因回退写成两条“客户端请求”；可在同一记录中保存尝试次数和实际 Provider。
   - 记录至少包含请求 ID、Key 标识/不可逆指纹、模型别名、实际 Provider、状态、回退次数、输入/输出 token、耗时和时间。
   - 鉴权/校验失败的 token 为 0；不得保存 Prompt、响应正文或明文 Key。Mock token 计数算法需确定、可测试并写入文档。
8. **可观测错误**
   - 所有响应带请求 ID；错误结构能区分网关校验、网关限流、Provider 错误、超时和流中断。
   - 提供受控输入或测试配置，稳定触发两个 Provider 的各类错误，不依赖随机故障。

### 可选增强

- 配置热加载、健康熔断或按权重路由；必须保持默认确定性测试路径。
- 管理员本地汇总视图，但不得扩展为公网租户管理或计费系统。
- `max_tokens` 等更多兼容字段；必须明确支持子集，不能声称完整兼容所有 OpenAI API。
- 用 SQLite 等持久化用量记录，并证明重启后一致性。

## 非功能要求

- **可运行性**：一条本地命令启动网关和两个 Mock Provider；不设置真实 Key 也能验收。
- **可靠性**：请求 ID 唯一；最终用量写入具备 exactly-once 的可观察效果；回退和 SSE 不能产生拼接答案或重复 `[DONE]`。
- **性能**：Mock 正常路径下，网关自身增加的 p95 延迟应小于 100ms（不含配置的 Provider 延迟），并说明测量方法。
- **并发**：至少用自动化测试证明两个 Key 的限流隔离和同一请求的最终记录唯一。
- **安全**：Key 使用单向哈希校验与常量时间比较；日志默认脱敏；请求体、消息长度和并发有上限。
- **隐私**：默认不记录 Prompt/响应正文；用量查询严格按 Key 隔离。
- **兼容性**：README 明确实现的是 OpenAI-compatible 子集、已支持字段和不支持项。

## 范围与限制

### 范围内

- 本地单进程或小型多进程网关。
- 两个 Mock Provider、稳定模型别名、Gateway Key、路由/回退、SSE、限流和用量审计。
- 合成消息、合成 Key 和确定性错误场景。

### 范围外

- 真实 OpenAI/其他 Provider 账号、API Key 池、凭证托管或账号共享。
- OAuth、用户注册、支付、充值、余额、开票、价格结算、转售或代充。
- 公网多租户服务、生产 SLA、跨地域高可用和真实商业网关运营。
- 代理个人 Codex 账号、共享 Codex 会话或把订阅能力转给其他用户。
- 声称完整实现 OpenAI API，或复制现有网关的参考实现代码。

### 技术与时间限制

- 周期为 10–14 天；先完成非流式契约，再实现 SSE、回退、限流和唯一用量。
- 技术栈可自选，但 HTTP/SSE 行为必须能通过自动化客户端测试，不要求复杂前端。
- 两个 Provider 必须是仓库内可运行 Mock；不得读取 `OPENAI_API_KEY` 或任何真实 Provider 凭证。
- Gateway Key 是本地合成测试凭证：明文只在初始化/请求端短暂存在，仓库、持久层和日志只放哈希或不可逆摘要。
- 不要求也不允许为了本作业把网关公开到互联网；本地或受控测试环境即可。

## 澄清机制

在“澄清 Issue”写明背景、歧义、候选方案、推荐、影响和未回复时的可逆假设。

例如“上游在第三个 chunk 失败后能否切备用”已有明确安全答案：不能拼接 Provider；若要改变必须先提出并证明协议语义。仅影响错误文案的问题可先记录假设；任何真实 Provider、账号共享、Key 池、付款、转售、公网多租户、隐私留存或费用问题都必须停止并等待组织者确认。题面未覆盖的 D/or 协议状态应显式记录，不能偷偷归入成功。

## 交付物与证据

- 可运行源码、依赖锁文件、安全配置示例、Mock Provider 和测试 Key 初始化工具。
- `README.md`：架构、兼容子集、Key 生命周期、启动、curl 示例、故障矩阵、SSE 语义、测试和限制。
- `docs/PRD.md`：调用方、协议、错误/回退规则、范围、验收映射和澄清决策。
- `docs/PLAN.md`：鉴权、路由、Provider、SSE、限流、用量写入的数据流与并发策略。
- 自动化测试：覆盖四个接口、Key 隔离、模型权限、回退矩阵、首 chunk 边界、唯一 `[DONE]`、限流隔离和唯一用量。
- `docs/TEST_EVIDENCE.md`：命令、结果、HTTP/SSE 证据、验收映射和敏感信息扫描结果。
- `docs/AI_COLLABORATION.md`：AI 建议、本人协议核验与未采纳建议；不得粘贴私人会话全文。
- `docs/RETROSPECTIVE.md`、阶段 Issue、PR 和可解释的提交历史。

## 公开可测试验收标准

| ID | Given / 前置条件 | When / 操作 | Then / 可观察结果 | 验证方式 |
|---|---|---|---|---|
| AC-01 | 服务与两个 Mock Provider 已启动 | 调用 `/health` 与授权后的 `/v1/models` | 健康摘要不泄密；模型列表只含该 Key 可用别名且结构稳定 | HTTP 集成测试 |
| AC-02 | 无 Key、无效 Key、无模型权限 Key | 分别调用 Chat Completions | 依次得到稳定 `401/401/403`；响应有请求 ID，日志无明文 Key | 参数化安全测试 |
| AC-03 | 主 Provider 正常 | 分别发送 `stream:false` 与 `stream:true` 请求 | 非流式结构包含 choices/usage；流式 JSON chunk 合法且只有一个 `[DONE]` | 协议测试 |
| AC-04 | 主 Provider 在首响应前超时、返回 `429` 或 `5xx` | 分别请求同一模型别名 | 每种场景最多回退一次并由备用成功；记录实际 Provider 与回退次数 | 参数化集成测试 |
| AC-05 | 主 Provider 返回 `400`、`401` 或 `403` | 分别请求 | 网关不调用备用 Provider，返回映射错误，用量记录显示回退 0 次 | 自动化测试 |
| AC-06 | 流式主 Provider 已发送一个内容 chunk 后故障 | 读取完整 SSE | 不切换 Provider；已有内容不重复；出现结构化终止错误且仅一个 `[DONE]` | SSE 字节流测试 |
| AC-07 | Key A 和 Key B 均有独立额度 | A 连续调用至限流，再由 B 调用 | A 得到网关 `429` 且 Provider 未被调用；B 仍能成功 | 并发/限流测试 |
| AC-08 | 一次请求经历主失败、备用成功 | 查询该 Key 的 `/v1/usage` | 该 `request_id` 只有一条最终记录，包含两次尝试摘要、实际 Provider 和合计 token | 数据一致性测试 |
| AC-09 | 分别触发鉴权失败、校验失败、限流、Provider 失败、成功和流中断 | 按请求 ID 查询存储/测试接口 | 每个入站请求恰好一条最终记录；失败 token 为 0 或已实际输出量；无正文/明文 Key | 参数化自动化测试 |
| AC-10 | 全新环境且没有任何真实 Provider 凭证 | 按 README 初始化本地 Key、启动并跑全套测试 | 四个接口、非流式/SSE、回退、限流和用量均可复现，未访问真实模型或公网 | 人工复现 + 命令证据 |

## 技术讲解与追问准备

请准备说明：OpenAI-compatible 子集边界；模型别名如何与 Provider 解耦；Key 哈希和指纹的区别；为什么 `400/401/403` 不回退；为何首 chunk 后不能切换；唯一 `[DONE]` 如何保证；每请求一条最终用量怎样处理并发和异常退出；限流为什么按 Key 隔离。

验收可能临时修改一种错误语义或增加一个模型别名。你需要先画出非流式/流式状态变化，评估鉴权、回退、用量和兼容性影响，再在独立分支实现并提供协议回归证据。

## 安全与合规

- 只允许两个本地 Mock Provider 和合成 Gateway Key；仓库、数据库、日志和证据不得出现真实或明文 Provider Key。
- 禁止账号共享、Codex 中转、Key 池、OAuth、支付、充值、转售、公网多租户或代替他人使用订阅。
- 不记录 Prompt/响应正文，不跨 Key 返回用量；错误、追踪和截图全部脱敏。
- 不得提交、共享或中转 Codex/模型账号、会话、Token、API Key、Cookie、私钥。
- 本项目是本地教学样板，不代表获得任何 Provider 的代理、转售或多用户运营授权；活动也不承诺就业结果。
