Commit graph

14257 commits

Author SHA1 Message Date
Erosika
67d64124c3 feat(desktop): autosave the inline provider panel, drop its Save button
Everything else on the settings page writes through on change; the panel
hoarding edits behind a Save button meant navigation silently discarded
them. Discrete controls (switch, select) commit on change, text-like fields
commit on blur, each as a one-key partial save — silent on success, toast
on failure, no full refresh so sibling drafts survive. A committed secret
clears its draft and flips the set pill locally. The modal keeps explicit
Save changes: a dialog is a transaction with Cancel semantics.
2026-07-08 10:45:22 -04:00
Erosika
7faacb6dab fix(desktop): drop panel tint, cap info tooltip width
Inputs use the app-wide desktop-input-chrome CSS (background set directly,
utilities lose the cascade), so the tinted panel surface was what made them
read flat — remove the tint and let the accent border + row hairlines carry
the structure, matching how inputs sit everywhere else in settings. The Tip
primitive styles for 2-3 word labels (bold, no wrap cap); give the info
variant a width cap, normal weight, and snug leading.
2026-07-07 12:49:36 -04:00
Erosika
a182ddbf9f feat(memory): per-field info tooltips + name the profile in the full-config modal
Fields can declare a longer 'info' text rendered as an (i) tooltip next to
the label in both the panel and the modal; honcho uses it to spell out the
session strategy, write frequency, and recall mode semantics. The modal
description claimed 'the active profile' without saying which — show the
active gateway profile name.
2026-07-07 11:52:25 -04:00
Erosika
8333b04483 fix(desktop): solid background wells for provider config inputs
One tint step between panel and input was imperceptible; use the solid
background token so fields read as wells on the tinted section.
2026-07-07 11:52:25 -04:00
Erosika
1ba70a6f70 fix(desktop): inline error + retry for failed panel loads, calmer surfaces
A failed config fetch (e.g. racing backend boot) left the panel on an
eternal spinner with only a toast; render the error inline with a Retry
button instead. Drop the panel surface from bg-card to bg-quinary and give
field inputs a quaternary well with a tertiary stroke so controls read as
distinct from the section they sit on.
2026-07-07 11:45:12 -04:00
Erosika
220e7ca274 refactor(honcho): list per-session first among session strategies 2026-07-07 11:45:12 -04:00
Erosika
35099685be feat(memory): provider actions extension point
Providers with behavior beyond fields (validate a server, start a local
instance, link a CLI profile) previously had no mount besides forking the
panel. Let the schema declare actions and one generic endpoint run them:
ProviderAction on ProviderConfigSchema, POST
/api/memory/providers/{name}/actions/{action} dispatching to
ACTION_HANDLERS in the plugin's config_actions.py (path-loaded and
import-light, like the schema), profile-scoped and off the event loop.
Handlers get the submitted values dict, return a JSON-able result, and
raise ValueError for user-facing 400s. The panel renders declared actions
as generic buttons beside Save. No bundled provider declares actions yet.
2026-07-07 11:00:25 -04:00
Erosika
76c063b3d9 fix(memory): profile-scope the provider config endpoints
The settings page follows the desktop's active-profile switcher, but the
provider config calls didn't: no profileScoped() on the client and no
profile param on the backend, so a multi-profile desktop edited the serving
process's config while every surrounding card showed the selected profile's.

