feat(threads): record and display model provenance in threads - #174
feat(threads): record and display model provenance in threads#174cashcon57 wants to merge 2 commits into
Conversation
f3ccd7e to
527b7ae
Compare
527b7ae to
9e7fa99
Compare
|
Apologies for the edits, I forgot to push the final edit that is represented in the image above. All good now! |
9e7fa99 to
551ad84
Compare
|
Rebased onto current main to resolve conflicts. Typecheck passing. |
|
Thanks for your patience on this one, @cashcon57 — it sat longer than it should have, and it deserved better, because the craft here is high. Verified on your branch merged onto current
One structural thing needs changing before I can take it, plus a design call and two nits. 1. We're already recording this
// The model/provider a message was produced by was previously not
// recorded anywhere, so a finished turn could not be attributed.
modelId,
providerId,Nothing consumes those yet — they were added in anticipation of exactly this feature. Your Two representations of one fact will drift, and there's a concrete payoff to collapsing them: threads already in users' stores carry What I'd like:
2. Should a single-model thread get a divider at all?
For the large majority of threads — one model, start to finish — that's a persistent line at the top of the conversation restating what the model selector already shows. My preference is to emit markers only where provenance actually changes: walk the thread, and if there's no change, render nothing. The first marker then becomes meaningful precisely because it's rare. If you'd rather keep the "served" line, make the case — you've clearly thought about this more than I have. But the current behaviour should be a decision rather than a side effect, and the test name should match whichever way it goes. 3. The stamp may flicker mid-streamYou return Folding this into point 1 makes the problem disappear, since Nits
Point 1 is the one I need; 2 is a question I'd like answered rather than a diff; 3 mostly dissolves with 1. Thanks for building this — the thread-level attribution gap is real, and you're the one who noticed 🧭 Generated by Claude Code |
Stamp metadata.modelProvenance = { modelId, providerId, backend? } onto
assistant messages as they are generated, riding the same metadata
pipeline that already persists finishReason/tokenSpeed/usage. The stamp
is emitted at stream start (so an aborted response stays attributed for
the rest of the session) and repeated on finish.
At render time, derive divider positions from the stamps: the first
stamped response yields a 'Served by <model>' line above its prompt,
and any later change of model, provider, or backend build yields a
'Switched to <model>' line at that point. A regenerate with a different
model anchors to the regenerated response. Threads that predate the
stamp pick up provenance from their next response onward.
The divider is a quiet hairline rule with the model id in small muted
text; provider and backend build (differentiating llama.cpp TurboQuant
builds from upstream) are shown in a tooltip. All display comes from
the persisted stamp, never live settings, so later backend upgrades
cannot rewrite history.
Drop the separate modelProvenance object and read modelId/providerId straight from the finish metadata the transport already writes, adding the backend build as a sibling field. Existing threads gain provenance dividers retroactively, and the stamp no longer depends on the start callback surviving streaming. Keep the served marker for single-model threads (see PR discussion), rename the test to match, use the house dark-mode logo treatment, and drop the second divider logo.
551ad84 to
d961a38
Compare
|
Thanks for the quick turnaround, @cashcon57 — point 1 landed exactly as I'd hoped, and the result is better than what I asked for. Verified on your branch (it's now based on
I also checked the retroactivity claim end-to-end rather than taking it on trust, because it's the most valuable part of this change and the easiest to get wrong. The round trip holds: Three things left. None is large, and only the first needs a diff. 1.
|
Visual
Describe Your Changes
Adds model provenance to threads: a quiet divider line showing which model
(and backend) serves the responses that follow, as discussed on Discord.
Recording. Each assistant message gets
metadata.modelProvenance = { modelId, providerId, backend? }stamped as it isgenerated, riding the same metadata pipeline that already persists
finishReason/tokenSpeed/usage. The stamp is emitted at stream start(so it survives an aborted generation) and repeated on finish.
backendis theversion_backendsetting of the serving provider (e.g. the llama.cppTurboQuant build tag), read at send time so a later backend upgrade cannot
rewrite the history of already-generated messages. No storage schema change.
Display. Divider positions are derived from the stamps at render time
(memoized walk in the thread view):
Served by <model id>above the prompt that led to it.Switched to <model id>above the prompt following the switch. The backend build being part of the
identity is deliberate: the same model on a different TurboQuant build is a
provenance change worth surfacing.
response itself (the original response above it was served by the old model).
this feature pick up provenance from their next response onward.
UI. Hairline rule with the model id in small muted text, bookended by the
app's asterisk mark (
transparent-logo.png, faded, theme-inverted), deliberatelyquiet so it does not compete with the conversation. Hovering shows a tooltip
with the model id, the standard provider display name (via
getProviderTitle,so TurboQuant and upstream llama.cpp are distinguished), and the backend build
when recorded. Strings are localized (
enonly; other locales fall back toEnglish per the i18n setup).
Tests. Unit tests for the derivation walk: first response, no-change,
model switch, provider-only change, backend-build-only change, regenerate
anchoring, pre-feature history, malformed stamps.
Fixes Issues
Self Checklist