cyberine
UsageRun recipes

Vision / multimodal tasks

Loop workers can forward image attachments to vision-capable models. Images are read from disk and sent as OpenAI-style imageurl content parts (base64 data URLs). The router automatically excludes...

Loop workers can forward image attachments to vision-capable models. Images are read from disk and sent as OpenAI-style image_url content parts (base64 data URLs). The router automatically excludes text-only workers when a task carries images, so a vision task is never accidentally dispatched to a model that cannot see it.

cyberine run --image <path> (ke9kx)

The one-command path takes images as a repeatable flag; png/jpg/gif/webp, the file must exist:

cyberine run -p --image ./shot.png "Which button is highlighted in this screenshot?"
cyberine run --image ./a.png --image ./b.png "What changed between these two screenshots?"
cyberine run --bg --image ./mockup.png "Describe the layout of this mockup"   # persisted under <runDir>/attachments/

What it does, measured 2026-09-07: with no --fleet, the build-fleet auto-router stands down and the run is pinned to the vision-capable build workers available on this machine (muse here -- WORKER_PROFILES[id].multimodal); the run then takes the single-step compare path, and orchestrate() drops every worker whose AgentInfo.acceptsImages is false (text-only loop workers, CLI workers without a native image flag) with a NOTE naming them, so a text-only worker can never answer "without the image" and look like a pass. A neutral filename PNG (left half red, right half blue) through muse came back "The left half is red and the right half is blue." --image refuses a remote server (CYBERINE_SERVER_URL).

Known Limitations and Deferred Work: the implement-fleet shape (--fleet/CYBERINE_BUILD_FLEET inside a git repo) reads CYBERINE_TASK and does not carry Task.images, so an image task never gets per-worker worktrees or DiffJudge -- it always runs the single-step compare path and says so. PDF, audio and video have no adapter support and are not accepted by the flag. Native SDK adapters (Anthropic, Bedrock, Vertex) leave acceptsImages undefined and are kept in the pool unfiltered.

Mark a task with image attachments by passing images: [{ path: 'relative/path.png' }] on the Task object, or use the CYBERINE_IMAGES env var with npm run cli:

# One image
CYBERINE_IMAGES=./docs/screenshot.png \
  CYBERINE_BUILD_FLEET=loop-openai-build \
  OPENAI_API_KEY=sk-... \
  npm run cli -- "describe the UI element in this screenshot"

# Multiple images (comma-separated)
CYBERINE_IMAGES=./docs/a.png,./docs/b.png \
  CYBERINE_BUILD_FLEET=loop-gemini-build \
  GEMINI_API_KEY=... \
  npm run cli -- "what changed between these two screenshots?"

Vision-capable build-fleet workers (all LoopAgentAdapter-based) include:

WorkerDefault model
loop-openai-buildgpt-4o
loop-gemini-buildgemini-2.5-pro
loop-xai-buildgrok-3
loop-azure-buildgpt-4o
loop-copilot-buildgpt-4o
loop-kimi-buildkimi-k2.7-code
loop-claude-buildanthropic/claude (via LiteLLM gateway)
loop-gpt-buildopenai/gpt (via LiteLLM gateway)

CYBERINE_BUILD_FLEET is required because the default compare fleet is text-only (DeepSeek/OpenRouter etc.). The router also works on Task objects directly:

import { routeTask } from './src/core/router.ts';
import { availableBuildWorkerIds } from './src/orchestrator/buildAdapters.ts';

const decision = routeTask(
  { prompt: 'describe this screenshot', images: [{ path: './screenshot.png' }] },
  availableBuildWorkerIds(),
);
// decision.workers contains only vision-capable ids; text-only workers are excluded.

When a loop worker is flagged as multimodal it forwards the images; otherwise it omits them and appends a short note so the model knows attachments were dropped. Set detail: 'low' or 'high' on a TaskImage to control resolution ('auto' is the default).

DeepSeek vision (T680). DeepSeek's only vision-capable id is deepseek-v4-flash-vision-exp (shipped 2026-08-21) — deepseek-v4-pro and deepseek-v4-flash both return HTTP 400 "This model does not support image". Pinning DEEPSEEK_MODEL=deepseek-v4-flash-vision-exp (bare form for DeepSeek-direct, or deepseek/deepseek-v4-flash-vision-exp when routed through OpenRouter/ opencode) makes loop-deepseek-direct and loop-deepseek-build resolve multimodal: true and forward images normally:

CYBERINE_IMAGES=./docs/screenshot.png \
  DEEPSEEK_MODEL=deepseek-v4-flash-vision-exp \
  DEEPSEEK_API_KEY=sk-... \
  npm run cli -- "describe the UI element in this screenshot"

Discoverable in the catalog (T687). deepseek-v4-flash-vision-exp is now a real entry in src/core/modelCatalog.ts, so cyberine models deepseek lists it and cyberine model use deepseek deepseek-v4-flash-vision-exp no longer prints the dim not in catalog warning. It is listed LAST on purpose — deepseek-v4-pro stays the provider default.

Vision cost (T683). deepseek-v4-flash-vision-exp is priced in src/core/modelPricing.ts identically to deepseek-v4-flash on every row — $0.22/$0.66 per 1M input(cache-miss)/output off-peak, $0.44/$1.32 peak, same 2500 concurrency limit — so there is no vision surcharge and the TUI cost chip, the oracle tool, and the daily spend cap all bill a vision dispatch correctly. Images need no separate accounting: DeepSeek converts them to tokens by dimension and bills them as ordinary input tokens alongside the text (live-confirmed — a real dispatch's usage carries no image-token field of any kind). Verify with:

npm run selftest:deepseek-peak-pricing   # sections 10-11 cover the vision row + the pinned live usage object

Known limitation / deferred work: deepseek-v4-flash-vision-exp itself still has no verified cache-HIT rate in modelPricing.ts (k92fe added the cachedInputCostPerMTok field to the table and filled it for the live deepseek-flash / deepseek-v4-pro rows this id used to mirror, but the id is dealiased server-side — see the T680/k1k86 notes above — and was left unpriced for cache rather than guessed), so a heavily-cached vision session still bills every input token at the cache-miss rate. Also unmodelled: FIM Completion (Beta) is "Not supported" on the vision model (flash and pro both support it in non-thinking mode) — recorded in docs/providers/deepseek.md but not enforced anywhere in code, so pinning the vision id and asking for an FIM completion fails at the provider, not at a Cyberine guard.

Known limitation / deferred work: the multimodal flag is a construction-time flag only — DEEPSEEK_MODEL must be pinned to the vision id BEFORE dispatch. loop-deepseek-build's WorkerProfile entry deliberately carries no static multimodal: true (that would let an image task route to it even when the router resolves its reasoning/flash tier variant, deepseek-v4-pro / deepseek-v4-flash, neither of which accepts images). So DeepSeek is NOT yet in the "Vision-capable build-fleet workers" table above and the router's automatic multimodal gate (routeTask) does not route to it — an image task with no other vision worker available will not auto-select DeepSeek even when the vision id is pinned. That routing gap is the separate, user-gated T689.