Agent konačno može da vidi slike! GLM-5V daje PaiCLI-u oči u prepoznavanju slika.
PaiCLI je već vrlo moćan, ima ReAct, Multi-Agent, MCP, Skill, Function Calling, praktično pokriva sve funkcionalnosti Claude Code-a.
Danas, PaiCLI-u dodajemo još jednu mogućnost: unos slika. Zvuči jednostavno, ali zaista mnogo stvari treba implementirati.
Osnova ove funkcije je multimodalni model. Čisti tekstni modeli ne mogu videti slike, na primer GLM-5.1, zato dodajemo GLM-5V model endpoint.

Pogledajmo efekt, paste-ujte početnu stranu Tehničke stranke.

Može tačno da prepoznaje ove informacije.

- Naziv sajta: Tehnička stranka (logo Tehničke stranke u gornjem levom uglu)
- Autor: više sadržaja potpisuje Marko Marković (ista kao prethodna slika)
- Ključni proizvodi: PaiFlow (Agent workflow), Pai Smart (RAG projekat)
01. Zašto GLM-5.1 ne može videti slike
GLM-5.1 je čisti tekstualni veliki jezički model. Njegov ulaz može samo biti tekst.
Tekst se kroz Tokenator seče na tokene, šalje se u Transformer za attention proračun, izlaz je takođe niz tokena koji se dekodira nazad u tekst. Tokom celog processa razmišljanja, model ima samo jedno „čulo“ — tekst.

GLM-5V dodaje jedan ključni komponent: Vision Encoder.

Ovaj Vision Encoder je obično unapred obučen ViT (Vision Transformer), njegov posao je da konvertuje jednu sliku u grupu „vizuelnih tokena“.

Konkretan proces je ovakav:
Prvi korak, sliku isece na fiksne delove patch (obično 14x14 ili 16x16 piksela jedan patch). Slika 224x224 će biti isečena na 16x16=256 patch-eva. Stvarni veliki model obrađuje sliku veće rezolucije, na primer slika 2000x2000 će biti isečena na hiljade patch-eva.
Drugi korak, svaki patch kroz ViT-ovu linearnu projekciju i više slojeva Transformer-a, pretvara se u fiksno-dimenzionalni embedding vektor. Ovi vektori su „vizuelni tokeni“, dimenzije se poklapaju sa embedding-om tekstualnih tokena.
Treći korak, grubo rečeno vizuelni tokeni i tekstualni tokeni ulaze u isti multimodalni process razmišljanja. Model kroz attention mehanizam, tekstualni tokeni mogu da „vide“ vizuelne tokene, vizuelni tokeni mogu se referisati na tekstualni kontekst. zvanično više GLM-5V arhitekturu opisuju kao „multimodalna fuzija“.

Dakle u suštini, multimodalni model „vidi sliku“ ne radi OCR ili prepoznavanje slike, već enkodira piksel informacije slike u isti vektorski reprezentacija kao tekst, neka Transformer koristi attention mehanizam da razume odnos između slike i teksta.
Ovo takođe objašnjava zašto unos slika troši mnogo token-a.
Slika 1000x1000, po 14x14 patch sečenju, ima oko 5000+ vizuelnih tokena.
Ovi tokeni isto kao tekstualni tokeni učestvuju u attention proračunu, zauzimaju kontekstni prozor, takođe učestvuju u API naplati.

