Address the 11 confirmed bugs and 4 cleanups from
context-pruning-and-framing-review.md, plus the image-accounting
regression it found on a second pass.
classifier framing:
- _previous_context walks back to the nearest assistant turn only,
skipping system/user/tool so raw tool output and the system prompt
never contaminate the framing signal; reuses context_prune.extract_text
instead of a duplicated block parser.
- classify() drops its redundant if/not-context branch.
- framing instruction moved out of the static system prompt and into
_classifier_user_content so it travels only with content that justifies
it (respects context_framing opt-out).
context pruning:
- extract_text counts image_url bytes so image-bearing tool results are
sized (and pruned) correctly.
- -- new, follow-up review -- image_url blocks now count toward the size
trigger but not toward the replacement text, and are stubbed to a short
marker when the message is trimmed, so an image-heavy tool result
genuinely shrinks rather than staying full-size while tokens_saved lied
and base64 leaked into the text block.
- tokens_saved is now the honest whole-list before/after reduction
(max(orig - final, 0)), never a per-message estimate that could drift.
- max_summarize_chars validated >= 3000 at config load; trim branch guards
against negative math and message growth.
- recency guard protects the newest tool result even with no user turn.
- non-text blocks survive trimming (type preserved) rather than being
flattened away.
- dropped/stat terminology corrected to summarized; _turn_of() removed;
module-level pinch defaults removed in favor of PinchConfig.
Dispatch and config:
- prune_context runs once before the measured-context routing decision
(reused at dispatch, no double-prune, no in-place mutation); no-op when
pinch is disabled.
- dead context=prev_context arg removed from the measured reroute.
- TaskRequest.context dual use documented.
15 new regression tests pin the fixes; 545 tests pass.
Port two ideas from the MIT-licensed alexrudloff/llmrouter:
1. Context-aware classification. chat_completions now feeds the classifier
the message before the last user turn, framed as llmrouter's
"Context: <prior>\n---\nMessage: <current>" (classifier.context_framing,
default on). A short follow-up ("Yes", "Try now?") inherits the prior
turn's complexity instead of being classified in isolation as trivial.
_classifier_user_content is a pure, testable framing helper; the system
prompt gains the inherit-from-context rule.
2. Relevance-based context pruning (context_prune.py). An optional,
pre-dispatch stage that trims old tool results from the provider-bound
conversation once it exceeds pinch.budget_tokens (pinch.enabled defaults
off). User/assistant/system messages are always kept verbatim; only old
tool results are summarized (head+tail with an elision marker) or dropped
to a placeholder, with role pairing preserved so the result still parses
as a conversation. Applies to both the streaming and non-streaming
upstream bodies; the classifier input is exempt (it already clamps).
Tests: +23 (pruning invariants, framing, previous-context extraction, pinch
config validation). 529 pass.