b671fb629a
The embedding client does not go through the chat onion -- embed() runs its own chain down to _emit(), which builds a ChatRequest on the spot and so far only fills session_id and parent_call_id. Changing chat() alone would have left every embed row with empty dimensions, which is exactly what the issue's second request asks for. The bigger find: the draft claimed serialization could not fail because the entry check already restricts values to scalars. It can. A float passes a naive type check and json.dumps writes it as the literal NaN, which is not valid JSON and which JSONB rejects; the failure then lands in the emitter's degrade path and turns a caller's input error into silently dropped telemetry. Now rejected at the entry with isfinite and again at serialization with allow_nan=False. Also states the validation runs at both public entries, not just chat(), and adds the RLS template the design had promised but never wrote down.
5.4 KiB
5.4 KiB
type, node_id, title, date
| type | node_id | title | date |
|---|---|---|---|
| design | design:issue11-caller-dimensions | 调用方自定义维度设计(issue #11) | 2026-08-17 |
调用方自定义维度设计(issue #11)
正文: 2026-08-17-issue11-caller-dimensions-design.md。状态: 待 Codex 审 → 待人类审批。
- 选定方案:
tenant_id提真实列(RLS 硬需求)+metaJSON 容器承载任意调用方自定义 KV(默认不建索引)。chat()/embed()各增两个带默认值的 keyword-only 参数,签名冻结承诺不破。端口 22 → 24 字段。 - 范围收窄(2026-08-17 人类决策): 只做调用方自定义的维度;请求自带信息(模型名/供应商/源名)继续走现有列,库不往
meta写任何自采信息。issue 第 4 条(保留期与访问控制)另开。 - 为什么必须提列而不能纯 JSON: 两条独立实证。① RLS 挂
meta->>'tenant_id'语法合法但会静默退化——PG 的 Planner Statistics and Security 规则在 RLS 场景下对非 LEAKPROOF 函数当作没有统计信息规划,而->>未标 leakproof;pgsql-general 实证案例的最终解法就是"索引列改成非 JSONB",Tom Lane 警告手工标 leakproof 是安全问题。② 与 RLS 无关的独立问题: planner 对 JSONB 本就无可用统计,@>走硬编码 0.1% 选择率,Heap 复现里行数低估 12 万倍、join 从 300ms 变 584 秒。 - 为什么不做"可配置提升列白名单": dbt/Airbyte/Fivetran 三家一致禁止用户自定义列(Fivetran 的后续 MERGE 直接把用户列置 NULL,官方方案是建视图)。本库场景更糟: 两个下游对同名 key 推断出不同类型时,第二个到达者的
ADD COLUMN被IF NOT EXISTS静默跳过,从此一直静默写错类型——不报错、持续污染。且_COLUMNS/_INSERT从常量变运行时拼接,SQL 注入面从零出现,端口"22 字段冻结"与列序断言全部失效。 - 同类系统佐证: LiteLLM(同为 LLM 网关、同为每调用一行进 PG)的
SpendLogs正是此形态——team_id/organization_id/end_user/session_id全部提列并索引,而metadata/request_tags无任何索引。Grafana Loki 的三层(labels 索引 / structured metadata 不索引但可筛 / log line)是同一分野。六家 LLM 可观测平台无一例外都是"少数物化列 + 一个 KV blob"。没有任何成熟系统允许任意 key 自动获得列/索引待遇;唯一的自动推断派 ES dynamic mapping 也是唯一有公开事故名的(mapping explosion)。 - 库止步于列 + policy 模板,绝不自动 ENABLE RLS: 启用 RLS 而无匹配 policy 是 default-deny(零行可写,静默不报错)。三个下游里只有 GovDoc 多租户,库若自动启用,另两家升级后遥测全量写失败,叠加"遥测写失败静默降级"铁律 = 无声全局丢数据——这才是 issue「不可逆」担忧的真正落点。另三条理由: 库无权知道角色拓扑;按最佳实践部署时库的运行时角色恰好不是表属主、无权
CREATE POLICY;SQLite 无 RLS,承诺它会让两后端语义不对等。先例(graphile-worker/Ent+Atlas/django-multitenant)一致把 policy 授权留给使用方。 - 哨兵值而非 NULL: PG 的
USING表达式返回 false 或 null 的行都不可见且静默跳过,故 NULL 的tenant_id不是"未归属"而是对所有人永久不可见的黑洞。用NOT NULL DEFAULT ''则老行可一条 SQL 审计;同时满足 PG 11+ 加非易失默认值列不重写全表、SQLite 要求 NOT NULL 列必须有非 NULL 常量默认值。 - 超限报错而非静默丢弃: Langfuse 的"value 超 200 字符直接丢弃"不抄,违反 P5。报错点在
chat()入口而非遥测写入点——遥测层一切失败都被降级成 warning,校验放那里等于没有校验(同overlay保护键先例)。 - 不进缓存 key:
cache_namespace已是必填的租户隔离维度并已进 key(ARCH §7.5),重复;且进 key 会让存量缓存全量冷启动。 - 被否决备选: 纯
metaJSON 不提列(RLS 静默退化);可配置提升列白名单(多下游共表静默写错类型);复用cache_namespace传租户(缓存隔离单位 ≠ 数据归属,会让下游无法表达"同租户多命名空间");tenant_id混在meta里当约定 key(拼错不报错,静默降级成普通维度)。 - 审查留痕(Codex,2026-08-17): 报 4 项,逐条核实后全部采纳。①
embed()路径覆盖不足——EmbeddingClient不走 chat 洋葱,_emit()在embedding.py:360现场构造ChatRequest,只改 chat 会导致 embed 行维度恒空,恰好落空 issue 第 2 条诉求;② 非有限 float 会击穿"序列化不可达"论断——json.dumps把nan写成NaN字面量(非合法 JSON,PG JSONB 拒收),失败会被降级吞成 warning,即调用方输入错误转化为静默丢遥测;实测确认后改为入口math.isfinite+ 序列化allow_nan=False双层收口;③ 校验入口表述只写chat(),与双路径 API 不一致;④ §4.5 承诺"提供 RLS 模板"却只给了索引模板,已补上含FORCE/USING+WITH CHECK/NULLIF(current_setting(...))的完整定稿。第 ② 条的推翻过程已写进正文 §6,因为"入口校验完备 ⇒ 下游不可能失败"这个推理模式容易复发。 - 另开议题:
_BACKFILL自动 ALTER 是否应降级为默认关闭(HangfireEnableHeavyMigrations先例、APScheduler 4.x 版本不认识即拒绝启动)——与本 issue 同源但属独立架构变更,按反 gold-plating 不纳入本次。