Sa nivoa koda, GLM-5.1 i GLM-5V u PaiCLI-u koriste potpuno različite puteve.
U GLMClient postoji metoda selectApiUrl():
private static String selectApiUrl(String model) {
String normalized = model == null ? "" : model.trim().toLowerCase();
if (normalized.startsWith("glm-5v")) {
return MULTIMODAL_API_URL; // open.bigmodel.cn/api/paas/v4/...
}
return CODING_API_URL; // open.bigmodel.cn/api/coding/paas/v4/...
}Dva potpuno različita API endpoint-a. Coding API iza njega je tekstualni inference servis. Multimodal API iza njega je inference servis sa Vision Encoder-om, može parsirati slikovne podatke.
02. Nadogradnja ContentPart protokola
Razumevši multimodalni princip, pogledajmo kako PaiCLI na nivou koda podržava.
Prvi korak je transformisanje LLM komunikacionog protokola.
Pre PaiCLI-jevog LlmClient.Message sadržaj je samo String, čist tekst. Agent šalje poruku modelu, samo stavlja string u JSON content polje, model vraća takođe string.
Multimodal Vision API zahteva da content ne može biti string, već niz, u kojem se mogu mešovito slagati text block i image block, svaki block ima svoj type i podatke.
Stvarno se šalje API JSON struktura izgleda ovako:
{
"role": "user",
"content": [
{"type": "text", "text": "pomozni mi analizirati ovaj screenshot"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgoAAAANSUh..."}}
]
}To znači da treba nadograditi podatkovnu strukturu Message sa „jedan string“ na „lista content blockova“.
Naša strategija je: dodati contentParts polje u Message, i očuvati originalni content field. Čiste tekstualne poruke nastavljaju koristiti content, samo poruke sa slikama koriste contentParts:
record Message(String role, String content, String reasoningContent,
List<ToolCall> toolCalls, String toolCallId,
List<ContentPart> contentParts) {
public static Message user(List<ContentPart> contentParts) {
return new Message("user", plainText(contentParts), null, null, null,
contentParts == null ? null : List.copyOf(contentParts));
}
public boolean hasContentParts() {
return contentParts != null && !contentParts.isEmpty();
}
}Obratite pažnju na plainText(contentParts) u Message.user() factory metodi, ona će spojiti sve tekstualne blokove u contentParts-u u jedan čist tekstualni fallback sačuvati u content polje. Tako čak i ako sledeća serializacijska logika ode u granu koja ne podržava content niz, poruka neće izgubiti tekstualne informacije.
Prilikom serializacije, AbstractOpenAiCompatibleClient.appendMessageContent() će odlučiti koju putnju da sledi na osnovu hasContentParts():
private void appendMessageContent(ObjectNode msgNode, Message msg) {
if (!msg.hasContentParts()) {
msgNode.put("content", msg.content()); // čist tekst: direktno stavi string
return;
}
ArrayNode contentArray = msgNode.putArray("content"); // multimodal: konstruiši niz
for (ContentPart part : msg.contentParts()) {
if (part.isText()) {
// text block
} else if (part.isImage()) {
// image_url block, konkretni format određuje podklasa toImageUrl()
}
}
}base64 slika će se po default-u konvertovati u data:image/png;base64,<payload> format data URI, staviti u image_url.url polje.
03. @image referenca parsiranje
Upotreba je jednostavna: @image:./shot.png.
pomozni mi analizirati ovaj screenshot @image:./shot.pngPaiCLI će u terminalu prikazati jednu liniju: [priložena slika: ./shot.png, mimeType=image/png, bytes=...], zatim odgovor modela će biti analizom na osnovu sadržaja slike.
Format puta podržava nekoliko:
@image:./shot.png # relativni put
@image:/Users/demo/Desktop/error-log.png # apsolutni put
@image:file:///Users/demo/Desktop/shot.png # file:// protokol
@image:<file:///Users/demo/Desktop/path with spaces.png> # zagrade omotač
@image:</Users/demo/Desktop/slikazaekran.png> # kineski putNaravno, ako je čisti tekstualni model kao DeepSeek V4, nije podržano.

Ključni regularni izraz izgleda ovako:
private static final Pattern IMAGE_REF = Pattern.compile(
"@image:(<[^>]+>|[^\\s<>\\u2010-\\u206F\\u3000-\\u303F\\uFF00-\\uFFEF]+)"
+ "|@clipboard(?![\\p{L}\\p{N}_])");Zagrada omotač sintaksa @image:<...> je za tretman puta sa razmacima ili specijalnim karakterima. Unutar zagrada može stati bilo koji karakter, sve dok se ne susretne sa > prestaje se parsovanje.
Nakon što se izparsira put, ImageReferenceParser takođe tretira file:// protokol prefiks i percent-encoding.
04. @clipboard screenshot grabljenje
PaiCLI podržava dva načina unosa clipboard-a.
Prvi je u razgovoru kucati @clipboard:
pomozni mi pogledati ovu sliku @clipboardDrugi je direktno pritisnuti Ctrl+V, PaiCLI će automatski grabiti clipboard sliku, i na kraju linije unosa dodati @image: referencu.
PaiCLI na macOS koristi AppleScript + osascript:
on run argv
set outputPath to item 1 of argv
try
set pngData to (the clipboard as «class PNGf»)
on error errMsg
error "Clipboard nema PNG podatke"
end try
set fh to open for access (POSIX file outputPath as string) with write permission
try
set eof of fh to 0
write pngData to fh
close access fh
on error errMsg
try
close access fh
end try
error errMsg
end try
end run«class PNGf» je Apple Event tip PNG podataka u macOS clipboard-u. Screenshot alati stavljaju u clipboard ovaj format.
Ali neke aplikacije kao Preview i deo Office softvera, stavljaju TIFF u clipboard. Zato PaiCLI ima fallback: ako ne može dobiti PNG, pokušava «class TIFF», nakon dobijanja TIFF koristi sistemski /usr/bin/sips da konvertuje u PNG.
Java strana kroz ProcessBuilder poziva osascript, script se prenosi kroz stdin (bez privremenog fajla), 8 sekundi timeout zaštita. Kompletan cold start oko 30ms, korisnik gotovo ne oseća.
Process process = new ProcessBuilder("/usr/bin/osascript", "-", outputPath).start();
try (var stdin = process.getOutputStream()) {
stdin.write(script.getBytes(StandardCharsets.UTF_8));
}
boolean completed = process.waitFor(8, TimeUnit.SECONDS);Linux i Windows koriste standardni Clipboard.getData(DataFlavor.imageFlavor) od AWT, headless okolina (SSH / Docker) će direktno prikazati „trenutna okolina nema GUI“.
Zašto ne koristiti uniformno Java AWT Clipboard API?
Zato što macOS Java AWT implementacija ima poznati problem: DataFlavor.imageFlavor dobija BufferedImage objekat, ali ovaj objekat pri serializaciji nazad u PNG bajt niz, colorspace će se degradirati iz Display P3 u sRGB, određene screenshot boje će imati očiglednu devijaciju. Kroz AppleScript direktno dobijanje PNG originalnih bajtova nema ovaj problem, podaci ne prolaze kroz Java colorspace konverziju, piksel nivo tačnost.
05. MCP screenshot
Prava snaga unos a slika dolazi u kombinaciji sa Chrome DevTools MCP.
Prethodno smo PaiCLI-u povezali Chrome DevTools MCP, Agent može kontrolisati pretraživač, navigirati stranice, screenshot. Ali screenshot se dobija samo jedan placeholder tekst, model ne vidi sadržaj slike.
otvori https://www.apple.com zatim screenshot, reci mi šta ima u glavnoj vizuelnoj slici početne stranice

Implementacija ove veze uključuje saradnju tri komponente.
Prvo je McpCallToolResult.
MCP alat vraća rezultat kao JSON niz, unutar content item postoji type: "text" i type: "image" dva tipa. PaiCLI u toToolOutput() metodu prolazi kroz ovaj niz, kada susretne image tip content, izvlači base64 podatke, nakon ImageProcessor predobrade, čuva u ToolOutput imageParts list:
if ("image".equals(type)) {
ImageProcessor.ProcessedImage processed = ImageProcessor.fromBase64(item.data(), mimeType);
imageParts.add(ImageProcessor.toContentPart(processed));
// istovremeno generiše tekst fallback, osigurava da alat poziv protokol nije narušen
}Tekst fallback se takođe istovremeno generiše, govori modelu „PaiCLI će u sledećem rundu staviti sliku kao sliku prilog“. Tako čak i ako provider ne podržava unos slike, rezultat alata je i dalje legalna tool poruka.
Zatim je Agent.appendImageToolMessages(). Ova metoda se poziva nakon završetka izvršenja alata, proverava da li svaki rezultat alata ima imageParts. Ako ima, konstruiše novu user poruku, sliku kao ContentPart dodaje u istoriju razgovora:
private void appendImageToolMessages(List<ToolExecutionResult> toolResults) {
for (ToolExecutionResult result : toolResults) {
if (!result.hasImageParts()) {
continue;
}
List<LlmClient.ContentPart> parts = new ArrayList<>();
parts.add(LlmClient.ContentPart.text(
"Alat " + result.name() + " vratio sadržaj slike, molim te analiziraj u kombinaciji sa gore navedenim tekstualnim rezultatima alata."));
parts.addAll(result.imageParts());
conversationHistory.add(LlmClient.Message.user(parts));
}
}Zašto koristiti user poruku umesto da direktno stavi sliku u tool poruku?
Zato što OpenAI API specifikacijom, tool role poruka podržava samo čist tekst content, ne podržava content niz. Ako silom prisilimo staviti image block u tool poruku, API će vratiti 400 grešku.
Zato PaiCLI tretira: tool poruka stavi tekst fallback (govori modelu da je ovaj alat vratio sliku), neposredno doda jednu user poruku stavi pravu sliku. Redosled poruka postaje assistant(tool_calls) → tool(text fallback) → user(text + image block).
Još jedan lako previdljiv problem: ekspanzija konteksta.
Svaka slika nakon base64 kodiranja prosečno 200KB~2MB, ako Agent izvrši desetak ReAct petlji, svaka rundu nosi istorijske slike, kontekst će brzo eksplodirati.
PaiCLI rešenje je pruneHistoricalImagePayloads(): pre početka svake nove ReAct iteracije, skenira sve poruke u istoriji razgovora, već obrađene slikovne blokove zamenjuje jednim linijama tekst placeholder [slika je izostavljena, vidi gore opis].
Samo čuva entity podatke najskorije jedne runde, starije sve sece. Tako model još uvek zna „šta slike je ranije video“, ali ne mora u svakoj iteraciji ponovno trošiti tokene tih slika, efikasnost korišćenja kontekstnog prozora se značajno povećava.
06. Poređenje dva screenshot-a
Više slika unos je moja omiljena funkcionalnost. Jedna poruka može istovremeno imati više priloga:
uporedi ova dva screenshot-a @image:./before.png @image:./after.pngPaiCLI će posebno obraditi dve slike, terminal će prikazati dva priloga, model može istovremeno videti dve slike i dati analizu poređenja.

Implementacijom, ImageReferenceParser.findRefs() će koristiti regularni izraz da skenira ceo unos, izvuči sve @image: referencu koje se poklapaju, čuva u List<ImageRef>, zatim jedan po jedan učitava i predobraduje. Svi uspešno učitane slike se dodaju u istu user poruku contentParts, jedna poruka može nositi više image block.
07. Predobrada slika
PaiCLI ne dobija sliku i odmah šalje na API.
Kompletnu decision tree u ImageProcessor.process() metodi, ključna logika ima tri sloja:
// 1) bajt unutar 5MB + nema alpha → direktne originalne bajtove, bez ikakvog tretmana
if (!overSize && !hasAlpha) {
return new ProcessedImage(Base64.getEncoder().encodeToString(bytes), ...);
}
// 2) ima alpha ali bajt unutar 5MB → belom pozadinom flatten pa PNG izlaz
if (!overSize && hasAlpha) {
byte[] flattened = writePng(flattenAlpha(image, width, height));
// ako flatten i dalje preveliko, pada na treći sloj
}
// 3) preko 5MB → proporcionalno smanji na 2000x2000, prvo PNG, preveliko zatim stepenasti JPEG quality drop
ResizeSize target = fitWithin(originalWidth, originalHeight, 2000, 2000);
BufferedImage resized = resize(image, target.width(), target.height());Zašto tri sloja umjesto bezumni kompresije?
Zato što svaki korak ima tradeoff performansi i kvaliteta:
Prvi sloj logike pokriva većinu svakodnevnih screenshot scena. macOS screenshot alati generišu PNG obično 500KB~3MB, nema alpha kanala (čak i ako je PNG format), direktno slanje najbolje, nulti CPU trošak.
Drugi sloj alpha flatten tretira PNG koje izvoze dizajn alati kao Sketch, Figma. Ove slike obično imaju transparent kanal, ako se ne tretira, transparent region će biti renderovan kao crna pozadina kada model analizira sadržaj slike, utiče na tačnost prepoznavanja. Belom pozadinom flatten vizuelni efekat je Konzistentan sa onim što korisnik vidi na ekranu.
Treći sloj je prava lossy kompresija, samo ogromne slike će ovde dospiti. Izbor 2000x2000 gornje granice baziran na razmatranju token troška: po 14x14 patch veličini grubo procenjuje, 2000x2000 slika generiše nivo deset hiljada vizuelnih tokena, već značajan kontekstni trošak.
GLM-5V-Turbo zvanični kontekstni prozor je 200K, veća slika revenue diminishing ozbiljno, model attention teško pokriva svaki detalj.
Alpha flatten koristi Java2D Graphics2D: prvo kreirati TYPE_INT_RGB (bez alpha) BufferedImage, popuniti belom pozadinom, zatim nacrtati originalnu sliku.
JPEG quality drop koristi ImageIO-ov ImageWriteParam:
private static final float[] JPEG_QUALITIES = {0.85f, 0.70f, 0.55f, 0.40f, 0.25f};
for (float quality : JPEG_QUALITIES) {
byte[] candidate = writeJpeg(resized, quality);
if (estimateBase64Size(candidate.length) <= API_IMAGE_MAX_BASE64_SIZE) {
return candidate; // prvi quality koji zadovoljava prestane
}
}Obratite pažnju da se ovde koristi estimateBase64Size() umesto direktno poređenja originalne bajt dužine.
API prenosi base64 kodirani string, base64 će pretvoriti svaka 3 bajta u 4 karaktera, inflation ratio je 4/3. 3.75MB JPEG, nakon base64 kodiranja je 5MB, upravo stoji na gornjoj granici. Ako ne razmatramo ovu inflation ratio, prema originalnoj bajt dužini se donese odluku, pojavljuje se „klijent misli da nije prešao granicu ali API vraća 413“.
Izvršno vreme kompletnog predobrade pipeline-a obično 50ms~200ms (zavisi od veličine slike i da li zahteva kompresiju).
PaiCLI kako napisati u CV?
Ako svi radite na multimodalnom unosu Agent-a ili predobradi slike, tako možete napisati u CV:
- Naziv projekta: PaiCLI (terminal AI program Agent)
- Opis projekta: Terminal AI program Agent baziran na Javi, podržava multi-model pristup, MCP ekosistem, multimodalni unos slika
- Tehnološki stack: Java 21, Maven, OpenAI-Compatible API, Chrome DevTools Protocol, MCP protokol
- Ključne odgovornosti:
- Nadogradio LlmClient.Message protokol, content iz String proširio na List<ContentPart>, podržava text/image_base64/image_url tri tipa
- Dizajnirao i implementirao
@image:/@clipboardprotokol unos slika, podržava file://, apsolutni put, relativni put, zagradom omotač itd. više formata puta - Implementirao macOS clipboard grabljenje slika (AppleScript + osascript), podržava PNG / TIFF dvostruki fallback i sips format konverziju, cold start 30ms
- Implementirao MCP alat image content do LLM unos slike injekciju lanac, kroz tool message + user image message sekvencu poruka rešio problem da tool role ne podržava content niz protokol ograničenje
