Skip to content

Instantly share code, notes, and snippets.

@Anemll
Last active September 20, 2026 16:32
Show Gist options
  • Select an option

  • Save Anemll/f363a783c745b6262fa156e4c9760b14 to your computer and use it in GitHub Desktop.

Select an option

Save Anemll/f363a783c745b6262fa156e4c9760b14 to your computer and use it in GitHub Desktop.
Pi: keep the newest four images for DeepSeek Spark requests without changing saved history

Pi image-history filter for DeepSeek on Spark

Work around this vLLM error without restarting the model server:

At most 4 image(s) may be provided in one prompt. (parameter=image)

Pi sends images from retained conversation history, not just the latest turn. This extension keeps the newest four images in each outgoing request and replaces older image blocks with text markers. Both user attachments and tool-result screenshots are covered. Saved session history, image files, model settings, and the server are left unchanged.

This is especially useful for computer-use and app-development agents that capture screenshots repeatedly. A screenshot taken one at a time can still accumulate with earlier screenshots in the next model request. Without a retention policy, image history can consume the context needed for code, instructions, logs, and tool results.

Install on the computer running Pi

  1. Download pi-spark-image-history.ts from this gist using Raw, saving it as ~/Downloads/pi-spark-image-history.ts (not an HTML page).

  2. Copy it into Pi's extension directory:

    mkdir -p ~/.pi/agent/extensions
    cp -i ~/Downloads/pi-spark-image-history.ts \
      ~/.pi/agent/extensions/pi-spark-image-history.ts
  3. In your existing Pi session, run:

    /reload
    /spark-images
    
  4. Confirm the command reports active, then continue the conversation.

New Pi processes load the extension automatically. No npm package or runtime dependencies are needed. Updating Pi alone does not install this workaround. As with any Pi extension, review the source before installing it: extensions run with your user's permissions.

Which models are affected?

The extension deliberately applies only to this profile:

Field Value
Model ID DeepSeek-v4.1-Flash-EXL3
API openai-completions
Provider ID gx10-dspark or gx10-vllm

If your provider has another ID, adjust the allowlist in isSparkModel() in the extension and run /reload. Do not rename a working model profile just to match the extension. The Droid/Gemini alias and other providers are not affected. MAX_IMAGES = 4 must match the server's allowed image count.

Check the DeepSeek Pi vision configuration

The four-image cap is model-specific, not a global limit on all Pi models. The extension checks the exact model ID DeepSeek-v4.1-Flash-EXL3, the API, and the provider allowlist before changing an outgoing request. Other model IDs are untouched, even under the same provider.

In ~/.pi/agent/models.json, check the matching entry in your existing provider's models array. It must declare image input:

{
  "id": "DeepSeek-v4.1-Flash-EXL3",
  "api": "openai-completions",
  "input": ["text", "image"]
}

This is a partial model entry, not a replacement for your settings file. Preserve your existing provider ID, endpoint, credentials, context window, output limit, reasoning settings, and other models. The API may also be inherited from the provider instead of repeated inside this entry.

Also check images.blockImages in ~/.pi/agent/settings.json and the current project's .pi/settings.json: it must not be true for image requests. images.autoResize controls image dimensions, not image count.

Pi's input field enables vision; the extension's MAX_IMAGES = 4 enforces the four-image request budget. Do not add an invented maxImages field to models.json. The model server must already accept image input and four images.

If you changed the model entry, open bare /model and reselect it to reload the model configuration. Use /reload for extension changes, then /spark-images to confirm the filter is active. Already-correct profiles need no settings changes.

Image size: tokens, RAM, and why four images

These are approximate, upper-end estimates for this DeepSeek V4.1 Flash deployment, not universal costs for every Pi model. The model's image-token budget is 1,024 per image; its scheduler conservatively allows 1,025 tokens including a margin. Smaller processed images can use fewer tokens. JPEG/PNG file size does not directly determine token cost.

Images retained in a request Approximate image-token budget Preprocessed image tensors, one resident copy
1 1,025 ~10 MiB
4 4,100 ~40 MiB
16 16,400 ~160 MiB
100 102,500 ~1 GiB

With a 240,000-token context, 100 large images can use about 43% of the context. After reserving 32,768 tokens for output, that leaves roughly 105,000 tokens for text, code, system instructions, and tool history. At 1,000 large images, the image budget alone is approximately one million tokens: far beyond this context window. Rows above four illustrate the cost of unbounded history; they are not tested serving limits for this deployment.

