Commit Graph

14 Commits

Author SHA1 Message Date
Steven Chong 47b5e412b3 feat: cache-stable history-collapse imaging for GPT (OpenAI) + Anthropic
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.
2026-06-20 21:11:24 -04:00
teamchong cf4188625a docs: add HISTORY_CACHE_MODEL.md, conceptual companion to caching/savings
Captures why imaging conversation history stays cache-safe as a session
grows: the quantized collapseChunk boundary, the one-time cache-create
burn + amortization gate, the protectedPrefix slab anchor, and the
relocate-don't-add cache_control rule. Grounded in src/core/history.ts
and transform.ts; prose mirror of the verified code.
2026-06-19 11:26:39 -04:00
teamchong af1e838f0f chore: release 0.3.1 — Drive-hosted demo, keep preview thumbnail
Demo clip moved to Google Drive (committed copy was too low-res); README keeps the preview thumbnail linking to the Drive video. Restoring the thumbnail also un-breaks the demo image on the npm 0.3.0 page. Version 0.3.0 -> 0.3.1 + CHANGELOG.
2026-06-17 15:42:42 -04:00
teamchong 85aed799e5 docs(readme): drop blurry committed demo video, link to Drive instead
The recorded clip was too low-res to read; remove the 8.9MB mp4 + thumbnail from the tree and point the Demo section at the Drive copy instead.
2026-06-17 15:15:10 -04:00
teamchong a9ff92d3e1 docs(readme): add side-by-side A/B demo video (Opus 4.8)
Attach the recorded plain-vs-pxpipe demo (cost-ab fix + effective-context recall) plus a poster thumbnail, embedded in a new Demo section with an honest caption (Opus opt-in; demo 2 shows pxpipe surfacing the lossy-count limit rather than fabricating). Fable headline clip to follow when Fable is back.
2026-06-17 13:02:37 -04:00
teamchong 8af9829872 feat(render): raise page ceiling to ~1932x1932 and align gate/paging geometry
Fable 5 / Opus 4.8 accept up to 2576px / 4784 visual tokens per image, but a
request with >20 images (pxpipe always sends many) is held to the stricter
<=2000px/side rule, so the real ceiling is ~1932x1932 (1928x1928 = 69x69 = 4761
tokens). MAX_HEIGHT_PX 1568->1932; dense tool/history pages now render at
DENSE_CONTENT_COLS=384 / DENSE_CONTENT_CHARS_PER_IMAGE=92160 (1928x1928 full
page) -- fewer image blocks at the same OCR-validated 5x8 cell. The static slab
is unchanged (313 cols / 1573x1280).

Also fixes a regression the code review caught: the break-even gate,
truncateForBudget, and estimateImageCount predicted against the slab geometry
(313 cols / 159 rows) while the dense renderer emits 384 cols / 240 rows, so
large tool_results were truncated far earlier than the 10-image cap required --
silently dropping output that would have rendered. A per-page char budget is
threaded through the prediction chain (defaults to READABLE so the slab and unit
tests are byte-identical) plus a denseGateGeometry helper, so the gate prices the
same page the renderer actually produces.
2026-06-17 10:01:33 -04:00
teamchong 50f3f1f86b docs: add prompt-caching alignment + honest savings-math doc
New docs/CACHING_AND_SAVINGS.md explains the two things that keep coming up:

1. How pxpipe stays aligned with Anthropic's prompt cache when it images the
   static prefix: split static/dynamic blocks first (keep drifting <env> etc.
   as cheap text, never imaged), then relocate (never add) the caller's
   cache_control onto the last image so the breakpoint anchors at the end of
   the stable slab. Covers the cache-friendly splice, the symmetric
   anti-flapping burn term, and the horizon-amortized history-collapse gate.

2. How savings are measured without double-counting the cache discount: both
   the real request and a count_tokens counterfactual of the original body are
   priced with identical cache rates at the same moment, so the discount
   cancels in the difference and only the token reduction survives.

Also reconcile the priorWarmTokens JSDoc in transform.ts: the burn term is
applied undivided, matching all three gate call sites and the symmetric
priorWarmImageTokens doc. The '/ N' was stale (the per-turn gate has no
horizon to divide by; the horizon scales the lifetime cost, not the burn).
2026-06-16 08:35:48 -04:00
teamchong 0277facc5d docs: reframe savings as workload-dependent snapshots; add demo + render-sizing doc
- README: cost savings now presented as measured snapshots (~59-70% end-to-end,
  ~72-74% compressed) bound to trace + workload, not a fixed 59%
