Back

What text agents lack when they encounter images#

Some agents built primarily for text and code cannot read images directly. When a user provides an error screenshot, a UI mockup, or a chart, an agent that receives only the file path cannot assess the facts inside the image.

A common approach is to have a vision model generate a long description and pass it to the main model. But the main model usually needs only a small amount of evidence relevant to the current task, such as error text, coordinates, button states, or chart values.

Why describing the whole image first is expensive#

A common solution sends the image to a vision model, generates a detailed description, and feeds it back to the main model:

Image → Vision model → Long description → Main model
plaintext

This approach creates several direct problems:

ProblemImpactData
High token usage2000-5000 tokens per callHigh cost
Too much irrelevant descriptionThe vision model outputs large amounts of unnecessary contentContext pollution
Context pollutionLong descriptions fill the main model’s contextReduced performance
Reasoning beyond its roleThe vision model makes decisions for the main modelUnclear responsibility

A practical example#

Suppose a user uploads a 400x300 error screenshot.

Traditional approach:

Vision model output: "This image shows a terminal window containing
a Python error message. The error type is ModuleNotFoundError, and
the message says that the module named 'requests' could not be found.
The error occurs on line 42 of /Users/user/project/app.py..."
→ About 3000 tokens
plaintext

Free Vision Skill:

VEP/1|src=zhipu/glm-4.6v-flash|m=error|a="ModuleNotFoundError: No module named 'requests'"|t="app.py:42"|c=0.97
→ About 130 tokens
plaintext

This example drops from about 3000 tokens to about 130, a reduction of roughly 95.7%. It represents only this screenshot and this set of prompts.


How Free Vision Skill works#

Free Vision Skill compresses a vision model’s output into an evidence packet for the current task, then passes it to the text agent to continue reasoning.

Responsibility boundaries#

Workflow#

Image + Question
  ↓
Free vision API, extracting only facts needed for the current task
  ↓
Compress into a VEP, or Visual Evidence Packet
  ↓
DeepSeek / Codex / Claude Code / OpenCode continues reasoning
plaintext

Current implementation#

CapabilityCurrent implementation
Visual evidence compressionVEP/1 compresses answers, OCR text, objects, issues, and key values into short records
Provider fallbackSelects providers by region and priority, switching to backups on failure
Local cacheSHA-256 keys, TTL, and LRU eviction
Credential storageSupports macOS Keychain, Linux Secret Service, and Windows Credential Manager
Concurrency controlRequestPool limits concurrent requests and retries with backoff
13Providers
24 hCache TTL
3Maximum concurrency
Project configuration recorded in this article

Core technology: the VEP/1 protocol#

VEP = Visual Evidence Packet

The vision model returns facts, not a complete analysis.

VEP format#

VEP/1|src=zhipu/glm-4.6v-flash|m=error|
a="Cannot find module ethers"|
t="src/app.ts:42"|
e=[dependency error]|
c=0.97
plaintext

Field definitions#

FieldMeaningExample
VEP/1Protocol versionVEP/1
srcProvider and modelzhipu/glm-4.6v-flash
mTask modeerror / ocr / ui / chart
aDirect answer"Cannot find module"
tOCR text"src/app.ts:42"
oKey objects[button, input, modal]
eVisible errors[overlapping, clipped]
vKey values["$99", "2024-12-31"]
cConfidence0.97
cacheCache statuscache=hit

Token comparison#

ScenarioVEP sizeMain model inputTraditional approachSavings
Error extraction~150 chars~50 tokens2000+ tokens97%
UI audit~400 chars~80 tokens3000+ tokens97%
Table OCR~500 chars~120 tokens4000+ tokens97%
Chart analysis~300 chars~70 tokens2500+ tokens97%

Main model input tokens in project examples

The traditional approach uses the table's lower bounds of 2000+, 3000+, 4000+, and 2500+. Its bars show conservative lower bounds, not exact measurements.

Error VEP50 tokens
Project examples in this article. VEP values and traditional lower bounds come from the table above.

Architecture#

System architecture#

flowchart TB
    C[CLI commands] --> P[Provider registry]
    P --> K[Cache lookup]
    K --> Q[Request pool and rate limit]
    Q --> V[Vision API]
    V --> E[VEP encoder]
    E --> O[Compact evidence packet]
