Plugin config save silently drops false/empty values and corrupts reordered lists (SaveConfigPreserveComments merge)

Author: cls3389Created Sep 17, 2026Updated Sep 17, 2026
LabelsFixed

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:

  1. Append-only for new keys (mergeMappingPreserve, around config_yaml.go:196-213): a key that does not exist in the old tree is only appended when its value is non-zero. So enabled: false, timeout: 0, "", [] can never be written for the first time — the new value is silently dropped.
  2. Sequence merge aligns object elements by identity (reorderSequenceForMerge / matchSequenceElement): for a list of objects like a plugin's vision_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.
  3. 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):

  1. PUT /v0/management/plugins/<id>/config changing primary_model, setting some enabled: false, and reordering vision_models.
  2. Observe 200 and the file being written.
  3. After hot reload, GET the 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