Skip to content

Provider reconfig: agent-config sync, Hermes One auto-provisioning, provider UI redesign & chat stream fixes - #862

Merged
fathah merged 3 commits into
mainfrom
provider-reconfig-design
Jul 22, 2026
Merged

Provider reconfig: agent-config sync, Hermes One auto-provisioning, provider UI redesign & chat stream fixes#862
fathah merged 3 commits into
mainfrom
provider-reconfig-design

Conversation

@fathah

@fathah fathah commented Jul 21, 2026

Copy link
Copy Markdown
Owner

This branch makes providers behave as one system across the desktop, the bundled hermes-agent, and the Hermes One backend — plus a light-based redesign of the provider surfaces and two chat-streaming correctness fixes the work surfaced along the way.

🔑 Provider routing fixes

Native provider keys were invisible in the model picker. providerRouteForEnvKey only knew keys carried by a PROVIDERS.setup card or a local preset; everything else fell to a dead custom + empty-base-URL route, which the active-model picker silently drops. A configured Nous Portal API key never appeared in the "Select default model" modal. A new NATIVE_ENV_KEY_ROUTES table maps each orphaned key to its hermes-agent slug (NOUS_API_KEY → nous, GLM_API_KEY → zai, KIMI_API_KEY → kimi-coding, MINIMAX[_CN]_API_KEY → minimax[-cn], NVIDIA, OPENCODE_ZEN/GO, HF_TOKEN → huggingface, PERPLEXITY → custom + base URL), verified against the agent's own plugins/model-providers/* registries. A test asserts no LLM-section key can ever land on the dead fallback again.

Session switches to custom-endpoint models routed to the wrong provider. /model <id> --provider custom lets the agent bind custom to the session's current base URL — so switching to hermesone-swift while a session sat on Nous sent the request to the Nous proxy (HTTP 404 … OpenRouter catalog). Fixed at both ends:

  • the desktop mirrors a keyed Hermes One into the agent's config.yaml as a named providers: hermesone: entry (base_url + key_env), so the gateway always has a slug that carries the right endpoint;
  • resolveDashboardProviderForModel now matches any gateway provider row by base URL (named user providers included, not just custom:* rows) before ever falling back to bare custom.
  • the config.yaml scaffold's inline providers: {} is rewritten into block form on upsert — the line-based parser couldn't index it, which silently disabled the mirror on real configs (and appending would have produced a duplicate top-level key).

🔄 Desktop ↔ hermes-agent provider sync

Named OpenAI-compatible providers now sync both ways with the agent's config.yaml via a new text-based bridge (src/main/agent-config-providers.ts) that preserves comments and unrelated keys:

  • Terminal → desktop: providers: dict entries and legacy custom_providers: list entries added via the CLI are imported on every provider-list / model-library read — cards, picker entries, and model rows appear without reseeding. A terminal entry's custom key_env value is aliased additively to the desktop's derived CUSTOM_PROVIDER_<NAME>_KEY so keys resolve everywhere. Previously custom_providers: was only honored once, at first models.json seed.
  • Desktop → terminal: adding/editing a custom provider in the UI upserts a matching providers: entry (patching fields in place — a terminal user's extra fields like transport: survive); removing a provider deletes it from both config.yaml shapes so it can't re-import.

🪪 Hermes One account: auto-provisioned key + credits

Signing in should yield model access without hand-copying keys:

  • On device login (and whenever the Providers screen loads signed-in with no key), the desktop calls the backend's POST /api/credits/keys with the bearer token and stores the one-time hs-live-… key as HERMESONE_API_KEY — which by itself makes the Hermes One card and picker entry appear. Idempotent (an existing key is never replaced), single-flight (concurrent calls can't double-issue), and local-mode only (a remote/SSH profile would strand orphan keys).
  • The account card now shows the account's AI-credit balance ($X.XX credits chip via GET /api/credits/balance).

