Commit Graph

17 Commits

Author SHA1 Message Date
iomgaa 7fabc792b2 feat: give zhipu, moonshot, anthropic and google a wire of their own
Eight segments now, and each one holds a ThinkingWire instead of two
fixed fragments: off, on_base, and the key a tier gets written to. The
two fragments could not say "on, at this depth", which is what every
current generation model wants.

Two deliberate behaviour changes fall out of it. The openai segment stops
reporting its shape as unknown — reasoning_effort is OpenAI's own field,
not a vendor dialect, so a compatible endpoint behind the gateway takes
it. And minimax's on-tier stops carrying a hardcoded medium: that was the
library picking a price for the caller, and medium is not even a tier GLM,
kimi or deepseek serve.

The issue #5 guards stay; their sample moves from "the openai segment" to
an explicitly registered unknown one, which is what they always meant to test.
2026-09-05 00:42:58 -04:00
iomgaa 1307a02b92 fix: close the failure modes review found in the new code
Three of them were the same shape as the bug this branch exists to fix:
something goes wrong, the library swallows it, and the caller is left
with a number that means the opposite of what happened.

The throttle key had no source in it. Five sources on one model is the
normal case here, so the first one to break would warn once and silence
the other four for the life of the process, and the message never said
which gateway to look at.

An unknown verdict in a cached entry threw away the whole response. The
rehydrator tolerates unknown fields but not unknown values of a known
field, so two library versions sharing a Redis would each invalidate
the other's entries: halved hit rate, and the only log line says the
cache rebuild failed. A purely observational field should not be able
to void a response whose content is intact.

Normalising for telemetry now degrades instead of raising, both for a
bare string and for a value outside the domain. Either one used to
reach the same except and cost the whole row, which is exactly how
1.3.0 lost nineteen calls without anyone noticing.
2026-08-26 02:37:24 -04:00
iomgaa 20a4a9ae47 feat: warn when the capability table and reality disagree
The M3 evidence sat at 08-02 for twenty-three days while nobody could
tell whether it still held. A declaration that goes stale in silence is
the failure this issue is really about, so the library now compares
what it declared against what it just observed and says so when the two
part ways.

Judgement is separated from logging: reconcile_thinking returns the
warning text, so tests assert on the text instead of parsing logs.
Two cases that look alike are kept apart — a model whose capability is
registered gets a drift warning quoting its evidence, an unregistered
one is never told the table said anything, because it never did.

False x UNKNOWN stays silent on purpose. UNKNOWN cannot falsify
anything, and warning on it would fire on every disabled call M3 makes
over the plain endpoint. A warning that always fires is not a warning.
2026-08-26 00:23:57 -04:00
iomgaa 8c5c23ae72 feat: carry the reasoning verdict through to LLMResponse
Both assembly paths fill it, streaming and non-streaming alike. Filling
only one is exactly the divergence this issue exposed: M3 returns
reasoning prose over SSE and nothing at all over the plain endpoint, so
a verdict computed on one path says nothing about the other.

The field defaults to UNKNOWN on both TransportResult and LLMResponse.
A transport that does not judge should not get to declare absence on
the provider's behalf, and a default that stays silent is the only one
that cannot lie.
2026-08-26 00:03:28 -04:00
iomgaa 33ed7ecdfc feat: cap telemetry bodies at a configurable length
Chat rows stored full message and response text with no upper bound, so
downstream contracts and tenders lived in llm_calls indefinitely. Add
_cap_text/_cap_messages in the single telemetry exit (_record), applied
after digest_messages and before json.dumps, plus to response/thinking.

Capping is per text, not over the serialized JSON: cutting the whole
string would emit invalid JSON into an unvalidated TEXT column. The cap
builds new dicts and never mutates in place — digest_messages passes
non-list content straight through as the same object, so an in-place cut
would silently poison the caller's messages and the cache key.

text_cap is required on TelemetryEmitter (internal class, three known
construction sites) and defaults to None on the three public clients, so
the default behaviour stays byte-for-byte identical. Settings wiring
lands separately.
2026-08-19 13:39:17 -04:00
iomgaa fa4a7e220b test: make the 429 red line actually catch its violation
Verifier mutation test: flipping _translate_429 to parse the summary
left all 824 tests green. The padding was one long string value, so the
cut landed inside it - and head-and-tail retention kept the trailing
error object, leaving the summary parseable. Many keys put the cut
between structural tokens, where the summary stops being valid JSON.
Mutation now fails as it should. Also splits OCR 429 out on its own.
2026-08-16 06:50:50 -04:00
iomgaa 0edb9d397a feat: keep the gateway's words on every non-2xx chat branch
Issue #10 Task 3: five branches each built their own message, so adding
the summary would have meant five copies. Table-driven classification
composes it in one place instead, and the 429 split still parses the
untruncated body - reading the summary would demote an oversized
insufficient_quota to a plain rate limit and stop force_open.
2026-08-16 06:07:29 -04:00
iomgaa 48805cb9fb fix: address the independent verification findings (issue #5, #6)
The verifier caught that the disable-direction evidence only proved "no
regression", not "actually took effect": on M3 the disabled runs and the
no-opinion baseline are identically distributed, because that model does
not reason by default anyway. So the disable runs alone cannot rule out
the very failure mode issue #5 is about -- the parameter being silently
dropped upstream. The bogus-value experiment that does rule it out was
sitting in the findings document instead of the test suite; it is now
case L3b, and the L3 assertion that could never fail is gone.

Also from the review: the e2e helper caught bare Exception, which would
have disguised a library bug as an unavailable source, exactly the
silence the reporting discipline exists to prevent; the unregistered
model warning fired on every request instead of once per source; and the
transport caught ValueError broadly enough to mislabel unrelated errors,
now narrowed to a dedicated ThinkingUnsupportedError.

The design and plan still described the original judgement criteria,
which the measurements had already overturned. Both now match what the
tests actually do, and the design no longer claims the only new failure
surface is the openai one -- dissect configures MiniMax-M2.7 with
ENABLE_THINKING=false and will fail at assembly, which has to be
coordinated before this merges.
2026-08-02 07:40:06 -04:00
iomgaa 82f4ec4910 feat: model the thinking switch as shape plus capability (issue #5)
enable_thinking=False was a no-op for minimax and openai sources: both
profiles had empty dicts on each side, so the payload update injected
nothing while the caller believed reasoning had been turned off. A
downstream project was blocked on exactly this.

The root cause is that an empty dict meant two different things -- "no
injection needed" and "we do not know how this provider spells it" --
and that a provider-level table cannot express what turned out to be a
per-model property. Live testing showed MiniMax-M3 can disable
reasoning via reasoning_effort while M2.7 and M2.5 cannot be disabled
at all, which two external registries independently confirm.

So the shape stays at provider level and a capability table joins it at
model level. Unknown, unsupported and no-opinion are now three distinct
values, and resolve_thinking is the single place they meet: it raises at
assembly time when a model cannot honour the request, warns and injects
for unregistered models, and injects silently otherwise. Every registered
capability carries the evidence it was derived from.

enable_thinking also joins the cache fingerprint, since it now really
does change the request body.
2026-08-02 06:20:24 -04:00
iomgaa 89ff916bc8 feat: collect reasoning_tokens from the provider usage payload (issue #6)
Reasoning tokens are already counted inside completion_tokens, so the
cost total was never wrong -- what was missing is the attribution: how
much of a call was spent thinking rather than answering.

LLMResponse and TransportResult each gain a trailing reasoning_tokens
field, and the telemetry port grows from 21 to 22 columns with the new
column appended in both backends so fresh and migrated schemas keep the
same physical order.

None means this particular call did not report the field, not that the
source never reports it: a relay that falls back to a local tokenizer
replaces the whole usage object and drops completion_tokens_details.
Downstream checks must therefore read "in (None, 0)"; no provider was
observed reporting a literal zero.
2026-08-02 05:55:37 -04:00
iomgaa 6bb64ca938 feat: accept sampling overlay on chat() and per-source extra_body
Priority is structured injection > per-call overlay > per-source config,
and extra_body now takes part in the cache fingerprint.
2026-07-31 21:17:49 -04:00
iomgaa 32d7869043 fix: harden the observability fields against the verifier findings 2026-07-31 08:28:41 -04:00
iomgaa 0aa7202c87 feat: collect provider cache tokens and reported model in transport 2026-07-31 07:51:47 -04:00
iomgaa 195454d2e3 fix: stop passing est_tokens off as measured usage
usage 帧缺失/非法时不再拿 est_tokens(最坏情形上界)当实测值,chat 与
embedding 两处兜底改记 0 并标 unavailable;打捞覆盖加 measured 前置条件,
避免 0/0 被洗成 estimated 而算出假的 0.0。embedding 全批合并扩三态(任一批
不可得 → 整体不可得),_total_cost 遇不可得批整体记 NULL。
2026-07-30 10:39:32 -04:00
iomgaa 0b8460b6d5 fix: address independent verification findings
Classify empty completions as transient per human ruling (fixes flaky
real-gateway smoke and prevents caching empty responses), rename the
factory injection parameter gate to breaker per the frozen design,
rewrite the probe-entry cleanup without except BaseException, declare
python-dotenv explicitly, add a mid-backoff cancellation test, and
record all implementation errata in the design and architecture docs.
2026-07-20 22:01:26 -04:00
iomgaa 7b9815f4bc feat: add gateway client with env-driven assembly
Includes config aggregation for multi-source env keys, from_env and
from_settings factories with explicit shared-backend injection,
gather_bounded, top-level exports, tightened import-linter layers with
the gate removed from the Makefile, and the finalized .env.example.
2026-07-20 07:47:05 -04:00
iomgaa 454a8b5e0f feat: add openai-compatible transport with sse parsing and error translation 2026-07-20 06:45:55 -04:00