The RAM column estimates the BF16 patch tensors produced by the installed processor: up to roughly 9,000 patches x 3 channels x 14 x 14 pixels x 2 bytes, or about 10 MiB per large image per copy. It is not the full request peak:

  • A decoded RGB image alone is about 5.9 MiB at 1920x1080, or 11.4 MiB at 2000x2000; image libraries may use additional storage.
  • Encoded/base64 request bodies, processor-cache copies, transport buffers, and temporary vision/prefill activations add overhead. API and engine processor caches can duplicate data. See vLLM processor-cache documentation.
  • For reference, this deployment's reserved 2.5 GiB KV pool per Spark has a reported capacity of 729,200 tokens. 102,500 image tokens represent roughly 0.35 GiB of KV-pool-equivalent capacity per Spark. This is an average capacity estimate, not a measurement of marginal allocation: that pool is already reserved and does not allocate another 0.35 GiB just for the request.
  • Chunked scheduling does not run all 100 full-size images through the vision encoder simultaneously. Do not multiply a single-image activation peak by 100 to predict GPU memory. Conversely, CPU-side images and preprocessing data may accumulate before those chunks execute.

Several GiB of request/preprocessing overhead is plausible for 100 large images, but the total peak has not been measured. Merely raising an image limit does not consume those tokens or load 100 copies of the vision tower; the actual request contents drive the cost.

Four images provide a bounded recent visual history while leaving most of the context for the coding task. This also avoids the server's four-image validation error. It does not guarantee faster prefill: replacing old image blocks can invalidate part of a cached prefix, as noted below.

Continuous computer use: Anemll macOS Agent

Anemll macOS Agent v0.2.1 — Multi-Client Skill Installer is a companion for Pi and other coding agents to control or assist with computer use, especially macOS app development, debugging, and UI testing. The local menu-bar host offers Accessibility inspection, mouse/keyboard actions, screenshots, OCR, and REST/MCP interfaces. The release adds skill and MCP setup for Pi, Droid, Claude, Codex, and Grok, including automatic pi-mcp-adapter installation when Pi is available.

An app-development loop often looks like: inspect the app, perform an action, capture the result, fix the code, rebuild, and capture again. Each capture can become another image in the coding agent's retained conversation. Even though the agent captured only one new screenshot, its next request may resend many older screenshots. A long-running loop can therefore exhaust context or hit the backend's image-count limit.

Recommended capture policy:

  • Prefer compact Accessibility/OCR text when it answers the question; use screenshots for visual layout, rendering bugs, and custom-drawn interfaces.
  • Capture the relevant window or region and resize appropriately instead of repeatedly sending an entire high-resolution desktop.
  • Keep the latest four images in outgoing Pi requests, preserving recent visual evidence without resending the entire screenshot history.
  • Record important older observations as text and re-read/reattach an old screenshot when a later comparison needs it. The filter does not generate those summaries automatically.

The macOS agent supplies the computer-use tools; this extension supplies the Pi-side image-retention policy. It does not change Droid or other clients, which need their own equivalent policy where necessary. See the v0.2.1 usage documentation for setup and the semantic-first workflow.

What you will see

  • /spark-images reports whether the filter is active and the last request's image count. This command does not send an inference request.
  • A footer status such as images 4/7 means four of seven images were sent in the most recently checked request. Custom footers may hide this status.
  • An interactive warning appears once per agent run when images are omitted.

Tradeoffs and limits

  • Older images stay in the saved transcript, but their pixels are not sent to the model. Re-read or reattach an older image to inspect it again. The text marker is not an image summary.
  • This bounds outgoing image history, not on-disk session size or Pi's memory for stored history. Text/tool history also continues growing and still needs normal compaction or a new session during long-running work.
  • More than four newly supplied images are also reduced to the newest four. Submit image comparisons in groups of at most four. Headless requests are filtered too, but there is no interactive warning in headless mode.
  • Replacing an older image changes the prompt prefix and may reduce KV-prefix cache reuse, causing additional prefill. This is not a throughput fix.
  • A later extension hook that adds images can exceed the limit again. The filter covers Chat Completions image_url blocks, not every provider format.
  • The extension does not log image bytes, prompt text, URLs, or credentials.

