Skip to content

Context snapshot publishes remainingContextTokens above contextBudget minus its own instruction tokens #568

Description

@danny-avila

What breaks

A context snapshot can publish a remainingContextTokens larger than contextBudget − effectiveInstructionTokens. That is impossible for a real prompt: the remaining headroom cannot exceed the budget left after the instructions the same snapshot reports.

Downstream effects:

  • syncBudgetDerivedFields (messages/budget) derives breakdown.messageTokens = max(0, contextBudget − effectiveInstructionTokens − remainingContextTokens), so the overshoot is clamped to messageTokens: 0 and hidden rather than surfaced.
  • LibreChat's context gauge computes used tokens as contextBudget − remainingContextTokens. That comes out below the instruction count it subtracts, so the breakdown's Messages row disappears and the percent and free space are understated (📏 fix: Keep the Context Gauge's Used Count Above Its Own Breakdown LibreChat#16368 makes the client robust to it).

Evidence

The last 400 persisted snapshots on a live deployment (LibreChat dev, @librechat/agents 3.9.6): 23 violate the invariant. Every one has breakdown.messageTokens: 0. Samples:

contextBudget effectiveInstructionTokens budget − instructions remainingContextTokens overshoot
947,625 12,887 934,738 936,899 2,161
947,625 7,800 939,825 942,151 2,326
947,625 15,538 932,087 935,020 2,933

It also reproduces deterministically in LibreChat's e2e mock: a first turn whose whole system prompt is a 57-token memory block, with a fake model reporting 2 input tokens, publishes contextBudget − remainingContextTokens = 2.

Candidates (not yet pinned)

From reading the built dist/esm:

  1. pruneMessages, main path (messages/prune, around the rawRemaining computation): the remaining count is computed in raw token space, scaled by calibrationRatio, and clamped with Math.min(pruningBudget, …), not pruningBudget − currentInstructionTokens. Rounding across the raw/calibrated conversion and the reclaimed orphan tokens are added after the instruction subtraction.
  2. The emergency-truncation retry recomputes the context with getMessagesWithinTokenLimit in calibrated space but the returned remaining still derives from the first, raw-space call (initialRemainingContextTokens).
  3. currentInstructionTokens can switch between the calibrated provider overhead (bestInstructionOverhead) and the estimate between calls, so a remaining carried as a baseline into contextPressureMeter.measure may have been measured against the other value.

The ~2–3K overshoot on real traffic is larger than rounding alone would explain, which points at 1 (reclaimed tokens) or 3.

Expected

Every published snapshot satisfies remainingContextTokens ≤ contextBudget − effectiveInstructionTokens, with remainingContextTokens and effectiveInstructionTokens taken from the same measurement. A cheap guard at publish time (where syncBudgetDerivedFields runs) plus a debug log when it trips would stop the silent clamp and show which path produces it.

Separately, LibreChat's e2e fake model reports input_tokens without the system prompt; real providers count it. That should be fixed in LibreChat's harness so calibration sees realistic input.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions