{"id":6721,"date":"2026-07-08T07:19:23","date_gmt":"2026-07-08T00:19:23","guid":{"rendered":"https:\/\/daiilynews.cu.ma\/?p=6721"},"modified":"2026-07-08T07:19:23","modified_gmt":"2026-07-08T00:19:23","slug":"thomasunise-visionbridge-give-text-only-llms-vision-a-tiny-openai-compatible-proxy-that-lets-reasoning-models-deepseek-qwen-glm-see-images-by-querying-a-separate-vision-model-through-to","status":"publish","type":"post","link":"https:\/\/daiilynews.cu.ma\/?p=6721","title":{"rendered":"thomasunise\/visionbridge: Give text-only LLMs vision. A tiny OpenAI-compatible proxy that lets reasoning models (DeepSeek, Qwen, GLM\u2026) see images by querying a separate vision model through tools: look, OCR, scan, crop, compare. No training, no weights. \u00b7 GitHub"},"content":{"rendered":"<p> <br \/>\n<br \/>\nGive text-only LLMs vision through a tiny OpenAI-compatible proxy.<br \/>\nVisionBridge sits between your chat UI and your models. It lets a reasoning model<br \/>\nfrom LM Studio, Ollama, vLLM, Z.ai, OpenRouter, or any OpenAI-compatible backend<br \/>\ninspect images by asking a separate vision model targeted questions.<br \/>\nNo training. No weights. Bring your own reasoning model and your own vision model.<\/p>\n<p>OpenWebUI \/ LibreChat \/ app<br \/>\n        |<br \/>\n        v<br \/>\nVisionBridge at \/v1\/chat\/completions<br \/>\n        |<br \/>\n        +&#8211; reasoning model: LM Studio \/ Ollama \/ vLLM \/ GLM \/ DeepSeek<br \/>\n        |<br \/>\n        +&#8211; vision model: LLaVA \/ Qwen-VL \/ GLM-V \/ GPT-4o-mini \/ any VLM<\/p>\n<p>VisionBridge receives an OpenAI-style chat request. If the request contains<br \/>\nimages, it stores the images, rewrites the prompt for the reasoning model, and<br \/>\nlets the reasoning model call tools:<\/p>\n<p>look(image_id, question) for targeted visual inspection<br \/>\nocr(image_id) for text extraction<br \/>\nscan(image_id) for a tiled full-document text sweep of dense pages<br \/>\ncrop_and_look(image_id, box, question) for zooming into a region<br \/>\n(small crops are automatically upscaled before the VLM sees them)<br \/>\ncompare(image_id_a, image_id_b, question) for judging two images together<\/p>\n<p>The vision model does the seeing. The reasoning model does the thinking.<br \/>\nBefore the loop starts, every image gets a one-shot scene description from<br \/>\nthe vision model (in parallel, cached by content), so the reasoning model<br \/>\nbegins with global context and spends its tool budget on details. Multiple<br \/>\ntool calls in one turn run concurrently, and repeated images across a<br \/>\nconversation are served from a content-addressed cache instead of being<br \/>\nre-fetched and re-decoded.<br \/>\nTool calling uses the backend&#8217;s native OpenAI function calling when<br \/>\navailable. In the default TOOL_MODE=auto, VisionBridge probes the reasoning<br \/>\nbackend once and transparently falls back to a prompt-JSON protocol for<br \/>\nbackends and models that don&#8217;t support tools \u2014 so it works with strong hosted<br \/>\nmodels and weak local ones alike.<br \/>\nThe loop is built to survive imperfect models: hallucinated image ids, bad<br \/>\ncrop boxes, malformed JSON, and vision-backend hiccups are fed back to the<br \/>\nreasoning model as observations so it can correct itself, and hitting the<br \/>\ninspection limit triggers a final best-effort answer instead of an error.<\/p>\n<p>docker run -p 8080:8080 \\<br \/>\n  -e REASONING_BASE_URL=http:\/\/host.docker.internal:1234\/v1 \\<br \/>\n  -e REASONING_API_KEY=lm-studio \\<br \/>\n  -e REASONING_MODEL=local-model \\<br \/>\n  -e VISION_BASE_URL=http:\/\/host.docker.internal:11434\/v1 \\<br \/>\n  -e VISION_API_KEY=ollama \\<br \/>\n  -e VISION_MODEL=llava:13b \\<br \/>\n  ghcr.io\/eekosystems\/visionbridge:latest<br \/>\nThen point your OpenAI-compatible client at:<br \/>\nBase URL: http:\/\/localhost:8080\/v1<br \/>\nAPI key: anything<br \/>\nModel: visionbridge<\/p>\n<p>cd visionbridge<br \/>\npython -m venv .venv<br \/>\nsource .venv\/bin\/activate    # Windows: .venv\\Scripts\\activate<br \/>\npip install -e &#8220;.(dev)&#8221;<br \/>\ncp .env.example .env         # Windows: copy .env.example .env<br \/>\nvisionbridge serve &#8211;host 0.0.0.0 &#8211;port 8080<br \/>\nCheck your backend configuration any time with:<\/p>\n<p>It verifies both backends are reachable, that the configured models exist, and<br \/>\nthat a test completion succeeds.<\/p>\n<p>All settings come from environment variables (or .env):<\/p>\n<p>Variable<br \/>\nDefault<br \/>\nPurpose<\/p>\n<p>REASONING_BASE_URL<br \/>\nhttp:\/\/localhost:1234\/v1<br \/>\nReasoning backend<\/p>\n<p>REASONING_API_KEY<br \/>\nlocal<br \/>\nReasoning API key<\/p>\n<p>REASONING_MODEL<br \/>\nlocal-model<br \/>\nReasoning model id<\/p>\n<p>VISION_BASE_URL<br \/>\nhttp:\/\/localhost:11434\/v1<br \/>\nVision backend<\/p>\n<p>VISION_API_KEY<br \/>\nlocal<br \/>\nVision API key<\/p>\n<p>VISION_MODEL<br \/>\nllava:13b<br \/>\nVision model id<\/p>\n<p>BRIDGE_MODEL_NAME<br \/>\nvisionbridge<br \/>\nModel name the proxy advertises<\/p>\n<p>TOOL_MODE<br \/>\nauto<br \/>\nauto, native, or prompt tool calling<\/p>\n<p>MAX_VISION_CALLS<br \/>\n8<br \/>\nReasoning turns before a forced final answer<\/p>\n<p>REQUEST_TIMEOUT_SECONDS<br \/>\n120<br \/>\nPer-backend-call timeout<\/p>\n<p>BACKEND_MAX_RETRIES<br \/>\n2<br \/>\nRetries on 429\/5xx\/transport errors<\/p>\n<p>SCENE_PRIMING<br \/>\ntrue<br \/>\nDescribe each image once before the loop starts<\/p>\n<p>ALLOW_FILE_URLS<br \/>\nfalse<br \/>\nAllow file:\/\/ image URLs<\/p>\n<p>ALLOW_PRIVATE_URLS<br \/>\nfalse<br \/>\nAllow image fetches from private\/internal hosts<\/p>\n<p>MAX_IMAGE_BYTES<br \/>\n20971520<br \/>\nMax bytes per image<\/p>\n<p>MAX_IMAGES_PER_REQUEST<br \/>\n16<br \/>\nMax images per request<\/p>\n<p>IMAGE_CACHE_ENTRIES<br \/>\n64<br \/>\nContent-addressed image cache size<\/p>\n<p>ENABLE_TRACES<br \/>\ntrue<br \/>\nKeep tool-loop traces for \/v1\/traces\/{id}<\/p>\n<p>BRIDGE_API_KEYS<br \/>\n(empty)<br \/>\nComma-separated keys; when set, \/v1\/* requires one<\/p>\n<p>EXTRA_MODELS<br \/>\n(empty)<br \/>\nJSON map of extra model names to backend pairs<\/p>\n<p>LOG_JSON<br \/>\nfalse<br \/>\nEmit JSON log lines<\/p>\n<p>Serving multiple model pairs<br \/>\nOne instance can serve several reasoning+vision pairs under different model<br \/>\nnames. Unspecified fields inherit from the default pair:<br \/>\nEXTRA_MODELS={&#8220;visionbridge-fast&#8221;: {&#8220;reasoning&#8221;: {&#8220;model&#8221;: &#8220;qwen3:8b&#8221;}, &#8220;vision&#8221;: {&#8220;model&#8221;: &#8220;llava:7b&#8221;}}}<br \/>\n(or point EXTRA_MODELS_FILE at a JSON file). All names appear in<br \/>\nGET \/v1\/models, and clients select a pair with the model field.<\/p>\n<p>Open LM Studio.<br \/>\nLoad your reasoning model.<br \/>\nGo to the Developer tab and start the local server.<br \/>\nUse the default base URL:<\/p>\n<p>REASONING_BASE_URL=http:\/\/localhost:1234\/v1<br \/>\nREASONING_API_KEY=lm-studio<br \/>\nREASONING_MODEL=local-model<br \/>\nLM Studio can also be used as the vision backend if you load a vision-capable<br \/>\nmodel and expose it through the same OpenAI-compatible server.<\/p>\n<p>REASONING_BASE_URL=http:\/\/localhost:11434\/v1<br \/>\nREASONING_API_KEY=ollama<br \/>\nREASONING_MODEL=qwen3:32b<\/p>\n<p>VISION_BASE_URL=http:\/\/localhost:11434\/v1<br \/>\nVISION_API_KEY=ollama<br \/>\nVISION_MODEL=llava:13b<\/p>\n<p>REASONING_BASE_URL=http:\/\/localhost:8000\/v1<br \/>\nREASONING_API_KEY=token-abc123<br \/>\nREASONING_MODEL=Qwen\/Qwen3-32B<\/p>\n<p>VISION_BASE_URL=http:\/\/localhost:8001\/v1<br \/>\nVISION_API_KEY=token-abc123<br \/>\nVISION_MODEL=Qwen\/Qwen3-VL-8B-Instruct<\/p>\n<p>In OpenWebUI:<br \/>\nAdmin Panel -> Settings -> Connections -> OpenAI-compatible<br \/>\nBase URL: http:\/\/host.docker.internal:8080\/v1<br \/>\nAPI Key: anything<br \/>\nModel: visionbridge<\/p>\n<p>VisionBridge intentionally exposes the small subset most clients need:<\/p>\n<p>GET \/health (add ?probe=true to verify both backends are reachable)<br \/>\nGET \/v1\/models<br \/>\nPOST \/v1\/chat\/completions<br \/>\nGET \/v1\/traces\/{completion_id} \u2014 the full tool-loop trace for a recent<br \/>\nrequest: every tool call, its arguments, the vision model&#8217;s observation, and<br \/>\ntimings. Invaluable for answering &#8220;why did it say that?&#8221;<\/p>\n<p>Responses include a usage block aggregated across every reasoning and vision<br \/>\ncall made for the request, and an X-Request-ID header for correlating logs.<br \/>\nStreaming: image-free requests are streamed token-by-token straight from<br \/>\nthe reasoning backend. Requests with images stream too: SSE heartbeats keep<br \/>\nthe connection alive while tools run, and in native tool-calling mode the<br \/>\nfinal answer is forwarded token-by-token as the reasoning model produces it<br \/>\n(prompt-JSON mode buffers the loop and streams the answer in chunks).<br \/>\nErrors: backend failures return HTTP 502, invalid inputs 400.<br \/>\nAuth: set BRIDGE_API_KEYS=key1,key2 to require a Bearer key on \/v1\/*<br \/>\nendpoints (\/health stays open). For anything internet-facing, still prefer a<br \/>\nreverse proxy with TLS in front.<\/p>\n<p>Image URL fetching is hardened by default: hosts resolving to private,<br \/>\nloopback, or link-local addresses are rejected (SSRF guard), redirects are<br \/>\nvalidated hop-by-hop, downloads are size-capped, and payloads must decode as<br \/>\nreal images. Run VisionBridge behind your own reverse proxy \/ auth layer if<br \/>\nyou expose it beyond localhost \u2014 it deliberately ships without built-in auth.<\/p>\n<p>benchmarks\/ contains a harness that scores VisionBridge against a direct<br \/>\nVLM (relaxed accuracy + ANLS) on DocVQA-style JSONL task sets. See<br \/>\nbenchmarks\/README.md.<\/p>\n<p>Actively developed. See CHANGELOG.md for what&#8217;s new in 0.2.0.<br \/>\nNext up:<\/p>\n<p>published benchmark numbers against caption-only prompting<br \/>\npersistent on-disk image\/primer cache<br \/>\nOpenWebUI and LibreChat screenshots<\/p>\n<p>Contributions welcome \u2014 see CONTRIBUTING.md.<\/p>\n<p>MIT<br \/>\n<br \/><br \/>\n<br \/><a href=\"https:\/\/github.com\/thomasunise\/visionbridge\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Give text-only LLMs vision through a tiny OpenAI-compatible proxy. VisionBridge sits between your chat UI and your models. It lets a reasoning model from LM Studio, Ollama, vLLM, Z.ai, OpenRouter, or any OpenAI-compatible backend inspect images by asking a separate vision model targeted questions. No training. No weights. Bring your own reasoning model and your [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":6722,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[676],"tags":[],"class_list":["post-6721","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tech-ai"],"_links":{"self":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts\/6721","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6721"}],"version-history":[{"count":0,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts\/6721\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/media\/6722"}],"wp:attachment":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6721"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6721"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6721"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}