- add demo/ (synthetic side-by-side /context demo: data, prompt, expected answer)
- add docs/RENDER_SIZING.md (image-sizing rules, rationale, commit history)
2026-06-15 22:33:31 -04:00
teamchong e520749b84 docs: example render is now real transformRequest output (reflow + instruction banner) 2026-06-09 21:57:50 -04:00
teamchong e20bf015c1 docs: add example render (full-density synthetic page) 2026-06-09 21:47:52 -04:00
teamchong 99bb931014 chore: rename all pixelpipe references to pxpipe
- code, tests, docs, README, wrangler.toml, help text
- data dir ~/.pixelpipe -> ~/.pxpipe (PXPIPE_LOG env var)
- rebuilt dist; 323/323 tests
2026-06-09 19:04:09 -04:00
teamchong 3d0f9ef08a scope: narrow model gate to fable-5, drop opus; switch dense render to bare 5x8 cell
- gate regex: fable-5 only (opus 4.7/4.8 read tax gone on fable: 100/100 novel-arithmetic, identical image billing)
- remove dead opus-4.6 cpt fork in transform.ts
- DENSE_RENDER_STYLE: 7x10 padded -> bare 5x8 cell (~42% fewer image tokens/page, recall flat in A/B)
- dashboard: compression defaults ON
- README/FINDINGS/TRANSFORM_INFO updated with 2026-06-09 measurements
2026-06-09 18:33:34 -04:00
teamchong c1d111a083 feat(transform): per-bucket char attribution telemetry (Task #18 Phase 1)
Adds the data collection half of the adaptive chars-per-token plan
without touching the gate. Every isCompressionProfitable call site
now credits its chars to a named bucket on TransformInfo, and those
counts flow through toTrackEvent into the persisted JSONL.

transform.ts:
- New BucketName union: 'static_slab' | 'reminder' |
  'tool_result_{structured,log,prose}' | 'history'
- bumpBucket() helper lazily allocates info.bucketChars
- toolResultBucket() routes by classifyContent shape so the post-
  compaction text we credit matches the shape the gate evaluated
- Bump sites: static slab (combinedRaw), reminder (reminderRaw),
  tool_result single-string (innerRaw), tool_result multi-block
  per TextBlock (innerTextRaw), history (collapsedChars)
- Surfaces historyTextChars separately so history's no-collapse
  rejections still contribute a denominator

tracker.ts:
- TrackEvent gains bucket_chars?: Partial<Record<BucketName, number>>
  and history_text_chars?: number
- toTrackEvent copies them when populated, drops empty bucket maps
  so happy-path events stay small

tests/tracker.test.ts:
- Asserts snake_case field names and nested shape land correctly
- Asserts absent buckets and pass-through events stay clean

No gate logic changed. CHARS_PER_TOKEN / SLAB_CHARS_PER_TOKEN /
HISTORY_CHARS_PER_TOKEN still drive isCompressionProfitable. Phase 2
will fit per-bucket slopes from the captured events and wire a
bucket-aware override.

docs/ADAPTIVE_CPT_PLAN.md: full plan + empirical basis (slab-only
regression on 87 events recovered cpt≈1.50 vs the baked 2.5
constant — 40% on non-slab compressions ship today).

Verified: tsc --noEmit clean; 290/290 tests pass (+2 new);
build emits dist/node.js.
2026-05-20 21:21:34 -04:00
teamchong b49764e64c docs: add TRANSFORM_INFO.md (compression strategy reference)
A single canonical document explaining how pixelpipe compresses Claude
Code requests end-to-end. Covers, in order:

  1. Why this proxy exists
  2. The static / dynamic split (DYNAMIC_BLOCK_TAGS, KNOWN_STATIC_TAGS)
  3. What the final request looks like (placement='user' layout)
  4. The cache_control budget (the one invariant that matters: ttl='1h')
  5. Per-message compressions (reminders + tool_results, no cache_control)
  6. Determinism and fingerprints (systemSha8, claudeMdSha8, firstUserSha8)
  7. The unknown-tag canary
  8. The savings math (effectiveCost / baselineCost from src/dashboard.ts)
  9. What deliberately did NOT get built
 10. Wiring map

Intended as the file a new contributor reads after CLAUDE.md but before
touching src/core/transform.ts. HANDOFF.md is session-state and rotates;
README.md is install/run; CLAUDE.md is rules; this is the architecture
reference that explains WHY the code looks the way it does.

Authored by the doc-writer teammate, KNOWN_STATIC_TAGS section
post-edited to reflect that e219b37 landed (not "future commit"). The
"source of truth" line in section 8 points at src/dashboard.ts directly
now that the Python reference has been removed (be372a0).

334 lines. No code changes; gates not re-run since this is doc-only.
2026-05-18 17:57:22 -04:00