Skip to content

feat: model name labels, SSE custom event filter, emoji chat titles#8286

Open
FedeCuci wants to merge 2 commits into
janhq:mainfrom
FedeCuci:feat/model-label-sse-emoji-titles
Open

feat: model name labels, SSE custom event filter, emoji chat titles#8286
FedeCuci wants to merge 2 commits into
janhq:mainfrom
FedeCuci:feat/model-label-sse-emoji-titles

Conversation

@FedeCuci
Copy link
Copy Markdown

@FedeCuci FedeCuci commented Jun 6, 2026

Describe Your Changes

  1. Show model name + provider logo above each assistant response. This is useful in case a user frequently uses different models in the same chat and wants to see which model was the one outputting a specific response. Other interfaces such as Open Web UI also do this.
  2. Filter custom SSE event types to fix bug: SSE parser fails on custom event types from OpenAI-compatible API servers #8280 (OpenAI-compatible servers).
  3. Auto-prefix generated chat titles with a relevant emoji. This is also similar to how Open Web UI does it. I believe adding an emoji to the chat titles helps the user remember chats with a visual cue rather than a title they never read.

Fixes Issues

Self Checklist

  • Added relevant comments, esp in complex areas
  • Updated docs (for bug fixes / features) - n.a, no API or config changes
  • Created issues for follow-up changes or refactoring needed - none needed

1. Show model name + provider logo above each assistant response
2. Filter custom SSE event types to fix janhq#8280 (OpenAI-compatible servers)
3. Auto-prefix generated chat titles with a relevant emoji
4. Add emoji font fallback for cross-platform rendering
@tokamak-pm
Copy link
Copy Markdown

tokamak-pm Bot commented Jun 7, 2026

PR Review: feat: model name labels, SSE custom event filter, emoji chat titles

Summary

This PR bundles three features in a single commit:

  1. Model name labels -- displays the model name and provider logo above each assistant message in the chat UI.
  2. SSE custom event filter -- adds a TransformStream that strips non-standard SSE event types (e.g. hermes.tool.progress) before they reach the AI SDK parser, fixing bug: SSE parser fails on custom event types from OpenAI-compatible API servers #8280.
  3. Emoji chat titles -- modifies the title summarization prompt to request an emoji prefix and updates cleanTitle() to preserve emoji characters.

Files changed: 7 (356 additions, 6 deletions)


Code Quality & Correctness

SSE Event Filter (model-factory.ts) -- This is the strongest part of the PR. The createSSEEventFilter() TransformStream is well-documented, handles chunked delivery, CRLF normalization, and incomplete blocks in the flush() handler. The integration into createCustomFetch is clean -- it only activates for text/event-stream responses. The test suite (sse-event-filter.test.ts) is thorough with 202 lines covering split chunks, CRLF, edge cases, and integration with createCustomFetch.

Emoji in titles (thread-title-summarizer.ts) -- The regex change from [^\p{L}\p{N}\s] to [^\p{L}\p{N}\p{Extended_Pictographic}\uFE0F\u200D\s] is correct and handles ZWJ sequences. Tests cover the key cases.

Emoji font fallback (index.css) -- Adding "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji" to --font-sans is a sensible cross-platform fix.


Issues Found

Bug: useModelProvider.getState() called inside useMemo (MessageItem.tsx)

const modelDisplayName = useMemo(() => {
  if (messageModelId) {
    const provider = useModelProvider.getState().providers.find(
      (p) => p.provider === messageModelProvider
    )
    ...
  }
}, [messageModelId, messageModelProvider, isLastMessage, selectedModel])

Calling useModelProvider.getState() (a Zustand store) inside useMemo is a React anti-pattern. The providers list is read imperatively and is not reactive -- if providers load or change after this component renders, the memoized value won't update. The dependency array only tracks messageModelId etc., not providers. This should either:

  • Subscribe to providers via useModelProvider((s) => s.providers) at the component level and pass it into the useMemo, or
  • Accept that for messages with metadata already set, this is effectively a one-time lookup (which may be acceptable if providers are always loaded before messages render, but it's fragile).

Dead code: modelProviderForDisplay always resolves to undefined for non-metadata messages

const modelProviderForDisplay = messageModelProvider ?? (isLastMessage ? undefined : undefined)

The ternary (isLastMessage ? undefined : undefined) always evaluates to undefined. This appears to be leftover from a draft where the fallback branch did something else. As a result, the provider logo/icon never renders for the backwards-compatibility case (last message without metadata), even though modelDisplayName is computed for it. This is a logic gap -- the label will show the model name but no icon for the current message during streaming until metadata is persisted.

Concern: PR bundles 3 independent features

These are three conceptually independent features (UI labels, SSE transport fix, title generation tweak) in a single commit and PR. This makes it harder to revert one feature without the others and complicates bisection. Ideally these would be separate PRs or at minimum separate commits.

Minor: getProviderLogo called twice in render path

{modelProviderForDisplay && getProviderLogo(modelProviderForDisplay) ? (
  <img src={getProviderLogo(modelProviderForDisplay)} ... />

getProviderLogo() is called twice with the same argument -- once for the conditional check and once for the src attribute. This is a minor inefficiency; extract it to a variable.

Minor: No test for the model label UI component

The SSE filter has excellent tests, but the MessageItem model label rendering has no unit/integration test. Given the useMemo complexity, a test that verifies the label renders correctly (with metadata, without metadata, for last message, etc.) would be valuable.


Risks & Regressions

  • SSE filter is low risk -- it only activates on text/event-stream responses and correctly passes through standard blocks. Unlikely to regress existing behavior.
  • Emoji title change is low risk -- the prompt change is additive and cleanTitle now accepts a superset of characters. Worst case: the model ignores the emoji instruction and titles work as before.
  • Model label has moderate risk -- the useModelProvider.getState() inside useMemo could cause stale data if providers load asynchronously. The dead modelProviderForDisplay ternary means the feature is partially broken for the fallback case.

Recommendation: improve needed

The SSE event filter is production-ready and well-tested. The emoji title feature is clean. However, the model name label feature has two code issues that should be addressed before merging:

  1. Fix the modelProviderForDisplay dead ternary to actually provide the current provider for the last-message fallback case.
  2. Either make the providers lookup reactive or add a comment explaining why the imperative getState() is safe in this context.
  3. Extract the double getProviderLogo() call.
  4. Consider splitting this into separate PRs (or at least separate commits) for easier revert/bisect.

Items 1-3 are small fixes; item 4 is a process suggestion.

@FedeCuci
Copy link
Copy Markdown
Author

FedeCuci commented Jun 7, 2026

Addressed the code-level review feedback:

  • Reactive providers lookup: replaced useModelProvider.getState() inside useMemo with a proper Zustand subscription (useModelProvider((s) => s.providers)) at the component level, adding providers to the dependency array so the label updates if providers change after first render.
  • Fixed dead modelProviderForDisplay ternary: the fallback branch always resolved to undefined, so the streaming last message showed a name but no provider logo. It now correctly falls back to selectedProvider from the store.
  • Extracted duplicate getProviderLogo() call into a modelProviderLogo variable used in both the conditional check and the src attribute.
  • Added 4 tests to the existing MessageItem.test.tsx covering: metadata-driven name + logo, unknown model ID falling back to raw id, last-message fallback to current selection, and no label for non-last messages without metadata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

bug: SSE parser fails on custom event types from OpenAI-compatible API servers

1 participant