ImageProcessor logs hard-coded format: 'webp' and quality: 80 in its compression stats instead of the configured options
Component
@tarko/context-engineer — multimodal/tarko/context-engineer/src/node/image-processor.ts
Bug description
ImageProcessor advertises compression statistics as one of its features and exposes a
configurable { quality, format } constructor, but the stats line it prints reports two
hard-coded constants instead of the values the instance is actually running with:
// context-engineer/src/node/image-processor.ts @ c2ad42e3
18: * - Provides compression statistics
...
23: constructor(options: { quality?: number; format?: 'webp' | 'jpeg' | 'png' } = {}) {
24: this.compressor = new ImageCompressor({
25: quality: options.quality ?? 5,
26: format: options.format ?? 'webp',
27: });
28: }
...
97: console.log('Image compression stats:', {
98: original: formatBytes(originalSize),
99: compressed: formatBytes(compressedSize),
100: ratio: `${compressionRatio.toFixed(2)}x (${compressionPercentage}% smaller)`,
101: format: 'webp',
102: quality: 80,
103: });So the three byte-size fields are measured from the real buffers, while format and quality
on the very next two lines are literals that never change.
The 80 is not a value ImageProcessor ever resolves: it is the compressor's own default, while
this class's default quality is 5 (:25), and neither field moves when the caller passes options:
// shared-media-utils/src/ImageCompressor.ts
34: public readonly options: ImageCompressionOptions;
39: quality: options?.quality ?? 80,Quality is not a neutral label here either: getPluginsForFormat() (ImageCompressor.ts:63-80)
feeds it straight into the encoder, and reads it back from this.options per format — divided by
100 for png (:64, used at :71-72), passed raw for jpeg (:68) and webp (:76).
Actual vs expected
- Actual: for
new ImageProcessor({ quality: 90, format: 'png' })the server logsformat: 'webp', quality: 80. - Expected: the log reports
format: 'png', quality: 90, i.e. what the instance compresses with.
How is this actually affecting real users?
Both shipped callers construct the processor with quality: 5:
// agent-server/src/api/controllers/queries.ts:10-13
const imageProcessor = new ImageProcessor({
quality: 5,
format: 'webp',
});
// agent-server-next/src/controllers/queries.ts:17-20 (same options, inside a try/fallback block)so every image query through the agent server logs quality: 80 while the encoder is being handed
5. Reachability, hop by hop:
- The query route:
agent-server/src/api/routes/queries.ts:15-17—router.post('/query', queriesController.executeQuery)insideapp.group('/api/v1/sessions', …)(mounted byroutes/index.ts:19) agent-server/src/api/controllers/queries.ts:38export async function executeQueryagent-server/src/api/controllers/queries.ts:57await imageProcessor.compressImagesInQuery(query)(also:116inexecuteStreamingQuery, andagent-server-next/src/controllers/queries.ts:87/:162)context-engineer/src/node/image-processor.ts:45-54maps eachimage_urlpart →:49this.compressImageUrl(part)image-processor.ts:97-103prints the stats line with the two constants.
ImageProcessor is part of the package's public surface
(context-engineer/src/node/index.ts:3 export * from './image-processor';), so this is not only
reachable through the two servers above. The log line is the only place a caller can see what the
processor decided to compress with: ImageCompressor.getOptionsDescription()
(ImageCompressor.ts:85-89) does report the live options, but ImageProcessor keeps its
compressor in a private field (image-processor.ts:21), so that check is not available either.
Reproduction
The stats line is observable without any encoder binary, because it is emitted after
compressToBuffer() resolves — a prototype spy on that one method is enough. Against main @
c2ad42e3 (with shared-media-utils built locally so the package's tests collect), a
tests/image-processor.test.ts covering the reported stats fields and the two fallback paths:
$ cd multimodal/tarko/context-engineer && CI=true ./node_modules/.bin/vitest run tests/image-processor.test.ts
❯ tests/image-processor.test.ts (7 tests | 3 failed) 35ms
× ImageProcessor > compression statistics > reports the quality the processor was constructed with 19ms
→ expected 80 to be 5 // Object.is equality
× ImageProcessor > compression statistics > reports the default quality when constructed without options 2ms
→ expected 80 to be 5 // Object.is equality
× ImageProcessor > compression statistics > reports the configured format instead of a fixed one 4ms
→ expected 'webp' to be 'png' // Object.is equality
Test Files 1 failed (1)
Tests 3 failed | 4 passed (7)The four passing cases are controls (byte sizes measured from the real buffers, one stats line per image in a query, the original part returned when compression rejects, and non-data-URI URLs left untouched) — they pass before and after, so the fix stays confined to the two reported fields.
Proposed fix
Resolve quality/format once in the constructor into a private readonly options, pass that
object to ImageCompressor (identical values to today, so compression behaviour does not change),
and log this.options.format / this.options.quality. The source diff is 8 additions / 4
deletions in one file.
Two adjacent problems are deliberately left out of scope, since fixing them needs a decision rather than a correction:
image-processor.ts:91labels the resultdata:image/webp;base64,regardless of the configured format — another literal. Deriving it fromthis.options.formatwould be closer but still not always right: all three plugins return their input unchanged when the bytes do not match what they encode ([email protected]/index.js:11-12for unreadable input,[email protected]/index.js:15-16for non-PNG,[email protected]/index.js:18-20for non-JPEG), so the configured format does not determine the returned bytes. Reporting the produced format needs the compressor to inspect the buffer it hands back (imagemin's own file-based API does this withfile-type).image-processor.ts:82strips the data-URI prefix with/^data:image\/\w+;base64,/, which does not matchimage/svg+xmlbecause+is outside\w— while the guard at:77(startsWith('data:image/')) does accept it. Measured with node: for adata:image/svg+xml;base64,…URL thereplace()is a no-op, so the 76-byte SVG is decoded from a string that still carries the 26-characterdata:image/svg+xml;base64,prefix and comes back as 93 different bytes (dec.equals(svg) === false). Those bytes are what gets handed to the encoder and whatoriginalSizeis measured from, so an SVG part produces a stats line about a buffer that never existed. Changing the regex alters which parts get compressed, so it belongs in a separate discussion.
Environment
- Branch/commit:
main@c2ad42e3 - OS: Windows 11 (the defect is OS-independent — it is two literals in a
console.log)
Happy to send the fix as a PR — I plan to work on it myself, so posting this to claim it. Before
writing anything I checked all 137 open PRs (file lists, paginated) at 2026-09-23T20:29Z: none
touches context-engineer/src/node/image-processor.ts or adds a test for it.
Source: bytedance/UI-TARS-desktop