mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
fix: remove sparse IDS image blocks
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user