fix(web): deep-merge config on PUT /api/config to preserve unrelated sections#13914
fix(web): deep-merge config on PUT /api/config to preserve unrelated sections#13914HiddenPuppy wants to merge 7 commits into
Conversation
…call messages for Kimi /coding Fixes NousResearch#13848 Kimi's /coding endpoint speaks the Anthropic Messages protocol but has its own thinking semantics: when thinking is enabled, Kimi validates message history and requires every prior assistant tool-call message to carry OpenAI-style reasoning_content. The Anthropic path never populated that field, and convert_messages_to_anthropic strips all Anthropic thinking blocks on third-party endpoints — so the request failed with HTTP 400: "thinking is enabled but reasoning_content is missing in assistant tool call message at index N" Now, when an assistant message contains tool_calls and a reasoning_content string, we append a {"type": "thinking", ...} block to the Anthropic content so Kimi can validate the history. This only affects assistant messages with tool_calls + reasoning_content; plain text assistant messages are unchanged.
Map tsuijinglei@gmail.com → hiddenpuppy.
…t exhaustion for fallback Fixes NousResearch#13887 OpenRouter returns HTTP 403 (not 402) for key-limit and spend-limit errors. The existing _is_payment_error() only checked 402/429/None, so these 403s were not classified as payment exhaustion and auxiliary calls would fail immediately instead of falling back to the next provider. Now _is_payment_error() also checks status == 403 and looks for OpenRouter-specific keywords: key limit, spending limit, spend limit, total limit, credit limit, quota exceeded. This keeps the fix scoped: only 403s with these specific phrases are treated as payment errors. Other 403s (auth, forbidden, etc.) still raise normally. The fallback path already skips the failed provider via _try_payment_fallback(), so no changes are needed there.
Map jerome@clawwork.ai → HiddenPuppy.
…H/tunnels Fixes NousResearch#13870 prompt_toolkit sends \x1b[6n (Device Status Report) to query cursor position; the terminal replies with \x1b[row;colR. Over SSH/cloudflared tunnels these CPR responses leak as raw text (e.g. 20
…sections Fixes NousResearch#13396 PUT /api/config was calling save_config() with only the payload from the client, which replaced the entire ~/.hermes/config.yaml file. If a dashboard UI sent only the sections it manages (agent, display), everything else (model, custom_providers, compression, auxiliary, fallback_model, etc.) was silently deleted. Now update_config() loads the existing config, deep-merges the incoming payload into it, and saves the merged result. Keys present in the payload overwrite existing values; keys absent from the payload are preserved. This matches the behavior third-party clients already assume and prevents accidental data loss.
austinpickett
left a comment
There was a problem hiding this comment.
Please use PULL_REQUEST_TEMPLATE.md, fix merge conflicts.
There was a problem hiding this comment.
Pull request overview
The PR's stated purpose is to fix issue #13396 by deep-merging the incoming payload of PUT /api/config into the on-disk config so unrelated sections aren't silently destroyed. However, the diff also bundles four unrelated changes (mailmap additions, a prompt_toolkit CPR workaround, OpenRouter 403 payment-error detection, and Kimi reasoning_content preservation in the Anthropic adapter) that aren't mentioned in the description.
Changes:
update_config()now loads the existing config, deep-merges the incoming payload, and saves the merged result.- Several unrelated fixes are bundled in (release mailmap, TUI CPR disable, payment-error and Anthropic-thinking-block tweaks).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
hermes_cli/web_server.py |
Imports _deep_merge and merges incoming PUT payload onto load_config() before saving. |
scripts/release.py |
Adds two .mailmap-style author entries. |
cli.py |
Constructs a Vt100_Output with enable_cpr=False to suppress CPR escape leaks over SSH/cloudflared. |
agent/auxiliary_client.py |
Treats certain OpenRouter HTTP 403 messages (key/spend/credit limit) as payment-exhaustion errors. |
agent/anthropic_adapter.py |
Emits a thinking block carrying assistant reasoning_content for Kimi /coding tool-call messages. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| existing = load_config() | ||
| incoming = _denormalize_config_from_web(body.config) | ||
| merged = _deep_merge(existing, incoming) | ||
| save_config(merged) |
| remove_env_value, | ||
| check_config_version, | ||
| redact_key, | ||
| _deep_merge, |
| existing = load_config() | ||
| incoming = _denormalize_config_from_web(body.config) | ||
| merged = _deep_merge(existing, incoming) | ||
| save_config(merged) |
…/api/config Addresses Copilot review comments: 1. Use read_raw_config() instead of load_config() as the merge base — load_config() injects DEFAULT_CONFIG values and expands env refs, which would bloat the saved file with defaults and leak expanded secrets. read_raw_config() returns the exact user-authored YAML without defaults or env expansion, which is the correct base for a merge-and-write. 2. Add public merge_and_save_config() in config.py so web_server.py doesn't import the private _deep_merge helper across modules. 3. update_config() now calls merge_and_save_config(incoming) for a single clear read-merge-save operation.
Summary
Fixes #13396
Problem
PUT
/api/configreplaces the entire~/.hermes/config.yamlfile with the provided payload. If a dashboard UI sends only the sections it manages (e.g.agent,display), everything else (model,custom_providers,compression,auxiliary,fallback_model, etc.) is silently deleted.Impact
Fix
In
update_config(), load the existing config first, deep-merge the incoming payload into it, then save the merged result. Keys present in the payload overwrite existing values; keys absent from the payload are preserved.The
_deep_mergefunction already exists inhermes_cli/config.pyand is used elsewhere for the same purpose.Changes
hermes_cli/web_server.py:update_config()— addedload_config()+_deep_merge()beforesave_config()Verification
curl -X PUT /api/configwith partial payload → inspect config.yaml → unrelated sections preservedChecklist