Skip to content

v3.16.0

Latest

Choose a tag to compare

@syswave-dev syswave-dev released this 25 Oct 17:54
· 2 commits to main since this release

Added

  • Reddit post images are described and transcribed. On an image post the image is often the actual content: PullMD used to return the title, a bare ![](...) and comments that all replied to a screenshot the reader could not see, so a downstream LLM had no way to follow the thread. With a vision provider configured, each eligible image of a Reddit post (a single image, a gallery, or an image referenced inline in a text post; comment images are not touched) now gets a short description (one or two sentences) plus a transcript of its legible text, set directly under the image as a blockquote labelled Bildbeschreibung / Bildtranskript (Image description / Image transcript with lang=en). The labels mark the text as machine-derived from the image, and every line is >-prefixed so transcribed text can never open a heading or be mistaken for the comments section. The feature is opt-in because it is the first time PullMD sends images found inside a page to a provider (until now vision only ran on uploads and direct image URLs). PULLMD_EMBEDDED_IMAGES is off (hard off, a request cannot switch it on), on-request (the default: no provider calls unless a request passes images=on, or images: "on" on the MCP read_url tool) or always (on unless a request passes images=off); an invalid value warns at startup and falls back to on-request. Behaviour change on upgrade: on an instance with a vision provider and the default on-request, image posts now carry a one-line Image not transcribed marker under each image that has no transcript, so an LLM reading the output (MCP returns no frontmatter by default) can tell context is missing and ask for it; without a vision provider, or with off, the body is byte-identical to 3.15.2 (the one exception: transcripts stored earlier, while a provider was configured, are still shown unless the instance is set to off). Frontmatter gains images_transcribed: "<done>/<total>" (also without a provider, as a hint that images were skipped) and the usual llm_* usage fields summed over the images. Limits: Reddit only, hosts i.redd.it and preview.redd.it only (checked on every redirect hop, on top of the SSRF guard), single images must be jpg/jpeg/png/webp, gallery and inline images must be non-animated images according to Reddit's metadata, and every download must be served as image/* (no GIF or video), at most PULLMD_EMBEDDED_IMAGES_MAX images per post (default 4, range 1-20; later gallery items stay marked), downloads capped at 20 MB, 90 seconds overall per post. Each new image costs one unit of the hourly PULLMD_LLM_RATE_LIMIT budget. Transcripts are stored per image, so the same image is transcribed once, whichever URL form of the post is requested (old.reddit.com, a redd.it short link, a /s/ share link, with or without a trailing slash or tracking parameters), for as long as cache retention keeps it: any later extraction (a cache miss, nocache, a share refresh) reuses the stored transcripts with no download, no model call and no budget, also for a request that passes images=off or does not ask for images (an instance set to off ignores them). Each result is stored as soon as it is in, so a gallery with one failing image keeps the others. An image that is unusable (HTTP 4xx other than 408/429, not an image, over 20 MB, host or SSRF rejection, or a provider answer of 400, 413, 415 or 422) counts as failed for that cached result only: it is not retried while that row is fresh, but it is retried after the row expires or with another URL form of the post. Failures are never stored per image, so neither a misconfigured model nor a post that points at another post's image id under a failing URL can mark an image failed for everyone. Clearing the whole cache forgets all stored transcripts. With always, a share link refresh (/s/:id, no sign-in needed) can transcribe new images too, charged to the viewer's address. A transient miss (budget exhausted, the 90 second deadline, an HTTP 5xx, a DNS failure, any other provider error) still serves the result but does not cache the page, and a share refresh keeps the old snapshot and restarts its one-hour refresh clock, so it is not refetched on every view. A cached row with fewer resolved images than the instance would now produce is treated as a miss for an images=on request, so images does not need nocache. The transcribed text is third-party content from an image and can carry prompt injection; treat it like any other fetched page text.
  • README: choosing a vision model. A small measured comparison (nine Reddit images, prompt tokens, time, cost, observed errors) of claude-haiku-5-5, gpt-4.1-mini, gpt-4o-mini and claude-sonnet-5-5, plus the settings for Claude models through Anthropic's OpenAI-compatible endpoint. The gpt-4o-mini default is unchanged.
  • Startup warning for open paid tiers (#72). With PULLMD_AUTH_MODE disabled and a provider key set (PULLMD_VISION_API_KEY, PULLMD_STT_API_KEY, PULLMD_LLM_API_KEY, PULLMD_PDF_OCR_API_KEY), the server now logs one [auth] line: every client that can reach the port can use those tiers, bounded only by PULLMD_LLM_RATE_LIMIT (or with no limit when it is 0). docker-compose.yml shows the loopback port binding 127.0.0.1:${PORT:-3000}:3000 as a comment. No defaults change.

Fixed

  • Image captions were cut off mid-sentence with verbose models. The plain caption path (image uploads and URLs that point straight at an image) capped the answer at 500 output tokens. Claude Haiku 5.5 writes long, structured captions and hit that cap, so the caption ended mid-sentence. The cap is now 1500 (the max_completion_tokens retry for reasoning models moves from 4000 to 5000, keeping the same reasoning headroom). Models that finish earlier stop on their own, so nothing changes for them.
  • Reddit text arrived HTML-escaped. Reddit's JSON API escapes &, < and > in every text field unless the request sets raw_json=1, so comment quotes came out as &gt;quote instead of a Markdown quote, links carried &amp; between their parameters, and the editor's empty-paragraph marker showed up as &amp;#x200B;. All Reddit JSON requests (posts, subreddit about and listing) now pass raw_json=1, and lines holding only a zero-width space (&#x200B;) are dropped from post and comment text.