-
1.2.1 — 调用方自定义维度 Stable
released this
2026-08-19 00:55:58 +08:00 | 129 commits to main since this release每次调用现在可以带上租户标识与任意调用方自定义维度,并逐条落进遥测表(issue #11)。
llm_calls存的是完整正文(digest_messages只对多模态image_url做 sha256,纯文本原样透传),多租户下游的合同与标书全文因此混在同一张表里,而原先的 22 列没有任何租户维度——能区分来源的只有session_id/parent_call_id两个调用方自填、库内不校验的自由字符串。不可逆性是这个 issue 的核心论点,且成立: 先启用遥测再补列,补列之前写进去的每一行都没有归属,事后无法还原哪行属于谁。
新增
- 四个公共方法各增两个 keyword-only 参数
tenant_id与meta,都带默认值None,既有调用点零改动:GatewayClient.chat()、EmbeddingClient.embed()、OcrClient.recognize_text()、OcrClient.parse_layout()。issue 只诉求前两条链路;OCR 经同一个TelemetryEmitter写同一张表,只覆盖两条会让同表内一部分行有归属、一部分永远空白,故一并纳入(与 issue #10 同一判断)。 - 遥测表
llm_calls新增两列,排在既有 22 列末尾,两端类型按各自后端的原生能力取:
列 Postgres SQLite tenant_idTEXT NOT NULL DEFAULT ''TEXT NOT NULL DEFAULT ''metaJSONB NOT NULL DEFAULT '{}'::jsonbTEXT NOT NULL DEFAULT '{}'- 老表经现有
_BACKFILL机制自动补列(先探测再ALTER,失败只逐行降级),补列后老行的tenant_id读出是空串而非 NULL。这个区别是刻意的: PG 的 RLSUSING表达式返回 false 或 null 的行都不可见、且静默跳过不报错,所以 NULL 的tenant_id在任何 policy 下都不是"未归属",而是对所有人永久不可见的黑洞;哨兵空串则显式可查,COUNT(*) WHERE tenant_id = ''一条 SQL 就能审出还有多少行待归属。补列本身两端都不停机: PG 11+ 加带非易失默认值的列不重写全表,SQLite 加列是元数据操作。 TelemetryRecorder.record_llm_call由 22 字段扩为 24(inspect.signature实测),ChatRequest同步新增两个带默认值的字段。meta以json.dumps(sort_keys=True, ensure_ascii=False, allow_nan=False)序列化,空 dict 落'{}'而非 NULL。
校验规则(超限报错,不静默丢弃)
校验在四个公共入口收口、进洋葱之前抛裸
ValueError,四条链路共用同一份实现:项 规则 tenant_id长度 ≤ 128;不得含首尾空白;空串是哨兵值的地盘,调用方传空串多为 bug meta键数≤ 16 meta键必须匹配 [a-z0-9_.]{1,64};pg_前缀保留给库将来的内建维度(本版库自身不写任何该前缀的键)meta值仅 str/int/float/bool,嵌套需调用方自行序列化;字符串值 ≤ 256 字符;float必须有限,nan/inf报错(它们不是合法 JSON,PG 的 JSONB 会拒收)报错点选在入口而非遥测写入点: 遥测层的一切失败都按降级方向铁律吞成 warning,校验放那里等于没有校验。超限一律报错,不采用"超长就丢弃"的做法——那违反 P5「严禁默认值掩盖错误」,会把调用方的输入错误转化成静默丢数据。
不变
tenant_id与meta都不进缓存 key。租户级的缓存隔离由既有的cache_namespace负责,重复进 key 只会让全部存量缓存冷启动;且meta承载的是审计维度而非语义维度,同 messages 同 namespace 下换个batch_id不应导致 miss。- 既有 22 列的列名与列序、
ON CONFLICT (call_id) DO NOTHING幂等、单条写失败逐行丢弃的降级方向全部未动。错误面零变更,下游except写法不受影响。 - 缓存命中行与终态失败行同样带维度,且读的是本次
request而不是缓存里的历史响应——这两类行恰恰是审计最需要的(命中意味着这次没花钱但确实发生了;终态失败意味着这个租户的请求没被服务)。
边界: 库只交付列,RLS 与索引由下游执行
库不会执行
ENABLE/FORCE ROW LEVEL SECURITY,也不会建任何索引。 需要数据库层的强制隔离,下游 DBA 必须自行执行 RLS DDL 与CREATE POLICY(并建(tenant_id, created_at)复合索引——启用 RLS 后 policy 会给每条查询隐式追加tenant_id等值谓词,它必然是前导列);不执行则tenant_id只是一个可查、可过滤的普通列,没有任何数据库层强制。不自动启用的首要理由是 default-deny: 启用 RLS 而无匹配 policy = 零行可写,且静默不报错。三个下游里只有一个是多租户,库若自动启用,其余部署升级后遥测全量写失败,再叠加遥测的静默降级铁律,就是无声全局丢数据——恰是本 issue 所担心的"不可逆"的最坏形态。其余理由: policy 必须绑定角色而库只拿到一条连接串;
CREATE POLICY/ALTER TABLE要求表属主,而按最佳实践部署时库的运行时角色恰好不是属主;SQLite 根本没有 RLS,承诺 RLS 会让两个后端语义不对等。RLS 模板与三个陷阱(表属主默认豁免 RLS 需
FORCE;租户上下文必须在显式事务内set_config(..., true),asyncpg 默认 autocommit 下单发SET LOCAL会当场失效而 PG 只发 warning;只写USING不写WITH CHECK时租户 A 能插入标着 B 的行)见 README「多租户与自定义维度」一节——那份模板随包分发,research-wiki/不在 sdist 内。升级提示
- 升级无需任何代码改动: 两个新参数都是带默认值的 keyword-only,既有调用点原样工作;不传即写入哨兵空串与空
{}。 - README 的安装 pin 由
>=1.2,<2收紧为>=1.2.1,<2。按>=1.2,<2装的下游不会被锁死(仍会拿到本版),但显式装 1.2.0 就没有租户维度。 - README 的配置参考表此前漏列了源级
MISSING_DONE与EXTRA_BODY(正文别处却引用了后者)、{SCOPE}__QUOTA_FULL、embedding 专用键、PGW_CACHE_BACKEND的memory档与三个可选PGW_*键,本版按config.py的_SOURCE_FIELDS与_load_pgw逐项补齐。代码零变更。
Downloads
- 四个公共方法各增两个 keyword-only 参数