Collapse old conversation history into rendered PNG sections so the model
reads a compact image instead of re-billed text, while preserving prompt
caching and tool-call behavior. Measures real vs compressed token/cost.
Core:
- GPT history collapse (openai-history.ts): append-only, o200k token-length
sectioning. Sections seal only at a tool-closed boundary (open call-id set
empty), so a function_call and its function_call_output never split across
the collapse cut. Fixes the OpenAI 400 "No tool call found for function
call output with call_id ..." that hit long Responses-API sessions.
- Anthropic cache contract (history.ts): append-only per-chunk rendering;
cache_control markers are preserved/moved, never added; chunk boundaries
align with caller marker seams for byte-stable prefix caching.
- GPT image budget (openai.ts): detail:'original' for gpt-5.x, flagship
vision-multiplier fix, patch cap; schema-strip preserves real descriptions.
- Savings accounting (openai-savings.ts): cached_tokens + vision-token basis.
Model scope (applicability.ts):
- Default imaged scope = claude-fable-5 + gpt-5.6.
- gpt-5.5 and claude-opus-4-8 stay opt-in: same pipeline, but they degrade
reading dense imaged history (gist drift), so silently imaging them by
default is wrong. Promotion is gated on an OCR/recall threshold.
Dashboard: GPT + Anthropic rendering, per-family model toggles, persisted
metrics, thumbnail-expired session UI, reflow/newline handling.
Tests: cache-alignment (GPT + Anthropic), history sectioning + tool-boundary
invariants, savings, dashboard, sessions/restart-restore. 452 passing.
The session hero computed (1 - sent / raw_count_tokens_baseline) — the same
cache-blind math just removed from the Details panel. It priced the all-text
baseline at full rate, ignoring that most of it would have been cheap
cache-reads, so a warm net-loss session showed a big "fewer tokens" win up top
while Details/Saved showed a loss. Two panels, one session, opposite signs.
- renderSessionSummaryFragment now reads baselineInputWeighted /
actualInputWeighted (the same cache-weighted pair as Details + the Saved
column) instead of rawActualTokens / rawBaselineTokens.
- Headline is input-only; dropped the output-on-both-sides lumping (output is
never "sent" and only dampened the %).
- Honest labels: "effective tokens … after normal cache discounts"; still no $
assumptions (0.1x/1.25x are token multipliers, not $/Mtok).
- types.ts: corrected stale field comments (raw fields are math-drawer only).
- +4 hero tests pinning direction to the weighted pair and that output can't
move the headline %.
374 tests pass; typecheck + build clean.