| Age | Commit message (Collapse) | Author | Files | Lines |
|
Make the chat toolbar icon-only (tooltips retained) and add an icon_size config key, default 28, so the drawn glyphs can be scaled. Qt was drawing the 24px pixmaps at its ~16px default; the explicit icon size fixes that.
Fix the deferred audio minors: AudioRecorder.start() now halts a prior source and checks the source error, so a failed capture no longer claims to be recording. Send now validates every pending modality in one gate, so a mixed image+audio clip cannot reach a model without vision. Sub-second clips no longer name voice-note-0s.wav, and non-WAV audio containers classify as unknown, guarded by a test.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Records why the block style carries white-space:pre-wrap and why the
padding is margins plus a spacer line: Qt's markdown stylesheet does not
cover pre, and Qt ignores padding and vertical margins on it. Also
describes the copy button and the constraints behind its shape, since
neither a widget nor :hover is available in Qt rich text.
|
|
The copy link was a word-plus-glyph sitting above the block, always at
full strength, with no feedback once clicked. It is now an icon-only
button under the block, since Qt cannot float it over the tint.
Qt draws almost no box properties on <pre>: padding and vertical margins
are ignored, so the inset is margin-left/right and the room above and
below is a tinted spacer line. The glyph alone renders small whatever
font-size says, so the button is a tinted pill instead: grey at rest,
the highlight colour under the pointer, and a tick on green once used.
Qt rich text has no :hover, so hovering is QTextBrowser.highlighted()
driving a re-render, the same way the toggles already re-render.
The tick reverts after a moment rather than standing indefinitely, which
would claim the clipboard still holds that block long after it stopped
being true. Copying a second block first cancels the first one's revert.
|
|
Two things made a fenced code block awkward to read. Qt's markdown
stylesheet wraps p and li but not pre, and that stylesheet is dropped
when only the body fragment is kept, so a long line widened the whole
transcript and left it scrolling sideways. The pre style now carries
white-space:pre-wrap itself.
Copying a block meant selecting it by hand. Each block now gets a copy
link above it, on the right, in the same scheme-dispatch the thinking
and search toggles already use, since Qt rich text has no widgets to
put a real button in. The block text is read back from the source
markdown because Qt emits one <pre> per line, which makes the block
unrecoverable from the rendered HTML; _close_fences runs first, so a
block still streaming is copyable mid-reply. Like the other schemes,
a forged copy link in a reply is defused.
|
|
Status messages now show a dismiss button; an error persists until
dismissed or replaced, and a transient message can be cleared early.
|
|
A searched turn makes one API call per search round and all of them are
billed, but prompt_tokens/completion_tokens held only the last round's
counts, so a searched turn under-reported its cost. The columns now hold
the sum across rounds; usage_json keeps each round's raw usage block as
a JSON array for external consumers to verify against. A single-round
turn stores the same numbers as before.
|
|
A searched turn on a cloud reasoning model ended at the thinking: the
model emitted the tool call, but finish_reason: "tool_calls" landed on
the same SSE line as the include_usage block, so the single-event parser
returned that line as a usage chunk and the loop never saw the tool
finish. The parser now emits every event a line carries, so the search
fires.
Also in this change:
- Replay each round's reasoning_content on the assistant tool-call
message, which interleaved-thinking models require to keep going.
- Add a per-provider replay_reasoning option (DeepSeek, SiliconFlow
GLM-4.7+) to carry prior turns' reasoning_content when search is on.
- Add an on-demand diagnostic log gated by $LLAMACHAT_DEBUG_LOG.
- Record provider, usage_json and reported_cost_usd per reply, so a
searched turn keeps every round's billed usage for external consumers.
|
|
- Send max_tokens = 32768 for non-local providers so reasoning models have room for both chain-of-thought and answer.
- Add per-provider thinking_budget option for endpoints (currently SiliconFlow) that cap reasoning tokens separately.
- Accept bare JSON arrays from /v1/models; some OpenAI-compatible endpoints omit the {"data": [...]} envelope.
|
|
The generated config ships the provider block commented out so a fresh
install never reaches an endpoint nobody configured. The README states
plainly that every turn resends the whole conversation, which is free
locally and billed per message on a cloud provider.
|
|
The projection prices the whole request, not just the draft, because
reopening a conversation resends its entire history and that is billed
per turn on a cloud provider. Seeing it before sending is the whole point
of the readout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Fires on selection rather than on send, so the interruption lands while
the user is already changing settings instead of mid-thought. Local
models never trigger it, and a cancelled dialog is recorded so a model
tried once never asks again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Metadata now resolves models.ini over provider defaults over presets.ini,
so a local model keeps getting its context and vision from the preset
while remaining overridable. A model whose vision support is unknown no
longer blocks an attachment: unknown is not the same as no, and the API
will reject an image if it really cannot take one.
Key resolution moves to the worker thread so a pass: key's pinentry
never freezes the GUI. KeyResolver gains a lock since StreamWorker and
TitleWorker can now overlap. Skipped providers surface in the status bar
rather than only on stderr. model_info uses dataclasses.replace to avoid
mutating the ModelInfo that resolve() may return from the store.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Three states stay distinct: blank for a free local model, ? for a cloud
model whose prices were never entered, and a figure when they were.
Collapsing the first two would make an unpriced cloud model read as free.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Field conversion is separated from the widget so the part with the edge
cases is testable without a running Qt application. Vision is tri-state
through a binary checkbox: an unchecked box with an unknown prefill stays
unknown rather than writing False, which would shadow a provider-level
vision=True through models.resolve's pick().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A provider that is unreachable or whose filter matched nothing is
reported rather than fatal: local models must stay usable when the
network is down. Clients are built on demand so a key is resolved only
when that provider is really used.
The model listing carries its own timeout, separate from the per-turn
one: the picker has to populate fast, and a dead cloud provider must
not hang it for minutes. Client.models() takes list_timeout (default 30)
and uses httpx.Timeout(list_timeout, connect=5), and MultiClient threads
its own list_timeout through to each listing call. The fan-out stays
serial on purpose: KeyResolver._cache is unlocked and only safe while
every caller resolves on the GUI thread.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The local router needs none, so the header is omitted entirely rather
than sent empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The convention was documented but not its consequence: because None means
keep, no caller can write NULL through this method, and that is invisible
from the signature. Unreachable today since every field is filled once
when a reply finishes and never cleared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Cost has to survive reopening a conversation, which means storing what
each reply used. The model goes on the message rather than the session
because sessions records only the current one, and a conversation that
switched models would otherwise be priced entirely at whichever is
selected now. All three columns are nullable so old rows read as unknown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A nan or inf in [providers.*] survived float() and reached the cost
arithmetic, rendering $nan or, for -inf, a negative cost. Worse than a
display wart: is_priced answered True, so the readout took the priced
branch and showed a poisoned figure in the one state the ? is there to
admit it cannot say.
Guarded at both ends. providers._number mirrors models._get, which
already covered the models.ini writer. is_priced states its own
predicate completely rather than trusting its callers, since the dialog
is a third writer and the file is documented for hand-editing.
Records why float money stays, and that exact-equality assertions in the
cost test hold for the chosen prices rather than in general.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Resolution merges per field, not per source, so an entry setting only
ctx_size still inherits the provider's prices. Cost prices each reply at
the model that produced it, and rows predating the token columns
contribute zero rather than a fabricated number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
float() accepts "nan" and "inf", so a hand-edited models.ini could feed
either into the cost arithmetic and render "$nan". Rejected once in _get
rather than at each downstream call site.
A model whose id is literally DEFAULT wrote a [DEFAULT] section, which a
stock configparser reader treats as defaults inherited by every other
model. Since the file is documented for hand-editing, that reintroduces
the contamination the renamed default_section exists to prevent, so the
id is escaped on write and mapped back on read.
Also drops the marker branch in save(), leaving mark_skipped() as the
sole writer of the "marker present iff no real keys" invariant, and
prunes two test blocks that could not fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Cloud models have no presets.ini entry, so context size, vision and
prices are recorded per model in a file the app writes. A cancelled
dialog leaves a marker so a model tried once never asks again, which is
distinct from an absent section meaning never asked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|