Accept ?profile= on both endpoints and resolve inside _profile_scope (the
skills/toolsets contract), spread profileScoped() into the two client calls,
and key the schema cache on the resolved config_schema.py path instead of
the provider name — user-installed plugins are per-profile, so one profile's
lookup must never answer for another's.
2026-07-07 10:53:43 -04:00
Erosika
703305751d style(memory): flatten comment blocks to single lines
Multi-line comment blocks and JSDoc-style headers across the provider
config surface compress to one line each; the why lives in commit messages
and docstrings, not comment essays.
2026-07-02 18:50:11 -04:00
Erosika
29016a50bc perf(memory): run provider config I/O off the event loop
GET walked honcho.json plus dozens of .env reads and PUT rewrote json/.env/
config.yaml — all synchronously inside async handlers, stalling every other
in-flight request behind one settings save on a slow disk. Offload both
bodies via asyncio.to_thread (the file's existing pattern) and skip the
config.yaml rewrite when memory.provider is already the saved provider.
2026-07-02 18:46:52 -04:00
Erosika
1f94e74a2d refactor(memory): collapse duplicated per-backend field helpers
The flat and honcho backends each carried their own copy of the read /
is_set / write-loop logic, differing only in which source dicts they
scanned. Fold them into _read_field/_field_is_set over a sources tuple and
a shared _apply_field_values, load .env once per request instead of once
per field, and use the project's is_truthy_value instead of a private
_TRUTHY set. Honcho non-secret is_set now reflects presence, so a stored
False/0 no longer reports as unset.
2026-07-02 18:45:27 -04:00
Erosika
41e59f6126 fix(memory): retry failed config schema loads instead of caching None
A syntax error in a provider's config_schema.py was cached as 'no schema'
until process restart, rendering a silent empty panel even after the file
was fixed. Cache only successful loads and genuine file-absence.
2026-07-02 18:42:31 -04:00
Erosika
0e09cc5cd4 fix(desktop): remount provider config panel when the provider changes
The panel instance survived a provider switch, so an in-flight fetch for the
old provider could resolve last and seed the new provider's panel with the
wrong schema — Save would then PUT those keys to the new provider's endpoint.
Key the mount on the provider name.
2026-07-02 18:42:31 -04:00
Erosika
4d4db4281e fix(desktop): submit only edited fields from the full-config modal
Unstored fields render their schema default, and 'Save all' persisted every
one of them — pinning values that runtime defaults still own. Concretely:
an existing Honcho user without an explicit observationMode runs 'unified'
via the client's migration guard, but one untouched save-all flipped them to
the schema's 'directional'. Diff against the seeded snapshot and submit only
what the user changed.
2026-07-02 18:42:31 -04:00
Erosika
a9cce6d844 fix(memory): write honcho config through the plugin's own resolution, locked
The panel's PUT hand-rolled its target: hardcoded profile-local path while
reads used resolve_config_path(), always the underscore host key while reads
honor legacy dot-form blocks, apiKey to the env store which the client ranks
below a JSON-stored key, and an unlocked read-modify-write of the file the
OAuth refresh loop guards with an advisory lock because refresh tokens are
single-use. Any of the first three silently shadowed or bypassed live
settings on save; the fourth could revoke the OAuth grant.

Route the write through resolve_config_path() and _host_block (updating the
resolved block in place), persist a saved apiKey into the host block where
the client reads it — never over an OAuth access token, which the refresh
loop owns — and take _config_refresh_lock around the read-modify-write.
Tolerate hosts:null instead of 500ing.
2026-07-02 18:39:23 -04:00
Erosika
822c8226d7 refactor(desktop): group memory provider config UI under settings/memory
The flat settings dir keeps absorbing per-feature components; move the
provider config surface (panel, modal, field control, colocated tests)
into the existing settings/memory folder beside the connect flow.
Consolidate the memory.provider option assertions in helpers.test.ts
into one exact-order check.
2026-07-02 18:17:24 -04:00
Erosika
2a632807e0 refactor(memory): move provider config schemas into their plugins
Each provider now declares its config surface in config_schema.py inside
its own plugin dir (plugins/memory/<name>/), loaded by file path like the
plugins themselves so plugin __init__ imports never reach the web server.
hermes_cli/memory_providers.py is gone; the shared field primitives and
loader live in plugins/memory/config_schema.py, and the schema tests move
to tests/plugins/memory/ alongside the other per-plugin suites.
2026-07-02 16:55:39 -04:00
Erosika
15c8c34c91 test(memory): cover Honcho config schema, host-block backend, and panel
Add registry/schema tests for the Honcho provider and keep the Hindsight
ones (now asserting inline). Add web_server tests for the honcho_host_block
backend (host/root scoping, native bool/number/json coercion, partial
saves, secret redaction). Port the inline-panel + full-config-modal desktop
tests and assert honcho is offered ahead of hindsight.
2026-07-02 16:48:24 -04:00
Erosika
4ef0672cf9 feat(desktop): inline memory config panel + full-config modal
Upgrade ProviderConfigPanel to a compact inline view (inline fields only)
with a Full config… modal that groups every field by section. Add the
generic kind-dispatched FieldControl (bool/number/json/select/secret/text)
and ProviderConfigModal. Extend MemoryProviderField with inline/group and
MemoryProviderConfig with docs_url. Order the provider enum honcho-first.
2026-07-02 16:47:11 -04:00
Erosika
101b9f8dcd feat(memory): dispatch /config reads+writes on provider storage backend
Generalize the memory-provider config endpoints to dispatch on
provider.storage: flat-json for simple providers, honcho_host_block for
Honcho's real profile-scoped honcho.json. Adds kind-aware coercion
(bool/number/json) and partial-save semantics so the inline panel never
clobbers full-config-only fields.
2026-07-02 16:45:51 -04:00
Erosika
94a0cb283e feat(memory): declare Honcho config schema, honcho-first registry
Add the grouped Honcho provider schema (connection/identity/session/
dialectic/recall/...) with inline + full-config field split and the
honcho_host_block storage backend. Keep Hindsight, mark its fields inline
so the compact panel is unchanged. Registry lists Honcho before Hindsight.
2026-07-02 16:45:51 -04:00
Brooklyn Nicholson
ab942330fc chore(release): map yingliang-zhang in AUTHOR_MAP for #57335 2026-07-02 15:44:37 -05:00
Yingliang Zhang
67472fbaa4 fix(tui_gateway): route setup.runtime_check and setup.status to RPC pool
setup.runtime_check and setup.status are polled by the Desktop frontend on
connect and periodically (use-status-snapshot → evaluateRuntimeReadiness), but
neither was in _LONG_HANDLERS — so dispatch() ran both inline on the WS reader
thread. Under GIL pressure from concurrent agent turns (terminal I/O, large
output, background-process completions) either can block for seconds:

- setup.runtime_check → resolve_runtime_provider() (config read, auth check,
  may probe the provider endpoint)
- setup.status → _has_any_provider_configured() (provider config + credential
  scan)

While either blocks the reader thread the WS read loop can't service later
requests; the frontend RPC timeout fires, the client drops the socket, and the
lost setup.runtime_check response reads as ready=false — a false "needs setup"
/ "Settings failed to load" even though the provider is configured.

Route both to the RPC pool (same precedent as #55545's session.list/pet.info/
process.list). The handlers are read-only and pool writes go through the
lock-guarded write_json, so there's no ordering or safety concern.

Test asserts all 5 frontend-polled RPCs are pool-routed.

Co-authored-by: izumi0uu <izumi0uu@gmail.com>
2026-07-02 15:44:37 -05:00
Brooklyn Nicholson
1501a338c3 fix(cli): stop profile-bound backends before deleting so rmtree converges
delete_profile stopped only the process named in gateway.pid, but a Desktop
app spawns a headless `serve`/`dashboard` backend per profile that holds the
profile's SQLite connection open and keeps writing sessions/WAL/sandbox files.
That backend is never in gateway.pid, so a CLI `hermes profile delete` run
while the Desktop app is up left it writing into the tree — rmtree's final
rmdir then failed with ENOTEMPTY (#47368 "Bug 2"), and pre-guard it also
resurrected the directory.

- _profile_bound_backend_pids(): find running Hermes backends bound to this
  profile via a `--profile <name>` selector or a HERMES_HOME env resolving to
  the profile dir. Tightly scoped — current-user only, backend subcommands
  (serve/dashboard/gateway) only so an interactive chat is never killed, and
  never this process or its ancestors.
- _stop_profile_backends(): terminate them (graceful, then force), best-effort
  so it can never make delete worse.
- _rmtree_with_retry(): a few spaced retries absorb the ENOTEMPTY / Windows
  file-lock race from a just-terminated writer's in-flight -wal/-shm/sandbox
  writes instead of failing the whole delete on a race the next attempt wins.

Complements the recreation guard (deleted profiles no longer reappear) and the
Desktop teardown-before-delete flow; this is the CLI-side convergence fix for a
delete run while a Desktop-managed backend is live.

Part of #47368.
2026-07-02 15:31:35 -05:00
Brooklyn Nicholson
5a6720b884 fix(desktop,tui-gateway,zai): stop thinking-off from reverting to medium
A Z.ai desktop user reported thinking reverting to medium after one turn,
burning ~200% of a week's credits in 4 days despite reasoning_effort: false
in config.yaml. Four compounding bugs:

- _session_info reported reasoning_effort "" for disabled reasoning,
  indistinguishable from unset — the desktop adopted it after the first
  turn, wiping its sticky "thinking off" pick so every later chat
  reverted to the default effort.
- config.set key=reasoning always wrote agent.reasoning_effort to global
  config.yaml, so every desktop model-menu selection (preset.effort ??
  'medium') clobbered the user's configured value. Now session-scoped
  like the messaging gateway's /reasoning, landing on
  create_reasoning_override so lazily-built sessions keep it too.
- YAML `reasoning_effort: false`/`off`/`no` (boolean False) was coerced
  to "" by every loader's `str(x or "")`, silently re-enabling thinking.
  parse_reasoning_effort now treats False/"false"/"disabled" as
  {"enabled": False}; loaders (tui gateway, gateway, cli, cron,
  delegate) pass the raw value through. The desktop config reader also
  crashed on the boolean (false.trim()), aborting voice/STT settings.
- The zai provider profile never sent thinking on the wire, and GLM-4.5+
  defaults to thinking ON server-side — so disabling reasoning was a
  silent no-op on direct Z.ai, the actual token burner. The profile now
  emits extra_body.thinking {"type": "enabled"|"disabled"} for
  thinking-capable GLM models, mirroring the DeepSeek profile.

Also: /new (session reset) now carries reasoning_config across the
rebuild like model_override; config.get reasoning prefers the session's
live value and maps a config False to "none"; Settings shows "Off"
instead of a blank select for hand-written false.
2026-07-02 15:23:47 -05:00
Tranquil-Flow
c3f06a8fda fix(desktop): refresh profile rail after deletion (#49289) 2026-07-02 15:18:49 -05:00
liuhao1024
c5e8a60b0a fix(desktop): skip ensureBackend after profile-delete teardown to prevent respawn loop
When the renderer sends a DELETE /api/profiles/{name} request, the IPC
handler tears down the profile's pool backend (or primary backend) via
prepareProfileDeleteRequest.  However, the very next line calls
ensureBackend(profile), which spawns a fresh pool backend for the just-
deleted profile.  The new backend's startup path calls ensure_hermes_home(),
which recreates the profile directory — defeating the deletion and leaving
the process as a zombie.

On the next Desktop restart the cycle repeats: the profile directory exists,
the Desktop spawns a backend, the backend recreates the directory after
deletion, and PIDs accumulate indefinitely.

Fix: make prepareProfileDeleteRequest return the torn-down profile name.
The IPC handler uses this to route the DELETE to the primary backend
instead of spawning a new pool backend for the deleted profile.

Fixes #52279
2026-07-02 15:18:49 -05:00
teknium1
254328bf56 fix(auth): remove stale loopback_pkce reference in xAI quarantine removal list
The terminal-refresh quarantine filtered in-memory entries on
source == "device_code" but built removed_ids from the deleted
"loopback_pkce" source name, so the revoked device-code entry was
never pruned from the persisted pool in auth.json. Also restores the
_print_loopback_ssh_hint test suite scoped to Spotify (the helper's
remaining caller) instead of deleting it wholesale.
2026-07-02 13:17:41 -07:00
Jaaneek
5ef0b8acb0 feat(auth): make xAI Grok OAuth device-code-only, drop loopback login
Replace the loopback/PKCE-callback server and manual-paste fallback with
the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The
flow works in headless/SSH/container sessions with no 127.0.0.1 listener,
shrinking the local attack surface.

- Poll the token endpoint with server-provided interval, honoring
  slow_down and expires_in; store tokens with auth_mode
  oauth_device_code.
- Adaptive proactive refresh skew for short-lived device-code JWTs;
  rotated tokens sync back to auth.json, the global root store, and the
  credential pool (no refresh-token replay).
- Clear source suppression on successful re-login (CLI + dashboard) and
  drop the duplicate dashboard pool entry so exactly one seeded
  device_code entry exists.
- Use the shared device_code source name for consistency with the
  nous/codex device-code providers.
- Desktop: remove the loopback OAuth flow states and dead type variants;
  pkce providers' sign-in URL selection is unchanged.
- Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted
  --manual-paste flag from documented commands.
2026-07-02 13:17:41 -07:00
LeonSGP43
472d75193f Prevent deleted profile skeleton revival 2026-07-02 15:11:56 -05:00
brooklyn!
6cffc37b5a
feat(desktop): collapse profile rail to a select past 13 profiles (#57306)
The colored-square rail stops scaling once a user racks up many profiles:
tiny drag targets and an endless horizontal scroll strip. Past a threshold
(13) the rail swaps the squares for a compact select dropdown — same active
tint + initial glyph, minus the drag-reorder / long-press-recolor / per-row
context menu that only make sense at small counts. Two render paths behind
one flag; the left default↔all toggle, the "+" create button, and Manage
stay put in both. Rename/delete/color remain reachable via Manage.
2026-07-02 19:15:07 +00:00
teknium1
a2d49de801 fix(terminal): also set MSYS2_ARG_CONV_EXCL for MSYS2/Cygwin bash fallback
MSYS_NO_PATHCONV is honored by Git for Windows bash only. _find_bash's
final shutil.which fallback can return MSYS2-proper or Cygwin bash,
which ignore it and honor MSYS2_ARG_CONV_EXCL instead. Set both so argv
path conversion stays disabled regardless of which bash flavor spawns.
Also subsumes the cmd /c mangling in #56147.
2026-07-02 11:48:03 -07:00
xxxigm
51c01062d4 test(terminal): cover MSYS_NO_PATHCONV defaults on Windows env builders 2026-07-02 11:48:03 -07:00
xxxigm
cc2abd570b fix(terminal): set MSYS_NO_PATHCONV for Windows Git Bash subprocesses
Git Bash mangles native Windows command flags (/FO, /TN, /Create) into
bogus paths. Hermes terminal and background spawns now opt out by default
so tasklist, schtasks, and wmic work without manual prefixes.

Fixes #56700.
2026-07-02 11:48:03 -07:00
helix4u
a9b5598909 fix(desktop): load remote model options before session
Both Desktop picker surfaces (status-bar model menu, settings/onboarding
dialog) only asked the connected gateway's model.options once a session
existed; before that they fell back to the Desktop REST/global options, which
can't see virtual providers a remote gateway exposes — including the MoA
presets from #53817. Centralize the fetch rule in requestModelOptions(): prefer
the connected gateway whenever one exists (no session_id needed — the RPC
resolves disk config), REST only when no gateway is connected.

The status-bar MoA preset section now renders from the same model.options
payload (the virtual `moa` provider row) instead of the local /api/model/moa
REST config, so remote presets appear correctly; the row is filtered out of
the main provider groups so presets don't list twice. Preset selection keeps
the persistent switchTo path from #56417 and drops the vestigial session gate —
like regular model rows, a pre-session pick ships on the next session.create.

Fixes #53817.

Rebased and reconciled with #56417 (persistent MoA selection), which landed
after this PR was opened and covered its one-shot-/moa half.
2026-07-02 12:50:46 -05:00
Brooklyn Nicholson
fe82b3a774 fix(desktop): read attachment previews local-first in remote mode
attachImagePath fetched its thumbnail through readDesktopFileDataUrl, which in
remote mode routes every read to the gateway fs bridge. Paperclip picks,
clipboard saves, and OS drops always produce paths on the LOCAL machine, so the
gateway read 404s — toasting "image preview failed" and dropping the thumbnail
even though the attach itself works (upload reads local bytes via the Electron
bridge). Read the local bridge first and fall back to the remote facade, which
still serves in-app drags from the remote project tree. Local mode is
unchanged (the facade already reads locally there).

Follow-up to #56572, which restored the remote paperclip picker and made this
path reachable from the picker as well.
2026-07-02 12:36:03 -05:00
helix4u
c19bfb50ad fix(desktop): restore remote file picker attachments 2026-07-02 12:32:30 -05:00
brooklyn!
eb506e656a
Merge pull request #57267 from NousResearch/bb/desktop-journey-memory-graph
feat(desktop): /journey opens the memory graph overlay instead of printing text
2026-07-02 12:30:28 -05:00
Brooklyn Nicholson
8da0a56ba8 style(desktop): fix pre-existing import-order lint in use-prompt-actions 2026-07-02 12:28:30 -05:00
Brooklyn Nicholson
931e2356af feat(desktop): /journey opens the memory graph overlay instead of printing text 2026-07-02 12:27:52 -05:00
Brooklyn Nicholson
42ca438131 style(desktop): fix import ordering + padding lint in remote-artifact files 2026-07-02 12:22:47 -05:00
helix4u
03406ae255 fix(desktop): restore remote artifact rendering 2026-07-02 12:22:47 -05:00
David Metcalfe
9738870489 fix(desktop): call checkUpdates() in startUpdatePoller so version pill auto-populates
startUpdatePoller() only called checkBackendUpdates() — never checkUpdates().
The statusbar version pill reads $updateStatus (set by checkUpdates()), so the
commit-behind counter stayed null after restart. It only appeared when the user
manually clicked the pill, which triggered checkUpdates() via openUpdateOverlayFor.

Added void checkUpdates() in three places alongside the existing
checkBackendUpdates() calls:
- On startup in startUpdatePoller()
- In the 30-minute setInterval callback
- In the onFocus handler

checkUpdates() uses the Electron IPC bridge (local git check), not the gateway,
so no mode gating is needed. The existing $updateChecking atom guard prevents
double-fire on overlap.

Fixes #53079
2026-07-02 11:52:57 -05:00
brooklyn!
63354edfd7
Merge pull request #57226 from NousResearch/bb/desktop-multiline-slash
fix(desktop): parse multiline slash commands so long-context skill/goal payloads stop vanishing (fixes #41323, supersedes #55541)
2026-07-02 11:42:00 -05:00
Brooklyn Nicholson
fb44b519d7 fix(desktop): parse multiline slash commands + hand degenerate payloads back
parseSlashCommand used /^(\S+)\s*(.*)$/ where `.` can't cross a newline and
`$` anchors end-of-string, so any slash command whose arg contained a newline
(/goal <multi-line text>, a skill command with a long pasted context) failed
the whole match, parsed as an empty name, and rendered "empty slash command"
while the payload vanished — cleared from the composer and absent from the
Up-arrow history ring, which only derives from sent user messages.

- name now splits on any whitespace ([\s\S]* arg), matching the CLI and the
  gateway's split(maxsplit=1); multiline args flow to slash.exec intact
- the residual empty-name branch (bare "/", "/ text") restores the submitted
  text to the composer draft instead of eating it

Fixes #41323. Fixes #55510.
2026-07-02 11:36:21 -05:00
David Zhang
30e947e0a0 feat(gateway): persist per-session /model overrides across gateway restarts
Per-session /model overrides (_session_model_overrides) were in-memory only,
so a gateway restart silently reverted every session to the global default
model. Persist the non-secret parts (model/provider/base_url ONLY — never
api_key) into the session entry in sessions.json and lazily rehydrate them
on first use after a restart, re-resolving credentials through the normal
runtime provider resolution.

- gateway/session.py: SessionEntry.model_override field with
  sanitize_model_override() (allowlist: model/provider/base_url) applied on
  both serialization and deserialization; SessionStore.set_model_override /
  get_model_override accessors. reset_session() already creates a fresh entry,
  so /new keeps its clear-on-reset semantics — a restart cannot resurrect an
  override the user reset away.
- gateway/slash_commands.py: write-through at both /model set sites (text
  command + picker) after storing the in-memory override.
- gateway/run.py: _rehydrate_session_model_override() called from
  _resolve_session_agent_runtime(); in-memory state always wins, credentials
  are re-resolved per provider (credential-less fallback on failure). Session
  expiry finalization also drops the persisted override.
- tests/gateway/test_session_model_override_persistence.py: restart
  round-trip, /new clearing, api_key-never-serialized (including tampered
  sessions.json), rehydration + live-state precedence + credential-failure
  degradation.

Salvaged from #3659 by @Git-on-my-level, narrowed to the restart-persistence
gap confirmed in triage.
2026-07-02 05:51:12 -07:00
Jneeee
b98baa3039 feat(config): extra HTTP headers for LLM API calls (#3526 salvage)
Named providers / custom_providers entries in config.yaml now accept an
extra_headers dict scoped to that endpoint — for reverse proxies, API
gateways, and custom auth schemes (e.g. Cloudflare Access service tokens).

- hermes_cli/config.py: normalize extra_headers on provider entries
  (_normalize_custom_provider_entry + providers-dict translation), add
  get_custom_provider_extra_headers /
  apply_custom_provider_extra_headers_to_client_kwargs helpers keyed on
  base_url (case/trailing-slash insensitive, no substring bypass —
  mirrors the TLS helpers)
- hermes_cli/runtime_provider.py: surface extra_headers in the resolved
  runtime for named custom providers (providers dict, legacy
  custom_providers list, and the credential-pool path)
- run_agent.py / agent/agent_init.py: merge per-provider extra_headers
  onto the OpenAI client default_headers at construction and on every
  _apply_client_headers_for_base_url re-application (credential swaps,
  rebuilds), most-specific level wins; OpenAI-wire only (native
  Anthropic/Bedrock scoped out)
- agent/auxiliary_client.py: accept model.extra_headers as an alias of
  model.default_headers for the global variant
- cli-config.yaml.example: documented commented example
- Header values are treated as secrets and never logged

Salvaged from PR #3526 by @jneeee, reimplemented against current main.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-07-02 05:33:25 -07:00
Mibayy
4a09b692ec feat(api-server): per-client model routing via model_routes (#3176 salvage)
Adds a no-code routing layer to the OpenAI-compatible API server so one
Hermes deployment can map different API clients to different
model/provider backends. Clients pick a backend by sending a configured
alias as the OpenAI 'model' field; unmatched values fall back to the
global model. Configured aliases are listed by GET /v1/models.

Precedence (highest first): session /model override > model_routes
route > global config. Route provider credentials resolve through
_resolve_runtime_agent_kwargs_for_provider (same seam as
channel_overrides); per-route api_key/base_url are upstream provider
credential overrides — never caller auth, never logged.

Salvaged and rebased from PR #3176 by @Mibayy onto current main.
2026-07-02 05:23:28 -07:00
Mibayy
ce9aa869fc feat(commands): /compact alias + --preview/--dry-run flags for /compress (#3243 salvage)
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / TypeScript (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Salvaged from PR #3243 by @Mibayy, reimplemented against current main
(the original diff targeted a removed gateway/run.py handler).

- /compact is now a first-class alias of /compress (CLI, gateway,
  Telegram/Slack/Discord command lists, autocomplete) — also fixes the
  dangling '/compact' references in gateway error messages
  (gateway/run.py context-exhausted banners).
- --preview / --dry-run: report what WOULD be compressed (message
  counts, token estimate, 'here [N]' boundary) without touching the
  transcript. Flags coexist with the existing 'here [N]' / focus-topic
  args on both the CLI and gateway surfaces via shared pure helpers in
  hermes_cli/partial_compress.py.
- --aggressive (LLM-free hard truncation) is intentionally NOT
  implemented: it would need its own transcript-persistence branch
  outside the guarded _compress_context rotation machinery (#44794
  data-loss class). The flag is recognized and returns an explanatory
  message pointing at '/compress here [N]' and /undo instead of being
  mis-parsed as a focus topic.
- locales: gateway.compress.aggressive_unsupported added to all 16
  catalogs (parity test enforced).
- release.py: AUTHOR_MAP entry for contributor credit.
2026-07-02 05:10:31 -07:00
Teknium
fb74ddf7fe fix(i18n): add gateway.verbose.mode_log to all locale catalogs 2026-07-02 05:09:38 -07:00