diff --git a/src/core/transform.ts b/src/core/transform.ts index 0e4c33e..69ea5c2 100644 --- a/src/core/transform.ts +++ b/src/core/transform.ts @@ -1402,8 +1402,10 @@ export async function transformRequest( // English-prose default 4 baked into CHARS_PER_TOKEN. Use a slab-specific // upper-bound cpt at this gate so JSON-dense system + tool-doc content // gets a fair break-even check. Host can still override via - // `o.charsPerToken` (e.g., to plug in a live empirical fit). - const slabCpt = o.charsPerToken !== undefined && o.charsPerToken !== CHARS_PER_TOKEN + // `opts.charsPerToken` (e.g., to plug in a live empirical fit). + // Discriminate on the *raw* `opts` so a host that genuinely wants the + // English-prose `4` can pin to it without colliding with the merged default. + const slabCpt = opts.charsPerToken !== undefined ? o.charsPerToken : SLAB_CHARS_PER_TOKEN; if (!isCompressionProfitable(combined, o.cols, undefined, numCols, slabCpt)) { @@ -1701,9 +1703,12 @@ export async function transformRequest( // count after wrapping. // History cpt is empirically ~1.09 (N=10 rejected-events sample) — JSON- // dense like the slab, so use the same conservative upper-bound cpt - // baked into HISTORY_CHARS_PER_TOKEN=2.5. Host override (o.charsPerToken) + // baked into HISTORY_CHARS_PER_TOKEN=2.5. Host override (opts.charsPerToken) // wins if the dashboard ever feeds back a live empirical fit. - const historyCpt = o.charsPerToken !== undefined && o.charsPerToken !== CHARS_PER_TOKEN + // Same discriminator as the slab path: check the *raw* `opts` so a host + // that genuinely wants `4` can pin to it without colliding with the merged + // default. + const historyCpt = opts.charsPerToken !== undefined ? o.charsPerToken : HISTORY_CHARS_PER_TOKEN; const historyProfitable = (text: string, cols: number): boolean => diff --git a/tests/history.test.ts b/tests/history.test.ts index 9fc0d63..b7787ba 100644 --- a/tests/history.test.ts +++ b/tests/history.test.ts @@ -474,14 +474,13 @@ describe('transformRequest history compression (always-on)', () => { expect(info.historyReason).toBe('collapsed'); expect(info.collapsedTurns).toBe(10); - // Counterfactual: a host override slightly above 4 (= "English-prose - // worse than the static default") forces the gate back into rejection - // territory. We use 4.5 rather than 4 because passing exactly the - // DEFAULTS value (4) collides with the `!== CHARS_PER_TOKEN` branch - // and gets re-rewritten to HISTORY_CHARS_PER_TOKEN — by design, so - // unspecified-host requests still get the empirical cpt. 4.5 bypasses - // that override gate and confirms the fix actually flows through the - // cpt argument, not some other side-effect. + // Counterfactual: a host override above 4 (= "English-prose territory, + // but worse than the historical default") forces the gate back into + // rejection — confirms the fix actually flows through the `cpt` + // argument, not some other side-effect. After fragility #2 (override- + // gate default-value collision), the override-gate uses `!== undefined` + // so 4.5 is honored as an explicit override on its own merits, not + // because it differs from any sentinel. const stale = await transformRequest(mkBody(msgs, bigPlain(80_000)), { charsPerToken: 4.5, }); @@ -489,6 +488,38 @@ describe('transformRequest history compression (always-on)', () => { expect(stale.info.collapsedTurns).toBeUndefined(); }); + it('explicit charsPerToken=4 is honored end-to-end (no silent swap to constants)', async () => { + // Regression for fragility #2: the override-gate previously used a + // `!== CHARS_PER_TOKEN` check that silently swapped 4 → SLAB_CHARS_PER_TOKEN + // (2.5) for unspecified hosts. That coupling broke the distinction + // between "host didn't override" and "host deliberately wants 4". The + // gate now uses `!== undefined` so a literal 4 stays a literal 4. + // + // Observable proof: a borderline-density fixture (1200-char bodies × 14 + // turns) that is rejected at cpt=4 but accepted at cpt=2.5. If the gate + // silently swapped explicit 4 → 2.5, this fixture would collapse — but + // with the fix it stays rejected, confirming the gate honored the literal + // 4. The companion test below pins cpt=2.5 collapse on the same shape. + const msgs: Message[] = []; + for (let i = 0; i < 14; i++) { + const body = `turn ${i}: ` + bigPlain(1200); + msgs.push(i % 2 === 0 ? usr(body) : asst(body)); + } + const explicit4 = await transformRequest(mkBody(msgs, bigPlain(80_000)), { + charsPerToken: 4, + }); + expect(explicit4.info.historyReason).toBe('not_profitable'); + expect(explicit4.info.collapsedTurns).toBeUndefined(); + + // Same shape at cpt=2.5 collapses — proves the fixture actually straddles + // the threshold and isn't a tautology. + const explicit25 = await transformRequest(mkBody(msgs, bigPlain(80_000)), { + charsPerToken: 2.5, + }); + expect(explicit25.info.historyReason).toBe('collapsed'); + expect(explicit25.info.collapsedTurns).toBe(10); + }); + it('history-image blocks carry NO cache_control (conservative first-cut)', async () => { const msgs: Message[] = []; for (let i = 0; i < 14; i++) { diff --git a/tests/render.test.ts b/tests/render.test.ts index 51304bd..3b09486 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -1900,6 +1900,44 @@ describe('transform', () => { expect(isCompressionProfitable(slab, 100, undefined, 2, 2.5)).toBe(true); }); + it('TransformOptions.charsPerToken: explicit 4 is honored (no silent swap to SLAB_CHARS_PER_TOKEN)', async () => { + // Fragility #2 regression: the override-gate previously used a `!== + // CHARS_PER_TOKEN` check that silently swapped 4 → 2.5 because the static + // default *also* happens to be 4. After the fix it uses `!== undefined`, + // so passing exactly 4 is honored as an explicit override. + // + // Observable proof: the 161k production-shape slab is REJECTED at cpt=4 + // (text=40,275 tok < image=44,000 tok) and ACCEPTED at the built-in + // SLAB_CHARS_PER_TOKEN=2.5 (text=64,440 tok). If the collision bypass + // breaks, the slab will compress under an explicit `4` — which would + // mean the host can't ever pin to the conservative English-prose value. + const parts: string[] = []; + let acc = 0; + const target = 161_101; + while (acc < target) { + const len = 60 + (acc % 40); + parts.push('A'.repeat(len) + (acc % 200 === 0 ? ' ' : '')); + acc += len + 1; + } + const slab = parts.join('\n').slice(0, target); + const req = JSON.stringify({ + model: 'claude-3-5-sonnet', + messages: [{ role: 'user', content: 'hi' }], + system: slab, + }); + const bytes = new TextEncoder().encode(req); + + // Built-in cpt (no override): slab compresses via SLAB_CHARS_PER_TOKEN=2.5. + const builtin = await transformRequest(bytes, { multiCol: 2 }); + expect(builtin.info.compressed).toBe(true); + + // Explicit cpt=4: host pinned to the English-prose value. The slab gate + // must honor it and reject the slab — not silently fall back to 2.5. + const overridden = await transformRequest(bytes, { multiCol: 2, charsPerToken: 4 }); + expect(overridden.info.compressed).toBe(false); + expect(overridden.info.reason).toMatch(/^not_profitable/); + }); + // --- Adaptive break-even: CHARS_PER_IMAGE derived from atlas cell, not hardcoded --- // Brief: when font-rater swaps to a smaller cell (e.g. Cozette 4×7), more chars // pack into one image, so the N-image break-even thresholds shift. Tests below