Helm API 是一个开源、自托管的 LLM 路由网关("LLM 世界的 nginx")。当前规格与历史附录索引见 docs/README.md(核心章节按 01–14 阅读,Self-Service Portal 单独列出)。本文件只放实现时必须遵守的原则与约定,不重复 spec 内容。
- 网关与 UI 解耦:核心网关(路由、分类、provider 执行、协议互译、存储)必须能脱离管理界面 headless 独立运行。core 逻辑不得 import 任何框架(Hono / SvelteKit)。
- 配置即代码:行为由
config/*.yaml+ 环境变量驱动,不靠改代码。配置加载用 Zod 校验,非法配置 fail-closed(拒绝启动),绝不带病运行。唯一例外(已拍板):observer 记忆压缩是网关内部自适应行为,默认零配置——价格/上下文窗口从 model catalog 自动解析(价格 pin 走pricing.yaml)、工作负载统计从存储数据现场推导、经济学先验是代码常量(AUTO_PRIORS)。仅config.memory.compaction暴露少量触发/保留参数的可选覆盖(segment_min_tokens / idle_flush_s / force_context_ratio / min_recent_messages / min_keep_ratio——全 optional 无默认值,缺省即内部先验,写了才生效);遗留config.memory.observer块拒绝启动。理由:会"撒谎"的旋钮比没有旋钮更糟(旧 economy 模式 17 个手调参数从未投产且多处与实现脱节),自适应输入让经济学无可配,但触发时机有真实运维语义。 - 辅助环节 fail-open,边界校验 fail-closed:分类/eval 失败回退到
runtime.default_lane(出厂值为balanced)并记录;Memory 失败保持请求不变,其他可选缓存/信号读取按各自契约降级。鉴权、schema、硬配额、显式禁用项与确定性客户端错误必须拒绝;执行链耗尽时返回协议形状的结构化错误。 - 确定性优先:第 1 层规则是纯函数、零网络、可单测;eval(第 2 层)
temperature:0、默认关闭、带缓存。 - 两种 fallback 分清楚:分类兜底(→
runtime.default_lane)与执行兜底(→ 链内下一个 model)是两套机制、两套日志字段,绝不混淆(见 docs/03、04)。 - 暴露 lane 抽象,不暴露模型市场;provider 别名是内部供应链细节。
- 密钥安全 + 正文可观测:API key 鉴权只依赖 sha256 哈希;如配置了 at-rest 加密密钥,可额外保存
secret_enc(AES-GCM 密文)供管理后台查看/轮转。Key Store、遥测、payload 表与常规日志绝不保存明文 key;唯一例外是首次 bootstrap 可把 root key 打印一次,并原子写入配置的0600recovery file,之后不会再次显示。完整 request/response 正文按运行时设置capture_payloads(默认开)记录到独立的request_payloads表,便于调试与审计;可在管理界面「系统设置」随时关闭,并按payload_retention_days自动清理。DecisionRecord仍保持脱敏(不含正文)作为纵深防御。 - 流式正确性是头号风险:协议互译的 SSE 事件映射(见 docs/05)必须有针对性测试覆盖。
| 层 | 选型 |
|---|---|
| 语言 | TypeScript(strict: true,ESM,Node 22+) |
| 包管理 | pnpm(workspace 单仓多包) |
| API / 网关 | Hono(Web 标准、streamSSE、可 headless)—— 不用 SvelteKit SSR 当 API |
| Web 界面 | SvelteKit + Tailwind,adapter-static SPA;Hono 在 /admin 托管运维面板、在 /portal 托管 key 持有者门户 |
| 校验 | Zod——schema 是类型唯一来源,内部请求/配置/eval 输出/错误模型全部 z.infer 出类型 |
| DB | 默认 SQLite,支持 Supabase(Postgres);统一抽象层随时切换(见下) |
| 单测 | Vitest |
| e2e | Playwright |
| Lint / Format | Biome(TS:core/shared/gateway);admin 的 .svelte 用 Prettier + prettier-plugin-svelte + svelte-check |
- core 定义 Store 端口接口(repository pattern):
KeyStore/TelemetryStore/ConfigStore等。 - 提供两个适配器:
sqlite(默认,本地文件)、supabase(Postgres)。 - 运行时按 config 选择实现;core 只依赖接口,不依赖具体 DB。
- 底层使用 Drizzle ORM:SQLite 绑定
better-sqlite3,Postgres/Supabase 绑定postgres-js;两套迁移与方言差异封在适配器里。
helm-api/
apps/
gateway/ # Hono:API + 托管 admin 静态资源 + healthz/version
admin/ # SvelteKit + Tailwind 管理界面
portal/ # SvelteKit key 持有者自助门户
packages/
core/ # 路由/分类/provider/协议互译/Store 端口 —— 框架无关
shared/ # Zod schema + 共享类型
config/ # 默认 lanes/policies/classifier/providers/... yaml
docs/ # 当前规格(01–14 + Portal)与已标注的历史/研究附录
CLAUDE.md
implementation-notes.md
LICENSE
红 → 绿 → 重构:先写失败测试,再写最小实现使其通过,再重构。不允许"先写实现后补测试"。
- 单元测试(Vitest):覆盖 core 全部纯逻辑——规则评分、tier 边界、能力过滤、fallback 链、协议 transformer、Store 适配器、config 校验。
- e2e(Playwright):覆盖关键链路——OpenAI/Anthropic 请求端到端路由、流式 SSE、协议互译、管理界面基本操作。
- 覆盖率到边际效应为止:correctness-critical 路径(路由决策、分类、transformer、fallback、错误处理)追求高覆盖;UI 脚手架、纯胶水代码不强求。不为凑 100% 写无意义测试。
- 流式与 tool-call 翻译必须有专门 case(docs/05 的坑位逐条覆盖)。
pnpm install
pnpm dev # 仅起 admin 开发服务器(网关无 watch:跑构建产物或 Docker)
pnpm --filter @helm/portal dev # 仅起 portal 开发服务器
CI=true pnpm exec vitest run path/to/relevant.test.ts # 本机只跑定向 Vitest
CI=true pnpm --filter @helm/gateway exec playwright test path/to/relevant.spec.ts # 定向 e2e
pnpm typecheck # tsc --noEmit
pnpm lint
pnpm build # 构建网关 + admin + portal + ops bundle- TypeScript
strict,无any(必要时unknown+ 收窄);ESM。 - Zod schema 与类型不重复定义——先写 schema,类型用
z.infer。 - 中文文档必须意译:保留技术事实、命令、字段名、端点与链接,但按自然中文重新组织句子,不逐句照搬英文语序;
README.zh-CN.md是独立中文文案,不是英文 README 的机械镜像。 - 错误以结构化形式产生,由 Protocol Adapter 翻成客户端协议形态(docs/07)。
- 结构化日志,全程透传
trace_id。 - 提供
/healthz与/version(Docker 部署/升级 SOP 依赖)。 - 客户端断连视为非 provider 故障(不触发熔断),见 docs/02。
- 强制 API key 鉴权;启动无 key 则生成一把 root key,打印一次并写入配置的
0600recovery file(docs/06)。 - 管理界面用 HTTP Basic;当前
buildServer()的部署入口只从HELM_ADMIN_*环境变量读取(resolveAdminAuth的 config 分支仅供直接调用/测试),未配凭证却显式开启时启动告警(docs/11)。 - 静态 provider 凭证经环境变量注入;OAuth token 只加密写入 Store。二者都不写进仓库或常规日志。
- 在分支上开发,开 PR,CI 全绿(typecheck + lint + build + 单测 + e2e + Docker smoke)方可合并;e2e(Playwright)作为独立 CI job 在 PR check 跑(mock 离线确定性,hermetic);不直接推
main。 - Commit message 结尾带:
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> - 只有用户明确要求时才提交/推送。
实现 spec 时,持续维护根目录 implementation-notes.md,记录:
- spec 未覆盖、你不得不自己做的决定;
- 你被迫修改 spec / 偏离 spec 的地方及原因;
- 权衡取舍(为什么选 A 不选 B);
- 任何用户应当知道的坑、限制、TODO。
新增条目追加在文件顶部,带日期与所属 spec 章节。
体积控制:该文件只保留最近 10 条完整记录;新条目入栈时,把被挤出的更早条目压缩成一行要点(保留日期、标题、关键决定/坑/TODO),移入文末「历史条目摘要」。完整原文经 git history 回溯。
- 能力与定价数据源:上游取 LiteLLM 的
model_prices_and_context_window.json,经pnpm sync:catalog同步成签入的 generated catalog;运行时读capabilities.yaml/pricing.yaml,手动条目覆盖生成项。不在运行时拉取(生成目录是供应链输入,不直接进运行时选择)。 - provider 执行层:在
packages/core/src/provider重写,移植 llm-router 的久经考验语义(熔断 OPEN/HALF_OPEN + 探测锁、首个有效 chunk 前记失败/后记成功、能力过滤显式 skip reason、:free429 跳过、abort 不算故障);用其现有测试当行为 checklist。不 import llm-router。 - eval 缓存键:
sha256(canonical-json),只哈希分类依赖输入——末条 user 消息(trim)、turn 数、排序后的 tool 名、response_format是否 JSON、是否含附件/vision;稳定键序、排除易变字段、不 lowercase。TTL 默认 300s,字段集可配,实现后验命中率。
- 暂无(如出现 spec 与实现冲突,记入 implementation-notes.md 并在 PR 中提出)。