mermaid

Caching and concurrency control operate around provider calls. VEP encoding handles only the vision results that have already returned. It does not make subsequent decisions for the main model.

Core modules#

Provider system (src/providers.ts)#

// Configuration for 13 providers
export function resolveProviderOrder(requested: string, region: Region): ProviderConfig[] {
	if (requested !== 'auto') return [getProvider(requested)];

	// Automatic fallback: prefer the same region, then use fallback providers
	const preferred = registry.providers
		.filter((p) => p.region === region)
		.sort((a, b) => a.priority - b.priority);

	const fallback = registry.providers
		.filter((p) => p.region !== region)
		.sort((a, b) => a.priority - b.priority);

	return [...preferred, ...fallback];
}
typescript

Smart cache (src/cache.ts)#

Concurrency control (src/pool.ts)#

class RequestPool<T> {
	// Retry with exponential backoff
	private async execute(item): Promise<void> {
		for (let attempt = 0; attempt <= maxRetries; attempt++) {
			try {
				const value = await this.withTimeout(fn(), timeoutMs);
				return resolve({ success: true, value });
			} catch (error) {
				// Exponential backoff: 1000ms → 2000ms → 4000ms
				const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
				await sleep(delay);
			}
		}
	}
}
typescript

VEP generation (src/vep.ts)#

export function toVep(result: VisionResult, maxChars: number): string {
	const parts = [
		'VEP/1',
		`src=${result.provider}/${result.model}`,
		`m=${result.mode}`,
		result.answer ? `a="${result.answer}"` : '',
		result.text ? `t="${result.text}"` : '',
		result.issues?.length ? `e=[${result.issues.join(',')}]` : '',
		`c=${result.confidence?.toFixed(2)}`
	]
		.filter(Boolean)
		.join('|');

	return compact.slice(0, maxChars);
}
typescript

Local performance records#

Health check optimization#

The 65-second and roughly 2-second figures below come from a local project example. It shows how serial probes can be replaced with limited concurrency. It does not mean a 32.5x speedup is achievable across different networks or providers.

Before optimization, v0.3 and earlier:

// Serial checks, each with a 5s timeout
for (const provider of providers) {
	await checkProviderHealth(provider); // 5s each
	await sleep(1000); // Delay between batches
}
// 13 providers × 5s = 65s
typescript

After optimization, v0.4:

// Check concurrent batches
const batchSize = 3;
for (let i = 0; i < providers.length; i += batchSize) {
	const batch = providers.slice(i, i + batchSize);
	await Promise.all(batch.map((p) => checkProviderHealth(p)));
	await sleep(500); // Delay between batches
}
// 13 providers: [3 + 3 + 3 + 1] = ~2s

// Local example: 65s -> ~2s
typescript

Cache performance#

The cache hit rate depends on whether images, questions, and providers repeat. The 7/8 result below comes from a very small project test. It is not a promise of a steady 90% hit rate.

TTL + LRU strategy:

TTL: 24 hours
Maximum entries: 1000
LRU eviction: least accessed first
plaintext

Measured data:

Hit Rate:     87.5% (7/8)
Misses:       1
Evictions:    0
Size:         8 entries
plaintext

Concurrency control#

RequestPool configuration:

{
  maxConcurrency: 3,    // Maximum concurrency
  timeoutMs: 30000,     // Timeout
  maxRetries: 2,        // Maximum retries
  baseDelayMs: 1000,    // Base delay
  maxDelayMs: 10000     // Maximum delay
}
typescript

Exponential backoff strategy:

Attempt 1: Failure → Delay 1000ms
Attempt 2: Failure → Delay 2000ms
Attempt 3: Failure → Give up (total: 3000ms)
plaintext

Practical use cases#

1. Analyze an error screenshot#

free-vision see --image ./error.png \
  --question "Extract only the error message and line number"
bash

VEP output:

VEP/1|src=zhipu/glm-4.6v-flash|m=error|
a="Cannot find module 'lodash'"|
t="webpack.config.js:15"|
e=[module resolution error]|
c=0.98
plaintext

