• 1.3.11 Stable

    iomgaa released this 2026-10-22 15:55:15 +08:00 | 0 commits to main since this release

    #34: OCR 新增整份文档解析 OcrClient.parse_document——CHSAnalyzer3 要把几百篇医学论文 PDF 整份转成带页码的正文与表格,而库只会返回版面框,且 PDF 被写死的 image.jpg 标签当成图片上传。两个症状同根: 库把 OCR 输入建模成「一张图 + 写死的标签」、输出建模成「只有框」,没有「文档」这个概念。设计 research-wiki/designs/2026-10-21-issue34-ocr-document-design.md。

    新增

    • OcrClient.parse_document(document, *, session_id, parent_call_id, tenant_id, meta, call_deadline_s) -> OcrDocumentResult: PDF 或图片整份交给 MonkeyOCR,返回逐块 OcrDocumentElement(继承 OcrLayoutElement,仅多一个 text:text/title 纯文本、interline_equation 裸 LaTeX、table HTML、image 空串)、page_sizes 与整篇 markdown。治理链路(限流/熔断/重试/换源/遥测/调用方维度/期限)复用 parse_layout 的同一循环。
    • 公共类型 OcrDocumentElement / OcrDocumentResult(包根导出);端口 OcrDocumentPort;transport 协议 OcrDocumentTransport(独立于 OcrTransport,不动旧 runtime_checkable Protocol 的成员,既有实现与测试替身不受影响)。
    • CallOperation / 遥测 operation 列新增取值 parse_document(无 DDL、无新列);占位 <ocr:document image_bytes=N>,response 摘要 <document elements=N pages=M md_chars=K> 永不写入正文。
    • 内容只取自 _middle.json 与配对的 .md,有意不读 _content_list.json(它是展示格式,星号、反引号、波浪线、美元符会被转义,实测 134 个文字块中 23 个含 \$)。

    修复

    • recognize_text(PDF) 的静默错误结果: 此前一切上传都标 image.jpg,而 MonkeyOCR 按扩展名分派。PDF 走图片分支时 /ocr/text 返回 success:true 与幻觉文本(实测 "The image contains a single character: 1"),下游完全无感。上传标签现在按内容判定(图片魔数优先,再看前 1KB 内的 %PDF-),三个方法共用一处;非 PDF 一律沿用 image.jpg(字节级不变)。

    行为变更

    • parse_layout(PDF) 的上传分支改变(图片数据集 → PDF 数据集)。差分实测: 文本型与无文字层的位图型 2 页 PDF,_middle.json / _content_list.json / .md 逐字相同;旋转页、带批注页未测,不构成保证。该路径无已知消费者(医院线只传图片)。

    • parse_document 对坏输入的处置(仅新路径;共享分类不变): 无法识别的字节本地拒绝、不发请求(RequestRejectedError,不带 status_code);服务把一切失败包成 500 "Parsing failed: …",其中已知的确定性坏输入(Failed to open stream / Cannot open empty stream / cannot save with zero pages / is no PDF)归 RequestRejectedError(status_code=500)——不重试、熔断记成功不计窗口。其余 500 仍是瞬时错误。标记集合可经 MonkeyOcrTransport(deterministic_failure_markers=...) 覆盖。

    • 结果包解析的外部数据异常边界统一落 ResultInvalidError(含加密成员、未知压缩方法、巨大整数、深嵌套 JSON),不漏出原生异常——漏出会让客户端既不写终态遥测行、又不归还半开探针。

    • 文档路径流式下载并限制整包 256 MiB;_middle.json ≤ 64 MiB、.md ≤ 16 MiB;解析在线程里执行,取消与 call_deadline_s 基本不被占住(json.loads 持有 GIL:实测 54 MiB 的 middle 解析约 1 秒、事件循环最大停顿约 0.3 秒;取消后线程自然收尾、占用默认 executor,正常大小是毫秒级)。parse_layout 的口径(bbox 整数化退化校验、page_idx 回退、无体积上限)保持原样。

    • 共用的第一段协议新增 download_url 校验: 服务响应里的畸形 download_url(控制字符、坏方括号、越界端口)此前会让 httpx.InvalidURL 等原生异常漏出 OcrClient(不写终态遥测行、半开探针不归还),现归瞬时错误、不发第二段请求。parse_layout 共用这一处,属顺带修复;不限制主机(基线接受绝对地址)。

    • 嗅探副作用: 前 1KB 内含 %PDF- 字样的非 PDF 文本在 recognize_text / parse_layout 上的上传标签由 image.jpg 变为 doc.pdf;BMP 魔数现要求 DIB 头大小字段合法(不再单凭 BM 两字节)。

    • 文档路径读取错误响应体只读前 64 KiB(摘要只需要几 KiB),不再把巨大的错误体整个读进内存。

    已知限制与运维须知(详见 README「1.3.11 OCR 整份文档解析」)

    • 服务端丢弃脚注、页眉页脚、页码,表格脚注里的显著性/缩写说明不会出现在结果里。
    • timeout_s 不是总耗时上界(逐次网络操作的超时);长文档的硬上限靠 call_deadline_s,并应 ≤ lease_ttl_s。库内无基于延迟的熔断,长文档不会被判慢。
    • 若 MonkeyOCR 部署为 sync-only 后端(出厂默认 lmdeploy),/parse 每实例全局串行,长文档会队头阻塞同实例上医院线的请求;请向运维确认 backend。
    • 多行/多 span 文本块、inline_equation span、嵌套 caption/footnote 子块三条路径无真实样本,仅由构造 fixture 测试。
    • 后续: #35(共享分类对确定性坏输入 500 仍归瞬时)、#36(recognize_text 不校验服务响应的 success)、#37(parse_layout 结果包解析仍漏出原生异常、download_url 不限制主机)。
    Downloads
  • 1.3.10 Stable

    iomgaa released this 2026-10-15 13:18:12 +08:00 | 20 commits to main since this release

    #30/#31 同根修复: 1.3.9 以 [DONE] 存在、正文非空、字段白名单判定成功,上游的正常结局被当瞬时错误重试到耗尽(magpie/glm 不发 [DONE] 时单次调用挂 1216s;只有思考的 length 截断每次写满 max_tokens 后重抽)。本版改以上游 finish_reason 与传输完整性为准,流式/非流式/缓存读写共用一份判定。设计 research-wiki/designs/2026-10-14-issue30-31-completion-verdict-design.md。

    修复

    • 缺 [DONE] 但完整的流(#31,两种模式): finish_reason 已到、其同帧或之后收到实测 usage、连接干净关闭 → 完整交回,用量记 measured,stream_end="eof_after_finish"。文本模式默认 MISSING_DONE=retry 原本同样会重试到耗尽;该键现在只管真截断(无 finish 或 finish 后无实测 usage)。
    • 只有思考的 length 截断(#30): 保真模式原样交回(content 空、思考完整、finish_reason=length),不再重抽。
    • null 与未知字段(#30): 任意层 null 视为缺省(refusal: null、tool_calls: null 不再判失败,非流式同样修复);保真模式未知字段按 OpenAI 流式惯例无损合并(字符串拼接、对象递归、带 index 数组按 index 合并、index/type/id/format 同值幂等),Gemini extra_content 一类工具层扩展可原样回传。
    • reasoning 别名: vLLM/OpenRouter 的 reasoning 字段计入活性(此前只有思考的流会被 TTFT 看门狗误杀),文本模式无非空 reasoning_content 时投影为 thinking。
    • 截断结果不再写缓存: 文本 salvage 结果此前会被缓存并在 TTL 内反复回放。

    行为变更

    • 文本模式 length/过滤且无正文 → ResultInvalidError(原 TransientError 重试到耗尽): 不重试、不换源、熔断记成功不计窗口;不经 AllSourcesExhausted 包装,直接到调用方。原因码见异常消息(length_without_content / filtered_without_content)。文本模式"只有思考 + stop"维持瞬时(证据收集中)。
    • finish_reason 为 network_error/error(GLM 等上游自报失败)即使有正文也判瞬时。
    • 保真模式: 只有思考/拒答/空白 + 明确 finish(非上游错误)→ 原样交回;length 截断的半截工具(缺 id/name)→ ResultInvalidError tool_truncated;无参函数全程只发 arguments: null(其余字段齐全)→ ResultInvalidError tool_arguments_null(确定性方言,交付值不改写)。
    • 文本模式 choice.index: null 视为 0(此前整帧被丢弃);tool_calls 中全为 null 的元素按 tool_calls: null 处理,不产出会被上游拒收的空数组。
    • 缓存: 只有自然结束(stop/tool_calls/无 finish 的旧口径)且有正文的结果写入;length/过滤/未登记 finish 值、salvaged、空正文均交付但不缓存,读到此类条目按 miss。未登记 finish 值按源告警一次。
    • 活性口径增补: 未知字段的非空字符串片段(如 reasoning)算进展,index/type/id/format 等标签键不算;stall_ms 计量随之覆盖。

    新增

    • LLMResponse / TransportResult 末尾追加 finish_reason: str | None、stream_end: StreamEnd | None(done / eof_after_finish / salvaged;非流式、缓存命中为 None);包根导出 StreamEnd。
    • 遥测 llm_calls 末尾追加可空列 stream_end(INSERT 字段 38 → 39,物理列 39 → 40;旧行 NULL 不回填;auto 档幂等补列,manual 档裁剪写入并点名缺列,补列 SQL 由 telemetry_schema_sql() 自取)。

    破坏性(请先读)

    • TelemetryRecorder.record_llm_call 新增必填 keyword stream_end: 自建 recorder 未跟进会在装配期被形状闸拒绝(**fields 形态不受影响)。

    提醒

    • 保真模式交付 finish_reason=length 的工具调用时 arguments 可能是半截 JSON: 执行工具前先查 finish_reason。
    • 未知字段合并冲突时会删除该字段并按(源, 路径)告警一次: 结果可能缺字段,以告警为准。典型形态: 带 index 的数组(如 reasoning_details)后续帧元素缺 index 即判形态改变,整个字段被删。
    • 诚实边界: 中转在上游出错后主动正常关闭连接,与真完整在传输层不可区分;防线是"finish 必须存在、不属上游错误、且其后有实测 usage"。
    • 旧版本写入的 salvage/length 文本缓存无法辨认: 受影响集合须在升级后首次读写前更换 namespace/salt(同 1.3.9 提醒)。
    Downloads
  • v1.3.9 22609c90d6

    v1.3.9 Stable

    iomgaa released this 2026-10-02 11:22:07 +08:00 | 33 commits to main since this release

    • 新增 chat(response_mode="openai"),返回 LLMResponse.completion:assistant 消息、工具调用、finish_reason、上游 response_id、usage 快照;支持流式聚合及非流式。
    • 修复 SSE 顶层 error 后接 DONE 被当成功、同帧 content/reasoning_content 丢失;工具实际进展纳入看门狗、首 token 与中段对冲。
    • 保真仅支持单 choice,拒绝 structured 组合;工具参数逐字拼接,不执行或修复工具;缺 DONE 不允许 salvage。
    • 保真缓存独立身份并完整回放;缺协议、畸形协议及无正文无工具的坏条目均按 miss 回源。旧文本缓存可能含曾被吞错的半截结果,受影响集合须在升级首次读写前更换 namespace/salt(含 per-call 覆盖),库不会自动辨认旧污染。
    • 新模式不绕过 managed/raw 冲突校验;raw-only 仍须源级/请求级均无受管意图。Transport.complete 新增必填 response_mode,自定义实现需适配。
    • 分发采用 Apache-2.0;README 补充常驻事件循环线程桥接,不新增同步 API。
    Downloads
  • v1.3.8 0e29deac1a

    iomgaa released this 2026-09-14 15:36:08 +08:00 | 39 commits to main since this release

    流式中段沉默的计量与应对(issue #27 / #28):generation_ms 口径重定义为净生成时间(扣除所有超阈值中段沉默)并配 stall_ms/stall_count 归因字段与遥测两列;另加一条默认关闭的中段沉默对冲(主路首 token 之后相邻增量 chunk 间隔超阈值时,向异源并发再发一次)。两键均不设时行为逐字等于 1.3.7(默认关闭回归门: 既有 unit/contracts 全套件语义断言一行未改)。

    行为变更: generation_ms 口径重定义(1.3.7 墙钟 → 本版净生成时间)

    Warning

    同一字段,两版口径不同。 1.3.7 的 generation_ms 是赢家那次 transport 调用的完整墙钟(中段沉默全部计入);本版起是净生成时间 = 墙钟 − Σ(所有 > {SCOPE}__STREAM__STALL_THRESHOLD_MS 的相邻增量 chunk 沉默),仍含 TTFT 与尾部(末 chunk → 流结束归活跃侧,墙钟基底口径的有意选择)。还原公式: 墙钟 = generation_ms + stall_ms(stall_ms 为 None 时墙钟 = generation_ms)。

    语义突变在一般情形下违反「字段只增不删不改名」精神,本次由人类以「窗口期最小代价」裁决放行(2026-09-11),依据三行: 该字段 1.3.7(2026-09-10)才引入、发布仅 2 天;消费者唯一(提出本需求的下游 Terminal-Bench,三参考项目均未消费);遥测 DB 从未存过 generation_ms,无历史账本纵向断裂。字段名「裸生成时间」原本承诺的就是净口径,本次重定义是兑现名字而非推翻承诺。max_inter_token_ms(单次最大相邻间隔)原语义一字不动——它与 generation_ms 是两个口径,不能互算。

    项 口径
    计入沉默的间隔 相邻增量 chunk(content 或 reasoning_content 均计,与看门狗/首 token 活性口径一致)间隔严格大于阈值(恰好等于不减);空 chunk / usage 帧 / SSE keepalive 不计时;chunk 边界是传输/解析缓冲产物而非 token 边界,本口径不假装是 ITL
    stall_ms: int | None 赢家路超阈值中段沉默总额(毫秒);与 stall_count 成对报告(只报净数无法行动)
    stall_count: int | None 超阈值相邻间隔个数
    None 条款(照 vLLM 排除条款) 非流式 / 增量 chunk <2 → 两字段 None 且 generation_ms 不退减(= 原墙钟);None = 「无沉默观测」而非 0,下游预算公式以 stall_ms is None 为分支条件
    对冲赢家 三值全部取赢家路;已完成但落选的对冲输家其沉默在其 attempt 遥测行(可按 logical_call_id join 还原);被取消的输家无 TransportResult、hedge_cancelled 行 stall 恒 None
    与业界基准口径的区隔 NVIDIA TTFT/ITL/E2EL 与 vLLM 的基准口径均不扣除沉默——扣沉默是本库自造口径,与基准数字比较时不得混用

    遥测两列(纯增量,可空)

    遥测表 llm_calls 末尾追加 stall_ms / stall_count 两可空列(INSERT 字段 36 → 38,物理列 37 → 39),落库值与 CallStats 快照同源(retry 层已取整的 int)。旧行 NULL 不回填(NULL 表达「补列之前根本没记过这件事」);auto 档幂等补列(先探测后 ALTER),manual 档按现有列裁剪写入并点名缺列,补列 SQL 由 telemetry_schema_sql() 自取。generation_ms 仍不进遥测列——要墙钟用 latency_ms 或 generation_ms + stall_ms 还原。TelemetryRecorder.record_llm_call 协议同步加两个无默认值 keyword-only 参数: 自建 recorder 未跟进会在装配期被形状闸当场拒绝(不是运行期静默丢行),**fields 形态不受影响;emit_cache_hit / emit_terminal_failure 与失败/取消 attempt 行两列恒 NULL。

    中段沉默对冲(默认关闭;无业界公开先例)

    新键 {SCOPE}__HEDGE__STALL_AFTER_S: 主路首 token 之后相邻增量 chunk 间隔超此阈值 → 向异源并发再发一路,先回者赢、输家取消。编排/记账/遥测全部复用 1.3.7 对冲语义(异源排除、完整准入、拿不到配额静默放弃、hedge_cancelled 遥测行、两败只计一次重试预算)。诚实标注: 业界对中段沉默的落地形态收敛为「看门狗掐流 → 重试」,「中段沉默触发并发副本」在任何主流网关/库中均无公开先例——本能力属超业界现状能力,启用前请理解其语义。

    请先读这三句:

    # 承诺 展开
    1 默认关闭,开启即用配额换延迟 不设该键时行为逐字等于 1.3.7。下游实测(Terminal-Bench 4407 次成功调用)沉默 >20s 占 19.0%——触发率远高于首 token 对冲(ttft >20s 仅 3/4407),开启后约两成调用会在触发窗口内 in-flight 翻倍
    2 输家可能已被上游计费,取消止不住 与 1.3.7 同一性质: 输家按 est 保留预扣(闸内保守记账,不是上游真实计费的计量);取消止得住等待,止不住上游已经烧掉的钱。对账靠 hedge_cancelled 遥测行按 logical_call_id join
    3 独立于 HEDGE__AFTER_S 生效,仅流式 chat 两阈值各自独立,可独设可组合;仅设 STALL_AFTER_S 时首 token 前的沉默不触发(窗在首 token 到达后才开);仅设它且请求非流式 → 逐字走单路(中段沉默无非流式概念,等价未配置);Embedding/OCR scope 配了任一对冲键 → 装配期 warning 后静默退化

    与看门狗的交互及不对称声明(运维「挂起率」统计须知): 主路沉默超 INTER_TOKEN_TIMEOUT_S 被看门狗掐死 → 照常进 Transient 重试链并喂熔断失败计数(1.3.6 起既有语义),对冲路已在跑时本轮不因主路之死而败(等副路裁定,主路 attempt 记账照常);对冲输家被取消 → 不喂熔断(1.3.7 已批准语义)。同为源沉默事件、两种处置,本版不改任一。

    配置面

    键 值域 缺省 守卫
    {SCOPE}__STREAM__STALL_THRESHOLD_MS 正整数毫秒 1000 仅改计量口径,不改任何行为;调整接口三层同权(env 键 / GatewaySettings.stall_threshold_ms / GatewayClient(stall_threshold_ms=...)),无 per-call 覆盖,不做自适应(防与对冲阈值耦出反馈环)
    {SCOPE}__HEDGE__STALL_AFTER_S 有限正数秒 未设 = 关闭 装配守卫: ≥ min over sources of (inter_token_timeout_s or timeout_s)(有效最小值,对冲窗永不开)→ ValueError;≥ call_deadline_s → ValueError;单源 scope 设阈值 → warning 允许

    GatewayClient(...) 直传 hedge_stall_after_s / stall_threshold_ms 两 keyword-only 参数走同一份装配守卫;from_settings/from_env 照常透传。

    Downloads
  • v1.3.7 a28d94451e

    iomgaa released this 2026-09-11 03:19:44 +08:00 | 53 commits to main since this release

    给 chat 链路加了一条默认关闭的长尾对冲(issue #24):一次尝试挂起超过阈值时,并发向另一个等价源再发一次,先回者赢、输家取消。另给 CallStats 追加三个字段,其中 generation_ms(裸生成时间)对四条链路全部生效,与是否开启对冲无关。

    关于对冲,请先读这三句

    # 承诺 展开
    1 默认关闭,开启即用配额换延迟 不配 {SCOPE}__HEDGE__AFTER_S 时行为逐字等于 1.3.6(默认关闭回归门: 既有 unit/contracts 全套件一行断言未改)。开启后触发窗口内 in-flight 翻倍——两路各自走完整准入(配额闸/熔断门/pacer),拿不到配额静默放弃、原请求继续等,饱和期不添乱
    2 输家可能已被上游计费,取消止不住 输家落既有取消路径按 est 保留预扣(闸内保守记账,不是上游真实计费的计量);取消能止住等待,止不住上游已经烧掉的钱。对账靠遥测: 输家 attempt 行 error="hedge_cancelled",与赢家行共享同一 logical_call_id 可 join
    3 对冲只对 chat 生效 EmbeddingClient/OcrClient 不加对冲参数;它们本版的收益是 CallStats.generation_ms 计时(见下)

    触发语义

    调用形态 触发判据
    流式 已过 hedge_after_s 且首 token 未至——慢生成不会被误对冲
    非流式 已过 hedge_after_s,纯总时长阈值。非流式无中途信号,「挂起 vs 慢生成」物理不可分,只能靠阈值取值控制误对冲(建议取源 p50 的数倍)

    输家取消不喂熔断/健康分(挂起 ≠ 源死亡);两路都失败才进既有重试循环且只计一次重试预算(对冲是一次尝试的加速形态,不是两次独立尝试;两路皆 429 时按既有规则免预算并退 stall 账,一路 429 一路真失败则计一次不退还)。

    CallStats 三新字段(全带默认值,1.3.6 的构造方式不炸)

    字段 口径
    generation_ms: int = 0 裸生成时间: 赢家/成功那次 transport 调用的墙钟,排除 admission 排队、重试退避、对冲触发前等待与清理遥测;与 total_latency_ms 的差值即「在等不在生成」的波动开销。结构化重问取最后一轮(覆盖),embedding 为各批 transport 之和(累加),缓存命中恒 0(未产生 transport 调用,0 是实测)
    hedges: int = 0 实际并发发出的对冲路数(触发但准入失败静默不计);未启用对冲恒 0
    hedge_won: bool = False 赢家是否为对冲路;无对冲恒 False

    与 call_deadline_s 的关系

    两者正交、可独立配置: deadline 让长尾更早失败,hedge 让调用更快成功。组合时装配守卫强制 hedge_after_s < call_deadline_s(违反即 ValueError);hedge_after_s ≥ min(源 timeout_s) 同样装配期报错(对冲永不可能触发);hedge_after_s ≥ min(已设 ttft_timeout_s) 与单源 scope 设阈值降为装配期 warning(流式档已被看门狗先行切断 / 运行期自然静默)。per-call chat(call_deadline_s=X) 使 X < 对冲阈值时该次调用对冲不触发,属合法语义不告警。

    配置面

    键 值域 缺省
    {SCOPE}__HEDGE__AFTER_S 有限正数秒(复用期限同款值域校验) 未设 = 关闭
    {SCOPE}__HEDGE__MAX_EXTRA int ∈ [1,3] 1;v1 仅单路对冲生效,>1 接受但装配期 warning(梯次追加为预留)

    GatewayClient(...) 直传两 keyword-only 参数(hedge_after_s / hedge_max_extra)走同一份装配守卫;from_settings/from_env 照常透传。embedding/OCR 的 settings 嵌 GatewaySettings 故守卫照常跑,但对冲键对这两条链路不生效。

    Downloads
  • v1.3.6 166b2865d0

    iomgaa released this 2026-09-10 23:31:41 +08:00 | 62 commits to main since this release

    给一次逻辑调用加了一条可选墙钟硬边界(issue #22),并修好取消路径的 TPM 结算与 Retry-After 非有限值防御。

    关于调用期限,请先读这三句

    # 承诺 展开
    1 期限治理的是「等待」,不是「返回时刻」 到期后取消在飞的尝试,但清理(遥测写入、限流结算、缓存收尾)仍在 finally 里跑完,允许超出期限。实测构造(两次慢遥测写入)里返回时刻达期限的 5–7 倍;库只对「等待被切断」给承诺,对「多久返回」不给上界
    2 到期 ≠ 未产出、≠ 未计费 上游可能已经算完并计费,只是结果在返回路上被丢弃(缓存写入慢于期限就是一例)。把 CallDeadlineExceeded 当成「这次没花钱」会低估成本
    3 不配置就是 1.3.5 语义,逐字不变 call_deadline_s 缺省 None 时根本不进 asyncio.timeout 上下文。故 1.3.5 的两条长等仍在: 纯 429 序列(429 不消耗重试预算)仍可能长时间等待;有限大的 Retry-After(如 3600s)仍照睡——库有意不用 backoff_max_s 去夹它,唯一制约手段就是本版这条期限

    公共面新增(三项,全为纯新增)

    # 位置 内容
    1 polygateway.CallDeadlineExceeded 新异常;带 scope / deadline_s,无 retry_after_s(到期不含「何时可再试」,给 0.0 会指示下游立刻重打饱和渠道)
    2 配置键 {SCOPE}__CALL_DEADLINE_S 缺省不设 = 不启用;非法值(0/负/nan/inf/非数)在装配期当场 ValueError
    3 三个 client 构造参数 + 四个公开方法的 keyword-only 参数 GatewayClient / EmbeddingClient / OcrClient 的 call_deadline_s;chat / embed / recognize_text / parse_layout 可 per-call 覆盖(None = 继承装配值,不提供「本次关闭」)。一次 embed 的 N 个批次共享同一份期限,不随批数放大

    Warning

    except GatewayUnavailableError 接不住 CallDeadlineExceeded。 新异常直接继承 PolyGatewayError,既不属四分类,也不在 GatewayUnavailableError 族内——期限到期是调用方自己设的边界,不是网关不可用。只有显式配了期限的调用方才会遇到它,需要处理就单列一条 except。遥测侧无需改动: 三个边界既有的 except PolyGatewayError 会接住它并照常写一条 terminal_failure 行(error_type='CallDeadlineExceeded'),零新增列。

    行为变更:取消路径的 TPM 结算口径

    情形 1.3.5 本版
    取消发生在端口已开始、结算尚未确定时 settle(0),入场预扣整笔退还 按 est 保留预扣(方向是宁多扣不空退: 上游可能已计费)
    结算已确定(含真实 usage 恰为 0 的成功、已判 SourceDead 的 0) 按已算出的值 一字不变,取消不覆写
    未被四分类接住的异常逃逸(RuntimeError 等) 0 仍按 0,本版不扩大语义(已登记为残留)

    启用期限后库自身会常规性触发取消路径,故这条记账修复与期限同版交付。OCR 的 settle(0) 不变——无 token 是事实而非「未知」。非取消路径的最终结算值与 1.3.5 逐字相同,只是算得更早(失败分支的结算决定前移到其第一个 await 之前)。

    其他

    • Retry-After: inf / 1e999(float() 会把它舍成 inf)此前会原样进入退避并让该次尝试睡到天荒地老;现按「无提示」处理,退回纯指数退避,并发一条带源名与判据词的 warning(不回显原始头,429 风暴下会淹掉真信号)。nan、空串、负数、HTTP-date 的既有值语义一字未动。
    • 限流 Lua、Permit 端口签名、遥测 schema、缓存 key 公式、重试预算与退避算法、熔断语义均未改动。
    Downloads
  • v1.3.5 ab00aa4457

    v1.3.5 Stable

    iomgaa released this 2026-09-10 01:53:20 +08:00 | 73 commits to main since this release

    把治理单位从「一次尝试」补齐到「一次逻辑调用」(issue #19、#23)。此前重试、换源、结构化重问、embedding 分批都各自独立可见,而「这一次调用总共打了几次、总共花了多久、最后为什么失败」在库外拼不出来;结构化耗尽、embedding/OCR 的无源与准入拒绝更是一条遥测行都没有。

    公共面四项变更

    # 位置 变更 谁会当场断
    1 polygateway.CallStats 新导出的 frozen dataclass(logical_call_id / attempts / total_latency_ms) 无(纯新增)
    2 LLMResponse / EmbeddingResponse / OcrTextResult / OcrLayoutResult 各追加末尾字段 call_stats: CallStats | None = None 按位置解包这四个类型的代码
    3 ports.TelemetryRecorder.record_llm_call() 新增 10 个无默认值 keyword-only 参数(26 → 36 参) 任何自建 recorder——且在装配期当场报错,不再是运行期静默丢行
    4 遥测表 llm_calls 追加 10 列(INSERT 字段 26 → 36,物理列 27 → 37);新增 event_kind='terminal_failure' 行 按旧口径计失败调用数的 SQL

    第 3 条的装配期报错是有意的:_record 的 except Exception 会把旧签名 recorder 的 TypeError 吞成 warning,后果是下游升级后 100% 丢遥测且调用照常成功。降级方向的铁律管的是运行期写失败,不是装配错误。**fields 形态的 recorder 不受影响。

    下游必须做的事

    动作 说明
    计失败调用改 WHERE event_kind = 'terminal_failure' 一次逻辑调用恰好一条终态行。error IS NOT NULL 跨尝试行与终态行,升级后计数会变大
    AVG(latency_ms) 按 event_kind 分组 终态行的 latency_ms 是整个逻辑调用的总耗时,与单次尝试不同量纲
    自建 recorder 补齐 10 参 或改成 **fields;同时同步自己的 schema 与 INSERT 字段
    PG manual 档下游补列 telemetry_schema_sql("postgres") 自取;不补则这 10 个维度按现有列裁剪后静默不落库(库发一条点名警告)
    注意失败行可能带 http_status_code = 200 MonkeyOCR 的 success != true 就是 200 下的失败,该列不可当成败判据

    费用口径不变:终态行 cost IS NULL、usage_source='unavailable'、token 为 0,不参与 SUM(cost)。400 密集负载下错误行会翻倍(既有 attempt 错误行 + 新增终态行),这是已批准的下游可见变化。

    其他

    • 新列全部可空、无默认值、追加在现有末列之后;旧行不回填(NULL 表达「补列之前根本没记过这件事」),滚动升级期新旧进程可写同一张表。
    • error_body 沿用 summarize_body 上限,结构化耗尽终态行的 error 带有界说明(不含模型原始正文);两者都不在 PGW_TELEMETRY_TEXT_CAP 覆盖面内。
    • 修正历史误标:embed() 非 200 的失败现在报 operation="embedding"。遥测新列 operation 由调用点给定,与 exc.operation 是两个语义。
    • 重试预算与退避、429 免预算与 stall 算法、取消结算、限流/熔断语义、缓存 key 公式一字未改。
    Downloads
  • v1.3.4 af57f93adc

    v1.3.4 Stable

    iomgaa released this 2026-09-09 19:22:25 +08:00 | 84 commits to main since this release

    Warning

    patch 版号不代表无迁移成本。 已登记但不含 AUTO 的 True/auto 配置及受管+raw 双来源现在明确拒绝;受影响缓存必须在首次新语义读写前显式换 namespace/salt。升级步骤见 README 迁移节。

    • 推理意图:已登记 AUTO 必须为能力清单成员,True 糖同约束;nearest 不代选强度。MiniMax on_base 改空,M3 要显式选 medium 等登记档;M2.5/M2.7 空 wire 流式 AUTO 各5轮定向复验通过,不代表全矩阵覆盖。未知仍尽力+warning,不保证开启。
    • 所有权:受管意图下两层 raw 推理控制同值/被遮蔽也拒绝;raw-only 与普通采样浅覆盖保留。自定义 on_base 禁止偷带强度。
    • 缓存迁移前置:受影响调用更换从未承载旧语义的 namespace/salt;同版本 fallback、能力表、wire 变化亦需迁移。不加自动指纹,未迁移仍可回放旧语义。M1–M9、per-call/多源/回滚示例见 README。
    • 测试证据:默认 FAIL,不整类 skip;404 仅完整独立证据可未覆盖,公共身份缺失无独立证据 FAIL。逐轮脱敏,UNKNOWN/SKIP/缺轮不算关闭覆盖;不可关闭与预期拒绝独立判定。缺型号级 400 机器字段基线仍 FAIL,不编造白名单。
    • 遥测守卫:真实客户端/临时 SQLite 的 embedding、OCR 双入口成败 NULL、chat 阳性与四种行来源回归;不新增 schema、生产端口或成功 SSE 捕获器。

    验收例外:2026-09-09 用户正式批准不再补全模型矩阵,失败/UNKNOWN/不可达/缺轮及缺下游现行配置证据作为本版例外保留,不冒称 PASS。embedding 实测 503 不符合严格404未覆盖条件,仍为 FAIL;claude-opus-5 开启档位命题仍 FAIL,不能把 HTTP 200 当推理开启证明。独立审查、红绿/变异、日常与定向实测均按适用范围复用;具体失败、网络诊断及原始证据见验证记录。该例外不取消下游缓存迁移前置,也不代表合并后检查、上传或外部包验收已执行。

    Downloads
  • v1.3.3 a716f12483

    iomgaa released this 2026-09-06 00:43:43 +08:00 | 104 commits to main since this release

    推理从「开 / 关」升级为档位(issue #20)。enable_thinking: bool | None 表达不了新一代模型:GLM-5.3 官方强制推理、只接受 low/high/max,none 不是它的档位——二态布尔在它上面无档可填,下游只能手写 extra_body,而那条路会静默绕过本库为推理准备的三道机制。本版把档位做成一等公民:八档封闭词汇、源级与请求级两个入口、能力表按档位登记、缓存 key 与遥测各加一维。

    版号是 patch(2026-09-05 人类指令,不因破坏性变更走 minor),但本版含五处破坏性变更与四条行为变更。 patch 版号从设计上就不承担预警职责,预警只能由这份 CHANGELOG 扛,故全部置于最前。

    请先读这一条(一):五处破坏性变更

    # 位置 变更 谁会当场断
    1 ThinkingCapability 构造签名 can_disable: bool → supported_efforts: tuple[Effort, ...] 自建能力表的调用方(关键字与位置两种构造都断)
    2 ports.Transport.complete() 新增无默认值参数 reasoning_effort 任何自建 transport 实现
    3 ports.TelemetryRecorder.record_llm_call() 新增无默认值参数 reasoning_effort(25 → 26 参) 任何自建 recorder 实现
    4 thinking.resolve_thinking() 第三参数由 bool 换成 Effort,返回类型由 Mapping 改为 ThinkingResolution 直调它的读侧代码一律断
    5 providers.ProviderProfile 两个字段 thinking_on / thinking_off → 单字段 thinking: ThinkingWire 自建 profile 的调用方

    第 1 条的 can_disable 保留为只读派生属性(Effort.NONE in supported_efforts),只读它的代码一行不用改;构造则两种写法都断:

    1.3.2 的写法 升级后
    ThinkingCapability(can_disable=True, evidence="…")(库自己那张表用的就是它) TypeError: ... got an unexpected keyword argument 'can_disable'
    ThinkingCapability(True, "…") TypeError: 'bool' object is not iterable——断在 __post_init__ 的去重校验里,错误信息看不出真实原因
    迁移写法 ThinkingCapability(supported_efforts=(Effort.NONE, Effort.AUTO), evidence="…")

    第 2、3 条按这两个端口的既有纪律不设默认值:库外没有第三方实现者,带默认值只会让漏传时静默落一个默认值。第 4 条的新返回值是 ThinkingResolution(payload, applied_effort)——原来那个 mapping 现在是 .payload,多出来的 .applied_effort 是开了 nearest 映射后真正发出去的那一档。

    请先读这一条(二):不改一行代码也会变的四条行为

    # 变更 影响
    1 glm-5.3 / glm-5.3-flash / gemini-3.1-pro 首次进入能力表,且三者都登记为关不掉推理 本版唯一会打断存量配置的一条。 1.3.2 里这三个型号未登记,给它们配 ENABLE_THINKING=false 会按 provider 形态尽力注入并放行(只发一条 warning);本版在装配期抛 ThinkingUnsupportedError。并排实测:deepseek/glm-5.3 + ENABLE_THINKING=false 在 1.3.2 返回 {"thinking": {"type": "disabled"}},在本版当场报错
    2 openai 段的开启方向由「形态未知即装配期报错」放宽为 on_base={} 把任意兼容厂商挂在 openai 段下并配 ENABLE_THINKING=true 的下游:1.3.2 在装配期报错,本版放行且一个字节都不注入——走模型自己的默认档。若该模型默认不推理,这个配置既不报错也不开推理(见下方「已知限制」)
    3 openai 段的关闭方向由「形态未知即装配期报错」放宽为 {"reasoning_effort": "none"} 同上但配 ENABLE_THINKING=false 的下游:1.3.2 在装配期报错,本版下发这个片段。放宽的依据是 reasoning_effort 是 OpenAI 官方字段而非厂商方言,经网关的兼容端点不会把它打到不认识它的厂商
    4 缓存 key 加入 reasoning_effort 只有新配 REASONING_EFFORT 的源冷启动一次;只配 ENABLE_THINKING 或什么都没配的源,key 字面量逐字不变(已按 1.3.2 的实现逐字比对)

    第 1 条是设计上有意为之:调用方要的是「不推理」的语义保证,给不了就必须说,而不是让它继续静默烧推理 token——升级后当场失败,正是这三个型号本来就关不掉推理的证据。报错文案带一条能立刻照做的替代(该模型最省的那一档 + 该配的 env 键名),不把人推回 extra_body 那条绕过库的路。

    qwen / deepseek / minimax 三段的注入形态逐字未变。 全量比对(4 个 1.3.2 已有的 provider 段 × 25 个模型 × ENABLE_THINKING 三态 = 300 种组合)显示,本版与 1.3.2 的差异只有上表第 1、2、3 条。minimax 的「开」尤其值得点名:它维持 {"reasoning_effort": "medium"} 逐字不变,因为真实网关实测显示 MiniMax-M3 在不带任何推理参数时不推理(5/5 轮),把它改成「不注入即为开」会让存量 ENABLE_THINKING=true 的调用静默停止推理。

    新增能力

    新增 说明
    八档 Effort:none / auto / minimal / low / medium / high / xhigh / max 封闭词汇,取四家参考实现共同收敛的那一套。none = 要求不推理(与「不表态」是两回事),auto = 要求推理但不指定强度
    {SCOPE}__{PROVIDER}__{N}__REASONING_EFFORT 源级默认档。ENABLE_THINKING 保留,降为它的语法糖(true ≡ auto、false ≡ none、缺省 ≡ 不表态);两键语义矛盾(如 true + none)在装配期报错,不做「后者赢」的静默兜底
    {SCOPE}__{PROVIDER}__{N}__EFFORT_FALLBACK error(缺省,报错)或 nearest(映射到最近档并 warning)。默认报错的理由是钱:一次静默的 medium → max 在部分模型上是数倍账单
    chat(reasoning_effort=...) 请求级覆盖,优先级高于源级;裸字符串会在入口归一
    LLMResponse.applied_effort 本次实际跑在哪一档(开了 nearest 时与请求档分叉)。字段追加在末尾,既有字段只增不改名
    四个新 provider 段 zhipu / moonshot / anthropic / google 连同 1.3.2 已有的 qwen / deepseek / minimax / openai 共八段。四段都是新增,不改变任何存量配置的行为
    能力表由 5 条扩到 24 条 1.3.2 只登记 5 个型号,其余一律走「按 provider 形态尽力注入 + warning」。本版新登记 19 个:qwen 4 款、deepseek 2 款、GLM 6 款、kimi 2 款、gpt 2 款、claude 2 款、gemini 1 款
    kimi-k3 首次登记为可关闭 它在 1.3.2 未登记(配 false 走尽力注入 + warning,不报错)。本版实测坐实可关:请求 none 后短提示词 5/5 轮 + 长上下文 3/3 轮无任何推理信号、completion 恒 9 token,与同模型 max 档(rt 33-146)的锚点可分。两源分歧由此了结——OpenRouter 的 mandatory:false 是对的,官方档位表没列 none 只是没列
    包根新增导出 Effort / EFFORT_ORDER / ThinkingWire / ThinkingResolution 深路径 import 会被内部重组打断,一律从 polygateway 包根取

    档位不支持时报错必带可执行替代:模型关不掉推理时,错误文案直接给出该模型最省的那一档和该配的 env 键名。只报错不给出路,下游只会退回 extra_body——而那正是 issue #20 的成因。

    遥测:第 26 个 INSERT 字段 reasoning_effort

    llm_calls 新增一列 reasoning_effort TEXT(INSERT 字段 25 → 26,物理列 26 → 27)。列可空,NULL 表示调用方没表态;它与 'none'(明确要求不推理)是两回事,折叠成任一档都等于替上游声称了一件它没说过的事。加这一列是为了让「不同档位是不是真有用」这类压测在数据侧能分组——此前 25 列里没有任何一列能回答「这一行跑在哪档」。

    成功行与失败行不是同一把尺子。 开了 EFFORT_FALLBACK=nearest 的源上,成功行记的是映射后的实发档(读 response.applied_effort);失败尝试没有响应、实发档无从得知,记的是请求档。故 GROUP BY reasoning_effort 不带 error IS NULL 时,两种尺子会混进同一个分组。缓存命中行与终态失败行同样只记请求档——它们手上没有选中源,源级档位与 nearest 映射都无从谈起。embedding / OCR 两条路径没有推理语义,该列恒 NULL。

    补列走既有的 PGW_TELEMETRY_SCHEMA_MODE,两端 DDL 与 COLUMNS 同源。manual 档的下游会看到一处文案变化:旧表的缺列告警会多点名 reasoning_effort 这个维度,并附上对应的 ALTER TABLE ADD COLUMN 语句。

    能力表口径:24 条里 17 条经 new-api 实测、7 条仍是文档推定

    DEFAULT_CAPABILITIES 共 24 条,每条 evidence 自报家门(实测日期、轮数 N、判据、锚点,或「文档推定」及其四方出处)。读能力表请以逐条 evidence 为准,本版不存在「能力表已全部实测」这回事。 未能实测的 7 条与原因:

    模型 未覆盖的原因
    claude-opus-5、claude-sonnet-5 该渠道 claude 全系返回 429「api key 7 天限额已用完」,5/5 轮失败;none 档还额外依赖网关把 reasoning_effort=none 转成 thinking 关闭形态,同样未经验证
    gemini-3.1-pro 该渠道本型号上游报错(bad_response_status_code / openai_error),5/5 轮失败,连默认档基线都没取到。默认档「官方文档说 high、OpenRouter 说 medium」两源打架仍未决,本版不选边
    gpt-5.4 全账号限流(429 All available accounts are currently rate-limited),5/5 轮失败。同代的 gpt-5.5 已实测且与清单逐字相符,可作旁证但不是本型号的证据
    glm-5、glm-5.1、glm-5.2 请求这三个型号时,渠道 5/5 轮把流量路由到 glm-5.3(issue #20 记录的 6/6 复现);拿到的行为不属于本型号,整组数据作废

    glm-5.2 的下游风险要单独说:在这条渠道上给它配 none,库会照文档推定放行,而真正服务请求的 glm-5.3 关不掉推理;运行期对账会喊,但那是事后。

    另有两条与实测相关的收获值得下游知道:同一批实测发现 zhipu / moonshot 这条渠道不校验档位值(未登记的 medium 也照单收下并返回 200),故「网关没报错」在这两家上不构成「该档受支持」的证据;而 openai 那条会校验(清单外的 max / minimal 被上游 400 拒)。

    已知限制:auto 不等于「强制开推理」(issue #21)

    reasoning_effort=auto(含它的语法糖 ENABLE_THINKING=true)在 on_base={} 的三个 provider 段(openai / anthropic / google)上表达的是「用模型自己的默认档」,库不注入任何字节。若某模型默认就不推理,这个配置既不报错也不开推理。正解是让 auto 受能力表约束——模型不支持「由模型自定」时报错并指路显式档位,属公共行为变更,留到下一版(gitea issue #21)。

    与之相连有一处刻意的不一致,请勿误读:DEFAULT_CAPABILITIES 里 MiniMax-M3 的 supported_efforts 不含 auto(实测结论——它的默认档不推理),而 minimax 段的 wire 会为 auto 注入 {"reasoning_effort":"medium"} 并被放行。resolve_thinking 的 Phase 5 对 auto 无条件放行(auto 不是写进 effort_key 的取值,而是「不写 effort_key」),能力表拦不住这条路;当前是由 wire 侧的权宜之计兜住的。别把它读成「能力表能挡住 auto」。

    Downloads
  • v1.3.2 6ec9ec7056

    1.3.2 Stable

    iomgaa released this 2026-08-28 18:17:15 +08:00 | 141 commits to main since this release

    1.3.2(2026-08-28)

    本版不改库代码。 tools/ 与 tests/ 都不在 pip 包内(脚本随仓库分发,见 README),故 1.3.2 的 wheel 与 1.3.1 除版本号外没有任何差异(__version__ 与包元数据是唯一的改动)。升级它不会改变任何库行为——本版的内容是运维脚本 tools/telemetry_retention.py 的一处契约扩展,以及测试隔离的重建。若你只用库本体,可以跳过本版。

    运维脚本:--table 让删除目标不再由连接环境决定(issue #18)

    tools/telemetry_retention.py 此前删哪张表,取决于连接的 search_path——它的首项是 "$user",所以换个角色跑同一条命令,目标可能就换了一张表。脚本会把解析到的限定名打出来,但那行打印与 DELETE 在同一次运行里,中间没有人。

    新增可选参数 --table <schema>.llm_calls:给了它,目标由参数精确解析(to_regclass 走引号限定名),绕开 search_path。

    情形 行为
    不给 --table 与 1.3.1 完全一致,现有 cron 不受影响;但 --apply 时会多打印一行,提示目标是推断来的
    表名段不是 llm_calls 退出 1。本脚本只清理遥测表,不是通用清理器——一次 --table audit.events 的手误,会对一张恰好也有 created_at / tenant_id 的业务表跑同一套分批 DELETE
    显式指定的表不存在/不可见 退出 2,消息附一句"PG 中未加引号建的标识符在 catalog 里是小写"(大小写手误是这里的高频原因)
    显式指定的是分区表 仍退出 3 让路给 DROP PARTITION,语义未变

    退出码契约未新增也未改动。建议 cron 一律带上 --table:那一行配置从此自己说明删的是哪张表。

    测试隔离:从"事后观测共享表"改成"权限上做不到"

    issue #18 报的是一条 PG 集成测试偶发红。查下来失败的断言并不在测被测脚本——它比对的是一张三个迁移项目也在写的表的前后行数,而报错时(61 行变 12 行)脚本本身被证明只动了自己的临时 schema。

    行数快照承载不了它想守的属性:别人一写就假红,而外部插入恰好抵消掉一次误删时又会假绿——后一半守的正是"审计表被删空"。现在这条属性交给数据库强制:跑脚本的测试角色拥有自己的临时表、对共享表没有任何授权,search_path 万一落空就是 permission denied 而不是"但愿有断言发现"。共享表 llm_calls 至此不再被本仓库任何测试读写,killed 的测试也不会再往里留孤儿行。

    对下游没有影响(测试不进包),列在这里是因为它解释了本版为何存在。

    Downloads