symfonic.services.budget.truncation¶
truncation ¶
Deterministic truncation with a fit guarantee.
The rule this module exists to enforce: the returned string, marker
included, costs no more than the ceiling it was given. That sounds obvious
and the helper this replaces did not hold it — it sliced to max_tokens *
chars_per_token characters and then appended "...", so every truncated
result was over budget by the width of the marker, and the same module's
floor-dividing estimator agreed that it fitted. Two errors in the same
direction hide each other.
Holding the guarantee against an arbitrary counter takes a search rather than arithmetic: the caller may have bound a provider tokenizer, and the number of tokens in a prefix is that tokenizer's business. Binary search over the kept character count finds the longest fitting prefix when the counter is monotone in prefix length, and merely a fitting prefix when it is not — but never an over-budget one, because a candidate becomes the answer only after being measured against the ceiling. The guarantee therefore costs nothing to a well-behaved counter and survives a badly-behaved one, which matters because a third party's arithmetic is not something this module gets to assume.
Two consequences worth naming. Truncation is idempotent: the result already fits, so a second pass returns it unchanged — which is what makes it safe to call defensively in a pipeline where two stages both budget. And truncation is monotone in the budget: a larger ceiling never keeps less text, so raising a limit cannot make a prompt smaller.
truncate ¶
truncate(text: str, max_tokens: int, counter: TokenCounter, policy: TruncationPolicy | None = None) -> TruncationResult
Cut text down until it costs at most max_tokens.
Returns the input untouched when it already fits — no marker, no copy, and
truncated=False, so a caller can tell "was not cut" from "was cut to
exactly the limit".
When the ceiling is too small to hold even the elision marker the result is the empty string rather than a bare marker. A marker on its own is not content: it would consume the entire allowance to communicate that the allowance was consumed.