-
1.3.1 — 推理是否发生成为一等返回值 Stable
released this
2026-08-26 16:39:14 +08:00 | 47 commits to main since this release「这次调用到底推理没推理」从此是库的一等返回值(issue #16 + #17):
LLMResponse.thinking_observation三态如实作答,判不出来时说unknown而不是伪装成「没推理」,并与推理能力表持续对账。版号是 patch,但本版含三处会影响下游的变更——深路径 import 断裂、端口签名扩参、一条新告警。patch 版号从设计上就不承担预警职责,预警只能由这份 CHANGELOG 扛,故三条置于最前。
请先读这一条(一):
polygateway.providers的深路径 import 断了推理相关的六个符号从
providers.py移进新模块polygateway.thinking。from polygateway.providers import ...引用其中任何一个,升级后当场ImportError:从 providers断掉的符号改成(推荐) 或 ThinkingCapability、ThinkingUnsupportedErrorfrom polygateway import ...from polygateway.thinking import ...get_capability、register_capability、resolve_thinkingfrom polygateway import ...from polygateway.thinking import ...DEFAULT_CAPABILITIESfrom polygateway.thinking import DEFAULT_CAPABILITIES— 前五个请改用包根 import: 它们此前只能深路径引用,而深路径引用正是模块重组会打断下游的原因——本版一并把它们提升到包根导出(连同本版新增的
ThinkingObservation,共六个新导出),给的就是一个此后不会因内部重组而变的引用点。DEFAULT_CAPABILITIES有意不进包根: 它是可变注册表的当前快照,不是稳定 API 面。providers.py保留的ProviderProfile/DEFAULT_PROFILES/get_provider/register_provider逐字未动。拆分本身不是顺手重构: 推理这件事从「请求侧注入什么参数」长成了「注入 + 响应侧裁定 + 两者对账」三件事,再留在 provider 注册表里,那个文件的职责就得用「和」来描述。
请先读这一条(二):
TelemetryRecorder.record_llm_call从 24 参变 25 参新增 keyword-only 参数
thinking_observation: str,且按该 Protocol 的既有纪律不设默认值(库外没有第三方实现者,带默认值只会让 emitter 漏传时静默落一个默认值)。自定义 recorder 实现必须同步补这个参数,否则调用时TypeError。库自带的SQLiteRecorder/PostgresRecorder已同步,不受影响。TelemetryRecorder之外的端口逐字未变;TelemetryStatusProvider不受影响。请先读这一条(三): MiniMax-M3 非流式开推理 = 付费买看不见的推理,库现在会说出来
2026-08-25 实测: M3 非流式开启推理时
completion_tokens从 3 涨到 53(推理段确实产生并计费),而响应里既没有reasoning_content正文、也没有usage.completion_tokens_details——钱花了,东西一个字都拿不到。这是上游行为,库修不了,但从本版起不再默不作声: 该档观测判为unknown,并按(模型, 方向)发一次 warning,说明「已注入开启参数,但本路径观测不到,推理内容可能已计费却不回传」。要拿到推理正文,该模型请走流式路径(实测 185 字符正文完整)。
诊断纠正: 不是模型不推理,是 MiniMax 停报
completion_tokens_detailsissue 判定「M3 开启推理静默失效,模型不推理」。实测推翻了这个诊断——绕开库用裸
httpx抓真实响应,M3 流式开启档拿到 124 字符完整推理过程,prompt_tokens194→216、completion_tokens3→60,三个独立信号一致。真正变的是 MiniMax 这一路上游不再返回
usage.completion_tokens_details(qwen 与 deepseek 在同一网关、同一 key 上照常返回),reasoning_tokens因此恒为None。而库把「推理是否发生」全押在这一个字段上,于是手里握着 185 字符推理正文,却对外报告「没推理」。缺口的形态是本版真正要修的东西: 库拿到的信息足以回答问题,却把答案丢掉,转而返回一个语义歧义的
None。三态,以及它为什么不能折叠成布尔
LLMResponse.thinking_observation(类型ThinkingObservation,StrEnum,缺省unknown)由多信号裁定,判据按证据硬度排序:值 判据 observed推理正文 thinking非空(事实本身),或reasoning_tokens > 0(上游对事实的转述)absentreasoning_tokens == 0——上游明确上报本次未推理,是正面证据unknown两个信号双缺,判不出来 unknown与absent不是一回事,把前者折叠进后者正是本次故障的病根。unknown没有证伪力: 它不能用来声称推理关掉了,也不能用来报警「没推理」。缺省取unknown使任何填不了这个字段的路径(非 OpenAI 兼容 transport、失败尝试、终态失败行)天然诚实——默认值本身不撒谎。对下游的口径变化: 统计「未推理」不要再写
reasoning_tokens IS NULL OR = 0,那个条件在供应商停报 usage 明细后会把推理了的调用一并算进去。改按thinking_observation分组,unknown独立成一档。声明 × 观测对账: 能力表过期从静默错觉变成日志里的告警
推理能力表(
can_disable)是静态声明,而静态声明必然过期——M3 的 evidence 曾停在 8-02 整整 23 天。过期的表现是静默错觉: 库照常注入关闭参数,模型照常推理,下游拿到推理内容却以为关了,全程无人吭声。本版在 transport 拿到结果处做一次比较,矛盾即 warning(不抛错——一次观测不足以否决一次成功的调用,矛盾结果已随响应与遥测落地,处置权归下游):
请求方向 观测 告警内容 关闭 observed关闭请求未被满足。能力表已登记则点出 evidence日期并指路复测更新;未登记则说明本次是按 provider 形态尽力注入开启 absent已注入开启参数,上游却明确上报未推理 开启 unknown已注入开启参数,但本路径观测不到;若为非流式,推理内容可能已计费却不回传 关闭 × unknown与「调用方没提要求」两类有意不表态: 前者没有证伪力,拿它报警等于每次关闭调用都喊一遍(M3 关闭档恒落此档),噪声即等于没有告警。同一(源, 模型, 方向)只喊一次,文案点名出问题的源——多源多账号下同一模型跨 N 个源是常态,键漏掉源名会让第一个出问题的源喊完之后其余源永久静音,而告警也定位不到该查哪个网关。保障的覆盖面必须说清楚: 对账只在可观测路径上成立(推理若真的发生,流式路径会带出正文,翻成
observed触发告警);M3 非流式那种两个信号双缺的路径,没有任何保障——本版让它可见,但不能让它可判。遥测新增一列
thinking_observationllm_calls加一列thinking_observation TEXT(可空,取值observed/absent/unknown),排在最末,SQLite 与 Postgres 两端 DDL 与补列语句同步。旧表按既有 backfill 路径补列: sqlite→auto 档自动补,postgres→manual 档点名缺列并给出可执行 SQL、同时按现有列裁剪INSERT继续写(不补列不会让遥测整体失效,只是少这一列)。补列失败仍只逐行降级、绝不判死。照 README「生产部署 DDL 模板」部署的下游不需要改模板: 那份模板用
LIKE llm_calls_seed从库自己建出的表派生列,与telemetry/schema.py同源,不存在手抄漂移(本版加了一条测试断言把这个同源性钉死)。其他
- 缓存回放的
thinking_observation是ThinkingObservation枚举实例而非裸字符串: JSON 复活出来的是str,与字段注解分叉,CacheMW._rehydrate现在显式转换。取值不在本版三态值域内时(多个项目共用同一 Redis、先升级的那个写入了新态)降级为unknown并单独告警,响应内容照常复活——一个纯可观测性字段不该有能力作废内容完好的缓存,否则未升级的项目会在这些 key 上每次真打网关、随后覆写回旧值,两个版本互相打对方的缓存;「整条作废」只留给真正破坏内容完整性的失败。 - M3 的推理能力
evidence刷新到 2026-08-25 复测。can_disable仍为True(reasoning_effort=none→ prompt 194 = 基线、completion 3、无正文,声明依然成立),同时补记两条限制: 推理信号在非流式路径不可观测;enable_thinking与thinking={"type":"enabled"}对该模型无效,只有reasoning_effort是真开关。 TransportResult同步新增该字段并由RetryMW透传;裁定在openai_compat的流式与非流式两条组装路径各做一次。- 遥测的新列只经
TelemetryEmitter._record这一个出口下沉给 recorder(单一 helper 铁律),且在那里由枚举归一化为裸str——StrEnum虽是str子类,asyncpg 的参数编码对str子类不保证接受,而遥测写失败只是一条 warning,这类问题不会当场炸,只会让 Postgres 那一路悄悄少一列数据。归一化按外部输入防御:LLMResponse无运行时校验,下游填裸str完全自然,而直接取.value会抛异常并被降级路径吞成丢掉整行遥测;域外取值同样只降级记unknown并单独告警,不拿整行当代价。
Downloads
- 缓存回放的