-
1.3.11 Stable
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、tableHTML、image空串)、page_sizes与整篇markdown。治理链路(限流/熔断/重试/换源/遥测/调用方维度/期限)复用parse_layout的同一循环。- 公共类型
OcrDocumentElement/OcrDocumentResult(包根导出);端口OcrDocumentPort;transport 协议OcrDocumentTransport(独立于OcrTransport,不动旧runtime_checkableProtocol 的成员,既有实现与测试替身不受影响)。 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_equationspan、嵌套 caption/footnote 子块三条路径无真实样本,仅由构造 fixture 测试。 - 后续: #35(共享分类对确定性坏输入 500 仍归瞬时)、#36(
recognize_text不校验服务响应的success)、#37(parse_layout结果包解析仍漏出原生异常、download_url不限制主机)。
Downloads
-
1.3.10 Stable
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同值幂等),Geminiextra_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)→
ResultInvalidErrortool_truncated;无参函数全程只发arguments: null(其余字段齐全)→ResultInvalidErrortool_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新增必填 keywordstream_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 Stable
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
- 新增
-
1.3.8 净生成时间口径与中段沉默对冲 Stable
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 边界,本口径不假装是 ITLstall_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_idjoin 还原);被取消的输家无 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_idjoin3 独立于 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 — 长尾对冲与裸生成时间 Stable
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可 join3 对冲只对 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-callchat(call_deadline_s=X)使 X < 对冲阈值时该次调用对冲不触发,属合法语义不告警。配置面
键 值域 缺省 {SCOPE}__HEDGE__AFTER_S有限正数秒(复用期限同款值域校验) 未设 = 关闭 {SCOPE}__HEDGE__MAX_EXTRAint ∈ [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 — 可选调用期限与取消结算修复 Stable
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/非数)在装配期当场ValueError3 三个 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 Stable
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 = 200MonkeyOCR 的 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 Stable
released this
2026-09-09 19:22:25 +08:00 | 84 commits to main since this releaseWarning
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
-
1.3.3 — 推理从开关升级为档位 Stable
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_FALLBACKerror(缺省,报错)或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_effortllm_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
-
1.3.2 Stable
released this
2026-08-28 18:17:15 +08:00 | 141 commits to main since this release1.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