docs: fold the OCR path into the approved scope for issue #11
OcrClient emits through the same helper and its rows land in the same table as chat rows. Covering only chat and embed would leave one table holding rows that have a tenant and rows that never will, and the issue's own irreversibility argument applies to those rows too. The design said two paths because the issue said two paths. Corrected at the source rather than only in the plan, so a later reader does not find OCR work with no design behind it.
This commit is contained in:
@@ -23,6 +23,8 @@ issue 只要租户维度。人类决定放大为**调用方自定义维度**的
|
||||
- **只做调用方自定义的维度**。请求自带信息(模型名、供应商、源名)继续走现有 `model`/`provider`/`source_name`/`model_reported` 列,**库不往新容器写任何自采信息**。
|
||||
- 保留期与访问控制不在本次范围(issue 第 4 条,另开)。
|
||||
|
||||
**范围补正(2026-08-17,写计划时发现后经人类追认)**: 覆盖**三条**遥测链路而非两条。issue 与本设计初稿都只说了 `chat()`/`embed()`,但 `OcrClient` 经同一 `TelemetryEmitter.emit_attempt` 写遥测(`ocr.py:426`),其 `_emit` 在 `ocr.py:398` 现场构造 `ChatRequest`,结构与 embedding 同构。**OCR 行与 chat 行落在同一张表**——只覆盖两条会让同一张表里一部分行有租户归属、一部分永远空白,且「先启用后加列则归属无法还原」这条不可逆性论证对 OCR 行同样成立。与 issue #10 同一判断(那次 issue 只报告 chat 的 400,OCR 被认定为同一缺陷的其余分支而一并修)。
|
||||
|
||||
### 1.3 明确不做
|
||||
|
||||
不做可配置的"提升列白名单"(见 §3 方案 C 的否决理由);不自动 `ENABLE ROW LEVEL SECURITY`;不自动建索引;不改动 `_BACKFILL` 的自动 ALTER 策略(调研提出的独立议题,属任务外重构,另开 issue)。
|
||||
@@ -86,9 +88,11 @@ issue 只要租户维度。人类决定放大为**调用方自定义维度**的
|
||||
|
||||
### 4.1 公共 API
|
||||
|
||||
两条路径对称新增两个 keyword-only 参数(F2/F3):
|
||||
四个公共方法对称新增两个 keyword-only 参数(F2/F3):
|
||||
|
||||
`chat(messages, *, ..., tenant_id: str | None = None, meta: Mapping[str, Any] | None = None)`,`embed(texts, *, ..., 同两参数)`。
|
||||
`chat(messages, *, ..., tenant_id: str | None = None, meta: Mapping[str, Any] | None = None)`;`embed(texts, *, ..., 同两参数)`;`recognize_text(image, *, ..., 同两参数)` 与 `parse_layout(image, *, ..., 同两参数)`。
|
||||
|
||||
**OCR 两个方法都要改**——只改一个即漏,而漏掉的那半会静默写出无归属的行。
|
||||
|
||||
**为什么 `tenant_id` 独立成参而不是 `meta` 里的一个约定 key**: 它是唯一享有真实列待遇的维度,独立成参让"这个 key 特殊"在签名上自明(P4 显式优于隐式);混在 `meta` 里则需要库偷偷抽取一个魔法 key,调用方拼错 `tenant_id`/`tenantId` 不会报错、只会静默降级成普通维度——正是本 issue 抱怨的失败形态。
|
||||
|
||||
@@ -110,7 +114,7 @@ issue 只要租户维度。人类决定放大为**调用方自定义维度**的
|
||||
|
||||
**超限必须报错,不得静默丢弃**。Langfuse 的做法是 value 超 200 字符直接丢弃——这条**不抄**,违反 P5「严禁默认值掩盖错误」。报错点选在**两个公共入口**(`chat()` 与 `embed()`)而非遥测写入点,理由是 F6: 遥测层的一切失败都被降级成 warning,校验放那里等于没有校验。这与 `overlay` 保护键的现有先例同构(`client.py:223` 的 `validate_request_overlay`),校验函数同样共用一份,不在两个入口各写一遍。
|
||||
|
||||
### 4.3 内部流转: 两条独立链路
|
||||
### 4.3 内部流转: 三条独立链路
|
||||
|
||||
`ChatRequest` 增 `tenant_id: str | None = None` 与 `meta: Mapping[str, Any] = field(default_factory=dict)`(F3)。二者是**只读快照**,库内中间件永不修改——与 `sampling` 字段同一纪律(issue #4 决策 A)。
|
||||
|
||||
@@ -118,7 +122,9 @@ issue 只要租户维度。人类决定放大为**调用方自定义维度**的
|
||||
|
||||
**embedding 走的是另一条链,必须单独贯穿(Codex 审查发现)**。`EmbeddingClient` 不经过 chat 洋葱: `embed()` → `_embed_batch()` → `_attempt()` → `_emit()`,而 `_emit()` 在 `embedding.py:360` **现场构造** `ChatRequest` 仅为复用同一个 Emitter,当前只填了 `session_id`/`parent_call_id`。若只改 `chat()`,结果是 chat 行有维度而 embed 行恒为空——**恰好落空 issue 第 2 条诉求**(两条路径都要能传)。故新维度须沿这四层逐层透传,并在 `_emit()` 构造 `ChatRequest` 时填入。
|
||||
|
||||
**不顺手重构这条链的参数列表**: 该链已在逐层传 `session_id`/`parent_call_id`,再加两个即四个同类参数,把它们收成一个值对象在美学上更优,但那会改动 embedding 现有的全部内部签名,属任务外重构(反 gold-plating)。本次只做加法;若日后参数继续增长,再单独立项。
|
||||
**OCR 是第三条链,同构同办**(2026-08-17 范围补正)。`recognize_text()`/`parse_layout()` → `_call()` → `_attempt()` → `_emit()`,同样在 `_emit()`(`ocr.py:398`)现场构造 `ChatRequest`。两个公共方法都是入口,都要校验并透传。
|
||||
|
||||
**不顺手重构这两条链的参数列表**: 它们已在逐层传 `session_id`/`parent_call_id`,再加两个即四个同类参数,把它们收成一个值对象在美学上更优,但那会改动 embedding 与 OCR 现有的全部内部签名,属任务外重构(反 gold-plating)。本次只做加法;若日后参数继续增长,再单独立项。
|
||||
|
||||
**缓存命中行与终态失败行同样带维度**: 前者读 `request` 而非缓存中的历史响应(维度是"本次调用由谁发起",不是历史那次);后者虽无具体源,但租户归属是已知的——这两行恰恰是审计最需要的(缓存命中意味着这次没花钱但确实发生了;终态失败意味着这个租户的请求没被服务)。
|
||||
|
||||
@@ -205,7 +211,7 @@ CREATE INDEX CONCURRENTLY idx_llm_calls_tenant_created
|
||||
|
||||
单元层——校验规则逐条红线(key 字符集/数量上限/value 类型/长度/`pg_` 前缀拒绝/**非有限 float**),每条断言**报错而非静默丢弃**(这是 §4.2 的核心承诺,也是与 Langfuse 分道的地方);`ChatRequest` 快照不可变;三个 emit 入口都带上维度(尤其**缓存命中行与终态失败行**——这两条最容易被漏,而它们恰是审计刚需)。
|
||||
|
||||
**两条链路各测一遍**——`chat()` 与 `embed()` 都必须有"传入维度 → 遥测行带该维度"的用例。embed 侧尤其不能省: 它经 `_embed_batch`/`_attempt`/`_emit` 四层透传,任一层漏传都不会报错、只会让维度恒为空,而这正是 issue 第 2 条诉求的落点。批量切批时**每一批的行都应带同一份维度**(维度属于本次 `embed()` 调用,不随批次变化)。
|
||||
**三条链路各测一遍**——`chat()`、`embed()`、OCR 两方法都必须有"传入维度 → 遥测行带该维度"的用例。embed 与 OCR 尤其不能省: 它们各经四层透传,任一层漏传都不会报错、只会让维度恒为空。批量切批时**每一批的行都应带同一份维度**(维度属于本次 `embed()` 调用,不随批次变化);OCR 的 `recognize_text` 与 `parse_layout` **各测一个**,只测一个会漏掉另一个的透传缺口。
|
||||
|
||||
非有限 float 单独一条: 断言 `chat(meta={"x": float("nan")})` 抛 `ValueError` 而**不是**写入时降级成 warning——这是 §6 记录的那个被推翻论断的机械化守卫。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user