Skip to content

symfonic.services.budget.contracts

contracts

The budget policy contracts: what an adopter declares, and what it may not.

A policy declares intent — how large the window is, how much of it each kind of content may claim, how exact the counting must be, what to do when content does not fit. It never declares a number of tokens for a specific block: that is derived, in one place (:mod:.allocation), from the window and the shares. The distinction is what makes two deployments with the same policy produce the same split, and what stops a capability from quietly voting itself a larger allowance than the operator granted.

The two ports are deliberately tiny. :class:TokenCounter is one method over a string, so a provider's exact tokenizer can be bound without this package knowing anything about that provider. :class:TokenEstimator is the same method under the name the prompting capability already uses — declared here so the two packages can be wired together without an import edge in either direction, which is what their rows of the dependency matrix require.

BudgetKind

Bases: Enum

The kinds of content that share a context window.

Declared in ladder order: iteration order is the canonical order of an allocation plan, so two policies that mean the same split are the same plan regardless of the order their lines were written in.

BudgetLine dataclass

BudgetLine(kind: BudgetKind, share: float, minimum: int = 0, overflow: OverflowAction = OverflowAction.TRUNCATE)

One kind's claim on the usable window.

share is a fraction of the usable window (the context window less the output reserve), and minimum is a floor in tokens for the case where a proportional share of a small window would round down to something useless.

BudgetPolicy dataclass

BudgetPolicy(context_window: int, output_reserve: int = 0, lines: tuple[BudgetLine, ...] = (), mode: CountingMode = CountingMode.HEURISTIC, provider: str = '', chars_per_token: int = DEFAULT_CHARS_PER_TOKEN, truncation: TruncationPolicy = TruncationPolicy())

The whole budgeting intent for one invocation.

Frozen and self-validating: every impossible combination is refused at construction, so a policy object in hand is a policy that can be satisfied by arithmetic — the only remaining way to fail is content that does not fit, which is :class:~.errors.BudgetOverflowError and a different type.

usable property

usable: int

Tokens available for input, after the output reserve is set aside.

line_for

line_for(kind: BudgetKind) -> BudgetLine | None

The declared line for kind, or None when it has no claim.

Source code in src/symfonic/services/budget/contracts.py
def line_for(self, kind: BudgetKind) -> BudgetLine | None:
    """The declared line for ``kind``, or ``None`` when it has no claim."""
    for line in self.lines:
        if line.kind is kind:
            return line
    return None

CountingMode

Bases: Enum

How exact the counting has to be.

EXACT fails when no counter is registered for the provider; that is the difference between it and EXACT_PREFERRED, and the whole reason both exist. An operator reconciling invoices needs the failure; an operator who merely prefers precision needs the fallback, plus a diagnostic saying it happened.

ExactCounterSpec dataclass

ExactCounterSpec(provider: str, counter: TokenCounter, offline: bool = False, revision: str = '')

An adopter's provider-exact counter, offered for registration.

offline is an assertion the registrant makes and the registry enforces: a counter that would fetch an encoding on first use is refused at registration, on the machine that wrote the config, rather than at 3am on the container that has no egress.

Keep

Bases: Enum

Which end of an over-long string survives truncation.

OverflowAction

Bases: Enum

What to do when a kind's content exceeds its allowance.

TokenCounter

Bases: Protocol

Counts the tokens in a string.

Implementations must be pure (same string, same number) and monotone non-decreasing in prefix length — truncation's binary search assumes the second, and its shrink loop survives an implementation that breaks it.

TokenEstimator

Bases: Protocol

The prompting capability's spelling of the same port.

symfonic.capabilities.prompting binds an estimator by structure, not by import: capability and runtime-service may not import each other, so the two protocols meet as identical shapes rather than as a shared base class.

TruncationPolicy dataclass

TruncationPolicy(keep: Keep = Keep.HEAD, marker: str = '...[truncated]')

How an over-long string is cut down.

The marker is mandatory. A silent elision is a defect rather than a style choice: neither the model nor the operator reading a transcript can tell truncated content from content that was always that short, and the two lead to opposite debugging conclusions.