🎨 Provider UI redesign (light-based, stroke-free)

  • Cards & Add-provider picker: titles show the plain provider name ("Hermes One", not "Hermes One API Key" — labels aren't suffix-strippable across locales); well-known providers are ordered first (Hermes One leads, AIML API demoted last) via a stable rank sort; search matches both the name and the full label.
  • Provider config modal: brand logo tile + name + live "Key verified" pill in the header; masked API key (hs-••••••••3k8d) with Show / Replace and a "Used by N models" meta line; models as compact chips with an inline "+ Add model ID" pill (autocomplete, Enter/blur commits) collapsing behind "+N more" past 10; hairline header/footer dividers, fills instead of strokes, 11–13px control type.
  • Select-default-model modal: rebuilt stroke-free (filled controls, elevation-only dropdown) and — functionally — the provider dropdown is no longer clipped by the modal's overflow: hidden, so long provider lists actually scroll.
  • Model Registry modal: the search field now shares the header/body inset instead of running edge-to-edge.
  • Profile switcher: the switch modal is now a fixed-size, command-palette-style picker — Cmd/Ctrl+P from anywhere, fuzzy search over name/id/model, running vs Stopped groups, keyboard navigation — in the same stroke-free material as the sidebar session menu.

💬 Chat streaming correctness

Both fixes key on the same insight: text assembled from dropped chunks is, by construction, a subsequence of the canonical text.

  • Duplicate thoughts: a lossy live reasoning row ("moon-k3 … ous") could never key-match its canonical DB row ("moonshotai/kimi-k3 … nous"), so both survived the merge and stacked in one Thought block. Reconciliation now drops a streamed reasoning row whose text is a subsequence of a same-turn DB reasoning row — while keeping genuinely distinct live segments and never cross-cancelling between turns.
  • Duplicate answers: when the upstream tags alternate chunks as reasoning, the streamed bubble ends up a garbled subset of the final text, and mergeStreamedWithFinal concatenated the two ("! What are we working on?\n\nHey! What are we working on today?"). A guarded subsequence branch (≥12 chars, ≥30% coverage) replaces the garbled stream with the final — the pre-tool-call + answer pair still stacks.

🧪 Tests & docs

~60 new tests: env-key routing invariants, provider ordering, the config.yaml bridge (parse/upsert/remove/{}-scaffold round-trips), import/mirror/delete sync, key provisioning (single-flight, failure paths), dashboard provider resolution (incl. the Nous-404 regression), and the two subsequence-based stream reconciliations. All 1795 pass; lat.md/ updated throughout (provider-setup, hermes-account-login, chat-commands, sidebar-navigation) and lat check is green.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR unifies provider behavior across the desktop, Hermes Agent, and Hermes One. The main changes are:

  • Two-way synchronization for named providers and agent configuration.
  • Automatic Hermes One key provisioning and credit balance display.
  • Native provider routing and custom-endpoint model selection fixes.
  • Redesigned provider, model, profile, and sidebar surfaces.
  • Chat-stream reconciliation for lossy reasoning and answer chunks.

Confidence Score: 5/5

The reviewed fixes appear safe to merge.

  • Profile-specific provisioning no longer shares work across profiles.
  • Provider configuration values preserve the reported quote and backslash cases.
  • Empty profile searches no longer send an undefined selection.
  • No additional blocking issue cleared the follow-up review bar.

Important Files Changed

Filename Overview
src/main/hermesone-provision.ts Provisioning now isolates concurrent work by profile and clears completed operations.
src/main/agent-config-providers.ts Provider values now escape quotes and backslashes before being written to agent configuration.
src/renderer/src/screens/Chat/lossyText.ts The shared matcher now requires ordered contiguous runs and minimum coverage before treating text as a lossy copy.
src/renderer/src/screens/Layout/ProfileSwitcher.tsx Keyboard selection now ignores Enter when the filtered profile list is empty.

Reviews (3): Last reviewed commit: "Review Fixes" | Re-trigger Greptile

Comment thread src/main/hermesone-provision.ts Outdated
Comment thread src/main/agent-config-providers.ts Outdated
Comment thread src/renderer/src/screens/Chat/sessionHistory.ts Outdated
Comment thread src/renderer/src/screens/Chat/dashboardEventAdapter.ts Outdated
Comment thread src/renderer/src/screens/Layout/ProfileSwitcher.tsx
@fathah

fathah commented Jul 22, 2026

Copy link
Copy Markdown
Owner Author

@greptile-apps review

@fathah
fathah merged commit a335df3 into main Jul 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant