Skip to content

ci: verify #818 head (do not merge) - #864

Merged
ViaJables merged 6 commits into
mainfrom
ci/verify-818
Sep 8, 2026
Merged

ci: verify #818 head (do not merge)#864
ViaJables merged 6 commits into
mainfrom
ci/verify-818

Conversation

@ViaJables

Copy link
Copy Markdown
Collaborator

CI-only. The maintainer commits pushed to #818's fork branch did not trigger a workflow run — its green checks are for the contributor's original commit, not for the review fixes. This mirrors the exact tree so the fixes are actually verified before #818 is merged. Close once #818 lands.

arhyneRWU and others added 6 commits September 1, 2026 21:25
`token_safety_margin` gains a rung between the tiktoken-is-exact check and
`DEFAULT_TOKEN_SAFETY_MARGIN`. When a provider has counted the request, the
margin becomes the measured ratio of its count to our local one, rather than
the 1.5 that covers the worst content anyone measured.

Deliberately a ratio and not a total. The margin is applied per component,
`BudgetPlan.total_input_tokens` is a computed property summing those parts, and
the planner recounts on mutated text inside its trim loop. A recorded total
stops being true the moment anything is trimmed; a ratio stays approximately
valid.

Clamped at 1.0: a provider counting below tiktoken may only reduce
over-inflation, never reclaim window. Rejected outright on a model-name
mismatch, a zero baseline, a non-positive count, or a non-finite ratio, each of
which falls through to the existing ladder — so an absent or broken measurement
is the 1.5 guess, by construction rather than by a second code path
remembering. The "estimated, not exact" warning stays quiet only when the
measurement actually supplied the margin.

Inert until a caller passes one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
`_sized_for` re-derived the current model's safety margin from its name and
config in order to restate a request in a candidate's units. That is correct
only while every count comes from the same estimate-plus-default path.

A provider-counted request carries a much tighter margin, and
`token_safety_margin` cannot know that happened — it still answers 1.5. Dividing
an already-tight number by a factor never applied understates the request by the
whole difference, at the exact point the router is deciding whether a candidate
can hold it. That is the #648 defect — an estimate that reads low, so the
request hard-fails — relocated to the routing boundary, which this function's
own docstring says it exists to prevent.

Callers may now state the margin their number was measured with.
`choose_document_model` and `suggest_document_model` pass it through; the
candidate's margin stays derived, because the candidate has not been counted.
Omitted, every caller keeps today's arithmetic to the token.

A supplied margin below 1.0, non-finite, or not a number at all is refused in
favour of the honest derivation, matching how `context_budget._configured_margin`
refuses the same thing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
New `native_token_count`: fetches a provider's own count for a prepared
request, or reports plainly that it could not.

Two properties, and the second is the load-bearing one. A returned figure is the
provider's own, unmodified. Anything going wrong returns nothing at all — the
caller drops its safety margin on the strength of a usable result, so a count
that comes back zero, short, or from a provider that counted half the request is
worse than no count, converting a conservative over-estimate into a confident
under-estimate.

So: gated before any I/O on a protocol that can actually count, since
`OpenAIModel.count_tokens` raises and every OpenAI-compatible model would
otherwise decrypt a key and build a client per chat turn to learn that. Wrapped
in a five-second timeout that is deliberately not the request's own settings —
`build_thinking_model_settings` sets 120s and the shared client retries a 429 six
times honouring Retry-After up to 60s, which would put a two-minute stall in
front of every message. Blanket `except` by necessity: Anthropic raises
ModelHTTPError, Google's genai errors are unwrapped, and a misconfigured
protocol reaches OpenAIModel; there is no complete and stable union of those.
CancelledError passes through, being the caller going away rather than a failure.

Google's count omits the system instruction — pydantic-ai attaches
`system_instruction` only for providers other than `google-gla`, and an
api-key GoogleProvider is `google-gla`. `covers_system_prompt` says so rather
than letting a caller silently drop a multi-kilobyte grounding preamble from its
budget.

Nothing imports this yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
Wires the native count into `chat_stream`. Before the request is sized, the
provider is asked what the assembled prompt actually costs; the same payload is
counted locally; the ratio becomes this turn's safety margin in place of the 1.5
guess.

The payload counted is the whole user prompt, documents included. `_build_chat_prompt`
is the existing assembly extracted verbatim so the pre-flight counts the request
chat is about to make rather than a reconstruction of it — in document chat the
documents are the payload, and they are also the digit-dense content the 1.5 was
sized for. Sampling the question and history alone would measure prose and apply
the answer to a budget table.

The baseline covers exactly the components the provider counted, which differ by
provider: Anthropic's count includes the system prompt, Google's does not for
api-key providers. Counting it on one side and not the other would depress the
ratio for a reason unrelated to tokenizer divergence, and a depressed ratio
clamps to a margin of 1.0 over a tiktoken figure that itself under-counts these
models — the hard-fail direction.

Routing is given the margin the number was measured with, rather than
re-deriving it. That value survives a model switch on purpose: it describes how
`requested_input_tokens` was measured, and that does not change when the model
does.

A count for one model is not passed to the planner for another. `_native_margin`
already rejects on a name mismatch, but relying on that silently is how the next
person learns it the hard way.

Anything unusable — ineligible protocol, timeout, 429, malformed response — leaves
the stream exactly as it is today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
The module coalesced the failure log but not the failure work. A
deployment whose egress to the count endpoint is slow, or whose org is
rate-limited there, paid the full 5s timeout on every chat turn ahead of
the first token -- permanently, and after the first occurrence it said so
only at DEBUG. There was no metric and no way out short of a restart.

Three consecutive failures now pause a model for five minutes. The check
sits after the protocol gate, so an ineligible model never enters the
circuit, and before any model construction, key decryption or request, so
an open circuit costs nothing. One request is let through when the
cooldown elapses, and any success clears the record, so an outage cannot
disable counting until the next deploy. State is per model name: a
failing Anthropic endpoint does not stop Gemini being counted. The clock
is monotonic, so a clock adjustment cannot strand a model in cooldown.

Skipping is strictly better than blocking here: the estimate path it
falls back to is the one that shipped before this feature, so a paused
model gets the same budget it would have got anyway, without the wait.

Two corrections to the changelog, both things it asserted that the code
does not do:

- "fires only for models an admin explicitly configured" is not true of
  Gemini. detect_api_protocol routes any name containing "gemini" to
  google on its own, so a deployment already running one gets the
  pre-flight on upgrade without changing anything. Issue #816 said this
  correctly and the entry lost it.
- "one unconditional round trip" is no longer true, which is the point of
  this commit.

The existing failure-logging tests needed updating rather than working
around: _CIRCUIT is per-process state exactly like _UNAVAILABLE_LOGGED,
so it joins that autouse reset -- without it, whether a test sees an open
circuit depends on collection order. The repeated-failure test now runs
50 turns rather than 4, pinning the bound instead of an arithmetic
coincidence.

Verified by disabling only the gate while keeping the state: the provider
is called on all 10 turns and the test fails on the count. Stashing the
whole module instead only proves a symbol is missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtRYNNEdrq6SPyPt8PjH1v
@ViaJables
ViaJables merged commit 0712093 into main Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants