Commit Graph

424 Commits

Author SHA1 Message Date
iomgaa 5e2c35a812 test: pin the expiry window and close settlement review gaps 2026-09-10 05:20:56 -04:00
iomgaa b1bc06e2b1 docs: document the optional call deadline and cancellation settlement
The deadline governs waiting, not the moment a call returns: cleanup
still runs in finally, so the return time is the deadline plus the
cleanup cost (5-7x the deadline in the measured fixture). An expiry
therefore does not mean nothing was produced or nothing was billed.
Leaving the key unset keeps 1.3.5 semantics verbatim, which also keeps
its two long waits: a pure 429 sequence can still wait for a long time,
and a large finite Retry-After is still slept in full because the
library deliberately does not clamp the hint with backoff_max_s.

- CHANGELOG unreleased section states those three sentences, records the
  cancellation settlement change (an attempt cancelled after the port
  started but before the settlement is known keeps its reservation at
  the source estimate: over-charge rather than refund something the
  upstream may already have billed; known settlements and unclassified
  escapes are untouched) and warns that except GatewayUnavailableError
  does not catch CallDeadlineExceeded
- README in four places: the capability table, the exception handling
  example, the "which exceptions reach the caller" table and the error
  model section, which now spells out the remaining large-but-finite
  Retry-After wait
- .env.example documents LLM__CALL_DEADLINE_S as commented out
- new findings file indexes the red/green evidence, the commands and
  their exit codes, what was not run and who covers it, and repeats the
  three residual risks

No version bump, no tag, no release: those belong to the release
checklist.
2026-09-10 04:33:55 -04:00
iomgaa da77b123ec fix: ignore non-finite Retry-After hints
A gateway that answers 429 with Retry-After: inf (or 1e999, which float()
happily rounds to inf) used to reach backoff_delay as retry_after_s=inf.
max(delay, retry_after) then picked it, and since the library deliberately
does not clamp the hint with backoff_max_s, the attempt slept forever.

- _parse_retry_after now rejects non-finite values via math.isinf and
  returns None, so the call falls back to plain exponential backoff
- it emits exactly one warning carrying the source name and the verdict
  word retry_after_not_finite, never the raw header: under a 429 storm an
  echoed header drowns the real signal and does not help localisation
- source_name becomes a required keyword-only argument; the function is
  private, so no default is given and a missed call site fails loudly
  instead of silently dropping the source identity from the warning
- nan keeps flowing through the existing seconds > 0 semantics; no new
  branch, no reordering of the parse
