fix: remove sparse IDS image blocks

This commit is contained in:
Steven Chong
2026-07-12 19:23:43 -04:00
parent 455ab636a2
commit 208c644f9c
7 changed files with 62 additions and 122 deletions
-49
View File
@@ -251,52 +251,3 @@ export function factSheetText(text: string): string {
const { kept } = extractFactSheetEntriesAllPages(text, FACTSHEET_PAGE_CHARS);
return factSheetTextFromEntries(kept);
}
/** Shape label for an IDS-block line (visual OCR aid, not a factsheet). */
function idsBlockLabel(tok: string): string {
if (SHAPE_HEX.test(tok)) return 'hex';
if (SHAPE_UUID.test(tok)) return 'uuid';
if (SHAPE_CAMEL.test(tok) && tok.length >= 8) return 'camel';
if (tok.includes('/')) return 'path';
if (SHAPE_NUM.test(tok) && tok.length <= 8) return 'port';
if (SHAPE_CONST.test(tok)) return 'const';
if (SHAPE_FLAG.test(tok)) return 'flag';
if (SHAPE_TICKET.test(tok)) return 'code';
return 'id';
}
/**
* Append a short multi-line IDS block of precision-critical tokens so each ID
* sits on its own rendered row (pure-image OCR aid at 5×8). Deterministic.
* Used on every imaged path (Claude + GPT + Grok). Grok pure-image packing
* validated 2026-07-11 (white+ids_block 7/7 4/4); other families get the same
* visual isolation as defense in depth alongside the text fact-sheet.
* Does not replace the text fact-sheet; the IDS block is rasterized into the PNG.
*/
export function appendIdsBlock(text: string, maxIds = 16): string {
if (!text) return text;
if (/\nIDS\n/.test(text) || text.startsWith('IDS\n')) return text;
const entries =
text.length <= MAX_SCAN
? extractFactSheetEntries(text)
: extractFactSheetEntriesAllPages(text, FACTSHEET_PAGE_CHARS).kept;
// Prefer tier-0 (hex, ports, camel, flags), then tier-1 paths — same
// consequence ranking as the fact-sheet budget, but keep a few paths so
// pure-image path probes stay visible as isolated rows.
const tokens: string[] = [];
const seen = new Set<string>();
const push = (t: string) => {
if (seen.has(t) || tokens.length >= maxIds) return;
seen.add(t);
tokens.push(t);
};
for (const { token } of entries) {
if (priorityTier(token) === 0) push(token);
}
for (const { token } of entries) {
if (priorityTier(token) === 1 && token.includes('/')) push(token);
}
if (tokens.length === 0) return text;
const lines = tokens.map((t) => `${idsBlockLabel(t)} ${t}`);
return `${text.replace(/\s+$/, '')}\nIDS\n${lines.join('\n')}\n`;
}
+1 -13
View File
@@ -13,7 +13,7 @@
import type { CacheControl, ContentBlock, ImageBlock, Message, TextBlock, ToolUseBlock, ToolResultBlock } from './types.js';
import { DENSE_CONTENT_CHARS_PER_IMAGE, DENSE_CONTENT_COLS, DENSE_RENDER_STYLE, neutralizeSentinel, reflow, renderTextToPngsWithCharLimit, roleSlotSegment, SLOT_MARK_ASSISTANT, SLOT_MARK_USER } from './render.js';
import { appendIdsBlock, factSheetText } from './factsheet.js';
import { factSheetText } from './factsheet.js';
import { bytesToBase64 } from './png.js';
/**
@@ -596,18 +596,6 @@ export async function collapseHistory(
chunkSlot = safeSlot;
}
}
// Universal pure-image IDS rows (hex/camel/path/port). Append the same
// \nIDS\n... suffix to text + slot so colorByRole wrap lines stay 1:1.
// appendIdsBlock trims trailing whitespace first, so do not slice by old length.
{
const withIds = appendIdsBlock(chunkRender);
if (withIds !== chunkRender) {
const idsAt = withIds.lastIndexOf('\nIDS\n');
const suffix = idsAt >= 0 ? withIds.slice(idsAt) : '';
chunkRender = withIds;
chunkSlot = chunkSlot.replace(/\s+$/, '') + suffix;
}
}
// Use the dense readable profile (not full-canvas) to keep code/config legible.
// colorByRole tints the structural <role> tags so turn boundaries are scannable
// in the history image; it's token-free (vision cost is by pixel dims, not PNG
-7
View File
@@ -29,7 +29,6 @@ import {
type RenderedImage,
type RenderStyle,
} from './render.js';
import { appendIdsBlock } from './factsheet.js';
import { DEFAULT_GPT_PROFILE, GPT_MAX_HEIGHT_PX } from './gpt-model-profiles.js';
import { countTokens as o200kCountTokens } from 'gpt-tokenizer/encoding/o200k_base';
@@ -103,8 +102,6 @@ export interface GptHistoryOptions {
* The returned `text` (o200k baseline + cache byte-stability) stays the
* ORIGINAL, un-reflowed transcript. */
reflow: boolean;
/** Append IDS block for pure-image exact recall (isolated ID rows). Default on. */
idsBlock: boolean;
}
export const GPT_HISTORY_DEFAULTS: GptHistoryOptions = {
@@ -122,7 +119,6 @@ export const GPT_HISTORY_DEFAULTS: GptHistoryOptions = {
style: DEFAULT_GPT_PROFILE.style,
maxImages: GPT_HISTORY_MAX_IMAGES,
reflow: true,
idsBlock: true,
};
/** One conversation item lowered to a renderable unit. */
@@ -359,7 +355,6 @@ export async function planGptCollapse(
// the chunk-snapped cache byte-stability, so it must not change shape here.
const safeText = neutralizeSentinel(text);
let renderText = o.reflow ? reflow(safeText) ?? safeText : text;
if (o.idsBlock) renderText = appendIdsBlock(renderText);
if (!isProfitable(renderText, o.cols)) {
return { ...base, reason: 'not_profitable', collapsedChars: text.length };
}
@@ -438,7 +433,6 @@ export async function planGptCollapse(
if (!sectionText || sectionText.length === 0) continue;
const safeSection = neutralizeSentinel(sectionText);
let sectionRender = o.reflow ? reflow(safeSection) ?? safeSection : sectionText;
if (o.idsBlock) sectionRender = appendIdsBlock(sectionRender);
// Readable portrait strips (≤768px wide) — legible to OpenAI vision, same as
// the static slab. renderTextToPngs caps each PNG at MAX_HEIGHT_PX so a tall
// section pages into N images, all still well under the 10,000-patch budget.
@@ -666,7 +660,6 @@ export async function planResponsesPairCollapse(
const source = pairs.map((pair) => pair.text).join('\n\n');
const safe = neutralizeSentinel(source);
let renderedText = o.reflow ? reflow(safe) ?? safe : safe;
if (o.idsBlock) renderedText = appendIdsBlock(renderedText);
const images = await renderTextToPngs(
renderedText, o.cols, o.style ?? {}, o.maxHeightPx,
);
+10 -14
View File
@@ -9,6 +9,7 @@
import {
renderTextToPngs,
reflow,
neutralizeSentinel,
shrinkColsToContent,
renderCellWidth,
renderCellHeight,
@@ -42,7 +43,7 @@ import {
type GptHistoryOptions,
} from './openai-history.js';
import { HISTORY_SYNTHETIC_INTRO, HISTORY_SYNTHETIC_OUTRO } from './history.js';
import { appendIdsBlock, factSheetText } from './factsheet.js';
import { factSheetText } from './factsheet.js';
import { countTokens as o200kCountTokens } from 'gpt-tokenizer/encoding/o200k_base';
// Per-model GPT rendering + vision-cost profiles (portrait-strip width, image-token
@@ -281,9 +282,6 @@ function gptHistoryOpts(
maxHeightPx: o.gptHistory?.maxHeightPx ?? profile.maxHeightPx,
style: o.gptHistory?.style ?? profile.style,
maxImages: o.gptHistory?.maxImages ?? configuredHistoryMaxImages(model),
// Production path for every family: isolated IDS rows in the image plus the
// adjacent text factsheet. Opt out per request with gptHistory.idsBlock: false.
idsBlock: o.gptHistory?.idsBlock ?? true,
};
}
@@ -302,16 +300,14 @@ function emptyInfo(reason?: string): TransformInfo {
};
}
/** Append IDS block so precision tokens get isolated pure-image rows (all models).
* Production also attaches factSheetText next to images (see slab/history below).
* IDS alone is not enough for Grok exact recall on live multi-seed. */
function prepareImagedRenderText(text: string): string {
return appendIdsBlock(text);
function prepareImagedRenderText(text: string, reflowEnabled: boolean): string {
return maybeReflow(text.trimEnd(), reflowEnabled);
}
function maybeReflow(text: string, enabled: boolean): string {
if (!enabled) return text;
return reflow(text) ?? text;
const safe = neutralizeSentinel(text);
return reflow(safe) ?? safe;
}
function isTextPart(part: unknown): part is OpenAITextPart {
@@ -833,7 +829,7 @@ export async function transformOpenAIChatCompletions(
const firstUser = firstUserText(req);
if (firstUser) info.firstUserSha8 = await sha8(firstUser);
const combined = maybeReflow(compactSlabWhitespace(combinedRaw), o.reflow);
const combined = compactSlabWhitespace(combinedRaw).trimEnd();
if (combined.length < o.minCompressChars) {
info.reason = `below_min_chars (${combined.length} < ${o.minCompressChars})`;
return { body, info };
@@ -845,7 +841,7 @@ export async function transformOpenAIChatCompletions(
? ' The glyph ↵ (U+21B5) marks an original hard line break in content; treat it as a real newline.'
: '';
const header = CHAT_HEADER.replace('\n====', reflowNote + '\n====');
const renderedText = prepareImagedRenderText(header + combined);
const renderedText = prepareImagedRenderText(header + combined, o.reflow);
const profile = resolveGptProfile(req.model);
const maxCols = o.cols ?? profile.stripCols;
const cols = Math.min(
@@ -1055,7 +1051,7 @@ export async function transformOpenAIResponses(
const firstUser = firstResponsesUserText(inputWasString, originalInputString, inputItems);
if (firstUser) info.firstUserSha8 = await sha8(firstUser);
const combined = maybeReflow(compactSlabWhitespace(combinedRaw), o.reflow);
const combined = compactSlabWhitespace(combinedRaw).trimEnd();
if (combined.length < o.minCompressChars) {
info.reason = `below_min_chars (${combined.length} < ${o.minCompressChars})`;
return { body, info };
@@ -1065,7 +1061,7 @@ export async function transformOpenAIResponses(
? ' The glyph ↵ (U+21B5) marks an original hard line break in content; treat it as a real newline.'
: '';
const header = RESPONSES_HEADER.replace('\n====', reflowNote + '\n====');
const renderedText = prepareImagedRenderText(header + combined);
const renderedText = prepareImagedRenderText(header + combined, o.reflow);
const profile = resolveGptProfile(req.model);
const maxCols = o.cols ?? profile.stripCols;
const cols = Math.min(
+3 -5
View File
@@ -35,7 +35,7 @@ import {
ANTHROPIC_SLAB_COLS,
renderTextToPngsWithCharLimit,
} from './render.js';
import { appendIdsBlock, factSheetText } from './factsheet.js';
import { factSheetText } from './factsheet.js';
import { stripSchemaDescriptions, schemaHasStructure } from './schema-strip.js';
import { bytesToBase64 } from './png.js';
import { collapseHistory, HISTORY_SYNTHETIC_INTRO } from './history.js';
@@ -1358,8 +1358,7 @@ export async function textToImageBlocks(
}> {
// Shrink before the numCols branch so gate and renderer see the same canvas width.
// If shrinkage drops below the full width, stay single-col (avoid wasting a divider column).
// IDS block: isolate precision tokens on their own rows (universal pure-image hex aid).
const renderText = appendIdsBlock(text);
const renderText = text;
const effectiveCols = shrinkWidth ? shrinkColsToContent(renderText, cols) : cols;
const effectiveNumCols = effectiveCols < cols ? 1 : numCols;
const imgs =
@@ -1705,8 +1704,7 @@ export async function transformRequest(
columnNoteImg +
reflowNoteImg +
'\n====================== BEGIN RENDERED CONTEXT ======================\n';
// IDS block on the imaged slab (same pure-image isolation as Grok/GPT).
const combinedWithHeader = appendIdsBlock(imageInstructionHeader + combined);
const combinedWithHeader = imageInstructionHeader + combined;
// Shrink the canvas to the longest actual line in what we'll *render*,
// so the gate's prediction and the renderer's output agree at the smallest
// legible width. The banner above sets the natural floor — no separate
-33
View File
@@ -4,7 +4,6 @@ import {
extractFactSheetEntries,
extractFactSheetEntriesAllPages,
factSheetText,
appendIdsBlock,
} from '../src/core/factsheet.js';
describe('factsheet extraction', () => {
@@ -172,35 +171,3 @@ describe('ticket-style codes and occurrence counts', () => {
});
});
describe('appendIdsBlock (pure-image IDS rows for all models)', () => {
it('appends an IDS block with hex, camel, path, and port labels', () => {
const text = [
'Done. The token cache key is a3f9c1e0b7d2. I renamed the field to tokenLedgerShard',
'and moved the tier math into src/core/anthropic-vision.ts. Proxy stays on port 47821.',
].join(' ');
const out = appendIdsBlock(text);
expect(out).toContain('\nIDS\n');
expect(out).toContain('hex a3f9c1e0b7d2');
expect(out).toContain('camel tokenLedgerShard');
expect(out).toContain('path src/core/anthropic-vision.ts');
expect(out).toContain('port 47821');
// original body preserved
expect(out.startsWith(text.trimEnd()) || out.includes('token cache key is a3f9c1e0b7d2')).toBe(true);
});
it('is idempotent — does not double-append', () => {
const text = 'key a3f9c1e0b7d2 path src/core/x.ts port 47821';
const once = appendIdsBlock(text);
expect(appendIdsBlock(once)).toBe(once);
});
it('is deterministic for cache stability', () => {
const text = 'hex a3f9c1e0b7d2 camel tokenLedgerShard path src/a/b.ts port 47821';
expect(appendIdsBlock(text)).toBe(appendIdsBlock(text));
});
it('returns the original text when nothing notable is present', () => {
expect(appendIdsBlock('the quick brown fox')).toBe('the quick brown fox');
});
});
+48 -1
View File
@@ -184,6 +184,30 @@ describe('transformOpenAIChatCompletions (gpt-5.6-sol)', () => {
expect(tools[0]!.function.parameters?.properties?.x?.description).toBeUndefined();
});
it('reflows chat static context that contains a literal newline sentinel', async () => {
const system = ('first line\nsecond line quotes ↵ literally\nthird line\n').repeat(80);
const body = enc.encode(JSON.stringify({
model: 'gpt-5.6-sol',
messages: [
{ role: 'system', content: system },
{ role: 'user', content: 'hello' },
],
}));
const result = await transformOpenAIChatCompletions(body, {
charsPerToken: 1,
minCompressChars: 1,
});
expect(result.info.compressed).toBe(true);
const source = result.info.imageSourceText!;
expect(source).not.toContain('\n');
expect(source).toContain('BEGIN RENDERED CONTEXT ======================↵## SYSTEM MESSAGE↵');
expect(source).toContain('second line quotes ⏎ literally↵third line');
expect(source).not.toMatch(/\s$/);
expect(source).not.toMatch(/↵$/);
});
it('images GPT tool definitions even when there is no instruction context', async () => {
const body = enc.encode(JSON.stringify({
model: 'gpt-5.6-sol',
@@ -325,6 +349,29 @@ describe('transformOpenAIResponses (gpt-5.6-sol)', () => {
expect(tools[0]!.parameters?.properties?.x?.description).toBeUndefined();
});
it('reflows Responses static context that contains a literal newline sentinel', async () => {
const instructions = ('alpha line\nbeta line quotes ↵ literally\ngamma line\n').repeat(80);
const body = enc.encode(JSON.stringify({
model: 'gpt-5.6-sol',
instructions,
input: [{ role: 'user', content: 'hello' }],
}));
const result = await transformOpenAIResponses(body, {
charsPerToken: 1,
minCompressChars: 1,
});
expect(result.info.compressed).toBe(true);
const source = result.info.imageSourceText!;
expect(source).not.toContain('\n');
expect(source).not.toContain('↵IDS↵');
expect(source).toContain('BEGIN RENDERED CONTEXT ======================↵## INSTRUCTIONS↵');
expect(source).toContain('beta line quotes ⏎ literally↵gamma line');
expect(source).not.toMatch(/\s$/);
expect(source).not.toMatch(/↵$/);
});
it('images developer/system items whose content is an input_text part array, not just a string', async () => {
// Responses allows message content as a string OR an array of parts. The
// array form for a developer/system item used to be dropped: not imaged and
@@ -956,7 +1003,7 @@ describe('resolveGptProfile (Claude on Responses)', () => {
describe('resolveGptProfile (Grok)', () => {
it('uses pure-image 5x8 packing with shorter white pages under 768px short side', () => {
// 2026-07-11 pure-image 5x8: white AA + IDS block is the stable 4/4 recipe
// Production uses the pure-image 5x8 white AA profile.
// (7/7 retest). No grid; paperGray 240 confabulates ports. Width stays 768.
const p = resolveGptProfile('grok-4.5');
expect(p.stripCols).toBe(152);