On the first day of a month, chats from the day before were listed under "Previous 7 days" in the sidebar instead of "Yesterday". The same happened on January 1st for chats from December 31st. Any chat from the previous calendar day is now listed under "Yesterday", whatever the month or year.
Fixes#31964
Read Aloud and response auto-playback always played at normal speed, regardless of the Speech Playback Speed setting. The speech is played one sentence at a time, and the browser put the speed back to normal every time a new sentence started. The chosen speed now stays in effect for every sentence. Voice mode already respected the setting.
Fixes#31870
In chat replies, inline math written right next to an em dash or en dash, with either $...$ or \(...\), was left unrendered, as in "constants—\(e\), \(i\) and \(0\)—in one equation" where the 0 showed up as plain "(0)". Both dashes are now allowed directly before or after a formula, like spaces, commas and brackets already are, so the math renders whenever a dash touches it.
iPhone photos were only converted to JPEG when the browser reported their type as exactly image/heic. Firefox reports image/heif and some browsers report no type at all, so those photos were uploaded as HEIC and vision models failed with an error. When conversion did run, the JPEG was still uploaded with the HEIC file name and type, so the model was told it was HEIC anyway. HEIC and HEIF photos are now converted whatever type the browser reports and are uploaded as a .jpg with the JPEG type, in chats, channels and notes.
Fixes#28411
On iOS and iPadOS, auto-playback of a finished reply is never heard and the speaker button stays stuck in "speaking", so the first tap only stops a playback that never started. WebKit rejects play() started from a network event, and the audio queue ignored that rejection, so its state never reset. Call mode on the same devices was silent too.
A rejected play() now resets the queue, returns the button to idle and shows a toast (an aborted play from stop or a message switch is ignored). The first user tap or keypress plays a 10 ms silent clip on the shared audio element, which WebKit then allows to play later without a gesture; if that attempt fails it retries on the next gesture. Call mode plays unmuted: WebKit pauses an element that is unmuted after play() outside a gesture, even once unlocked.
The unlock and call mode change follow the reporter's on-device tests (iPhone iOS 27, iPad iPadOS 26.6.2). Verified in Chromium with autoplay restricted: the base queue wedges and drops later chunks; with the fix it reports the error, recovers, the unlock plays once and never interrupts audio already playing, and chunks queued during the unlock still play.
Fixes#30262
Mermaid diagrams and the shared SVG sanitizer now accept only same-origin and data: references. Image URLs, class styles and directive config are checked on the parsed diagram before it renders, and the sanitizer drops attribute values and stylesheet rules that point at another origin. use elements keep local #id references only.
SVGPanZoom and the SVG file preview call the shared sanitizer instead of keeping their own configs, so SVG artifacts and uploaded SVG files follow the same rule. Uploaded SVGs that reference external sprites now render those parts blank, which is the point of the change.
A diagram that references an external resource now reports an error instead of rendering, and an SVG artifact keeps everything except the rules that reference one.
Uploading an .svg to a chat attached it as a vision image input, so the model received a data URI it could not decode. PIL-backed servers answered "cannot identify image file" and OpenAI answered "The image data you provided does not represent a valid image". No setting made it work.
SVG now takes the ordinary file upload path, so its XML source is extracted and indexed and the model can answer questions about it. Rasterizing was the alternative and it would have discarded the part of an SVG a model reads best, the source itself. Raster formats are untouched and still go up as image inputs.
A shared helper replaces the ad hoc image/ prefix checks at the points that decide image input versus document, on both ends. It normalises the content type first, because a stored "image/SVG+xml" or a trailing charset parameter slipped past a plain comparison.
One behaviour change worth knowing: an SVG now needs the model to have the file upload capability, where before it rode in as an image.
Fixes#30100
The branch navigation handlers each carried their own copy of the loop that walks a message's childrenIds down to the deepest child, eleven copies in total across the single-response view, the multi-response view and Chat.svelte. They now call one getDeepestChildId helper in the frontend utils, which tracks the ids it has already visited and stops when one repeats, the same way the message list build already does.
The helper also absorbs the "start from the last root message" fallback that two of the call sites repeated inline, so every site is now a single call.
Link URLs rendered from markdown, citations and web search results now pass a scheme check before they reach an anchor. A new safeLinkUrl helper sits beside isValidHttpUrl and keeps http, https, mailto, tel and relative URLs, returning undefined for anything else so the label renders without a link. Markdown links with an unusual scheme (ftp, sms, and application deep links such as obsidian or vscode) render as plain text from now on.
The citation checks move off a substring test for "http" onto isValidHttpUrl, which is what Citations.svelte already uses for the same question. That also drops two long-standing quirks: an uppercase HTTP:// source never rendered as a link, and a filename merely containing "http" rendered as a dead external one.
* fix: render channel mentions whose ID contains spaces or parentheses, such as workspace model IDs
* fix: accept any non-whitespace mention ID, matching the model ID rule on dev
Tool calls to an OpenAPI tool server put every argument the model returned into the JSON request body, including the parameters that were already substituted into the URL. Servers that validate their input strictly (additionalProperties: false) answered 422 "unexpected property", so reads worked and every write through an endpoint with a path or query parameter failed.
The body is now built from the model's arguments minus the operation's declared parameters, keeping any name the requestBody schema declares as a property of its own, so an endpoint that wants the resource id in the body as well as in the path still gets it.
The filter only runs when the resolved body schema lists its properties. A free-form, composed or non-JSON body offers nothing to check a name against, so those requests go out exactly as before.
src/lib/apis/index.ts carries the same request builder for direct tool server connections and had the same bug, so it gets the same fix.
Fixes#29716
The colocated vitest files (shortcuts, the colon fence marked extension) now
live in open-webui/tests under frontend/, next to the rest of the regression
suite, where they run against the source of any ref through the shared
regression workflow. This removes the copies here; the unit-tests workflow job
and the test:frontend script stay and pass with no tests.
src/lib/utils/_template_old.ts goes as well: it imports vitest but its name
never matched the test glob, so those tests have not run since they were added.
GET /api/version/updates returned the running version as latest whenever
the GitHub request failed, so an instance that cannot reach GitHub
reported itself up to date however far behind it was. The exception was
logged at debug, below the default level, so nothing recorded that the
check never happened.
The failure path now returns latest: None and logs at warning.
A null latest cannot be passed to compareVersion as it stood.
current.localeCompare(null) coerces to the string "null", and "0.10.2"
sorts before it, so the function returned true. The backend change alone
would have turned a false (latest) into a false update-available plus a
toast, so the guard is part of the fix.
The three callers stop substituting the running version in their catch,
and the two badge surfaces gain a third state. When latest is unknown
the badge is plain text, since there is no release to link to.
Admin Settings > General was wrong in a worse way than reported: it
initialised updateAvailable false with latest set to the running
version, and never checked on mount, so it claimed (latest) having made
no request at all. It now matches About.svelte, which starts unknown and
checks on mount.
Closes#29580
Newer OpenAI chat models put metadata on the opening line of a colon fence block, like :::writing{variant="email" id="48173" subject="Short question" recipient="mail@example.com"}. The tokenizer matched that line and discarded it, so every block rendered under the same generic "Writing" heading no matter what it contained.
The opening line is now parsed into an attributes map on the token and the header uses it: the subject becomes the title, the recipient follows it and the full string is reachable on hover when the row is too narrow for it. Blocks without metadata render exactly as before, and the other fence types get the parsed attributes for free.
Attributes are read only from inside the {...} braces, not from the whole opening line. Scanning the whole line turned ordinary prose containing key="value" into metadata, and it backtracked quadratically: a 40k character opening line took 586ms to parse, and that runs again on every re-lex while the message streams. Anchored to the braces it is 0.0ms.
Nothing here turns the recipient into a link or a send action. That metadata is model output and can be steered by whatever is in the context, so a prefilled mail action is a separate decision rather than a side effect of parsing.