mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
47b5e412b3
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.
61 lines
2.6 KiB
TypeScript
61 lines
2.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import { renderSessionSummaryFragment } from '../src/dashboard/fragments.js';
|
||
import type { StatsPayload } from '../src/dashboard/types.js';
|
||
|
||
/**
|
||
* The hero reads the SAME cache-weighted lifetime pair as the header strip
|
||
* (serveStats), not the per-session payload — so it can't disagree with the
|
||
* "$ saved" tiles and it stops swinging on tiny samples. The old bug divided
|
||
* raw count_tokens (cache-blind) by sent tokens and could claim a big "fewer
|
||
* tokens" win on a session the Saved column showed as a net loss. These pin
|
||
* direction to `baseline_input_weighted` vs `actual_input_weighted`.
|
||
*/
|
||
function payload(p: Partial<StatsPayload>): StatsPayload {
|
||
return {
|
||
compressed_requests: 1,
|
||
output_weighted: 695, // 139 raw × 5 output_multiplier
|
||
pricing_assumptions: { output_multiplier: 5 },
|
||
...p,
|
||
} as StatsPayload;
|
||
}
|
||
|
||
describe('renderSessionSummaryFragment hero', () => {
|
||
it('shows "fewer tokens" when the weighted image beat weighted text', () => {
|
||
const html = renderSessionSummaryFragment(
|
||
payload({ baseline_input_weighted: 7000, actual_input_weighted: 1800 }),
|
||
);
|
||
expect(html).toContain('fewer tokens');
|
||
expect(html).not.toContain('more tokens');
|
||
expect(html).toContain('74%'); // 1 - 1800/7000
|
||
});
|
||
|
||
it('flips to "more tokens" on a warm net-loss session (matches Saved "-")', () => {
|
||
// The exact trap: raw text (e.g. 7.2k) would look like a huge win, but the
|
||
// cache-weighted text baseline (1,546) is below what imaging actually sent (1,863).
|
||
const html = renderSessionSummaryFragment(
|
||
payload({ baseline_input_weighted: 1546, actual_input_weighted: 1863 }),
|
||
);
|
||
expect(html).toContain('more tokens');
|
||
expect(html).not.toContain('fewer tokens');
|
||
expect(html).toContain('hero-neg'); // red styling on a loss
|
||
});
|
||
|
||
it('never lumps output into the headline ratio', () => {
|
||
// Same input pair, wildly different output — headline % must not move.
|
||
const a = renderSessionSummaryFragment(
|
||
payload({ baseline_input_weighted: 2000, actual_input_weighted: 1000, output_weighted: 50 }),
|
||
);
|
||
const b = renderSessionSummaryFragment(
|
||
payload({ baseline_input_weighted: 2000, actual_input_weighted: 1000, output_weighted: 45000 }),
|
||
);
|
||
expect(a).toContain('50%');
|
||
expect(b).toContain('50%');
|
||
});
|
||
|
||
it('renders the warming-up state with no measured requests', () => {
|
||
const html = renderSessionSummaryFragment(payload({ compressed_requests: 0 }));
|
||
expect(html).toContain('Warming up');
|
||
expect(html).not.toContain('fewer tokens');
|
||
});
|
||
});
|