apps/server/docs/brainstorms/2026-05-15-llm-router-replacement-requirements.md
在 apps/server 内部新建一个 LLM/TTS 路由模块替换 knoway。LLM 走单格式直传 + 请求内 key fallback;同一逻辑模型可挂多个 upstream,v1 实装 upstream 间 fallback(一个 upstream 的全部 key 都失败时切下一个 upstream),upstream 间的 LB / 加权分流延后。TTS 用 adapter interface 抽象 provider,v1 发 Azure / Alibaba cosyvoice / Volcengine 三家手写 REST 适配器;voice catalog 用提交仓库的静态 JSON。完整 OTel + Grafana + healthz 覆盖,一次性切流配 revert 回滚兜底。Provider key 必须 envelope-encrypted 后再写入 configKV,OTel key id 用 SHA-256 前 8 字符脱敏。审计日志(保留用户请求 / 响应体)暂不做,作为 TODO 留给 v2。
今天 apps/server 的 /api/v1/openai/* 是个薄代理,把 chat completions、audio/speech、audio/voices 转发给 sidecar 部署的 knoway。
knoway 的两个核心限制把生产推到痛点上:
复合代价:用户侧黑时间 + 维护两套部署 + ops 在 key 异常时不仅要换 key 还要管 knoway 这层。
/api/v1/openai/* 调用 chat completions / TTS / voice listing。看不到内部 fallback 细节,只在所有 key 都失败时收到错误。apps/server 内执行 key 选择、fallback、TTS 协议转换、voices 列表组装。F1. LLM chat completion,首 key 成功
/api/v1/openai/chat/completionsF2. LLM chat completion,请求内 fallback
/api/v1/openai/chat/completions,配置中存在 ≥ 2 个 keyF3. LLM chat completion,全 key 用尽
F4. TTS speech,单 provider 多 key fallback
/api/v1/openai/audio/speech,请求模型对应一个 TTS provider(v1 是 Azure / Alibaba cosyvoice / Volcengine 之一)F5. Voices listing
/api/v1/openai/audio/voices?model=...DEFAULT_TTS_VOICES 推荐 map → 返回unspeech 包的 Voice / VoiceFormat / VoiceLanguage 类型)F6. Client 真错与上游错的分离
LLM 路由
/v1/chat/completions(含 SSE 流式)的代理;客户端契约与 knoway 时代完全一致(status、shape、stream 协议)。TTS 路由
/v1/audio/speech(OpenAI 兼容);v1 三个 adapter:Azure Cognitive Services Speech、Alibaba Cloud DashScope cosyvoice、Volcengine TTS。/v1/audio/voices?model=... 返回该 provider 的 voice catalog;catalog 用提交仓库的静态 JSON 维护,月级 ops 手动刷新。响应形状沿用 frontend 已用的 Voice / VoiceFormat / VoiceLanguage 类型(来自 unspeech 包,纯类型导入,无运行时依赖)。Observability
gen_ai.system、gen_ai.request.model、gen_ai.response.model、gen_ai.usage.*、gen_ai.operation.name)加上 airi 自定义网关属性:哪个 upstream、哪个 key、fallback 深度、触发原因。Key 标识必须是 SHA-256(key) 的前 8 字符,不是 raw key 的前 N 字符。OTel 数据会出本进程到 Grafana / 第三方 backend,raw key 前缀外泄等于秘密外泄。Health 端点
配置与运维
configKV 抽象。注意:当前 configKV 是 Postgres 为 truth + Redis 为 cache 的封装(apps/server/src/services/config-kv.ts),且 ConfigEntrySchemas 是 Valibot 闭合常量对象 —— 不支持任意 provider 名的动态 key。本次必须新增一个 composite schema 条目(如 LLM_ROUTER_CONFIG)承载整棵路由器配置树。这是 schema design 工作,不是"零配置改动复用现有抽象"。airi.gateway.config.reload 按 instance 维度分组)确认每个 instance 已切换到新版本。Key 撤销场景下(最敏感),未传播完成期间的请求必须仍能用旧 key 继续服务用户,不能因为传播未完成而 5xx —— 但 ops 必须能基于上述信号判断"还有多少 instance 没切"。具体实现机制(pub/sub 失效本地 cache vs 每请求读 configKV vs Redis TTL)planning 阶段定,但有界 + 可观测是 v1 安全级 requirement。迁移
审计日志(v1 不做 — TODO)
fallback.depth=1 且 fallback.reason="429"。key.exhausted.count{provider} 计数 +1。model 字段是配置中不存在的 alias。路由器在打 upstream 之前的本地校验阶段拒绝,返回 4xx(客户端错),不进 fallback 流程,OTel 不记录 fallback 事件。/v1/audio/speech,model 指向配置中的 cosyvoice provider。路由器调用 Alibaba adapter 把 OpenAI 输入翻译成 DashScope 原生请求格式,拿到音频后翻译回 OpenAI 响应格式给客户端。客户端无感知协议层差异。/v1/audio/voices?model=microsoft-azure-tts-alias。路由器返回 Azure provider 的静态 voice catalog,shape 与 frontend 现有 Voice 类型消费者一致;configKV 里的推荐 voice map 已合并进响应。fallback.depth > 0.5 持续 24 小时 或 某单 key 连续贡献 > 30 分钟 > 80% 的上游错误,下个 sprint 必须把"持久化死活状态 + admin enable/disable"上线。不是"等数据决定"的开放问题,是触发条件已经约定好的自动晋升。这避免了"interesting graph but no action"的漂移。/v1 对客户端的接口契约(保持 OpenAI 兼容,与 knoway 时代完全一致)。packages/llm-router 或 packages/tts-router —— 内联在 apps/server,无第二 consumer;adapter interface self-contained 方便未来真抽。/api/v1/openai/audio/*。Voice / VoiceFormat / VoiceLanguage:保持与 frontend 既有消费者兼容;纯类型导入,无运行时依赖(CLAUDE.md "Import types from the module that owns the contract" 原则)。apps/server,不抽独立 package:无第二 consumer = YAGNI。adapter interface 设计成 self-contained,未来真有第二 consumer 抽包成本低。airi.gateway.key.id 必须是 SHA-256(key) 前 8 字符,不是 raw key 前缀。Raw 前缀外泄 = 秘密外泄到 Grafana / 第三方 OTel backend。理由来自 security reviewer (SEC4)。当前 v1 不做。这里列出真要做审计 / 隐私时需要带哪些配套,作为 v2 起点参考。两档:内部 debug 用就够 / 想真正合规要做的全套。
只解决你说的 "有人刷接口我们想知道他发了啥",不上合规级别:
llm_request_log 表加 request_body / response_body 两列。bodyLimit、响应侧 8MB 兼容长上下文输出),超限截断 + flag 标记。这一档需要面对的两个真问题(reviewer 共识,记在这等做的时候处理):
llm_request_audit 子表通过 FK 关联。只有真要上合规审查或 EU/中国大陆正式商用前才考虑。每条都要工程 + 法务双投入:
llm_request_log,还有任何带 userId 的 audit 表)。audit_access_log 表记录 admin 操作。判断什么时候要从 Tier 1 升 Tier 2:用户量超过 ~1k DAU、有商业合同方要求、有 EU/中国大陆用户付费、出过用户对自己数据的投诉、收到监管问询。任一触发就该开 Tier 2 brainstorm。
如果 "有人刷接口" 升级到 "有人用我们的服务跑非法内容生成",需要的是实时审查而非事后审计:
这条跟 Tier 1/2 是不同形态的工程(实时管道 vs 事后日志),单独开 brainstorm。
configKV 服务(Redis-backed)作为路由配置 + key 列表的存储。沿用 apps/server/src/services/config-kv.ts 已有抽象,不新建表。gen_ai.* 标准属性 + airi.* 自定义属性,参见 apps/server/docs/ai-context/observability-conventions.md)。(input, options) → ArrayBuffer | ReadableStream 还是分 chat-style / TTS-style?)—— planning 阶段定。