LLeMbas CLI 1.5.1
ci / check (push) Canceled after 0s

An image counted as half a million tokens: the context estimate measured its
base64 as text, so a 1-3 MB screenshot read as "context at 150-280%" the
moment view_image added it, and the session compacted after every image.
An image now counts by its dimensions (28 px patches, at most 16k tokens, a
flat 1600 when unreadable) everywhere the CLI estimates -- the meter, pruning,
/context and the no-usage fallback of all five dialects. Images over 1600 px
or 500 KB are shrunk to 1600 px when ImageMagick is there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-23 21:24:31 +00:00
1 parent 025b067e38
commit 82918ba5a8
12 files changed
+196 -18

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
import { afterEach, describe, expect, test } from "bun:test"
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { createApp } from "../src/app.ts"
import type { AskReply, Event } from "../src/bus/index.ts"
import { paths } from "../src/config/paths.ts"
import { estimateValue, IMAGE_FALLBACK_TOKENS, imageSize, imageTokens } from "../src/provider/tokens.ts"
import { delta, fakeProvider, toolCall, usage, type Fake } from "./fake-provider.ts"
/** A PNG-shaped file of `bytes` bytes that says it is `w`×`h` (header only; the rest is filler). */
function png(w: number, h: number, bytes: number): Buffer {
const out = Buffer.alloc(bytes, 0x41)
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13, 0x49, 0x48, 0x44, 0x52]).copy(out, 0)
out.writeUInt32BE(w, 16)
out.writeUInt32BE(h, 20)
return out
}
let fake: Fake | undefined
afterEach(() => fake?.stop())
describe("an image counts by its dimensions, never its encoding", () => {
test("sizes are read from PNG, GIF and JPEG headers", () => {
expect(imageSize(png(2560, 1440, 64))).toEqual({ width: 2560, height: 1440 })
const gif = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x40, 0x01, 0xf0, 0x00])
expect(imageSize(gif)).toEqual({ width: 320, height: 240 })
// SOI, an APP0 segment of 16 bytes, then SOF0 with height 600 and width 800.
const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, ...Array(14).fill(0), 0xff, 0xc0, 0x00, 0x11, 0x08, 0x02, 0x58, 0x03, 0x20, 0x03])
expect(imageSize(jpeg)).toEqual({ width: 800, height: 600 })
})
test("a 1.5 MB 2560×1440 screenshot is thousands of tokens, not half a million", () => {
const data = png(2560, 1440, 1_500_000).toString("base64")
expect(imageTokens(data)).toBe(Math.ceil(2560 / 28) * Math.ceil(1440 / 28))
const message = { role: "user", parts: [{ type: "text", text: "look" }, { type: "image", mime: "image/png", data }] }
expect(estimateValue(message)).toBeLessThan(5_000)
// The same image in each dialect's wire shape.
expect(estimateValue([{ content: [{ type: "image_url", image_url: { url: `data:image/png;base64,${data}` } }] }])).toBeLessThan(5_000)
expect(estimateValue([{ content: [{ type: "image", source: { type: "base64", media_type: "image/png", data } }] }])).toBeLessThan(5_000)
expect(estimateValue([{ role: "user", content: "look", images: [data] }])).toBeLessThan(5_000)
})
test("an image whose size cannot be read has a flat cost", () => {
expect(imageTokens(Buffer.alloc(300_000, 7).toString("base64"))).toBe(IMAGE_FALLBACK_TOKENS)
})
test("view_image of a 1.5 MB screenshot at 20% of the window does not compact", async () => {
fake = fakeProvider([
{ chunks: [toolCall(0, "c1", "view_image", '{"path":"shot.png"}'), usage(52_000, 50)] },
{ chunks: [delta({ content: "a screenshot" }), usage(56_000, 5)] },
])
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "connections.yaml"), `connections:\n f:\n dialect: openai-chat\n base_url: ${fake.url}\n models:\n m: { vision: true, context: 262144 }\n`, { mode: 0o600 })
writeFileSync(join(paths.config, "config.yaml"), "model: f/m\n")
const root = mkdtempSync(join(tmpdir(), "ph-img-"))
writeFileSync(join(root, "shot.png"), png(2560, 1440, 1_500_000))
const app = createApp({ cwd: root, mode: "edit", store: false, asker: { ask: async (): Promise<AskReply> => ({ kind: "once" }) } })
const notices: string[] = []
app.bus.on((e: Event) => {
if (e.type === "notice") notices.push(e.message)
})
await app.engine.prompt("look at shot.png")
expect(notices.filter((n) => n.includes("compacting") || n.includes("pruned"))).toEqual([])
expect(fake.requests.length).toBe(2)
})
})