2026-09-10 04:25:09 -04:00
iomgaa 9474c76ab0 feat: add an optional per-call wall-clock deadline
Give one logical call an optional hard wall-clock boundary (issue #22).
Leaving it unset keeps 1.3.5 behaviour verbatim: the timeout context is
never entered when deadline_s is None.

- new deadline.py: ensure_call_deadline() range check (None or a finite
  positive number; bool/0/nan/inf and out-of-range ints are rejected as
  ValueError so OverflowError never leaks) plus with_call_deadline(),
  which distinguishes an expiry from a TimeoutError raised by the body
  or its cleanup via a local-variable identity comparison rather than
  cm.expired() alone
- new CallDeadlineExceeded: deliberately outside the four categories and
  not a GatewayUnavailableError, and carries no retry_after_s
- new {SCOPE}__CALL_DEADLINE_S key, guarded on the env, direct
  construction and dataclasses.replace paths
- three clients take a call_deadline_s constructor argument and a
  keyword-only per-call override on chat/embed/recognize_text/
  parse_layout; None inherits the assembled value
- validation runs before the awaitable is created, so an illegal value
  cannot strand an un-awaited coroutine
- one embed call shares a single deadline across all of its batches
- import-linter gains a polygateway.deadline layer

- cover where the deadline lands: backoff sleep, admission polling,
  the structured re-ask ladder and embedding's batch loop, plus the
  empty-texts early return that stays outside it
- cover what an expiry costs: exactly one terminal_failure row carrying
  error_type=CallDeadlineExceeded, a cancelled attempt row sharing its
  logical_call_id, cleanup that outlives the deadline (lower bound only)
  and an already-billed success being discarded
- pin the injected clock as orthogonal: a 10^6 second jump never expires
  a call, yet total_latency_ms still reads that clock
2026-09-10 04:14:44 -04:00
iomgaa 1ff83bbfe0 fix: settle cancelled attempts against the source estimate
取消发生在"端口已开始、结算尚未确定"时,原先按 settle(0) 把入场预扣整笔
退还,等于把可能已被上游计费的用量退回闸里;启用调用期限后库自身会常规性
触发该路径,故先修记账再启用。

改动只落在取消路径的取值上(设计 §6.3 矩阵 S3/S5/S7):
- actual 初值保持 0,另设函数内局部阶段变量 settlement_known(不进任何签名);
- 成功路径算出用量后置位,真实 usage 恰为 0 同样算"已知",不被取消覆写;
- 已处理领域失败分支把结算决定前移到其第一个 await 之前(同级
  except CancelledError 接不住本块 await 上的取消,它直穿 finally),
  故 SourceDead 的既有 0 在取消下被保住,瞬时失败仍是 est,值与 1.3.5 逐字相同;
- 未被四分类接住的异常不经上述分支,仍按 0 退全款(S8,本版不扩大语义);
- OCR 的 0 token 是事实而非未知,settle(0) 不变,只补注释。

取消窗口一律用真实 asyncio.Event 钉死(不再 sleep 撞窗口),并加防越界回归:
RuntimeError 逃逸仍结 0、真实 usage 为 0 的成功仍结 0。
2026-09-10 00:48:07 -04:00
iomgaa 6c640fcca3 docs: record approved call deadline design and plan 2026-09-10 00:33:13 -04:00
iomgaa d2455e8fd4 docs: record 1.3.5 release completion and external verification
Merge-time gates, registry artifacts and anonymous page checks for 1.3.5
recorded as a separate finding; the validation finding gains a status
pointer so its historic 'not yet executed' section is not silently stale.
Released main/tag and uploaded artifacts are untouched.
2026-09-09 14:02:15 -04:00
iomgaa ab00aa4457 chore: merge release 1.3.5 call observability v1.3.5 2026-09-09 13:26:21 -04:00
iomgaa 433039be79 chore: prepare release 1.3.5
Version bump in pyproject and __init__, CHANGELOG dated 2026-09-09,
README install lower bound raised to >=1.3.5, and release-prep evidence
(remote check, gates, real gateway smoke and one bounded live probe)
recorded in the 1.3.5 validation finding.
2026-09-09 13:21:40 -04:00
iomgaa b4812e12c8 fix: degrade terminal telemetry failures instead of masking domain errors
终态出口 `emit_terminal_once` 此前只让 `_record` 内的 except 兜住落库,而
诊断字段的提取(`_error_fields` → `_structured_detail` → `format_bounded_errors`)
在降级 try **之外**求值。下游经公共端口(自实现 `StructuredOutputStrategy`
或 transport)构造出 `ResultInvalidError(validation_errors=(非 str,))` 时,提取期
抛的 `TypeError` 会顶替调用方本该收到的领域异常——错误四分类被击穿(下游
`except ResultInvalidError` 落空),且 `claim_terminal()` 已消耗故终态行照样丢,
同时违反"遥测写失败降级不冒泡"。

改法与 RetryMW 的 attempt 出口(`retry.py::_emit`)同款: 把快照冻结与 await
整段包进 try,`CancelledError` 原样上抛、其余落一条 warning。终态行按已批准的
best effort(兜底命中时该次逻辑调用 0 条终态行,不补写)。异常类型校验与
`ResultInvalidError` 的既有设计均未改动。

顺带同步审查报告的 Minor 项: SQLite recorder docstring 26 → 36 字段、
research-wiki 索引重建、ARCHITECTURE 的 `sampling` 段落终态调用点口径,
并删除 `TelemetryMW` 迁移后无读取点的 `self._now` 死字段(保留形参,
避免平白打断既有装配写法)。
2026-09-09 12:50:51 -04:00
iomgaa 067b15be48 docs: document logical call telemetry and migration impact
Field counts come from inspect, not memory: record_llm_call takes 36
parameters, COLUMNS has 36 entries, the physical table has 37.

- README: capability table says 36 fields and names the three row kinds;
  new section covers reading call_stats, the five SQL migration items,
  the attribution query and the storage-side upgrade
- README/.env.example/ARCHITECTURE: error_body follows the summarize_body
  limit and the structured-exhaustion error carries its own bounded
  explanation, so neither is inside PGW_TELEMETRY_TEXT_CAP coverage
- ARCHITECTURE 7.8: the ten columns with per-column semantics, the I3/I4
  invariants, operation versus exc.operation, and the assembly gate
- CHANGELOG: unreleased section listing the four public changes and what
  downstream must do, in particular counting failures by event_kind and
  the assembly-time error for custom recorders
- schemas/llm-calls: the ten columns plus a three-row-kind section
- metrics/call-telemetry-coverage: 1.3.5 coverage contract, real live
  baselines left unfilled rather than stating a fake percentage

Validation record records the T4 evidence: mechanical migration red then
green, the four PG acceptance cases, the seven-item mutation matrix with
all seven killed and the copy restored to an identical digest, plus the
PYTHONPATH pitfall that made the first mutation round silently test the
original source.

Version numbers and release steps are deliberately untouched.
2026-09-09 12:17:50 -04:00
iomgaa 7b2f6105f3 test: cover the ten call observability columns on real Postgres
Migrate the PG telemetry fixtures to the 36-field recorder and add the
storage compatibility acceptance the plan calls for.

Mechanical migration:
- _EXPECTED_COLUMNS 27 -> 37 physical columns
- _record_minimal gains the ten keys in the same shape as the unit suite
- _PRE_TENANT_COLUMNS now excludes 14 columns, derived from
  _CALL_OBSERVABILITY_COLUMNS instead of a second hand-written list, and
  the two manual-mode warnings assert a notice derived from COLUMNS order
  so a column that silently drops out of the warning turns the test red

New TestCallObservabilityColumnsAcceptance, all on a 27-column 1.3.4
shaped table built by the existing pg_sandbox factory:
- auto appends the ten columns in the same order as a fresh database and
  old rows keep NULL in every one of them (no backfill, no sentinel)
- manual sends no DDL, trims the INSERT, and still round-trips the other
  26 columns value by value
- an old-version writer using insert_sql with the 1.3.4 column set and a
  new-version writer share one table, and event_kind filtering counts
  neither the old rows as failures nor as successes

_minimal_fields is split out of _record_minimal so the simulated old
process reuses the same values rather than copying them.

Verified against the real lab Postgres: 30 passed.
2026-09-09 12:17:34 -04:00
iomgaa 393f2bf617 feat: record call observability columns and terminal failure rows
Grow the telemetry contract from 26 to 36 fields and give every logical
call a failure terminal row, so SQL can finally answer "how many calls
failed" and "why did the whole pool die".

Schema and port move together with the emitter writes in one commit:
splitting them would ship columns that nothing populates.

- schema: append 10 nullable columns (scope, operation, logical_call_id,
  event_kind, http_status_code, error_type, cause_type, error_body,
  attempts, total_latency_ms) to all five definition sites in one order
- ports: 10 keyword-only parameters without defaults; the protocol
  signature is now the single source the assembly gate derives from
- emitter: take domain exception objects instead of pre-flattened text
  and pin down the diagnostics in one helper; a relabelled 503 stays
  503 and success rows leave all five columns NULL
- emitter: reject recorders whose record_llm_call cannot accept the
  current field shape at assembly time, since _record would otherwise
  swallow the TypeError and drop every row while calls keep succeeding
- clients: write at most one terminal row per logical call through a
  single shared exit, deduplicated by the call context; TelemetryMW
  stops writing terminals so the two sites cannot double count
- clients: cancellation stays best effort and propagates, non-domain
  exceptions get no terminal row and keep their classification
- transports: give _status_to_error an explicit operation and fix the
  historically mislabelled embedding HTTP failures
- structured: promote the bounded error formatter so the reask feedback
  and the terminal explanation share one set of limits

Terminal rows carry no cost and no tokens, so cost aggregation is
unchanged; failure counts must now filter on event_kind.
2026-09-09 11:27:52 -04:00
iomgaa 87c261bf73 feat: track logical call statistics across governed calls 2026-09-09 10:04:39 -04:00
iomgaa 300ced5dbd docs: record approved call observability design and plan 2026-09-09 09:42:24 -04:00
iomgaa a81cc91124 docs: propose logical call statistics and failure diagnostics 2026-09-09 09:08:47 -04:00
iomgaa e71a623b04 docs: remove trailing blank line from release evidence 2026-09-09 07:28:02 -04:00
iomgaa 0c1165965f docs: record verified 1.3.4 publication and release checks 2026-09-09 07:27:19 -04:00
iomgaa af57f93adc chore: merge release 1.3.4 thinking contracts v1.3.4 2026-09-09 06:56:21 -04:00
iomgaa dae12f9a16 chore: prepare release 1.3.4 2026-09-09 06:52:27 -04:00
iomgaa b7e6943497 test: keep cancelled embedding probe rounds incomplete 2026-09-09 05:14:37 -04:00
iomgaa 7f6a824e79 test: complete embedding probe report identity and round counts 2026-09-09 05:06:39 -04:00
iomgaa 3eb22d2a55 test: fix structured reask evidence and live coverage conclusions 2026-09-09 03:34:48 -04:00
iomgaa d332287b28 docs: document reasoning ownership and explicit cache migration 2026-09-09 02:41:25 -04:00
iomgaa 73008ad7d5 test: apply evidence-based live checks without hiding regressions 2026-09-09 02:40:13 -04:00
iomgaa 16fa0ca474 docs: record deterministic reasoning contract validation 2026-09-09 01:39:13 -04:00
iomgaa c710c3a7ec fix: explain explicit auto migration and verify probe cleanup 2026-09-09 01:37:07 -04:00
iomgaa a0a33c0c01 test: guard custom reasoning roots at the transport boundary 2026-09-09 01:35:14 -04:00
iomgaa 47488ee4fd test: guard reasoning-free telemetry through real client paths 2026-09-09 01:34:36 -04:00
iomgaa d0078c1be5 test: pin explicit cache migration and reasoning row semantics 2026-09-09 01:34:33 -04:00
iomgaa 71f1bdf26b test: isolate factory checks from developer proxy settings 2026-09-09 01:27:25 -04:00
iomgaa 8e61a66342 fix: reject conflicting raw reasoning overrides before sending 2026-09-09 01:26:34 -04:00
iomgaa 1ee74c35a8 fix: validate ownership of managed reasoning parameters 2026-09-09 01:24:54 -04:00
iomgaa 4ed144c9e4 fix: enforce registered auto reasoning capabilities 2026-09-09 01:22:18 -04:00
iomgaa dda55567ae docs: register thinking contracts and record baseline checks 2026-09-09 00:48:57 -04:00
iomgaa 2553fc7f34 docs: record approved thinking contracts and implementation plan 2026-09-09 00:48:31 -04:00
iomgaa 6a090541be test: skip L8 when the channel drops the model instead of failing
The 2:25 slow run left exactly one red: kimi-for-coding answers 404
model_not_found because the channel removed it from the account group
between 09:44 (four green probes, correct model_reported) and 15:00. L8
was reading that as "the capability table drifted", which is a statement
about the model the channel no longer serves.

The 404/model_not_found rule already used by T10 now lives in one helper
and is applied on the L1-L9 side too, via the same unreachable fallback:
that one rejection skips and records an uncovered row, every other
RequestRejectedError still bubbles, since those are the real failures
this suite exists to catch.
2026-09-05 18:51:22 -04:00
iomgaa 758a127f06 test: stop reading channel outages as library defects in live e2e
L9's "unknown shape" sample was the openai profile, which 1.3.3 gave a real
shape (off/on_base/effort_key all set), so the guard had nothing to reject.
It now registers a shapeless provider of its own and tests the mechanism
rather than whichever profile happens to be blank that month.

L8 checks the reported model before judging the capability table: this channel
answers glm-5 / glm-5.1 / glm-5.2 with glm-5.3, which is a routing problem the
library already warns about, not drift. All three are guarded, including the
one that passed by luck.

T10 tells 404 model_not_found (the channel dropped the model) apart from 400
(the tier really is refused), reading the status code and the body's type field
rather than the whole message; only the latter still counts as a conclusion
about a tier. An all-skipped tier list now skips instead of going green.

TestMiniMaxM3 gained the unreachable fallback its own docstring promised: an
outage now skips and leaves an uncovered row, where before it failed ahead of
_record and left no trace of what happened.
2026-09-05 16:24:24 -04:00
iomgaa a716f12483 Merge branch 'feat/issue-20-reasoning-effort'
推理从「开 / 关」升级为档位(issue #20)。八档 Effort 词汇、源级与请求级两个入口、
能力表按档位登记并经真实网关实测校正、缓存 key 与遥测各加一维。

issue #20 抱怨的三道机制全部恢复: 能力表查得到、装配期报错触发且带可执行替代、
运行期对账成立。核心争议由 T10 的约 500 次真实调用了结——glm-5.3 确实关不掉推理,
issue 里那个 rt≈1.2 是短提示词下的采样噪声。
v1.3.3
2026-09-05 12:27:38 -04:00
iomgaa 9021425875 docs: measure the 1.3.3 notes against 1.3.2, not against this branch
The behaviour section described round trips that only ever existed inside the
branch. Verified every claim against `git show 6ec9ec7:<file>` plus a 300-combo
side-by-side run (4 old provider slots x 25 models x three ENABLE_THINKING
states); exactly two differences exist.

- glm-5.3 / glm-5.3-flash / gemini-3.1-pro are newly registered as unable to
  stop reasoning. They were unregistered in 1.3.2, so ENABLE_THINKING=false
  passed; now it fails at assembly. That is the one change that breaks an
  existing config, so it leads the table.
- The openai slot moves from "shape unknown, refuse at assembly" to the standard
  OpenAI shapes in both directions. Neither openai nor anthropic nor google ever
  injected medium; minimax did and still does, byte for byte.
- kimi-k3 was never registered before, so it changes nothing for anyone. Moved
  to new capabilities as a first registration.
- Both keyword and positional ThinkingCapability construction break, not just
  positional; spell out the migration form.

.env.example: name all eight registry slots, and say that "on" injects nothing
on the three slots whose on_base is empty.
2026-09-05 12:25:24 -04:00
iomgaa bb9ef038c7 test: make the assembly guard fail when it forgets the source's fallback
The transport half of the effort_fallback wiring got a test last round; the
assembly half did not. Mutating _guard_thinking's fallback=source.effort_fallback
to a hardcoded "error" leaves the whole suite green, yet a zhipu/glm-5.3 source
carrying REASONING_EFFORT=medium + EFFORT_FALLBACK=nearest goes from assembling
fine to being refused at assembly. Pin it down: from_env must return a client.
2026-09-05 12:20:31 -04:00
iomgaa 85892fb1b5 fix: stop telling people the cache key records the mapped tier
It records the asked-for one. CacheMW sits outside the transport in the
onion, so at lookup time the nearest-mapping has not happened yet and the
applied tier does not exist. Telemetry's success rows do record the mapped
tier, which is where the confusion came from — the warning conflated the
two and would have sent anyone debugging a cache miss the wrong way.

Also repairs the design doc: the 2026-09-05 rollback note had been spliced
into the equivalence table, orphaning its last row, and §3.1 still said
seven tiers after `auto` made it eight.
2026-09-05 11:30:52 -04:00
iomgaa e9607b2f0c docs: cut 1.3.3 notes for the tier work
CHANGELOG gets all five breaking changes, not the one the draft had:
ThinkingCapability's constructor, two ports that grew a parameter with
no default, resolve_thinking's new return type, and ProviderProfile's
single wire field. Behaviour changes get their own section, including
the one that is easy to miss — the openai fallback segment no longer
refuses an unknown shape, so a downstream that parks a foreign model
there and asks for thinking used to fail at assembly and now sends
nothing at all. minimax is called out as the exception it is: the gateway
proved M3 does not think without a parameter, so that segment keeps its
medium and its downstreams see no change this release.

The capability table is reported as it stands — 17 of 24 rows measured,
7 still on documentation, with the reason each one went unmeasured, so
nobody reads "measured" into a row that is a guess. The auto limitation
and its deliberate MiniMax-M3 inconsistency are written down rather than
left for someone to trip over; issue #21 holds the real fix.

ARCHITECTURE had five claims that measurement showed had gone false:
the cache key formula, the field count, the reconcile predicate and its
throttle key, and two field lists. README's FIELD set was missing the
two new keys it calls exhaustive.

docs-convention still opened by announcing a 17-page site that has not
existed since August. It now says what is actually there — one placeholder
page pointing at .env.example, CHANGELOG and the source docstrings — and
says which four files carry the sync gate while the site is down.
2026-09-05 11:22:01 -04:00
iomgaa a194f4326e fix: put the tier back where "on" by itself is not on
The minimax wire lost its tier value on the assumption that these models
reason by default, so injecting nothing still reads as "on". T10 measured
the real gateway and the assumption does not hold: MiniMax-M3 with no
reasoning parameter did not reason in 5 of 5 rounds, while all six
strength values worked. Existing downstreams on ENABLE_THINKING=true
went from reasoning to silently not reasoning, and the capability table
cannot catch it because phase 5 lets auto through unconditionally.

Restore on_base to the old {"reasoning_effort": "medium"} verbatim. This
is a stopgap - it hands the tier choice back to the library, which this
work set out to remove. The real fix is to constrain auto by the
capability table, a public behaviour change tracked as issue #21.

The assertions that said "minimax injects no tier on the on-tier" go
back with it; each carries a note on why it moved twice.
2026-09-05 10:48:08 -04:00
iomgaa f5e6fafe8d test: replace the guessed tier table with what the gateway actually does
T10 经 new-api 中转对 26 个可调用模型逐个实测(约 500 次真实调用),把
DEFAULT_CAPABILITIES 里的文档推定换成实测结论,并把实测本身固化成可复跑、
可只跑单模型的 slow 用例。

三处与文档推定不符:
- kimi-k3 实测可关(关闭档 completion 恒 9 且与 max 档锚点可分),T1 的保守
  登记被推翻,OpenRouter 的 mandatory:false 胜出;
- MiniMax-M3 的 auto 实测不推理(minimax 的"开"是不注入任何参数,而 M3 默认档
  本就不推理),六个强度值反而全部生效且彼此等价,故清单由 (none, auto) 换成
  none + 六个强度值;
- glm-5.3 关不掉推理坐实(请求 none 后 4/5 轮仍观测到推理),issue #20 的核心
  争议就此了结——当时短提示词下的 rt≈1.2 是采样噪声。

新增 kimi-for-coding(现有该型号自己的实测证据,不再走 Phase 3)。

用例带三条判据: 关闭方向要求每轮未观测到推理;短提示词的"关掉了"必须过长
上下文复核(glm-5.3-flash 正是短 5/5 未观测、长上下文 2/3 露馅);上游整片
不回传推理信号时另取无魔数锚点,不许把"没看见"写成"没发生"。另加一道模型
身份核对——该渠道把 glm-5/5.1/5.2 全部路由到 glm-5.3,那三组数据一律作废。

未覆盖 9 个型号(claude 三个 7 天限额用尽、gemini 两个上游报错、gpt-5.4 限流、
glm 三个被路由),evidence 逐条写明原因,不得被后续文档写成"已实测"。

单元测试里三条以 MiniMax-M3 当"纯开关型"样本的用例改用 glm-4.6v——实测后
M3 不再是那个形状,而 glm-4.6v 是全表证据最硬的 (none, auto)。
2026-09-05 10:35:30 -04:00
iomgaa f9b357b9d7 test: give two silent guards something to fail on again
`test_extra_body_overrides_the_profile_slot` guards that a source's
`extra_body` wins over the slot the profile injects. Since T5 made
`enable_thinking=True` mean `auto`, its minimax fixture injected an empty
fragment — swapping the two update calls changed nothing and the whole
suite stayed green, while a real source (zhipu, glm-5.3, tier `low`)
would have shipped `low` instead of the `high` the caller wrote. Move the
fixture onto that combination, where the two sides write the same key.

`emit_attempt`'s docstring promises `reasoning_applies` carries no
default so that a missed argument is a TypeError rather than a silent
`True`; nothing enforced it. Pin the signature the way `record_llm_call`
is already pinned.
2026-09-05 06:54:40 -04:00
iomgaa 4866e6b858 fix: normalise a bare tier string at the fourth door
`resolve_thinking` is exported, and its third argument turned from `bool`
into `Effort` this cycle — so the most natural downstream call now passes
the `"low"` it read out of JSON or config. Every gate inside compares by
identity, and `"none" is Effort.NONE` is always false: the tier was not
rejected, it was silently misjudged. Phase 2 read the on-form for a
request that wanted the off-form, Phase 4 was skipped entirely on a model
that cannot be disabled, and the only visible symptom arrived much later
as an AttributeError from `.value` — an exception that is neither
documented nor one of the four error classes.

Design 4.4 already lists this as the fourth entry a tier can come in
through; this makes the code agree with it.
2026-09-05 06:49:34 -04:00
iomgaa 32b92a8894 docs: count the breaking changes again, and say where the fourth door is 2026-09-05 06:37:57 -04:00
iomgaa e06cd8e8b7 feat: record which tier a call actually ran at
Twenty-five columns and not one of them answered "which tier was this?",
so the question the whole issue exists to settle - does a higher tier buy
anything - had no way to group its data.

The three emit entry points deliberately disagree, the way sampling
already does. A successful attempt records what the transport actually
sent: with EFFORT_FALLBACK=nearest a request for medium goes out as low,
and recomputing here would file the row under a tier that never left the
process. A failed attempt has no response to read, so it falls back to
the requested tier - which is exactly right for the tier errors that are
rejected before any HTTP happens, because the rejected tier is the
signal. Cache hits and terminal failures have no chosen source at all,
so a source-level tier is not a thing they could report.

emit_attempt now demands to be told whether the path reasons at all.
Embedding and OCR share the emitter but never send reasoning parameters;
without the flag a source that mistakenly carries ENABLE_THINKING would
hang a tier on a call that could not possibly have run at one.

The value lands as a plain str. StrEnum is a str subclass and asyncpg
promises nothing about encoding subclasses, and a telemetry write that
fails is only a warning - Postgres would just quietly lose the column.
NULL means nobody declared a tier, which is not the same statement as
'none', and the two must never be folded together.
2026-09-05 05:57:29 -04:00
iomgaa 9832dcee63 docs: name the three doors a tier can enter through, before a fourth appears 2026-09-05 05:12:26 -04:00