80 lines
12 KiB
Markdown
80 lines
12 KiB
Markdown
# Changelog
|
||
|
||
## 1.0.4(2026-07-31)
|
||
|
||
响应可观测字段扩展(issue #3)。下游 dissect 要把每次调用落成一行审计记录,其中两列拿不到值:供应商侧 prompt cache 命中了多少 token、这次调用实际跑的是哪个模型版本。前者关系到能否把「缓存命中率差异带来的成本」与「实验条件本身带来的成本」分开,后者关系到实验快照的可复现性。本次把两者暴露到公共类型与遥测表,并让成本换算认识缓存单价。
|
||
|
||
### 新增(纯增字段,不破坏任何现有调用方)
|
||
|
||
- **`LLMResponse` 新增 `cached_prompt_tokens: int | None` 与 `model_reported: str | None`。** 前者是供应商 prompt cache 命中的输入 token 数(OpenAI 兼容格式的 `usage.prompt_tokens_details.cached_tokens`),后者是 API 响应体里的 `model` 字段(与 `.env` 配的别名可能分叉——供应商把别名指向新权重时,只有它认得出真正跑的那个版本)。两者均带默认值 `None`,逐字段传参的 fake 构造零改动。
|
||
- **`None` 与 `0` 是两回事,不可混同。** `None` = 该源不上报这个数(下游据此声明「本源不可做缓存成本校正」);`0` = 该源上报了一次真实零命中。网关报文一律不可信:形态异常(负数、字符串、`bool`、`prompt_tokens_details` 非 dict)一律归 `None` 且绝不抛异常——可观测字段缺失不得打断调用。
|
||
- **遥测表 `llm_calls` 新增 `cached_prompt_tokens` 与 `model_reported` 两列**,`TelemetryRecorder` 端口由 18 字段扩为 20。两个后端在初始化期对**已存在的旧表幂等补列**——`CREATE TABLE IF NOT EXISTS` 不会给旧表加列,不补则每行写入都被逐行 warning 丢弃、遥测静默全失。两侧都是**先探测缺列、只在真缺列时才 ALTER**(SQLite 查 `PRAGMA table_info`,Postgres 查 `pg_attribute`):`ADD COLUMN IF NOT EXISTS` 即使列已存在也会先取 ACCESS EXCLUSIVE 锁,而遥测是内联 await,让每个进程的首次写入都去锁共享审计表会拖垮业务调用;稳态下一条 ALTER 都不会发。**补列失败只降级为逐行丢弃,绝不会让 recorder 整体失能**(应用账号只有 INSERT 权限时,`ALTER TABLE` 的 ownership 检查早于存在性判断,列齐全也会失败)。
|
||
- **`PricingTable` 支持可选的缓存读取单价 `cached_input_per_1m`。** 配了该档且本次有命中时按 `(prompt - cached) × input + cached × cached_input` 分段计价,消除 cost 的系统性高估;**未配则不猜折扣率**,退化为现状全额输入价(P5 严禁默认值掩盖)。旧价格表文件与 embedding 侧的三参 `cost()` 调用零改动。命中数超过输入总数时按总数夹取并 warning,不产生负成本。
|
||
|
||
### 下游请读
|
||
|
||
- **`cache_hit` 与新字段是两个不同的东西。** `cache_hit` 指的始终是 **PolyGateway 自身的响应缓存**(未产生网关调用),而 `cached_prompt_tokens` 指的是**供应商服务器**复用了提示词前缀、那部分按更低单价计费——真实调用里天天发生,`cache_hit` 永远看不见它。字段名保持不变(改名会破坏迁移兼容),语义已在 docstring 中消歧。
|
||
- **统计供应商缓存命中率必须写 `WHERE cache_hit = false`。** 缓存命中行的这两个字段是**原样回放**的历史值(与 `model`、`prompt_tokens` 同一口径:`CacheMW` 只覆写与本次调用相关的时序字段),计入会重复计数。这与 1.0.3 里 `cost` 缺口口径的坑是同一类。
|
||
- 缓存命中行的 `cost` 仍恒为 `0.0`(未产生新调用),该短路排在任何单价换算之前,不受缓存单价档影响。
|
||
- 旧格式的缓存条目(缺这两个键)照常可重建为 `None`,不会回源;历史遥测行的新列为 NULL。
|
||
|
||
## 1.0.3(2026-07-30)
|
||
|
||
`est_tokens` 解耦(issue #2):一个常量此前被派了两份对"保守"定义相反的差事——TPM 入场预扣(押多了只是慢,安全)与 usage 缺失时的用量兜底(按上界记账只会账单虚高)。本次把两者拆开。
|
||
|
||
### 行为收紧/变更(下游请读)
|
||
|
||
- **`usage_source` 新增第三个值 `unavailable`。** 值域由 `measured`/`estimated` 两态变三态:`unavailable` 表示用量信息不可得(usage 帧缺失、失败尝试、终态失败),`estimated` 收窄为"有实测数字但可信度降级"(只剩打捞路径这一个生产者:收到 usage 帧但流被截断)。历史库里既有的 `estimated` 行语义不变、读兼容;按 `usage_source` 分支的下游代码需要认识新值。OCR 成功行**不受影响**,仍是 `measured`(0 token 是事实而非未知)。
|
||
- **用量不可得的行,`cost` 由数值变 NULL。** 此前 usage 帧缺失时库拿 `est_tokens`(按定义是最坏情形上界)当实测值,又整块塞进 `completion_tokens` 换算——输出单价通常是输入的数倍,实测双重高估约 26 倍;`est_tokens=0` 时则算出 `0.0`,让"免费"与"未知"在数据上不可区分。现在这类行如实记 `0/0` + `unavailable` + `cost=NULL`。`SUM(cost)` 天然跳过 NULL,账目缺口用 `WHERE usage_source = 'unavailable' AND cache_hit = false` 量化(**`cache_hit` 限定不可省**:缓存命中行未产生新调用,cost 仍是事实上的 `0.0`,本无缺口)。成本汇总若此前依赖"cost 非空"的隐含假设,请复核。
|
||
- **`est_tokens` 由必填降为可选调优覆盖。** 装配校验 `tpm > 0 ⇒ est_tokens > 0` 已删除——它把供应商配额(运维能从配额页抄到)与库的实现细节(预扣量,无人能正确取值)绑死。未填时库按 `max(1, tpm // 60)` 派生("一次调用约占一秒钟的配额份额",尺度无关:任何配额规模都收敛到约 60 个在途)。字段与 `{SCOPE}__{PROVIDER}__{N}__EST_TOKENS` 环境键**保留不删不改名**,显式填值仍然优先。此前为绕开该校验而把 `tpm` 限死为 0 的调用方,现可填真实 TPM。
|
||
|
||
## 1.0.2(2026-07-30)
|
||
|
||
1.0.1 的续作:那一版把三条跨字段守卫收进构造期后,独立验证发现 `from_env` 上还留着同一类的 15 条校验与 4 条规范化,一并收拢。
|
||
|
||
### 修复
|
||
|
||
- **后端选择与条件必填项在任何构造路径上都校验。** 以下此前只有 `from_env` 拦得住,`from_settings()` 与直接构造一律放行:`limiter_backend`/`breaker_backend`/`cache_backend`/`telemetry_backend`/`selector`/`quota_full` 六个字段的合法域;取 `redis` 的后端必须有 `redis_url`;启用缓存必须有 `cache_namespace` 与正 `cache_ttl_s`;`telemetry_backend` 取 `sqlite`/`postgres` 时对应的路径/DSN 必填;`structured_max_retries` 非负;`scope` 非空。
|
||
- **`client.py` 五处断言的前提现在真的成立。** `assert settings.redis_url is not None # 内部不变量: config 已校验` 之类的注释此前在 `from_settings` 路上是假的:断言开启时抛不含任何字段信息的 `AssertionError`,`python -O` 下断言被移除、错误退化为 redis 库抛出的连接串解析异常。注释已改为点明由哪个校验方法保证。
|
||
- **构造路补齐了 `from_env` 一直在做的规范化**,两条装配路对同一输入产出同一个值:
|
||
- `scope` 小写并去空白。它直接进 Redis key(`pgw:limit:{scope}:…`、`pgw:gate:{scope}:…`),此前一个进程走 `from_env("LLM")` 拿到 `llm`、另一个直接构造传 `"LLM"`,**同一逻辑 scope 的限流与熔断状态会分裂到两套命名空间**,各记各的配额与熔断状态,分布式治理静默失效且不报错。
|
||
- `redis_url`、`pricing_path` 的空串归 `None`。留着空串会骗过 `is None` 判断,把错误推迟成 redis 客户端的连接串解析异常或 `Is a directory: '.'`。
|
||
- Postgres DSN 剥掉 SQLAlchemy 驱动后缀(`postgresql+asyncpg://…` 的 `+asyncpg` asyncpg 不认)。这一条剥的时候会发一条 warning——库动了调用方给的值,不该静默;日志只出现 scheme 段,DSN 带密码,整串不进日志。经 `from_env` 装配的不受影响也不会有这条 warning(`_load_pg_dsn` 早就剥干净了)。
|
||
- **`EmbeddingSettings` 的 `batch_size` / `expected_dim` 域校验也移入构造期**,此前只有 `EmbeddingSettings.from_env` 校验,直接构造出 `batch_size=-3` 要到 `EmbeddingClient` 构造时才 fail-loud。
|
||
|
||
### 行为收紧(下游请读)
|
||
|
||
同 1.0.1:经 `from_env()` 装配的调用方**不受影响**。手工构造 `GatewaySettings` 或对它 `dataclasses.replace` 的调用方,若配置组合非法,现在会在构造期抛 `ValueError` 并点出字段名,而不是留到运行时表现为静默不建后端、裸 `AssertionError` 或第三方库的天书报错。
|
||
|
||
**一处静默改值需要留意**:此前手工构造传 `scope="LLM"`(非全小写)的调用方,升级后 scope 会被规范化为 `llm`,**Redis key 随之从 `pgw:limit:LLM:…` 切到 `pgw:limit:llm:…`**。这正是本次要修的问题——旧行为下这批 key 与 `from_env` 装配的进程根本不在同一命名空间;但切换发生的那一刻,旧键上的在途租约会被遗弃,靠 TTL 自愈。滚动升级期间建议留意限流配额短暂偏松。
|
||
|
||
## 1.0.1(2026-07-30)
|
||
|
||
### 修复
|
||
|
||
- **装配守卫在任何构造路径上都生效,不再只在 `from_env` 上。** 三条跨字段不变量(源 `timeout_s` ≤ `lease_ttl_s`、`stall_window_s` ≥ 最大源 TTFT、`probe_ttl_s` ≥ 最慢源 `timeout_s` + 5)原先只在 `GatewaySettings.from_env` 里校验,而装配有两条官方路——走 `from_settings()` 或直接构造能装出违反不变量的配置且不报错,故障留到运行时才表现为:租约先于请求过期使并发悄悄超出配额、正常慢首包被误判卡死掐断、半开探针在途即被接管。守卫已收进 `GatewaySettings.__post_init__`,与 `types.py` 各子配置一致,三个 client(Gateway/Ocr/Embedding)的全部工厂一并覆盖。
|
||
- 新增 `sources` 非空校验。此前零源配置只在 `from_env` 路径被拦,直接构造可装出必然选源失败的 client。
|
||
|
||
### 行为收紧(下游请读)
|
||
|
||
直接构造 `GatewaySettings` 或对它做 `dataclasses.replace` 时,若上述组合非法,**现在会在构造期抛 `ValueError`**,而不是留到运行时。经 `from_env()` 装配的调用方**不受影响**——那条路本就跑这些守卫。手工拼配置(如从 YAML 读出后构造)的调用方若此前撞上过上述任一故障,升级后会在启动时立即得到点名字段的报错。
|
||
|
||
守卫报错文案的**补救建议**改为点字段名(`lease_ttl_s`、`backpressure.stall_window_s`、`breaker.probe_ttl_s`)。原文案已点出字段名,但建议部分给的是环境变量键(如"调大 `PGW_LEASE_TTL_S`"),而不走 env 的调用方从没设过那些键。键名映射见 `.env.example` 与 wiki `参考-配置键`。
|
||
|
||
## 1.0.0(2026-07-22)
|
||
|
||
首个正式版。统一 LLM/VLM/OCR/Embedding 调度与中转库,治理单位为一次模型调用;经 GovDoc-SaaS 与 CHSAnalyzer 两个真实项目全量迁移验收(ARCHITECTURE §11)。
|
||
|
||
- **M1 核心**: types/errors/ports 内核、OpenAI 兼容 httpx transport(SSE + 非流式)、三层活性看门狗、自研重试(错误四分类驱动,换源/退避/Retry-After)、多源多账号 + 选源 + 源冷却、内存限流/熔断、Redis/内存响应缓存(key 含 namespace/salt/多模态摘要)、SQLite 遥测(18 字段必录)、结构化输出阶梯(json_repair/原生 schema + 有界重问)、provider 注册表、`from_env` 装配。
|
||
- **M2 分布式**: Redis 六道闸限流(Lua,契约测试双后端共用)、跨进程熔断(单探针租约 + epoch fencing)、背压 stall 双条件判定、Postgres 遥测、pricing 成本、EmbeddingClient(分批/维度校验)。
|
||
- **M2.5 治理韧性**: 双通道熔断(失败率窗 + 连败 + 健康证据抑制)、健康感知选源(EWMA×在途 P2C 缺省)、AIMD 自适应并发、429 免重试预算、健康门槛降权;故障混编 soak 同场景 58.1%→98.96%。
|
||
- **M3 OCR**: OcrTextPort/OcrLayoutPort 端口族 + MonkeyOCR 双端点 transport(数值防御下沉)、OcrClient 独立治理循环、`check_health()` 逐源预检;OCR soak 1500 调用 99.73%。
|
||
- **M4 迁移验证**: GovDoc 与 CHS 全量迁移(合计约 −6800 行项目治理代码由库继任),原测试全绿 + 真实冒烟 + 50 样本回归;Gitea PyPI 分发。
|
||
|
||
安装(实验室 Gitea PyPI):
|
||
|
||
```bash
|
||
pip install --index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ \
|
||
--extra-index-url https://pypi.org/simple/ "polygateway[redis,postgres,structured]==1.0.*"
|
||
```
|