12 KiB
ADR-0167:可选、受预算约束的 AI Model Gateway
- 状态:Accepted
- 日期:2026-07-26
- 关联:RFC D-07、D-12、D-13、D-156;ADR-0156、ADR-0157
背景
QingLong 3.0 必须同时覆盖低配路由设备和集群节点。AI 能力如果直接进入
runtime-core、本机 application 或 cluster-control 的默认依赖闭包,即使用户关闭
AI,也会承担 SDK、加载、内存和供应链成本;如果为每个 Provider、Gateway、Copilot
分别建立只有一个文件的 package,又会继续放大 workspace 拓扑和维护负担。
第一条 Phase 2 切片需要先建立真实、可测试的远程模型调用边界,但不能冒充产品 Copilot、MCP、credential 管理、持久化 Trace 或 UI 已经完成。
决策
1. 新增一个能力族 package,而不是多个技术层 package
新增 @qinglong/ai,当前包含:
- ModelProvider、request/result/chunk/usage、policy 与 audit contract;
- 请求驱动的
BoundedModelGateway; - 零第三方 SDK 的
OpenAiCompatibleProvider; - 复用 runtime-core StepRun 的 durable ModelInvocation contract;
- 双方言 feature migration、原子 repository、durable coordinator 与 bounded recovery;
- Project-bound provider credential binding、content-free credential audit 与 可清零 authorization material lease;
- 非流式、SSE 流式和安全/资源契约测试。
这是新增 workspace package 的例外:它是可独立排除的安装、发布、加载和依赖边界, 且承载一个完整能力族,不按 interface、adapter 或厂商继续拆包。workspace importer 预算从 21 显式调整为 22,未知第 23 个 QL3 package 仍被 fail-closed 门禁拒绝。
该 package 只有一个 @qinglong/runtime-core workspace production dependency,没有
第三方 production dependency。依赖方向固定为 AI → runtime-core,以复用唯一
StepRun.kind=model 状态机;runtime-core 不得反向依赖 AI。源码审计继续禁止它导入
legacy 根、local、cluster、worker、数据库、Kubernetes、S3 或 HTTP SDK。现有
edge/standalone Profile 与 cluster image 都不依赖 AI,因此这个 workspace 依赖不会把
AI 带入默认产物。
2. 每次调用必须带完整运行身份与受信策略
每次 generate 或 stream 必须绑定:
projectId;runId与stepRunId;traceId与requestId;- 最长 5 分钟且可取消的绝对 deadline。
调用方不能在 request 中自报预算。Gateway 从 composition 注入的 policy provider 解析 provider/model allowlist、输入/输出字节、输出/总 token 和费用上限。费用策略 开启时,Provider 未返回费用也必须失败关闭,不能将 missing 解释为 zero。
进程内并发是资源盾牌,不是分布式 quota:达到上限立即返回
MODEL_GATEWAY_BUSY,不建立无界等待队列。Project 级 durable quota、跨副本费用和
并发裁决仍属于后续 repository/management plane。
3. 字节、token、流和审计都必须有硬边界
首版硬上限为:
- 最多 64 条 message;
- 每条 content 最多 64 KiB;
- 全部输入最多 256 KiB;
- 输出最多 1 MiB;
- 请求最多 32,768 output token;
- deadline 最长 5 分钟;
- 每个 Gateway 最多 8 个 Provider,进程并发配置最多 64。
策略只能在这些硬上限内进一步收紧。非流式结果必须返回一致的 usage;流式结束必须 提供 final usage,否则不能证明 token/费用预算,调用以协议错误收敛。Gateway 对每个 delta 累计 UTF-8 字节,不把完整输出缓存在内存。
Audit sink 只接收 Project/Run/StepRun/Trace/request identity、policy revision、 domain-separated request digest、输入/输出字节、usage、阶段与固定 error code。 Prompt 和模型输出不进入 audit record。admission audit 提交失败时,外部请求不得 开始;模型已经返回后 completion audit 失败时,结果不交给上层,不能把未审计调用 伪装为成功。
admission sink 可能是 durable start barrier,因此不得用 Abort race 让其在后台继续
提交。deadline 在 admission 期间到达时,Gateway 等待有自身数据库 timeout 的 sink
收敛;如果 start 已 durable,则立即追加 timed_out completion,且 provider I/O
保持为零。provider 调用期 deadline 与 caller abort 分别使用
MODEL_INVOCATION_DEADLINE_EXCEEDED 和 MODEL_INVOCATION_ABORTED,不能把两者
混为同一个失败事实。
durable admission 返回 existing 时 Gateway 必须抛出
MODEL_INVOCATION_REPLAY_BLOCKED,不得再次执行 provider。这个保守规则同时覆盖并发
重复请求、进程崩溃和 COMMIT 响应丢失;未知结果只由显式 recovery 收敛为
lost/outcome_unknown。
4. OpenAI-compatible adapter 保持远程、单次和有界
远程 endpoint 默认必须使用 HTTPS。只有显式配置的 loopback endpoint 可使用 HTTP, 用于本机模型服务;URL 不接受内嵌 credential、query 或 fragment。
Adapter:
- 每次从 credential provider 取得短生命周期 authorization header;
- authorization header 最多 4 KiB,拒绝 CR/LF;
- 每次
generate只发送一个请求,不自动 retry; - 不实现默认 fallback、cache、circuit breaker、timer、watcher 或后台 worker;
- 单响应或完整 SSE wire bytes 最多 8 MiB;
listModels最多接收 256 项;- SSE parser 支持任意网络分片与 LF/CRLF event boundary,要求
[DONE]; - stream 请求显式开启 usage,保留最后的 usage 供 Gateway 复验。
retry/fallback 会改变计费与副作用语义,必须在后续 durable invocation identity、 预算 ledger 和 Provider capability 完成后由 Gateway 决策,不能由 adapter 静默执行。
5. Profile composition 必须 disabled-first 且按部署规模付费
@qinglong/ai/profile 是同一能力包内的显式组合入口,不新增 workspace package。
它只接收 structural storage/provider loader,不反向依赖 local-sqlite、
cluster-postgres、local-application 或 cluster-control:
- disabled 时只验证 Profile 边界并记录
disabled,storage、provider 和 credential loader 调用次数必须为零; - enabled 时固定执行 storage load/readiness → bounded incomplete recovery → provider/credential load → active,recovery 截断或失败时 provider credential 仍不可达;
- Edge、Standalone、Cluster 默认并发分别为 1、4、32,单次 startup recovery 上限 分别为 4、32、128;
- stop 先撤销新 admission。仍有 active operation 时只返回
draining,不创建 timer/watcher;active operation 归零后按 provider → storage 反向释放; - active result 只暴露受 gate 保护的 generate/stream/manual-resolution seam,不把 raw repository、provider credential 或网络 authority 交给 transport。
这让小路由器保持 1 个并发、零后台任务,也允许 Cluster importer 注入共享 PostgreSQL authority。默认 Edge/Standalone application 和现有 cluster image 仍未 import 该入口;启用 AI 使用独立产物与门禁。
6. Credential 必须经过 Project-bound SecretRef 与短生命周期 lease
@qinglong/ai/provider-credential 在同一能力包内建立 exact binding、Secret
material structural port 和 content-free audit,不新增 workspace package。每次调用
按 Project + Provider 重新解析 binding 和 SecretRef;未固定 version 的 SecretRef
自然取得当前版本,不缓存 token、不创建 watcher。
material 使用 consumer-owned bytes 和显式 dispose。OpenAI-compatible adapter 只在
请求建立阶段持有 authorization lease,fetch 收敛后立即释放;audit 失败、malformed
lease 或 fetch 失败都不能遗留 material。credentialed listModels 必须提供 Project
与 request identity。
本机 EncryptedLocalSecretService 已提供结构兼容的可清零 material 解析,但正式
产品 importer、secret.use authority、durable credential audit,以及 Cluster
KMS/Vault adapter 仍属于后续门禁。详见 ADR-0169。
被否决方案
- 把 ModelProvider 放进 runtime-core root:禁用 AI 的设备仍承担依赖与加载耦合。
- 为 Gateway 和每个厂商拆 package:没有独立部署收益,继续制造单文件包。
- 把 Provider 塞进 local-application 或 cluster-control:两种 Profile 复制实现, 并让常驻组合根默认取得外部网络与 credential authority。
- 直接引入厂商 SDK:首版协议简单,SDK 会显著扩大 edge 依赖与漏洞面。
- Provider 内自动 retry/fallback:可能重复计费,也绕过 Project 预算与审计。
- 只限制 token:Prompt、HTTP response 和 SSE buffer 仍可按字节耗尽内存。
- 记录 Prompt/输出以便调试:默认扩大 Secret、日志和个人数据泄露面。
验证
@qinglong/aibuild/check/test 通过;- 默认 suite 为 50 pass、1 条 PostgreSQL 条件 skip,另有 PostgreSQL 真库 1 pass, 覆盖 generate、split-CRLF SSE、final usage、策略拒绝、字节/token budget、policy/provider deadline、无隐藏队列并发、consumer cancellation、 cancellation-audit failure cleanup、durable admission 不脱离、provider identity、 content-free audit、HTTPS/explicit loopback、单请求/响应上限,以及 ModelInvocation 与真实 StepRun mutation chain、双方言原子 repository、独立 feature history、 replay block、bounded recovery 和 PostgreSQL COMMIT-response-loss;
- frozen lockfile 已登记一个 runtime-core workspace dependency、零第三方生产依赖的 importer;
- cluster dependency audit 覆盖 22 个 QL3 importer、AI 的 14 个源码文件且
findings=[]; - QL3 CI 在 runtime-core 后独立运行 AI contract suite,并在角色迁移后运行 AI PostgreSQL 真库门。
为证明“默认不包含 AI”,本轮重新执行了真实 package archive → offline production
install → import closure 门。原先所有 Profile 共用 4 MiB/512 files 会把基础 runtime
和完整 application 错当成同一档位:当前完整 application 已包含 Tool/Result 等现有
能力,实际超过 4 MiB,而这与 AI package 无关。因此基础 Profile 继续保持
4 MiB/512,edge-application 与 standalone-application 使用显式
5 MiB/640 files 门,RSS 上限仍保持 16 MiB:
- edge:3,902,728 bytes、478 files、40 loaded modules、10,878,976 bytes RSS delta;
- edge-application:4,544,802 bytes、589 files、87 loaded modules、 16,187,392 bytes RSS delta;
- standalone-application:4,544,934 bytes、589 files、87 loaded modules、 15,925,248 bytes RSS delta。
三份 package 清单都不含 @qinglong/ai。application RSS 已接近 16 MiB 门,因此后续
不能把 AI 直接接入默认 application;启用 AI 必须有独立产物/RSS 门,现有 application
继续增长前还应评估 runtime bundling、declaration pruning 或更窄的部署闭包。不能再
用提高同一个默认上限掩盖能力增长。
独立 AI Profile 制品和 disabled resource 门现已补齐:
- edge 基线重跑仍精确为 3,902,728 bytes、478 files、40 loaded modules;
- edge-ai 为 4,212,508 bytes、508 files、41 loaded modules、10,993,664 bytes RSS delta;
- standalone-ai 为 4,212,580 bytes、508 files、41 loaded modules、 10,780,672 bytes RSS delta;
- 直接加载
@qinglong/ai/profile只新增 1 个模块,本机观测 RSS 增量 409,600 bytes;Edge/Standalone/Cluster disabled activation 最慢 0.159 ms,三者 storage/provider loader 均为零; - cluster dependency 与 deployment audit 均
findings=[]/compatible=true。
后续门禁
当前实现不进入任何 production Profile,也不开放 HTTP/MCP/UI route。进入产品前仍需:
- credential binding 的双方言 durable audit、
secret.use产品 authority,以及 Cluster KMS/Vault material adapter; - durable usage/cost ledger、低敏查询与 retention;
- Project quota、费用表、路由、fallback 与 circuit breaker;
- read-only Copilot 与 MCP Resource/Prompt/Tool 的授权入口;
- cluster 多副本并发 quota 与 AI invocation 数据行级 HA fault;
- 正式 Edge/Standalone/Cluster 产品 importer、配置 ceremony 与 route authority。