The main model continues reasoning: “The error says the lodash module cannot be found at webpack.config.js:15. Solution: run npm install lodash.”


2. Review a UI#

free-vision see --image ./ui-screenshot.png \
  --question "List all clipped, overlapping, or disabled UI elements"
bash

VEP output:

VEP/1|src=zhipu/glm-4.6v-flash|m=ui|
o=[{name:"Submit",issue:"disabled"},{name:"Avatar",issue:"clipped"}]|
c=0.95
plaintext

3. Extract a table with OCR#

free-vision see --image ./table.png \
  --question "Extract all text and the table structure"
bash

VEP output:

VEP/1|src=zhipu/glm-4.6v-flash|m=ocr|
a="Q3 sales report"|
t=["Product","Sales","Growth rate"],["A",12000,"15%"],["B",8500,"8%"]|
c=0.92
plaintext

4. Extract chart data#

free-vision see --image ./chart.png \
  --question "Return only the chart title, trend, and three key values"
bash

VEP output:

VEP/1|src=zhipu/glm-4.6v-flash|m=chart|
a="Monthly revenue growth"|
v=[45200,58300,72100]|
c=0.96
plaintext

5. Optimize with automatic cropping#

free-vision see --image ./screenshot.png --auto-crop \
  --question "Extract only the error message"
bash

Cropping result:

✂️  Cropped: 400x300 → 369x58
   Reduction: 82%
   Saved to: ./screenshot.cropped.png
plaintext

Result: the image size is reduced by 82%, further lowering token usage.


Supported AI agents#

Free Vision Skill is not tied to a particular main model. It suits any text-only agent.

Claude Code#

# The Claude Code Hook automatically detects images
npx skills add lora-sys/free-vision-skill
bash

Codex#

# One-command installation script
curl -fsSL https://raw.githubusercontent.com/lora-sys/free-vision-skill/main/installers/codex-install.sh | bash
bash

OpenCode#

{
	"agents": {
		"coder": {
			"skills": ["free-vision"],
			"vision": {
				"provider": "auto",
				"region": "cn",
				"auto-detect-images": true
			}
		}
	}
}
json

DeepSeek / others#

# General-purpose invocation
free-vision see --image ./screenshot.png --question "Your question"
bash

Development history and plans#

v0.1.0 MVP, completed#

  • Provider registry with 13 providers
  • VEP/1 protocol
  • Automatic fallback
  • SHA-256 local cache
  • .env and Keychain

v0.2 Integration improvements, completed#

  • Claude Code Hook smart detection
  • Provider health checks
  • Codex one-command installation script
  • OpenCode agent integration

v0.3 Advanced features, completed#

  • Windows Credential Manager
  • VEP Schema Validator
  • Image auto-crop with —auto-crop

v0.4 Performance optimization, completed#

  • Cache TTL + LRU eviction
  • Request pool with configurable concurrency
  • Rate limiter using a token bucket
  • Exponential backoff retry
  • Parallel failover for providers
  • Performance tests, 30/30 passing

v1.0 Production readiness, planned#

  • Comprehensive error handling and recovery
  • Full test coverage, >80%
  • Performance monitoring and logging
  • Enterprise-grade security audit
  • Complete API documentation
  • Plugin system

Where it fits#

Free Vision Skill suits situations where the main model cannot read images directly but the task needs a small amount of verifiable information from screenshots, UIs, tables, or charts. Its main purpose is to shorten the visual context sent back to the main model and handle providers, caching, and failover in one tool.

The token, cache, and latency figures in this article all come from project examples or local tests. They help explain implementation trade-offs, but they are not fixed performance guarantees across models or network conditions. In actual use, record the input image, question, provider, VEP length, total time, and whether the task was completed. Then judge whether the compression is worthwhile.

Project: lora-sys/free-vision-skill ↗. Licensed under MIT.

End of article · Back to writing

Comments

Comments are public and managed through GitHub Discussions.

Preparing the comment section…

Sign in with GitHub once the comment box is ready, or open the repository discussions directly. Open GitHub Discussions ↗

Comments are published to the public discussions of the lora-sys/loraSys repository.

Other contact methods