Plugin config save silently drops false/empty values and corrupts reordered lists (SaveConfigPreserveComments merge)
Summary
PUT /v0/management/plugins/:id/config returns 200 {"status":"ok"} and the config file is rewritten, but the saved values silently do not take effect. The root cause is the comment-preserving merge in SaveConfigPreserveComments (internal/config/config_yaml.go), which is unsuitable for plugin config subtrees.
Root cause
When persisting, the handler loads the existing config.yaml into a yaml.Node tree and merges the freshly marshalled config into it in place (mergeMappingPreserve / mergeNodePreserve). That merge has three behaviors that corrupt plugin configs:
- Append-only for new keys (
mergeMappingPreserve, aroundconfig_yaml.go:196-213): a key that does not exist in the old tree is only appended when its value is non-zero. Soenabled: false,timeout: 0,"",[]can never be written for the first time — the new value is silently dropped. - Sequence merge aligns object elements by identity (
reorderSequenceForMerge/matchSequenceElement): for a list of objects like a plugin'svision_models, reordering or removing an entry merges old nodes positionally by a matched identity field, leaving stale fields from the old element attached to the wrong model. - Nested mappings are append-only: removing a nested key (e.g. a bridge under
bridges) is not deleted by the merge.
Plugin configs are opaque blobs owned by the plugin (PluginInstanceConfig.Raw already preserves the whole subtree), so the lossy merge has no upside here — the saved file should mirror the in-memory config exactly.
Reproduction
With any plugin that has boolean / numeric / list settings (e.g. a vision bridge with vision_models):
PUT /v0/management/plugins/<id>/configchangingprimary_model, setting someenabled: false, and reorderingvision_models.- Observe
200and the file being written. - After hot reload,
GETthe config back: the false booleans and the new order are missing; old values remain.
Suggested fix
Replace the whole plugins.configs subtree with the generated tree before the generic merge, and skip that path inside mergeMappingPreserve. I have a working patch with tests (internal/config/plugin_config_save_test.go) covering false booleans, reordered lists, removed plugins and newly created subtrees; happy to open a PR.
Environment
- CLIProxyAPI v7.2.159 (also present in earlier versions)
- linux/amd64, Docker
Source: router-for-me/CLIProxyAPI