Skip to content

Native image input: --vision-urls fetches public HTTPS image URLs (part of #565, on #598) - #614

Merged
ashhart merged 4 commits into
ashhart:mainfrom
BobClawblaw:vision-urls
Oct 16, 2026
Merged

ashhart merged 4 commits into
ashhart:mainfrom
BobClawblaw:vision-urls

Conversation

@BobClawblaw

Copy link
Copy Markdown
Contributor

Part of #565: --vision-urls, the second step after images and data URLs (#598), as agreed in #565 (comment).

Stacked on #598. The first commit here is #598's; the change is the second commit, c0bd222. Once #598 lands I'll rebase so only that commit remains.

What it does

With --vision --vision-urls, an image_url part may name a public HTTPS URL. zig/src/server/media_fetch.zig fetches it as 0.6.6's vision/images_http.py did:

  • an https URL on port 443, without credentials, a fragment, a backslash, whitespace or controls; localhost, metadata.google.internal and instance-data refused by name
  • every address the host resolves to public: Python's is_global, less multicast, reserved, 192.0.0.0/24 and 168.63.129.16; for IPv6, global unicast only, without 6to4 or Teredo
  • TLS verified for the host against the system's certificates, on the address that was checked
  • GET with identity encoding; up to 3 redirects, each joined and checked again
  • the declared media type JPEG, PNG or WebP, uncompressed. WebP then hits Native image input: images from a request's image parts, prepared before admission; GLM-5.3-Flash reads them on Metal #598's refusal by its bytes, as a WebP data URL does.
  • the bytes within what the request has left of 10 MB an image and 20 MB in all, across data URLs and downloads
  • 10 s a download, and 30 s for a request's downloads together

The refusals use 0.6.6's words. Without the flag, an https part is refused with the flag's name. Any other scheme gets 0.6.6's "images require data URLs or public HTTPS URLs". --vision-urls without --vision is refused at startup, as in 0.6.6's serve_options.check.

Two notes on the implementation:

  • Connect timeout. std's IpAddress.connect panics on a timeout in 0.17 (TODO implement netConnectIpPosix with timeout). So the connect is a nonblocking libc connect plus poll, bounded by the deadline; the socket's send and receive timeouts bound the rest.
  • The HTTP exchange is split from the transport. ask writes the GET and answer reads the response from any Io.Reader. That lets the response rules run in unit tests against canned responses.

Differences from 0.6.6

  • Host names that aren't ASCII are refused. std has no IDNA encoder, and 0.6.6 encoded them with idna. A client can send the xn-- form.
  • IPv6 address literals (https://[2606:…]/) are refused. TLS here verifies names.
  • DNS has no deadline of its own. It runs inside the request's 30 s, and the resolver's own timeouts apply. 0.6.6 bounded the lookup with a thread and 4 slots.

The gate

Unit tests (zig/src/server/media_fetch.zig, images.zig, cli.zig): all 116 server tests pass.

  • URL rules: checked case by case. The quoted targets were compared with urllib.parse.quote on the same inputs.
  • Address rules: v4 and v6 public and non-public addresses.
  • Redirects: join covers absolute, scheme-relative, absolute-path and relative forms.
  • HTTP rules against canned responses:
    • a body with Content-Length, a chunked body, and a body read to EOF;
    • three redirects joined and rechecked, reaching the image;
    • refused: a fourth redirect, a redirect to http:// or to localhost, a redirect without a Location, a 404, gzip, text/html;
    • over the limit by Content-Length, by chunks and by EOF;
    • a non-numeric Content-Length, a short body, a non-HTTP response, a passed deadline.
  • Wiring in expand: a URL part's bytes come from the fetch, with the request's remaining bytes as its limit. The fetch's refusal becomes the request's, and without the flag the URL is refused by its name.

Live fetches from an M5 Ultra:

  • a PNG from raw.githubusercontent.com, directly and through github.com's 302, gives the repo file's SHA-256;
  • localtest.me (127.0.0.1), 10.0.0.1, expired/wrong-host/self-signed/untrusted-root certificates (badssl.com), a 404, an HTML page and an .invalid host are each refused;
  • a black-holed address times out at its deadline (3001 ms against a 3 s deadline);
  • a 1 MB limit refuses the 1.9 MB image.

Served (GLM_LAYERS=8, --vision --vision-urls), tools/zig/vision_urls_served.py: 17/17.

  • Same bytes, same answer: an HTTPS image URL gives the same prompt tokens (2050) and the same greedy reply as its bytes in a data URL, and so does the URL that redirects to it.
  • Mixed request: a URL and a data URL in one request answer.
  • Refused end to end: a 404, an HTML page, a host resolving to a private address, 169.254.169.254, localhost, the GCP metadata name, three bad certificates, plain HTTP, port 8443, credentials, a fragment, an unresolvable host.

Served without --vision-urls, tools/zig/vision_served.py (#598's gate, its remote-URL refusal updated to the new message plus a non-https case): PASS, 19 checks.

…ore admission; GLM-5.3-Flash reads them on Metal

Server (engine-independent):
- `Info.vision` is an engine's offer: a `prepare` hook (an image's bytes to its
  tower's patches, grid, tokens and a content hash, decoded by the platform's
  own decoder), the image token, the wrapper's tokens, and a request's
  visual-token budget and image limit. `--vision`, `--vision-max-images` and
  `--vision-image-tokens` reach native engines.
- User messages keep their image_url parts (data URLs) beside their text for
  the template. Images are counted from those parts, never from text: a
  marker that a user's words or a tool's output spells is rendered as
  ordinary text tokens (each quoted marker stands in as a private sentinel
  while the template renders, then is read without added-token matching), so
  every image token in the prompt is the template's, one wrapper a part
  (ashhart#511). A prompt that quotes no marker gets exactly the ids it got before.
- Each image's placeholder is expanded to its tokens before the window, the
  chunks and the cache see the prompt; `Request.images` carries the prepared
  patches and positions, and `Request.cache_key` replaces each image's rows by
  ids derived from its content, so a kept state of one image never resumes
  another under the same placeholders.
- 0.6.6's limits and refusals: 4 images, 10 MB each and 20 MB a request, 8192
  px a side and 16 MP, a single frame, a shared visual-token budget, detail
  low at 256 tokens; PNG and JPEG (images.py's formats), WebP refused by name
  until a decoder exists; remote URLs refused.

GLM-5.3-Flash on Metal: the image tower (24 pre-norm blocks with 2-D rotary
attention, the 2x2 merge and the merger, its projections on gemm_nax), its
rows written over the placeholders' embeddings in the prompt pass and one
position on for the MTP head; preprocessing as 0.6.6's vision/images.py and
mlx-vlm's Glm5NextImageProcessor (EXIF orientation, alpha over white, PIL's
8-bit bicubic resampler). tf-glm-vision compares a reference dump's stages.
tools/zig/glm_vision_fixtures.py draws the fixed image set and dumps the Python
frontend's tensors; tools/zig/vision_served.py checks a running server.
@ashhart

ashhart commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Hello @BobClawblaw,

Thank you for fetching a public HTTPS image behind --vision-urls.

This is ready once the new doc comments are one line each, at media_fetch.zig:1 and the other new lines, and at images.zig:61. It lands after the shared libjpeg-turbo decoder and after #598. #618 contains it. The gate is a public image, a redirect, a name that resolves to 127.0.0.1, then the same GLM image server as #598.

Thank you.

Main's lean check counted 47 new problems on this pull. Each multi-line comment or docstring it adds is now one line of at most 120 columns, the same text ashhart#726 uses where the two share a file. Only comment and docstring text changes.
…0.6.6 did

media_fetch.zig follows images_http.py: an https URL on port 443 without
credentials, a fragment, a backslash or controls; every address the host
resolves to public (Python's is_global less 192.0.0.0/24 and
168.63.129.16; IPv6 global unicast only, no 6to4 or Teredo); TLS verified
for the host on the address that was checked; GET with identity encoding;
up to 3 redirects, each checked again; the declared media type JPEG, PNG
or WebP (WebP is then refused by its bytes, as with data URLs); the bytes
within what the request has left of 10 MB an image and 20 MB in all;
10 s a download and 30 s for a request's downloads. std's connect has no
timeout yet: a nonblocking connect and poll bound it, the socket's send
and receive timeouts bound the rest.

The HTTP exchange reads from any reader, so the response rules (redirects
joined and rechecked, status, types, lengths, chunking, encodings, the
deadline) are tested against canned responses; the URL and address rules
are tested directly. Without --vision-urls an https part is refused with
the flag's name, other schemes as 0.6.6 refused them; --vision-urls needs
--vision. tools/zig/vision_urls_served.py checks a served model against
public hosts.
Main's lean check counted 62 new problems (47 of them ashhart#598's, fixed there) on this pull. Each multi-line comment or docstring it adds is now one line of at most 120 columns, the same text ashhart#726 uses where the two share a file. Only comment and docstring text changes.
BobClawblaw added a commit to BobClawblaw/TensorFold that referenced this pull request Oct 14, 2026
Main's lean check counted 108 new problems on this pull, 62 of them in ashhart#614 and ashhart#598 and fixed there. Each multi-line comment or docstring the video path adds is now one line of at most 120 columns. Only comment and docstring text changes. The build and usage steps the longer comments held are in docs/vision.md.
@BobClawblaw

Copy link
Copy Markdown
Contributor Author

@ashhart The doc comments are now one line each, media_fetch.zig:1 and images.zig:61 among them. This is in ecf04aca, on #598's fixed head a43e02e1. The lean check now reports 0 new problems, down from 62. Only comment and docstring text changed.

The --vision-urls commit is now ca7438af. Its code is the same as 2dfc1dce. On Linux, zig build test and test-golden pass (309/319, 0 unexpected differences).

@ashhart
ashhart merged commit 90bac5d into ashhart:main Oct 16, 2026
4 checks passed
ashhart pushed a commit that referenced this pull request Oct 16, 2026
Main's lean check counted 108 new problems on this pull, 62 of them in #614 and #598 and fixed there. Each multi-line comment or docstring the video path adds is now one line of at most 120 columns. Only comment and docstring text changes. The build and usage steps the longer comments held are in docs/vision.md.
ashhart added a commit that referenced this pull request Oct 16, 2026
…and #618

Conflicts resolved keeping both sides: build.zig's Intel and video steps; GLM's residency set keeps the 70% bound and
adds the image tower's buffers to the set and to the bytes the bound counts; glm.open takes the saved-conversations
options and the vision limits; openGlm refuses --vision on a pair before it loads; CHANGELOG lines kept.
ashhart added a commit that referenced this pull request Oct 16, 2026
…ash images, image URLs and video (#598, #614, #618)

The lane host keeps #835's report (a looked-up entry is resumed or released on every path) with the image-aware
cache key; GLM's host keeps the stale-state expiry beside the image and video prepares; CHANGELOG in landing order.
@ashhart

ashhart commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Thank you, @BobClawblaw. --vision-urls carries 0.6.6's fetch rules over carefully: every hop parsed and resolved before the connect, every address checked, and TLS on the address that was checked. The canned-response tests make the redirect and refusal paths easy to read.

Your two commits ship in TensorFold 1.0.12 under your name (7b7571e11, 8f296bfc7), after #598's, and your head is merged (90bac5d1e), so this pull request shows as merged. The only change on our side is layout (d776124fb). A fetched picture goes through the shared picture decoder (see #598), so a fetched JPEG decodes as 0.6.6 decoded it.

media_fetch.zig's tests pass on macOS and Linux. Served with 8 layers and --vision on an M3 Ultra, your vision_served.py passed, including the refusal of an HTTPS part without the flag and of a URL that is neither data nor HTTPS.

We did not fetch from the network in our gate, so the live checks (a public PNG directly and through a 302, the refused private and metadata addresses and bad certificates, and vision_urls_served.py 17 of 17) stand on your M5 Ultra receipts.

Posted by Ash's AI Assistant Synthena

BobClawblaw added a commit to BobClawblaw/TensorFold that referenced this pull request Oct 16, 2026
1.0.12 lands ashhart#598 and ashhart#614 in their reworked form (core/picture, the shared decoders), so the server, GLM and image
files take main's. The 27B's CUDA engine joins main's CUDA host: the family's options, fp8 caches, the image tower's
room and its own image offer beside Nemotron's, and its host-memory prompt cache on its zero-anchored grid
(cuda_snaps.cacheRules). --kv-cache fp8 moves into prefill_attention_rows.cuh, which every causal, tree and full
kernel now includes: an F8 path widens e4m3 rows with their scales as they stage, the rest unchanged. Nemotron's
worker takes its Source through Target.of.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants