| Age | Commit message (Collapse) | Author | Files | Lines |
|
Providers come from a [providers.*] table. A bare top-level base_url
synthesizes the local provider so existing configs keep working, and an
explicit [providers.local] wins over it. Unset numbers stay None so
'unknown' never collapses into zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Titles: once a reply lands in a session still wearing its placeholder
title, the exchange goes back to the model in a short side request asking
for six words or fewer. Keyed on the placeholder rather than the turn
number, so an empty first reply does not forfeit titling for the session
and a one-shot window is titled from the question that was answered.
Thinking is disabled for that request, or a reasoning model spends the
whole budget thinking and returns nothing.
Empty replies: the final search round now says so in the tool result.
Withdrawing the tool schema is invisible to the model, which asks for
another search regardless; the request then surfaces as literal
<tool_call> text or vanishes into the thinking block, leaving the reply
empty either way. The note rides on the tool result because Qwen3.5's
template rejects a trailing system message outright.
max_searches now defaults to 2. The other half of that bug was
llama-server's quantized KV cache: with cache-type-k/v = q8_0 three of
five turns broke, and f16 answered six of six.
Measured against a live router, not mocked.
|
|
A second tool call in the same turn, issued after the model has seen the
first set of results, does not arrive as a tool_calls delta. It comes back
as literal <tool_call><function=web_search> text inside reasoning_content,
with finish_reason stop, no tool_calls and empty content. There is no
structured call for the loop to act on, so the turn ends and the user is
shown a blank reply.
Reproduced 3/3 with Qwen3.5-9B through the router; the first call of a turn
parses correctly every time, so this is specific to a call that follows a
tool result.
A retry with the tool withdrawn was tried and rejected: it produced an
answer only some of the time and could surface the raw XML in the reply,
which is worse than the blank it replaces. Capping at one search avoids
reaching the broken state at all. Empty replies drop from 3/3 to about 1/3
and the XML no longer leaks, but the failure is not eliminated.
Raise max_searches once the chat template parses follow-up calls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The model is offered a web_search tool and decides when a question needs
current information. llamachat runs the query against SearXNG's JSON API,
feeds the results back, and the model answers from them. Off unless both
search_enabled and search_url are set, so an upgrade never starts talking
to the network on its own.
The loop lives in backend.stream_chat: a round ending in tool_calls is
searched for, the result appended, and the request re-sent. After
max_searches rounds the tool is withdrawn, which forces an answer rather
than letting an uncertain model search forever.
Searches show as a collapsible block above the thinking block, listing
each query and its sources. The queries are visible on purpose: when an
answer is wrong it is usually the query that was wrong, and without
seeing it a bad search and a bad answer look identical.
Two failures found while testing against the live stack shaped the
design:
The model has no clock, so it falls back on its training cutoff and
writes that year into the query itself ("latest kernel ... 2025"),
poisoning the results before they are fetched. The current date now goes
into the system prompt whenever search is on, with an instruction not to
date its own queries.
A SearXNG whose engines are all rate-limited or CAPTCHA'd returns a
valid response with zero results. Reporting that as "no results" tells
the model the web is empty and invites a confident answer from stale
training data, so a search where every engine failed is now an error
naming the engines.
Results are attacker-influenced text entering the model's context. Only
title, url and the snippet survive, snippets are truncated, result text
is escaped on display, result links are never fetched automatically, and
a reply forging the search block's URL scheme has it defused as the
reasoning scheme already was. None of that stops a poisoned snippet from
influencing the answer, which is why the sources stay visible.
Adds eight test groups, 23 to 31, all hermetic behind a fake HTTP layer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A delta carrying both reasoning_content and an empty content was
classified by truthiness, so the reasoning fell through to the content
branch. The tail of the model's thinking was stored and displayed as the
answer, splitting mid-sentence across the two fields.
Classify on the presence of the field instead, and return nothing for an
empty reasoning delta rather than falling through.
The visible symptom was a code block swallowing the message: leaked
thinking is dense with backticks, and an odd count leaves a run open to
the end of the reply. Close an unterminated fence before parsing, with a
closer matching the opener's length, and drop a lone dangling inline
backtick. Streaming needs this regardless, since a fence is unclosed on
nearly every frame.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Ctrl+N starts a fresh conversation and puts the cursor in the input box.
Ctrl+F jumps to the search field.
The search field sits in the top bar, but its results render in the history
panel, so focusing it reveals a hidden panel rather than leaving the search
with nowhere to show its hits.
Escape now backs out of the search field before it hides the window: first
press clears the text, second returns to the input, third hides. Previously
a stray Escape while filtering dismissed the whole window.
That check reads focusWidget() rather than hasFocus(), so it still holds
when the window is not the active one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The history panel now hides, on the ☰ button in the top bar and on Ctrl+\.
Its width is captured before hiding and reapplied on show, so toggling does
not snap the panel back to a default. Both the width and whether it was
hidden persist between runs.
Layout state lives in state.ini beside the config rather than at QSettings'
default path, which keeps it obvious where it is and lets the checks point
it somewhere temporary instead of writing to the real one.
The button's checked state, not isVisible(), is the authority for whether
the panel is shown: isVisible() is False for every child of a window that
has not been mapped yet, so consulting it during startup would disagree
with what the user sees.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Two additions to the chat window.
A context meter in the top bar shows how much of the active model's window
the next request will occupy: system prompt, prior turns, attachments and
the current draft. The router does not report a usable context size in
router mode (/props returns n_ctx 0), so the limit comes from ctx-size in
presets.ini. Streaming requests now ask for usage statistics, which makes
the figure exact after each reply at no extra round trip; before that it is
an estimate marked with a leading ~.
The estimate reads low on models with a reasoning budget, since thinking
tokens cannot be known before the reply arrives. The tooltip says so.
System prompts are markdown files in the prompts/ directory beside the
config, one per file, so they can be edited in an editor and kept in
version control. default.md is global, any other file is a named preset
that replaces it rather than adding to it, and a conversation may instead
carry one-off text or opt out entirely. The choice is stored per session so
reopening a chat restores the prompt it was built with.
Prompt names are reduced to safe filenames, so a name like ../../etc/passwd
cannot write outside the prompts directory, and a name that reduces to
nothing is rejected rather than creating a dotfile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A native PySide6 chat client for a local llama.cpp server in router mode.
Runs as a single persistent process with a Unix-socket control channel, so
a Hyprland keybind toggles the window with a socket round trip rather than
a process start. The control commands do not import Qt and answer in under
a tenth of a second.
Features: one-shot and multi-turn chat modes, runtime model discovery from
/v1/models, streaming replies rendered as markdown, collapsible reasoning
for models with a thinking budget, drag-and-drop file and image attachment
with context-aware truncation, and SQLite history with FTS5 search.
Markdown is parsed with MarkdownNoHTML so markup in a reply is displayed
rather than interpreted, and links a reply produces cannot drive the
interface.
Includes the Hyprland Lua snippets for autostart, keybind and window rules,
and a self-check suite covering everything except the GUI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|