Verification

Verified with Pi 0.85.1 and 0.86.1 extension loaders. The 16 offline tests cover 0, 1, 4, 5, 7, 100, and 1,000 images, tool-call preservation, immutable inputs, model scoping, and UI failures. A saved-session check using Pi 0.86.1's actual serializer reduced five active-context images to the newest four, without altering the original payload or session file. No inference request was sent by that check; it is not a vision-quality benchmark.

The extension file's SHA-256:

43461497d1df9208d728d3d3b5c123c292bc7802af88b5271acfd62a4ebeee94

Check a downloaded copy with shasum -a 256 pi-spark-image-history.ts.

Disable

Move pi-spark-image-history.ts outside ~/.pi/agent/extensions/, then run /reload. Renaming it to another .ts file in the same folder does not disable auto-discovery.

References: Pi extensions and hot reload, Pi model configuration, Pi image settings.

// SPDX-License-Identifier: MIT
// Install in ~/.pi/agent/extensions/, then /reload in Pi.
// Request-only filtering: never edits transcripts, files, or model settings.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export const MAX_IMAGES = 4;
export const OMITTED_IMAGE = "[Older image omitted from this request by the Spark four-image filter. Saved history is unchanged. Re-read or reattach the image to inspect it again.]";
export function isSparkModel(model: any): boolean {
return model?.id === "DeepSeek-v4.1-Flash-EXL3"
&& model?.api === "openai-completions"
&& ["gx10-dspark", "gx10-vllm"].includes(model?.provider);
}
// Pi serializes both user attachments and tool-result images as image_url
// blocks in Chat Completions messages. Count across the whole request.
export function limitImages(payload: any) {
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
const total = messages.reduce((count: number, message: any) => count + (
Array.isArray(message?.content)
? message.content.filter((part: any) => part?.type === "image_url").length
: 0
), 0);
const removed = Math.max(0, total - MAX_IMAGES);
if (!removed) return { payload, total, removed, kept: total };
let remaining = removed;
const filtered = messages.map((message: any) => {
if (!remaining || !Array.isArray(message?.content)) return message;
let changed = false;
const content = message.content.map((part: any) => {
if (part?.type !== "image_url" || !remaining) return part;
remaining--;
changed = true;
return { type: "text", text: OMITTED_IMAGE };
});
return changed ? { ...message, content } : message;
});
return { payload: { ...payload, messages: filtered }, total, removed, kept: MAX_IMAGES };
}
export default function (pi: ExtensionAPI) {
let warned = false;
let last: { total: number; kept: number; removed: number } | undefined;
const status = (ctx: any, model = ctx.model) => {
if (!ctx.hasUI) return;
ctx.ui.setStatus("spark-image-history", isSparkModel(model)
? (last ? `images ${last.kept}/${last.total}` : "images: latest 4")
: undefined);
};
pi.on("session_start", (_event, ctx) => {
warned = false;
last = undefined;
status(ctx);
});
pi.on("model_select", (event, ctx) => {
warned = false;
last = undefined;
status(ctx, event.model);
});
pi.on("agent_start", () => { warned = false; });
pi.on("before_provider_request", (event, ctx) => {
if (!isSparkModel(ctx.model)) return;
const result = limitImages(event.payload);
last = { total: result.total, kept: result.kept, removed: result.removed };
// A footer/notification failure must not disable request filtering.
try {
status(ctx);
if (result.removed && !warned && ctx.hasUI) {
ctx.ui.notify(`Spark: sending the latest ${MAX_IMAGES} of ${result.total} images; ${result.removed} older image(s) replaced with text in this request only. Saved history is unchanged. Submit new image batches in groups of at most four.`, "warning");
warned = true;
}
} catch { /* UI only; the filtered payload is still returned below. */ }
if (result.removed) return result.payload;
});
pi.registerCommand("spark-images", {
description: "Show the Spark four-image request filter status",
handler: async (_args, ctx) => {
ctx.ui.notify(isSparkModel(ctx.model)
? `Spark image filter active: newest ${MAX_IMAGES} images only; saved history unchanged.`
+ (last ? ` Last request: ${last.kept}/${last.total} images sent.` : " No request checked yet.")
: "Spark image filter loaded but inactive for the selected model/provider.", "info");
},
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment